feat: implement multi-send transaction command (#11738)
Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com> Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>
This commit is contained in:
parent
0c0b4da114
commit
6a9b8247f6
|
@ -39,6 +39,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
* (cli) [\#11738](https://github.com/cosmos/cosmos-sdk/pull/11738) Add `tx auth multi-sign` as alias of `tx auth multisign` for consistency with `multi-send`.
|
||||||
|
* (cli) [\#11738](https://github.com/cosmos/cosmos-sdk/pull/11738) Add `tx bank multi-send` command for bulk send of coins to multiple accounts.
|
||||||
* (grpc) [\#11642](https://github.com/cosmos/cosmos-sdk/pull/11642) Implement `ABCIQuery` in the Tendermint gRPC service, which proxies ABCI `Query` requests directly to the application.
|
* (grpc) [\#11642](https://github.com/cosmos/cosmos-sdk/pull/11642) Implement `ABCIQuery` in the Tendermint gRPC service, which proxies ABCI `Query` requests directly to the application.
|
||||||
* (x/upgrade) [\#11551](https://github.com/cosmos/cosmos-sdk/pull/11551) Update `ScheduleUpgrade` for chains to schedule an automated upgrade on `BeginBlock` without having to go though governance.
|
* (x/upgrade) [\#11551](https://github.com/cosmos/cosmos-sdk/pull/11551) Update `ScheduleUpgrade` for chains to schedule an automated upgrade on `BeginBlock` without having to go though governance.
|
||||||
* (cli) [\#11548](https://github.com/cosmos/cosmos-sdk/pull/11548) Add Tendermint's `inspect` command to the `tendermint` sub-command.
|
* (cli) [\#11548](https://github.com/cosmos/cosmos-sdk/pull/11548) Add Tendermint's `inspect` command to the `tendermint` sub-command.
|
||||||
|
|
|
@ -410,6 +410,71 @@ func (coins Coins) SafeSub(coinsB ...Coin) (Coins, bool) {
|
||||||
return diff, diff.IsAnyNegative()
|
return diff, diff.IsAnyNegative()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MulInt performs the scalar multiplication of coins with a `multiplier`
|
||||||
|
// All coins are multipled by x
|
||||||
|
// e.g.
|
||||||
|
// {2A, 3B} * 2 = {4A, 6B}
|
||||||
|
// {2A} * 0 panics
|
||||||
|
// Note, if IsValid was true on Coins, IsValid stays true.
|
||||||
|
func (coins Coins) MulInt(x Int) Coins {
|
||||||
|
coins, ok := coins.SafeMulInt(x)
|
||||||
|
if !ok {
|
||||||
|
panic("multiplying by zero is an invalid operation on coins")
|
||||||
|
}
|
||||||
|
|
||||||
|
return coins
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeMulInt performs the same arithmetic as MulInt but returns false
|
||||||
|
// if the `multiplier` is zero because it makes IsValid return false.
|
||||||
|
func (coins Coins) SafeMulInt(x Int) (Coins, bool) {
|
||||||
|
if x.IsZero() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make(Coins, len(coins))
|
||||||
|
for i, coin := range coins {
|
||||||
|
coin := coin
|
||||||
|
res[i] = NewCoin(coin.Denom, coin.Amount.Mul(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuoInt performs the scalar division of coins with a `divisor`
|
||||||
|
// All coins are divided by x and trucated.
|
||||||
|
// e.g.
|
||||||
|
// {2A, 30B} / 2 = {1A, 15B}
|
||||||
|
// {2A} / 2 = {1A}
|
||||||
|
// {4A} / {8A} = {0A}
|
||||||
|
// {2A} / 0 = panics
|
||||||
|
// Note, if IsValid was true on Coins, IsValid stays true,
|
||||||
|
// unless the `divisor` is greater than the smallest coin amount.
|
||||||
|
func (coins Coins) QuoInt(x Int) Coins {
|
||||||
|
coins, ok := coins.SafeQuoInt(x)
|
||||||
|
if !ok {
|
||||||
|
panic("dividing by zero is an invalid operation on coins")
|
||||||
|
}
|
||||||
|
|
||||||
|
return coins
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeQuoInt performs the same arithmetic as QuoInt but returns an error
|
||||||
|
// if the division cannot be done.
|
||||||
|
func (coins Coins) SafeQuoInt(x Int) (Coins, bool) {
|
||||||
|
if x.IsZero() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var res Coins
|
||||||
|
for _, coin := range coins {
|
||||||
|
coin := coin
|
||||||
|
res = append(res, NewCoin(coin.Denom, coin.Amount.Quo(x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, true
|
||||||
|
}
|
||||||
|
|
||||||
// Max takes two valid Coins inputs and returns a valid Coins result
|
// Max takes two valid Coins inputs and returns a valid Coins result
|
||||||
// where for every denom D, AmountOf(D) of the result is the maximum
|
// where for every denom D, AmountOf(D) of the result is the maximum
|
||||||
// of AmountOf(D) of the inputs. Note that the result might be not
|
// of AmountOf(D) of the inputs. Note that the result might be not
|
||||||
|
|
|
@ -18,7 +18,7 @@ var (
|
||||||
|
|
||||||
type coinTestSuite struct {
|
type coinTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
ca0, ca1, ca2, cm0, cm1, cm2 sdk.Coin
|
ca0, ca1, ca2, ca4, cm0, cm1, cm2, cm4 sdk.Coin
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoinTestSuite(t *testing.T) {
|
func TestCoinTestSuite(t *testing.T) {
|
||||||
|
@ -30,8 +30,10 @@ func (s *coinTestSuite) SetupSuite() {
|
||||||
zero := sdk.NewInt(0)
|
zero := sdk.NewInt(0)
|
||||||
one := sdk.OneInt()
|
one := sdk.OneInt()
|
||||||
two := sdk.NewInt(2)
|
two := sdk.NewInt(2)
|
||||||
s.ca0, s.ca1, s.ca2 = sdk.Coin{testDenom1, zero}, sdk.Coin{testDenom1, one}, sdk.Coin{testDenom1, two}
|
four := sdk.NewInt(4)
|
||||||
s.cm0, s.cm1, s.cm2 = sdk.Coin{testDenom2, zero}, sdk.Coin{testDenom2, one}, sdk.Coin{testDenom2, two}
|
|
||||||
|
s.ca0, s.ca1, s.ca2, s.ca4 = sdk.NewCoin(testDenom1, zero), sdk.NewCoin(testDenom1, one), sdk.NewCoin(testDenom1, two), sdk.NewCoin(testDenom1, four)
|
||||||
|
s.cm0, s.cm1, s.cm2, s.cm4 = sdk.NewCoin(testDenom2, zero), sdk.NewCoin(testDenom2, one), sdk.NewCoin(testDenom2, two), sdk.NewCoin(testDenom2, four)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -224,6 +226,58 @@ func (s *coinTestSuite) TestSubCoinAmount() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *coinTestSuite) TestMulIntCoins() {
|
||||||
|
testCases := []struct {
|
||||||
|
input sdk.Coins
|
||||||
|
multiplier sdk.Int
|
||||||
|
expected sdk.Coins
|
||||||
|
shouldPanic bool
|
||||||
|
}{
|
||||||
|
{sdk.Coins{s.ca2}, sdk.NewInt(0), sdk.Coins{s.ca0}, true},
|
||||||
|
{sdk.Coins{s.ca2}, sdk.NewInt(2), sdk.Coins{s.ca4}, false},
|
||||||
|
{sdk.Coins{s.ca1, s.cm2}, sdk.NewInt(2), sdk.Coins{s.ca2, s.cm4}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert := s.Assert()
|
||||||
|
for i, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
if tc.shouldPanic {
|
||||||
|
assert.Panics(func() { tc.input.MulInt(tc.multiplier) })
|
||||||
|
} else {
|
||||||
|
res := tc.input.MulInt(tc.multiplier)
|
||||||
|
assert.True(res.IsValid())
|
||||||
|
assert.Equal(tc.expected, res, "multiplication of coins is incorrect, tc #%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *coinTestSuite) TestQuoIntCoins() {
|
||||||
|
testCases := []struct {
|
||||||
|
input sdk.Coins
|
||||||
|
divisor sdk.Int
|
||||||
|
expected sdk.Coins
|
||||||
|
isValid bool
|
||||||
|
shouldPanic bool
|
||||||
|
}{
|
||||||
|
{sdk.Coins{s.ca2, s.ca1}, sdk.NewInt(0), sdk.Coins{s.ca0, s.ca0}, true, true},
|
||||||
|
{sdk.Coins{s.ca2}, sdk.NewInt(4), sdk.Coins{s.ca0}, false, false},
|
||||||
|
{sdk.Coins{s.ca2, s.cm4}, sdk.NewInt(2), sdk.Coins{s.ca1, s.cm2}, true, false},
|
||||||
|
{sdk.Coins{s.ca4}, sdk.NewInt(2), sdk.Coins{s.ca2}, true, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert := s.Assert()
|
||||||
|
for i, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
if tc.shouldPanic {
|
||||||
|
assert.Panics(func() { tc.input.QuoInt(tc.divisor) })
|
||||||
|
} else {
|
||||||
|
res := tc.input.QuoInt(tc.divisor)
|
||||||
|
assert.Equal(tc.isValid, res.IsValid())
|
||||||
|
assert.Equal(tc.expected, res, "quotient of coins is incorrect, tc #%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *coinTestSuite) TestIsGTECoin() {
|
func (s *coinTestSuite) TestIsGTECoin() {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
inputOne sdk.Coin
|
inputOne sdk.Coin
|
||||||
|
|
|
@ -32,7 +32,8 @@ type BroadcastReq struct {
|
||||||
// GetSignCommand returns the sign command
|
// GetSignCommand returns the sign command
|
||||||
func GetMultiSignCommand() *cobra.Command {
|
func GetMultiSignCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "multisign [file] [name] [[signature]...]",
|
Use: "multi-sign [file] [name] [[signature]...]",
|
||||||
|
Aliases: []string{"multisign"},
|
||||||
Short: "Generate multisig signatures for transactions generated offline",
|
Short: "Generate multisig signatures for transactions generated offline",
|
||||||
Long: strings.TrimSpace(
|
Long: strings.TrimSpace(
|
||||||
fmt.Sprintf(`Sign transactions created with the --generate-only flag that require multisig signatures.
|
fmt.Sprintf(`Sign transactions created with the --generate-only flag that require multisig signatures.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/client"
|
"github.com/cosmos/cosmos-sdk/client"
|
||||||
|
@ -10,6 +12,8 @@ import (
|
||||||
"github.com/cosmos/cosmos-sdk/x/bank/types"
|
"github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var FlagSplit = "split"
|
||||||
|
|
||||||
// NewTxCmd returns a root CLI command handler for all x/bank transaction commands.
|
// NewTxCmd returns a root CLI command handler for all x/bank transaction commands.
|
||||||
func NewTxCmd() *cobra.Command {
|
func NewTxCmd() *cobra.Command {
|
||||||
txCmd := &cobra.Command{
|
txCmd := &cobra.Command{
|
||||||
|
@ -20,7 +24,10 @@ func NewTxCmd() *cobra.Command {
|
||||||
RunE: client.ValidateCmd,
|
RunE: client.ValidateCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
txCmd.AddCommand(NewSendTxCmd())
|
txCmd.AddCommand(
|
||||||
|
NewSendTxCmd(),
|
||||||
|
NewMultiSendTxCmd(),
|
||||||
|
)
|
||||||
|
|
||||||
return txCmd
|
return txCmd
|
||||||
}
|
}
|
||||||
|
@ -29,9 +36,11 @@ func NewTxCmd() *cobra.Command {
|
||||||
func NewSendTxCmd() *cobra.Command {
|
func NewSendTxCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "send [from_key_or_address] [to_address] [amount]",
|
Use: "send [from_key_or_address] [to_address] [amount]",
|
||||||
Short: `Send funds from one account to another.
|
Short: "Send funds from one account to another.",
|
||||||
Note, the '--from' flag is ignored as it is implied from [from_key_or_address].
|
Long: `Send funds from one account to another.
|
||||||
When using '--dry-run' a key name cannot be used, only a bech32 address.`,
|
Note, the '--from' flag is ignored as it is implied from [from_key_or_address].
|
||||||
|
When using '--dry-run' a key name cannot be used, only a bech32 address.
|
||||||
|
`,
|
||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cmd.Flags().Set(flags.FlagFrom, args[0])
|
cmd.Flags().Set(flags.FlagFrom, args[0])
|
||||||
|
@ -60,3 +69,77 @@ func NewSendTxCmd() *cobra.Command {
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewMultiSendTxCmd returns a CLI command handler for creating a MsgMultiSend transaction.
|
||||||
|
// For a better UX this command is limited to send funds from one account to two or more accounts.
|
||||||
|
func NewMultiSendTxCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "multi-send [from_key_or_address] [to_address_1, to_address_2, ...] [amount]",
|
||||||
|
Short: "Send funds from one account to two or more accounts.",
|
||||||
|
Long: `Send funds from one account to two or more accounts.
|
||||||
|
By default, sends the [amount] to each address of the list.
|
||||||
|
Using the '--split' flag, the [amount] is split equally between the addresses.
|
||||||
|
Note, the '--from' flag is ignored as it is implied from [from_key_or_address].
|
||||||
|
When using '--dry-run' a key name cannot be used, only a bech32 address.
|
||||||
|
`,
|
||||||
|
Args: cobra.MinimumNArgs(4),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cmd.Flags().Set(flags.FlagFrom, args[0])
|
||||||
|
clientCtx, err := client.GetClientTxContext(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
coins, err := sdk.ParseCoinsNormalized(args[len(args)-1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if coins.IsZero() {
|
||||||
|
return fmt.Errorf("must send positive amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
split, err := cmd.Flags().GetBool(FlagSplit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalAddrs := sdk.NewInt(int64(len(args) - 2))
|
||||||
|
// coins to be received by the addresses
|
||||||
|
sendCoins := coins
|
||||||
|
if split {
|
||||||
|
sendCoins = coins.QuoInt(totalAddrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var output []types.Output
|
||||||
|
for _, arg := range args[1 : len(args)-1] {
|
||||||
|
toAddr, err := sdk.AccAddressFromBech32(arg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
output = append(output, types.NewOutput(toAddr, sendCoins))
|
||||||
|
}
|
||||||
|
|
||||||
|
// amount to be send from the from address
|
||||||
|
var amount sdk.Coins
|
||||||
|
if split {
|
||||||
|
// user input: 1000stake to send to 3 addresses
|
||||||
|
// actual: 333stake to each address (=> 999stake actually sent)
|
||||||
|
amount = sendCoins.MulInt(totalAddrs)
|
||||||
|
} else {
|
||||||
|
amount = coins.MulInt(totalAddrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgMultiSend([]types.Input{types.NewInput(clientCtx.FromAddress, amount)}, output)
|
||||||
|
|
||||||
|
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().Bool(FlagSplit, false, "Send the equally split token amount to each address")
|
||||||
|
|
||||||
|
flags.AddTxFlagsToCmd(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/cosmos/cosmos-sdk/client"
|
"github.com/cosmos/cosmos-sdk/client"
|
||||||
"github.com/cosmos/cosmos-sdk/testutil"
|
"github.com/cosmos/cosmos-sdk/testutil"
|
||||||
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
|
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/cli"
|
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,6 +19,18 @@ func MsgSendExec(clientCtx client.Context, from, to, amount fmt.Stringer, extraA
|
||||||
return clitestutil.ExecTestCLICmd(clientCtx, bankcli.NewSendTxCmd(), args)
|
return clitestutil.ExecTestCLICmd(clientCtx, bankcli.NewSendTxCmd(), args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MsgMultiSendExec(clientCtx client.Context, from sdk.AccAddress, to []sdk.AccAddress, amount fmt.Stringer, extraArgs ...string) (testutil.BufferWriter, error) {
|
||||||
|
args := []string{from.String()}
|
||||||
|
for _, addr := range to {
|
||||||
|
args = append(args, addr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, amount.String())
|
||||||
|
args = append(args, extraArgs...)
|
||||||
|
|
||||||
|
return clitestutil.ExecTestCLICmd(clientCtx, bankcli.NewMultiSendTxCmd(), args)
|
||||||
|
}
|
||||||
|
|
||||||
func QueryBalancesExec(clientCtx client.Context, address fmt.Stringer, extraArgs ...string) (testutil.BufferWriter, error) {
|
func QueryBalancesExec(clientCtx client.Context, address fmt.Stringer, extraArgs ...string) (testutil.BufferWriter, error) {
|
||||||
args := []string{address.String(), fmt.Sprintf("--%s=json", cli.OutputFlag)}
|
args := []string{address.String(), fmt.Sprintf("--%s=json", cli.OutputFlag)}
|
||||||
args = append(args, extraArgs...)
|
args = append(args, extraArgs...)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build norace
|
||||||
// +build norace
|
// +build norace
|
||||||
|
|
||||||
package testutil
|
package testutil
|
||||||
|
|
|
@ -471,6 +471,7 @@ func (s *IntegrationTestSuite) TestNewSendTxCmd() {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
tc := tc
|
||||||
|
|
||||||
|
s.Require().NoError(s.network.WaitForNextBlock())
|
||||||
s.Run(tc.name, func() {
|
s.Run(tc.name, func() {
|
||||||
clientCtx := val.ClientCtx
|
clientCtx := val.ClientCtx
|
||||||
|
|
||||||
|
@ -488,6 +489,141 @@ func (s *IntegrationTestSuite) TestNewSendTxCmd() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationTestSuite) TestNewMultiSendTxCmd() {
|
||||||
|
val := s.network.Validators[0]
|
||||||
|
testAddr := sdk.AccAddress("cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
from sdk.AccAddress
|
||||||
|
to []sdk.AccAddress
|
||||||
|
amount sdk.Coins
|
||||||
|
args []string
|
||||||
|
expectErr bool
|
||||||
|
expectedCode uint32
|
||||||
|
respType proto.Message
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid transaction",
|
||||||
|
val.Address,
|
||||||
|
[]sdk.AccAddress{val.Address, testAddr},
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)),
|
||||||
|
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
|
||||||
|
),
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
|
||||||
|
},
|
||||||
|
false, 0, &sdk.TxResponse{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid split transaction",
|
||||||
|
val.Address,
|
||||||
|
[]sdk.AccAddress{val.Address, testAddr},
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)),
|
||||||
|
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
|
||||||
|
),
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("--%s=true", cli.FlagSplit),
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
|
||||||
|
},
|
||||||
|
false, 0, &sdk.TxResponse{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not enough arguments",
|
||||||
|
val.Address,
|
||||||
|
[]sdk.AccAddress{val.Address},
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)),
|
||||||
|
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
|
||||||
|
),
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
|
||||||
|
},
|
||||||
|
true, 0, &sdk.TxResponse{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain-id shouldn't be used with offline and generate-only flags",
|
||||||
|
val.Address,
|
||||||
|
[]sdk.AccAddress{val.Address, testAddr},
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)),
|
||||||
|
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
|
||||||
|
),
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagOffline),
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagGenerateOnly),
|
||||||
|
},
|
||||||
|
true, 0, &sdk.TxResponse{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not enough fees",
|
||||||
|
val.Address,
|
||||||
|
[]sdk.AccAddress{val.Address, testAddr},
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)),
|
||||||
|
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
|
||||||
|
),
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(1))).String()),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
sdkerrors.ErrInsufficientFee.ABCICode(),
|
||||||
|
&sdk.TxResponse{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not enough gas",
|
||||||
|
val.Address,
|
||||||
|
[]sdk.AccAddress{val.Address, testAddr},
|
||||||
|
sdk.NewCoins(
|
||||||
|
sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), sdk.NewInt(10)),
|
||||||
|
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
|
||||||
|
),
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
|
||||||
|
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
|
||||||
|
"--gas=10",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
sdkerrors.ErrOutOfGas.ABCICode(),
|
||||||
|
&sdk.TxResponse{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
s.Require().NoError(s.network.WaitForNextBlock())
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
clientCtx := val.ClientCtx
|
||||||
|
|
||||||
|
bz, err := MsgMultiSendExec(clientCtx, tc.from, tc.to, tc.amount, tc.args...)
|
||||||
|
if tc.expectErr {
|
||||||
|
s.Require().Error(err)
|
||||||
|
} else {
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(bz.Bytes(), tc.respType), bz.String())
|
||||||
|
txResp := tc.respType.(*sdk.TxResponse)
|
||||||
|
s.Require().Equal(tc.expectedCode, txResp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewCoin(denom string, amount sdk.Int) *sdk.Coin {
|
func NewCoin(denom string, amount sdk.Int) *sdk.Coin {
|
||||||
coin := sdk.NewCoin(denom, amount)
|
coin := sdk.NewCoin(denom, amount)
|
||||||
return &coin
|
return &coin
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
types "github.com/cosmos/cosmos-sdk/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMsgSendRoute(t *testing.T) {
|
func TestMsgSendRoute(t *testing.T) {
|
||||||
|
@ -184,24 +185,46 @@ func TestMsgMultiSendValidation(t *testing.T) {
|
||||||
{false, MsgMultiSend{}}, // no input or output
|
{false, MsgMultiSend{}}, // no input or output
|
||||||
{false, MsgMultiSend{Inputs: []Input{input1}}}, // just input
|
{false, MsgMultiSend{Inputs: []Input{input1}}}, // just input
|
||||||
{false, MsgMultiSend{Outputs: []Output{output1}}}, // just output
|
{false, MsgMultiSend{Outputs: []Output{output1}}}, // just output
|
||||||
{false, MsgMultiSend{
|
{
|
||||||
|
false,
|
||||||
|
MsgMultiSend{
|
||||||
Inputs: []Input{NewInput(emptyAddr, atom123)}, // invalid input
|
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}},
|
Outputs: []Output{output1}},
|
||||||
},
|
},
|
||||||
{true, MsgMultiSend{
|
{
|
||||||
|
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},
|
Inputs: []Input{input1, input2},
|
||||||
Outputs: []Output{outputMulti}},
|
Outputs: []Output{outputMulti},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
true,
|
||||||
|
MsgMultiSend{
|
||||||
|
Inputs: []Input{NewInput(addr2, atom123.MulInt(types.NewInt(2)))},
|
||||||
|
Outputs: []Output{output1, output1},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue