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
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 @@
"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) 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
}

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

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/msg_server.go#L100-L101

Added lines #L100 - L101 were not covered by tests

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

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
}

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 122 in x/bank/keeper/msg_server.go

View check run for this annotation

Codecov / codecov/patch

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

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L127 - L128 were not covered by tests

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,
},
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

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
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 @@
"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 @@
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
}

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L112 - L113 were not covered by tests

addrMap := make(map[string][]byte)
for _, coin := range input.Coins {
if !coin.Amount.IsPositive() {
continue

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

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L118

Added line #L118 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 126 in x/bank/keeper/send.go

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L126

Added line #L126 was not covered by tests
}

// cache bytes address
if _, ok := addrMap[output.Address]; !ok {
addr, err := k.ak.AddressCodec().StringToBytes(output.Address)
if err != nil {
return err
}

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

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L133-L134

Added lines #L133 - L134 were not covered by tests

addrMap[output.Address] = addr
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

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
beer-1 marked this conversation as resolved.
Show resolved Hide resolved
}

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

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L145-L146

Added lines #L145 - L146 were not covered by tests
}

return nil
beer-1 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 @@

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

Expand Down Expand Up @@ -531,3 +532,55 @@
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
senderVMAddr, err := vmtypes.NewAccountAddressFromBytes(sender)
if err != nil {
return err
}

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L545 - L546 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

x/move/keeper/bank.go#L554-L555

Added lines #L554 - L555 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 562 in x/move/keeper/bank.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L561 - L562 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L568 - L569 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L573 - L574 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,
)
}
32 changes: 32 additions & 0 deletions x/move/keeper/bank_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,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 := input.MoveKeeper.MoveBankKeeper()

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)
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -39,11 +39,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
Loading