Skip to content

Commit

Permalink
support multisend
Browse files Browse the repository at this point in the history
  • Loading branch information
beer-1 committed Oct 16, 2024
1 parent 8414620 commit 668726a
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 14 deletions.
48 changes: 45 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 @@ 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"
Expand Down Expand Up @@ -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) {
Expand Down
100 changes: 100 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,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)

Expand Down
50 changes: 44 additions & 6 deletions x/bank/keeper/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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.
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
53 changes: 53 additions & 0 deletions x/move/keeper/bank.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

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

Expand Down Expand Up @@ -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,
)
}
32 changes: 32 additions & 0 deletions x/move/keeper/bank_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
11 changes: 6 additions & 5 deletions x/move/types/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 668726a

Please sign in to comment.