Skip to content

Commit

Permalink
feat: implement multi-send transaction command (#11738)
Browse files Browse the repository at this point in the history
Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>
  • Loading branch information
3 people committed Apr 29, 2022
1 parent 0c0b4da commit 6a9b824
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -39,6 +39,8 @@ Ref: https://keepachangelog.com/en/1.0.0/

### 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.
* (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.
Expand Down
65 changes: 65 additions & 0 deletions types/coin.go
Expand Up @@ -410,6 +410,71 @@ func (coins Coins) SafeSub(coinsB ...Coin) (Coins, bool) {
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
// 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
Expand Down
60 changes: 57 additions & 3 deletions types/coin_test.go
Expand Up @@ -18,7 +18,7 @@ var (

type coinTestSuite struct {
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) {
Expand All @@ -30,8 +30,10 @@ func (s *coinTestSuite) SetupSuite() {
zero := sdk.NewInt(0)
one := sdk.OneInt()
two := sdk.NewInt(2)
s.ca0, s.ca1, s.ca2 = sdk.Coin{testDenom1, zero}, sdk.Coin{testDenom1, one}, sdk.Coin{testDenom1, two}
s.cm0, s.cm1, s.cm2 = sdk.Coin{testDenom2, zero}, sdk.Coin{testDenom2, one}, sdk.Coin{testDenom2, two}
four := sdk.NewInt(4)

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

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -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() {
cases := []struct {
inputOne sdk.Coin
Expand Down
5 changes: 3 additions & 2 deletions x/auth/client/cli/tx_multisign.go
Expand Up @@ -32,8 +32,9 @@ type BroadcastReq struct {
// GetSignCommand returns the sign command
func GetMultiSignCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "multisign [file] [name] [[signature]...]",
Short: "Generate multisig signatures for transactions generated offline",
Use: "multi-sign [file] [name] [[signature]...]",
Aliases: []string{"multisign"},
Short: "Generate multisig signatures for transactions generated offline",
Long: strings.TrimSpace(
fmt.Sprintf(`Sign transactions created with the --generate-only flag that require multisig signatures.
Expand Down
93 changes: 88 additions & 5 deletions x/bank/client/cli/tx.go
@@ -1,6 +1,8 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client"
Expand All @@ -10,6 +12,8 @@ import (
"github.com/cosmos/cosmos-sdk/x/bank/types"
)

var FlagSplit = "split"

// NewTxCmd returns a root CLI command handler for all x/bank transaction commands.
func NewTxCmd() *cobra.Command {
txCmd := &cobra.Command{
Expand All @@ -20,18 +24,23 @@ func NewTxCmd() *cobra.Command {
RunE: client.ValidateCmd,
}

txCmd.AddCommand(NewSendTxCmd())
txCmd.AddCommand(
NewSendTxCmd(),
NewMultiSendTxCmd(),
)

return txCmd
}

// NewSendTxCmd returns a CLI command handler for creating a MsgSend transaction.
func NewSendTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "send [from_key_or_address] [to_address] [amount]",
Short: `Send funds from one account to another.
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.`,
Use: "send [from_key_or_address] [to_address] [amount]",
Short: "Send funds from one account to another.",
Long: `Send funds from one account to another.
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),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Flags().Set(flags.FlagFrom, args[0])
Expand Down Expand Up @@ -60,3 +69,77 @@ func NewSendTxCmd() *cobra.Command {

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
}
13 changes: 13 additions & 0 deletions x/bank/client/testutil/cli_helpers.go
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/testutil"
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"
)

Expand All @@ -18,6 +19,18 @@ func MsgSendExec(clientCtx client.Context, from, to, amount fmt.Stringer, extraA
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) {
args := []string{address.String(), fmt.Sprintf("--%s=json", cli.OutputFlag)}
args = append(args, extraArgs...)
Expand Down
1 change: 1 addition & 0 deletions x/bank/client/testutil/cli_test.go
@@ -1,3 +1,4 @@
//go:build norace
// +build norace

package testutil
Expand Down

0 comments on commit 6a9b824

Please sign in to comment.