feat: add capability to reformat Transactions before broadcasting (#12936)
## Description Increase customized signing capabilities by: - Adding a hook to preprocess transactions before broadcasting This PR introduces the ability to preprocess transactions before broadcasting, a feature that provides the ability to add extensions, or otherwise customize the payload to satisfy a chain's requirements. For adaptability, the parameters include the keytype to allow differentiation between formatting transactions for Ledger vs. software keys. ## PR This PR introduces a hook to preprocess transactions, chiefly to add extensions, from a higher-level chain. Key changes and relevant files: - Add `PreprocessTxFn` type and fields to both `Context` and `Factory` - Instantiate the `Factory` with the `PreprocessTxFn` provided by the `Context` - Call `PreprocessTx` with the appropriate parameters after signing Closes: #XXXX --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
This commit is contained in:
parent
0024a0bf44
commit
f8b55ff8ee
5794
CHANGELOG.md
5794
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||
@ -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
|
||||
@ -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))
|
||||
|
||||
@ -40,6 +40,7 @@ type Factory struct {
|
||||
gasPrices sdk.DecCoins
|
||||
signMode signing.SignMode
|
||||
simulateAndExecute bool
|
||||
preprocessTxHook client.PreprocessTxFn
|
||||
}
|
||||
|
||||
// NewFactoryCLI creates a new Factory.
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
@ -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))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user