diff --git a/codec/types/interface_registry.go b/codec/types/interface_registry.go
index ee729993af6..5d7e72e890c 100644
--- a/codec/types/interface_registry.go
+++ b/codec/types/interface_registry.go
@@ -43,7 +43,7 @@ type InterfaceRegistry interface {
// the interface iface.
//
// Ex:
- // registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSend{})
+ // registry.RegisterImplementations((*sdk.Msg)(nil), &MsgSend{}, &MsgMultiSend{})
RegisterImplementations(iface interface{}, impls ...proto.Message)
// ListAllInterfaces list the type URLs of all registered interfaces.
diff --git a/docs/core/proto-docs.md b/docs/core/proto-docs.md
index f30ade8a7d9..a2115be784d 100644
--- a/docs/core/proto-docs.md
+++ b/docs/core/proto-docs.md
@@ -79,7 +79,9 @@
- [cosmos/bank/v1beta1/bank.proto](#cosmos/bank/v1beta1/bank.proto)
- [DenomUnit](#cosmos.bank.v1beta1.DenomUnit)
+ - [Input](#cosmos.bank.v1beta1.Input)
- [Metadata](#cosmos.bank.v1beta1.Metadata)
+ - [Output](#cosmos.bank.v1beta1.Output)
- [Params](#cosmos.bank.v1beta1.Params)
- [SendEnabled](#cosmos.bank.v1beta1.SendEnabled)
- [Supply](#cosmos.bank.v1beta1.Supply)
@@ -114,6 +116,8 @@
- [Query](#cosmos.bank.v1beta1.Query)
- [cosmos/bank/v1beta1/tx.proto](#cosmos/bank/v1beta1/tx.proto)
+ - [MsgMultiSend](#cosmos.bank.v1beta1.MsgMultiSend)
+ - [MsgMultiSendResponse](#cosmos.bank.v1beta1.MsgMultiSendResponse)
- [MsgSend](#cosmos.bank.v1beta1.MsgSend)
- [MsgSendResponse](#cosmos.bank.v1beta1.MsgSendResponse)
@@ -1584,6 +1588,22 @@ denomination unit of the basic token.
+
+
+### Input
+Input models transaction input.
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| `address` | [string](#string) | | |
+| `coins` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | |
+
+
+
+
+
+
### Metadata
@@ -1609,6 +1629,22 @@ Since: cosmos-sdk 0.43 |
+
+
+### Output
+Output models transaction outputs.
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| `address` | [string](#string) | | |
+| `coins` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | |
+
+
+
+
+
+
### Params
@@ -2095,6 +2131,32 @@ Query defines the gRPC querier service.
+
+
+### MsgMultiSend
+MsgMultiSend represents an arbitrary multi-in, multi-out send message.
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| `inputs` | [Input](#cosmos.bank.v1beta1.Input) | repeated | |
+| `outputs` | [Output](#cosmos.bank.v1beta1.Output) | repeated | |
+
+
+
+
+
+
+
+
+### MsgMultiSendResponse
+MsgMultiSendResponse defines the Msg/MultiSend response type.
+
+
+
+
+
+
### MsgSend
@@ -2136,6 +2198,7 @@ Msg defines the bank Msg service.
| Method Name | Request Type | Response Type | Description | HTTP Verb | Endpoint |
| ----------- | ------------ | ------------- | ------------| ------- | -------- |
| `Send` | [MsgSend](#cosmos.bank.v1beta1.MsgSend) | [MsgSendResponse](#cosmos.bank.v1beta1.MsgSendResponse) | Send defines a method for sending coins from one account to another account. | |
+| `MultiSend` | [MsgMultiSend](#cosmos.bank.v1beta1.MsgMultiSend) | [MsgMultiSendResponse](#cosmos.bank.v1beta1.MsgMultiSendResponse) | MultiSend defines a method for sending coins from some accounts to other accounts. | |
diff --git a/proto/cosmos/bank/v1beta1/bank.proto b/proto/cosmos/bank/v1beta1/bank.proto
index 475ca93aeaa..df91008df64 100644
--- a/proto/cosmos/bank/v1beta1/bank.proto
+++ b/proto/cosmos/bank/v1beta1/bank.proto
@@ -23,6 +23,26 @@ message SendEnabled {
bool enabled = 2;
}
+// Input models transaction input.
+message Input {
+ option (gogoproto.equal) = false;
+ option (gogoproto.goproto_getters) = false;
+
+ string address = 1;
+ repeated cosmos.base.v1beta1.Coin coins = 2
+ [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
+}
+
+// Output models transaction outputs.
+message Output {
+ option (gogoproto.equal) = false;
+ option (gogoproto.goproto_getters) = false;
+
+ string address = 1;
+ repeated cosmos.base.v1beta1.Coin coins = 2
+ [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
+}
+
// Supply represents a struct that passively keeps track of the total supply
// amounts in the network.
// This message is deprecated now that supply is indexed by denom.
diff --git a/proto/cosmos/bank/v1beta1/tx.proto b/proto/cosmos/bank/v1beta1/tx.proto
index b9e26d5d494..26b2ab41f4c 100644
--- a/proto/cosmos/bank/v1beta1/tx.proto
+++ b/proto/cosmos/bank/v1beta1/tx.proto
@@ -11,6 +11,9 @@ option go_package = "github.com/cosmos/cosmos-sdk/x/bank/types";
service Msg {
// Send defines a method for sending coins from one account to another account.
rpc Send(MsgSend) returns (MsgSendResponse);
+
+ // MultiSend defines a method for sending coins from some accounts to other accounts.
+ rpc MultiSend(MsgMultiSend) returns (MsgMultiSendResponse);
}
// MsgSend represents a message to send coins from one account to another.
@@ -26,3 +29,14 @@ message MsgSend {
// MsgSendResponse defines the Msg/Send response type.
message MsgSendResponse {}
+
+// MsgMultiSend represents an arbitrary multi-in, multi-out send message.
+message MsgMultiSend {
+ option (gogoproto.equal) = false;
+
+ repeated Input inputs = 1 [(gogoproto.nullable) = false];
+ repeated Output outputs = 2 [(gogoproto.nullable) = false];
+}
+
+// MsgMultiSendResponse defines the Msg/MultiSend response type.
+message MsgMultiSendResponse {}
diff --git a/simapp/params/weights.go b/simapp/params/weights.go
index 984eee939be..746e304de2b 100644
--- a/simapp/params/weights.go
+++ b/simapp/params/weights.go
@@ -3,6 +3,7 @@ package params
// Default simulation operation weights for messages and gov proposals
const (
DefaultWeightMsgSend int = 100
+ DefaultWeightMsgMultiSend int = 10
DefaultWeightMsgSetWithdrawAddress int = 50
DefaultWeightMsgWithdrawDelegationReward int = 50
DefaultWeightMsgWithdrawValidatorCommission int = 50
diff --git a/x/authz/keeper/keeper_test.go b/x/authz/keeper/keeper_test.go
index 0421590b7dc..078c6e7bc47 100644
--- a/x/authz/keeper/keeper_test.go
+++ b/x/authz/keeper/keeper_test.go
@@ -13,7 +13,6 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
- govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
var bankSendAuthMsgType = banktypes.SendAuthorization{}.MsgTypeURL()
@@ -73,7 +72,7 @@ func (s *TestSuite) TestKeeper() {
s.Require().Equal(authorization.MsgTypeURL(), bankSendAuthMsgType)
s.T().Log("verify fetching authorization with wrong msg type fails")
- authorization, _ = app.AuthzKeeper.GetCleanAuthorization(ctx, granteeAddr, granterAddr, sdk.MsgTypeURL(&govtypes.MsgDeposit{}))
+ authorization, _ = app.AuthzKeeper.GetCleanAuthorization(ctx, granteeAddr, granterAddr, sdk.MsgTypeURL(&banktypes.MsgMultiSend{}))
s.Require().Nil(authorization)
s.T().Log("verify fetching authorization with wrong grantee fails")
diff --git a/x/bank/app_test.go b/x/bank/app_test.go
index 1f85e8e4d70..ba8085026a5 100644
--- a/x/bank/app_test.go
+++ b/x/bank/app_test.go
@@ -45,6 +45,44 @@ var (
halfCoins = sdk.Coins{sdk.NewInt64Coin("foocoin", 5)}
sendMsg1 = types.NewMsgSend(addr1, addr2, coins)
+
+ multiSendMsg1 = &types.MsgMultiSend{
+ Inputs: []types.Input{types.NewInput(addr1, coins)},
+ Outputs: []types.Output{types.NewOutput(addr2, coins)},
+ }
+ multiSendMsg2 = &types.MsgMultiSend{
+ Inputs: []types.Input{types.NewInput(addr1, coins)},
+ Outputs: []types.Output{
+ types.NewOutput(addr2, halfCoins),
+ types.NewOutput(addr3, halfCoins),
+ },
+ }
+ multiSendMsg3 = &types.MsgMultiSend{
+ Inputs: []types.Input{
+ types.NewInput(addr1, coins),
+ types.NewInput(addr4, coins),
+ },
+ Outputs: []types.Output{
+ types.NewOutput(addr2, coins),
+ types.NewOutput(addr3, coins),
+ },
+ }
+ multiSendMsg4 = &types.MsgMultiSend{
+ Inputs: []types.Input{
+ types.NewInput(addr2, coins),
+ },
+ Outputs: []types.Output{
+ types.NewOutput(addr1, coins),
+ },
+ }
+ multiSendMsg5 = &types.MsgMultiSend{
+ Inputs: []types.Input{
+ types.NewInput(addr1, coins),
+ },
+ Outputs: []types.Output{
+ types.NewOutput(moduleAccAddr, coins),
+ },
+ }
)
func TestSendNotEnoughBalance(t *testing.T) {
@@ -81,3 +119,220 @@ func TestSendNotEnoughBalance(t *testing.T) {
require.Equal(t, res2.GetAccountNumber(), origAccNum)
require.Equal(t, res2.GetSequence(), origSeq+1)
}
+
+func TestMsgMultiSendWithAccounts(t *testing.T) {
+ acc := &authtypes.BaseAccount{
+ Address: addr1.String(),
+ }
+
+ genAccs := []authtypes.GenesisAccount{acc}
+ app := simapp.SetupWithGenesisAccounts(genAccs)
+ ctx := app.BaseApp.NewContext(false, tmproto.Header{})
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 67))))
+
+ app.Commit()
+
+ res1 := app.AccountKeeper.GetAccount(ctx, addr1)
+ require.NotNil(t, res1)
+ require.Equal(t, acc, res1.(*authtypes.BaseAccount))
+
+ testCases := []appTestCase{
+ {
+ desc: "make a valid tx",
+ msgs: []sdk.Msg{multiSendMsg1},
+ accNums: []uint64{0},
+ accSeqs: []uint64{0},
+ expSimPass: true,
+ expPass: true,
+ privKeys: []cryptotypes.PrivKey{priv1},
+ expectedBalances: []expectedBalance{
+ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 57)}},
+ {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}},
+ },
+ },
+ {
+ desc: "wrong accNum should pass Simulate, but not Deliver",
+ msgs: []sdk.Msg{multiSendMsg1, multiSendMsg2},
+ accNums: []uint64{1}, // wrong account number
+ accSeqs: []uint64{1},
+ expSimPass: true, // doesn't check signature
+ expPass: false,
+ privKeys: []cryptotypes.PrivKey{priv1},
+ },
+ {
+ desc: "wrong accSeq should not pass Simulate",
+ msgs: []sdk.Msg{multiSendMsg5},
+ accNums: []uint64{0},
+ accSeqs: []uint64{0}, // wrong account sequence
+ expSimPass: false,
+ expPass: false,
+ privKeys: []cryptotypes.PrivKey{priv1},
+ },
+ }
+
+ for _, tc := range testCases {
+ header := tmproto.Header{Height: app.LastBlockHeight() + 1}
+ txGen := simapp.MakeTestEncodingConfig().TxConfig
+ _, _, err := simapp.SignCheckDeliver(t, txGen, app.BaseApp, header, tc.msgs, "", tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...)
+ if tc.expPass {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ }
+
+ for _, eb := range tc.expectedBalances {
+ simapp.CheckBalance(t, app, eb.addr, eb.coins)
+ }
+ }
+}
+
+func TestMsgMultiSendMultipleOut(t *testing.T) {
+ acc1 := &authtypes.BaseAccount{
+ Address: addr1.String(),
+ }
+ acc2 := &authtypes.BaseAccount{
+ Address: addr2.String(),
+ }
+
+ genAccs := []authtypes.GenesisAccount{acc1, acc2}
+ app := simapp.SetupWithGenesisAccounts(genAccs)
+ ctx := app.BaseApp.NewContext(false, tmproto.Header{})
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42))))
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr2, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42))))
+
+ app.Commit()
+
+ testCases := []appTestCase{
+ {
+ msgs: []sdk.Msg{multiSendMsg2},
+ accNums: []uint64{0},
+ accSeqs: []uint64{0},
+ expSimPass: true,
+ expPass: true,
+ privKeys: []cryptotypes.PrivKey{priv1},
+ expectedBalances: []expectedBalance{
+ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}},
+ {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 47)}},
+ {addr3, sdk.Coins{sdk.NewInt64Coin("foocoin", 5)}},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ header := tmproto.Header{Height: app.LastBlockHeight() + 1}
+ txGen := simapp.MakeTestEncodingConfig().TxConfig
+ _, _, err := simapp.SignCheckDeliver(t, txGen, app.BaseApp, header, tc.msgs, "", tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...)
+ require.NoError(t, err)
+
+ for _, eb := range tc.expectedBalances {
+ simapp.CheckBalance(t, app, eb.addr, eb.coins)
+ }
+ }
+}
+
+func TestMsgMultiSendMultipleInOut(t *testing.T) {
+ acc1 := &authtypes.BaseAccount{
+ Address: addr1.String(),
+ }
+ acc2 := &authtypes.BaseAccount{
+ Address: addr2.String(),
+ }
+ acc4 := &authtypes.BaseAccount{
+ Address: addr4.String(),
+ }
+
+ genAccs := []authtypes.GenesisAccount{acc1, acc2, acc4}
+ app := simapp.SetupWithGenesisAccounts(genAccs)
+ ctx := app.BaseApp.NewContext(false, tmproto.Header{})
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42))))
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr2, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42))))
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr4, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42))))
+
+ app.Commit()
+
+ testCases := []appTestCase{
+ {
+ msgs: []sdk.Msg{multiSendMsg3},
+ accNums: []uint64{0, 2},
+ accSeqs: []uint64{0, 0},
+ expSimPass: true,
+ expPass: true,
+ privKeys: []cryptotypes.PrivKey{priv1, priv4},
+ expectedBalances: []expectedBalance{
+ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}},
+ {addr4, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}},
+ {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 52)}},
+ {addr3, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ header := tmproto.Header{Height: app.LastBlockHeight() + 1}
+ txGen := simapp.MakeTestEncodingConfig().TxConfig
+ _, _, err := simapp.SignCheckDeliver(t, txGen, app.BaseApp, header, tc.msgs, "", tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...)
+ require.NoError(t, err)
+
+ for _, eb := range tc.expectedBalances {
+ simapp.CheckBalance(t, app, eb.addr, eb.coins)
+ }
+ }
+}
+
+func TestMsgMultiSendDependent(t *testing.T) {
+ acc1 := authtypes.NewBaseAccountWithAddress(addr1)
+ acc2 := authtypes.NewBaseAccountWithAddress(addr2)
+ err := acc2.SetAccountNumber(1)
+ require.NoError(t, err)
+
+ genAccs := []authtypes.GenesisAccount{acc1, acc2}
+ app := simapp.SetupWithGenesisAccounts(genAccs)
+ ctx := app.BaseApp.NewContext(false, tmproto.Header{})
+
+ require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42))))
+
+ app.Commit()
+
+ testCases := []appTestCase{
+ {
+ msgs: []sdk.Msg{multiSendMsg1},
+ accNums: []uint64{0},
+ accSeqs: []uint64{0},
+ expSimPass: true,
+ expPass: true,
+ privKeys: []cryptotypes.PrivKey{priv1},
+ expectedBalances: []expectedBalance{
+ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}},
+ {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}},
+ },
+ },
+ {
+ msgs: []sdk.Msg{multiSendMsg4},
+ accNums: []uint64{1},
+ accSeqs: []uint64{0},
+ expSimPass: true,
+ expPass: true,
+ privKeys: []cryptotypes.PrivKey{priv2},
+ expectedBalances: []expectedBalance{
+ {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 42)}},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ header := tmproto.Header{Height: app.LastBlockHeight() + 1}
+ txGen := simapp.MakeTestEncodingConfig().TxConfig
+ _, _, err := simapp.SignCheckDeliver(t, txGen, app.BaseApp, header, tc.msgs, "", tc.accNums, tc.accSeqs, tc.expSimPass, tc.expPass, tc.privKeys...)
+ require.NoError(t, err)
+
+ for _, eb := range tc.expectedBalances {
+ simapp.CheckBalance(t, app, eb.addr, eb.coins)
+ }
+ }
+}
diff --git a/x/bank/bench_test.go b/x/bank/bench_test.go
index 90d63c34efb..4ba2624b762 100644
--- a/x/bank/bench_test.go
+++ b/x/bank/bench_test.go
@@ -58,3 +58,45 @@ func BenchmarkOneBankSendTxPerBlock(b *testing.B) {
height++
}
}
+
+func BenchmarkOneBankMultiSendTxPerBlock(b *testing.B) {
+ b.ReportAllocs()
+ // Add an account at genesis
+ acc := authtypes.BaseAccount{
+ Address: addr1.String(),
+ }
+
+ // Construct genesis state
+ genAccs := []authtypes.GenesisAccount{&acc}
+ benchmarkApp := simapp.SetupWithGenesisAccounts(genAccs)
+ ctx := benchmarkApp.BaseApp.NewContext(false, tmproto.Header{})
+
+ // some value conceivably higher than the benchmarks would ever go
+ require.NoError(b, simapp.FundAccount(benchmarkApp.BankKeeper, ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 100000000000))))
+
+ benchmarkApp.Commit()
+ txGen := simappparams.MakeTestEncodingConfig().TxConfig
+
+ // Precompute all txs
+ txs, err := simapp.GenSequenceOfTxs(txGen, []sdk.Msg{multiSendMsg1}, []uint64{0}, []uint64{uint64(0)}, b.N, priv1)
+ require.NoError(b, err)
+ b.ResetTimer()
+
+ height := int64(3)
+
+ // Run this with a profiler, so its easy to distinguish what time comes from
+ // Committing, and what time comes from Check/Deliver Tx.
+ for i := 0; i < b.N; i++ {
+ benchmarkApp.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: height}})
+ _, _, err := benchmarkApp.Check(txGen.TxEncoder(), txs[i])
+ if err != nil {
+ panic("something is broken in checking transaction")
+ }
+
+ _, _, err = benchmarkApp.Deliver(txGen.TxEncoder(), txs[i])
+ require.NoError(b, err)
+ benchmarkApp.EndBlock(abci.RequestEndBlock{Height: height})
+ benchmarkApp.Commit()
+ height++
+ }
+}
diff --git a/x/bank/handler.go b/x/bank/handler.go
index d17e2547fcc..0fb0f53a4cd 100644
--- a/x/bank/handler.go
+++ b/x/bank/handler.go
@@ -19,6 +19,10 @@ func NewHandler(k keeper.Keeper) sdk.Handler {
res, err := msgServer.Send(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
+ case *types.MsgMultiSend:
+ res, err := msgServer.MultiSend(sdk.WrapSDKContext(ctx), msg)
+ return sdk.WrapServiceResult(ctx, res, err)
+
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized bank message type: %T", msg)
}
diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go
index 4077f60c7c0..74218383cb0 100644
--- a/x/bank/keeper/keeper_test.go
+++ b/x/bank/keeper/keeper_test.go
@@ -350,6 +350,90 @@ func (suite *IntegrationTestSuite) TestSendCoinsNewAccount() {
suite.Require().NotNil(app.AccountKeeper.GetAccount(ctx, addr2))
}
+func (suite *IntegrationTestSuite) TestInputOutputNewAccount() {
+ app, ctx := suite.app, suite.ctx
+
+ balances := sdk.NewCoins(newFooCoin(100), newBarCoin(50))
+ addr1 := sdk.AccAddress([]byte("addr1_______________"))
+ acc1 := app.AccountKeeper.NewAccountWithAddress(ctx, addr1)
+ app.AccountKeeper.SetAccount(ctx, acc1)
+ suite.Require().NoError(simapp.FundAccount(app.BankKeeper, ctx, addr1, balances))
+
+ acc1Balances := app.BankKeeper.GetAllBalances(ctx, addr1)
+ suite.Require().Equal(balances, acc1Balances)
+
+ addr2 := sdk.AccAddress([]byte("addr2_______________"))
+
+ suite.Require().Nil(app.AccountKeeper.GetAccount(ctx, addr2))
+ suite.Require().Empty(app.BankKeeper.GetAllBalances(ctx, addr2))
+
+ inputs := []types.Input{
+ {Address: addr1.String(), Coins: sdk.NewCoins(newFooCoin(30), newBarCoin(10))},
+ }
+ outputs := []types.Output{
+ {Address: addr2.String(), Coins: sdk.NewCoins(newFooCoin(30), newBarCoin(10))},
+ }
+
+ suite.Require().NoError(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs))
+
+ expected := sdk.NewCoins(newFooCoin(30), newBarCoin(10))
+ acc2Balances := app.BankKeeper.GetAllBalances(ctx, addr2)
+ suite.Require().Equal(expected, acc2Balances)
+ suite.Require().NotNil(app.AccountKeeper.GetAccount(ctx, addr2))
+}
+
+func (suite *IntegrationTestSuite) TestInputOutputCoins() {
+ app, ctx := suite.app, suite.ctx
+ balances := sdk.NewCoins(newFooCoin(90), newBarCoin(30))
+
+ addr1 := sdk.AccAddress([]byte("addr1_______________"))
+ acc1 := app.AccountKeeper.NewAccountWithAddress(ctx, addr1)
+ app.AccountKeeper.SetAccount(ctx, acc1)
+
+ addr2 := sdk.AccAddress([]byte("addr2_______________"))
+ acc2 := app.AccountKeeper.NewAccountWithAddress(ctx, addr2)
+ app.AccountKeeper.SetAccount(ctx, acc2)
+
+ addr3 := sdk.AccAddress([]byte("addr3_______________"))
+ acc3 := app.AccountKeeper.NewAccountWithAddress(ctx, addr3)
+ app.AccountKeeper.SetAccount(ctx, acc3)
+
+ inputs := []types.Input{
+ {Address: addr1.String(), Coins: sdk.NewCoins(newFooCoin(30), newBarCoin(10))},
+ {Address: addr1.String(), Coins: sdk.NewCoins(newFooCoin(30), newBarCoin(10))},
+ }
+ outputs := []types.Output{
+ {Address: addr2.String(), Coins: sdk.NewCoins(newFooCoin(30), newBarCoin(10))},
+ {Address: addr3.String(), Coins: sdk.NewCoins(newFooCoin(30), newBarCoin(10))},
+ }
+
+ suite.Require().Error(app.BankKeeper.InputOutputCoins(ctx, inputs, []types.Output{}))
+ suite.Require().Error(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs))
+
+ suite.Require().NoError(simapp.FundAccount(app.BankKeeper, ctx, addr1, balances))
+
+ insufficientInputs := []types.Input{
+ {Address: addr1.String(), Coins: sdk.NewCoins(newFooCoin(300), newBarCoin(100))},
+ {Address: addr1.String(), Coins: sdk.NewCoins(newFooCoin(300), newBarCoin(100))},
+ }
+ insufficientOutputs := []types.Output{
+ {Address: addr2.String(), Coins: sdk.NewCoins(newFooCoin(300), newBarCoin(100))},
+ {Address: addr3.String(), Coins: sdk.NewCoins(newFooCoin(300), newBarCoin(100))},
+ }
+ suite.Require().Error(app.BankKeeper.InputOutputCoins(ctx, insufficientInputs, insufficientOutputs))
+ suite.Require().NoError(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs))
+
+ acc1Balances := app.BankKeeper.GetAllBalances(ctx, addr1)
+ expected := sdk.NewCoins(newFooCoin(30), newBarCoin(10))
+ suite.Require().Equal(expected, acc1Balances)
+
+ acc2Balances := app.BankKeeper.GetAllBalances(ctx, addr2)
+ suite.Require().Equal(expected, acc2Balances)
+
+ acc3Balances := app.BankKeeper.GetAllBalances(ctx, addr3)
+ suite.Require().Equal(expected, acc3Balances)
+}
+
func (suite *IntegrationTestSuite) TestSendCoins() {
app, ctx := suite.app, suite.ctx
balances := sdk.NewCoins(newFooCoin(100), newBarCoin(50))
@@ -561,6 +645,104 @@ func (suite *IntegrationTestSuite) TestMsgSendEvents() {
suite.Require().Equal(abci.Event(event2), events[9])
}
+func (suite *IntegrationTestSuite) TestMsgMultiSendEvents() {
+ app, ctx := suite.app, suite.ctx
+
+ app.BankKeeper.SetParams(ctx, types.DefaultParams())
+
+ addr := sdk.AccAddress([]byte("addr1_______________"))
+ addr2 := sdk.AccAddress([]byte("addr2_______________"))
+ addr3 := sdk.AccAddress([]byte("addr3_______________"))
+ addr4 := sdk.AccAddress([]byte("addr4_______________"))
+ acc := app.AccountKeeper.NewAccountWithAddress(ctx, addr)
+ acc2 := app.AccountKeeper.NewAccountWithAddress(ctx, addr2)
+
+ app.AccountKeeper.SetAccount(ctx, acc)
+ app.AccountKeeper.SetAccount(ctx, acc2)
+
+ newCoins := sdk.NewCoins(sdk.NewInt64Coin(fooDenom, 50))
+ newCoins2 := sdk.NewCoins(sdk.NewInt64Coin(barDenom, 100))
+ inputs := []types.Input{
+ {Address: addr.String(), Coins: newCoins},
+ {Address: addr2.String(), Coins: newCoins2},
+ }
+ outputs := []types.Output{
+ {Address: addr3.String(), Coins: newCoins},
+ {Address: addr4.String(), Coins: newCoins2},
+ }
+
+ suite.Require().Error(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs))
+
+ events := ctx.EventManager().ABCIEvents()
+ suite.Require().Equal(0, len(events))
+
+ // Set addr's coins but not addr2's coins
+ suite.Require().NoError(simapp.FundAccount(app.BankKeeper, ctx, addr, sdk.NewCoins(sdk.NewInt64Coin(fooDenom, 50))))
+ suite.Require().Error(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs))
+
+ events = ctx.EventManager().ABCIEvents()
+ suite.Require().Equal(8, len(events)) // 7 events because account funding causes extra minting + coin_spent + coin_recv events
+
+ event1 := sdk.Event{
+ Type: sdk.EventTypeMessage,
+ Attributes: []abci.EventAttribute{},
+ }
+ event1.Attributes = append(
+ event1.Attributes,
+ abci.EventAttribute{Key: []byte(types.AttributeKeySender), Value: []byte(addr.String())},
+ )
+ suite.Require().Equal(abci.Event(event1), events[7])
+
+ // Set addr's coins and addr2's coins
+ suite.Require().NoError(simapp.FundAccount(app.BankKeeper, ctx, addr, sdk.NewCoins(sdk.NewInt64Coin(fooDenom, 50))))
+ newCoins = sdk.NewCoins(sdk.NewInt64Coin(fooDenom, 50))
+
+ suite.Require().NoError(simapp.FundAccount(app.BankKeeper, ctx, addr2, sdk.NewCoins(sdk.NewInt64Coin(barDenom, 100))))
+ newCoins2 = sdk.NewCoins(sdk.NewInt64Coin(barDenom, 100))
+
+ suite.Require().NoError(app.BankKeeper.InputOutputCoins(ctx, inputs, outputs))
+
+ events = ctx.EventManager().ABCIEvents()
+ suite.Require().Equal(28, len(events)) // 25 due to account funding + coin_spent + coin_recv events
+
+ event2 := sdk.Event{
+ Type: sdk.EventTypeMessage,
+ Attributes: []abci.EventAttribute{},
+ }
+ event2.Attributes = append(
+ event2.Attributes,
+ abci.EventAttribute{Key: []byte(types.AttributeKeySender), Value: []byte(addr2.String())},
+ )
+ event3 := sdk.Event{
+ Type: types.EventTypeTransfer,
+ Attributes: []abci.EventAttribute{},
+ }
+ event3.Attributes = append(
+ event3.Attributes,
+ abci.EventAttribute{Key: []byte(types.AttributeKeyRecipient), Value: []byte(addr3.String())},
+ )
+ event3.Attributes = append(
+ event3.Attributes,
+ abci.EventAttribute{Key: []byte(sdk.AttributeKeyAmount), Value: []byte(newCoins.String())})
+ event4 := sdk.Event{
+ Type: types.EventTypeTransfer,
+ Attributes: []abci.EventAttribute{},
+ }
+ event4.Attributes = append(
+ event4.Attributes,
+ abci.EventAttribute{Key: []byte(types.AttributeKeyRecipient), Value: []byte(addr4.String())},
+ )
+ event4.Attributes = append(
+ event4.Attributes,
+ abci.EventAttribute{Key: []byte(sdk.AttributeKeyAmount), Value: []byte(newCoins2.String())},
+ )
+ // events are shifted due to the funding account events
+ suite.Require().Equal(abci.Event(event1), events[21])
+ suite.Require().Equal(abci.Event(event2), events[23])
+ suite.Require().Equal(abci.Event(event3), events[25])
+ suite.Require().Equal(abci.Event(event4), events[27])
+}
+
func (suite *IntegrationTestSuite) TestSpendableCoins() {
app, ctx := suite.app, suite.ctx
now := tmtime.Now()
diff --git a/x/bank/keeper/msg_server.go b/x/bank/keeper/msg_server.go
index c91a7cc9157..5d940b459e4 100644
--- a/x/bank/keeper/msg_server.go
+++ b/x/bank/keeper/msg_server.go
@@ -69,3 +69,38 @@ func (k msgServer) Send(goCtx context.Context, msg *types.MsgSend) (*types.MsgSe
return &types.MsgSendResponse{}, nil
}
+
+func (k msgServer) MultiSend(goCtx context.Context, msg *types.MsgMultiSend) (*types.MsgMultiSendResponse, error) {
+ ctx := sdk.UnwrapSDKContext(goCtx)
+
+ // NOTE: totalIn == totalOut should already have been checked
+ for _, in := range msg.Inputs {
+ if err := k.IsSendEnabledCoins(ctx, in.Coins...); err != nil {
+ return nil, err
+ }
+ }
+
+ for _, out := range msg.Outputs {
+ accAddr, err := sdk.AccAddressFromBech32(out.Address)
+ if err != nil {
+ panic(err)
+ }
+ if k.BlockedAddr(accAddr) {
+ return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive transactions", out.Address)
+ }
+ }
+
+ err := k.InputOutputCoins(ctx, msg.Inputs, msg.Outputs)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ sdk.EventTypeMessage,
+ sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
+ ),
+ )
+
+ return &types.MsgMultiSendResponse{}, nil
+}
diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go
index f2c90f92a4b..e6758d01982 100644
--- a/x/bank/keeper/send.go
+++ b/x/bank/keeper/send.go
@@ -16,6 +16,7 @@ import (
type SendKeeper interface {
ViewKeeper
+ InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
SendManyCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddrs []sdk.AccAddress, amts []sdk.Coins) error
@@ -69,6 +70,67 @@ func (k BaseSendKeeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSpace.SetParamSet(ctx, ¶ms)
}
+// 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 BaseSendKeeper) InputOutputCoins(ctx sdk.Context, inputs []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.ValidateInputsOutputs(inputs, outputs); err != nil {
+ return err
+ }
+
+ for _, in := range inputs {
+ inAddress, err := sdk.AccAddressFromBech32(in.Address)
+ if err != nil {
+ return err
+ }
+
+ err = k.subUnlockedCoins(ctx, inAddress, in.Coins)
+ if err != nil {
+ return err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ sdk.EventTypeMessage,
+ sdk.NewAttribute(types.AttributeKeySender, in.Address),
+ ),
+ )
+ }
+
+ for _, out := range outputs {
+ outAddress, err := sdk.AccAddressFromBech32(out.Address)
+ if err != nil {
+ return err
+ }
+ err = k.addCoins(ctx, outAddress, out.Coins)
+ if err != nil {
+ return err
+ }
+
+ ctx.EventManager().EmitEvent(
+ sdk.NewEvent(
+ types.EventTypeTransfer,
+ sdk.NewAttribute(types.AttributeKeyRecipient, out.Address),
+ sdk.NewAttribute(sdk.AttributeKeyAmount, out.Coins.String()),
+ ),
+ )
+
+ // Create account if recipient does not exist.
+ //
+ // NOTE: This should ultimately be removed in favor a more flexible approach
+ // such as delegated fee messages.
+ accExists := k.ak.HasAccount(ctx, outAddress)
+ if !accExists {
+ defer telemetry.IncrCounter(1, "new", "account")
+ k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, outAddress))
+ }
+ }
+
+ return nil
+}
+
// SendCoins transfers amt coins from a sending account to a receiving account.
// An error is returned upon failure.
func (k BaseSendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error {
diff --git a/x/bank/simulation/operations.go b/x/bank/simulation/operations.go
index 78d4d3e6cf6..1d09238c3a3 100644
--- a/x/bank/simulation/operations.go
+++ b/x/bank/simulation/operations.go
@@ -18,7 +18,8 @@ import (
// Simulation operation weights constants
const (
- OpWeightMsgSend = "op_weight_msg_send"
+ OpWeightMsgSend = "op_weight_msg_send"
+ OpWeightMsgMultiSend = "op_weight_msg_multisend"
)
// WeightedOperations returns all the operations from the module with their respective weights
@@ -26,18 +27,28 @@ func WeightedOperations(
appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper, bk keeper.Keeper,
) simulation.WeightedOperations {
- var weightMsgSend int
+ var weightMsgSend, weightMsgMultiSend int
appParams.GetOrGenerate(cdc, OpWeightMsgSend, &weightMsgSend, nil,
func(_ *rand.Rand) {
weightMsgSend = simappparams.DefaultWeightMsgSend
},
)
+ appParams.GetOrGenerate(cdc, OpWeightMsgMultiSend, &weightMsgMultiSend, nil,
+ func(_ *rand.Rand) {
+ weightMsgMultiSend = simappparams.DefaultWeightMsgMultiSend
+ },
+ )
+
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgSend,
SimulateMsgSend(ak, bk),
),
+ simulation.NewWeightedOperation(
+ weightMsgMultiSend,
+ SimulateMsgMultiSend(ak, bk),
+ ),
}
}
@@ -150,6 +161,229 @@ func sendMsgSend(
return gasInfo, nil
}
+// SimulateMsgMultiSend tests and runs a single msg multisend, with randomized, capped number of inputs/outputs.
+// all accounts in msg fields exist in state
+func SimulateMsgMultiSend(ak types.AccountKeeper, bk keeper.Keeper) simtypes.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
+ accs []simtypes.Account, chainID string,
+ ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
+
+ // random number of inputs/outputs between [1, 3]
+ inputs := make([]types.Input, r.Intn(3)+1)
+ outputs := make([]types.Output, r.Intn(3)+1)
+
+ // collect signer privKeys
+ privs := make([]cryptotypes.PrivKey, len(inputs))
+
+ // use map to check if address already exists as input
+ usedAddrs := make(map[string]bool)
+
+ var totalSentCoins sdk.Coins
+ for i := range inputs {
+ // generate random input fields, ignore to address
+ from, _, coins, skip := randomSendFields(r, ctx, accs, bk, ak)
+
+ // make sure account is fresh and not used in previous input
+ for usedAddrs[from.Address.String()] {
+ from, _, coins, skip = randomSendFields(r, ctx, accs, bk, ak)
+ }
+
+ if skip {
+ return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgMultiSend, "skip all transfers"), nil, nil
+ }
+
+ // set input address in used address map
+ usedAddrs[from.Address.String()] = true
+
+ // set signer privkey
+ privs[i] = from.PrivKey
+
+ // set next input and accumulate total sent coins
+ inputs[i] = types.NewInput(from.Address, coins)
+ totalSentCoins = totalSentCoins.Add(coins...)
+ }
+
+ // Check send_enabled status of each sent coin denom
+ if err := bk.IsSendEnabledCoins(ctx, totalSentCoins...); err != nil {
+ return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgMultiSend, err.Error()), nil, nil
+ }
+
+ for o := range outputs {
+ outAddr, _ := simtypes.RandomAcc(r, accs)
+
+ var outCoins sdk.Coins
+ // split total sent coins into random subsets for output
+ if o == len(outputs)-1 {
+ outCoins = totalSentCoins
+ } else {
+ // take random subset of remaining coins for output
+ // and update remaining coins
+ outCoins = simtypes.RandSubsetCoins(r, totalSentCoins)
+ totalSentCoins = totalSentCoins.Sub(outCoins)
+ }
+
+ outputs[o] = types.NewOutput(outAddr.Address, outCoins)
+ }
+
+ // remove any output that has no coins
+
+ for i := 0; i < len(outputs); {
+ if outputs[i].Coins.Empty() {
+ outputs[i] = outputs[len(outputs)-1]
+ outputs = outputs[:len(outputs)-1]
+ } else {
+ // continue onto next coin
+ i++
+ }
+ }
+
+ msg := &types.MsgMultiSend{
+ Inputs: inputs,
+ Outputs: outputs,
+ }
+ gasInfo, err := sendMsgMultiSend(r, app, bk, ak, msg, ctx, chainID, privs)
+ if err != nil {
+ return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "invalid transfers"), nil, err
+ }
+
+ return simtypes.NewOperationMsg(msg, true, "", gasInfo.GasWanted, gasInfo.GasUsed, nil), nil, nil
+ }
+}
+
+// SimulateMsgMultiSendToModuleAccount sends coins to Module Accounts
+func SimulateMsgMultiSendToModuleAccount(ak types.AccountKeeper, bk keeper.Keeper, moduleAccCount int) simtypes.Operation {
+ return func(
+ r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
+ accs []simtypes.Account, chainID string,
+ ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
+
+ inputs := make([]types.Input, 2)
+ outputs := make([]types.Output, moduleAccCount)
+ // collect signer privKeys
+ privs := make([]cryptotypes.PrivKey, len(inputs))
+
+ var totalSentCoins sdk.Coins
+ for i := range inputs {
+ sender := accs[i]
+ privs[i] = sender.PrivKey
+ spendable := bk.SpendableCoins(ctx, sender.Address)
+ coins := simtypes.RandSubsetCoins(r, spendable)
+ inputs[i] = types.NewInput(sender.Address, coins)
+ totalSentCoins = totalSentCoins.Add(coins...)
+ }
+
+ if err := bk.IsSendEnabledCoins(ctx, totalSentCoins...); err != nil {
+ return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgMultiSend, err.Error()), nil, nil
+ }
+
+ moduleAccounts := getModuleAccounts(ak, ctx, moduleAccCount)
+ for i := range outputs {
+ var outCoins sdk.Coins
+ // split total sent coins into random subsets for output
+ if i == len(outputs)-1 {
+ outCoins = totalSentCoins
+ } else {
+ // take random subset of remaining coins for output
+ // and update remaining coins
+ outCoins = simtypes.RandSubsetCoins(r, totalSentCoins)
+ totalSentCoins = totalSentCoins.Sub(outCoins)
+ }
+
+ outputs[i] = types.NewOutput(moduleAccounts[i].Address, outCoins)
+ }
+
+ // remove any output that has no coins
+
+ for i := 0; i < len(outputs); {
+ if outputs[i].Coins.Empty() {
+ outputs[i] = outputs[len(outputs)-1]
+ outputs = outputs[:len(outputs)-1]
+ } else {
+ // continue onto next coin
+ i++
+ }
+ }
+
+ msg := &types.MsgMultiSend{
+ Inputs: inputs,
+ Outputs: outputs,
+ }
+ gasInfo, err := sendMsgMultiSend(r, app, bk, ak, msg, ctx, chainID, privs)
+ if err != nil {
+ return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "invalid transfers"), nil, err
+ }
+
+ return simtypes.NewOperationMsg(msg, true, "", gasInfo.GasWanted, gasInfo.GasUsed, nil), nil, nil
+ }
+}
+
+// sendMsgMultiSend sends a transaction with a MsgMultiSend from a provided random
+// account.
+func sendMsgMultiSend(
+ r *rand.Rand, app *baseapp.BaseApp, bk keeper.Keeper, ak types.AccountKeeper,
+ msg *types.MsgMultiSend, ctx sdk.Context, chainID string, privkeys []cryptotypes.PrivKey,
+) (sdk.GasInfo, error) {
+
+ accountNumbers := make([]uint64, len(msg.Inputs))
+ sequenceNumbers := make([]uint64, len(msg.Inputs))
+
+ for i := 0; i < len(msg.Inputs); i++ {
+ addr, err := sdk.AccAddressFromBech32(msg.Inputs[i].Address)
+ if err != nil {
+ panic(err)
+ }
+ acc := ak.GetAccount(ctx, addr)
+ accountNumbers[i] = acc.GetAccountNumber()
+ sequenceNumbers[i] = acc.GetSequence()
+ }
+
+ var (
+ fees sdk.Coins
+ err error
+ )
+
+ addr, err := sdk.AccAddressFromBech32(msg.Inputs[0].Address)
+ if err != nil {
+ panic(err)
+ }
+
+ // feePayer is the first signer, i.e. first input address
+ feePayer := ak.GetAccount(ctx, addr)
+ spendable := bk.SpendableCoins(ctx, feePayer.GetAddress())
+
+ coins, hasNeg := spendable.SafeSub(msg.Inputs[0].Coins)
+ if !hasNeg {
+ feeCoins := coins.FilterDenoms([]string{sdk.DefaultBondDenom})
+ fees, err = simtypes.RandomFees(r, ctx, feeCoins)
+ if err != nil {
+ return sdk.GasInfo{}, err
+ }
+ }
+
+ txGen := simappparams.MakeTestEncodingConfig().TxConfig
+ tx, err := helpers.GenTx(
+ txGen,
+ []sdk.Msg{msg},
+ fees,
+ helpers.DefaultGenTxGas,
+ chainID,
+ accountNumbers,
+ sequenceNumbers,
+ privkeys...,
+ )
+ if err != nil {
+ return sdk.GasInfo{}, err
+ }
+
+ gasInfo, _, err := app.Deliver(txGen.TxEncoder(), tx)
+ if err != nil {
+ return sdk.GasInfo{}, err
+ }
+
+ return gasInfo, nil
+}
+
// randomSendFields returns the sender and recipient simulation accounts as well
// as the transferred amount.
func randomSendFields(
diff --git a/x/bank/simulation/operations_test.go b/x/bank/simulation/operations_test.go
index f013bfe427e..aeaa9f93890 100644
--- a/x/bank/simulation/operations_test.go
+++ b/x/bank/simulation/operations_test.go
@@ -48,6 +48,7 @@ func (suite *SimTestSuite) TestWeightedOperations() {
opMsgName string
}{
{simappparams.DefaultWeightMsgSend, types.ModuleName, types.TypeMsgSend},
+ {simappparams.DefaultWeightMsgMultiSend, types.ModuleName, types.TypeMsgMultiSend},
}
for i, w := range weightesOps {
@@ -89,6 +90,38 @@ func (suite *SimTestSuite) TestSimulateMsgSend() {
suite.Require().Len(futureOperations, 0)
}
+// TestSimulateMsgSend tests the normal scenario of a valid message of type TypeMsgMultiSend.
+// Abonormal scenarios, where the message is created by an errors, are not tested here.
+func (suite *SimTestSuite) TestSimulateMsgMultiSend() {
+ // setup 3 accounts
+ s := rand.NewSource(1)
+ r := rand.New(s)
+ accounts := suite.getTestingAccounts(r, 3)
+
+ // begin a new block
+ suite.app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: suite.app.LastBlockHeight() + 1, AppHash: suite.app.LastCommitID().Hash}})
+
+ // execute operation
+ op := simulation.SimulateMsgMultiSend(suite.app.AccountKeeper, suite.app.BankKeeper)
+ operationMsg, futureOperations, err := op(r, suite.app.BaseApp, suite.ctx, accounts, "")
+ require := suite.Require()
+ require.NoError(err)
+
+ var msg types.MsgMultiSend
+ types.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg)
+
+ require.True(operationMsg.OK)
+ require.Len(msg.Inputs, 3)
+ require.Equal("cosmos1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7u4x9a0", msg.Inputs[1].Address)
+ require.Equal("185121068stake", msg.Inputs[1].Coins.String())
+ require.Len(msg.Outputs, 2)
+ require.Equal("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.Outputs[1].Address)
+ require.Equal("260469617stake", msg.Outputs[1].Coins.String())
+ require.Equal(types.TypeMsgMultiSend, msg.Type())
+ require.Equal(types.ModuleName, msg.Route())
+ require.Len(futureOperations, 0)
+}
+
func (suite *SimTestSuite) TestSimulateModuleAccountMsgSend() {
const (
accCount = 1
@@ -121,6 +154,35 @@ func (suite *SimTestSuite) TestSimulateModuleAccountMsgSend() {
suite.Require().Len(futureOperations, 0)
}
+func (suite *SimTestSuite) TestSimulateMsgMultiSendToModuleAccount() {
+ const (
+ accCount = 2
+ mAccCount = 2
+ )
+
+ s := rand.NewSource(1)
+ r := rand.New(s)
+ accounts := suite.getTestingAccounts(r, accCount)
+
+ // begin a new block
+ suite.app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: suite.app.LastBlockHeight() + 1, AppHash: suite.app.LastCommitID().Hash}})
+
+ // execute operation
+ op := simulation.SimulateMsgMultiSendToModuleAccount(suite.app.AccountKeeper, suite.app.BankKeeper, mAccCount)
+
+ operationMsg, futureOperations, err := op(r, suite.app.BaseApp, suite.ctx, accounts, "")
+ suite.Require().Error(err)
+
+ var msg types.MsgMultiSend
+ types.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg)
+
+ suite.Require().False(operationMsg.OK) // sending tokens to a module account should fail
+ suite.Require().Equal(operationMsg.Comment, "invalid transfers")
+ suite.Require().Equal(types.TypeMsgMultiSend, msg.Type())
+ suite.Require().Equal(types.ModuleName, msg.Route())
+ suite.Require().Len(futureOperations, 0)
+}
+
func (suite *SimTestSuite) getTestingAccounts(r *rand.Rand, n int) []simtypes.Account {
accounts := simtypes.RandomAccounts(r, n)
diff --git a/x/bank/spec/02_keepers.md b/x/bank/spec/02_keepers.md
index 2c275591b4d..3671819a755 100644
--- a/x/bank/spec/02_keepers.md
+++ b/x/bank/spec/02_keepers.md
@@ -15,7 +15,8 @@ permissions are limited in the way that you expect.
## Blocklisting Addresses
The `x/bank` module accepts a map of addresses that are considered blocklisted
-from directly and explicitly receiving funds through means such as `MsgSend` and direct API calls like `SendCoinsFromModuleToAccount`.
+from directly and explicitly receiving funds through means such as `MsgSend` and
+`MsgMultiSend` and direct API calls like `SendCoinsFromModuleToAccount`.
Typically, these addresses are module accounts. If these addresses receive funds
outside the expected rules of the state machine, invariants are likely to be
@@ -98,6 +99,7 @@ accounts. The send keeper does not alter the total supply (mint or burn coins).
type SendKeeper interface {
ViewKeeper
+ InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
GetParams(ctx sdk.Context) types.Params
diff --git a/x/bank/spec/03_messages.md b/x/bank/spec/03_messages.md
index 3884c01df7d..61f83abf3d8 100644
--- a/x/bank/spec/03_messages.md
+++ b/x/bank/spec/03_messages.md
@@ -13,3 +13,15 @@ The message will fail under the following conditions:
- The coins do not have sending enabled
- The `to` address is restricted
+
+## MsgMultiSend
+
+Send coins from and to a series of different address. If any of the receiving addresses do not correspond to an existing account, a new account is created.
++++ https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L33-L39
+
+The message will fail under the following conditions:
+
+- Any of the coins do not have sending enabled
+- Any of the `to` addresses are restricted
+- Any of the coins are locked
+- The inputs and outputs do not correctly correspond to one another
diff --git a/x/bank/spec/04_events.md b/x/bank/spec/04_events.md
index e9a7f5140e2..e71b820571a 100644
--- a/x/bank/spec/04_events.md
+++ b/x/bank/spec/04_events.md
@@ -18,6 +18,15 @@ The bank module emits the following events:
| message | action | send |
| message | sender | {senderAddress} |
+### MsgMultiSend
+
+| Type | Attribute Key | Attribute Value |
+| -------- | ------------- | ------------------ |
+| transfer | recipient | {recipientAddress} |
+| transfer | amount | {amount} |
+| message | module | bank |
+| message | action | multisend |
+| message | sender | {senderAddress} |
## Keeper events
diff --git a/x/bank/types/bank.pb.go b/x/bank/types/bank.pb.go
index cccddaad6e3..8557be66e56 100644
--- a/x/bank/types/bank.pb.go
+++ b/x/bank/types/bank.pb.go
@@ -131,6 +131,84 @@ func (m *SendEnabled) GetEnabled() bool {
return false
}
+// Input models transaction input.
+type Input struct {
+ Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
+ Coins github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,2,rep,name=coins,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"coins"`
+}
+
+func (m *Input) Reset() { *m = Input{} }
+func (m *Input) String() string { return proto.CompactTextString(m) }
+func (*Input) ProtoMessage() {}
+func (*Input) Descriptor() ([]byte, []int) {
+ return fileDescriptor_dd052eee12edf988, []int{2}
+}
+func (m *Input) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *Input) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_Input.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *Input) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Input.Merge(m, src)
+}
+func (m *Input) XXX_Size() int {
+ return m.Size()
+}
+func (m *Input) XXX_DiscardUnknown() {
+ xxx_messageInfo_Input.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Input proto.InternalMessageInfo
+
+// Output models transaction outputs.
+type Output struct {
+ Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
+ Coins github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,2,rep,name=coins,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"coins"`
+}
+
+func (m *Output) Reset() { *m = Output{} }
+func (m *Output) String() string { return proto.CompactTextString(m) }
+func (*Output) ProtoMessage() {}
+func (*Output) Descriptor() ([]byte, []int) {
+ return fileDescriptor_dd052eee12edf988, []int{3}
+}
+func (m *Output) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *Output) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_Output.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *Output) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Output.Merge(m, src)
+}
+func (m *Output) XXX_Size() int {
+ return m.Size()
+}
+func (m *Output) XXX_DiscardUnknown() {
+ xxx_messageInfo_Output.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Output proto.InternalMessageInfo
+
// Supply represents a struct that passively keeps track of the total supply
// amounts in the network.
// This message is deprecated now that supply is indexed by denom.
@@ -144,7 +222,7 @@ func (m *Supply) Reset() { *m = Supply{} }
func (m *Supply) String() string { return proto.CompactTextString(m) }
func (*Supply) ProtoMessage() {}
func (*Supply) Descriptor() ([]byte, []int) {
- return fileDescriptor_dd052eee12edf988, []int{2}
+ return fileDescriptor_dd052eee12edf988, []int{4}
}
func (m *Supply) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -192,7 +270,7 @@ func (m *DenomUnit) Reset() { *m = DenomUnit{} }
func (m *DenomUnit) String() string { return proto.CompactTextString(m) }
func (*DenomUnit) ProtoMessage() {}
func (*DenomUnit) Descriptor() ([]byte, []int) {
- return fileDescriptor_dd052eee12edf988, []int{3}
+ return fileDescriptor_dd052eee12edf988, []int{5}
}
func (m *DenomUnit) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -268,7 +346,7 @@ func (m *Metadata) Reset() { *m = Metadata{} }
func (m *Metadata) String() string { return proto.CompactTextString(m) }
func (*Metadata) ProtoMessage() {}
func (*Metadata) Descriptor() ([]byte, []int) {
- return fileDescriptor_dd052eee12edf988, []int{4}
+ return fileDescriptor_dd052eee12edf988, []int{6}
}
func (m *Metadata) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -342,6 +420,8 @@ func (m *Metadata) GetSymbol() string {
func init() {
proto.RegisterType((*Params)(nil), "cosmos.bank.v1beta1.Params")
proto.RegisterType((*SendEnabled)(nil), "cosmos.bank.v1beta1.SendEnabled")
+ proto.RegisterType((*Input)(nil), "cosmos.bank.v1beta1.Input")
+ proto.RegisterType((*Output)(nil), "cosmos.bank.v1beta1.Output")
proto.RegisterType((*Supply)(nil), "cosmos.bank.v1beta1.Supply")
proto.RegisterType((*DenomUnit)(nil), "cosmos.bank.v1beta1.DenomUnit")
proto.RegisterType((*Metadata)(nil), "cosmos.bank.v1beta1.Metadata")
@@ -350,42 +430,44 @@ func init() {
func init() { proto.RegisterFile("cosmos/bank/v1beta1/bank.proto", fileDescriptor_dd052eee12edf988) }
var fileDescriptor_dd052eee12edf988 = []byte{
- // 547 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0xbf, 0x6f, 0xd3, 0x40,
- 0x14, 0xce, 0x35, 0x3f, 0x48, 0x2e, 0xb0, 0x1c, 0x11, 0x72, 0x23, 0x61, 0x1b, 0x4b, 0x48, 0x29,
- 0xa2, 0x4e, 0x0a, 0x0c, 0x28, 0x0b, 0x52, 0xca, 0x0f, 0x31, 0x20, 0x21, 0x57, 0x08, 0x09, 0x86,
- 0xe8, 0x1c, 0x5f, 0xc3, 0xa9, 0xbe, 0x3b, 0x2b, 0x77, 0xa9, 0xea, 0xff, 0x80, 0x91, 0x91, 0xb1,
- 0x33, 0x2b, 0xfc, 0x0f, 0x74, 0xac, 0x60, 0x61, 0x0a, 0x28, 0x59, 0x98, 0xfb, 0x17, 0x20, 0xdf,
- 0x39, 0x89, 0x2b, 0x05, 0xc4, 0xe4, 0xf7, 0xbd, 0xf7, 0xbd, 0xef, 0xbd, 0xfb, 0xee, 0x0c, 0xed,
- 0x91, 0x90, 0x4c, 0xc8, 0x6e, 0x88, 0xf9, 0x51, 0xf7, 0x78, 0x2f, 0x24, 0x0a, 0xef, 0x69, 0xe0,
- 0x27, 0x13, 0xa1, 0x04, 0xba, 0x6e, 0xea, 0xbe, 0x4e, 0xe5, 0xf5, 0x76, 0x6b, 0x2c, 0xc6, 0x42,
- 0xd7, 0xbb, 0x59, 0x64, 0xa8, 0xed, 0x6d, 0x43, 0x1d, 0x9a, 0x42, 0xde, 0x67, 0x4a, 0xeb, 0x29,
- 0x92, 0xac, 0xa6, 0x8c, 0x04, 0xe5, 0xa6, 0xee, 0x7d, 0x07, 0xb0, 0xf6, 0x12, 0x4f, 0x30, 0x93,
- 0xe8, 0x10, 0x5e, 0x95, 0x84, 0x47, 0x43, 0xc2, 0x71, 0x18, 0x93, 0xc8, 0x02, 0x6e, 0xb9, 0xd3,
- 0xbc, 0xe7, 0xfa, 0x1b, 0xf6, 0xf0, 0x0f, 0x08, 0x8f, 0x9e, 0x18, 0xde, 0xe0, 0xd6, 0xc5, 0xcc,
- 0xb9, 0x99, 0x62, 0x16, 0xf7, 0xbd, 0x62, 0xff, 0x5d, 0xc1, 0xa8, 0x22, 0x2c, 0x51, 0xa9, 0x17,
- 0x34, 0xe5, 0x9a, 0x8f, 0xde, 0xc2, 0x56, 0x44, 0x0e, 0xf1, 0x34, 0x56, 0xc3, 0x4b, 0xf3, 0xb6,
- 0x5c, 0xd0, 0xa9, 0x0f, 0x76, 0x2e, 0x66, 0xce, 0x6d, 0xa3, 0xb6, 0x89, 0x55, 0x54, 0x45, 0x39,
- 0xa1, 0xb0, 0x4c, 0xbf, 0xf2, 0xf1, 0xd4, 0x29, 0x79, 0xcf, 0x60, 0xb3, 0x90, 0x44, 0x2d, 0x58,
- 0x8d, 0x08, 0x17, 0xcc, 0x02, 0x2e, 0xe8, 0x34, 0x02, 0x03, 0x90, 0x05, 0xaf, 0x5c, 0x1a, 0x1d,
- 0x2c, 0x61, 0xbf, 0x9e, 0x89, 0xfc, 0x3e, 0x75, 0x80, 0xf7, 0x19, 0xc0, 0xda, 0xc1, 0x34, 0x49,
- 0xe2, 0x14, 0x61, 0x58, 0x55, 0x42, 0xe1, 0x38, 0xf7, 0x65, 0x7b, 0xed, 0x8b, 0x24, 0x2b, 0x5f,
- 0xf6, 0x05, 0xe5, 0x83, 0xde, 0xd9, 0xcc, 0x29, 0x7d, 0xfa, 0xe9, 0x74, 0xc6, 0x54, 0xbd, 0x9b,
- 0x86, 0xfe, 0x48, 0xb0, 0xfc, 0x52, 0xf2, 0xcf, 0xae, 0x8c, 0x8e, 0xba, 0x2a, 0x4d, 0x88, 0xd4,
- 0x0d, 0x32, 0x30, 0xca, 0xfd, 0xa7, 0xef, 0xf3, 0xb9, 0xdf, 0xbe, 0xec, 0x3e, 0xbc, 0xf3, 0xcf,
- 0xee, 0x13, 0xf3, 0x6e, 0x62, 0x32, 0xc6, 0xa3, 0xb4, 0x7b, 0xdc, 0x7b, 0xd0, 0xf3, 0xcd, 0x9e,
- 0xcf, 0x2d, 0xe0, 0xbd, 0x86, 0x8d, 0xc7, 0xd9, 0x11, 0x5f, 0x71, 0xaa, 0xfe, 0x72, 0xf8, 0x36,
- 0xac, 0x93, 0x93, 0x44, 0x70, 0xc2, 0x95, 0x3e, 0xfd, 0xb5, 0x60, 0x85, 0x33, 0x63, 0x70, 0x4c,
- 0xb1, 0x24, 0xd2, 0x2a, 0xbb, 0xe5, 0x4e, 0x23, 0x58, 0x42, 0xef, 0x2b, 0x80, 0xf5, 0x17, 0x44,
- 0xe1, 0x08, 0x2b, 0x8c, 0x5c, 0xd8, 0x8c, 0x88, 0x1c, 0x4d, 0x68, 0xa2, 0xa8, 0xe0, 0xb9, 0x7c,
- 0x31, 0x85, 0x1e, 0x65, 0x0c, 0x2e, 0xd8, 0x70, 0xca, 0xa9, 0x92, 0xd6, 0x96, 0x36, 0xce, 0xde,
- 0xf8, 0xa0, 0x56, 0xfb, 0x06, 0x30, 0x5a, 0x86, 0x12, 0x21, 0x58, 0xc9, 0xec, 0xb5, 0xca, 0x5a,
- 0x5b, 0xc7, 0xd9, 0x76, 0x11, 0x95, 0x49, 0x8c, 0x53, 0xab, 0xa2, 0xd3, 0x4b, 0x98, 0xb1, 0x39,
- 0x66, 0xc4, 0xaa, 0x1a, 0x76, 0x16, 0xa3, 0x1b, 0xb0, 0x26, 0x53, 0x16, 0x8a, 0xd8, 0xaa, 0xe9,
- 0x6c, 0x8e, 0x06, 0xfb, 0x67, 0x73, 0x1b, 0x9c, 0xcf, 0x6d, 0xf0, 0x6b, 0x6e, 0x83, 0x0f, 0x0b,
- 0xbb, 0x74, 0xbe, 0xb0, 0x4b, 0x3f, 0x16, 0x76, 0xe9, 0xcd, 0xce, 0xff, 0xf8, 0xae, 0x2f, 0x2f,
- 0xac, 0xe9, 0x7f, 0xe8, 0xfe, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x17, 0x87, 0x70, 0x78, 0xcb,
- 0x03, 0x00, 0x00,
+ // 592 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x54, 0xbf, 0x6f, 0xd3, 0x40,
+ 0x14, 0xf6, 0x35, 0x8d, 0x49, 0x2f, 0xb0, 0x1c, 0x15, 0x72, 0x23, 0x61, 0x1b, 0x4b, 0x48, 0x29,
+ 0xa2, 0x4e, 0x0a, 0x0c, 0x28, 0x0b, 0x52, 0xca, 0x0f, 0x75, 0x40, 0x20, 0x57, 0x08, 0x09, 0x86,
+ 0xe8, 0x9c, 0xbb, 0x06, 0xab, 0xf6, 0x9d, 0x95, 0x3b, 0x57, 0xf5, 0x7f, 0xc0, 0x04, 0x8c, 0x8c,
+ 0x9d, 0x59, 0xe1, 0x7f, 0xa0, 0x63, 0x05, 0x0b, 0x53, 0x40, 0xc9, 0xc2, 0xdc, 0xbf, 0x00, 0xf9,
+ 0xce, 0xf9, 0x51, 0x29, 0x20, 0x06, 0x06, 0xa6, 0xbc, 0xef, 0xbd, 0xef, 0x7d, 0xef, 0xe9, 0xbb,
+ 0xe7, 0x40, 0xbb, 0xcf, 0x45, 0xc2, 0x45, 0x2b, 0xc4, 0xec, 0xa0, 0x75, 0xb8, 0x1d, 0x52, 0x89,
+ 0xb7, 0x15, 0xf0, 0xd3, 0x21, 0x97, 0x1c, 0x5d, 0xd6, 0x75, 0x5f, 0xa5, 0xca, 0x7a, 0x63, 0x7d,
+ 0xc0, 0x07, 0x5c, 0xd5, 0x5b, 0x45, 0xa4, 0xa9, 0x8d, 0x0d, 0x4d, 0xed, 0xe9, 0x42, 0xd9, 0xa7,
+ 0x4b, 0xf3, 0x29, 0x82, 0xce, 0xa6, 0xf4, 0x79, 0xc4, 0x74, 0xdd, 0xfb, 0x0a, 0xa0, 0xf9, 0x14,
+ 0x0f, 0x71, 0x22, 0xd0, 0x3e, 0xbc, 0x28, 0x28, 0x23, 0x3d, 0xca, 0x70, 0x18, 0x53, 0x62, 0x01,
+ 0xb7, 0xd2, 0xac, 0xdf, 0x72, 0xfd, 0x25, 0x7b, 0xf8, 0x7b, 0x94, 0x91, 0x07, 0x9a, 0xd7, 0xbd,
+ 0x76, 0x36, 0x72, 0xae, 0xe6, 0x38, 0x89, 0x3b, 0xde, 0x62, 0xff, 0x4d, 0x9e, 0x44, 0x92, 0x26,
+ 0xa9, 0xcc, 0xbd, 0xa0, 0x2e, 0xe6, 0x7c, 0xf4, 0x12, 0xae, 0x13, 0xba, 0x8f, 0xb3, 0x58, 0xf6,
+ 0xce, 0xcd, 0x5b, 0x71, 0x41, 0xb3, 0xd6, 0xdd, 0x3c, 0x1b, 0x39, 0xd7, 0xb5, 0xda, 0x32, 0xd6,
+ 0xa2, 0x2a, 0x2a, 0x09, 0x0b, 0xcb, 0x74, 0x56, 0xdf, 0x1f, 0x3b, 0x86, 0xf7, 0x08, 0xd6, 0x17,
+ 0x92, 0x68, 0x1d, 0x56, 0x09, 0x65, 0x3c, 0xb1, 0x80, 0x0b, 0x9a, 0x6b, 0x81, 0x06, 0xc8, 0x82,
+ 0x17, 0xce, 0x8d, 0x0e, 0xa6, 0xb0, 0x53, 0x2b, 0x44, 0x7e, 0x1e, 0x3b, 0xc0, 0x7b, 0x03, 0x60,
+ 0x75, 0x97, 0xa5, 0x99, 0x2c, 0xd8, 0x98, 0x90, 0x21, 0x15, 0xa2, 0x54, 0x99, 0x42, 0x84, 0x61,
+ 0xb5, 0x30, 0x54, 0x58, 0x2b, 0xca, 0xb0, 0x8d, 0xb9, 0x61, 0x82, 0xce, 0x0c, 0xdb, 0xe1, 0x11,
+ 0xeb, 0xb6, 0x4f, 0x46, 0x8e, 0xf1, 0xe1, 0xbb, 0xd3, 0x1c, 0x44, 0xf2, 0x55, 0x16, 0xfa, 0x7d,
+ 0x9e, 0x94, 0xaf, 0x55, 0xfe, 0x6c, 0x09, 0x72, 0xd0, 0x92, 0x79, 0x4a, 0x85, 0x6a, 0x10, 0x81,
+ 0x56, 0xee, 0xd4, 0x5e, 0xeb, 0x85, 0x0c, 0xef, 0x2d, 0x80, 0xe6, 0x93, 0x4c, 0xfe, 0x47, 0x1b,
+ 0x7d, 0x04, 0xd0, 0xdc, 0xcb, 0xd2, 0x34, 0xce, 0x8b, 0xb9, 0x92, 0x4b, 0x1c, 0x97, 0xa7, 0xf3,
+ 0x6f, 0xe7, 0x2a, 0xe5, 0xce, 0xc3, 0x72, 0x2e, 0xf8, 0xf2, 0x69, 0xeb, 0xee, 0x8d, 0x3f, 0x76,
+ 0x1f, 0xe9, 0x4f, 0x2b, 0xa6, 0x03, 0xdc, 0xcf, 0x5b, 0x87, 0xed, 0x3b, 0x6d, 0x5f, 0xef, 0xb9,
+ 0x6b, 0x01, 0xef, 0x39, 0x5c, 0xbb, 0x5f, 0x5c, 0xc1, 0x33, 0x16, 0xc9, 0xdf, 0xdc, 0x47, 0x03,
+ 0xd6, 0xe8, 0x51, 0xca, 0x19, 0x65, 0x52, 0x1d, 0xc8, 0xa5, 0x60, 0x86, 0x95, 0xf7, 0x71, 0x84,
+ 0x05, 0x15, 0x56, 0xc5, 0xad, 0x28, 0xef, 0x35, 0xf4, 0x3e, 0x03, 0x58, 0x7b, 0x4c, 0x25, 0x26,
+ 0x58, 0x62, 0xe4, 0xc2, 0x3a, 0xa1, 0xa2, 0x3f, 0x8c, 0x52, 0x19, 0x71, 0x56, 0xca, 0x2f, 0xa6,
+ 0xd0, 0xbd, 0x82, 0xc1, 0x78, 0xd2, 0xcb, 0x58, 0x24, 0xa7, 0x0f, 0x66, 0x2f, 0xfd, 0xe6, 0x66,
+ 0xfb, 0x06, 0x90, 0x4c, 0x43, 0x81, 0x10, 0x5c, 0x2d, 0xec, 0xb5, 0x2a, 0x4a, 0x5b, 0xc5, 0xc5,
+ 0x76, 0x24, 0x12, 0x69, 0x8c, 0x73, 0x6b, 0x55, 0x5f, 0x46, 0x09, 0x0b, 0x36, 0xc3, 0x09, 0xb5,
+ 0xaa, 0x9a, 0x5d, 0xc4, 0xe8, 0x0a, 0x34, 0x45, 0x9e, 0x84, 0x3c, 0xb6, 0x4c, 0x95, 0x2d, 0x51,
+ 0x77, 0xe7, 0x64, 0x6c, 0x83, 0xd3, 0xb1, 0x0d, 0x7e, 0x8c, 0x6d, 0xf0, 0x6e, 0x62, 0x1b, 0xa7,
+ 0x13, 0xdb, 0xf8, 0x36, 0xb1, 0x8d, 0x17, 0x9b, 0x7f, 0xe3, 0xbb, 0x7a, 0xbc, 0xd0, 0x54, 0x7f,
+ 0x33, 0xb7, 0x7f, 0x05, 0x00, 0x00, 0xff, 0xff, 0xcc, 0x03, 0xbf, 0xe9, 0xee, 0x04, 0x00, 0x00,
}
func (this *SendEnabled) Equal(that interface{}) bool {
@@ -531,6 +613,94 @@ func (m *SendEnabled) MarshalToSizedBuffer(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
+func (m *Input) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *Input) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *Input) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if len(m.Coins) > 0 {
+ for iNdEx := len(m.Coins) - 1; iNdEx >= 0; iNdEx-- {
+ {
+ size, err := m.Coins[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+ if err != nil {
+ return 0, err
+ }
+ i -= size
+ i = encodeVarintBank(dAtA, i, uint64(size))
+ }
+ i--
+ dAtA[i] = 0x12
+ }
+ }
+ if len(m.Address) > 0 {
+ i -= len(m.Address)
+ copy(dAtA[i:], m.Address)
+ i = encodeVarintBank(dAtA, i, uint64(len(m.Address)))
+ i--
+ dAtA[i] = 0xa
+ }
+ return len(dAtA) - i, nil
+}
+
+func (m *Output) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *Output) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *Output) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if len(m.Coins) > 0 {
+ for iNdEx := len(m.Coins) - 1; iNdEx >= 0; iNdEx-- {
+ {
+ size, err := m.Coins[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+ if err != nil {
+ return 0, err
+ }
+ i -= size
+ i = encodeVarintBank(dAtA, i, uint64(size))
+ }
+ i--
+ dAtA[i] = 0x12
+ }
+ }
+ if len(m.Address) > 0 {
+ i -= len(m.Address)
+ copy(dAtA[i:], m.Address)
+ i = encodeVarintBank(dAtA, i, uint64(len(m.Address)))
+ i--
+ dAtA[i] = 0xa
+ }
+ return len(dAtA) - i, nil
+}
+
func (m *Supply) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
@@ -729,6 +899,44 @@ func (m *SendEnabled) Size() (n int) {
return n
}
+func (m *Input) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ l = len(m.Address)
+ if l > 0 {
+ n += 1 + l + sovBank(uint64(l))
+ }
+ if len(m.Coins) > 0 {
+ for _, e := range m.Coins {
+ l = e.Size()
+ n += 1 + l + sovBank(uint64(l))
+ }
+ }
+ return n
+}
+
+func (m *Output) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ l = len(m.Address)
+ if l > 0 {
+ n += 1 + l + sovBank(uint64(l))
+ }
+ if len(m.Coins) > 0 {
+ for _, e := range m.Coins {
+ l = e.Size()
+ n += 1 + l + sovBank(uint64(l))
+ }
+ }
+ return n
+}
+
func (m *Supply) Size() (n int) {
if m == nil {
return 0
@@ -1013,6 +1221,238 @@ func (m *SendEnabled) Unmarshal(dAtA []byte) error {
}
return nil
}
+func (m *Input) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowBank
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: Input: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: Input: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 1:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowBank
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return ErrInvalidLengthBank
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex < 0 {
+ return ErrInvalidLengthBank
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Address = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
+ case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Coins", wireType)
+ }
+ var msglen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowBank
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ msglen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if msglen < 0 {
+ return ErrInvalidLengthBank
+ }
+ postIndex := iNdEx + msglen
+ if postIndex < 0 {
+ return ErrInvalidLengthBank
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Coins = append(m.Coins, types.Coin{})
+ if err := m.Coins[len(m.Coins)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+ return err
+ }
+ iNdEx = postIndex
+ default:
+ iNdEx = preIndex
+ skippy, err := skipBank(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if (skippy < 0) || (iNdEx+skippy) < 0 {
+ return ErrInvalidLengthBank
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+func (m *Output) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowBank
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: Output: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: Output: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 1:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowBank
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return ErrInvalidLengthBank
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex < 0 {
+ return ErrInvalidLengthBank
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Address = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
+ case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Coins", wireType)
+ }
+ var msglen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowBank
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ msglen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if msglen < 0 {
+ return ErrInvalidLengthBank
+ }
+ postIndex := iNdEx + msglen
+ if postIndex < 0 {
+ return ErrInvalidLengthBank
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Coins = append(m.Coins, types.Coin{})
+ if err := m.Coins[len(m.Coins)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+ return err
+ }
+ iNdEx = postIndex
+ default:
+ iNdEx = preIndex
+ skippy, err := skipBank(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if (skippy < 0) || (iNdEx+skippy) < 0 {
+ return ErrInvalidLengthBank
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
func (m *Supply) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
diff --git a/x/bank/types/codec.go b/x/bank/types/codec.go
index 3bef66d1b72..a29b4864351 100644
--- a/x/bank/types/codec.go
+++ b/x/bank/types/codec.go
@@ -14,12 +14,14 @@ import (
// on the provided LegacyAmino codec. These types are used for Amino JSON serialization.
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgSend{}, "cosmos-sdk/MsgSend", nil)
+ cdc.RegisterConcrete(&MsgMultiSend{}, "cosmos-sdk/MsgMultiSend", nil)
cdc.RegisterConcrete(&SendAuthorization{}, "cosmos-sdk/SendAuthorization", nil)
}
func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgSend{},
+ &MsgMultiSend{},
)
registry.RegisterImplementations(
(*authz.Authorization)(nil),
diff --git a/x/bank/types/errors.go b/x/bank/types/errors.go
index 91305ade9e8..8446d957b67 100644
--- a/x/bank/types/errors.go
+++ b/x/bank/types/errors.go
@@ -6,7 +6,9 @@ import (
// x/bank module sentinel errors
var (
- // Error codes 2-4 were already assigned and removed, but should not be recycled.
+ ErrNoInputs = sdkerrors.Register(ModuleName, 2, "no inputs to send transaction")
+ ErrNoOutputs = sdkerrors.Register(ModuleName, 3, "no outputs to send transaction")
+ ErrInputOutputMismatch = sdkerrors.Register(ModuleName, 4, "sum inputs != sum outputs")
ErrSendDisabled = sdkerrors.Register(ModuleName, 5, "send transactions are disabled")
ErrDenomMetadataNotFound = sdkerrors.Register(ModuleName, 6, "client denom metadata not found")
ErrInvalidKey = sdkerrors.Register(ModuleName, 7, "invalid key")
diff --git a/x/bank/types/msgs.go b/x/bank/types/msgs.go
index d60d0b2c02c..ae1813aa8d7 100644
--- a/x/bank/types/msgs.go
+++ b/x/bank/types/msgs.go
@@ -8,7 +8,8 @@ import (
// bank message types
const (
- TypeMsgSend = "send"
+ TypeMsgSend = "send"
+ TypeMsgMultiSend = "multisend"
)
var _ sdk.Msg = &MsgSend{}
@@ -61,3 +62,130 @@ func (msg MsgSend) GetSigners() []sdk.AccAddress {
}
return []sdk.AccAddress{from}
}
+
+var _ sdk.Msg = &MsgMultiSend{}
+
+// NewMsgMultiSend - construct arbitrary multi-in, multi-out send msg.
+func NewMsgMultiSend(in []Input, out []Output) *MsgMultiSend {
+ return &MsgMultiSend{Inputs: in, Outputs: out}
+}
+
+// Route Implements Msg
+func (msg MsgMultiSend) Route() string { return RouterKey }
+
+// Type Implements Msg
+func (msg MsgMultiSend) Type() string { return TypeMsgMultiSend }
+
+// ValidateBasic Implements Msg.
+func (msg MsgMultiSend) ValidateBasic() error {
+ // this just makes sure all the inputs and outputs are properly formatted,
+ // not that they actually have the money inside
+ if len(msg.Inputs) == 0 {
+ return ErrNoInputs
+ }
+
+ if len(msg.Outputs) == 0 {
+ return ErrNoOutputs
+ }
+
+ return ValidateInputsOutputs(msg.Inputs, msg.Outputs)
+}
+
+// GetSignBytes Implements Msg.
+func (msg MsgMultiSend) GetSignBytes() []byte {
+ return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg))
+}
+
+// GetSigners Implements Msg.
+func (msg MsgMultiSend) GetSigners() []sdk.AccAddress {
+ addrs := make([]sdk.AccAddress, len(msg.Inputs))
+ for i, in := range msg.Inputs {
+ addr, _ := sdk.AccAddressFromBech32(in.Address)
+ addrs[i] = addr
+ }
+
+ return addrs
+}
+
+// ValidateBasic - validate transaction input
+func (in Input) ValidateBasic() error {
+ _, err := sdk.AccAddressFromBech32(in.Address)
+ if err != nil {
+ return err
+ }
+
+ if !in.Coins.IsValid() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, in.Coins.String())
+ }
+
+ if !in.Coins.IsAllPositive() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, in.Coins.String())
+ }
+
+ return nil
+}
+
+// NewInput - create a transaction input, used with MsgMultiSend
+//nolint:interfacer
+func NewInput(addr sdk.AccAddress, coins sdk.Coins) Input {
+ return Input{
+ Address: addr.String(),
+ Coins: coins,
+ }
+}
+
+// ValidateBasic - validate transaction output
+func (out Output) ValidateBasic() error {
+ _, err := sdk.AccAddressFromBech32(out.Address)
+ if err != nil {
+ return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid output address (%s)", err)
+ }
+
+ if !out.Coins.IsValid() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, out.Coins.String())
+ }
+
+ if !out.Coins.IsAllPositive() {
+ return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, out.Coins.String())
+ }
+
+ return nil
+}
+
+// NewOutput - create a transaction output, used with MsgMultiSend
+//nolint:interfacer
+func NewOutput(addr sdk.AccAddress, coins sdk.Coins) Output {
+ return Output{
+ Address: addr.String(),
+ Coins: coins,
+ }
+}
+
+// ValidateInputsOutputs validates that each respective input and output is
+// valid and that the sum of inputs is equal to the sum of outputs.
+func ValidateInputsOutputs(inputs []Input, outputs []Output) error {
+ var totalIn, totalOut sdk.Coins
+
+ for _, in := range inputs {
+ if err := in.ValidateBasic(); err != nil {
+ return err
+ }
+
+ totalIn = totalIn.Add(in.Coins...)
+ }
+
+ for _, out := range outputs {
+ if err := out.ValidateBasic(); err != nil {
+ return err
+ }
+
+ totalOut = totalOut.Add(out.Coins...)
+ }
+
+ // make sure inputs and outputs match
+ if !totalIn.IsEqual(totalOut) {
+ return ErrInputOutputMismatch
+ }
+
+ return nil
+}
diff --git a/x/bank/types/msgs_test.go b/x/bank/types/msgs_test.go
index 3e0119fdf03..75be4f11857 100644
--- a/x/bank/types/msgs_test.go
+++ b/x/bank/types/msgs_test.go
@@ -71,3 +71,199 @@ func TestMsgSendGetSigners(t *testing.T) {
// TODO: fix this !
require.Equal(t, fmt.Sprintf("%v", res), "[696E707574313131313131313131313131313131]")
}
+
+func TestMsgMultiSendRoute(t *testing.T) {
+ // Construct a MsgSend
+ addr1 := sdk.AccAddress([]byte("input"))
+ addr2 := sdk.AccAddress([]byte("output"))
+ coins := sdk.NewCoins(sdk.NewInt64Coin("atom", 10))
+ var msg = MsgMultiSend{
+ Inputs: []Input{NewInput(addr1, coins)},
+ Outputs: []Output{NewOutput(addr2, coins)},
+ }
+
+ // TODO some failures for bad result
+ require.Equal(t, msg.Route(), RouterKey)
+ require.Equal(t, msg.Type(), "multisend")
+}
+
+func TestInputValidation(t *testing.T) {
+ addr1 := sdk.AccAddress([]byte("_______alice________"))
+ addr2 := sdk.AccAddress([]byte("________bob_________"))
+ addrEmpty := sdk.AccAddress([]byte(""))
+ addrLong := sdk.AccAddress([]byte("Purposefully long address"))
+
+ someCoins := sdk.NewCoins(sdk.NewInt64Coin("atom", 123))
+ multiCoins := sdk.NewCoins(sdk.NewInt64Coin("atom", 123), sdk.NewInt64Coin("eth", 20))
+
+ emptyCoins := sdk.NewCoins()
+ emptyCoins2 := sdk.NewCoins(sdk.NewInt64Coin("eth", 0))
+ someEmptyCoins := sdk.Coins{sdk.NewInt64Coin("eth", 10), sdk.NewInt64Coin("atom", 0)}
+ unsortedCoins := sdk.Coins{sdk.NewInt64Coin("eth", 1), sdk.NewInt64Coin("atom", 1)}
+
+ cases := []struct {
+ expectedErr string // empty means no error expected
+ txIn Input
+ }{
+ // auth works with different apps
+ {"", NewInput(addr1, someCoins)},
+ {"", NewInput(addr2, someCoins)},
+ {"", NewInput(addr2, multiCoins)},
+ {"", NewInput(addrLong, someCoins)},
+
+ {"empty address string is not allowed", NewInput(addrEmpty, someCoins)},
+ {": invalid coins", NewInput(addr1, emptyCoins)}, // invalid coins
+ {": invalid coins", NewInput(addr1, emptyCoins2)}, // invalid coins
+ {"10eth,0atom: invalid coins", NewInput(addr1, someEmptyCoins)}, // invalid coins
+ {"1eth,1atom: invalid coins", NewInput(addr1, unsortedCoins)}, // unsorted coins
+ }
+
+ for i, tc := range cases {
+ err := tc.txIn.ValidateBasic()
+ if tc.expectedErr == "" {
+ require.Nil(t, err, "%d: %+v", i, err)
+ } else {
+ require.EqualError(t, err, tc.expectedErr, "%d", i)
+ }
+ }
+}
+
+func TestOutputValidation(t *testing.T) {
+ addr1 := sdk.AccAddress([]byte("_______alice________"))
+ addr2 := sdk.AccAddress([]byte("________bob_________"))
+ addrEmpty := sdk.AccAddress([]byte(""))
+ addrLong := sdk.AccAddress([]byte("Purposefully long address"))
+
+ someCoins := sdk.NewCoins(sdk.NewInt64Coin("atom", 123))
+ multiCoins := sdk.NewCoins(sdk.NewInt64Coin("atom", 123), sdk.NewInt64Coin("eth", 20))
+
+ emptyCoins := sdk.NewCoins()
+ emptyCoins2 := sdk.NewCoins(sdk.NewInt64Coin("eth", 0))
+ someEmptyCoins := sdk.Coins{sdk.NewInt64Coin("eth", 10), sdk.NewInt64Coin("atom", 0)}
+ unsortedCoins := sdk.Coins{sdk.NewInt64Coin("eth", 1), sdk.NewInt64Coin("atom", 1)}
+
+ cases := []struct {
+ expectedErr string // empty means no error expected
+ txOut Output
+ }{
+ // auth works with different apps
+ {"", NewOutput(addr1, someCoins)},
+ {"", NewOutput(addr2, someCoins)},
+ {"", NewOutput(addr2, multiCoins)},
+ {"", NewOutput(addrLong, someCoins)},
+
+ {"Invalid output address (empty address string is not allowed): invalid address", NewOutput(addrEmpty, someCoins)},
+ {": invalid coins", NewOutput(addr1, emptyCoins)}, // invalid coins
+ {": invalid coins", NewOutput(addr1, emptyCoins2)}, // invalid coins
+ {"10eth,0atom: invalid coins", NewOutput(addr1, someEmptyCoins)}, // invalid coins
+ {"1eth,1atom: invalid coins", NewOutput(addr1, unsortedCoins)}, // unsorted coins
+ }
+
+ for i, tc := range cases {
+ err := tc.txOut.ValidateBasic()
+ if tc.expectedErr == "" {
+ require.Nil(t, err, "%d: %+v", i, err)
+ } else {
+ require.EqualError(t, err, tc.expectedErr, "%d", i)
+ }
+ }
+}
+
+func TestMsgMultiSendValidation(t *testing.T) {
+ addr1 := sdk.AccAddress([]byte("_______alice________"))
+ addr2 := sdk.AccAddress([]byte("________bob_________"))
+ atom123 := sdk.NewCoins(sdk.NewInt64Coin("atom", 123))
+ atom124 := sdk.NewCoins(sdk.NewInt64Coin("atom", 124))
+ eth123 := sdk.NewCoins(sdk.NewInt64Coin("eth", 123))
+ atom123eth123 := sdk.NewCoins(sdk.NewInt64Coin("atom", 123), sdk.NewInt64Coin("eth", 123))
+
+ input1 := NewInput(addr1, atom123)
+ input2 := NewInput(addr1, eth123)
+ output1 := NewOutput(addr2, atom123)
+ output2 := NewOutput(addr2, atom124)
+ outputMulti := NewOutput(addr2, atom123eth123)
+
+ var emptyAddr sdk.AccAddress
+
+ cases := []struct {
+ valid bool
+ tx MsgMultiSend
+ }{
+ {false, MsgMultiSend{}}, // no input or output
+ {false, MsgMultiSend{Inputs: []Input{input1}}}, // just input
+ {false, MsgMultiSend{Outputs: []Output{output1}}}, // just output
+ {false, MsgMultiSend{
+ Inputs: []Input{NewInput(emptyAddr, atom123)}, // invalid input
+ Outputs: []Output{output1}}},
+ {false, MsgMultiSend{
+ Inputs: []Input{input1},
+ Outputs: []Output{{emptyAddr.String(), atom123}}}, // invalid output
+ },
+ {false, MsgMultiSend{
+ Inputs: []Input{input1},
+ Outputs: []Output{output2}}, // amounts dont match
+ },
+ {true, MsgMultiSend{
+ Inputs: []Input{input1},
+ Outputs: []Output{output1}},
+ },
+ {true, MsgMultiSend{
+ Inputs: []Input{input1, input2},
+ Outputs: []Output{outputMulti}},
+ },
+ }
+
+ for i, tc := range cases {
+ err := tc.tx.ValidateBasic()
+ if tc.valid {
+ require.Nil(t, err, "%d: %+v", i, err)
+ } else {
+ require.NotNil(t, err, "%d", i)
+ }
+ }
+}
+
+func TestMsgMultiSendGetSignBytes(t *testing.T) {
+ addr1 := sdk.AccAddress([]byte("input"))
+ addr2 := sdk.AccAddress([]byte("output"))
+ coins := sdk.NewCoins(sdk.NewInt64Coin("atom", 10))
+ var msg = MsgMultiSend{
+ Inputs: []Input{NewInput(addr1, coins)},
+ Outputs: []Output{NewOutput(addr2, coins)},
+ }
+ res := msg.GetSignBytes()
+
+ expected := `{"type":"cosmos-sdk/MsgMultiSend","value":{"inputs":[{"address":"cosmos1d9h8qat57ljhcm","coins":[{"amount":"10","denom":"atom"}]}],"outputs":[{"address":"cosmos1da6hgur4wsmpnjyg","coins":[{"amount":"10","denom":"atom"}]}]}}`
+ require.Equal(t, expected, string(res))
+}
+
+func TestMsgMultiSendGetSigners(t *testing.T) {
+ var msg = MsgMultiSend{
+ Inputs: []Input{
+ NewInput(sdk.AccAddress([]byte("input111111111111111")), nil),
+ NewInput(sdk.AccAddress([]byte("input222222222222222")), nil),
+ NewInput(sdk.AccAddress([]byte("input333333333333333")), nil),
+ },
+ }
+
+ res := msg.GetSigners()
+ // TODO: fix this !
+ require.Equal(t, "[696E707574313131313131313131313131313131 696E707574323232323232323232323232323232 696E707574333333333333333333333333333333]", fmt.Sprintf("%v", res))
+}
+
+func TestMsgSendSigners(t *testing.T) {
+ signers := []sdk.AccAddress{
+ {1, 2, 3},
+ {4, 5, 6},
+ {7, 8, 9},
+ }
+
+ someCoins := sdk.NewCoins(sdk.NewInt64Coin("atom", 123))
+ inputs := make([]Input, len(signers))
+ for i, signer := range signers {
+ inputs[i] = NewInput(signer, someCoins)
+ }
+ tx := NewMsgMultiSend(inputs, nil)
+
+ require.Equal(t, signers, tx.GetSigners())
+}
diff --git a/x/bank/types/tx.pb.go b/x/bank/types/tx.pb.go
index 94db3dbb5d1..6dd52c4b64d 100644
--- a/x/bank/types/tx.pb.go
+++ b/x/bank/types/tx.pb.go
@@ -107,37 +107,135 @@ func (m *MsgSendResponse) XXX_DiscardUnknown() {
var xxx_messageInfo_MsgSendResponse proto.InternalMessageInfo
+// MsgMultiSend represents an arbitrary multi-in, multi-out send message.
+type MsgMultiSend struct {
+ Inputs []Input `protobuf:"bytes,1,rep,name=inputs,proto3" json:"inputs"`
+ Outputs []Output `protobuf:"bytes,2,rep,name=outputs,proto3" json:"outputs"`
+}
+
+func (m *MsgMultiSend) Reset() { *m = MsgMultiSend{} }
+func (m *MsgMultiSend) String() string { return proto.CompactTextString(m) }
+func (*MsgMultiSend) ProtoMessage() {}
+func (*MsgMultiSend) Descriptor() ([]byte, []int) {
+ return fileDescriptor_1d8cb1613481f5b7, []int{2}
+}
+func (m *MsgMultiSend) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *MsgMultiSend) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_MsgMultiSend.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *MsgMultiSend) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_MsgMultiSend.Merge(m, src)
+}
+func (m *MsgMultiSend) XXX_Size() int {
+ return m.Size()
+}
+func (m *MsgMultiSend) XXX_DiscardUnknown() {
+ xxx_messageInfo_MsgMultiSend.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_MsgMultiSend proto.InternalMessageInfo
+
+func (m *MsgMultiSend) GetInputs() []Input {
+ if m != nil {
+ return m.Inputs
+ }
+ return nil
+}
+
+func (m *MsgMultiSend) GetOutputs() []Output {
+ if m != nil {
+ return m.Outputs
+ }
+ return nil
+}
+
+// MsgMultiSendResponse defines the Msg/MultiSend response type.
+type MsgMultiSendResponse struct {
+}
+
+func (m *MsgMultiSendResponse) Reset() { *m = MsgMultiSendResponse{} }
+func (m *MsgMultiSendResponse) String() string { return proto.CompactTextString(m) }
+func (*MsgMultiSendResponse) ProtoMessage() {}
+func (*MsgMultiSendResponse) Descriptor() ([]byte, []int) {
+ return fileDescriptor_1d8cb1613481f5b7, []int{3}
+}
+func (m *MsgMultiSendResponse) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *MsgMultiSendResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_MsgMultiSendResponse.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *MsgMultiSendResponse) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_MsgMultiSendResponse.Merge(m, src)
+}
+func (m *MsgMultiSendResponse) XXX_Size() int {
+ return m.Size()
+}
+func (m *MsgMultiSendResponse) XXX_DiscardUnknown() {
+ xxx_messageInfo_MsgMultiSendResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_MsgMultiSendResponse proto.InternalMessageInfo
+
func init() {
proto.RegisterType((*MsgSend)(nil), "cosmos.bank.v1beta1.MsgSend")
proto.RegisterType((*MsgSendResponse)(nil), "cosmos.bank.v1beta1.MsgSendResponse")
+ proto.RegisterType((*MsgMultiSend)(nil), "cosmos.bank.v1beta1.MsgMultiSend")
+ proto.RegisterType((*MsgMultiSendResponse)(nil), "cosmos.bank.v1beta1.MsgMultiSendResponse")
}
func init() { proto.RegisterFile("cosmos/bank/v1beta1/tx.proto", fileDescriptor_1d8cb1613481f5b7) }
var fileDescriptor_1d8cb1613481f5b7 = []byte{
- // 346 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x49, 0xce, 0x2f, 0xce,
- 0xcd, 0x2f, 0xd6, 0x4f, 0x4a, 0xcc, 0xcb, 0xd6, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4,
- 0x2f, 0xa9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x86, 0xc8, 0xea, 0x81, 0x64, 0xf5,
- 0xa0, 0xb2, 0x52, 0x22, 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0x79, 0x7d, 0x10, 0x0b, 0xa2, 0x54, 0x4a,
- 0x0e, 0x6e, 0x50, 0x71, 0x2a, 0xdc, 0xa0, 0xe4, 0xfc, 0xcc, 0x3c, 0x0c, 0x79, 0x24, 0x8b, 0xc0,
- 0xe6, 0x82, 0xe5, 0x95, 0x5e, 0x31, 0x72, 0xb1, 0xfb, 0x16, 0xa7, 0x07, 0xa7, 0xe6, 0xa5, 0x08,
- 0x59, 0x71, 0xf1, 0xa4, 0x15, 0xe5, 0xe7, 0xc6, 0x27, 0xa6, 0xa4, 0x14, 0xa5, 0x16, 0x17, 0x4b,
- 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x3a, 0x89, 0x7f, 0xba, 0x27, 0x2f, 0x5c, 0x99, 0x98, 0x9b, 0x63,
- 0xa5, 0x84, 0x2c, 0xab, 0x14, 0xc4, 0x0d, 0xe2, 0x3a, 0x42, 0x78, 0x42, 0x26, 0x5c, 0x5c, 0x25,
- 0xf9, 0x70, 0x9d, 0x4c, 0x60, 0x9d, 0xa2, 0x9f, 0xee, 0xc9, 0x0b, 0x42, 0x74, 0x22, 0xe4, 0x94,
- 0x82, 0x38, 0x4b, 0xf2, 0x61, 0xba, 0x92, 0xb9, 0xd8, 0x12, 0x73, 0xf3, 0x4b, 0xf3, 0x4a, 0x24,
- 0x98, 0x15, 0x98, 0x35, 0xb8, 0x8d, 0x24, 0xf5, 0xe0, 0x3e, 0x2f, 0x4e, 0x85, 0xf9, 0x5c, 0xcf,
- 0x39, 0x3f, 0x33, 0xcf, 0xc9, 0xe0, 0xc4, 0x3d, 0x79, 0x86, 0x55, 0xf7, 0xe5, 0x35, 0xd2, 0x33,
- 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xa1, 0x7e, 0x83, 0x50, 0xba, 0xc5, 0x29,
- 0xd9, 0xfa, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x60, 0x0d, 0xc5, 0x41, 0x50, 0xa3, 0xad, 0x38, 0x3a,
- 0x16, 0xc8, 0x33, 0xbc, 0x58, 0x20, 0xcf, 0xa0, 0x24, 0xc8, 0xc5, 0x0f, 0xf5, 0x6b, 0x50, 0x6a,
- 0x71, 0x41, 0x7e, 0x5e, 0x71, 0xaa, 0x51, 0x20, 0x17, 0xb3, 0x6f, 0x71, 0xba, 0x90, 0x17, 0x17,
- 0x0b, 0x38, 0x08, 0x64, 0xf4, 0xb0, 0x04, 0xbd, 0x1e, 0x54, 0x93, 0x94, 0x0a, 0x3e, 0x59, 0x98,
- 0x91, 0x4e, 0xce, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3,
- 0x84, 0xc7, 0x72, 0x0c, 0x17, 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x10, 0xa5, 0x89, 0xd7,
- 0xed, 0x15, 0x90, 0x48, 0x02, 0x7b, 0x21, 0x89, 0x0d, 0x1c, 0x3d, 0xc6, 0x80, 0x00, 0x00, 0x00,
- 0xff, 0xff, 0x8f, 0x4a, 0xcb, 0x2c, 0x29, 0x02, 0x00, 0x00,
+ // 436 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0x3f, 0xef, 0xd2, 0x40,
+ 0x1c, 0xc6, 0x7b, 0x3f, 0x08, 0x3f, 0x39, 0x48, 0x0c, 0x05, 0x15, 0x2b, 0x69, 0xb1, 0x71, 0x80,
+ 0xc1, 0xab, 0xa0, 0x83, 0xa9, 0x93, 0x65, 0xd2, 0xa4, 0x31, 0xa9, 0x93, 0x2e, 0xa6, 0x7f, 0xce,
+ 0xda, 0x40, 0x7b, 0x0d, 0x77, 0x35, 0xf0, 0x0e, 0x4c, 0x5c, 0x7c, 0x09, 0xcc, 0xc6, 0x17, 0xc2,
+ 0xc8, 0xe8, 0x84, 0x06, 0x16, 0xe3, 0xc8, 0x2b, 0x30, 0xbd, 0xfe, 0x81, 0x44, 0xc4, 0xa9, 0xbd,
+ 0x3c, 0xdf, 0xcf, 0xd3, 0xe7, 0xe9, 0xf7, 0x60, 0xcf, 0x25, 0x34, 0x24, 0x54, 0x73, 0xec, 0x68,
+ 0xaa, 0x7d, 0x1c, 0x39, 0x98, 0xd9, 0x23, 0x8d, 0x2d, 0x50, 0x3c, 0x27, 0x8c, 0x88, 0xed, 0x4c,
+ 0x45, 0xa9, 0x8a, 0x72, 0x55, 0xea, 0xf8, 0xc4, 0x27, 0x5c, 0xd7, 0xd2, 0xb7, 0x6c, 0x54, 0x92,
+ 0x4b, 0x23, 0x8a, 0x4b, 0x23, 0x97, 0x04, 0xd1, 0x5f, 0xfa, 0xc9, 0x87, 0xb8, 0x2f, 0xd7, 0xd5,
+ 0xdf, 0x00, 0x5e, 0x9b, 0xd4, 0x7f, 0x8d, 0x23, 0x4f, 0xd4, 0x61, 0xf3, 0xfd, 0x9c, 0x84, 0xef,
+ 0x6c, 0xcf, 0x9b, 0x63, 0x4a, 0xbb, 0xa0, 0x0f, 0x06, 0x75, 0xe3, 0xce, 0x61, 0xab, 0xb4, 0x97,
+ 0x76, 0x38, 0xd3, 0xd5, 0x53, 0x55, 0xb5, 0x1a, 0xe9, 0xf1, 0x79, 0x76, 0x12, 0x9f, 0x40, 0xc8,
+ 0x48, 0x49, 0x5e, 0x71, 0xf2, 0xd6, 0x61, 0xab, 0xb4, 0x32, 0xf2, 0xa8, 0xa9, 0x56, 0x9d, 0x91,
+ 0x82, 0x72, 0x61, 0xcd, 0x0e, 0x49, 0x12, 0xb1, 0x6e, 0xa5, 0x5f, 0x19, 0x34, 0xc6, 0x77, 0x51,
+ 0xd9, 0x9c, 0xe2, 0xa2, 0x39, 0x9a, 0x90, 0x20, 0x32, 0x1e, 0xad, 0xb7, 0x8a, 0xf0, 0xf5, 0x87,
+ 0x32, 0xf0, 0x03, 0xf6, 0x21, 0x71, 0x90, 0x4b, 0x42, 0x2d, 0xef, 0x96, 0x3d, 0x1e, 0x52, 0x6f,
+ 0xaa, 0xb1, 0x65, 0x8c, 0x29, 0x07, 0xa8, 0x95, 0x5b, 0xeb, 0x37, 0x3e, 0xad, 0x14, 0xe1, 0xd7,
+ 0x4a, 0x11, 0xd4, 0x16, 0xbc, 0x99, 0x77, 0xb5, 0x30, 0x8d, 0x49, 0x44, 0xb1, 0xfa, 0x19, 0xc0,
+ 0xa6, 0x49, 0x7d, 0x33, 0x99, 0xb1, 0x80, 0xff, 0x84, 0xa7, 0xb0, 0x16, 0x44, 0x71, 0xc2, 0xd2,
+ 0xfa, 0x69, 0x24, 0x09, 0x9d, 0x59, 0x06, 0x7a, 0x91, 0x8e, 0x18, 0xd5, 0x34, 0x93, 0x95, 0xcf,
+ 0x8b, 0xcf, 0xe0, 0x35, 0x49, 0x18, 0x47, 0xaf, 0x38, 0x7a, 0xef, 0x2c, 0xfa, 0x8a, 0xcf, 0xe4,
+ 0x6c, 0x41, 0xe8, 0x55, 0x1e, 0xf0, 0x36, 0xec, 0x9c, 0x86, 0x29, 0x52, 0x8e, 0xbf, 0x01, 0x58,
+ 0x31, 0xa9, 0x2f, 0xbe, 0x84, 0x55, 0x1e, 0xb2, 0x77, 0xd6, 0x39, 0xef, 0x26, 0x3d, 0xb8, 0xa4,
+ 0x16, 0x9e, 0xe2, 0x1b, 0x58, 0x3f, 0xb6, 0xbe, 0xff, 0x2f, 0xa4, 0x1c, 0x91, 0x86, 0xff, 0x1d,
+ 0x29, 0xac, 0x8d, 0xc9, 0x7a, 0x27, 0x83, 0xcd, 0x4e, 0x06, 0x3f, 0x77, 0x32, 0xf8, 0xb2, 0x97,
+ 0x85, 0xcd, 0x5e, 0x16, 0xbe, 0xef, 0x65, 0xe1, 0xed, 0xf0, 0xe2, 0xf6, 0x16, 0xd9, 0x35, 0xe5,
+ 0x4b, 0x74, 0x6a, 0xfc, 0x82, 0x3e, 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x78, 0xdc, 0x7c, 0x0b,
+ 0x2b, 0x03, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@@ -154,6 +252,8 @@ const _ = grpc.SupportPackageIsVersion4
type MsgClient interface {
// Send defines a method for sending coins from one account to another account.
Send(ctx context.Context, in *MsgSend, opts ...grpc.CallOption) (*MsgSendResponse, error)
+ // MultiSend defines a method for sending coins from some accounts to other accounts.
+ MultiSend(ctx context.Context, in *MsgMultiSend, opts ...grpc.CallOption) (*MsgMultiSendResponse, error)
}
type msgClient struct {
@@ -173,10 +273,21 @@ func (c *msgClient) Send(ctx context.Context, in *MsgSend, opts ...grpc.CallOpti
return out, nil
}
+func (c *msgClient) MultiSend(ctx context.Context, in *MsgMultiSend, opts ...grpc.CallOption) (*MsgMultiSendResponse, error) {
+ out := new(MsgMultiSendResponse)
+ err := c.cc.Invoke(ctx, "/cosmos.bank.v1beta1.Msg/MultiSend", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
// MsgServer is the server API for Msg service.
type MsgServer interface {
// Send defines a method for sending coins from one account to another account.
Send(context.Context, *MsgSend) (*MsgSendResponse, error)
+ // MultiSend defines a method for sending coins from some accounts to other accounts.
+ MultiSend(context.Context, *MsgMultiSend) (*MsgMultiSendResponse, error)
}
// UnimplementedMsgServer can be embedded to have forward compatible implementations.
@@ -186,6 +297,9 @@ type UnimplementedMsgServer struct {
func (*UnimplementedMsgServer) Send(ctx context.Context, req *MsgSend) (*MsgSendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
}
+func (*UnimplementedMsgServer) MultiSend(ctx context.Context, req *MsgMultiSend) (*MsgMultiSendResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method MultiSend not implemented")
+}
func RegisterMsgServer(s grpc1.Server, srv MsgServer) {
s.RegisterService(&_Msg_serviceDesc, srv)
@@ -209,6 +323,24 @@ func _Msg_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{
return interceptor(ctx, in, info, handler)
}
+func _Msg_MultiSend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(MsgMultiSend)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MsgServer).MultiSend(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/cosmos.bank.v1beta1.Msg/MultiSend",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MsgServer).MultiSend(ctx, req.(*MsgMultiSend))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
var _Msg_serviceDesc = grpc.ServiceDesc{
ServiceName: "cosmos.bank.v1beta1.Msg",
HandlerType: (*MsgServer)(nil),
@@ -217,6 +349,10 @@ var _Msg_serviceDesc = grpc.ServiceDesc{
MethodName: "Send",
Handler: _Msg_Send_Handler,
},
+ {
+ MethodName: "MultiSend",
+ Handler: _Msg_MultiSend_Handler,
+ },
},
Streams: []grpc.StreamDesc{},
Metadata: "cosmos/bank/v1beta1/tx.proto",
@@ -296,6 +432,80 @@ func (m *MsgSendResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
+func (m *MsgMultiSend) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *MsgMultiSend) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *MsgMultiSend) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if len(m.Outputs) > 0 {
+ for iNdEx := len(m.Outputs) - 1; iNdEx >= 0; iNdEx-- {
+ {
+ size, err := m.Outputs[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+ if err != nil {
+ return 0, err
+ }
+ i -= size
+ i = encodeVarintTx(dAtA, i, uint64(size))
+ }
+ i--
+ dAtA[i] = 0x12
+ }
+ }
+ if len(m.Inputs) > 0 {
+ for iNdEx := len(m.Inputs) - 1; iNdEx >= 0; iNdEx-- {
+ {
+ size, err := m.Inputs[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+ if err != nil {
+ return 0, err
+ }
+ i -= size
+ i = encodeVarintTx(dAtA, i, uint64(size))
+ }
+ i--
+ dAtA[i] = 0xa
+ }
+ }
+ return len(dAtA) - i, nil
+}
+
+func (m *MsgMultiSendResponse) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *MsgMultiSendResponse) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *MsgMultiSendResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ return len(dAtA) - i, nil
+}
+
func encodeVarintTx(dAtA []byte, offset int, v uint64) int {
offset -= sovTx(v)
base := offset
@@ -339,6 +549,36 @@ func (m *MsgSendResponse) Size() (n int) {
return n
}
+func (m *MsgMultiSend) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ if len(m.Inputs) > 0 {
+ for _, e := range m.Inputs {
+ l = e.Size()
+ n += 1 + l + sovTx(uint64(l))
+ }
+ }
+ if len(m.Outputs) > 0 {
+ for _, e := range m.Outputs {
+ l = e.Size()
+ n += 1 + l + sovTx(uint64(l))
+ }
+ }
+ return n
+}
+
+func (m *MsgMultiSendResponse) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ return n
+}
+
func sovTx(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
@@ -543,6 +783,174 @@ func (m *MsgSendResponse) Unmarshal(dAtA []byte) error {
}
return nil
}
+func (m *MsgMultiSend) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowTx
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: MsgMultiSend: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: MsgMultiSend: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 1:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Inputs", wireType)
+ }
+ var msglen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowTx
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ msglen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if msglen < 0 {
+ return ErrInvalidLengthTx
+ }
+ postIndex := iNdEx + msglen
+ if postIndex < 0 {
+ return ErrInvalidLengthTx
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Inputs = append(m.Inputs, Input{})
+ if err := m.Inputs[len(m.Inputs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+ return err
+ }
+ iNdEx = postIndex
+ case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Outputs", wireType)
+ }
+ var msglen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowTx
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ msglen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if msglen < 0 {
+ return ErrInvalidLengthTx
+ }
+ postIndex := iNdEx + msglen
+ if postIndex < 0 {
+ return ErrInvalidLengthTx
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Outputs = append(m.Outputs, Output{})
+ if err := m.Outputs[len(m.Outputs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+ return err
+ }
+ iNdEx = postIndex
+ default:
+ iNdEx = preIndex
+ skippy, err := skipTx(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if (skippy < 0) || (iNdEx+skippy) < 0 {
+ return ErrInvalidLengthTx
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+func (m *MsgMultiSendResponse) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowTx
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: MsgMultiSendResponse: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: MsgMultiSendResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ default:
+ iNdEx = preIndex
+ skippy, err := skipTx(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if (skippy < 0) || (iNdEx+skippy) < 0 {
+ return ErrInvalidLengthTx
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
func skipTx(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0