From e17be874bb0e3a246b74752e9b8894855cab9b03 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Mon, 28 Jun 2021 07:42:08 -0400 Subject: [PATCH] refactor: Move some methods inside TX Factory (#9421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Putting some things inside the factory (which was very anemic struct) has helped me to understand the flow. Feel free to merge if you see some benefit. closes: #XXXX --- Before we can merge this PR, please make sure that all the following items have been checked off. If any of the checklist items are not applicable, please leave them but write a little note why. - [ ] Targeted PR against correct branch (see [CONTRIBUTING.md](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work. - [ ] Code follows the [module structure standards](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules/structure.md). - [ ] Wrote unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [ ] Updated relevant documentation (`docs/`) or specification (`x//spec/`) - [ ] Added relevant `godoc` [comments](https://blog.golang.org/godoc-documenting-go-code). - [ ] Added a relevant changelog entry to the `Unreleased` section in `CHANGELOG.md` - [ ] Re-reviewed `Files changed` in the Github PR explorer - [ ] Review `Codecov Report` in the comment section below once CI passes --- CHANGELOG.md | 5 +- client/tx/factory.go | 134 ++++++++++++++++++++++++++++++++ client/tx/tx.go | 139 ++-------------------------------- client/tx/tx_test.go | 10 +-- x/auth/client/tx.go | 6 -- x/genutil/client/cli/gentx.go | 4 +- 6 files changed, 150 insertions(+), 148 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5a759a8204..bef52a021cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### API Breaking Changes +* (client/tx) [\#9421](https://github.com/cosmos/cosmos-sdk/pull/9421/) `BuildUnsignedTx`, `BuildSimTx`, `PrintUnsignedStdTx` functions are moved to + the Tx Factory as methods. + ## [v0.43.0-rc0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.43.0-rc0) - 2021-06-25 ### Features @@ -111,7 +115,6 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (codec) [\#9251](https://github.com/cosmos/cosmos-sdk/pull/9251) Rename `clientCtx.JSONMarshaler` to `clientCtx.JSONCodec` as per #9226. * (x/bank) [\#9271](https://github.com/cosmos/cosmos-sdk/pull/9271) SendEnabledCoin(s) renamed to IsSendEnabledCoin(s) to better reflect its functionality. - ### State Machine Breaking * (x/{bank,distrib,gov,slashing,staking}) [\#8363](https://github.com/cosmos/cosmos-sdk/issues/8363) Store keys have been modified to allow for variable-length addresses. diff --git a/client/tx/factory.go b/client/tx/factory.go index 2cb951922d0a..3dc994b3917d 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -1,11 +1,16 @@ package tx import ( + "errors" + "fmt" + "os" + "github.com/spf13/pflag" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) @@ -189,3 +194,132 @@ func (f Factory) WithTimeoutHeight(height uint64) Factory { f.timeoutHeight = height return f } + +// BuildUnsignedTx builds a transaction to be signed given a set of messages. +// Once created, the fee, memo, and messages are set. +func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (client.TxBuilder, error) { + if f.chainID == "" { + return nil, fmt.Errorf("chain ID required but not specified") + } + + fees := f.fees + + if !f.gasPrices.IsZero() { + if !fees.IsZero() { + return nil, errors.New("cannot provide both fees and gas prices") + } + + glDec := sdk.NewDec(int64(f.gas)) + + // Derive the fees based on the provided gas prices, where + // fee = ceil(gasPrice * gasLimit). + fees = make(sdk.Coins, len(f.gasPrices)) + + for i, gp := range f.gasPrices { + fee := gp.Amount.Mul(glDec) + fees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + } + + tx := f.txConfig.NewTxBuilder() + + if err := tx.SetMsgs(msgs...); err != nil { + return nil, err + } + + tx.SetMemo(f.memo) + tx.SetFeeAmount(fees) + tx.SetGasLimit(f.gas) + tx.SetTimeoutHeight(f.TimeoutHeight()) + + return tx, nil +} + +// PrintUnsignedTx will generate an unsigned transaction and print it to the writer +// specified by ctx.Output. If simulation was requested, the gas will be +// simulated and also printed to the same writer before the transaction is +// printed. +func (f Factory) PrintUnsignedTx(clientCtx client.Context, msgs ...sdk.Msg) error { + if f.SimulateAndExecute() { + if clientCtx.Offline { + return errors.New("cannot estimate gas in offline mode") + } + + _, adjusted, err := CalculateGas(clientCtx, f, msgs...) + if err != nil { + return err + } + + f = f.WithGas(adjusted) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: f.Gas()}) + } + + tx, err := f.BuildUnsignedTx(msgs...) + if err != nil { + return err + } + + json, err := clientCtx.TxConfig.TxJSONEncoder()(tx.GetTx()) + if err != nil { + return err + } + + return clientCtx.PrintString(fmt.Sprintf("%s\n", json)) +} + +// BuildSimTx creates an unsigned tx with an empty single signature and returns +// the encoded transaction or an error if the unsigned transaction cannot be +// built. +func (f Factory) BuildSimTx(msgs ...sdk.Msg) ([]byte, error) { + txb, err := f.BuildUnsignedTx(msgs...) + if err != nil { + return nil, err + } + + // Create an empty signature literal as the ante handler will populate with a + // sentinel pubkey. + sig := signing.SignatureV2{ + PubKey: &secp256k1.PubKey{}, + Data: &signing.SingleSignatureData{ + SignMode: f.signMode, + }, + Sequence: f.Sequence(), + } + if err := txb.SetSignatures(sig); err != nil { + return nil, err + } + + return f.txConfig.TxEncoder()(txb.GetTx()) +} + +// Prepare ensures the account defined by ctx.GetFromAddress() exists and +// if the account number and/or the account sequence number are zero (not set), +// they will be queried for and set on the provided Factory. A new Factory with +// the updated fields will be returned. +func (f Factory) Prepare(clientCtx client.Context) (Factory, error) { + fc := f + + from := clientCtx.GetFromAddress() + + if err := fc.accountRetriever.EnsureExists(clientCtx, from); err != nil { + return fc, err + } + + initNum, initSeq := fc.accountNumber, fc.sequence + if initNum == 0 || initSeq == 0 { + num, seq, err := fc.accountRetriever.GetAccountNumberSequence(clientCtx, from) + if err != nil { + return fc, err + } + + if initNum == 0 { + fc = fc.WithAccountNumber(num) + } + + if initSeq == 0 { + fc = fc.WithSequence(seq) + } + } + + return fc, nil +} diff --git a/client/tx/tx.go b/client/tx/tx.go index 8e20be369bfd..ee0c7553de83 100644 --- a/client/tx/tx.go +++ b/client/tx/tx.go @@ -14,7 +14,6 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/input" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -45,49 +44,17 @@ func GenerateOrBroadcastTxWithFactory(clientCtx client.Context, txf Factory, msg } if clientCtx.GenerateOnly { - return GenerateTx(clientCtx, txf, msgs...) + return txf.PrintUnsignedTx(clientCtx, msgs...) } return BroadcastTx(clientCtx, txf, msgs...) } -// GenerateTx will generate an unsigned transaction and print it to the writer -// specified by ctx.Output. If simulation was requested, the gas will be -// simulated and also printed to the same writer before the transaction is -// printed. -func GenerateTx(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { - if txf.SimulateAndExecute() { - if clientCtx.Offline { - return errors.New("cannot estimate gas in offline mode") - } - - _, adjusted, err := CalculateGas(clientCtx, txf, msgs...) - if err != nil { - return err - } - - txf = txf.WithGas(adjusted) - _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: txf.Gas()}) - } - - tx, err := BuildUnsignedTx(txf, msgs...) - if err != nil { - return err - } - - json, err := clientCtx.TxConfig.TxJSONEncoder()(tx.GetTx()) - if err != nil { - return err - } - - return clientCtx.PrintString(fmt.Sprintf("%s\n", json)) -} - // BroadcastTx attempts to generate, sign and broadcast a transaction with the // given set of messages. It will also simulate gas requirements if necessary. // It will return an error upon failure. func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { - txf, err := prepareFactory(clientCtx, txf) + txf, err := txf.Prepare(clientCtx) if err != nil { return err } @@ -106,7 +73,7 @@ func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { return nil } - tx, err := BuildUnsignedTx(txf, msgs...) + tx, err := txf.BuildUnsignedTx(msgs...) if err != nil { return err } @@ -197,7 +164,7 @@ func WriteGeneratedTxResponse( } } - tx, err := BuildUnsignedTx(txf, msgs...) + tx, err := txf.BuildUnsignedTx(msgs...) if rest.CheckBadRequestError(w, err) { return } @@ -217,78 +184,12 @@ func WriteGeneratedTxResponse( _, _ = w.Write(output) } -// BuildUnsignedTx builds a transaction to be signed given a set of messages. The -// transaction is initially created via the provided factory's generator. Once -// created, the fee, memo, and messages are set. -func BuildUnsignedTx(txf Factory, msgs ...sdk.Msg) (client.TxBuilder, error) { - if txf.chainID == "" { - return nil, fmt.Errorf("chain ID required but not specified") - } - - fees := txf.fees - - if !txf.gasPrices.IsZero() { - if !fees.IsZero() { - return nil, errors.New("cannot provide both fees and gas prices") - } - - glDec := sdk.NewDec(int64(txf.gas)) - - // Derive the fees based on the provided gas prices, where - // fee = ceil(gasPrice * gasLimit). - fees = make(sdk.Coins, len(txf.gasPrices)) - - for i, gp := range txf.gasPrices { - fee := gp.Amount.Mul(glDec) - fees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) - } - } - - tx := txf.txConfig.NewTxBuilder() - - if err := tx.SetMsgs(msgs...); err != nil { - return nil, err - } - - tx.SetMemo(txf.memo) - tx.SetFeeAmount(fees) - tx.SetGasLimit(txf.gas) - tx.SetTimeoutHeight(txf.TimeoutHeight()) - - return tx, nil -} - -// BuildSimTx creates an unsigned tx with an empty single signature and returns -// the encoded transaction or an error if the unsigned transaction cannot be -// built. -func BuildSimTx(txf Factory, msgs ...sdk.Msg) ([]byte, error) { - txb, err := BuildUnsignedTx(txf, msgs...) - if err != nil { - return nil, err - } - - // Create an empty signature literal as the ante handler will populate with a - // sentinel pubkey. - sig := signing.SignatureV2{ - PubKey: &secp256k1.PubKey{}, - Data: &signing.SingleSignatureData{ - SignMode: txf.signMode, - }, - Sequence: txf.Sequence(), - } - if err := txb.SetSignatures(sig); err != nil { - return nil, err - } - - return txf.txConfig.TxEncoder()(txb.GetTx()) -} - // CalculateGas simulates the execution of a transaction and returns the // simulation response obtained by the query and the adjusted gas amount. func CalculateGas( clientCtx gogogrpc.ClientConn, txf Factory, msgs ...sdk.Msg, ) (*tx.SimulateResponse, uint64, error) { - txBytes, err := BuildSimTx(txf, msgs...) + txBytes, err := txf.BuildSimTx(msgs...) if err != nil { return nil, 0, err } @@ -304,36 +205,6 @@ func CalculateGas( return simRes, uint64(txf.GasAdjustment() * float64(simRes.GasInfo.GasUsed)), nil } -// prepareFactory ensures the account defined by ctx.GetFromAddress() exists and -// if the account number and/or the account sequence number are zero (not set), -// they will be queried for and set on the provided Factory. A new Factory with -// the updated fields will be returned. -func prepareFactory(clientCtx client.Context, txf Factory) (Factory, error) { - from := clientCtx.GetFromAddress() - - if err := txf.accountRetriever.EnsureExists(clientCtx, from); err != nil { - return txf, err - } - - initNum, initSeq := txf.accountNumber, txf.sequence - if initNum == 0 || initSeq == 0 { - num, seq, err := txf.accountRetriever.GetAccountNumberSequence(clientCtx, from) - if err != nil { - return txf, err - } - - if initNum == 0 { - txf = txf.WithAccountNumber(num) - } - - if initSeq == 0 { - txf = txf.WithSequence(seq) - } - } - - return txf, nil -} - // SignWithPrivKey signs a given tx with the given private key, and returns the // corresponding SignatureV2 if the signing is successful. func SignWithPrivKey( diff --git a/client/tx/tx_test.go b/client/tx/tx_test.go index b095d05b9e5d..54bcc7ade649 100644 --- a/client/tx/tx_test.go +++ b/client/tx/tx_test.go @@ -107,7 +107,7 @@ func TestBuildSimTx(t *testing.T) { WithSignMode(txCfg.SignModeHandler().DefaultMode()) msg := banktypes.NewMsgSend(sdk.AccAddress("from"), sdk.AccAddress("to"), nil) - bz, err := tx.BuildSimTx(txf, msg) + bz, err := txf.BuildSimTx(msg) require.NoError(t, err) require.NotNil(t, bz) } @@ -122,7 +122,7 @@ func TestBuildUnsignedTx(t *testing.T) { WithChainID("test-chain") msg := banktypes.NewMsgSend(sdk.AccAddress("from"), sdk.AccAddress("to"), nil) - tx, err := tx.BuildUnsignedTx(txf, msg) + tx, err := txf.BuildUnsignedTx(msg) require.NoError(t, err) require.NotNil(t, tx) @@ -169,11 +169,11 @@ func TestSign(t *testing.T) { WithSignMode(signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) msg1 := banktypes.NewMsgSend(info1.GetAddress(), sdk.AccAddress("to"), nil) msg2 := banktypes.NewMsgSend(info2.GetAddress(), sdk.AccAddress("to"), nil) - txb, err := tx.BuildUnsignedTx(txfNoKeybase, msg1, msg2) + txb, err := txfNoKeybase.BuildUnsignedTx(msg1, msg2) requireT.NoError(err) - txb2, err := tx.BuildUnsignedTx(txfNoKeybase, msg1, msg2) + txb2, err := txfNoKeybase.BuildUnsignedTx(msg1, msg2) requireT.NoError(err) - txbSimple, err := tx.BuildUnsignedTx(txfNoKeybase, msg2) + txbSimple, err := txfNoKeybase.BuildUnsignedTx(msg2) requireT.NoError(err) testCases := []struct { diff --git a/x/auth/client/tx.go b/x/auth/client/tx.go index 80debcc1dedf..47268e643902 100644 --- a/x/auth/client/tx.go +++ b/x/auth/client/tx.go @@ -30,12 +30,6 @@ func (gr GasEstimateResponse) String() string { return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) } -// PrintUnsignedStdTx builds an unsigned StdTx and prints it to os.Stdout. -func PrintUnsignedStdTx(txBldr tx.Factory, clientCtx client.Context, msgs []sdk.Msg) error { - err := tx.GenerateTx(clientCtx, txBldr, msgs...) - return err -} - // SignTx signs a transaction managed by the TxBuilder using a `name` key stored in Keybase. // The new signature is appended to the TxBuilder when overwrite=false or overwritten otherwise. // Don't perform online validation or lookups if offline is true. diff --git a/x/genutil/client/cli/gentx.go b/x/genutil/client/cli/gentx.go index e6433b2f46f6..3b1aaceb0207 100644 --- a/x/genutil/client/cli/gentx.go +++ b/x/genutil/client/cli/gentx.go @@ -156,14 +156,14 @@ $ %s gentx my-key-name 1000000stake --home=/path/to/home/dir --keyring-backend=o if key.GetType() == keyring.TypeOffline || key.GetType() == keyring.TypeMulti { cmd.PrintErrln("Offline key passed in. Use `tx sign` command to sign.") - return authclient.PrintUnsignedStdTx(txBldr, clientCtx, []sdk.Msg{msg}) + return txBldr.PrintUnsignedTx(clientCtx, msg) } // write the unsigned transaction to the buffer w := bytes.NewBuffer([]byte{}) clientCtx = clientCtx.WithOutput(w) - if err = authclient.PrintUnsignedStdTx(txBldr, clientCtx, []sdk.Msg{msg}); err != nil { + if err = txBldr.PrintUnsignedTx(clientCtx, msg); err != nil { return errors.Wrap(err, "failed to print unsigned std tx") }