Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multisend #286

Merged
merged 13 commits into from
Oct 24, 2024
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ARG TARGETARCH
ARG GOARCH

# See https://github.com/initia-labs/movevm/releases
ENV LIBMOVEVM_VERSION=v0.5.0
ENV LIBMOVEVM_VERSION=v0.5.1

# Install necessary packages
RUN set -eux; apk add --no-cache ca-certificates build-base git cmake
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ require (
github.com/hashicorp/go-metrics v0.5.3
github.com/initia-labs/OPinit v0.5.5
// we also need to update `LIBMOVEVM_VERSION` of Dockerfile#9
github.com/initia-labs/movevm v0.5.0
github.com/initia-labs/movevm v0.5.1
github.com/noble-assets/forwarding/v2 v2.0.0-20240521090705-86712c4c9e43
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -732,8 +732,8 @@ github.com/initia-labs/OPinit/api v0.5.1 h1:zwyJf7HtKJCKvLJ1R9PjVfJO1L+d/jKoeFyT
github.com/initia-labs/OPinit/api v0.5.1/go.mod h1:gHK6DEWb3/DqQD5LjKirUx9jilAh2UioXanoQdgqVfU=
github.com/initia-labs/cometbft v0.0.0-20240923045653-ba99eb347236 h1:+HmPQ1uptOe4r5oQHuHMG5zF1F3maNoEba5uiTUMnlk=
github.com/initia-labs/cometbft v0.0.0-20240923045653-ba99eb347236/go.mod h1:GPHp3/pehPqgX1930HmK1BpBLZPxB75v/dZg8Viwy+o=
github.com/initia-labs/movevm v0.5.0 h1:dBSxoVyUumSE4x6/ZSOWtvbtZpw+V4W25/NH6qLU0uQ=
github.com/initia-labs/movevm v0.5.0/go.mod h1:aUWdvFZPdULjJ2McQTE+mLnfnG3CLAz0TWJRFzFFUwg=
github.com/initia-labs/movevm v0.5.1 h1:Nl5SizJIfRLM6clz/zV8dOFUXcnlMP2wOQsZB/mmU2w=
github.com/initia-labs/movevm v0.5.1/go.mod h1:aUWdvFZPdULjJ2McQTE+mLnfnG3CLAz0TWJRFzFFUwg=
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls=
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
Expand Down
49 changes: 46 additions & 3 deletions x/bank/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
"context"

"github.com/hashicorp/go-metrics"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/telemetry"
Expand Down Expand Up @@ -85,7 +83,52 @@
}

func (k msgServer) MultiSend(goCtx context.Context, msg *types.MsgMultiSend) (*types.MsgMultiSendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "not supported")
if len(msg.Inputs) == 0 {
return nil, types.ErrNoInputs
}

if len(msg.Inputs) != 1 {
return nil, types.ErrMultipleSenders
}

if len(msg.Outputs) == 0 {
return nil, types.ErrNoOutputs
}

in := msg.Inputs[0]
if err := types.ValidateInputOutputs(in, msg.Outputs); err != nil {
return nil, err
}

ctx := sdk.UnwrapSDKContext(goCtx)

// NOTE: totalIn == totalOut should already have been checked
if err := k.IsSendEnabledCoins(ctx, in.Coins...); err != nil {
return nil, err
}

Check warning on line 108 in x/bank/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/msg_server.go#L107-L108

Added lines #L107 - L108 were not covered by tests

if base, ok := k.Keeper.(BaseKeeper); ok {
for _, out := range msg.Outputs {
accAddr, err := base.ak.AddressCodec().StringToBytes(out.Address)
if err != nil {
return nil, err
}

Check warning on line 115 in x/bank/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/msg_server.go#L114-L115

Added lines #L114 - L115 were not covered by tests

if k.BlockedAddr(accAddr) {
return nil, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", out.Address)
}

}
} else {
return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}

Check warning on line 124 in x/bank/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/msg_server.go#L122-L124

Added lines #L122 - L124 were not covered by tests

err := k.InputOutputCoins(ctx, msg.Inputs[0], msg.Outputs)
if err != nil {
return nil, err
}

Check warning on line 129 in x/bank/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/msg_server.go#L128-L129

Added lines #L128 - L129 were not covered by tests

return &types.MsgMultiSendResponse{}, nil
}

func (k msgServer) UpdateParams(goCtx context.Context, req *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
Expand Down
114 changes: 114 additions & 0 deletions x/bank/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,120 @@ func TestMsgSend(t *testing.T) {
}
}

func TestMsgMultiSend(t *testing.T) {
ctx, input := createDefaultTestInput(t)

origDenom := "sendableCoin"
origCoins := sdk.NewCoins(sdk.NewInt64Coin(origDenom, 100))
sendCoins := sdk.NewCoins(sdk.NewInt64Coin(origDenom, 50))
input.BankKeeper.SetSendEnabled(ctx, origDenom, true)

testCases := []struct {
name string
input *banktypes.MsgMultiSend
expErr bool
expErrMsg string
}{
{
name: "no inputs to send transaction",
input: &banktypes.MsgMultiSend{},
expErr: true,
expErrMsg: "no inputs to send transaction",
},
{
name: "no inputs to send transaction",
input: &banktypes.MsgMultiSend{
Outputs: []banktypes.Output{
{Address: addrs[4].String(), Coins: sendCoins},
},
},
expErr: true,
expErrMsg: "no inputs to send transaction",
},
{
name: "more than one inputs to send transaction",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
{Address: addrs[0].String(), Coins: origCoins},
},
},
expErr: true,
expErrMsg: "multiple senders not allowed",
},
{
name: "no outputs to send transaction",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
},
expErr: true,
expErrMsg: "no outputs to send transaction",
},
{
name: "invalid send to blocked address",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
Outputs: []banktypes.Output{
{Address: addrs[1].String(), Coins: sendCoins},
{Address: authtypes.NewModuleAddress(govtypes.ModuleName).String(), Coins: sendCoins},
},
},
expErr: true,
expErrMsg: "is not allowed to receive funds",
},
{
name: "input/output amount mismatch",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
Outputs: []banktypes.Output{
{Address: addrs[1].String(), Coins: origCoins},
{Address: addrs[2].String(), Coins: sendCoins},
},
},
expErr: true,
expErrMsg: "sum inputs != sum outputs",
},
{
name: "valid send",
input: &banktypes.MsgMultiSend{
Inputs: []banktypes.Input{
{Address: addrs[0].String(), Coins: origCoins},
},
Outputs: []banktypes.Output{
{Address: addrs[1].String(), Coins: sendCoins},
{Address: addrs[2].String(), Coins: sendCoins},
},
},
expErr: false,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if len(tc.input.Inputs) > 0 && !tc.input.Inputs[0].Coins.IsZero() && tc.input.Inputs[0].Address != "" {
fromAddr, err := input.AccountKeeper.AddressCodec().StringToBytes(tc.input.Inputs[0].Address)
require.NoError(t, err)
input.Faucet.Fund(ctx, fromAddr, tc.input.Inputs[0].Coins...)
}

_, err := bankkeeper.NewMsgServerImpl(input.BankKeeper).MultiSend(ctx, tc.input)
if tc.expErr {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expErrMsg)
} else {
require.NoError(t, err)
}
})
}
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

func TestMsgSetSendEnabled(t *testing.T) {
ctx, input := createDefaultTestInput(t)

Expand Down
85 changes: 79 additions & 6 deletions x/bank/keeper/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"fmt"

"cosmossdk.io/core/store"
"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
cosmosbank "github.com/cosmos/cosmos-sdk/x/bank/keeper"
"github.com/cosmos/cosmos-sdk/x/bank/types"

Expand Down Expand Up @@ -104,11 +104,84 @@
return k.Params.Set(ctx, params)
}

// InputOutputCoins performs multi-send functionality. It accepts a series of
// inputs that correspond to a series of outputs. It returns an error if the
// inputs and outputs don't lineup or if any single transfer of tokens fails.
func (k MoveSendKeeper) InputOutputCoins(ctx context.Context, inputs types.Input, outputs []types.Output) error {
return sdkerrors.ErrNotSupported
// InputOutputCoins performs multi-send functionality. It transfers coins from a single sender
// to multiple recipients. An error is returned upon failure.
func (k MoveSendKeeper) InputOutputCoins(ctx context.Context, input types.Input, outputs []types.Output) error {
// Safety check ensuring that when sending coins the keeper must maintain the
// Check supply invariant and validity of Coins.
if err := types.ValidateInputOutputs(input, outputs); err != nil {
return err
}

Check warning on line 114 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L113-L114

Added lines #L113 - L114 were not covered by tests
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

fromAddr, err := k.ak.AddressCodec().StringToBytes(input.Address)
if err != nil {
return err
}

Check warning on line 119 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L118-L119

Added lines #L118 - L119 were not covered by tests

// event emission
sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(types.AttributeKeySender, input.Address),
),
)
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

// emit coin spent event
sdkCtx.EventManager().EmitEvent(
types.NewCoinSpentEvent(fromAddr, input.Coins),
)

// emit coin received events and do address caching
addrMap := make(map[string][]byte)
for _, output := range outputs {
addr, err := k.ak.AddressCodec().StringToBytes(output.Address)
if err != nil {
return err
}

Check warning on line 141 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L140-L141

Added lines #L140 - L141 were not covered by tests

// cache bytes address
addrMap[output.Address] = addr

// emit coin received event
sdkCtx.EventManager().EmitEvent(
types.NewCoinReceivedEvent(addr, output.Coins),
)

// emit transfer event (for compatibility with cosmos bank)
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeTransfer,
sdk.NewAttribute(types.AttributeKeyRecipient, output.Address),
sdk.NewAttribute(sdk.AttributeKeyAmount, output.Coins.String()),
),
)
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

for _, coin := range input.Coins {
if !coin.Amount.IsPositive() {
continue

Check warning on line 163 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L163

Added line #L163 was not covered by tests
}

recipients := make([]sdk.AccAddress, 0, len(outputs))
amounts := make([]math.Int, 0, len(outputs))
for _, output := range outputs {
amount := output.Coins.AmountOf(coin.Denom)
if !amount.IsPositive() {
continue

Check warning on line 171 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L171

Added line #L171 was not covered by tests
}

recipients = append(recipients, addrMap[output.Address])
amounts = append(amounts, output.Coins.AmountOf(coin.Denom))
}

err = k.mk.MultiSend(ctx, fromAddr, coin.Denom, recipients, amounts)
if err != nil {
return err
}

Check warning on line 181 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L180-L181

Added lines #L180 - L181 were not covered by tests
beer-1 marked this conversation as resolved.
Show resolved Hide resolved
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

return nil
}

// SendCoins transfers amt coins from a sending account to a receiving account.
Expand Down
1 change: 1 addition & 0 deletions x/bank/types/expected_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MoveBankKeeper interface {
SendCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
MintCoins(ctx context.Context, addr sdk.AccAddress, amount sdk.Coins) error
BurnCoins(ctx context.Context, addr sdk.AccAddress, amount sdk.Coins) error
MultiSend(ctx context.Context, fromAddr sdk.AccAddress, denom string, toAddrs []sdk.AccAddress, amounts []math.Int) error

// supply
GetSupply(ctx context.Context, denom string) (math.Int, error)
Expand Down
59 changes: 59 additions & 0 deletions x/move/keeper/bank.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"context"
"encoding/json"
"errors"
"fmt"

Expand Down Expand Up @@ -531,3 +532,61 @@
false,
)
}

func (k MoveBankKeeper) MultiSend(
ctx context.Context,
sender sdk.AccAddress,
denom string,
recipients []sdk.AccAddress,
amounts []math.Int,
) error {
beer-1 marked this conversation as resolved.
Show resolved Hide resolved
if len(recipients) != len(amounts) {
return moderrors.Wrapf(types.ErrInvalidRequest, "recipients and amounts length mismatch")

Check warning on line 544 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L544

Added line #L544 was not covered by tests
} else if len(recipients) == 0 {
return moderrors.Wrapf(types.ErrInvalidRequest, "recipients and amounts length should be greater than 0")
}

Check warning on line 547 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L546-L547

Added lines #L546 - L547 were not covered by tests

senderVMAddr, err := vmtypes.NewAccountAddressFromBytes(sender)
if err != nil {
return err
}

Check warning on line 552 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L551-L552

Added lines #L551 - L552 were not covered by tests

metadata, err := types.MetadataAddressFromDenom(denom)
if err != nil {
return err
}

Check warning on line 557 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L556-L557

Added lines #L556 - L557 were not covered by tests
metadataArg, err := json.Marshal(metadata.String())
if err != nil {
return err
}

Check warning on line 561 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L560-L561

Added lines #L560 - L561 were not covered by tests

recipientAddrs := make([]string, len(recipients))
for i, toAddr := range recipients {
toVmAddr, err := vmtypes.NewAccountAddressFromBytes(toAddr)
if err != nil {
return err
}

Check warning on line 568 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L567-L568

Added lines #L567 - L568 were not covered by tests

recipientAddrs[i] = toVmAddr.String()
}
recipientsArg, err := json.Marshal(recipientAddrs)
if err != nil {
return err
}

Check warning on line 575 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L574-L575

Added lines #L574 - L575 were not covered by tests

amountsArg, err := json.Marshal(amounts)
if err != nil {
return err
}

Check warning on line 580 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L579-L580

Added lines #L579 - L580 were not covered by tests
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

return k.executeEntryFunction(
ctx,
[]vmtypes.AccountAddress{vmtypes.StdAddress, senderVMAddr},
vmtypes.StdAddress,
types.MoveModuleNameCoin,
types.FunctionNameCoinSudoMultiSend,
[]vmtypes.TypeTag{},
[][]byte{metadataArg, recipientsArg, amountsArg},
true,
)
}
Loading
Loading