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
92 changes: 86 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,91 @@
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 {
// Create account if recipient does not exist.
outAddress := addrMap[output.Address]
accExists := k.ak.HasAccount(ctx, outAddress)
if !accExists {
defer telemetry.IncrCounter(1, "new", "account")
k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, outAddress))
}

amount := output.Coins.AmountOf(coin.Denom)
if !amount.IsPositive() {
continue

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

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L179

Added line #L179 was not covered by tests
}

recipients = append(recipients, outAddress)
amounts = append(amounts, output.Coins.AmountOf(coin.Denom))
}

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

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

View check run for this annotation

Codecov / codecov/patch

x/bank/keeper/send.go#L187-L188

Added lines #L187 - L188 were not covered by tests
}

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
Loading
Loading