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: add capability to reformat Transactions before broadcasting #12936

Merged
merged 9 commits into from
Aug 30, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (x/authz) [#12648](https://github.com/cosmos/cosmos-sdk/pull/12648) Add an allow list, an optional list of addresses allowed to receive bank assests via authz MsgSend grant.
* (sdk.Coins) [#12627](https://github.com/cosmos/cosmos-sdk/pull/12627) Make a Denoms method on sdk.Coins.
* (testutil) [#12973](https://github.com/cosmos/cosmos-sdk/pull/12973) Add generic `testutil.RandSliceElem` function which selects a random element from the list.
* (client) [#12936](https://github.com/cosmos/cosmos-sdk/pull/12936) Add capability to preprocess transactions before broadcasting from a higher level chain.

### Improvements

Expand Down
11 changes: 11 additions & 0 deletions client/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// PreprocessTxFn defines a hook by which chains can preprocess transactions before broadcasting
type PreprocessTxFn func(chainID string, key keyring.KeyType, tx TxBuilder) error

// Context implements a typical context created in SDK modules for transaction
// handling and queries.
type Context struct {
Expand Down Expand Up @@ -50,6 +53,7 @@ type Context struct {
FeePayer sdk.AccAddress
FeeGranter sdk.AccAddress
Viper *viper.Viper
PreprocessTxHook PreprocessTxFn

// IsAux is true when the signer is an auxiliary signer (e.g. the tipper).
IsAux bool
Expand Down Expand Up @@ -262,6 +266,13 @@ func (ctx Context) WithAux(isAux bool) Context {
return ctx
}

// WithPreprocessTxHook returns the context with the provided preprocessing hook, which
// enables chains to preprocess the transaction using the builder.
func (ctx Context) WithPreprocessTxHook(preprocessFn PreprocessTxFn) Context {
ctx.PreprocessTxHook = preprocessFn
return ctx
}

// PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout
func (ctx Context) PrintString(str string) error {
return ctx.PrintBytes([]byte(str))
Expand Down
26 changes: 26 additions & 0 deletions client/tx/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Factory struct {
gasPrices sdk.DecCoins
signMode signing.SignMode
simulateAndExecute bool
preprocessTxHook client.PreprocessTxFn
}

// NewFactoryCLI creates a new Factory.
Expand Down Expand Up @@ -97,6 +98,8 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) Factory {
gasPricesStr, _ := flagSet.GetString(flags.FlagGasPrices)
f = f.WithGasPrices(gasPricesStr)

f = f.WithPreprocessTxHook(clientCtx.PreprocessTxHook)

return f
}

Expand Down Expand Up @@ -242,6 +245,29 @@ func (f Factory) WithFeePayer(fp sdk.AccAddress) Factory {
return f
}

// WithPreprocessTxHook returns a copy of the Factory with an updated preprocess tx function,
// allows for preprocessing of transaction data using the TxBuilder.
func (f Factory) WithPreprocessTxHook(preprocessFn client.PreprocessTxFn) Factory {
f.preprocessTxHook = preprocessFn
return f
}

// PreprocessTx calls the preprocessing hook with the factory parameters and
// returns the result.
func (f Factory) PreprocessTx(keyname string, builder client.TxBuilder) error {
if f.preprocessTxHook == nil {
// Allow pass-through
return nil
}

key, err := f.Keybase().Key(keyname)
if err != nil {
return fmt.Errorf("Error retrieving key from keyring: %w", err)
}

return f.preprocessTxHook(f.chainID, key.GetType(), builder)
}

// 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) {
Expand Down
15 changes: 12 additions & 3 deletions client/tx/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,19 @@ func Sign(txf Factory, name string, txBuilder client.TxBuilder, overwriteSig boo
}

if overwriteSig {
return txBuilder.SetSignatures(sig)
err = txBuilder.SetSignatures(sig)
} else {
prevSignatures = append(prevSignatures, sig)
err = txBuilder.SetSignatures(prevSignatures...)
}
prevSignatures = append(prevSignatures, sig)
return txBuilder.SetSignatures(prevSignatures...)

if err != nil {
return fmt.Errorf("Unable to set signatures on payload: %w", err)
}

// Run optional preprocessing if specified. By default, this is unset
// and will return nil.
return txf.PreprocessTx(name, txBuilder)
}

// GasEstimateResponse defines a response definition for tx gas estimation.
Expand Down
78 changes: 78 additions & 0 deletions client/tx/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import (
clienttestutil "github.com/cosmos/cosmos-sdk/client/testutil"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing"
ante "github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/cosmos/cosmos-sdk/x/auth/signing"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
Expand Down Expand Up @@ -314,6 +317,81 @@ func TestSign(t *testing.T) {
}
}

func TestPreprocessHook(t *testing.T) {
txConfig, cdc := newTestTxConfig(t)
requireT := require.New(t)
path := hd.CreateHDPath(118, 0, 0).String()
kb, err := keyring.New(t.Name(), "test", t.TempDir(), nil, cdc)
requireT.NoError(err)

from := "test_key"
kr, _, err := kb.NewMnemonic(from, keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
requireT.NoError(err)

extVal := &testdata.Cat{
Moniker: "einstein",
Lives: 9,
}
extAny, err := codectypes.NewAnyWithValue(extVal)
requireT.NoError(err)

coin := sdk.Coin{
Denom: "atom",
Amount: sdk.NewInt(20),
}
newTip := &txtypes.Tip{
Amount: sdk.Coins{coin},
Tipper: "galaxy",
}

preprocessHook := client.PreprocessTxFn(func(chainID string, key keyring.KeyType, tx client.TxBuilder) error {
extensionBuilder, ok := tx.(authtx.ExtensionOptionsTxBuilder)
requireT.True(ok)

// Set new extension and tip
extensionBuilder.SetExtensionOptions(extAny)
tx.SetTip(newTip)

return nil
})

txfDirect := tx.Factory{}.
WithTxConfig(txConfig).
WithAccountNumber(50).
WithSequence(23).
WithFees("50stake").
WithMemo("memo").
WithChainID("test-chain").
WithKeybase(kb).
WithSignMode(signingtypes.SignMode_SIGN_MODE_DIRECT).
WithPreprocessTxHook(preprocessHook)

addr1, err := kr.GetAddress()
requireT.NoError(err)
msg1 := banktypes.NewMsgSend(addr1, sdk.AccAddress("to"), nil)
msg2 := banktypes.NewMsgSend(addr2, sdk.AccAddress("to"), nil)
txb, err := txfDirect.BuildUnsignedTx(msg1, msg2)

err = tx.Sign(txfDirect, from, txb, false)
requireT.NoError(err)

// Run preprocessing
err = txfDirect.PreprocessTx(from, txb)
requireT.NoError(err)

hasExtOptsTx, ok := txb.(ante.HasExtensionOptionsTx)
requireT.True(ok)

hasOneExt := len(hasExtOptsTx.GetExtensionOptions()) == 1
requireT.True(hasOneExt)

opt := hasExtOptsTx.GetExtensionOptions()[0]
requireT.Equal(opt, extAny)

tip := txb.GetTx().GetTip()
requireT.Equal(tip, newTip)
}

func testSigners(require *require.Assertions, tr signing.Tx, pks ...cryptotypes.PubKey) []signingtypes.SignatureV2 {
sigs, err := tr.GetSignaturesV2()
require.Len(sigs, len(pks))
Expand Down
Loading