diff --git a/x/bank/keeper/msg_server.go b/x/bank/keeper/msg_server.go index 092bf905..6571c219 100644 --- a/x/bank/keeper/msg_server.go +++ b/x/bank/keeper/msg_server.go @@ -4,8 +4,6 @@ import ( "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" @@ -85,7 +83,51 @@ func (k msgServer) Send(goCtx context.Context, msg *types.MsgSend) (*types.MsgSe } 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 + } + + for _, out := range msg.Outputs { + if base, ok := k.Keeper.(BaseKeeper); ok { + accAddr, err := base.ak.AddressCodec().StringToBytes(out.Address) + if err != nil { + return nil, err + } + + 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) + } + } + + err := k.InputOutputCoins(ctx, msg.Inputs[0], msg.Outputs) + if err != nil { + return nil, err + } + + return &types.MsgMultiSendResponse{}, nil } func (k msgServer) UpdateParams(goCtx context.Context, req *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) { diff --git a/x/bank/keeper/msg_server_test.go b/x/bank/keeper/msg_server_test.go index 2e2a46e9..06981c4f 100644 --- a/x/bank/keeper/msg_server_test.go +++ b/x/bank/keeper/msg_server_test.go @@ -170,6 +170,106 @@ 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: "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) + } + }) + } +} + func TestMsgSetSendEnabled(t *testing.T) { ctx, input := createDefaultTestInput(t) diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index 351291cf..0039f555 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -5,11 +5,11 @@ import ( "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" @@ -104,11 +104,49 @@ func (k MoveSendKeeper) SetParams(ctx context.Context, params types.Params) erro 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 { + fromAddr, err := k.ak.AddressCodec().StringToBytes(input.Address) + if err != nil { + return err + } + + addrMap := make(map[string][]byte) + for _, coin := range input.Coins { + if !coin.Amount.IsPositive() { + continue + } + + 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 + } + + // cache bytes address + if _, ok := addrMap[output.Address]; !ok { + addr, err := k.ak.AddressCodec().StringToBytes(output.Address) + if err != nil { + return err + } + + addrMap[output.Address] = addr + } + + 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 + } + } + + return nil } // SendCoins transfers amt coins from a sending account to a receiving account. diff --git a/x/bank/types/expected_keeper.go b/x/bank/types/expected_keeper.go index 44d6a09f..0b143408 100644 --- a/x/bank/types/expected_keeper.go +++ b/x/bank/types/expected_keeper.go @@ -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) diff --git a/x/move/keeper/bank.go b/x/move/keeper/bank.go index 5c3d2ab3..b03bf5fa 100644 --- a/x/move/keeper/bank.go +++ b/x/move/keeper/bank.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "encoding/json" "errors" "fmt" @@ -531,3 +532,55 @@ func (k MoveBankKeeper) SendCoin( false, ) } + +func (k MoveBankKeeper) MultiSend( + ctx context.Context, + sender sdk.AccAddress, + denom string, + recipients []sdk.AccAddress, + amounts []math.Int, +) error { + senderVMAddr, err := vmtypes.NewAccountAddressFromBytes(sender) + if err != nil { + return err + } + + metadata, err := types.MetadataAddressFromDenom(denom) + if err != nil { + return err + } + metadataArg, err := json.Marshal(metadata.String()) + if err != nil { + return err + } + + recipientAddrs := make([]string, len(recipients)) + for i, toAddr := range recipients { + toVmAddr, err := vmtypes.NewAccountAddressFromBytes(toAddr) + if err != nil { + return err + } + + recipientAddrs[i] = toVmAddr.String() + } + recipientsArg, err := json.Marshal(recipientAddrs) + if err != nil { + return err + } + + amountsArg, err := json.Marshal(amounts) + if err != nil { + return err + } + + return k.executeEntryFunction( + ctx, + []vmtypes.AccountAddress{vmtypes.StdAddress, senderVMAddr}, + vmtypes.StdAddress, + types.MoveModuleNameCoin, + types.FunctionNameCoinSudoMultiSend, + []vmtypes.TypeTag{}, + [][]byte{metadataArg, recipientsArg, amountsArg}, + true, + ) +} diff --git a/x/move/keeper/bank_test.go b/x/move/keeper/bank_test.go index 492b773a..18feda65 100644 --- a/x/move/keeper/bank_test.go +++ b/x/move/keeper/bank_test.go @@ -225,3 +225,35 @@ func Test_BurnCoins(t *testing.T) { require.Equal(t, sdk.NewCoin("foo", sdkmath.NewInt(500_000)), input.BankKeeper.GetBalance(ctx, twoAddr, "foo")) require.Equal(t, sdk.NewCoin(barDenom, sdkmath.NewInt(500_000)), input.BankKeeper.GetBalance(ctx, twoAddr, barDenom)) } + +func Test_MultiSend(t *testing.T) { + ctx, input := createDefaultTestInput(t) + moveBankKeeper := keeper.NewMoveBankKeeper(&input.MoveKeeper) + + bz, err := hex.DecodeString("0000000000000000000000000000000000000002") + require.NoError(t, err) + twoAddr := sdk.AccAddress(bz) + + bz, err = hex.DecodeString("0000000000000000000000000000000000000003") + require.NoError(t, err) + threeAddr := sdk.AccAddress(bz) + + bz, err = hex.DecodeString("0000000000000000000000000000000000000004") + require.NoError(t, err) + fourAddr := sdk.AccAddress(bz) + + bz, err = hex.DecodeString("0000000000000000000000000000000000000005") + require.NoError(t, err) + fiveAddr := sdk.AccAddress(bz) + + amount := sdk.NewCoins(sdk.NewCoin(bondDenom, sdkmath.NewIntFromUint64(1_000_000))) + input.Faucet.Fund(ctx, twoAddr, amount...) + + err = moveBankKeeper.MultiSend(ctx, twoAddr, bondDenom, []sdk.AccAddress{threeAddr, fourAddr, fiveAddr}, []sdkmath.Int{sdkmath.NewIntFromUint64(300_000), sdkmath.NewIntFromUint64(400_000), sdkmath.NewIntFromUint64(300_000)}) + require.NoError(t, err) + + require.Equal(t, sdk.NewCoin(bondDenom, sdkmath.ZeroInt()), input.BankKeeper.GetBalance(ctx, twoAddr, bondDenom)) + require.Equal(t, uint64(300_000), input.BankKeeper.GetBalance(ctx, threeAddr, bondDenom).Amount.Uint64()) + require.Equal(t, uint64(400_000), input.BankKeeper.GetBalance(ctx, fourAddr, bondDenom).Amount.Uint64()) + require.Equal(t, uint64(300_000), input.BankKeeper.GetBalance(ctx, fiveAddr, bondDenom).Amount.Uint64()) +} diff --git a/x/move/types/connector.go b/x/move/types/connector.go index 337902ed..e330f490 100644 --- a/x/move/types/connector.go +++ b/x/move/types/connector.go @@ -38,11 +38,12 @@ const ( FunctionNameInitiaNftBurn = "burn" // function names for coin - FunctionNameCoinBalance = "balance" - FunctionNameCoinRegister = "register" - FunctionNameCoinTransfer = "transfer" - FunctionNameCoinSudoTransfer = "sudo_transfer" - FunctionNameCoinWhitelist = "whitelist" + FunctionNameCoinBalance = "balance" + FunctionNameCoinRegister = "register" + FunctionNameCoinTransfer = "transfer" + FunctionNameCoinSudoTransfer = "sudo_transfer" + FunctionNameCoinSudoMultiSend = "sudo_multisend" + FunctionNameCoinWhitelist = "whitelist" // function names for staking FunctionNameStakingInitializeForChain = "initialize_for_chain"