From bea3f9b7a3c49061f17d01ed686e542e4d5fbde8 Mon Sep 17 00:00:00 2001 From: Hieu Vu <72878483+hieuvubk@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:04:34 +0700 Subject: [PATCH] feat(x/auth): batch transactions file format & broadcast multi transactions (#18692) Co-authored-by: Aleksandr Bezobchuk Co-authored-by: Marko --- x/auth/CHANGELOG.md | 2 ++ x/auth/client/cli/broadcast.go | 30 ++++++++++------ x/auth/client/tx.go | 34 ++++++++++++++++++ x/auth/client/tx_test.go | 64 ++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/x/auth/CHANGELOG.md b/x/auth/CHANGELOG.md index 85ad31d779..a2dc6cb2a6 100644 --- a/x/auth/CHANGELOG.md +++ b/x/auth/CHANGELOG.md @@ -27,6 +27,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* [#18281](https://github.com/cosmos/cosmos-sdk/pull/18281) Support broadcasting multiple transactions. + ### Improvements ### API Breaking Changes diff --git a/x/auth/client/cli/broadcast.go b/x/auth/client/cli/broadcast.go index 033be29fc4..f110e459dd 100644 --- a/x/auth/client/cli/broadcast.go +++ b/x/auth/client/cli/broadcast.go @@ -35,22 +35,32 @@ filename, the command reads from standard input.`), return errors.New("cannot broadcast tx during offline mode") } - stdTx, err := authclient.ReadTxFromFile(clientCtx, args[0]) + txs, err := authclient.ReadTxsFromFile(clientCtx, args[0]) if err != nil { return err } - txBytes, err := clientCtx.TxConfig.TxEncoder()(stdTx) - if err != nil { - return err - } + txEncoder := clientCtx.TxConfig.TxEncoder() + for _, tx := range txs { + txBytes, err1 := txEncoder(tx) + if err1 != nil { + err = errors.Join(err, err1) + continue + } - res, err := clientCtx.BroadcastTx(txBytes) - if err != nil { - return err + res, err2 := clientCtx.BroadcastTx(txBytes) + if err2 != nil { + err = errors.Join(err, err2) + continue + } + if res != nil { + err3 := clientCtx.PrintProto(res) + if err3 != nil { + err = errors.Join(err, err3) + } + } } - - return clientCtx.PrintProto(res) + return err }, } diff --git a/x/auth/client/tx.go b/x/auth/client/tx.go index 55a7454552..8cd6e118e0 100644 --- a/x/auth/client/tx.go +++ b/x/auth/client/tx.go @@ -115,6 +115,40 @@ func ReadTxFromFile(ctx client.Context, filename string) (tx sdk.Tx, err error) return ctx.TxConfig.TxJSONDecoder()(bytes) } +// Read and decode a multi transactions (must be in Txs format) from the given filename. +// Can pass "-" to read from stdin. +func ReadTxsFromFile(ctx client.Context, filename string) (tx []sdk.Tx, err error) { + var fileBuff []byte + var txs []sdk.Tx + + if filename == "-" { + fileBuff, err = io.ReadAll(os.Stdin) + } else { + fileBuff, err = os.ReadFile(filename) + } + + if err != nil { + return nil, fmt.Errorf("failed to read batch txs from file %s: %w", filename, err) + } + + // In SignBatchCmd, the output prints each tx line by line separated by "\n". + // So we split the output bytes to slice of tx bytes, + // last elemet always be empty bytes. + txsBytes := bytes.Split(fileBuff, []byte("\n")) + txDecoder := ctx.TxConfig.TxJSONDecoder() + for _, txBytes := range txsBytes { + if len(txBytes) == 0 { + continue + } + tx, err := txDecoder(txBytes) + if err != nil { + return nil, err + } + txs = append(txs, tx) + } + return txs, nil +} + // ReadTxsFromInput reads multiples txs from the given filename(s). Can pass "-" to read from stdin. // Unlike ReadTxFromFile, this function does not decode the txs. func ReadTxsFromInput(txCfg client.TxConfig, filenames ...string) (scanner *BatchScanner, err error) { diff --git a/x/auth/client/tx_test.go b/x/auth/client/tx_test.go index d9ac390662..b5979a8066 100644 --- a/x/auth/client/tx_test.go +++ b/x/auth/client/tx_test.go @@ -71,6 +71,70 @@ func TestReadTxFromFile(t *testing.T) { require.Equal(t, txBuilder.GetTx().GetFee(), txBldr.GetTx().GetFee()) } +func TestReadTxsFromFile(t *testing.T) { + t.Parallel() + + encodingConfig := moduletestutil.MakeTestEncodingConfig() + interfaceRegistry := encodingConfig.InterfaceRegistry + txConfig := encodingConfig.TxConfig + + clientCtx := client.Context{} + clientCtx = clientCtx.WithInterfaceRegistry(interfaceRegistry) + clientCtx = clientCtx.WithTxConfig(txConfig) + + // Set up 2 txs + txBuilders := make([]client.TxBuilder, 2) + // Set up tx 1 + txBuilders[0] = txConfig.NewTxBuilder() + txBuilders[0].SetFeeAmount(sdk.Coins{sdk.NewInt64Coin("atom", 150)}) + txBuilders[0].SetGasLimit(uint64(50000)) + txBuilders[0].SetMemo("foomemo") + // Set up tx 2 + txBuilders[1] = txConfig.NewTxBuilder() + txBuilders[1].SetFeeAmount(sdk.Coins{sdk.NewInt64Coin("atom", 200)}) + txBuilders[1].SetGasLimit(uint64(60000)) + txBuilders[1].SetMemo("foomemo2") + + // Write txs to the file + encodedTx1, err := txConfig.TxJSONEncoder()(txBuilders[0].GetTx()) + require.NoError(t, err) + encodedTx2, err := txConfig.TxJSONEncoder()(txBuilders[1].GetTx()) + require.NoError(t, err) + + tx1String := string(encodedTx1) + "\n" + tx2String := string(encodedTx2) + "\n" + jsonBatchTxsFile := testutil.WriteToNewTempFile(t, tx1String+tx2String) + jsonSingleTxFile := testutil.WriteToNewTempFile(t, tx1String) + + // Read it back + + // 2 txs case + decodedBatchTxs, err := authclient.ReadTxsFromFile(clientCtx, jsonBatchTxsFile.Name()) + require.NoError(t, err) + require.Equal(t, len(decodedBatchTxs), 2) + for i, decodedTx := range decodedBatchTxs { + txBldr, err := txConfig.WrapTxBuilder(decodedTx) + require.NoError(t, err) + + wantTx := txBuilders[i].GetTx() + gotTx := txBldr.GetTx() + require.Equal(t, wantTx.GetMemo(), gotTx.GetMemo()) + require.Equal(t, wantTx.GetFee(), gotTx.GetFee()) + } + + // single tx case + decodedSingleTx, err := authclient.ReadTxsFromFile(clientCtx, jsonSingleTxFile.Name()) + require.NoError(t, err) + require.Equal(t, len(decodedSingleTx), 1) + txBldr, err := txConfig.WrapTxBuilder(decodedSingleTx[0]) + require.NoError(t, err) + + wantTx := txBuilders[0].GetTx() + gotTx := txBldr.GetTx() + require.Equal(t, wantTx.GetMemo(), gotTx.GetMemo()) + require.Equal(t, wantTx.GetFee(), gotTx.GetFee()) +} + func TestBatchScanner_Scan(t *testing.T) { t.Parallel()