Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(x/bank/cli)!: Allow MultiSendTx to Support Transactions with a Single Recipient #21920

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x/bank/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

* [#17569](https://github.com/cosmos/cosmos-sdk/pull/17569) Introduce a new message type, `MsgBurn`, to burn coins.
* [#20014](https://github.com/cosmos/cosmos-sdk/pull/20014) Support app wiring for `SendRestrictionFn`.
* [#21920](https://github.com/cosmos/cosmos-sdk/pull/21920) Allow `MultiSendTx` to support transactions with a single recipient.

### Improvements

Expand Down
30 changes: 14 additions & 16 deletions x/bank/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,21 @@ func NewTxCmd() *cobra.Command {
}

// 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.
// This version allows sending funds from one account to one 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.
Use: "multi-send <from_key_or_address> <to_address_1>... <amount>",
Short: "Send funds from one account to one or more accounts.",
Long: `Send funds from one account to one or more accounts.
By default, sends the [amount] to each address in 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] and
Note, the '--from' flag is ignored as it is implied from [from_key_or_address] and
separate addresses with space.
When using '--dry-run' a key name cannot be used, only a bech32 address.`,
Example: fmt.Sprintf("%s tx bank multi-send cosmos1... cosmos1... cosmos1... cosmos1... 10stake", version.AppName),
Args: cobra.MinimumNArgs(4),
Example: fmt.Sprintf("%s tx bank multi-send cosmos1... cosmos1... cosmos1... 10stake", version.AppName),
Args: cobra.MinimumNArgs(3), // Changed minimum argument count to 3
RunE: func(cmd *cobra.Command, args []string) error {
err := cmd.Flags().Set(flags.FlagFrom, args[0])
err := cmd.Flags().Set(flags.FlagFrom, args[0]) // Set the first argument as the sender
if err != nil {
return err
}
Expand All @@ -59,7 +59,7 @@ When using '--dry-run' a key name cannot be used, only a bech32 address.`,
return err
}

coins, err := sdk.ParseCoinsNormalized(args[len(args)-1])
coins, err := sdk.ParseCoinsNormalized(args[len(args)-1]) // The last argument is the amount
if err != nil {
return err
}
Expand All @@ -73,15 +73,15 @@ When using '--dry-run' a key name cannot be used, only a bech32 address.`,
return err
}

totalAddrs := sdkmath.NewInt(int64(len(args) - 2))
totalAddrs := sdkmath.NewInt(int64(len(args) - 2)) // Calculate the number of recipients
// coins to be received by the addresses
sendCoins := coins
if split {
sendCoins = coins.QuoInt(totalAddrs)
sendCoins = coins.QuoInt(totalAddrs) // Logic to split the amount among recipients
}

var output []types.Output
for _, arg := range args[1 : len(args)-1] {
for _, arg := range args[1 : len(args)-1] { // Process each recipient
_, err = clientCtx.AddressCodec.StringToBytes(arg)
if err != nil {
return err
Expand All @@ -90,11 +90,9 @@ When using '--dry-run' a key name cannot be used, only a bech32 address.`,
output = append(output, types.NewOutput(arg, sendCoins))
}

// amount to be send from the from address
// Calculate the total amount to be sent by the sender
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)
Expand Down
122 changes: 121 additions & 1 deletion x/bank/client/cli/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,127 @@ func (s *CLITestSuite) SetupSuite() {
WithConsensusAddressCodec(addresscodec.NewBech32Codec("cosmosvalcons"))
}

func (s *CLITestSuite) TestMultiSendTxCmd() {
func (s *CLITestSuite) TestMultiSendTxCmd_SingleRecipient() {
accounts := testutil.CreateKeyringAccounts(s.T(), s.kr, 2) // Create only 2 accounts: one sender and one recipient
accountStr := make([]string, len(accounts))
for i, acc := range accounts {
addrStr, err := s.baseCtx.AddressCodec.BytesToString(acc.Address)
s.Require().NoError(err)
accountStr[i] = addrStr
}

cmd := cli.NewMultiSendTxCmd()
cmd.SetOutput(io.Discard)

extraArgs := []string{
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin("photon", sdkmath.NewInt(10))).String()),
fmt.Sprintf("--%s=test-chain", flags.FlagChainID),
}

testCases := []struct {
name string
ctxGen func() client.Context
from string
to []string
amount sdk.Coins
extraArgs []string
expectErrMsg string
}{
{
"valid transaction with single recipient",
func() client.Context {
return s.baseCtx
},
accountStr[0], // Sending account
[]string{
accountStr[1], // Only one recipient account
},
sdk.NewCoins(
sdk.NewCoin("stake", sdkmath.NewInt(10)),
sdk.NewCoin("photon", sdkmath.NewInt(40)),
),
extraArgs,
"",
},
{
"invalid from Address",
func() client.Context {
return s.baseCtx
},
"foo",
[]string{
accountStr[1],
},
sdk.NewCoins(
sdk.NewCoin("stake", sdkmath.NewInt(10)),
sdk.NewCoin("photon", sdkmath.NewInt(40)),
),
extraArgs,
"key not found",
},
{
"invalid recipient",
func() client.Context {
return s.baseCtx
},
accountStr[0],
[]string{
"bar",
},
sdk.NewCoins(
sdk.NewCoin("stake", sdkmath.NewInt(10)),
sdk.NewCoin("photon", sdkmath.NewInt(40)),
),
extraArgs,
"invalid bech32 string",
},
{
"invalid amount",
func() client.Context {
return s.baseCtx
},
accountStr[0],
[]string{
accountStr[1],
},
nil,
extraArgs,
"must send positive amount",
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
ctx := svrcmd.CreateExecuteContext(context.Background())

var args []string
args = append(args, tc.from) // Sending account
args = append(args, tc.to...) // Recipient account(s)
args = append(args, tc.amount.String()) // Amount
args = append(args, tc.extraArgs...) // Additional flags

cmd.SetContext(ctx)
cmd.SetArgs(args)

s.Require().NoError(client.SetCmdClientContextHandler(tc.ctxGen(), cmd))

out, err := clitestutil.ExecTestCLICmd(tc.ctxGen(), cmd, args)
if tc.expectErrMsg != "" {
s.Require().Error(err)
s.Require().Contains(out.String(), tc.expectErrMsg)
} else {
s.Require().NoError(err)
msg := &sdk.TxResponse{}
s.Require().NoError(tc.ctxGen().Codec.UnmarshalJSON(out.Bytes(), msg), out.String())
}
})
}
}

func (s *CLITestSuite) TestMultiSendTxCmd_MultiRecipient() {
accounts := testutil.CreateKeyringAccounts(s.T(), s.kr, 3)
accountStr := make([]string, len(accounts))
for i, acc := range accounts {
Expand Down
Loading