From 4adfc9a21b644347860b49c22f4cd435f22e765d Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Sun, 14 Jan 2024 11:58:43 -0300 Subject: [PATCH 01/14] Move and adapt client/tx to client/v2/tx --- client/v2/autocli/keyring/interface.go | 19 +- client/v2/autocli/keyring/no_keyring.go | 20 + client/v2/tx/account_retriever.go | 24 ++ client/v2/tx/aux_builder.go | 273 +++++++++++++ client/v2/tx/broadcast.go | 140 +++++++ client/v2/tx/builder.go | 29 ++ client/v2/tx/cometbft.go | 36 ++ client/v2/tx/config.go | 30 ++ client/v2/tx/context.go | 424 ++++++++++++++++++++ client/v2/tx/factory.go | 502 ++++++++++++++++++++++++ client/v2/tx/query.go | 307 +++++++++++++++ client/v2/tx/tx.go | 379 ++++++++++++++++++ client/v2/tx/types.go | 23 ++ codec/types/any.go | 12 +- codec/types/util.go | 15 +- types/tx_msg.go | 4 +- types/tx_msg_v2.go | 68 ++++ 17 files changed, 2293 insertions(+), 12 deletions(-) create mode 100644 client/v2/tx/account_retriever.go create mode 100644 client/v2/tx/aux_builder.go create mode 100644 client/v2/tx/broadcast.go create mode 100644 client/v2/tx/builder.go create mode 100644 client/v2/tx/cometbft.go create mode 100644 client/v2/tx/config.go create mode 100644 client/v2/tx/context.go create mode 100644 client/v2/tx/factory.go create mode 100644 client/v2/tx/query.go create mode 100644 client/v2/tx/tx.go create mode 100644 client/v2/tx/types.go create mode 100644 types/tx_msg_v2.go diff --git a/client/v2/autocli/keyring/interface.go b/client/v2/autocli/keyring/interface.go index fa448bd20599..4dcae0bc9e39 100644 --- a/client/v2/autocli/keyring/interface.go +++ b/client/v2/autocli/keyring/interface.go @@ -2,10 +2,19 @@ package keyring import ( signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" ) +// Options are used to configure a keyring. +type Options struct{} + +type Option func(*Options) + +// KeyType reflects a human-readable type for key listing. +type KeyType uint + +type Record struct{} + // Keyring is an interface used for signing transactions. // It aims to be simplistic and easy to use. type Keyring interface { @@ -18,6 +27,14 @@ type Keyring interface { // GetPubKey returns the public key of the key with the given name. GetPubKey(name string) (cryptotypes.PubKey, error) + // Key and KeyByAddress return keys by uid and address respectively. + Key(uid string) (*Record, error) + KeyByAddress(address []byte) (*Record, error) + + GetRecordAddress(record *Record) ([]byte, error) + GetRecordName(record *Record) string + GetRecordType(record *Record) KeyType + // Sign signs the given bytes with the key with the given name. Sign(name string, msg []byte, signMode signingv1beta1.SignMode) ([]byte, error) } diff --git a/client/v2/autocli/keyring/no_keyring.go b/client/v2/autocli/keyring/no_keyring.go index e14267cee5e3..9a0dc60fbda1 100644 --- a/client/v2/autocli/keyring/no_keyring.go +++ b/client/v2/autocli/keyring/no_keyring.go @@ -14,6 +14,26 @@ var errNoKeyring = errors.New("no keyring configured") type NoKeyring struct{} +func (k NoKeyring) Key(uid string) (*Record, error) { + return nil, errNoKeyring +} + +func (k NoKeyring) KeyByAddress(address []byte) (*Record, error) { + return nil, errNoKeyring +} + +func (k NoKeyring) GetRecordAddress(record *Record) ([]byte, error) { + return nil, errNoKeyring +} + +func (k NoKeyring) GetRecordName(record *Record) string { + return "" +} + +func (k NoKeyring) GetRecordType(record *Record) KeyType { + return 0 +} + func (k NoKeyring) List() ([]string, error) { return nil, errNoKeyring } diff --git a/client/v2/tx/account_retriever.go b/client/v2/tx/account_retriever.go new file mode 100644 index 000000000000..ca3f3b020e1a --- /dev/null +++ b/client/v2/tx/account_retriever.go @@ -0,0 +1,24 @@ +package tx + +import ( + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Account defines a read-only version of the auth module's AccountI. +type Account interface { + GetAddress() sdk.AccAddress + GetPubKey() cryptotypes.PubKey // can return nil. + GetAccountNumber() uint64 + GetSequence() uint64 +} + +// AccountRetriever defines the interfaces required by transactions to +// ensure an account exists and to be able to query for account fields necessary +// for signing. +type AccountRetriever interface { + GetAccount(clientCtx txContext, addr sdk.AccAddress) (Account, error) + GetAccountWithHeight(clientCtx txContext, addr sdk.AccAddress) (Account, int64, error) + EnsureExists(clientCtx txContext, addr sdk.AccAddress) error + GetAccountNumberSequence(clientCtx txContext, addr sdk.AccAddress) (accNum, accSeq uint64, err error) +} diff --git a/client/v2/tx/aux_builder.go b/client/v2/tx/aux_builder.go new file mode 100644 index 000000000000..a2c50f9e2fe4 --- /dev/null +++ b/client/v2/tx/aux_builder.go @@ -0,0 +1,273 @@ +package tx + +import ( + "context" + + "github.com/cosmos/gogoproto/proto" + "google.golang.org/protobuf/types/known/anypb" + + txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + txsigning "cosmossdk.io/x/tx/signing" + "cosmossdk.io/x/tx/signing/aminojson" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" +) + +// AuxTxBuilder is a client-side builder for creating an AuxSignerData. +type AuxTxBuilder struct { + // msgs is used to store the sdk.Msgs that are added to the + // TxBuilder. It's also added inside body.Messages, because: + // - b.msgs is used for constructing the AMINO sign bz, + // - b.body is used for constructing the DIRECT_AUX sign bz. + msgs []sdk.MsgV2 + body *txv1beta1.TxBody + auxSignerData *tx.AuxSignerData +} + +// NewAuxTxBuilder creates a new client-side builder for constructing an +// AuxSignerData. +func NewAuxTxBuilder() AuxTxBuilder { + return AuxTxBuilder{} +} + +// SetAddress sets the aux signer's bech32 address. +func (b *AuxTxBuilder) SetAddress(addr string) { + b.checkEmptyFields() + + b.auxSignerData.Address = addr +} + +// SetMemo sets a memo in the tx. +func (b *AuxTxBuilder) SetMemo(memo string) { + b.checkEmptyFields() + + b.body.Memo = memo + b.auxSignerData.SignDoc.BodyBytes = nil +} + +// SetTimeoutHeight sets a timeout height in the tx. +func (b *AuxTxBuilder) SetTimeoutHeight(height uint64) { + b.checkEmptyFields() + + b.body.TimeoutHeight = height + b.auxSignerData.SignDoc.BodyBytes = nil +} + +// SetMsgs sets an array of Msgs in the tx. +func (b *AuxTxBuilder) SetMsgs(msgs ...sdk.MsgV2) error { + anys := make([]*anypb.Any, len(msgs)) + for i, msg := range msgs { + legacyAny, err := codectypes.NewAnyWithValue(msg) + if err != nil { + return err + } + anys[i] = &anypb.Any{ + TypeUrl: legacyAny.TypeUrl, + Value: legacyAny.Value, + } + } + + b.checkEmptyFields() + + b.msgs = msgs + b.body.Messages = anys + b.auxSignerData.SignDoc.BodyBytes = nil + + return nil +} + +// SetAccountNumber sets the aux signer's account number in the AuxSignerData. +func (b *AuxTxBuilder) SetAccountNumber(accNum uint64) { + b.checkEmptyFields() + + b.auxSignerData.SignDoc.AccountNumber = accNum +} + +// SetChainID sets the chain id in the AuxSignerData. +func (b *AuxTxBuilder) SetChainID(chainID string) { + b.checkEmptyFields() + + b.auxSignerData.SignDoc.ChainId = chainID +} + +// SetSequence sets the aux signer's sequence in the AuxSignerData. +func (b *AuxTxBuilder) SetSequence(accSeq uint64) { + b.checkEmptyFields() + + b.auxSignerData.SignDoc.Sequence = accSeq +} + +// SetPubKey sets the aux signer's pubkey in the AuxSignerData. +func (b *AuxTxBuilder) SetPubKey(pk cryptotypes.PubKey) error { + anyPk, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return err + } + + b.checkEmptyFields() + b.auxSignerData.SignDoc.PublicKey = anyPk + + return nil +} + +// SetSignMode sets the aux signer's sign mode. Allowed sign modes are +// DIRECT_AUX and LEGACY_AMINO_JSON. +func (b *AuxTxBuilder) SetSignMode(mode signing.SignMode) error { + switch mode { + case signing.SignMode_SIGN_MODE_DIRECT_AUX, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON: + default: + return sdkerrors.ErrInvalidRequest.Wrapf("AuxTxBuilder can only sign with %s or %s", + signing.SignMode_SIGN_MODE_DIRECT_AUX, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) + } + + b.auxSignerData.Mode = mode + return nil +} + +// SetSignature sets the aux signer's signature in the AuxSignerData. +func (b *AuxTxBuilder) SetSignature(sig []byte) { + b.checkEmptyFields() + + b.auxSignerData.Sig = sig +} + +// SetExtensionOptions sets the aux signer's extension options. +func (b *AuxTxBuilder) SetExtensionOptions(extOpts ...*codectypes.Any) { + b.checkEmptyFields() + + anyExtOpts := make([]*anypb.Any, len(extOpts)) + for i, extOpt := range extOpts { + anyExtOpts[i] = &anypb.Any{ + TypeUrl: extOpt.TypeUrl, + Value: extOpt.Value, + } + } + b.body.ExtensionOptions = anyExtOpts + b.auxSignerData.SignDoc.BodyBytes = nil +} + +// SetSignature sets the aux signer's signature. +func (b *AuxTxBuilder) SetNonCriticalExtensionOptions(extOpts ...*codectypes.Any) { + b.checkEmptyFields() + + anyNonCritExtOpts := make([]*anypb.Any, len(extOpts)) + for i, extOpt := range extOpts { + anyNonCritExtOpts[i] = &anypb.Any{ + TypeUrl: extOpt.TypeUrl, + Value: extOpt.Value, + } + } + b.body.NonCriticalExtensionOptions = anyNonCritExtOpts + b.auxSignerData.SignDoc.BodyBytes = nil +} + +// GetSignBytes returns the builder's sign bytes. +func (b *AuxTxBuilder) GetSignBytes() ([]byte, error) { + auxTx := b.auxSignerData + if auxTx == nil { + return nil, sdkerrors.ErrLogic.Wrap("aux tx is nil, call setters on AuxTxBuilder first") + } + + body := b.body + if body == nil { + return nil, sdkerrors.ErrLogic.Wrap("tx body is nil, call setters on AuxTxBuilder first") + } + + sd := auxTx.SignDoc + if sd == nil { + return nil, sdkerrors.ErrLogic.Wrap("sign doc is nil, call setters on AuxTxBuilder first") + } + + bodyBz, err := proto.Marshal(body) + if err != nil { + return nil, err + } + + sd.BodyBytes = bodyBz + if err = sd.ValidateBasic(); err != nil { + return nil, err + } + + var signBz []byte + switch b.auxSignerData.Mode { + case signing.SignMode_SIGN_MODE_DIRECT_AUX: + { + signBz, err = proto.Marshal(b.auxSignerData.SignDoc) + if err != nil { + return nil, err + } + } + case signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON: + { + handler := aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{ + FileResolver: proto.HybridResolver, + }) + + auxBody := &txv1beta1.TxBody{ + Messages: body.Messages, + Memo: body.Memo, + TimeoutHeight: body.TimeoutHeight, + // AuxTxBuilder has no concern with extension options, so we set them to nil. + // This preserves pre-PR#16025 behavior where extension options were ignored, this code path: + // https://github.com/cosmos/cosmos-sdk/blob/ac3c209326a26b46f65a6cc6f5b5ebf6beb79b38/client/tx/aux_builder.go#L193 + // https://github.com/cosmos/cosmos-sdk/blob/ac3c209326a26b46f65a6cc6f5b5ebf6beb79b38/x/auth/migrations/legacytx/stdsign.go#L49 + ExtensionOptions: nil, + NonCriticalExtensionOptions: nil, + } + + signBz, err = handler.GetSignBytes( + context.Background(), + txsigning.SignerData{ + Address: b.auxSignerData.Address, + ChainID: b.auxSignerData.SignDoc.ChainId, + AccountNumber: b.auxSignerData.SignDoc.AccountNumber, + Sequence: b.auxSignerData.SignDoc.Sequence, + PubKey: nil, + }, + txsigning.TxData{ + Body: auxBody, + AuthInfo: &txv1beta1.AuthInfo{ + SignerInfos: nil, + // Aux signer never signs over fee. + // For LEGACY_AMINO_JSON, we use the convention to sign + // over empty fees. + // ref: https://github.com/cosmos/cosmos-sdk/pull/10348 + Fee: &txv1beta1.Fee{}, + }, + }, + ) + return signBz, err + } + default: + return nil, sdkerrors.ErrInvalidRequest.Wrapf("got unknown sign mode %s", b.auxSignerData.Mode) + } + + return signBz, nil +} + +// GetAuxSignerData returns the builder's AuxSignerData. +func (b *AuxTxBuilder) GetAuxSignerData() (tx.AuxSignerData, error) { + if err := b.auxSignerData.ValidateBasic(); err != nil { + return tx.AuxSignerData{}, err + } + + return *b.auxSignerData, nil +} + +func (b *AuxTxBuilder) checkEmptyFields() { + if b.body == nil { + b.body = &txv1beta1.TxBody{} + } + + if b.auxSignerData == nil { + b.auxSignerData = &tx.AuxSignerData{} + if b.auxSignerData.SignDoc == nil { + b.auxSignerData.SignDoc = &tx.SignDocDirectAux{} + } + } +} diff --git a/client/v2/tx/broadcast.go b/client/v2/tx/broadcast.go new file mode 100644 index 000000000000..8ecd7edcaae8 --- /dev/null +++ b/client/v2/tx/broadcast.go @@ -0,0 +1,140 @@ +package tx + +import ( + "context" + "fmt" + "github.com/cometbft/cometbft/mempool" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "strings" +) + +// BroadcastTx broadcasts a transactions either synchronously or asynchronously +// based on the context parameters. The result of the broadcast is parsed into +// an intermediate structure which is logged if the context has a logger +// defined. +func (ctx txContext) BroadcastTx(txBytes []byte) (res *sdk.TxResponse, err error) { + switch ctx.BroadcastMode { + case flags.BroadcastSync: + res, err = ctx.BroadcastTxSync(txBytes) + + case flags.BroadcastAsync: + res, err = ctx.BroadcastTxAsync(txBytes) + + default: + return nil, fmt.Errorf("unsupported return type %s; supported types: sync, async", ctx.BroadcastMode) + } + + return res, err +} + +// CheckCometError checks if the error returned from BroadcastTx is a +// CometBFT error that is returned before the tx is submitted due to +// precondition checks that failed. If an CometBFT error is detected, this +// function returns the correct code back in TxResponse. +// +// TODO: Avoid brittle string matching in favor of error matching. This requires +// a change to CometBFT's RPCError type to allow retrieval or matching against +// a concrete error type. +func CheckCometError(err error, tx cmttypes.Tx) *sdk.TxResponse { + if err == nil { + return nil + } + + errStr := strings.ToLower(err.Error()) + txHash := fmt.Sprintf("%X", tx.Hash()) + + switch { + case strings.Contains(errStr, strings.ToLower(mempool.ErrTxInCache.Error())): + return &sdk.TxResponse{ + Code: sdkerrors.ErrTxInMempoolCache.ABCICode(), + Codespace: sdkerrors.ErrTxInMempoolCache.Codespace(), + TxHash: txHash, + } + + case strings.Contains(errStr, "mempool is full"): + return &sdk.TxResponse{ + Code: sdkerrors.ErrMempoolIsFull.ABCICode(), + Codespace: sdkerrors.ErrMempoolIsFull.Codespace(), + TxHash: txHash, + } + + case strings.Contains(errStr, "tx too large"): + return &sdk.TxResponse{ + Code: sdkerrors.ErrTxTooLarge.ABCICode(), + Codespace: sdkerrors.ErrTxTooLarge.Codespace(), + TxHash: txHash, + } + + default: + return nil + } +} + +// BroadcastTxSync broadcasts transaction bytes to a CometBFT node +// synchronously (i.e. returns after CheckTx execution). +func (ctx txContext) BroadcastTxSync(txBytes []byte) (*sdk.TxResponse, error) { + node, err := ctx.GetNode() + if err != nil { + return nil, err + } + + res, err := node.BroadcastTxSync(context.Background(), txBytes) + if errRes := CheckCometError(err, txBytes); errRes != nil { + return errRes, nil + } + + return sdk.NewResponseFormatBroadcastTx(res), err +} + +// BroadcastTxAsync broadcasts transaction bytes to a CometBFT node +// asynchronously (i.e. returns immediately). +func (ctx txContext) BroadcastTxAsync(txBytes []byte) (*sdk.TxResponse, error) { + node, err := ctx.GetNode() + if err != nil { + return nil, err + } + + res, err := node.BroadcastTxAsync(context.Background(), txBytes) + if errRes := CheckCometError(err, txBytes); errRes != nil { + return errRes, nil + } + + return sdk.NewResponseFormatBroadcastTx(res), err +} + +// ServiceBroadcast is a helper function to broadcast a Tx with the correct gRPC types +// from the tx service. Calls `clientCtx.BroadcastTx` under the hood. +func ServiceBroadcast(_ context.Context, clientCtx txContext, req *tx.BroadcastTxRequest) (*tx.BroadcastTxResponse, error) { + if req == nil || req.TxBytes == nil { + return nil, status.Error(codes.InvalidArgument, "invalid empty tx") + } + + clientCtx = clientCtx.WithBroadcastMode(normalizeBroadcastMode(req.Mode)) + resp, err := clientCtx.BroadcastTx(req.TxBytes) + if err != nil { + return nil, err + } + + return &tx.BroadcastTxResponse{ + TxResponse: resp, + }, nil +} + +// normalizeBroadcastMode converts a broadcast mode into a normalized string +// to be passed into the clientCtx. +func normalizeBroadcastMode(mode tx.BroadcastMode) string { + switch mode { + case tx.BroadcastMode_BROADCAST_MODE_ASYNC: + return "async" + case tx.BroadcastMode_BROADCAST_MODE_SYNC: + return "sync" + default: + return "unspecified" + } +} diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go new file mode 100644 index 000000000000..7196e1021811 --- /dev/null +++ b/client/v2/tx/builder.go @@ -0,0 +1,29 @@ +package tx + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" +) + +type ExtendedTxBuilder interface { + SetExtensionOptions(extOpts ...*codectypes.Any) +} + +// TxBuilder defines an interface which an application-defined concrete transaction +// type must implement. Namely, it must be able to set messages, generate +// signatures, and provide canonical bytes to sign over. The transaction must +// also know how to encode itself. +type TxBuilder interface { + GetTx() TxV2 + SetMsgs(msgs ...sdk.MsgV2) error + SetSignatures(signatures ...signingtypes.SignatureV2) error + SetMemo(memo string) + SetFeeAmount(amount sdk.Coins) + SetFeePayer(feePayer sdk.AccAddress) + SetGasLimit(limit uint64) + SetTimeoutHeight(height uint64) + SetFeeGranter(feeGranter sdk.AccAddress) + AddAuxSignerData(tx.AuxSignerData) error +} diff --git a/client/v2/tx/cometbft.go b/client/v2/tx/cometbft.go new file mode 100644 index 000000000000..0b5ea64f224d --- /dev/null +++ b/client/v2/tx/cometbft.go @@ -0,0 +1,36 @@ +package tx + +import ( + "context" + + rpcclient "github.com/cometbft/cometbft/rpc/client" + coretypes "github.com/cometbft/cometbft/rpc/core/types" +) + +// CometRPC defines the interface of a CometBFT RPC client needed for +// queries and transaction handling. +type CometRPC interface { + rpcclient.ABCIClient + + Validators(ctx context.Context, height *int64, page, perPage *int) (*coretypes.ResultValidators, error) + Status(context.Context) (*coretypes.ResultStatus, error) + Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) + BlockByHash(ctx context.Context, hash []byte) (*coretypes.ResultBlock, error) + BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) + BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) + Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) + Tx(ctx context.Context, hash []byte, prove bool) (*coretypes.ResultTx, error) + TxSearch( + ctx context.Context, + query string, + prove bool, + page, perPage *int, + orderBy string, + ) (*coretypes.ResultTxSearch, error) + BlockSearch( + ctx context.Context, + query string, + page, perPage *int, + orderBy string, + ) (*coretypes.ResultBlockSearch, error) +} diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go new file mode 100644 index 000000000000..337cd3e0c963 --- /dev/null +++ b/client/v2/tx/config.go @@ -0,0 +1,30 @@ +package tx + +import ( + txsigning "cosmossdk.io/x/tx/signing" + sdk "github.com/cosmos/cosmos-sdk/types" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" +) + +// TxEncodingConfig defines an interface that contains transaction +// encoders and decoders +type TxEncodingConfig interface { + TxEncoder() sdk.TxV2Encoder + TxDecoder() sdk.TxV2Decoder + TxJSONEncoder() sdk.TxV2Encoder + TxJSONDecoder() sdk.TxV2Decoder + MarshalSignatureJSON([]signingtypes.SignatureV2) ([]byte, error) + UnmarshalSignatureJSON([]byte) ([]signingtypes.SignatureV2, error) +} + +// TxConfig defines an interface a client can utilize to generate an +// application-defined concrete transaction type. The type returned must +// implement TxBuilder. +type TxConfig interface { + TxEncodingConfig + + NewTxBuilder() TxBuilder + WrapTxBuilder(sdk.Tx) (TxBuilder, error) + SignModeHandler() *txsigning.HandlerMap + SigningContext() *txsigning.Context +} diff --git a/client/v2/tx/context.go b/client/v2/tx/context.go new file mode 100644 index 000000000000..cfc8286cdd97 --- /dev/null +++ b/client/v2/tx/context.go @@ -0,0 +1,424 @@ +package tx + +import ( + "bufio" + "context" + "cosmossdk.io/client/v2/autocli/keyring" + "encoding/json" + "fmt" + "io" + "os" + "path" + "strings" + + "github.com/cosmos/gogoproto/proto" + "github.com/spf13/viper" + "google.golang.org/grpc" + "sigs.k8s.io/yaml" + + "cosmossdk.io/core/address" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + 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 + +// txContext implements a typical context created in SDK modules for transaction +// handling and queries. +type txContext struct { + FromAddress sdk.AccAddress + Client CometRPC + GRPCClient *grpc.ClientConn + ChainID string + Codec codec.Codec + InterfaceRegistry codectypes.InterfaceRegistry + Input io.Reader + Keyring keyring.Keyring + KeyringOptions []keyring.Option + KeyringDir string + KeyringDefaultKeyName string + Output io.Writer + OutputFormat string + Height int64 + HomeDir string + From string + BroadcastMode string + FromName string + SignModeStr string + Simulate bool + GenerateOnly bool + Offline bool + SkipConfirm bool + TxConfig TxConfig + AccountRetriever AccountRetriever + NodeURI string + 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 + + // CmdContext is the context.txContext from the Cobra command. + CmdContext context.Context + + // Address codecs + AddressCodec address.Codec + ValidatorAddressCodec address.Codec + ConsensusAddressCodec address.Codec +} + +// WithCmdContext returns a copy of the context with an updated context.txContext, +// usually set to the cobra cmd context. +func (ctx txContext) WithCmdContext(c context.Context) txContext { + ctx.CmdContext = c + return ctx +} + +// WithKeyring returns a copy of the context with an updated keyring. +func (ctx txContext) WithKeyring(k keyring.Keyring) txContext { + ctx.Keyring = k + return ctx +} + +// WithKeyringOptions returns a copy of the context with an updated keyring. +func (ctx txContext) WithKeyringOptions(opts ...keyring.Option) txContext { + ctx.KeyringOptions = opts + return ctx +} + +// WithInput returns a copy of the context with an updated input. +func (ctx txContext) WithInput(r io.Reader) txContext { + // convert to a bufio.Reader to have a shared buffer between the keyring and the + // the Commands, ensuring a read from one advance the read pointer for the other. + // see https://github.com/cosmos/cosmos-sdk/issues/9566. + ctx.Input = bufio.NewReader(r) + return ctx +} + +// WithCodec returns a copy of the txContext with an updated Codec. +func (ctx txContext) WithCodec(m codec.Codec) txContext { + ctx.Codec = m + return ctx +} + +// WithOutput returns a copy of the context with an updated output writer (e.g. stdout). +func (ctx txContext) WithOutput(w io.Writer) txContext { + ctx.Output = w + return ctx +} + +// WithFrom returns a copy of the context with an updated from address or name. +func (ctx txContext) WithFrom(from string) txContext { + ctx.From = from + return ctx +} + +// WithOutputFormat returns a copy of the context with an updated OutputFormat field. +func (ctx txContext) WithOutputFormat(format string) txContext { + ctx.OutputFormat = format + return ctx +} + +// WithNodeURI returns a copy of the context with an updated node URI. +func (ctx txContext) WithNodeURI(nodeURI string) txContext { + ctx.NodeURI = nodeURI + return ctx +} + +// WithHeight returns a copy of the context with an updated height. +func (ctx txContext) WithHeight(height int64) txContext { + ctx.Height = height + return ctx +} + +// WithClient returns a copy of the context with an updated RPC client +// instance. +func (ctx txContext) WithClient(client CometRPC) txContext { + ctx.Client = client + return ctx +} + +// WithGRPCClient returns a copy of the context with an updated GRPC client +// instance. +func (ctx txContext) WithGRPCClient(grpcClient *grpc.ClientConn) txContext { + ctx.GRPCClient = grpcClient + return ctx +} + +// WithChainID returns a copy of the context with an updated chain ID. +func (ctx txContext) WithChainID(chainID string) txContext { + ctx.ChainID = chainID + return ctx +} + +// WithHomeDir returns a copy of the txContext with HomeDir set. +func (ctx txContext) WithHomeDir(dir string) txContext { + if dir != "" { + ctx.HomeDir = dir + } + return ctx +} + +// WithKeyringDir returns a copy of the txContext with KeyringDir set. +func (ctx txContext) WithKeyringDir(dir string) txContext { + ctx.KeyringDir = dir + return ctx +} + +// WithKeyringDefaultKeyName returns a copy of the txContext with KeyringDefaultKeyName set. +func (ctx txContext) WithKeyringDefaultKeyName(keyName string) txContext { + ctx.KeyringDefaultKeyName = keyName + return ctx +} + +// WithGenerateOnly returns a copy of the context with updated GenerateOnly value +func (ctx txContext) WithGenerateOnly(generateOnly bool) txContext { + ctx.GenerateOnly = generateOnly + return ctx +} + +// WithSimulation returns a copy of the context with updated Simulate value +func (ctx txContext) WithSimulation(simulate bool) txContext { + ctx.Simulate = simulate + return ctx +} + +// WithOffline returns a copy of the context with updated Offline value. +func (ctx txContext) WithOffline(offline bool) txContext { + ctx.Offline = offline + return ctx +} + +// WithFromName returns a copy of the context with an updated from account name. +func (ctx txContext) WithFromName(name string) txContext { + ctx.FromName = name + return ctx +} + +// WithFromAddress returns a copy of the context with an updated from account +// address. +func (ctx txContext) WithFromAddress(addr sdk.AccAddress) txContext { + ctx.FromAddress = addr + return ctx +} + +// WithFeePayerAddress returns a copy of the context with an updated fee payer account +// address. +func (ctx txContext) WithFeePayerAddress(addr sdk.AccAddress) txContext { + ctx.FeePayer = addr + return ctx +} + +// WithFeeGranterAddress returns a copy of the context with an updated fee granter account +// address. +func (ctx txContext) WithFeeGranterAddress(addr sdk.AccAddress) txContext { + ctx.FeeGranter = addr + return ctx +} + +// WithBroadcastMode returns a copy of the context with an updated broadcast +// mode. +func (ctx txContext) WithBroadcastMode(mode string) txContext { + ctx.BroadcastMode = mode + return ctx +} + +// WithSignModeStr returns a copy of the context with an updated SignMode +// value. +func (ctx txContext) WithSignModeStr(signModeStr string) txContext { + ctx.SignModeStr = signModeStr + return ctx +} + +// WithSkipConfirmation returns a copy of the context with an updated SkipConfirm +// value. +func (ctx txContext) WithSkipConfirmation(skip bool) txContext { + ctx.SkipConfirm = skip + return ctx +} + +// WithTxConfig returns the context with an updated TxConfig +func (ctx txContext) WithTxConfig(generator TxConfig) txContext { + ctx.TxConfig = generator + return ctx +} + +// WithAccountRetriever returns the context with an updated AccountRetriever +func (ctx txContext) WithAccountRetriever(retriever AccountRetriever) txContext { + ctx.AccountRetriever = retriever + return ctx +} + +// WithInterfaceRegistry returns the context with an updated InterfaceRegistry +func (ctx txContext) WithInterfaceRegistry(interfaceRegistry codectypes.InterfaceRegistry) txContext { + ctx.InterfaceRegistry = interfaceRegistry + return ctx +} + +// WithViper returns the context with Viper field. This Viper instance is used to read +// client-side config from the config file. +func (ctx txContext) WithViper(prefix string) txContext { + v := viper.New() + + if prefix == "" { + executableName, _ := os.Executable() + prefix = path.Base(executableName) + } + + v.SetEnvPrefix(prefix) + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + v.AutomaticEnv() + ctx.Viper = v + return ctx +} + +// WithAux returns a copy of the context with an updated IsAux value. +func (ctx txContext) WithAux(isAux bool) txContext { + ctx.IsAux = isAux + return ctx +} + +// WithPreprocessTxHook returns the context with the provided preprocessing hook, which +// enables chains to preprocess the transaction using the builder. +func (ctx txContext) WithPreprocessTxHook(preprocessFn PreprocessTxFn) txContext { + ctx.PreprocessTxHook = preprocessFn + return ctx +} + +// WithAddressCodec returns the context with the provided address codec. +func (ctx txContext) WithAddressCodec(addressCodec address.Codec) txContext { + ctx.AddressCodec = addressCodec + return ctx +} + +// WithValidatorAddressCodec returns the context with the provided validator address codec. +func (ctx txContext) WithValidatorAddressCodec(validatorAddressCodec address.Codec) txContext { + ctx.ValidatorAddressCodec = validatorAddressCodec + return ctx +} + +// WithConsensusAddressCodec returns the context with the provided consensus address codec. +func (ctx txContext) WithConsensusAddressCodec(consensusAddressCodec address.Codec) txContext { + ctx.ConsensusAddressCodec = consensusAddressCodec + return ctx +} + +// PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout +func (ctx txContext) PrintString(str string) error { + return ctx.PrintBytes([]byte(str)) +} + +// PrintBytes prints the raw bytes to ctx.Output if it's defined, otherwise to os.Stdout. +// NOTE: for printing a complex state object, you should use ctx.PrintOutput +func (ctx txContext) PrintBytes(o []byte) error { + writer := ctx.Output + if writer == nil { + writer = os.Stdout + } + + _, err := writer.Write(o) + return err +} + +// PrintProto outputs toPrint to the ctx.Output based on ctx.OutputFormat which is +// either text or json. If text, toPrint will be YAML encoded. Otherwise, toPrint +// will be JSON encoded using ctx.Codec. An error is returned upon failure. +func (ctx txContext) PrintProto(toPrint proto.Message) error { + // always serialize JSON initially because proto json can't be directly YAML encoded + out, err := ctx.Codec.MarshalJSON(toPrint) + if err != nil { + return err + } + return ctx.printOutput(out) +} + +// PrintRaw is a variant of PrintProto that doesn't require a proto.Message type +// and uses a raw JSON message. No marshaling is performed. +func (ctx txContext) PrintRaw(toPrint json.RawMessage) error { + return ctx.printOutput(toPrint) +} + +func (ctx txContext) printOutput(out []byte) error { + var err error + if ctx.OutputFormat == "text" { + out, err = yaml.JSONToYAML(out) + if err != nil { + return err + } + } + + writer := ctx.Output + if writer == nil { + writer = os.Stdout + } + + _, err = writer.Write(out) + if err != nil { + return err + } + + if ctx.OutputFormat != "text" { + // append new-line for formats besides YAML + _, err = writer.Write([]byte("\n")) + if err != nil { + return err + } + } + + return nil +} + +// GetFromFields returns a from account address, account name and keyring type, given either an address or key name. +// If clientCtx.Simulate is true the keystore is not accessed and a valid address must be provided +// If clientCtx.GenerateOnly is true the keystore is only accessed if a key name is provided +// If from is empty, the default key if specified in the context will be used +func GetFromFields(clientCtx txContext, kr keyring.Keyring, from string) (sdk.AccAddress, string, keyring.KeyType, error) { + if from == "" && clientCtx.KeyringDefaultKeyName != "" { + from = clientCtx.KeyringDefaultKeyName + _ = clientCtx.PrintString(fmt.Sprintf("No key name or address provided; using the default key: %s\n", clientCtx.KeyringDefaultKeyName)) + } + + if from == "" { + return nil, "", 0, nil + } + + addr, err := clientCtx.AddressCodec.StringToBytes(from) + switch { + case clientCtx.Simulate: + if err != nil { + return nil, "", 0, fmt.Errorf("a valid address must be provided in simulation mode: %w", err) + } + + return addr, "", 0, nil + + case clientCtx.GenerateOnly: + if err == nil { + return addr, "", 0, nil + } + } + + var k *keyring.Record + if err == nil { + k, err = kr.KeyByAddress(addr) + if err != nil { + return nil, "", 0, err + } + } else { + k, err = kr.Key(from) + if err != nil { + return nil, "", 0, err + } + } + + addr, err = kr.GetRecordAddress(k) + if err != nil { + return nil, "", 0, err + } + + return addr, kr.GetRecordName(k), kr.GetRecordType(k), nil +} diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go new file mode 100644 index 000000000000..17b6bf8d0f80 --- /dev/null +++ b/client/v2/tx/factory.go @@ -0,0 +1,502 @@ +package tx + +import ( + "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/math" + "errors" + "fmt" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + + "github.com/cosmos/cosmos-sdk/client/flags" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/go-bip39" + "github.com/spf13/pflag" + "math/big" + "os" + "strings" +) + +// Factory defines a client transaction factory that facilitates generating and +// signing an application-specific transaction. +type Factory struct { + keybase keyring.Keyring + + // TODO: check if autocli (AppOptions) already manages these fields + txConfig TxConfig + accountRetriever AccountRetriever + + // account related + accountNumber uint64 + sequence uint64 + + // fees related + fees sdk.Coins + feeGranter sdk.AccAddress + feePayer sdk.AccAddress + + // gas related + gas uint64 + gasAdjustment float64 + gasPrices sdk.DecCoins + + // tx generation config params + timeoutHeight uint64 + chainID string + fromName string + offline bool + memo string + generateOnly bool + simulateAndExecute bool + + extOptions []*codectypes.Any + signMode signing.SignMode // TODO: check if autocli (AppOptions) already manages this + + preprocessTxHook PreprocessTxFn +} + +func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) { + if clientCtx.Viper == nil { + clientCtx = clientCtx.WithViper("") + } + + if err := clientCtx.Viper.BindPFlags(flagSet); err != nil { + return Factory{}, fmt.Errorf("failed to bind flags to viper: %w", err) + } + + signMode := signing.SignMode_SIGN_MODE_UNSPECIFIED + switch clientCtx.SignModeStr { + case flags.SignModeDirect: + signMode = signing.SignMode_SIGN_MODE_DIRECT + case flags.SignModeLegacyAminoJSON: + signMode = signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON + case flags.SignModeDirectAux: + signMode = signing.SignMode_SIGN_MODE_DIRECT_AUX + case flags.SignModeTextual: + signMode = signing.SignMode_SIGN_MODE_TEXTUAL + case flags.SignModeEIP191: + signMode = signing.SignMode_SIGN_MODE_EIP_191 + } + + var accNum, accSeq uint64 + if clientCtx.Offline { + if flagSet.Changed(flags.FlagAccountNumber) && flagSet.Changed(flags.FlagSequence) { + accNum = clientCtx.Viper.GetUint64(flags.FlagAccountNumber) + accSeq = clientCtx.Viper.GetUint64(flags.FlagSequence) + } else { + return Factory{}, fmt.Errorf("account-number and sequence must be set in offline mode") + } + } + + gasAdj := clientCtx.Viper.GetFloat64(flags.FlagGasAdjustment) + memo := clientCtx.Viper.GetString(flags.FlagNote) + timeoutHeight := clientCtx.Viper.GetUint64(flags.FlagTimeoutHeight) + + gasStr := clientCtx.Viper.GetString(flags.FlagGas) + gasSetting, _ := flags.ParseGasSetting(gasStr) + + f := Factory{ + txConfig: clientCtx.TxConfig, + accountRetriever: clientCtx.AccountRetriever, + keybase: clientCtx.Keyring, + chainID: clientCtx.ChainID, + fromName: clientCtx.FromName, + offline: clientCtx.Offline, + generateOnly: clientCtx.GenerateOnly, + gas: gasSetting.Gas, + simulateAndExecute: gasSetting.Simulate, + accountNumber: accNum, + sequence: accSeq, + timeoutHeight: timeoutHeight, + gasAdjustment: gasAdj, + memo: memo, + signMode: signMode, + feeGranter: clientCtx.FeeGranter, + feePayer: clientCtx.FeePayer, + } + + feesStr := clientCtx.Viper.GetString(flags.FlagFees) + f = f.WithFees(feesStr) + + gasPricesStr := clientCtx.Viper.GetString(flags.FlagGasPrices) + f = f.WithGasPrices(gasPricesStr) + + f = f.WithPreprocessTxHook(clientCtx.PreprocessTxHook) + + return f, nil +} + +// 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. +// Note: When in offline mode, the Prepare does nothing and returns the original factory. +func (f Factory) Prepare(clientCtx txContext) (Factory, error) { + if clientCtx.Offline { + return f, nil + } + + fc := f + from := clientCtx.FromAddress + + 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 +} + +// 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.MsgV2) (TxBuilder, error) { + if f.offline && f.generateOnly { + if f.chainID != "" { + return nil, errors.New("chain ID cannot be used when offline and generate-only flags are set") + } + } else if f.chainID == "" { + return nil, errors.New("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") + } + + // f.gas is a uint64 and we should convert to LegacyDec + // without the risk of under/overflow via uint64->int64. + glDec := math.LegacyNewDecFromBigInt(new(big.Int).SetUint64(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()) + } + } + + // Prevent simple inclusion of a valid mnemonic in the memo field + if f.memo != "" && bip39.IsMnemonicValid(strings.ToLower(f.memo)) { + return nil, errors.New("cannot provide a valid mnemonic seed in the memo field") + } + + 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.SetFeeGranter(f.feeGranter) + tx.SetFeePayer(f.feePayer) + tx.SetTimeoutHeight(f.TimeoutHeight()) + + if etx, ok := tx.(ExtendedTxBuilder); ok { + etx.SetExtensionOptions(f.extOptions...) + } + + 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 txContext, msgs ...sdk.MsgV2) error { + if f.SimulateAndExecute() { + if clientCtx.Offline { + return errors.New("cannot estimate gas in offline mode") + } + + // Prepare TxFactory with acc & seq numbers as CalculateGas requires + // account and sequence numbers to be set + preparedTxf, err := f.Prepare(clientCtx) + if err != nil { + return err + } + + _, adjusted, err := CalculateGas(clientCtx, preparedTxf, msgs...) + if err != nil { + return err + } + + f = f.WithGas(adjusted) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: f.Gas()}) + } + + unsignedTx, err := f.BuildUnsignedTx(msgs...) + if err != nil { + return err + } + + encoder := f.txConfig.TxJSONEncoder() + if encoder == nil { + return errors.New("cannot print unsigned tx: tx json encoder is nil") + } + + json, err := encoder(unsignedTx.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.MsgV2) ([]byte, error) { + txb, err := f.BuildUnsignedTx(msgs...) + if err != nil { + return nil, err + } + + pk, err := f.getSimPK() + 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: pk, + Data: f.getSimSignatureData(pk), + Sequence: f.Sequence(), + } + if err := txb.SetSignatures(sig); err != nil { + return nil, err + } + + encoder := f.txConfig.TxEncoder() + if encoder == nil { + return nil, fmt.Errorf("cannot simulate tx: tx encoder is nil") + } + + return encoder(txb.GetTx()) +} + +// WithTxConfig returns a copy of the Factory with an updated TxConfig. +func (f Factory) WithTxConfig(g TxConfig) Factory { + f.txConfig = g + return f +} + +// WithAccountRetriever returns a copy of the Factory with an updated AccountRetriever. +func (f Factory) WithAccountRetriever(ar AccountRetriever) Factory { + f.accountRetriever = ar + return f +} + +// WithChainID returns a copy of the Factory with an updated chainID. +func (f Factory) WithChainID(chainID string) Factory { + f.chainID = chainID + return f +} + +// WithGas returns a copy of the Factory with an updated gas value. +func (f Factory) WithGas(gas uint64) Factory { + f.gas = gas + return f +} + +// WithFees returns a copy of the Factory with an updated fee. +func (f Factory) WithFees(fees string) Factory { + parsedFees, err := sdk.ParseCoinsNormalized(fees) + if err != nil { + panic(err) + } + + f.fees = parsedFees + return f +} + +// WithGasPrices returns a copy of the Factory with updated gas prices. +func (f Factory) WithGasPrices(gasPrices string) Factory { + parsedGasPrices, err := sdk.ParseDecCoins(gasPrices) + if err != nil { + panic(err) + } + + f.gasPrices = parsedGasPrices + return f +} + +// WithKeybase returns a copy of the Factory with updated Keybase. +func (f Factory) WithKeybase(keybase keyring.Keyring) Factory { + f.keybase = keybase + return f +} + +// WithFromName returns a copy of the Factory with updated fromName +// fromName will be use for building a simulation tx. +func (f Factory) WithFromName(fromName string) Factory { + f.fromName = fromName + return f +} + +// WithSequence returns a copy of the Factory with an updated sequence number. +func (f Factory) WithSequence(sequence uint64) Factory { + f.sequence = sequence + return f +} + +// WithMemo returns a copy of the Factory with an updated memo. +func (f Factory) WithMemo(memo string) Factory { + f.memo = memo + return f +} + +// WithAccountNumber returns a copy of the Factory with an updated account number. +func (f Factory) WithAccountNumber(accnum uint64) Factory { + f.accountNumber = accnum + return f +} + +// WithGasAdjustment returns a copy of the Factory with an updated gas adjustment. +func (f Factory) WithGasAdjustment(gasAdj float64) Factory { + f.gasAdjustment = gasAdj + return f +} + +// WithSimulateAndExecute returns a copy of the Factory with an updated gas +// simulation value. +func (f Factory) WithSimulateAndExecute(sim bool) Factory { + f.simulateAndExecute = sim + return f +} + +// WithSignMode returns a copy of the Factory with an updated sign mode value. +func (f Factory) WithSignMode(mode signing.SignMode) Factory { + f.signMode = mode + return f +} + +// WithTimeoutHeight returns a copy of the Factory with an updated timeout height. +func (f Factory) WithTimeoutHeight(height uint64) Factory { + f.timeoutHeight = height + return f +} + +// WithFeeGranter returns a copy of the Factory with an updated fee granter. +func (f Factory) WithFeeGranter(fg sdk.AccAddress) Factory { + f.feeGranter = fg + return f +} + +// WithFeePayer returns a copy of the Factory with an updated fee granter. +func (f Factory) WithFeePayer(fp sdk.AccAddress) Factory { + f.feePayer = fp + 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 PreprocessTxFn) Factory { + f.preprocessTxHook = preprocessFn + return f +} + +func (f Factory) WithExtensionOptions(extOpts ...*codectypes.Any) Factory { + f.extOptions = extOpts + return f +} + +// PreprocessTx calls the preprocessing hook with the factory parameters and +// returns the result. +func (f Factory) PreprocessTx(keyname string, builder 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, f.Keybase().GetRecordType(key), builder) +} + +func (f Factory) AccountNumber() uint64 { return f.accountNumber } +func (f Factory) Sequence() uint64 { return f.sequence } +func (f Factory) Gas() uint64 { return f.gas } +func (f Factory) GasAdjustment() float64 { return f.gasAdjustment } +func (f Factory) Keybase() keyring.Keyring { return f.keybase } +func (f Factory) ChainID() string { return f.chainID } +func (f Factory) Memo() string { return f.memo } +func (f Factory) Fees() sdk.Coins { return f.fees } +func (f Factory) GasPrices() sdk.DecCoins { return f.gasPrices } +func (f Factory) AccountRetriever() AccountRetriever { return f.accountRetriever } +func (f Factory) TimeoutHeight() uint64 { return f.timeoutHeight } +func (f Factory) FromName() string { return f.fromName } +func (f Factory) SimulateAndExecute() bool { return f.simulateAndExecute } +func (f Factory) SignMode() signing.SignMode { + return f.signMode +} + +// getSimPK gets the public key to use for building a simulation tx. +// Note, we should only check for keys in the keybase if we are in simulate and execute mode, +// e.g. when using --gas=auto. +// When using --dry-run, we are is simulation mode only and should not check the keybase. +// Ref: https://github.com/cosmos/cosmos-sdk/issues/11283 +func (f Factory) getSimPK() (cryptotypes.PubKey, error) { + var ( + err error + pk cryptotypes.PubKey = &secp256k1.PubKey{} // use default public key type + ) + + if f.simulateAndExecute && f.keybase != nil { + pk, err = f.keybase.GetPubKey(f.fromName) + if err != nil { + return nil, err + } + } + + return pk, nil +} + +// getSimSignatureData based on the pubKey type gets the correct SignatureData type +// to use for building a simulation tx. +func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) signing.SignatureData { + multisigPubKey, ok := pk.(*multisig.LegacyAminoPubKey) // TODO: abstract out multisig pubkey + if !ok { + return &signing.SingleSignatureData{SignMode: f.signMode} + } + + multiSignatureData := make([]signing.SignatureData, 0, multisigPubKey.Threshold) + for i := uint32(0); i < multisigPubKey.Threshold; i++ { + multiSignatureData = append(multiSignatureData, &signing.SingleSignatureData{ + SignMode: f.SignMode(), + }) + } + + return &signing.MultiSignatureData{ + Signatures: multiSignatureData, + } +} diff --git a/client/v2/tx/query.go b/client/v2/tx/query.go new file mode 100644 index 000000000000..b6d16ff570e0 --- /dev/null +++ b/client/v2/tx/query.go @@ -0,0 +1,307 @@ +package tx + +import ( + "context" + errorsmod "cosmossdk.io/errors" + "fmt" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + "github.com/cosmos/cosmos-sdk/types/tx" + gogogrpc "github.com/cosmos/gogoproto/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/metadata" + "reflect" + "strconv" + "strings" + + "github.com/cockroachdb/errors" + abci "github.com/cometbft/cometbft/abci/types" + rpcclient "github.com/cometbft/cometbft/rpc/client" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "cosmossdk.io/store/rootmulti" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + _ gogogrpc.ClientConn = txContext{} + + // fallBackCodec is used by Context in case Codec is not set. + // it can process every gRPC type, except the ones which contain + // interfaces in their types. + fallBackCodec = codec.NewProtoCodec(types.NewInterfaceRegistry()) +) + +// GetNode returns an RPC client. If the context's client is not defined, an +// error is returned. +func (ctx txContext) GetNode() (CometRPC, error) { + if ctx.Client == nil { + return nil, errors.New("no RPC client is defined in offline mode") + } + + return ctx.Client, nil +} + +// Query performs a query to a CometBFT node with the provided path. +// It returns the result and height of the query upon success or an error if +// the query fails. +func (ctx txContext) Query(path string) ([]byte, int64, error) { + return ctx.query(path, nil) +} + +// QueryWithData performs a query to a CometBFT node with the provided path +// and a data payload. It returns the result and height of the query upon success +// or an error if the query fails. +func (ctx txContext) QueryWithData(path string, data []byte) ([]byte, int64, error) { + return ctx.query(path, data) +} + +// QueryStore performs a query to a CometBFT node with the provided key and +// store name. It returns the result and height of the query upon success +// or an error if the query fails. +func (ctx txContext) QueryStore(key []byte, storeName string) ([]byte, int64, error) { + return ctx.queryStore(key, storeName, "key") +} + +// QueryABCI performs a query to a CometBFT node with the provide RequestQuery. +// It returns the ResultQuery obtained from the query. The height used to perform +// the query is the RequestQuery Height if it is non-zero, otherwise the context +// height is used. +func (ctx txContext) QueryABCI(req abci.RequestQuery) (abci.ResponseQuery, error) { + return ctx.queryABCI(req) +} + +// GetFromAddress returns the from address from the context's name. +func (ctx txContext) GetFromAddress() sdk.AccAddress { + return ctx.FromAddress +} + +// Invoke implements the grpc ClientConn.Invoke method +func (ctx txContext) Invoke(grpcCtx context.Context, method string, req, reply interface{}, opts ...grpc.CallOption) (err error) { + // Two things can happen here: + // 1. either we're broadcasting a Tx, in which call we call CometBFT's broadcast endpoint directly, + // 2-1. or we are querying for state, in which case we call grpc if grpc client set. + // 2-2. or we are querying for state, in which case we call ABCI's Query if grpc client not set. + + // In both cases, we don't allow empty request args (it will panic unexpectedly). + if reflect.ValueOf(req).IsNil() { + return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "request cannot be nil") + } + + // Case 1. Broadcasting a Tx. + if reqProto, ok := req.(*tx.BroadcastTxRequest); ok { + res, ok := reply.(*tx.BroadcastTxResponse) + if !ok { + return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "expected %T, got %T", (*tx.BroadcastTxResponse)(nil), req) + } + + broadcastRes, err := TxServiceBroadcast(grpcCtx, ctx, reqProto) + if err != nil { + return err + } + *res = *broadcastRes + + return err + } + + if ctx.GRPCClient != nil { + // Case 2-1. Invoke grpc. + return ctx.GRPCClient.Invoke(grpcCtx, method, req, reply, opts...) + } + + // Case 2-2. Querying state via abci query. + reqBz, err := ctx.gRPCCodec().Marshal(req) + if err != nil { + return err + } + + // parse height header + md, _ := metadata.FromOutgoingContext(grpcCtx) + if heights := md.Get(grpctypes.GRPCBlockHeightHeader); len(heights) > 0 { + height, err := strconv.ParseInt(heights[0], 10, 64) + if err != nil { + return err + } + if height < 0 { + return errorsmod.Wrapf( + sdkerrors.ErrInvalidRequest, + "client.Context.Invoke: height (%d) from %q must be >= 0", height, grpctypes.GRPCBlockHeightHeader) + } + + ctx = ctx.WithHeight(height) + } + + abciReq := abci.RequestQuery{ + Path: method, + Data: reqBz, + Height: ctx.Height, + } + + res, err := ctx.QueryABCI(abciReq) + if err != nil { + return err + } + + err = ctx.gRPCCodec().Unmarshal(res.Value, reply) + if err != nil { + return err + } + + // Create header metadata. For now the headers contain: + // - block height + // We then parse all the call options, if the call option is a + // HeaderCallOption, then we manually set the value of that header to the + // metadata. + md = metadata.Pairs(grpctypes.GRPCBlockHeightHeader, strconv.FormatInt(res.Height, 10)) + for _, callOpt := range opts { + header, ok := callOpt.(grpc.HeaderCallOption) + if !ok { + continue + } + + *header.HeaderAddr = md + } + + if ctx.InterfaceRegistry != nil { + return types.UnpackInterfaces(reply, ctx.InterfaceRegistry) + } + + return nil +} + +// NewStream implements the grpc ClientConn.NewStream method +func (txContext) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, fmt.Errorf("streaming rpc not supported") +} + +// gRPCCodec checks if Context's Codec is codec.GRPCCodecProvider +// otherwise it returns fallBackCodec. +func (ctx txContext) gRPCCodec() encoding.Codec { + if ctx.Codec == nil { + return fallBackCodec.GRPCCodec() + } + + pc, ok := ctx.Codec.(codec.GRPCCodecProvider) + if !ok { + return fallBackCodec.GRPCCodec() + } + + return pc.GRPCCodec() +} + +func (ctx txContext) queryABCI(req abci.RequestQuery) (abci.ResponseQuery, error) { + node, err := ctx.GetNode() + if err != nil { + return abci.ResponseQuery{}, err + } + + var queryHeight int64 + if req.Height != 0 { + queryHeight = req.Height + } else { + // fallback on the context height + queryHeight = ctx.Height + } + + opts := rpcclient.ABCIQueryOptions{ + Height: queryHeight, + Prove: req.Prove, + } + + result, err := node.ABCIQueryWithOptions(context.Background(), req.Path, req.Data, opts) + if err != nil { + return abci.ResponseQuery{}, err + } + + if !result.Response.IsOK() { + return abci.ResponseQuery{}, sdkErrorToGRPCError(result.Response) + } + + // data from trusted node or subspace query doesn't need verification + if !opts.Prove || !isQueryStoreWithProof(req.Path) { + return result.Response, nil + } + + return result.Response, nil +} + +// TxServiceBroadcast is a helper function to broadcast a Tx with the correct gRPC types +// from the tx service. Calls `clientCtx.BroadcastTx` under the hood. +func TxServiceBroadcast(_ context.Context, clientCtx txContext, req *tx.BroadcastTxRequest) (*tx.BroadcastTxResponse, error) { + if req == nil || req.TxBytes == nil { + return nil, status.Error(codes.InvalidArgument, "invalid empty tx") + } + + clientCtx = clientCtx.WithBroadcastMode(normalizeBroadcastMode(req.Mode)) + resp, err := clientCtx.BroadcastTx(req.TxBytes) + if err != nil { + return nil, err + } + + return &tx.BroadcastTxResponse{ + TxResponse: resp, + }, nil +} + +func sdkErrorToGRPCError(resp abci.ResponseQuery) error { + switch resp.Code { + case sdkerrors.ErrInvalidRequest.ABCICode(): + return status.Error(codes.InvalidArgument, resp.Log) + case sdkerrors.ErrUnauthorized.ABCICode(): + return status.Error(codes.Unauthenticated, resp.Log) + case sdkerrors.ErrKeyNotFound.ABCICode(): + return status.Error(codes.NotFound, resp.Log) + default: + return status.Error(codes.Unknown, resp.Log) + } +} + +// query performs a query to a CometBFT node with the provided store name +// and path. It returns the result and height of the query upon success +// or an error if the query fails. +func (ctx txContext) query(path string, key []byte) ([]byte, int64, error) { + resp, err := ctx.queryABCI(abci.RequestQuery{ + Path: path, + Data: key, + Height: ctx.Height, + }) + if err != nil { + return nil, 0, err + } + + return resp.Value, resp.Height, nil +} + +// queryStore performs a query to a CometBFT node with the provided a store +// name and path. It returns the result and height of the query upon success +// or an error if the query fails. +func (ctx txContext) queryStore(key []byte, storeName, endPath string) ([]byte, int64, error) { + path := fmt.Sprintf("/store/%s/%s", storeName, endPath) + return ctx.query(path, key) +} + +// isQueryStoreWithProof expects a format like /// +// queryType must be "store" and subpath must be "key" to require a proof. +func isQueryStoreWithProof(path string) bool { + if !strings.HasPrefix(path, "/") { + return false + } + + paths := strings.SplitN(path[1:], "/", 3) + + switch { + case len(paths) != 3: + return false + case paths[0] != "store": + return false + case rootmulti.RequireProof("/" + paths[2]): + return true + } + + return false +} diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go new file mode 100644 index 000000000000..ecdafc5eeff1 --- /dev/null +++ b/client/v2/tx/tx.go @@ -0,0 +1,379 @@ +package tx + +import ( + "bufio" + "context" + authsigning "cosmossdk.io/x/auth/signing" + "errors" + "fmt" + "github.com/cosmos/cosmos-sdk/client/input" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + gogogrpc "github.com/cosmos/gogoproto/grpc" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/spf13/pflag" + "os" +) + +// GasEstimateResponse defines a response definition for tx gas estimation. +type GasEstimateResponse struct { + GasEstimate uint64 `json:"gas_estimate" yaml:"gas_estimate"` +} + +func (gr GasEstimateResponse) String() string { + return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) +} + +// GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction +// or sign it and broadcast it returning an error upon failure. +func GenerateOrBroadcastTxCLI(clientCtx txContext, flagSet *pflag.FlagSet, msgs ...sdk.MsgV2) error { + txf, err := NewFactoryCLI(clientCtx, flagSet) + if err != nil { + return err + } + + return GenerateOrBroadcastTxWithFactory(clientCtx, txf, msgs...) +} + +// GenerateOrBroadcastTxWithFactory will either generate and print an unsigned transaction +// or sign it and broadcast it returning an error upon failure. +func GenerateOrBroadcastTxWithFactory(clientCtx txContext, txf Factory, msgs ...sdk.MsgV2) error { + // Validate all msgs before generating or broadcasting the tx. + // We were calling ValidateBasic separately in each CLI handler before. + // Right now, we're factorizing that call inside this function. + // ref: https://github.com/cosmos/cosmos-sdk/pull/9236#discussion_r623803504 + for _, msg := range msgs { + m, ok := msg.(sdk.HasValidateBasic) + if !ok { + continue + } + + if err := m.ValidateBasic(); err != nil { + return err + } + } + + // If the --aux flag is set, we simply generate and print the AuxSignerData. + if clientCtx.IsAux { + auxSignerData, err := makeAuxSignerData(clientCtx, txf, msgs...) + if err != nil { + return err + } + + return clientCtx.PrintProto(&auxSignerData) + } + + if clientCtx.GenerateOnly { + return txf.PrintUnsignedTx(clientCtx, msgs...) + } + + return BroadcastTx(clientCtx, txf, msgs...) +} + +// 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 txContext, txf Factory, msgs ...sdk.MsgV2) error { + txf, err := txf.Prepare(clientCtx) + if err != nil { + return err + } + + if txf.SimulateAndExecute() || clientCtx.Simulate { + 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()}) + } + + if clientCtx.Simulate { + return nil + } + + tx, err := txf.BuildUnsignedTx(msgs...) + if err != nil { + return err + } + + if !clientCtx.SkipConfirm { + encoder := txf.txConfig.TxJSONEncoder() + if encoder == nil { + return errors.New("failed to encode transaction: tx json encoder is nil") + } + + txBytes, err := encoder(tx.GetTx()) + if err != nil { + return fmt.Errorf("failed to encode transaction: %w", err) + } + + if err := clientCtx.PrintRaw(txBytes); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "error: %v\n%s\n", err, txBytes) + } + + buf := bufio.NewReader(os.Stdin) + ok, err := input.GetConfirmation("confirm transaction before signing and broadcasting", buf, os.Stderr) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "error: %v\ncanceled transaction\n", err) + return err + } + if !ok { + _, _ = fmt.Fprintln(os.Stderr, "canceled transaction") + return nil + } + } + + if err = Sign(clientCtx.CmdContext, txf, clientCtx.FromName, tx, true); err != nil { + return err + } + + txBytes, err := clientCtx.TxConfig.TxEncoder()(tx.GetTx()) + if err != nil { + return err + } + + // broadcast to a CometBFT node + res, err := clientCtx.BroadcastTx(txBytes) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) +} + +// 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.MsgV2, +) (*tx.SimulateResponse, uint64, error) { + txBytes, err := txf.BuildSimTx(msgs...) + if err != nil { + return nil, 0, err + } + + txSvcClient := tx.NewServiceClient(clientCtx) + simRes, err := txSvcClient.Simulate(context.Background(), &tx.SimulateRequest{ + TxBytes: txBytes, + }) + if err != nil { + return nil, 0, err + } + + return simRes, uint64(txf.GasAdjustment() * float64(simRes.GasInfo.GasUsed)), nil +} + +// Sign signs a given tx with a named key. The bytes signed over are canconical. +// The resulting signature will be added to the transaction builder overwriting the previous +// ones if overwrite=true (otherwise, the signature will be appended). +// Signing a transaction with mutltiple signers in the DIRECT mode is not supported and will +// return an error. +// An error is returned upon failure. +func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, overwriteSig bool) error { + if txf.keybase == nil { + return errors.New("keybase must be set prior to signing a transaction") + } + + var err error + signMode := txf.signMode + if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { + // use the SignModeHandler's default mode if unspecified + signMode, err = authsigning.APISignModeToInternal(txf.txConfig.SignModeHandler().DefaultMode()) + if err != nil { + return err + } + } + + pubKey, err := txf.keybase.GetPubKey(name) + if err != nil { + return err + } + + signerData := authsigning.SignerData{ + ChainID: txf.chainID, + AccountNumber: txf.accountNumber, + Sequence: txf.sequence, + PubKey: pubKey, + Address: sdk.AccAddress(pubKey.Address()).String(), + } + + // For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on + // TxBuilder under the hood, and SignerInfos is needed to generated the + // sign bytes. This is the reason for setting SetSignatures here, with a + // nil signature. + // + // Note: this line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it + // also doesn't affect its generated sign bytes, so for code's simplicity + // sake, we put it here. + sigData := signing.SingleSignatureData{ + SignMode: signMode, + Signature: nil, + } + sig := signing.SignatureV2{ + PubKey: pubKey, + Data: &sigData, + Sequence: txf.Sequence(), + } + + var prevSignatures []signing.SignatureV2 + if !overwriteSig { + prevSignatures, err = txBuilder.GetTx().GetSignaturesV2() + if err != nil { + return err + } + } + // Overwrite or append signer infos. + var sigs []signing.SignatureV2 + if overwriteSig { + sigs = []signing.SignatureV2{sig} + } else { + sigs = append(sigs, prevSignatures...) + sigs = append(sigs, sig) + } + if err := txBuilder.SetSignatures(sigs...); err != nil { + return err + } + + if err := checkMultipleSigners(txBuilder.GetTx()); err != nil { + return err + } + + bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, txf.txConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) + if err != nil { + return err + } + + // Sign those bytes + sigBytes, err := txf.keybase.Sign(name, bytesToSign, signMode) + if err != nil { + return err + } + + // Construct the SignatureV2 struct + sigData = signing.SingleSignatureData{ + SignMode: signMode, + Signature: sigBytes, + } + sig = signing.SignatureV2{ + PubKey: pubKey, + Data: &sigData, + Sequence: txf.Sequence(), + } + + if overwriteSig { + err = txBuilder.SetSignatures(sig) + } else { + prevSignatures = append(prevSignatures, sig) + err = 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) +} + +// makeAuxSignerData generates an AuxSignerData from the client inputs. +func makeAuxSignerData(clientCtx txContext, f Factory, msgs ...sdk.MsgV2) (tx.AuxSignerData, error) { + b := NewAuxTxBuilder() + fromAddress, name, _, err := GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) + if err != nil { + return tx.AuxSignerData{}, err + } + + b.SetAddress(fromAddress.String()) + if clientCtx.Offline { + b.SetAccountNumber(f.accountNumber) + b.SetSequence(f.sequence) + } else { + accNum, seq, err := clientCtx.AccountRetriever.GetAccountNumberSequence(clientCtx, fromAddress) + if err != nil { + return tx.AuxSignerData{}, err + } + b.SetAccountNumber(accNum) + b.SetSequence(seq) + } + + err = b.SetMsgs(msgs...) + if err != nil { + return tx.AuxSignerData{}, err + } + + err = b.SetSignMode(f.SignMode()) + if err != nil { + return tx.AuxSignerData{}, err + } + + pubKey, err := clientCtx.Keyring.GetPubKey(name) + if err != nil { + return tx.AuxSignerData{}, err + } + + err = b.SetPubKey(pubKey) + if err != nil { + return tx.AuxSignerData{}, err + } + + b.SetChainID(clientCtx.ChainID) + signBz, err := b.GetSignBytes() + if err != nil { + return tx.AuxSignerData{}, err + } + + sig, err := clientCtx.Keyring.Sign(name, signBz, f.signMode) + if err != nil { + return tx.AuxSignerData{}, err + } + b.SetSignature(sig) + + return b.GetAuxSignerData() +} + +// checkMultipleSigners checks that there can be maximum one DIRECT signer in +// a tx. +func checkMultipleSigners(tx TxV2) error { + directSigners := 0 + sigsV2, err := tx.GetSignaturesV2() + if err != nil { + return err + } + for _, sig := range sigsV2 { + directSigners += countDirectSigners(sig.Data) + if directSigners > 1 { + return sdkerrors.ErrNotSupported.Wrap("txs signed with CLI can have maximum 1 DIRECT signer") + } + } + + return nil +} + +// countDirectSigners counts the number of DIRECT signers in a signature data. +func countDirectSigners(data signing.SignatureData) int { + switch data := data.(type) { + case *signing.SingleSignatureData: + if data.SignMode == signing.SignMode_SIGN_MODE_DIRECT { + return 1 + } + + return 0 + case *signing.MultiSignatureData: + directSigners := 0 + for _, d := range data.Signatures { + directSigners += countDirectSigners(d) + } + + return directSigners + default: + panic("unreachable case") + } +} diff --git a/client/v2/tx/types.go b/client/v2/tx/types.go new file mode 100644 index 000000000000..c8f711a9bdfa --- /dev/null +++ b/client/v2/tx/types.go @@ -0,0 +1,23 @@ +package tx + +import ( + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" +) + +type SigVerifiableTxV2 interface { + types.TxV2 + GetSigners() ([][]byte, error) + GetPubKeys() ([]cryptotypes.PubKey, error) // If signer already has pubkey in context, this list will have nil in its place + GetSignaturesV2() ([]signing.SignatureV2, error) +} + +type TxV2 interface { + SigVerifiableTxV2 + + types.TxV2WithMemo + types.TxV2WithFee + types.TxV2WithTimeoutHeight + types.HasValidateBasic +} diff --git a/codec/types/any.go b/codec/types/any.go index 557cda0687ca..75de0c36a61a 100644 --- a/codec/types/any.go +++ b/codec/types/any.go @@ -2,6 +2,7 @@ package types import ( fmt "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" protov2 "google.golang.org/protobuf/proto" @@ -58,7 +59,7 @@ type Any struct { // returns an error if that value couldn't be packed. This also caches // the packed value so that it can be retrieved from GetCachedValue without // unmarshaling -func NewAnyWithValue(v proto.Message) (*Any, error) { +func NewAnyWithValue[T sdk.ProtoMessage](v T) (*Any, error) { if v == nil { return nil, errorsmod.Wrap(sdkerrors.ErrPackAny, "Expecting non nil value to create a new Any") } @@ -67,12 +68,15 @@ func NewAnyWithValue(v proto.Message) (*Any, error) { bz []byte err error ) - if msg, ok := v.(protov2.Message); ok { + + switch msg := any(v).(type) { + case sdk.Msg: + bz, err = proto.Marshal(msg) + case sdk.MsgV2: protov2MarshalOpts := protov2.MarshalOptions{Deterministic: true} bz, err = protov2MarshalOpts.Marshal(msg) - } else { - bz, err = proto.Marshal(v) } + if err != nil { return nil, err } diff --git a/codec/types/util.go b/codec/types/util.go index b29fb33b5034..f8a0f9bed969 100644 --- a/codec/types/util.go +++ b/codec/types/util.go @@ -1,15 +1,18 @@ package types import ( + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" - protov2 "google.golang.org/protobuf/proto" ) // MsgTypeURL returns the TypeURL of a `sdk.Msg`. -func MsgTypeURL(msg proto.Message) string { - if m, ok := msg.(protov2.Message); ok { - return "/" + string(m.ProtoReflect().Descriptor().FullName()) +func MsgTypeURL[T sdk.ProtoMessage](v T) string { + switch msg := any(v).(type) { + case sdk.Msg: + return "/" + proto.MessageName(msg) + case sdk.MsgV2: + return "/" + string(msg.ProtoReflect().Descriptor().FullName()) + default: + return "" } - - return "/" + proto.MessageName(msg) } diff --git a/types/tx_msg.go b/types/tx_msg.go index 399dafd4cc43..6743f37168ff 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -32,7 +32,9 @@ type ( // transaction type to be able to set and return the transaction fee. Fee interface { GetGas() uint64 - GetAmount() Coins + GetFee() Coins + FeePayer() []byte + FeeGranter() []byte } // Signature defines an interface for an application application-defined diff --git a/types/tx_msg_v2.go b/types/tx_msg_v2.go new file mode 100644 index 000000000000..771f5bfd8a2d --- /dev/null +++ b/types/tx_msg_v2.go @@ -0,0 +1,68 @@ +package types + +import ( + "encoding/json" + "fmt" + protov2 "google.golang.org/protobuf/proto" + + "github.com/cosmos/cosmos-sdk/codec" +) + +type ( + // MsgV2 defines the interface a transaction message needed to fulfill. + MsgV2 = protov2.Message + + ProtoMessage interface { + Msg | MsgV2 + } + + // TxV2 defines an interface a transaction must fulfill. + TxV2 interface { + // GetMsgs gets the transaction's messages as google.golang.org/protobuf/proto.Message's. + GetMsgs() ([]protov2.Message, error) + } + + // TxV2WithFee defines the interface to be implemented by Tx to use the FeeDecorators + TxV2WithFee interface { + TxV2 + Fee + } + + // TxV2WithMemo must have GetMemo() method to use ValidateMemoDecorator + TxV2WithMemo interface { + TxV2 + GetMemo() string + } + + // TxV2WithTimeoutHeight extends the Tx interface by allowing a transaction to + // set a height timeout. + TxV2WithTimeoutHeight interface { + TxV2 + GetTimeoutHeight() uint64 + } + + // TxV2Decoder unmarshals transaction bytes + TxV2Decoder func(txBytes []byte) (TxV2, error) + + // TxV2Encoder marshals transaction to bytes + TxV2Encoder func(tx TxV2) ([]byte, error) +) + +// GetMsgV2FromTypeURL returns a `sdk.MsgV2` message type from a type URL +func GetMsgV2FromTypeURL(cdc codec.Codec, input string) (MsgV2, error) { + var msg MsgV2 + bz, err := json.Marshal(struct { + Type string `json:"@type"` + }{ + Type: input, + }) + if err != nil { + return nil, err + } + + if err := cdc.UnmarshalInterfaceJSON(bz, &msg); err != nil { + return nil, fmt.Errorf("failed to determine sdk.Msg for %s URL : %w", input, err) + } + + return msg, nil +} From f302340ffb63643636e87fffbdcb234288f38ade Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Wed, 6 Mar 2024 15:46:54 -0300 Subject: [PATCH 02/14] Replace sdk types for API ones --- client/v2/tx/builder.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index 7196e1021811..8ad8f57ed758 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -1,10 +1,9 @@ package tx import ( + txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" codectypes "github.com/cosmos/cosmos-sdk/codec/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/tx" - signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/gogoproto/types" ) type ExtendedTxBuilder interface { @@ -16,14 +15,15 @@ type ExtendedTxBuilder interface { // signatures, and provide canonical bytes to sign over. The transaction must // also know how to encode itself. type TxBuilder interface { - GetTx() TxV2 - SetMsgs(msgs ...sdk.MsgV2) error - SetSignatures(signatures ...signingtypes.SignatureV2) error + GetTx() txv1beta1.Tx + //SignTx(Signer, Tx) error, txv1beta1.Tx + + SetMsgs(msgs ...*types.Any) error SetMemo(memo string) - SetFeeAmount(amount sdk.Coins) - SetFeePayer(feePayer sdk.AccAddress) + SetFeeAmount(amount txv1beta1.Fee) + SetFeePayer(feePayer string) SetGasLimit(limit uint64) SetTimeoutHeight(height uint64) - SetFeeGranter(feeGranter sdk.AccAddress) - AddAuxSignerData(tx.AuxSignerData) error + SetFeeGranter(feeGranter string) + SetAuxSignerData(data txv1beta1.AuxSignerData) error } From a1e0c0614dbeb5ddb94945d37900e5a9c0ee35b4 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Tue, 12 Mar 2024 08:49:34 -0300 Subject: [PATCH 03/14] wip --- client/flags/flags.go | 19 +++ client/v2/tx/builder.go | 14 +- client/v2/tx/config.go | 71 +++++++--- client/v2/tx/factory.go | 307 ++++++++++++++++++++++++++-------------- client/v2/tx/tx.go | 114 --------------- 5 files changed, 284 insertions(+), 241 deletions(-) diff --git a/client/flags/flags.go b/client/flags/flags.go index f2af30eb0714..1e791948da46 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -2,6 +2,7 @@ package flags import ( "fmt" + "github.com/cosmos/cosmos-sdk/types/tx/signing" "strconv" "github.com/spf13/cobra" @@ -201,3 +202,21 @@ func ParseGasSetting(gasStr string) (GasSetting, error) { return GasSetting{false, gas}, nil } } + +func ParseSignMode(signModeStr string) signing.SignMode { + signMode := signing.SignMode_SIGN_MODE_UNSPECIFIED + switch signModeStr { + case SignModeDirect: + signMode = signing.SignMode_SIGN_MODE_DIRECT + case SignModeLegacyAminoJSON: + signMode = signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON + case SignModeDirectAux: + signMode = signing.SignMode_SIGN_MODE_DIRECT_AUX + case SignModeTextual: + signMode = signing.SignMode_SIGN_MODE_TEXTUAL + case SignModeEIP191: + signMode = signing.SignMode_SIGN_MODE_EIP_191 + } + + return signMode +} diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index 8ad8f57ed758..d23a83b4f689 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -3,7 +3,8 @@ package tx import ( txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "github.com/cosmos/gogoproto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "google.golang.org/protobuf/types/known/anypb" ) type ExtendedTxBuilder interface { @@ -16,14 +17,19 @@ type ExtendedTxBuilder interface { // also know how to encode itself. type TxBuilder interface { GetTx() txv1beta1.Tx - //SignTx(Signer, Tx) error, txv1beta1.Tx - - SetMsgs(msgs ...*types.Any) error + Sign() error + SetMsgs(msgs ...anypb.Any) error SetMemo(memo string) SetFeeAmount(amount txv1beta1.Fee) SetFeePayer(feePayer string) SetGasLimit(limit uint64) SetTimeoutHeight(height uint64) SetFeeGranter(feeGranter string) + SetUnordered(v bool) SetAuxSignerData(data txv1beta1.AuxSignerData) error } + +type TxBuilderProvider interface { + NewTxBuilder() TxBuilder + WrapTxBuilder(sdk.Tx) (TxBuilder, error) +} diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index 337cd3e0c963..8298c166ada7 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -2,29 +2,68 @@ package tx import ( txsigning "cosmossdk.io/x/tx/signing" + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" ) -// TxEncodingConfig defines an interface that contains transaction -// encoders and decoders -type TxEncodingConfig interface { - TxEncoder() sdk.TxV2Encoder - TxDecoder() sdk.TxV2Decoder - TxJSONEncoder() sdk.TxV2Encoder - TxJSONDecoder() sdk.TxV2Decoder +type TxConfig interface { + GetTxParams() TxParameters + GetSignConfig() TxSigningConfig +} + +type TxSigningConfig interface { + SignModeHandler() *txsigning.HandlerMap + SigningContext() *txsigning.Context MarshalSignatureJSON([]signingtypes.SignatureV2) ([]byte, error) UnmarshalSignatureJSON([]byte) ([]signingtypes.SignatureV2, error) } -// TxConfig defines an interface a client can utilize to generate an -// application-defined concrete transaction type. The type returned must -// implement TxBuilder. -type TxConfig interface { - TxEncodingConfig +type TxParameters struct { + timeoutHeight uint64 + chainID string + memo string + signMode signingtypes.SignMode - NewTxBuilder() TxBuilder - WrapTxBuilder(sdk.Tx) (TxBuilder, error) - SignModeHandler() *txsigning.HandlerMap - SigningContext() *txsigning.Context + AccountConfig + GasConfig + FeeConfig + ExecutionOptions + ExtensionOptions +} + +// AccountConfig defines the 'account' related fields in a transaction. +type AccountConfig struct { + accountNumber uint64 + sequence uint64 + fromName string + fromAddress sdk.AccAddress +} + +// GasConfig defines the 'gas' related fields in a transaction. +type GasConfig struct { + gas uint64 + gasAdjustment float64 + gasPrices sdk.DecCoins +} + +// FeeConfig defines the 'fee' related fields in a transaction. +type FeeConfig struct { + fees sdk.Coins + feeGranter sdk.AccAddress + feePayer sdk.AccAddress +} + +// ExecutionOptions defines the transaction execution options ran by the client +type ExecutionOptions struct { + unordered bool + offline bool + generateOnly bool + simulateAndExecute bool + preprocessTxHook client.PreprocessTxFn +} + +type ExtensionOptions struct { + extOptions []*codectypes.Any } diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 17b6bf8d0f80..3e8f722feafa 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -3,6 +3,7 @@ package tx import ( "cosmossdk.io/client/v2/autocli/keyring" "cosmossdk.io/math" + authsigning "cosmossdk.io/x/auth/signing" "errors" "fmt" "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -22,42 +23,24 @@ import ( "strings" ) +// TxCodecProvider defines an interface that contains transaction +// encoders and decoders +type TxCodecProvider interface { + TxEncoder() sdk.TxV2Encoder + TxDecoder() sdk.TxV2Decoder + TxJSONEncoder() sdk.TxV2Encoder + TxJSONDecoder() sdk.TxV2Decoder +} + // Factory defines a client transaction factory that facilitates generating and // signing an application-specific transaction. type Factory struct { - keybase keyring.Keyring - - // TODO: check if autocli (AppOptions) already manages these fields - txConfig TxConfig - accountRetriever AccountRetriever - - // account related - accountNumber uint64 - sequence uint64 - - // fees related - fees sdk.Coins - feeGranter sdk.AccAddress - feePayer sdk.AccAddress - - // gas related - gas uint64 - gasAdjustment float64 - gasPrices sdk.DecCoins - - // tx generation config params - timeoutHeight uint64 - chainID string - fromName string - offline bool - memo string - generateOnly bool - simulateAndExecute bool - - extOptions []*codectypes.Any - signMode signing.SignMode // TODO: check if autocli (AppOptions) already manages this - - preprocessTxHook PreprocessTxFn + keybase keyring.Keyring + txBuilderProvider TxBuilderProvider + codec TxCodecProvider + accountRetriever AccountRetriever + signingConfig TxSigningConfig + TxParameters } func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) { @@ -69,20 +52,6 @@ func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) return Factory{}, fmt.Errorf("failed to bind flags to viper: %w", err) } - signMode := signing.SignMode_SIGN_MODE_UNSPECIFIED - switch clientCtx.SignModeStr { - case flags.SignModeDirect: - signMode = signing.SignMode_SIGN_MODE_DIRECT - case flags.SignModeLegacyAminoJSON: - signMode = signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON - case flags.SignModeDirectAux: - signMode = signing.SignMode_SIGN_MODE_DIRECT_AUX - case flags.SignModeTextual: - signMode = signing.SignMode_SIGN_MODE_TEXTUAL - case flags.SignModeEIP191: - signMode = signing.SignMode_SIGN_MODE_EIP_191 - } - var accNum, accSeq uint64 if clientCtx.Offline { if flagSet.Changed(flags.FlagAccountNumber) && flagSet.Changed(flags.FlagSequence) { @@ -93,41 +62,59 @@ func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) } } - gasAdj := clientCtx.Viper.GetFloat64(flags.FlagGasAdjustment) + if clientCtx.Offline && clientCtx.GenerateOnly { + if clientCtx.ChainID != "" { + return Factory{}, errors.New("chain ID cannot be used when offline and generate-only flags are set") + } + } else if clientCtx.ChainID == "" { + return Factory{}, errors.New("chain ID required but not specified") + } + + signMode := flags.ParseSignMode(clientCtx.SignModeStr) memo := clientCtx.Viper.GetString(flags.FlagNote) timeoutHeight := clientCtx.Viper.GetUint64(flags.FlagTimeoutHeight) + unordered := clientCtx.Viper.GetBool(flags.FlagUnordered) + gasAdj := clientCtx.Viper.GetFloat64(flags.FlagGasAdjustment) gasStr := clientCtx.Viper.GetString(flags.FlagGas) gasSetting, _ := flags.ParseGasSetting(gasStr) - - f := Factory{ - txConfig: clientCtx.TxConfig, - accountRetriever: clientCtx.AccountRetriever, - keybase: clientCtx.Keyring, - chainID: clientCtx.ChainID, - fromName: clientCtx.FromName, - offline: clientCtx.Offline, - generateOnly: clientCtx.GenerateOnly, - gas: gasSetting.Gas, - simulateAndExecute: gasSetting.Simulate, - accountNumber: accNum, - sequence: accSeq, - timeoutHeight: timeoutHeight, - gasAdjustment: gasAdj, - memo: memo, - signMode: signMode, - feeGranter: clientCtx.FeeGranter, - feePayer: clientCtx.FeePayer, - } + gasPricesStr := clientCtx.Viper.GetString(flags.FlagGasPrices) feesStr := clientCtx.Viper.GetString(flags.FlagFees) - f = f.WithFees(feesStr) - gasPricesStr := clientCtx.Viper.GetString(flags.FlagGasPrices) - f = f.WithGasPrices(gasPricesStr) - - f = f.WithPreprocessTxHook(clientCtx.PreprocessTxHook) + f := Factory{ + accountRetriever: clientCtx.AccountRetriever, + keybase: clientCtx.Keyring, + TxParameters: TxParameters{ + timeoutHeight: timeoutHeight, + memo: memo, + chainID: clientCtx.ChainID, + signMode: signMode, + AccountConfig: AccountConfig{ + accountNumber: accNum, + sequence: accSeq, + fromName: clientCtx.FromName, + fromAddress: clientCtx.FromAddress, + }, + GasConfig: GasConfig{ + gas: gasSetting.Gas, + gasAdjustment: gasAdj, + }, + FeeConfig: FeeConfig{ + feeGranter: clientCtx.FeeGranter, + feePayer: clientCtx.FeePayer, + }, + ExecutionOptions: ExecutionOptions{ + unordered: unordered, + offline: clientCtx.Offline, + generateOnly: clientCtx.GenerateOnly, + simulateAndExecute: gasSetting.Simulate, + preprocessTxHook: clientCtx.PreprocessTxHook, + }, + }, + } + f = f.WithFees(feesStr).WithGasPrices(gasPricesStr) return f, nil } @@ -137,47 +124,42 @@ func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) // A new Factory with the updated fields will be returned. // Note: When in offline mode, the Prepare does nothing and returns the original factory. func (f Factory) Prepare(clientCtx txContext) (Factory, error) { - if clientCtx.Offline { + if f.ExecutionOptions.offline { return f, nil } - fc := f - from := clientCtx.FromAddress + if f.fromAddress.Empty() { + return f, errors.New("missing 'from address' field") + } - if err := fc.accountRetriever.EnsureExists(clientCtx, from); err != nil { - return fc, err + if err := f.accountRetriever.EnsureExists(clientCtx, f.fromAddress); err != nil { + return f, err } - initNum, initSeq := fc.accountNumber, fc.sequence - if initNum == 0 || initSeq == 0 { - num, seq, err := fc.accountRetriever.GetAccountNumberSequence(clientCtx, from) + if f.accountNumber == 0 || f.sequence == 0 { + fc := f + num, seq, err := fc.accountRetriever.GetAccountNumberSequence(clientCtx, f.fromAddress) if err != nil { - return fc, err + return f, err } - if initNum == 0 { + if f.accountNumber == 0 { fc = fc.WithAccountNumber(num) } - if initSeq == 0 { + if f.sequence == 0 { fc = fc.WithSequence(seq) } + + return fc, nil } - return fc, nil + return f, nil } // 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.MsgV2) (TxBuilder, error) { - if f.offline && f.generateOnly { - if f.chainID != "" { - return nil, errors.New("chain ID cannot be used when offline and generate-only flags are set") - } - } else if f.chainID == "" { - return nil, errors.New("chain ID required but not specified") - } - fees := f.fees if !f.gasPrices.IsZero() { @@ -204,24 +186,24 @@ func (f Factory) BuildUnsignedTx(msgs ...sdk.MsgV2) (TxBuilder, error) { return nil, errors.New("cannot provide a valid mnemonic seed in the memo field") } - tx := f.txConfig.NewTxBuilder() + txBuilder := f.txBuilderProvider.NewTxBuilder() - if err := tx.SetMsgs(msgs...); err != nil { + if err := txBuilder.SetMsgs(msgs...); err != nil { return nil, err } - tx.SetMemo(f.memo) - tx.SetFeeAmount(fees) - tx.SetGasLimit(f.gas) - tx.SetFeeGranter(f.feeGranter) - tx.SetFeePayer(f.feePayer) - tx.SetTimeoutHeight(f.TimeoutHeight()) + txBuilder.SetMemo(f.memo) + txBuilder.SetFeeAmount(fees) + txBuilder.SetGasLimit(f.gas) + txBuilder.SetFeeGranter(f.feeGranter) + txBuilder.SetFeePayer(f.feePayer) + txBuilder.SetTimeoutHeight(f.timeoutHeight) - if etx, ok := tx.(ExtendedTxBuilder); ok { + if etx, ok := txBuilder.(ExtendedTxBuilder); ok { etx.SetExtensionOptions(f.extOptions...) } - return tx, nil + return txBuilder, nil } // PrintUnsignedTx will generate an unsigned transaction and print it to the writer @@ -255,7 +237,7 @@ func (f Factory) PrintUnsignedTx(clientCtx txContext, msgs ...sdk.MsgV2) error { return err } - encoder := f.txConfig.TxJSONEncoder() + encoder := f.codec.TxJSONEncoder() if encoder == nil { return errors.New("cannot print unsigned tx: tx json encoder is nil") } @@ -456,9 +438,7 @@ func (f Factory) AccountRetriever() AccountRetriever { return f.accountRetriever func (f Factory) TimeoutHeight() uint64 { return f.timeoutHeight } func (f Factory) FromName() string { return f.fromName } func (f Factory) SimulateAndExecute() bool { return f.simulateAndExecute } -func (f Factory) SignMode() signing.SignMode { - return f.signMode -} +func (f Factory) SignMode() signing.SignMode { return f.signMode } // getSimPK gets the public key to use for building a simulation tx. // Note, we should only check for keys in the keybase if we are in simulate and execute mode, @@ -500,3 +480,116 @@ func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) signing.SignatureDat Signatures: multiSignatureData, } } + +// Sign signs a given tx with a named key. The bytes signed over are canconical. +// The resulting signature will be added to the transaction builder overwriting the previous +// ones if overwrite=true (otherwise, the signature will be appended). +// Signing a transaction with mutltiple signers in the DIRECT mode is not supported and will +// return an error. +// An error is returned upon failure. +func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, overwriteSig bool) error { + if txf.keybase == nil { + return errors.New("keybase must be set prior to signing a transaction") + } + + var err error + signMode := txf.signMode + if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { + // use the SignModeHandler's default mode if unspecified + signMode, err = authsigning.APISignModeToInternal(txf.txConfig.SignModeHandler().DefaultMode()) + if err != nil { + return err + } + } + + pubKey, err := txf.keybase.GetPubKey(name) + if err != nil { + return err + } + + signerData := authsigning.SignerData{ + ChainID: txf.chainID, + AccountNumber: txf.accountNumber, + Sequence: txf.sequence, + PubKey: pubKey, + Address: sdk.AccAddress(pubKey.Address()).String(), + } + + // For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on + // TxBuilder under the hood, and SignerInfos is needed to generated the + // sign bytes. This is the reason for setting SetSignatures here, with a + // nil signature. + // + // Note: this line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it + // also doesn't affect its generated sign bytes, so for code's simplicity + // sake, we put it here. + sigData := signing.SingleSignatureData{ + SignMode: txf.signMode, + Signature: nil, + } + sig := signing.SignatureV2{ + PubKey: pubKey, + Data: &sigData, + Sequence: txf.Sequence(), + } + + var prevSignatures []signing.SignatureV2 + if !overwriteSig { + prevSignatures, err = txBuilder.GetTx().GetSignaturesV2() + if err != nil { + return err + } + } + // Overwrite or append signer infos. + var sigs []signing.SignatureV2 + if overwriteSig { + sigs = []signing.SignatureV2{sig} + } else { + sigs = append(sigs, prevSignatures...) + sigs = append(sigs, sig) + } + if err := txBuilder.SetSignatures(sigs...); err != nil { + return err + } + + if err := checkMultipleSigners(txBuilder.GetTx()); err != nil { + return err + } + + bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, txf.txConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) + if err != nil { + return err + } + + // Sign those bytes + sigBytes, err := txf.keybase.Sign(name, bytesToSign, signMode) + if err != nil { + return err + } + + // Construct the SignatureV2 struct + sigData = signing.SingleSignatureData{ + SignMode: signMode, + Signature: sigBytes, + } + sig = signing.SignatureV2{ + PubKey: pubKey, + Data: &sigData, + Sequence: txf.Sequence(), + } + + if overwriteSig { + err = txBuilder.SetSignatures(sig) + } else { + prevSignatures = append(prevSignatures, sig) + err = 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) +} diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index ecdafc5eeff1..842d698eb9e3 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -3,7 +3,6 @@ package tx import ( "bufio" "context" - authsigning "cosmossdk.io/x/auth/signing" "errors" "fmt" "github.com/cosmos/cosmos-sdk/client/input" @@ -170,119 +169,6 @@ func CalculateGas( return simRes, uint64(txf.GasAdjustment() * float64(simRes.GasInfo.GasUsed)), nil } -// Sign signs a given tx with a named key. The bytes signed over are canconical. -// The resulting signature will be added to the transaction builder overwriting the previous -// ones if overwrite=true (otherwise, the signature will be appended). -// Signing a transaction with mutltiple signers in the DIRECT mode is not supported and will -// return an error. -// An error is returned upon failure. -func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, overwriteSig bool) error { - if txf.keybase == nil { - return errors.New("keybase must be set prior to signing a transaction") - } - - var err error - signMode := txf.signMode - if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { - // use the SignModeHandler's default mode if unspecified - signMode, err = authsigning.APISignModeToInternal(txf.txConfig.SignModeHandler().DefaultMode()) - if err != nil { - return err - } - } - - pubKey, err := txf.keybase.GetPubKey(name) - if err != nil { - return err - } - - signerData := authsigning.SignerData{ - ChainID: txf.chainID, - AccountNumber: txf.accountNumber, - Sequence: txf.sequence, - PubKey: pubKey, - Address: sdk.AccAddress(pubKey.Address()).String(), - } - - // For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on - // TxBuilder under the hood, and SignerInfos is needed to generated the - // sign bytes. This is the reason for setting SetSignatures here, with a - // nil signature. - // - // Note: this line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it - // also doesn't affect its generated sign bytes, so for code's simplicity - // sake, we put it here. - sigData := signing.SingleSignatureData{ - SignMode: signMode, - Signature: nil, - } - sig := signing.SignatureV2{ - PubKey: pubKey, - Data: &sigData, - Sequence: txf.Sequence(), - } - - var prevSignatures []signing.SignatureV2 - if !overwriteSig { - prevSignatures, err = txBuilder.GetTx().GetSignaturesV2() - if err != nil { - return err - } - } - // Overwrite or append signer infos. - var sigs []signing.SignatureV2 - if overwriteSig { - sigs = []signing.SignatureV2{sig} - } else { - sigs = append(sigs, prevSignatures...) - sigs = append(sigs, sig) - } - if err := txBuilder.SetSignatures(sigs...); err != nil { - return err - } - - if err := checkMultipleSigners(txBuilder.GetTx()); err != nil { - return err - } - - bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, txf.txConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) - if err != nil { - return err - } - - // Sign those bytes - sigBytes, err := txf.keybase.Sign(name, bytesToSign, signMode) - if err != nil { - return err - } - - // Construct the SignatureV2 struct - sigData = signing.SingleSignatureData{ - SignMode: signMode, - Signature: sigBytes, - } - sig = signing.SignatureV2{ - PubKey: pubKey, - Data: &sigData, - Sequence: txf.Sequence(), - } - - if overwriteSig { - err = txBuilder.SetSignatures(sig) - } else { - prevSignatures = append(prevSignatures, sig) - err = 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) -} - // makeAuxSignerData generates an AuxSignerData from the client inputs. func makeAuxSignerData(clientCtx txContext, f Factory, msgs ...sdk.MsgV2) (tx.AuxSignerData, error) { b := NewAuxTxBuilder() From 52a9e0f9d77881f75ef30f3a25b31db749db7b49 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Tue, 12 Mar 2024 18:50:09 -0300 Subject: [PATCH 04/14] wip --- client/cmd.go | 8 ++++++++ client/v2/tx/builder.go | 6 +++--- client/v2/tx/config.go | 3 +-- client/v2/tx/factory.go | 19 ++++++------------- client/v2/tx/tx.go | 2 +- client/v2/tx/types.go | 11 ----------- types/tx_msg_v2.go | 21 +-------------------- 7 files changed, 20 insertions(+), 50 deletions(-) diff --git a/client/cmd.go b/client/cmd.go index 61412200fd5f..962fc86bf0a4 100644 --- a/client/cmd.go +++ b/client/cmd.go @@ -318,6 +318,14 @@ func readTxCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, err } } + if clientCtx.Viper == nil { + clientCtx = clientCtx.WithViper("") + } + + if err = clientCtx.Viper.BindPFlags(flagSet); err != nil { + return clientCtx, fmt.Errorf("failed to bind flags to viper: %w", err) + } + return clientCtx, nil } diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index d23a83b4f689..ab80124fb5f4 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -3,7 +3,6 @@ package tx import ( txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" codectypes "github.com/cosmos/cosmos-sdk/codec/types" - sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/protobuf/types/known/anypb" ) @@ -18,7 +17,7 @@ type ExtendedTxBuilder interface { type TxBuilder interface { GetTx() txv1beta1.Tx Sign() error - SetMsgs(msgs ...anypb.Any) error + SetMsgs(msgs ...*anypb.Any) error SetMemo(memo string) SetFeeAmount(amount txv1beta1.Fee) SetFeePayer(feePayer string) @@ -26,10 +25,11 @@ type TxBuilder interface { SetTimeoutHeight(height uint64) SetFeeGranter(feeGranter string) SetUnordered(v bool) + SetSignatures(doc txv1beta1.SignDoc) error SetAuxSignerData(data txv1beta1.AuxSignerData) error } type TxBuilderProvider interface { NewTxBuilder() TxBuilder - WrapTxBuilder(sdk.Tx) (TxBuilder, error) + WrapTxBuilder(txv1beta1.Tx) (TxBuilder, error) } diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index 8298c166ada7..d5492753b619 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -2,7 +2,6 @@ package tx import ( txsigning "cosmossdk.io/x/tx/signing" - "github.com/cosmos/cosmos-sdk/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -61,7 +60,7 @@ type ExecutionOptions struct { offline bool generateOnly bool simulateAndExecute bool - preprocessTxHook client.PreprocessTxFn + preprocessTxHook PreprocessTxFn } type ExtensionOptions struct { diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 3e8f722feafa..9cb53f3bde7b 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -1,6 +1,7 @@ package tx import ( + "context" "cosmossdk.io/client/v2/autocli/keyring" "cosmossdk.io/math" authsigning "cosmossdk.io/x/auth/signing" @@ -44,14 +45,6 @@ type Factory struct { } func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) { - if clientCtx.Viper == nil { - clientCtx = clientCtx.WithViper("") - } - - if err := clientCtx.Viper.BindPFlags(flagSet); err != nil { - return Factory{}, fmt.Errorf("failed to bind flags to viper: %w", err) - } - var accNum, accSeq uint64 if clientCtx.Offline { if flagSet.Changed(flags.FlagAccountNumber) && flagSet.Changed(flags.FlagSequence) { @@ -275,7 +268,7 @@ func (f Factory) BuildSimTx(msgs ...sdk.MsgV2) ([]byte, error) { return nil, err } - encoder := f.txConfig.TxEncoder() + encoder := f.codec.TxEncoder() if encoder == nil { return nil, fmt.Errorf("cannot simulate tx: tx encoder is nil") } @@ -283,9 +276,9 @@ func (f Factory) BuildSimTx(msgs ...sdk.MsgV2) ([]byte, error) { return encoder(txb.GetTx()) } -// WithTxConfig returns a copy of the Factory with an updated TxConfig. -func (f Factory) WithTxConfig(g TxConfig) Factory { - f.txConfig = g +// WithTxParameters returns a copy of the Factory with an updated TxConfig. +func (f Factory) WithTxParameters(p TxParameters) Factory { + f.TxParameters = p return f } @@ -496,7 +489,7 @@ func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, ov signMode := txf.signMode if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { // use the SignModeHandler's default mode if unspecified - signMode, err = authsigning.APISignModeToInternal(txf.txConfig.SignModeHandler().DefaultMode()) + signMode, err = authsigning.APISignModeToInternal(txf.signingConfig.SignModeHandler().DefaultMode()) if err != nil { return err } diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index 842d698eb9e3..2c17bf53c0d4 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -27,7 +27,7 @@ func (gr GasEstimateResponse) String() string { // GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxCLI(clientCtx txContext, flagSet *pflag.FlagSet, msgs ...sdk.MsgV2) error { +func GenerateOrBroadcastTxCLI(clientCtx txContext, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { txf, err := NewFactoryCLI(clientCtx, flagSet) if err != nil { return err diff --git a/client/v2/tx/types.go b/client/v2/tx/types.go index c8f711a9bdfa..a0b4ac6b89f7 100644 --- a/client/v2/tx/types.go +++ b/client/v2/tx/types.go @@ -2,22 +2,11 @@ package tx import ( cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) type SigVerifiableTxV2 interface { - types.TxV2 GetSigners() ([][]byte, error) GetPubKeys() ([]cryptotypes.PubKey, error) // If signer already has pubkey in context, this list will have nil in its place GetSignaturesV2() ([]signing.SignatureV2, error) } - -type TxV2 interface { - SigVerifiableTxV2 - - types.TxV2WithMemo - types.TxV2WithFee - types.TxV2WithTimeoutHeight - types.HasValidateBasic -} diff --git a/types/tx_msg_v2.go b/types/tx_msg_v2.go index 771f5bfd8a2d..c8fc50f44f4d 100644 --- a/types/tx_msg_v2.go +++ b/types/tx_msg_v2.go @@ -19,26 +19,7 @@ type ( // TxV2 defines an interface a transaction must fulfill. TxV2 interface { // GetMsgs gets the transaction's messages as google.golang.org/protobuf/proto.Message's. - GetMsgs() ([]protov2.Message, error) - } - - // TxV2WithFee defines the interface to be implemented by Tx to use the FeeDecorators - TxV2WithFee interface { - TxV2 - Fee - } - - // TxV2WithMemo must have GetMemo() method to use ValidateMemoDecorator - TxV2WithMemo interface { - TxV2 - GetMemo() string - } - - // TxV2WithTimeoutHeight extends the Tx interface by allowing a transaction to - // set a height timeout. - TxV2WithTimeoutHeight interface { - TxV2 - GetTimeoutHeight() uint64 + GetMsgs() ([]MsgV2, error) } // TxV2Decoder unmarshals transaction bytes From 6206e35658ed3d534870c9f0be825178256c33a9 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Wed, 13 Mar 2024 09:30:23 -0300 Subject: [PATCH 05/14] Move Sign func into Factory --- client/v2/tx/builder.go | 1 - client/v2/tx/factory.go | 28 ++++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index ab80124fb5f4..146cff705817 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -16,7 +16,6 @@ type ExtendedTxBuilder interface { // also know how to encode itself. type TxBuilder interface { GetTx() txv1beta1.Tx - Sign() error SetMsgs(msgs ...*anypb.Any) error SetMemo(memo string) SetFeeAmount(amount txv1beta1.Fee) diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 9cb53f3bde7b..55433010d017 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -480,30 +480,30 @@ func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) signing.SignatureDat // Signing a transaction with mutltiple signers in the DIRECT mode is not supported and will // return an error. // An error is returned upon failure. -func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, overwriteSig bool) error { - if txf.keybase == nil { +func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, overwriteSig bool) error { + if f.keybase == nil { return errors.New("keybase must be set prior to signing a transaction") } var err error - signMode := txf.signMode + signMode := f.signMode if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { // use the SignModeHandler's default mode if unspecified - signMode, err = authsigning.APISignModeToInternal(txf.signingConfig.SignModeHandler().DefaultMode()) + signMode, err = authsigning.APISignModeToInternal(f.signingConfig.SignModeHandler().DefaultMode()) if err != nil { return err } } - pubKey, err := txf.keybase.GetPubKey(name) + pubKey, err := f.keybase.GetPubKey(name) if err != nil { return err } signerData := authsigning.SignerData{ - ChainID: txf.chainID, - AccountNumber: txf.accountNumber, - Sequence: txf.sequence, + ChainID: f.chainID, + AccountNumber: f.accountNumber, + Sequence: f.sequence, PubKey: pubKey, Address: sdk.AccAddress(pubKey.Address()).String(), } @@ -517,13 +517,13 @@ func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, ov // also doesn't affect its generated sign bytes, so for code's simplicity // sake, we put it here. sigData := signing.SingleSignatureData{ - SignMode: txf.signMode, + SignMode: signMode, Signature: nil, } sig := signing.SignatureV2{ PubKey: pubKey, Data: &sigData, - Sequence: txf.Sequence(), + Sequence: f.sequence, } var prevSignatures []signing.SignatureV2 @@ -549,13 +549,13 @@ func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, ov return err } - bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, txf.txConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) + bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, f.signingConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) if err != nil { return err } // Sign those bytes - sigBytes, err := txf.keybase.Sign(name, bytesToSign, signMode) + sigBytes, err := f.keybase.Sign(name, bytesToSign, signMode) if err != nil { return err } @@ -568,7 +568,7 @@ func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, ov sig = signing.SignatureV2{ PubKey: pubKey, Data: &sigData, - Sequence: txf.Sequence(), + Sequence: f.sequence, } if overwriteSig { @@ -584,5 +584,5 @@ func Sign(ctx context.Context, txf Factory, name string, txBuilder TxBuilder, ov // Run optional preprocessing if specified. By default, this is unset // and will return nil. - return txf.PreprocessTx(name, txBuilder) + return f.PreprocessTx(name, txBuilder) } From 394e09f942bf27e30f5a03911afba2bb314cb607 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Wed, 3 Apr 2024 18:45:22 -0300 Subject: [PATCH 06/14] Add TxEncodingConfig in TxConfig --- client/v2/tx/config.go | 12 ++++++++++- client/v2/tx/factory.go | 45 +++++++++++++++++------------------------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index d5492753b619..df3c4b7b8eb0 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -10,6 +10,16 @@ import ( type TxConfig interface { GetTxParams() TxParameters GetSignConfig() TxSigningConfig + GetTxEncodingConfig() TxEncodingConfig +} + +type TxEncodingConfig interface { + TxEncoder() sdk.TxEncoder + TxDecoder() sdk.TxDecoder + TxJSONEncoder() sdk.TxEncoder + TxJSONDecoder() sdk.TxDecoder + MarshalSignatureJSON([]signingtypes.SignatureV2) ([]byte, error) + UnmarshalSignatureJSON([]byte) ([]signingtypes.SignatureV2, error) } type TxSigningConfig interface { @@ -64,5 +74,5 @@ type ExecutionOptions struct { } type ExtensionOptions struct { - extOptions []*codectypes.Any + ExtOptions []*codectypes.Any } diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 55433010d017..603a27faf39b 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -24,21 +24,12 @@ import ( "strings" ) -// TxCodecProvider defines an interface that contains transaction -// encoders and decoders -type TxCodecProvider interface { - TxEncoder() sdk.TxV2Encoder - TxDecoder() sdk.TxV2Decoder - TxJSONEncoder() sdk.TxV2Encoder - TxJSONDecoder() sdk.TxV2Decoder -} - // Factory defines a client transaction factory that facilitates generating and // signing an application-specific transaction. type Factory struct { keybase keyring.Keyring txBuilderProvider TxBuilderProvider - codec TxCodecProvider + codec TxEncodingConfig accountRetriever AccountRetriever signingConfig TxSigningConfig TxParameters @@ -79,30 +70,30 @@ func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) accountRetriever: clientCtx.AccountRetriever, keybase: clientCtx.Keyring, TxParameters: TxParameters{ - timeoutHeight: timeoutHeight, - memo: memo, - chainID: clientCtx.ChainID, - signMode: signMode, + TimeoutHeight: timeoutHeight, + Memo: memo, + ChainID: clientCtx.ChainID, + SignMode: signMode, AccountConfig: AccountConfig{ - accountNumber: accNum, - sequence: accSeq, - fromName: clientCtx.FromName, - fromAddress: clientCtx.FromAddress, + AccountNumber: accNum, + Sequence: accSeq, + FromName: clientCtx.FromName, + FromAddress: clientCtx.FromAddress, }, GasConfig: GasConfig{ - gas: gasSetting.Gas, - gasAdjustment: gasAdj, + Gas: gasSetting.Gas, + GasAdjustment: gasAdj, }, FeeConfig: FeeConfig{ - feeGranter: clientCtx.FeeGranter, - feePayer: clientCtx.FeePayer, + FeeGranter: clientCtx.FeeGranter, + FeePayer: clientCtx.FeePayer, }, ExecutionOptions: ExecutionOptions{ - unordered: unordered, - offline: clientCtx.Offline, - generateOnly: clientCtx.GenerateOnly, - simulateAndExecute: gasSetting.Simulate, - preprocessTxHook: clientCtx.PreprocessTxHook, + Unordered: unordered, + Offline: clientCtx.Offline, + GenerateOnly: clientCtx.GenerateOnly, + SimulateAndExecute: gasSetting.Simulate, + PreprocessTxHook: clientCtx.PreprocessTxHook, }, }, } From c1a18ab19762fc908193ad912903f2d0f327f52a Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Tue, 9 Apr 2024 18:49:11 -0300 Subject: [PATCH 07/14] wip --- client/tx/aux_builder.go | 12 +- client/v2/tx/account_retriever.go | 24 -- client/v2/tx/aux_builder.go | 18 +- client/v2/tx/aux_builder_test.go | 252 ++++++++++++++++++ client/v2/tx/broadcast.go | 140 ---------- client/v2/tx/builder.go | 32 +-- client/v2/tx/cometbft.go | 36 --- client/v2/tx/config.go | 16 +- client/v2/tx/context.go | 424 ------------------------------ client/v2/tx/factory.go | 215 ++++++++------- client/v2/tx/query.go | 307 --------------------- client/v2/tx/tx.go | 28 +- client/v2/tx/types.go | 16 +- types/tx_msg_v2.go | 49 ---- 14 files changed, 425 insertions(+), 1144 deletions(-) delete mode 100644 client/v2/tx/account_retriever.go create mode 100644 client/v2/tx/aux_builder_test.go delete mode 100644 client/v2/tx/broadcast.go delete mode 100644 client/v2/tx/cometbft.go delete mode 100644 client/v2/tx/context.go delete mode 100644 client/v2/tx/query.go delete mode 100644 types/tx_msg_v2.go diff --git a/client/tx/aux_builder.go b/client/tx/aux_builder.go index ade6622f87a0..d67f7d65416f 100644 --- a/client/tx/aux_builder.go +++ b/client/tx/aux_builder.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/gogoproto/proto" "google.golang.org/protobuf/types/known/anypb" - txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" txsigning "cosmossdk.io/x/tx/signing" "cosmossdk.io/x/tx/signing/aminojson" @@ -25,7 +25,7 @@ type AuxTxBuilder struct { // - b.msgs is used for constructing the AMINO sign bz, // - b.body is used for constructing the DIRECT_AUX sign bz. msgs []sdk.Msg - body *txv1beta1.TxBody + body *apitx.TxBody auxSignerData *tx.AuxSignerData } @@ -208,7 +208,7 @@ func (b *AuxTxBuilder) GetSignBytes() ([]byte, error) { FileResolver: proto.HybridResolver, }) - auxBody := &txv1beta1.TxBody{ + auxBody := &apitx.TxBody{ Messages: body.Messages, Memo: body.Memo, TimeoutHeight: body.TimeoutHeight, @@ -231,13 +231,13 @@ func (b *AuxTxBuilder) GetSignBytes() ([]byte, error) { }, txsigning.TxData{ Body: auxBody, - AuthInfo: &txv1beta1.AuthInfo{ + AuthInfo: &apitx.AuthInfo{ SignerInfos: nil, // Aux signer never signs over fee. // For LEGACY_AMINO_JSON, we use the convention to sign // over empty fees. // ref: https://github.com/cosmos/cosmos-sdk/pull/10348 - Fee: &txv1beta1.Fee{}, + Fee: &apitx.Fee{}, }, }, ) @@ -261,7 +261,7 @@ func (b *AuxTxBuilder) GetAuxSignerData() (tx.AuxSignerData, error) { func (b *AuxTxBuilder) checkEmptyFields() { if b.body == nil { - b.body = &txv1beta1.TxBody{} + b.body = &apitx.TxBody{} } if b.auxSignerData == nil { diff --git a/client/v2/tx/account_retriever.go b/client/v2/tx/account_retriever.go deleted file mode 100644 index ca3f3b020e1a..000000000000 --- a/client/v2/tx/account_retriever.go +++ /dev/null @@ -1,24 +0,0 @@ -package tx - -import ( - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Account defines a read-only version of the auth module's AccountI. -type Account interface { - GetAddress() sdk.AccAddress - GetPubKey() cryptotypes.PubKey // can return nil. - GetAccountNumber() uint64 - GetSequence() uint64 -} - -// AccountRetriever defines the interfaces required by transactions to -// ensure an account exists and to be able to query for account fields necessary -// for signing. -type AccountRetriever interface { - GetAccount(clientCtx txContext, addr sdk.AccAddress) (Account, error) - GetAccountWithHeight(clientCtx txContext, addr sdk.AccAddress) (Account, int64, error) - EnsureExists(clientCtx txContext, addr sdk.AccAddress) error - GetAccountNumberSequence(clientCtx txContext, addr sdk.AccAddress) (accNum, accSeq uint64, err error) -} diff --git a/client/v2/tx/aux_builder.go b/client/v2/tx/aux_builder.go index a2c50f9e2fe4..d67f7d65416f 100644 --- a/client/v2/tx/aux_builder.go +++ b/client/v2/tx/aux_builder.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/gogoproto/proto" "google.golang.org/protobuf/types/known/anypb" - txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" txsigning "cosmossdk.io/x/tx/signing" "cosmossdk.io/x/tx/signing/aminojson" @@ -24,8 +24,8 @@ type AuxTxBuilder struct { // TxBuilder. It's also added inside body.Messages, because: // - b.msgs is used for constructing the AMINO sign bz, // - b.body is used for constructing the DIRECT_AUX sign bz. - msgs []sdk.MsgV2 - body *txv1beta1.TxBody + msgs []sdk.Msg + body *apitx.TxBody auxSignerData *tx.AuxSignerData } @@ -59,7 +59,7 @@ func (b *AuxTxBuilder) SetTimeoutHeight(height uint64) { } // SetMsgs sets an array of Msgs in the tx. -func (b *AuxTxBuilder) SetMsgs(msgs ...sdk.MsgV2) error { +func (b *AuxTxBuilder) SetMsgs(msgs ...sdk.Msg) error { anys := make([]*anypb.Any, len(msgs)) for i, msg := range msgs { legacyAny, err := codectypes.NewAnyWithValue(msg) @@ -151,7 +151,7 @@ func (b *AuxTxBuilder) SetExtensionOptions(extOpts ...*codectypes.Any) { b.auxSignerData.SignDoc.BodyBytes = nil } -// SetSignature sets the aux signer's signature. +// SetNonCriticalExtensionOptions sets the aux signer's non-critical extension options. func (b *AuxTxBuilder) SetNonCriticalExtensionOptions(extOpts ...*codectypes.Any) { b.checkEmptyFields() @@ -208,7 +208,7 @@ func (b *AuxTxBuilder) GetSignBytes() ([]byte, error) { FileResolver: proto.HybridResolver, }) - auxBody := &txv1beta1.TxBody{ + auxBody := &apitx.TxBody{ Messages: body.Messages, Memo: body.Memo, TimeoutHeight: body.TimeoutHeight, @@ -231,13 +231,13 @@ func (b *AuxTxBuilder) GetSignBytes() ([]byte, error) { }, txsigning.TxData{ Body: auxBody, - AuthInfo: &txv1beta1.AuthInfo{ + AuthInfo: &apitx.AuthInfo{ SignerInfos: nil, // Aux signer never signs over fee. // For LEGACY_AMINO_JSON, we use the convention to sign // over empty fees. // ref: https://github.com/cosmos/cosmos-sdk/pull/10348 - Fee: &txv1beta1.Fee{}, + Fee: &apitx.Fee{}, }, }, ) @@ -261,7 +261,7 @@ func (b *AuxTxBuilder) GetAuxSignerData() (tx.AuxSignerData, error) { func (b *AuxTxBuilder) checkEmptyFields() { if b.body == nil { - b.body = &txv1beta1.TxBody{} + b.body = &apitx.TxBody{} } if b.auxSignerData == nil { diff --git a/client/v2/tx/aux_builder_test.go b/client/v2/tx/aux_builder_test.go new file mode 100644 index 000000000000..b0c7e3e874eb --- /dev/null +++ b/client/v2/tx/aux_builder_test.go @@ -0,0 +1,252 @@ +package tx_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/testutil" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + typestx "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/cosmos-sdk/x/counter" + countertypes "github.com/cosmos/cosmos-sdk/x/counter/types" +) + +const ( + memo = "waboom" + timeoutHeight = uint64(5) +) + +var ( + _, pub1, addr1 = testdata.KeyTestPubAddr() + rawSig = []byte("dummy") + msg1 = &countertypes.MsgIncreaseCounter{Signer: addr1.String(), Count: 1} + + chainID = "test-chain" +) + +func TestAuxTxBuilder(t *testing.T) { + counterModule := counter.AppModule{} + cdc := moduletestutil.MakeTestEncodingConfig(testutil.CodecOptions{}, counterModule).Codec + reg := codectypes.NewInterfaceRegistry() + + testdata.RegisterInterfaces(reg) + // required for test case: "GetAuxSignerData works for DIRECT_AUX" + counterModule.RegisterInterfaces(reg) + + var b tx.AuxTxBuilder + + testcases := []struct { + name string + malleate func() error + expErr bool + expErrStr string + }{ + { + "cannot set SIGN_MODE_DIRECT", + func() error { + return b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT) + }, + true, "AuxTxBuilder can only sign with SIGN_MODE_DIRECT_AUX or SIGN_MODE_LEGACY_AMINO_JSON", + }, + { + "cannot set invalid pubkey", + func() error { + return b.SetPubKey(cryptotypes.PubKey(nil)) + }, + true, "failed packing protobuf message to Any", + }, + { + "cannot set invalid Msg", + func() error { + return b.SetMsgs(sdk.Msg(nil)) + }, + true, "failed packing protobuf message to Any", + }, + { + "GetSignBytes body should not be nil", + func() error { + _, err := b.GetSignBytes() + return err + }, + true, "aux tx is nil, call setters on AuxTxBuilder first", + }, + { + "GetSignBytes pubkey should not be nil", + func() error { + require.NoError(t, b.SetMsgs(msg1)) + + _, err := b.GetSignBytes() + return err + }, + true, "public key cannot be empty: invalid pubkey", + }, + { + "GetSignBytes invalid sign mode", + func() error { + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + + _, err := b.GetSignBytes() + return err + }, + true, "got unknown sign mode SIGN_MODE_UNSPECIFIED", + }, + { + "GetSignBytes works for DIRECT_AUX", + func() error { + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + require.NoError(t, b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX)) + + _, err := b.GetSignBytes() + return err + }, + false, "", + }, + { + "GetAuxSignerData address should not be empty", + func() error { + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + require.NoError(t, b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX)) + + _, err := b.GetSignBytes() + require.NoError(t, err) + + _, err = b.GetAuxSignerData() + return err + }, + true, "address cannot be empty: invalid request", + }, + { + "GetAuxSignerData signature should not be empty", + func() error { + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + b.SetAddress(addr1.String()) + require.NoError(t, b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX)) + + _, err := b.GetSignBytes() + require.NoError(t, err) + + _, err = b.GetAuxSignerData() + return err + }, + true, "signature cannot be empty: no signatures supplied", + }, + { + "GetAuxSignerData works for DIRECT_AUX", + func() error { + b.SetAccountNumber(1) + b.SetSequence(2) + b.SetTimeoutHeight(timeoutHeight) + b.SetMemo(memo) + b.SetChainID(chainID) + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + b.SetAddress(addr1.String()) + err := b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX) + require.NoError(t, err) + + _, err = b.GetSignBytes() + require.NoError(t, err) + b.SetSignature(rawSig) + + auxSignerData, err := b.GetAuxSignerData() + + // Make sure auxSignerData is correctly populated + checkCorrectData(t, cdc, auxSignerData, signing.SignMode_SIGN_MODE_DIRECT_AUX) + + return err + }, + false, "", + }, + { + "GetSignBytes works for LEGACY_AMINO_JSON", + func() error { + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + b.SetAddress(addr1.String()) + err := b.SetSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) + require.NoError(t, err) + + _, err = b.GetSignBytes() + return err + }, + false, "", + }, + { + "GetAuxSignerData works for LEGACY_AMINO_JSON", + func() error { + b.SetAccountNumber(1) + b.SetSequence(2) + b.SetTimeoutHeight(timeoutHeight) + b.SetMemo(memo) + b.SetChainID(chainID) + require.NoError(t, b.SetMsgs(msg1)) + require.NoError(t, b.SetPubKey(pub1)) + b.SetAddress(addr1.String()) + err := b.SetSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) + require.NoError(t, err) + + _, err = b.GetSignBytes() + require.NoError(t, err) + b.SetSignature(rawSig) + + auxSignerData, err := b.GetAuxSignerData() + + // Make sure auxSignerData is correctly populated + checkCorrectData(t, cdc, auxSignerData, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) + + return err + }, + false, "", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + b = tx.NewAuxTxBuilder() + err := tc.malleate() + + if tc.expErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expErrStr) + } else { + require.NoError(t, err) + } + }) + } +} + +// checkCorrectData that the auxSignerData's content matches the inputs we gave. +func checkCorrectData(t *testing.T, cdc codec.Codec, auxSignerData typestx.AuxSignerData, signMode signing.SignMode) { + t.Helper() + pkAny, err := codectypes.NewAnyWithValue(pub1) + require.NoError(t, err) + msgAny, err := codectypes.NewAnyWithValue(msg1) + require.NoError(t, err) + + var body typestx.TxBody + err = cdc.Unmarshal(auxSignerData.SignDoc.BodyBytes, &body) + require.NoError(t, err) + + require.Equal(t, uint64(1), auxSignerData.SignDoc.AccountNumber) + require.Equal(t, uint64(2), auxSignerData.SignDoc.Sequence) + require.Equal(t, timeoutHeight, body.TimeoutHeight) + require.Equal(t, memo, body.Memo) + require.Equal(t, chainID, auxSignerData.SignDoc.ChainId) + require.Equal(t, msgAny, body.GetMessages()[0]) + require.Equal(t, pkAny, auxSignerData.SignDoc.PublicKey) + require.Equal(t, signMode, auxSignerData.Mode) + require.Equal(t, rawSig, auxSignerData.Sig) +} diff --git a/client/v2/tx/broadcast.go b/client/v2/tx/broadcast.go deleted file mode 100644 index 8ecd7edcaae8..000000000000 --- a/client/v2/tx/broadcast.go +++ /dev/null @@ -1,140 +0,0 @@ -package tx - -import ( - "context" - "fmt" - "github.com/cometbft/cometbft/mempool" - cmttypes "github.com/cometbft/cometbft/types" - "github.com/cosmos/cosmos-sdk/client/flags" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/types/tx" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "strings" -) - -// BroadcastTx broadcasts a transactions either synchronously or asynchronously -// based on the context parameters. The result of the broadcast is parsed into -// an intermediate structure which is logged if the context has a logger -// defined. -func (ctx txContext) BroadcastTx(txBytes []byte) (res *sdk.TxResponse, err error) { - switch ctx.BroadcastMode { - case flags.BroadcastSync: - res, err = ctx.BroadcastTxSync(txBytes) - - case flags.BroadcastAsync: - res, err = ctx.BroadcastTxAsync(txBytes) - - default: - return nil, fmt.Errorf("unsupported return type %s; supported types: sync, async", ctx.BroadcastMode) - } - - return res, err -} - -// CheckCometError checks if the error returned from BroadcastTx is a -// CometBFT error that is returned before the tx is submitted due to -// precondition checks that failed. If an CometBFT error is detected, this -// function returns the correct code back in TxResponse. -// -// TODO: Avoid brittle string matching in favor of error matching. This requires -// a change to CometBFT's RPCError type to allow retrieval or matching against -// a concrete error type. -func CheckCometError(err error, tx cmttypes.Tx) *sdk.TxResponse { - if err == nil { - return nil - } - - errStr := strings.ToLower(err.Error()) - txHash := fmt.Sprintf("%X", tx.Hash()) - - switch { - case strings.Contains(errStr, strings.ToLower(mempool.ErrTxInCache.Error())): - return &sdk.TxResponse{ - Code: sdkerrors.ErrTxInMempoolCache.ABCICode(), - Codespace: sdkerrors.ErrTxInMempoolCache.Codespace(), - TxHash: txHash, - } - - case strings.Contains(errStr, "mempool is full"): - return &sdk.TxResponse{ - Code: sdkerrors.ErrMempoolIsFull.ABCICode(), - Codespace: sdkerrors.ErrMempoolIsFull.Codespace(), - TxHash: txHash, - } - - case strings.Contains(errStr, "tx too large"): - return &sdk.TxResponse{ - Code: sdkerrors.ErrTxTooLarge.ABCICode(), - Codespace: sdkerrors.ErrTxTooLarge.Codespace(), - TxHash: txHash, - } - - default: - return nil - } -} - -// BroadcastTxSync broadcasts transaction bytes to a CometBFT node -// synchronously (i.e. returns after CheckTx execution). -func (ctx txContext) BroadcastTxSync(txBytes []byte) (*sdk.TxResponse, error) { - node, err := ctx.GetNode() - if err != nil { - return nil, err - } - - res, err := node.BroadcastTxSync(context.Background(), txBytes) - if errRes := CheckCometError(err, txBytes); errRes != nil { - return errRes, nil - } - - return sdk.NewResponseFormatBroadcastTx(res), err -} - -// BroadcastTxAsync broadcasts transaction bytes to a CometBFT node -// asynchronously (i.e. returns immediately). -func (ctx txContext) BroadcastTxAsync(txBytes []byte) (*sdk.TxResponse, error) { - node, err := ctx.GetNode() - if err != nil { - return nil, err - } - - res, err := node.BroadcastTxAsync(context.Background(), txBytes) - if errRes := CheckCometError(err, txBytes); errRes != nil { - return errRes, nil - } - - return sdk.NewResponseFormatBroadcastTx(res), err -} - -// ServiceBroadcast is a helper function to broadcast a Tx with the correct gRPC types -// from the tx service. Calls `clientCtx.BroadcastTx` under the hood. -func ServiceBroadcast(_ context.Context, clientCtx txContext, req *tx.BroadcastTxRequest) (*tx.BroadcastTxResponse, error) { - if req == nil || req.TxBytes == nil { - return nil, status.Error(codes.InvalidArgument, "invalid empty tx") - } - - clientCtx = clientCtx.WithBroadcastMode(normalizeBroadcastMode(req.Mode)) - resp, err := clientCtx.BroadcastTx(req.TxBytes) - if err != nil { - return nil, err - } - - return &tx.BroadcastTxResponse{ - TxResponse: resp, - }, nil -} - -// normalizeBroadcastMode converts a broadcast mode into a normalized string -// to be passed into the clientCtx. -func normalizeBroadcastMode(mode tx.BroadcastMode) string { - switch mode { - case tx.BroadcastMode_BROADCAST_MODE_ASYNC: - return "async" - case tx.BroadcastMode_BROADCAST_MODE_SYNC: - return "sync" - default: - return "unspecified" - } -} diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index 146cff705817..de7d4a7060d0 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -1,13 +1,15 @@ package tx import ( - txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" + apibase "cosmossdk.io/api/cosmos/base/v1beta1" + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" + "cosmossdk.io/client/v2/offchain" codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "google.golang.org/protobuf/types/known/anypb" + sdk "github.com/cosmos/cosmos-sdk/types" ) type ExtendedTxBuilder interface { - SetExtensionOptions(extOpts ...*codectypes.Any) + SetExtensionOptions(...*codectypes.Any) } // TxBuilder defines an interface which an application-defined concrete transaction @@ -15,20 +17,20 @@ type ExtendedTxBuilder interface { // signatures, and provide canonical bytes to sign over. The transaction must // also know how to encode itself. type TxBuilder interface { - GetTx() txv1beta1.Tx - SetMsgs(msgs ...*anypb.Any) error - SetMemo(memo string) - SetFeeAmount(amount txv1beta1.Fee) - SetFeePayer(feePayer string) - SetGasLimit(limit uint64) - SetTimeoutHeight(height uint64) - SetFeeGranter(feeGranter string) - SetUnordered(v bool) - SetSignatures(doc txv1beta1.SignDoc) error - SetAuxSignerData(data txv1beta1.AuxSignerData) error + GetTx() apitx.Tx + SetMsgs(...sdk.Msg) error + SetMemo(string) + SetFeeAmount([]apibase.Coin) + SetFeePayer(string) + SetGasLimit(uint64) + SetTimeoutHeight(uint64) + SetFeeGranter(string) + SetUnordered(bool) + SetSignatures(...offchain.OffchainSignature) error + SetAuxSignerData(apitx.AuxSignerData) error } type TxBuilderProvider interface { NewTxBuilder() TxBuilder - WrapTxBuilder(txv1beta1.Tx) (TxBuilder, error) + WrapTxBuilder(apitx.Tx) (TxBuilder, error) } diff --git a/client/v2/tx/cometbft.go b/client/v2/tx/cometbft.go deleted file mode 100644 index 0b5ea64f224d..000000000000 --- a/client/v2/tx/cometbft.go +++ /dev/null @@ -1,36 +0,0 @@ -package tx - -import ( - "context" - - rpcclient "github.com/cometbft/cometbft/rpc/client" - coretypes "github.com/cometbft/cometbft/rpc/core/types" -) - -// CometRPC defines the interface of a CometBFT RPC client needed for -// queries and transaction handling. -type CometRPC interface { - rpcclient.ABCIClient - - Validators(ctx context.Context, height *int64, page, perPage *int) (*coretypes.ResultValidators, error) - Status(context.Context) (*coretypes.ResultStatus, error) - Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) - BlockByHash(ctx context.Context, hash []byte) (*coretypes.ResultBlock, error) - BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) - BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) - Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) - Tx(ctx context.Context, hash []byte, prove bool) (*coretypes.ResultTx, error) - TxSearch( - ctx context.Context, - query string, - prove bool, - page, perPage *int, - orderBy string, - ) (*coretypes.ResultTxSearch, error) - BlockSearch( - ctx context.Context, - query string, - page, perPage *int, - orderBy string, - ) (*coretypes.ResultBlockSearch, error) -} diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index df3c4b7b8eb0..017e18d28ecf 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -2,24 +2,28 @@ package tx import ( txsigning "cosmossdk.io/x/tx/signing" + "github.com/cosmos/cosmos-sdk/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" ) +// TxConfig defines an interface a client can utilize to generate an +// application-defined concrete transaction type. The type returned must +// implement TxBuilder. type TxConfig interface { - GetTxParams() TxParameters - GetSignConfig() TxSigningConfig - GetTxEncodingConfig() TxEncodingConfig + TxEncodingConfig + TxSigningConfig + TxBuilderProvider } +// TxEncodingConfig defines an interface that contains transaction +// encoders and decoders type TxEncodingConfig interface { TxEncoder() sdk.TxEncoder TxDecoder() sdk.TxDecoder TxJSONEncoder() sdk.TxEncoder TxJSONDecoder() sdk.TxDecoder - MarshalSignatureJSON([]signingtypes.SignatureV2) ([]byte, error) - UnmarshalSignatureJSON([]byte) ([]signingtypes.SignatureV2, error) } type TxSigningConfig interface { @@ -70,7 +74,7 @@ type ExecutionOptions struct { offline bool generateOnly bool simulateAndExecute bool - preprocessTxHook PreprocessTxFn + preprocessTxHook client.PreprocessTxFn } type ExtensionOptions struct { diff --git a/client/v2/tx/context.go b/client/v2/tx/context.go deleted file mode 100644 index cfc8286cdd97..000000000000 --- a/client/v2/tx/context.go +++ /dev/null @@ -1,424 +0,0 @@ -package tx - -import ( - "bufio" - "context" - "cosmossdk.io/client/v2/autocli/keyring" - "encoding/json" - "fmt" - "io" - "os" - "path" - "strings" - - "github.com/cosmos/gogoproto/proto" - "github.com/spf13/viper" - "google.golang.org/grpc" - "sigs.k8s.io/yaml" - - "cosmossdk.io/core/address" - - "github.com/cosmos/cosmos-sdk/codec" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - 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 - -// txContext implements a typical context created in SDK modules for transaction -// handling and queries. -type txContext struct { - FromAddress sdk.AccAddress - Client CometRPC - GRPCClient *grpc.ClientConn - ChainID string - Codec codec.Codec - InterfaceRegistry codectypes.InterfaceRegistry - Input io.Reader - Keyring keyring.Keyring - KeyringOptions []keyring.Option - KeyringDir string - KeyringDefaultKeyName string - Output io.Writer - OutputFormat string - Height int64 - HomeDir string - From string - BroadcastMode string - FromName string - SignModeStr string - Simulate bool - GenerateOnly bool - Offline bool - SkipConfirm bool - TxConfig TxConfig - AccountRetriever AccountRetriever - NodeURI string - 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 - - // CmdContext is the context.txContext from the Cobra command. - CmdContext context.Context - - // Address codecs - AddressCodec address.Codec - ValidatorAddressCodec address.Codec - ConsensusAddressCodec address.Codec -} - -// WithCmdContext returns a copy of the context with an updated context.txContext, -// usually set to the cobra cmd context. -func (ctx txContext) WithCmdContext(c context.Context) txContext { - ctx.CmdContext = c - return ctx -} - -// WithKeyring returns a copy of the context with an updated keyring. -func (ctx txContext) WithKeyring(k keyring.Keyring) txContext { - ctx.Keyring = k - return ctx -} - -// WithKeyringOptions returns a copy of the context with an updated keyring. -func (ctx txContext) WithKeyringOptions(opts ...keyring.Option) txContext { - ctx.KeyringOptions = opts - return ctx -} - -// WithInput returns a copy of the context with an updated input. -func (ctx txContext) WithInput(r io.Reader) txContext { - // convert to a bufio.Reader to have a shared buffer between the keyring and the - // the Commands, ensuring a read from one advance the read pointer for the other. - // see https://github.com/cosmos/cosmos-sdk/issues/9566. - ctx.Input = bufio.NewReader(r) - return ctx -} - -// WithCodec returns a copy of the txContext with an updated Codec. -func (ctx txContext) WithCodec(m codec.Codec) txContext { - ctx.Codec = m - return ctx -} - -// WithOutput returns a copy of the context with an updated output writer (e.g. stdout). -func (ctx txContext) WithOutput(w io.Writer) txContext { - ctx.Output = w - return ctx -} - -// WithFrom returns a copy of the context with an updated from address or name. -func (ctx txContext) WithFrom(from string) txContext { - ctx.From = from - return ctx -} - -// WithOutputFormat returns a copy of the context with an updated OutputFormat field. -func (ctx txContext) WithOutputFormat(format string) txContext { - ctx.OutputFormat = format - return ctx -} - -// WithNodeURI returns a copy of the context with an updated node URI. -func (ctx txContext) WithNodeURI(nodeURI string) txContext { - ctx.NodeURI = nodeURI - return ctx -} - -// WithHeight returns a copy of the context with an updated height. -func (ctx txContext) WithHeight(height int64) txContext { - ctx.Height = height - return ctx -} - -// WithClient returns a copy of the context with an updated RPC client -// instance. -func (ctx txContext) WithClient(client CometRPC) txContext { - ctx.Client = client - return ctx -} - -// WithGRPCClient returns a copy of the context with an updated GRPC client -// instance. -func (ctx txContext) WithGRPCClient(grpcClient *grpc.ClientConn) txContext { - ctx.GRPCClient = grpcClient - return ctx -} - -// WithChainID returns a copy of the context with an updated chain ID. -func (ctx txContext) WithChainID(chainID string) txContext { - ctx.ChainID = chainID - return ctx -} - -// WithHomeDir returns a copy of the txContext with HomeDir set. -func (ctx txContext) WithHomeDir(dir string) txContext { - if dir != "" { - ctx.HomeDir = dir - } - return ctx -} - -// WithKeyringDir returns a copy of the txContext with KeyringDir set. -func (ctx txContext) WithKeyringDir(dir string) txContext { - ctx.KeyringDir = dir - return ctx -} - -// WithKeyringDefaultKeyName returns a copy of the txContext with KeyringDefaultKeyName set. -func (ctx txContext) WithKeyringDefaultKeyName(keyName string) txContext { - ctx.KeyringDefaultKeyName = keyName - return ctx -} - -// WithGenerateOnly returns a copy of the context with updated GenerateOnly value -func (ctx txContext) WithGenerateOnly(generateOnly bool) txContext { - ctx.GenerateOnly = generateOnly - return ctx -} - -// WithSimulation returns a copy of the context with updated Simulate value -func (ctx txContext) WithSimulation(simulate bool) txContext { - ctx.Simulate = simulate - return ctx -} - -// WithOffline returns a copy of the context with updated Offline value. -func (ctx txContext) WithOffline(offline bool) txContext { - ctx.Offline = offline - return ctx -} - -// WithFromName returns a copy of the context with an updated from account name. -func (ctx txContext) WithFromName(name string) txContext { - ctx.FromName = name - return ctx -} - -// WithFromAddress returns a copy of the context with an updated from account -// address. -func (ctx txContext) WithFromAddress(addr sdk.AccAddress) txContext { - ctx.FromAddress = addr - return ctx -} - -// WithFeePayerAddress returns a copy of the context with an updated fee payer account -// address. -func (ctx txContext) WithFeePayerAddress(addr sdk.AccAddress) txContext { - ctx.FeePayer = addr - return ctx -} - -// WithFeeGranterAddress returns a copy of the context with an updated fee granter account -// address. -func (ctx txContext) WithFeeGranterAddress(addr sdk.AccAddress) txContext { - ctx.FeeGranter = addr - return ctx -} - -// WithBroadcastMode returns a copy of the context with an updated broadcast -// mode. -func (ctx txContext) WithBroadcastMode(mode string) txContext { - ctx.BroadcastMode = mode - return ctx -} - -// WithSignModeStr returns a copy of the context with an updated SignMode -// value. -func (ctx txContext) WithSignModeStr(signModeStr string) txContext { - ctx.SignModeStr = signModeStr - return ctx -} - -// WithSkipConfirmation returns a copy of the context with an updated SkipConfirm -// value. -func (ctx txContext) WithSkipConfirmation(skip bool) txContext { - ctx.SkipConfirm = skip - return ctx -} - -// WithTxConfig returns the context with an updated TxConfig -func (ctx txContext) WithTxConfig(generator TxConfig) txContext { - ctx.TxConfig = generator - return ctx -} - -// WithAccountRetriever returns the context with an updated AccountRetriever -func (ctx txContext) WithAccountRetriever(retriever AccountRetriever) txContext { - ctx.AccountRetriever = retriever - return ctx -} - -// WithInterfaceRegistry returns the context with an updated InterfaceRegistry -func (ctx txContext) WithInterfaceRegistry(interfaceRegistry codectypes.InterfaceRegistry) txContext { - ctx.InterfaceRegistry = interfaceRegistry - return ctx -} - -// WithViper returns the context with Viper field. This Viper instance is used to read -// client-side config from the config file. -func (ctx txContext) WithViper(prefix string) txContext { - v := viper.New() - - if prefix == "" { - executableName, _ := os.Executable() - prefix = path.Base(executableName) - } - - v.SetEnvPrefix(prefix) - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - v.AutomaticEnv() - ctx.Viper = v - return ctx -} - -// WithAux returns a copy of the context with an updated IsAux value. -func (ctx txContext) WithAux(isAux bool) txContext { - ctx.IsAux = isAux - return ctx -} - -// WithPreprocessTxHook returns the context with the provided preprocessing hook, which -// enables chains to preprocess the transaction using the builder. -func (ctx txContext) WithPreprocessTxHook(preprocessFn PreprocessTxFn) txContext { - ctx.PreprocessTxHook = preprocessFn - return ctx -} - -// WithAddressCodec returns the context with the provided address codec. -func (ctx txContext) WithAddressCodec(addressCodec address.Codec) txContext { - ctx.AddressCodec = addressCodec - return ctx -} - -// WithValidatorAddressCodec returns the context with the provided validator address codec. -func (ctx txContext) WithValidatorAddressCodec(validatorAddressCodec address.Codec) txContext { - ctx.ValidatorAddressCodec = validatorAddressCodec - return ctx -} - -// WithConsensusAddressCodec returns the context with the provided consensus address codec. -func (ctx txContext) WithConsensusAddressCodec(consensusAddressCodec address.Codec) txContext { - ctx.ConsensusAddressCodec = consensusAddressCodec - return ctx -} - -// PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout -func (ctx txContext) PrintString(str string) error { - return ctx.PrintBytes([]byte(str)) -} - -// PrintBytes prints the raw bytes to ctx.Output if it's defined, otherwise to os.Stdout. -// NOTE: for printing a complex state object, you should use ctx.PrintOutput -func (ctx txContext) PrintBytes(o []byte) error { - writer := ctx.Output - if writer == nil { - writer = os.Stdout - } - - _, err := writer.Write(o) - return err -} - -// PrintProto outputs toPrint to the ctx.Output based on ctx.OutputFormat which is -// either text or json. If text, toPrint will be YAML encoded. Otherwise, toPrint -// will be JSON encoded using ctx.Codec. An error is returned upon failure. -func (ctx txContext) PrintProto(toPrint proto.Message) error { - // always serialize JSON initially because proto json can't be directly YAML encoded - out, err := ctx.Codec.MarshalJSON(toPrint) - if err != nil { - return err - } - return ctx.printOutput(out) -} - -// PrintRaw is a variant of PrintProto that doesn't require a proto.Message type -// and uses a raw JSON message. No marshaling is performed. -func (ctx txContext) PrintRaw(toPrint json.RawMessage) error { - return ctx.printOutput(toPrint) -} - -func (ctx txContext) printOutput(out []byte) error { - var err error - if ctx.OutputFormat == "text" { - out, err = yaml.JSONToYAML(out) - if err != nil { - return err - } - } - - writer := ctx.Output - if writer == nil { - writer = os.Stdout - } - - _, err = writer.Write(out) - if err != nil { - return err - } - - if ctx.OutputFormat != "text" { - // append new-line for formats besides YAML - _, err = writer.Write([]byte("\n")) - if err != nil { - return err - } - } - - return nil -} - -// GetFromFields returns a from account address, account name and keyring type, given either an address or key name. -// If clientCtx.Simulate is true the keystore is not accessed and a valid address must be provided -// If clientCtx.GenerateOnly is true the keystore is only accessed if a key name is provided -// If from is empty, the default key if specified in the context will be used -func GetFromFields(clientCtx txContext, kr keyring.Keyring, from string) (sdk.AccAddress, string, keyring.KeyType, error) { - if from == "" && clientCtx.KeyringDefaultKeyName != "" { - from = clientCtx.KeyringDefaultKeyName - _ = clientCtx.PrintString(fmt.Sprintf("No key name or address provided; using the default key: %s\n", clientCtx.KeyringDefaultKeyName)) - } - - if from == "" { - return nil, "", 0, nil - } - - addr, err := clientCtx.AddressCodec.StringToBytes(from) - switch { - case clientCtx.Simulate: - if err != nil { - return nil, "", 0, fmt.Errorf("a valid address must be provided in simulation mode: %w", err) - } - - return addr, "", 0, nil - - case clientCtx.GenerateOnly: - if err == nil { - return addr, "", 0, nil - } - } - - var k *keyring.Record - if err == nil { - k, err = kr.KeyByAddress(addr) - if err != nil { - return nil, "", 0, err - } - } else { - k, err = kr.Key(from) - if err != nil { - return nil, "", 0, err - } - } - - addr, err = kr.GetRecordAddress(k) - if err != nil { - return nil, "", 0, err - } - - return addr, kr.GetRecordName(k), kr.GetRecordType(k), nil -} diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 603a27faf39b..f8a1c1a2507c 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -2,11 +2,13 @@ package tx import ( "context" + apibase "cosmossdk.io/api/cosmos/base/v1beta1" "cosmossdk.io/client/v2/autocli/keyring" "cosmossdk.io/math" authsigning "cosmossdk.io/x/auth/signing" "errors" "fmt" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/types/tx/signing" "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" @@ -27,15 +29,21 @@ import ( // Factory defines a client transaction factory that facilitates generating and // signing an application-specific transaction. type Factory struct { - keybase keyring.Keyring - txBuilderProvider TxBuilderProvider - codec TxEncodingConfig - accountRetriever AccountRetriever - signingConfig TxSigningConfig - TxParameters + keybase keyring.Keyring + accountRetriever client.AccountRetriever + txConfig TxConfig + txParams TxParameters } -func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) { +func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, error) { + if clientCtx.Viper == nil { + clientCtx = clientCtx.WithViper("") + } + + if err := clientCtx.Viper.BindPFlags(flagSet); err != nil { + return Factory{}, fmt.Errorf("failed to bind flags to viper: %w", err) + } + var accNum, accSeq uint64 if clientCtx.Offline { if flagSet.Changed(flags.FlagAccountNumber) && flagSet.Changed(flags.FlagSequence) { @@ -69,35 +77,36 @@ func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) f := Factory{ accountRetriever: clientCtx.AccountRetriever, keybase: clientCtx.Keyring, - TxParameters: TxParameters{ - TimeoutHeight: timeoutHeight, - Memo: memo, - ChainID: clientCtx.ChainID, - SignMode: signMode, + txParams: TxParameters{ + timeoutHeight: timeoutHeight, + memo: memo, + chainID: clientCtx.ChainID, + signMode: signMode, AccountConfig: AccountConfig{ - AccountNumber: accNum, - Sequence: accSeq, - FromName: clientCtx.FromName, - FromAddress: clientCtx.FromAddress, + accountNumber: accNum, + sequence: accSeq, + fromName: clientCtx.FromName, + fromAddress: clientCtx.FromAddress, }, GasConfig: GasConfig{ - Gas: gasSetting.Gas, - GasAdjustment: gasAdj, + gas: gasSetting.Gas, + gasAdjustment: gasAdj, }, FeeConfig: FeeConfig{ - FeeGranter: clientCtx.FeeGranter, - FeePayer: clientCtx.FeePayer, + feeGranter: clientCtx.FeeGranter, + feePayer: clientCtx.FeePayer, }, ExecutionOptions: ExecutionOptions{ - Unordered: unordered, - Offline: clientCtx.Offline, - GenerateOnly: clientCtx.GenerateOnly, - SimulateAndExecute: gasSetting.Simulate, - PreprocessTxHook: clientCtx.PreprocessTxHook, + unordered: unordered, + offline: clientCtx.Offline, + generateOnly: clientCtx.GenerateOnly, + simulateAndExecute: gasSetting.Simulate, + preprocessTxHook: clientCtx.PreprocessTxHook, }, }, } + // Properties that need special parsing f = f.WithFees(feesStr).WithGasPrices(gasPricesStr) return f, nil } @@ -107,31 +116,31 @@ func NewFactoryCLI(clientCtx txContext, flagSet *pflag.FlagSet) (Factory, error) // they will be queried for and set on the provided Factory. // A new Factory with the updated fields will be returned. // Note: When in offline mode, the Prepare does nothing and returns the original factory. -func (f Factory) Prepare(clientCtx txContext) (Factory, error) { - if f.ExecutionOptions.offline { +func (f Factory) Prepare(clientCtx client.Context) (Factory, error) { + if f.txParams.ExecutionOptions.offline { return f, nil } - if f.fromAddress.Empty() { + if f.txParams.fromAddress.Empty() { return f, errors.New("missing 'from address' field") } - if err := f.accountRetriever.EnsureExists(clientCtx, f.fromAddress); err != nil { + if err := f.accountRetriever.EnsureExists(clientCtx, f.txParams.fromAddress); err != nil { return f, err } - if f.accountNumber == 0 || f.sequence == 0 { + if f.txParams.accountNumber == 0 || f.txParams.sequence == 0 { fc := f - num, seq, err := fc.accountRetriever.GetAccountNumberSequence(clientCtx, f.fromAddress) + num, seq, err := fc.accountRetriever.GetAccountNumberSequence(clientCtx, f.txParams.fromAddress) if err != nil { return f, err } - if f.accountNumber == 0 { + if f.txParams.accountNumber == 0 { fc = fc.WithAccountNumber(num) } - if f.sequence == 0 { + if f.txParams.sequence == 0 { fc = fc.WithSequence(seq) } @@ -143,48 +152,56 @@ func (f Factory) Prepare(clientCtx txContext) (Factory, error) { // 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.MsgV2) (TxBuilder, error) { - fees := f.fees +func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (TxBuilder, error) { + if f.txParams.offline && f.txParams.generateOnly { + if f.txParams.chainID != "" { + return nil, errors.New("chain ID cannot be used when offline and generate-only flags are set") + } + } else if f.txParams.chainID == "" { + return nil, errors.New("chain ID required but not specified") + } + + fees := f.txParams.fees - if !f.gasPrices.IsZero() { + if !f.txParams.gasPrices.IsZero() { if !fees.IsZero() { return nil, errors.New("cannot provide both fees and gas prices") } // f.gas is a uint64 and we should convert to LegacyDec // without the risk of under/overflow via uint64->int64. - glDec := math.LegacyNewDecFromBigInt(new(big.Int).SetUint64(f.gas)) + glDec := math.LegacyNewDecFromBigInt(new(big.Int).SetUint64(f.txParams.gas)) // Derive the fees based on the provided gas prices, where // fee = ceil(gasPrice * gasLimit). - fees = make(sdk.Coins, len(f.gasPrices)) + apiFees := make([]apibase.Coin, len(f.txParams.gasPrices)) - for i, gp := range f.gasPrices { + for i, gp := range f.txParams.gasPrices { fee := gp.Amount.Mul(glDec) - fees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + apiFees[i] = apibase.Coin{Denom: gp.Denom, Amount: fee.Ceil().RoundInt().String()} } } // Prevent simple inclusion of a valid mnemonic in the memo field - if f.memo != "" && bip39.IsMnemonicValid(strings.ToLower(f.memo)) { + if f.txParams.memo != "" && bip39.IsMnemonicValid(strings.ToLower(f.txParams.memo)) { return nil, errors.New("cannot provide a valid mnemonic seed in the memo field") } - txBuilder := f.txBuilderProvider.NewTxBuilder() + txBuilder := f.txConfig.NewTxBuilder() if err := txBuilder.SetMsgs(msgs...); err != nil { return nil, err } - txBuilder.SetMemo(f.memo) - txBuilder.SetFeeAmount(fees) - txBuilder.SetGasLimit(f.gas) - txBuilder.SetFeeGranter(f.feeGranter) - txBuilder.SetFeePayer(f.feePayer) - txBuilder.SetTimeoutHeight(f.timeoutHeight) + txBuilder.SetMemo(f.txParams.memo) + txBuilder.SetFeeAmount(apiFees) + txBuilder.SetGasLimit(f.txParams.gas) + txBuilder.SetFeeGranter(f.txParams.feeGranter.String()) + txBuilder.SetFeePayer(f.txParams.feePayer.String()) + txBuilder.SetTimeoutHeight(f.txParams.timeoutHeight) if etx, ok := txBuilder.(ExtendedTxBuilder); ok { - etx.SetExtensionOptions(f.extOptions...) + etx.SetExtensionOptions(f.txParams.ExtOptions...) } return txBuilder, nil @@ -194,7 +211,7 @@ func (f Factory) BuildUnsignedTx(msgs ...sdk.MsgV2) (TxBuilder, error) { // 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 txContext, msgs ...sdk.MsgV2) error { +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") @@ -221,7 +238,7 @@ func (f Factory) PrintUnsignedTx(clientCtx txContext, msgs ...sdk.MsgV2) error { return err } - encoder := f.codec.TxJSONEncoder() + encoder := f.txConfig.TxJSONEncoder() if encoder == nil { return errors.New("cannot print unsigned tx: tx json encoder is nil") } @@ -259,7 +276,7 @@ func (f Factory) BuildSimTx(msgs ...sdk.MsgV2) ([]byte, error) { return nil, err } - encoder := f.codec.TxEncoder() + encoder := f.txConfig.TxEncoder() if encoder == nil { return nil, fmt.Errorf("cannot simulate tx: tx encoder is nil") } @@ -267,27 +284,21 @@ func (f Factory) BuildSimTx(msgs ...sdk.MsgV2) ([]byte, error) { return encoder(txb.GetTx()) } -// WithTxParameters returns a copy of the Factory with an updated TxConfig. -func (f Factory) WithTxParameters(p TxParameters) Factory { - f.TxParameters = p - return f -} - // WithAccountRetriever returns a copy of the Factory with an updated AccountRetriever. -func (f Factory) WithAccountRetriever(ar AccountRetriever) Factory { +func (f Factory) WithAccountRetriever(ar client.AccountRetriever) Factory { f.accountRetriever = ar return f } // WithChainID returns a copy of the Factory with an updated chainID. func (f Factory) WithChainID(chainID string) Factory { - f.chainID = chainID + f.txParams.chainID = chainID return f } // WithGas returns a copy of the Factory with an updated gas value. func (f Factory) WithGas(gas uint64) Factory { - f.gas = gas + f.txParams.gas = gas return f } @@ -298,7 +309,7 @@ func (f Factory) WithFees(fees string) Factory { panic(err) } - f.fees = parsedFees + f.txParams.fees = parsedFees return f } @@ -309,7 +320,7 @@ func (f Factory) WithGasPrices(gasPrices string) Factory { panic(err) } - f.gasPrices = parsedGasPrices + f.txParams.gasPrices = parsedGasPrices return f } @@ -322,81 +333,81 @@ func (f Factory) WithKeybase(keybase keyring.Keyring) Factory { // WithFromName returns a copy of the Factory with updated fromName // fromName will be use for building a simulation tx. func (f Factory) WithFromName(fromName string) Factory { - f.fromName = fromName + f.txParams.fromName = fromName return f } // WithSequence returns a copy of the Factory with an updated sequence number. func (f Factory) WithSequence(sequence uint64) Factory { - f.sequence = sequence + f.txParams.sequence = sequence return f } // WithMemo returns a copy of the Factory with an updated memo. func (f Factory) WithMemo(memo string) Factory { - f.memo = memo + f.txParams.memo = memo return f } // WithAccountNumber returns a copy of the Factory with an updated account number. func (f Factory) WithAccountNumber(accnum uint64) Factory { - f.accountNumber = accnum + f.txParams.accountNumber = accnum return f } // WithGasAdjustment returns a copy of the Factory with an updated gas adjustment. func (f Factory) WithGasAdjustment(gasAdj float64) Factory { - f.gasAdjustment = gasAdj + f.txParams.gasAdjustment = gasAdj return f } // WithSimulateAndExecute returns a copy of the Factory with an updated gas // simulation value. func (f Factory) WithSimulateAndExecute(sim bool) Factory { - f.simulateAndExecute = sim + f.txParams.simulateAndExecute = sim return f } // WithSignMode returns a copy of the Factory with an updated sign mode value. func (f Factory) WithSignMode(mode signing.SignMode) Factory { - f.signMode = mode + f.txParams.signMode = mode return f } // WithTimeoutHeight returns a copy of the Factory with an updated timeout height. func (f Factory) WithTimeoutHeight(height uint64) Factory { - f.timeoutHeight = height + f.txParams.timeoutHeight = height return f } // WithFeeGranter returns a copy of the Factory with an updated fee granter. func (f Factory) WithFeeGranter(fg sdk.AccAddress) Factory { - f.feeGranter = fg + f.txParams.feeGranter = fg return f } // WithFeePayer returns a copy of the Factory with an updated fee granter. func (f Factory) WithFeePayer(fp sdk.AccAddress) Factory { - f.feePayer = fp + f.txParams.feePayer = fp 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 PreprocessTxFn) Factory { - f.preprocessTxHook = preprocessFn + f.txParams.preprocessTxHook = preprocessFn return f } func (f Factory) WithExtensionOptions(extOpts ...*codectypes.Any) Factory { - f.extOptions = extOpts + f.txParams.ExtOptions = extOpts return f } // PreprocessTx calls the preprocessing hook with the factory parameters and // returns the result. func (f Factory) PreprocessTx(keyname string, builder TxBuilder) error { - if f.preprocessTxHook == nil { + if f.txParams.preprocessTxHook == nil { // Allow pass-through return nil } @@ -406,23 +417,23 @@ func (f Factory) PreprocessTx(keyname string, builder TxBuilder) error { return fmt.Errorf("error retrieving key from keyring: %w", err) } - return f.preprocessTxHook(f.chainID, f.Keybase().GetRecordType(key), builder) + return f.txParams.preprocessTxHook(f.txParams.chainID, f.Keybase().GetRecordType(key), builder) } -func (f Factory) AccountNumber() uint64 { return f.accountNumber } -func (f Factory) Sequence() uint64 { return f.sequence } -func (f Factory) Gas() uint64 { return f.gas } -func (f Factory) GasAdjustment() float64 { return f.gasAdjustment } -func (f Factory) Keybase() keyring.Keyring { return f.keybase } -func (f Factory) ChainID() string { return f.chainID } -func (f Factory) Memo() string { return f.memo } -func (f Factory) Fees() sdk.Coins { return f.fees } -func (f Factory) GasPrices() sdk.DecCoins { return f.gasPrices } -func (f Factory) AccountRetriever() AccountRetriever { return f.accountRetriever } -func (f Factory) TimeoutHeight() uint64 { return f.timeoutHeight } -func (f Factory) FromName() string { return f.fromName } -func (f Factory) SimulateAndExecute() bool { return f.simulateAndExecute } -func (f Factory) SignMode() signing.SignMode { return f.signMode } +func (f Factory) AccountNumber() uint64 { return f.txParams.accountNumber } +func (f Factory) Sequence() uint64 { return f.txParams.sequence } +func (f Factory) Gas() uint64 { return f.txParams.gas } +func (f Factory) GasAdjustment() float64 { return f.txParams.gasAdjustment } +func (f Factory) Keybase() keyring.Keyring { return f.keybase } +func (f Factory) ChainID() string { return f.txParams.chainID } +func (f Factory) Memo() string { return f.txParams.memo } +func (f Factory) Fees() sdk.Coins { return f.txParams.fees } +func (f Factory) GasPrices() sdk.DecCoins { return f.txParams.gasPrices } +func (f Factory) AccountRetriever() client.AccountRetriever { return f.accountRetriever } +func (f Factory) TimeoutHeight() uint64 { return f.txParams.timeoutHeight } +func (f Factory) FromName() string { return f.txParams.fromName } +func (f Factory) SimulateAndExecute() bool { return f.txParams.simulateAndExecute } +func (f Factory) SignMode() signing.SignMode { return f.txParams.signMode } // getSimPK gets the public key to use for building a simulation tx. // Note, we should only check for keys in the keybase if we are in simulate and execute mode, @@ -435,8 +446,8 @@ func (f Factory) getSimPK() (cryptotypes.PubKey, error) { pk cryptotypes.PubKey = &secp256k1.PubKey{} // use default public key type ) - if f.simulateAndExecute && f.keybase != nil { - pk, err = f.keybase.GetPubKey(f.fromName) + if f.txParams.simulateAndExecute && f.keybase != nil { + pk, err = f.keybase.GetPubKey(f.txParams.fromName) if err != nil { return nil, err } @@ -450,7 +461,7 @@ func (f Factory) getSimPK() (cryptotypes.PubKey, error) { func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) signing.SignatureData { multisigPubKey, ok := pk.(*multisig.LegacyAminoPubKey) // TODO: abstract out multisig pubkey if !ok { - return &signing.SingleSignatureData{SignMode: f.signMode} + return &signing.SingleSignatureData{SignMode: f.txParams.signMode} } multiSignatureData := make([]signing.SignatureData, 0, multisigPubKey.Threshold) @@ -477,10 +488,10 @@ func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, ove } var err error - signMode := f.signMode + signMode := f.txParams.signMode if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { // use the SignModeHandler's default mode if unspecified - signMode, err = authsigning.APISignModeToInternal(f.signingConfig.SignModeHandler().DefaultMode()) + signMode, err = authsigning.APISignModeToInternal(f.txConfig.SignModeHandler().DefaultMode()) if err != nil { return err } @@ -492,9 +503,9 @@ func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, ove } signerData := authsigning.SignerData{ - ChainID: f.chainID, - AccountNumber: f.accountNumber, - Sequence: f.sequence, + ChainID: f.txParams.chainID, + AccountNumber: f.txParams.accountNumber, + Sequence: f.txParams.sequence, PubKey: pubKey, Address: sdk.AccAddress(pubKey.Address()).String(), } @@ -514,7 +525,7 @@ func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, ove sig := signing.SignatureV2{ PubKey: pubKey, Data: &sigData, - Sequence: f.sequence, + Sequence: f.txParams.sequence, } var prevSignatures []signing.SignatureV2 @@ -540,7 +551,7 @@ func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, ove return err } - bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, f.signingConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) + bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, f.txConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) if err != nil { return err } @@ -559,7 +570,7 @@ func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, ove sig = signing.SignatureV2{ PubKey: pubKey, Data: &sigData, - Sequence: f.sequence, + Sequence: f.txParams.sequence, } if overwriteSig { diff --git a/client/v2/tx/query.go b/client/v2/tx/query.go deleted file mode 100644 index b6d16ff570e0..000000000000 --- a/client/v2/tx/query.go +++ /dev/null @@ -1,307 +0,0 @@ -package tx - -import ( - "context" - errorsmod "cosmossdk.io/errors" - "fmt" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/codec/types" - grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" - "github.com/cosmos/cosmos-sdk/types/tx" - gogogrpc "github.com/cosmos/gogoproto/grpc" - "google.golang.org/grpc" - "google.golang.org/grpc/encoding" - "google.golang.org/grpc/metadata" - "reflect" - "strconv" - "strings" - - "github.com/cockroachdb/errors" - abci "github.com/cometbft/cometbft/abci/types" - rpcclient "github.com/cometbft/cometbft/rpc/client" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "cosmossdk.io/store/rootmulti" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" -) - -var ( - _ gogogrpc.ClientConn = txContext{} - - // fallBackCodec is used by Context in case Codec is not set. - // it can process every gRPC type, except the ones which contain - // interfaces in their types. - fallBackCodec = codec.NewProtoCodec(types.NewInterfaceRegistry()) -) - -// GetNode returns an RPC client. If the context's client is not defined, an -// error is returned. -func (ctx txContext) GetNode() (CometRPC, error) { - if ctx.Client == nil { - return nil, errors.New("no RPC client is defined in offline mode") - } - - return ctx.Client, nil -} - -// Query performs a query to a CometBFT node with the provided path. -// It returns the result and height of the query upon success or an error if -// the query fails. -func (ctx txContext) Query(path string) ([]byte, int64, error) { - return ctx.query(path, nil) -} - -// QueryWithData performs a query to a CometBFT node with the provided path -// and a data payload. It returns the result and height of the query upon success -// or an error if the query fails. -func (ctx txContext) QueryWithData(path string, data []byte) ([]byte, int64, error) { - return ctx.query(path, data) -} - -// QueryStore performs a query to a CometBFT node with the provided key and -// store name. It returns the result and height of the query upon success -// or an error if the query fails. -func (ctx txContext) QueryStore(key []byte, storeName string) ([]byte, int64, error) { - return ctx.queryStore(key, storeName, "key") -} - -// QueryABCI performs a query to a CometBFT node with the provide RequestQuery. -// It returns the ResultQuery obtained from the query. The height used to perform -// the query is the RequestQuery Height if it is non-zero, otherwise the context -// height is used. -func (ctx txContext) QueryABCI(req abci.RequestQuery) (abci.ResponseQuery, error) { - return ctx.queryABCI(req) -} - -// GetFromAddress returns the from address from the context's name. -func (ctx txContext) GetFromAddress() sdk.AccAddress { - return ctx.FromAddress -} - -// Invoke implements the grpc ClientConn.Invoke method -func (ctx txContext) Invoke(grpcCtx context.Context, method string, req, reply interface{}, opts ...grpc.CallOption) (err error) { - // Two things can happen here: - // 1. either we're broadcasting a Tx, in which call we call CometBFT's broadcast endpoint directly, - // 2-1. or we are querying for state, in which case we call grpc if grpc client set. - // 2-2. or we are querying for state, in which case we call ABCI's Query if grpc client not set. - - // In both cases, we don't allow empty request args (it will panic unexpectedly). - if reflect.ValueOf(req).IsNil() { - return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "request cannot be nil") - } - - // Case 1. Broadcasting a Tx. - if reqProto, ok := req.(*tx.BroadcastTxRequest); ok { - res, ok := reply.(*tx.BroadcastTxResponse) - if !ok { - return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "expected %T, got %T", (*tx.BroadcastTxResponse)(nil), req) - } - - broadcastRes, err := TxServiceBroadcast(grpcCtx, ctx, reqProto) - if err != nil { - return err - } - *res = *broadcastRes - - return err - } - - if ctx.GRPCClient != nil { - // Case 2-1. Invoke grpc. - return ctx.GRPCClient.Invoke(grpcCtx, method, req, reply, opts...) - } - - // Case 2-2. Querying state via abci query. - reqBz, err := ctx.gRPCCodec().Marshal(req) - if err != nil { - return err - } - - // parse height header - md, _ := metadata.FromOutgoingContext(grpcCtx) - if heights := md.Get(grpctypes.GRPCBlockHeightHeader); len(heights) > 0 { - height, err := strconv.ParseInt(heights[0], 10, 64) - if err != nil { - return err - } - if height < 0 { - return errorsmod.Wrapf( - sdkerrors.ErrInvalidRequest, - "client.Context.Invoke: height (%d) from %q must be >= 0", height, grpctypes.GRPCBlockHeightHeader) - } - - ctx = ctx.WithHeight(height) - } - - abciReq := abci.RequestQuery{ - Path: method, - Data: reqBz, - Height: ctx.Height, - } - - res, err := ctx.QueryABCI(abciReq) - if err != nil { - return err - } - - err = ctx.gRPCCodec().Unmarshal(res.Value, reply) - if err != nil { - return err - } - - // Create header metadata. For now the headers contain: - // - block height - // We then parse all the call options, if the call option is a - // HeaderCallOption, then we manually set the value of that header to the - // metadata. - md = metadata.Pairs(grpctypes.GRPCBlockHeightHeader, strconv.FormatInt(res.Height, 10)) - for _, callOpt := range opts { - header, ok := callOpt.(grpc.HeaderCallOption) - if !ok { - continue - } - - *header.HeaderAddr = md - } - - if ctx.InterfaceRegistry != nil { - return types.UnpackInterfaces(reply, ctx.InterfaceRegistry) - } - - return nil -} - -// NewStream implements the grpc ClientConn.NewStream method -func (txContext) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { - return nil, fmt.Errorf("streaming rpc not supported") -} - -// gRPCCodec checks if Context's Codec is codec.GRPCCodecProvider -// otherwise it returns fallBackCodec. -func (ctx txContext) gRPCCodec() encoding.Codec { - if ctx.Codec == nil { - return fallBackCodec.GRPCCodec() - } - - pc, ok := ctx.Codec.(codec.GRPCCodecProvider) - if !ok { - return fallBackCodec.GRPCCodec() - } - - return pc.GRPCCodec() -} - -func (ctx txContext) queryABCI(req abci.RequestQuery) (abci.ResponseQuery, error) { - node, err := ctx.GetNode() - if err != nil { - return abci.ResponseQuery{}, err - } - - var queryHeight int64 - if req.Height != 0 { - queryHeight = req.Height - } else { - // fallback on the context height - queryHeight = ctx.Height - } - - opts := rpcclient.ABCIQueryOptions{ - Height: queryHeight, - Prove: req.Prove, - } - - result, err := node.ABCIQueryWithOptions(context.Background(), req.Path, req.Data, opts) - if err != nil { - return abci.ResponseQuery{}, err - } - - if !result.Response.IsOK() { - return abci.ResponseQuery{}, sdkErrorToGRPCError(result.Response) - } - - // data from trusted node or subspace query doesn't need verification - if !opts.Prove || !isQueryStoreWithProof(req.Path) { - return result.Response, nil - } - - return result.Response, nil -} - -// TxServiceBroadcast is a helper function to broadcast a Tx with the correct gRPC types -// from the tx service. Calls `clientCtx.BroadcastTx` under the hood. -func TxServiceBroadcast(_ context.Context, clientCtx txContext, req *tx.BroadcastTxRequest) (*tx.BroadcastTxResponse, error) { - if req == nil || req.TxBytes == nil { - return nil, status.Error(codes.InvalidArgument, "invalid empty tx") - } - - clientCtx = clientCtx.WithBroadcastMode(normalizeBroadcastMode(req.Mode)) - resp, err := clientCtx.BroadcastTx(req.TxBytes) - if err != nil { - return nil, err - } - - return &tx.BroadcastTxResponse{ - TxResponse: resp, - }, nil -} - -func sdkErrorToGRPCError(resp abci.ResponseQuery) error { - switch resp.Code { - case sdkerrors.ErrInvalidRequest.ABCICode(): - return status.Error(codes.InvalidArgument, resp.Log) - case sdkerrors.ErrUnauthorized.ABCICode(): - return status.Error(codes.Unauthenticated, resp.Log) - case sdkerrors.ErrKeyNotFound.ABCICode(): - return status.Error(codes.NotFound, resp.Log) - default: - return status.Error(codes.Unknown, resp.Log) - } -} - -// query performs a query to a CometBFT node with the provided store name -// and path. It returns the result and height of the query upon success -// or an error if the query fails. -func (ctx txContext) query(path string, key []byte) ([]byte, int64, error) { - resp, err := ctx.queryABCI(abci.RequestQuery{ - Path: path, - Data: key, - Height: ctx.Height, - }) - if err != nil { - return nil, 0, err - } - - return resp.Value, resp.Height, nil -} - -// queryStore performs a query to a CometBFT node with the provided a store -// name and path. It returns the result and height of the query upon success -// or an error if the query fails. -func (ctx txContext) queryStore(key []byte, storeName, endPath string) ([]byte, int64, error) { - path := fmt.Sprintf("/store/%s/%s", storeName, endPath) - return ctx.query(path, key) -} - -// isQueryStoreWithProof expects a format like /// -// queryType must be "store" and subpath must be "key" to require a proof. -func isQueryStoreWithProof(path string) bool { - if !strings.HasPrefix(path, "/") { - return false - } - - paths := strings.SplitN(path[1:], "/", 3) - - switch { - case len(paths) != 3: - return false - case paths[0] != "store": - return false - case rootmulti.RequireProof("/" + paths[2]): - return true - } - - return false -} diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index 2c17bf53c0d4..4ef6ba74b2c2 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/input" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -16,18 +17,9 @@ import ( "os" ) -// GasEstimateResponse defines a response definition for tx gas estimation. -type GasEstimateResponse struct { - GasEstimate uint64 `json:"gas_estimate" yaml:"gas_estimate"` -} - -func (gr GasEstimateResponse) String() string { - return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) -} - // GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxCLI(clientCtx txContext, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { +func GenerateOrBroadcastTxCLI(clientCtx client.Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { txf, err := NewFactoryCLI(clientCtx, flagSet) if err != nil { return err @@ -38,7 +30,7 @@ func GenerateOrBroadcastTxCLI(clientCtx txContext, flagSet *pflag.FlagSet, msgs // GenerateOrBroadcastTxWithFactory will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxWithFactory(clientCtx txContext, txf Factory, msgs ...sdk.MsgV2) error { +func GenerateOrBroadcastTxWithFactory(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { // Validate all msgs before generating or broadcasting the tx. // We were calling ValidateBasic separately in each CLI handler before. // Right now, we're factorizing that call inside this function. @@ -74,7 +66,7 @@ func GenerateOrBroadcastTxWithFactory(clientCtx txContext, txf Factory, msgs ... // 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 txContext, txf Factory, msgs ...sdk.MsgV2) error { +func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.MsgV2) error { txf, err := txf.Prepare(clientCtx) if err != nil { return err @@ -170,17 +162,17 @@ func CalculateGas( } // makeAuxSignerData generates an AuxSignerData from the client inputs. -func makeAuxSignerData(clientCtx txContext, f Factory, msgs ...sdk.MsgV2) (tx.AuxSignerData, error) { +func makeAuxSignerData(clientCtx client.Context, f Factory, msgs ...sdk.Msg) (tx.AuxSignerData, error) { b := NewAuxTxBuilder() - fromAddress, name, _, err := GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) + fromAddress, name, _, err := client.GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) if err != nil { return tx.AuxSignerData{}, err } b.SetAddress(fromAddress.String()) if clientCtx.Offline { - b.SetAccountNumber(f.accountNumber) - b.SetSequence(f.sequence) + b.SetAccountNumber(f.AccountNumber()) + b.SetSequence(f.Sequence()) } else { accNum, seq, err := clientCtx.AccountRetriever.GetAccountNumberSequence(clientCtx, fromAddress) if err != nil { @@ -216,7 +208,7 @@ func makeAuxSignerData(clientCtx txContext, f Factory, msgs ...sdk.MsgV2) (tx.Au return tx.AuxSignerData{}, err } - sig, err := clientCtx.Keyring.Sign(name, signBz, f.signMode) + sig, err := clientCtx.Keyring.Sign(name, signBz, f.SignMode()) if err != nil { return tx.AuxSignerData{}, err } @@ -227,7 +219,7 @@ func makeAuxSignerData(clientCtx txContext, f Factory, msgs ...sdk.MsgV2) (tx.Au // checkMultipleSigners checks that there can be maximum one DIRECT signer in // a tx. -func checkMultipleSigners(tx TxV2) error { +func checkMultipleSigners(tx sdk.Tx) error { directSigners := 0 sigsV2, err := tx.GetSignaturesV2() if err != nil { diff --git a/client/v2/tx/types.go b/client/v2/tx/types.go index a0b4ac6b89f7..2cdee1fd222f 100644 --- a/client/v2/tx/types.go +++ b/client/v2/tx/types.go @@ -1,12 +1,12 @@ package tx -import ( - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/types/tx/signing" -) +import "fmt" -type SigVerifiableTxV2 interface { - GetSigners() ([][]byte, error) - GetPubKeys() ([]cryptotypes.PubKey, error) // If signer already has pubkey in context, this list will have nil in its place - GetSignaturesV2() ([]signing.SignatureV2, error) +// GasEstimateResponse defines a response definition for tx gas estimation. +type GasEstimateResponse struct { + GasEstimate uint64 `json:"gas_estimate" yaml:"gas_estimate"` +} + +func (gr GasEstimateResponse) String() string { + return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) } diff --git a/types/tx_msg_v2.go b/types/tx_msg_v2.go deleted file mode 100644 index c8fc50f44f4d..000000000000 --- a/types/tx_msg_v2.go +++ /dev/null @@ -1,49 +0,0 @@ -package types - -import ( - "encoding/json" - "fmt" - protov2 "google.golang.org/protobuf/proto" - - "github.com/cosmos/cosmos-sdk/codec" -) - -type ( - // MsgV2 defines the interface a transaction message needed to fulfill. - MsgV2 = protov2.Message - - ProtoMessage interface { - Msg | MsgV2 - } - - // TxV2 defines an interface a transaction must fulfill. - TxV2 interface { - // GetMsgs gets the transaction's messages as google.golang.org/protobuf/proto.Message's. - GetMsgs() ([]MsgV2, error) - } - - // TxV2Decoder unmarshals transaction bytes - TxV2Decoder func(txBytes []byte) (TxV2, error) - - // TxV2Encoder marshals transaction to bytes - TxV2Encoder func(tx TxV2) ([]byte, error) -) - -// GetMsgV2FromTypeURL returns a `sdk.MsgV2` message type from a type URL -func GetMsgV2FromTypeURL(cdc codec.Codec, input string) (MsgV2, error) { - var msg MsgV2 - bz, err := json.Marshal(struct { - Type string `json:"@type"` - }{ - Type: input, - }) - if err != nil { - return nil, err - } - - if err := cdc.UnmarshalInterfaceJSON(bz, &msg); err != nil { - return nil, fmt.Errorf("failed to determine sdk.Msg for %s URL : %w", input, err) - } - - return msg, nil -} From 686ec79c35b4c90c2ec0660b1a6a2dbe6dbb3b5d Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Mon, 15 Apr 2024 10:05:10 -0300 Subject: [PATCH 08/14] continue clien/v2 imports cleanup --- client/flags/flags.go | 16 +- client/v2/offchain/sign.go | 6 +- client/v2/offchain/signature.go | 11 + client/v2/tx/aux_builder.go | 5 +- client/v2/tx/builder.go | 14 +- client/v2/tx/config.go | 6 +- client/v2/tx/context.go | 457 ++++++++++++++++++++++++++++++++ client/v2/tx/factory.go | 315 ++++++++++++---------- client/v2/tx/tx.go | 21 +- client/v2/tx/types.go | 21 +- codec/types/util.go | 15 +- crypto/types/types.go | 18 ++ 12 files changed, 722 insertions(+), 183 deletions(-) create mode 100644 client/v2/tx/context.go diff --git a/client/flags/flags.go b/client/flags/flags.go index ef9bf4c22ca4..cb7f64ebee73 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -1,8 +1,8 @@ package flags import ( + apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" "fmt" - "github.com/cosmos/cosmos-sdk/types/tx/signing" "strconv" "github.com/spf13/cobra" @@ -203,19 +203,19 @@ func ParseGasSetting(gasStr string) (GasSetting, error) { } } -func ParseSignMode(signModeStr string) signing.SignMode { - signMode := signing.SignMode_SIGN_MODE_UNSPECIFIED +func ParseSignModeStr(signModeStr string) apisigning.SignMode { + signMode := apisigning.SignMode_SIGN_MODE_UNSPECIFIED switch signModeStr { case SignModeDirect: - signMode = signing.SignMode_SIGN_MODE_DIRECT + signMode = apisigning.SignMode_SIGN_MODE_DIRECT case SignModeLegacyAminoJSON: - signMode = signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON + signMode = apisigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON case SignModeDirectAux: - signMode = signing.SignMode_SIGN_MODE_DIRECT_AUX + signMode = apisigning.SignMode_SIGN_MODE_DIRECT_AUX case SignModeTextual: - signMode = signing.SignMode_SIGN_MODE_TEXTUAL + signMode = apisigning.SignMode_SIGN_MODE_TEXTUAL case SignModeEIP191: - signMode = signing.SignMode_SIGN_MODE_EIP_191 + signMode = apisigning.SignMode_SIGN_MODE_EIP_191 } return signMode diff --git a/client/v2/offchain/sign.go b/client/v2/offchain/sign.go index fd01227ed13e..7171140c1884 100644 --- a/client/v2/offchain/sign.go +++ b/client/v2/offchain/sign.go @@ -28,7 +28,7 @@ const ( signMode = apisigning.SignMode_SIGN_MODE_TEXTUAL ) -type signerData struct { +type SignerData struct { Address string ChainID string AccountNumber uint64 @@ -90,7 +90,7 @@ func sign(ctx client.Context, fromName, digest string) (*apitx.Tx, error) { return nil, err } - signerData := signerData{ + signerData := SignerData{ Address: addr, ChainID: ExpectedChainID, AccountNumber: ExpectedAccountNumber, @@ -139,7 +139,7 @@ func sign(ctx client.Context, fromName, digest string) (*apitx.Tx, error) { // getSignBytes gets the bytes to be signed for the given Tx and SignMode. func getSignBytes(ctx context.Context, handlerMap *txsigning.HandlerMap, - signerData signerData, + signerData SignerData, tx *builder, ) ([]byte, error) { txData, err := tx.GetSigningTxData() diff --git a/client/v2/offchain/signature.go b/client/v2/offchain/signature.go index d7b9769de983..7cf980dadf67 100644 --- a/client/v2/offchain/signature.go +++ b/client/v2/offchain/signature.go @@ -20,6 +20,17 @@ type SingleSignatureData struct { Signature []byte } +type MultiSignatureData struct { + // BitArray is a compact way of indicating which signers from the multisig key + // have signed + BitArray []byte + + // Signatures is the nested SignatureData's for each signer + Signatures []SignatureData +} + +func (m *MultiSignatureData) isSignatureData() {} + type OffchainSignature struct { // PubKey is the public key to use for verifying the signature PubKey cryptotypes.PubKey diff --git a/client/v2/tx/aux_builder.go b/client/v2/tx/aux_builder.go index d67f7d65416f..cded148bfbc0 100644 --- a/client/v2/tx/aux_builder.go +++ b/client/v2/tx/aux_builder.go @@ -2,6 +2,7 @@ package tx import ( "context" + apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" "github.com/cosmos/gogoproto/proto" "google.golang.org/protobuf/types/known/anypb" @@ -117,9 +118,9 @@ func (b *AuxTxBuilder) SetPubKey(pk cryptotypes.PubKey) error { // SetSignMode sets the aux signer's sign mode. Allowed sign modes are // DIRECT_AUX and LEGACY_AMINO_JSON. -func (b *AuxTxBuilder) SetSignMode(mode signing.SignMode) error { +func (b *AuxTxBuilder) SetSignMode(mode apitxsigning.SignMode) error { switch mode { - case signing.SignMode_SIGN_MODE_DIRECT_AUX, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON: + case apitxsigning.SignMode_SIGN_MODE_DIRECT_AUX, apitxsigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON: default: return sdkerrors.ErrInvalidRequest.Wrapf("AuxTxBuilder can only sign with %s or %s", signing.SignMode_SIGN_MODE_DIRECT_AUX, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index de7d4a7060d0..e09f9c814c65 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -1,11 +1,11 @@ package tx import ( - apibase "cosmossdk.io/api/cosmos/base/v1beta1" - apitx "cosmossdk.io/api/cosmos/tx/v1beta1" "cosmossdk.io/client/v2/offchain" + txsigning "cosmossdk.io/x/tx/signing" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" + typestx "github.com/cosmos/cosmos-sdk/types/tx" ) type ExtendedTxBuilder interface { @@ -17,20 +17,22 @@ type ExtendedTxBuilder interface { // signatures, and provide canonical bytes to sign over. The transaction must // also know how to encode itself. type TxBuilder interface { - GetTx() apitx.Tx + GetTx() typestx.Tx + GetSigningTxData() txsigning.TxData + SetMsgs(...sdk.Msg) error SetMemo(string) - SetFeeAmount([]apibase.Coin) + SetFeeAmount([]sdk.Coin) SetFeePayer(string) SetGasLimit(uint64) SetTimeoutHeight(uint64) SetFeeGranter(string) SetUnordered(bool) SetSignatures(...offchain.OffchainSignature) error - SetAuxSignerData(apitx.AuxSignerData) error + SetAuxSignerData(typestx.AuxSignerData) error } type TxBuilderProvider interface { NewTxBuilder() TxBuilder - WrapTxBuilder(apitx.Tx) (TxBuilder, error) + WrapTxBuilder(typestx.Tx) (TxBuilder, error) } diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index 017e18d28ecf..bb99840aee01 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -1,8 +1,8 @@ package tx import ( + apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" txsigning "cosmossdk.io/x/tx/signing" - "github.com/cosmos/cosmos-sdk/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -37,7 +37,7 @@ type TxParameters struct { timeoutHeight uint64 chainID string memo string - signMode signingtypes.SignMode + signMode apitxsigning.SignMode AccountConfig GasConfig @@ -74,7 +74,7 @@ type ExecutionOptions struct { offline bool generateOnly bool simulateAndExecute bool - preprocessTxHook client.PreprocessTxFn + preprocessTxHook PreprocessTxFn } type ExtensionOptions struct { diff --git a/client/v2/tx/context.go b/client/v2/tx/context.go new file mode 100644 index 000000000000..0a7edeb9fa07 --- /dev/null +++ b/client/v2/tx/context.go @@ -0,0 +1,457 @@ +package tx + +import ( + "bufio" + "context" + "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/core/address" + "encoding/json" + "fmt" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + "github.com/spf13/viper" + "google.golang.org/grpc" + "io" + "os" + "path" + "sigs.k8s.io/yaml" + "strings" +) + +// 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 { + Client client.CometRPC + GRPCClient *grpc.ClientConn + ChainID string + Codec codec.Codec + InterfaceRegistry codectypes.InterfaceRegistry + Input io.Reader + Keyring keyring.Keyring + KeyringOptions []keyring.Option + KeyringDir string + KeyringDefaultKeyName string + Output io.Writer + OutputFormat string + Height int64 + HomeDir string + // From is a name or an address of a keyring account used to set FromName and FromAddress fields. + // Should be set by the "from" flag. + From string + // Name of a keyring account used to sign transactions. + FromName string + // Address of a keyring account used to sign transactions. + FromAddress string + BroadcastMode string + SignModeStr string + UseLedger bool + Simulate bool + GenerateOnly bool + Offline bool + SkipConfirm bool + TxConfig TxConfig + AccountRetriever client.AccountRetriever + NodeURI string + FeePayer string + FeeGranter string + Viper *viper.Viper + LedgerHasProtobuf bool + PreprocessTxHook PreprocessTxFn + + // IsAux is true when the signer is an auxiliary signer (e.g. the tipper). + IsAux bool + + // CmdContext is the context.Context from the Cobra command. + CmdContext context.Context + + // Address codecs + AddressCodec address.Codec + ValidatorAddressCodec address.Codec + ConsensusAddressCodec address.Codec + + // Bech32 address prefixes. + AddressPrefix string + ValidatorPrefix string +} + +// WithCmdContext returns a copy of the context with an updated context.Context, +// usually set to the cobra cmd context. +func (ctx Context) WithCmdContext(c context.Context) Context { + ctx.CmdContext = c + return ctx +} + +// WithKeyring returns a copy of the context with an updated keyring. +func (ctx Context) WithKeyring(k keyring.Keyring) Context { + ctx.Keyring = k + return ctx +} + +// WithKeyringOptions returns a copy of the context with an updated keyring. +func (ctx Context) WithKeyringOptions(opts ...keyring.Option) Context { + ctx.KeyringOptions = opts + return ctx +} + +// WithInput returns a copy of the context with an updated input. +func (ctx Context) WithInput(r io.Reader) Context { + // convert to a bufio.Reader to have a shared buffer between the keyring and the + // the Commands, ensuring a read from one advance the read pointer for the other. + // see https://github.com/cosmos/cosmos-sdk/issues/9566. + ctx.Input = bufio.NewReader(r) + return ctx +} + +// WithCodec returns a copy of the Context with an updated Codec. +func (ctx Context) WithCodec(m codec.Codec) Context { + ctx.Codec = m + return ctx +} + +// WithOutput returns a copy of the context with an updated output writer (e.g. stdout). +func (ctx Context) WithOutput(w io.Writer) Context { + ctx.Output = w + return ctx +} + +// WithFrom returns a copy of the context with an updated from address or name. +func (ctx Context) WithFrom(from string) Context { + ctx.From = from + return ctx +} + +// WithOutputFormat returns a copy of the context with an updated OutputFormat field. +func (ctx Context) WithOutputFormat(format string) Context { + ctx.OutputFormat = format + return ctx +} + +// WithNodeURI returns a copy of the context with an updated node URI. +func (ctx Context) WithNodeURI(nodeURI string) Context { + ctx.NodeURI = nodeURI + return ctx +} + +// WithHeight returns a copy of the context with an updated height. +func (ctx Context) WithHeight(height int64) Context { + ctx.Height = height + return ctx +} + +// WithClient returns a copy of the context with an updated RPC client +// instance. +func (ctx Context) WithClient(client client.CometRPC) Context { + ctx.Client = client + return ctx +} + +// WithGRPCClient returns a copy of the context with an updated GRPC client +// instance. +func (ctx Context) WithGRPCClient(grpcClient *grpc.ClientConn) Context { + ctx.GRPCClient = grpcClient + return ctx +} + +// WithUseLedger returns a copy of the context with an updated UseLedger flag. +func (ctx Context) WithUseLedger(useLedger bool) Context { + ctx.UseLedger = useLedger + return ctx +} + +// WithChainID returns a copy of the context with an updated chain ID. +func (ctx Context) WithChainID(chainID string) Context { + ctx.ChainID = chainID + return ctx +} + +// WithHomeDir returns a copy of the Context with HomeDir set. +func (ctx Context) WithHomeDir(dir string) Context { + if dir != "" { + ctx.HomeDir = dir + } + return ctx +} + +// WithKeyringDir returns a copy of the Context with KeyringDir set. +func (ctx Context) WithKeyringDir(dir string) Context { + ctx.KeyringDir = dir + return ctx +} + +// WithKeyringDefaultKeyName returns a copy of the Context with KeyringDefaultKeyName set. +func (ctx Context) WithKeyringDefaultKeyName(keyName string) Context { + ctx.KeyringDefaultKeyName = keyName + return ctx +} + +// WithGenerateOnly returns a copy of the context with updated GenerateOnly value +func (ctx Context) WithGenerateOnly(generateOnly bool) Context { + ctx.GenerateOnly = generateOnly + return ctx +} + +// WithSimulation returns a copy of the context with updated Simulate value +func (ctx Context) WithSimulation(simulate bool) Context { + ctx.Simulate = simulate + return ctx +} + +// WithOffline returns a copy of the context with updated Offline value. +func (ctx Context) WithOffline(offline bool) Context { + ctx.Offline = offline + return ctx +} + +// WithFromName returns a copy of the context with an updated from account name. +func (ctx Context) WithFromName(name string) Context { + ctx.FromName = name + return ctx +} + +// WithFromAddress returns a copy of the context with an updated from account +// address. +func (ctx Context) WithFromAddress(addr string) Context { + ctx.FromAddress = addr + return ctx +} + +// WithFeePayerAddress returns a copy of the context with an updated fee payer account +// address. +func (ctx Context) WithFeePayerAddress(addr string) Context { + ctx.FeePayer = addr + return ctx +} + +// WithFeeGranterAddress returns a copy of the context with an updated fee granter account +// address. +func (ctx Context) WithFeeGranterAddress(addr string) Context { + ctx.FeeGranter = addr + return ctx +} + +// WithBroadcastMode returns a copy of the context with an updated broadcast +// mode. +func (ctx Context) WithBroadcastMode(mode string) Context { + ctx.BroadcastMode = mode + return ctx +} + +// WithSignModeStr returns a copy of the context with an updated SignMode +// value. +func (ctx Context) WithSignModeStr(signModeStr string) Context { + ctx.SignModeStr = signModeStr + return ctx +} + +// WithSkipConfirmation returns a copy of the context with an updated SkipConfirm +// value. +func (ctx Context) WithSkipConfirmation(skip bool) Context { + ctx.SkipConfirm = skip + return ctx +} + +// WithTxConfig returns the context with an updated TxConfig +func (ctx Context) WithTxConfig(generator TxConfig) Context { + ctx.TxConfig = generator + return ctx +} + +// WithAccountRetriever returns the context with an updated AccountRetriever +func (ctx Context) WithAccountRetriever(retriever client.AccountRetriever) Context { + ctx.AccountRetriever = retriever + return ctx +} + +// WithInterfaceRegistry returns the context with an updated InterfaceRegistry +func (ctx Context) WithInterfaceRegistry(interfaceRegistry codectypes.InterfaceRegistry) Context { + ctx.InterfaceRegistry = interfaceRegistry + return ctx +} + +// WithViper returns the context with Viper field. This Viper instance is used to read +// client-side config from the config file. +func (ctx Context) WithViper(prefix string) Context { + v := viper.New() + + if prefix == "" { + executableName, _ := os.Executable() + prefix = path.Base(executableName) + } + + v.SetEnvPrefix(prefix) + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + v.AutomaticEnv() + ctx.Viper = v + return ctx +} + +// WithAux returns a copy of the context with an updated IsAux value. +func (ctx Context) WithAux(isAux bool) Context { + ctx.IsAux = isAux + return ctx +} + +// WithLedgerHasProtobuf returns the context with the provided boolean value, indicating +// whether the target Ledger application can support Protobuf payloads. +func (ctx Context) WithLedgerHasProtobuf(val bool) Context { + ctx.LedgerHasProtobuf = val + 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 +} + +// WithAddressCodec returns the context with the provided address codec. +func (ctx Context) WithAddressCodec(addressCodec address.Codec) Context { + ctx.AddressCodec = addressCodec + return ctx +} + +// WithValidatorAddressCodec returns the context with the provided validator address codec. +func (ctx Context) WithValidatorAddressCodec(validatorAddressCodec address.Codec) Context { + ctx.ValidatorAddressCodec = validatorAddressCodec + return ctx +} + +// WithConsensusAddressCodec returns the context with the provided consensus address codec. +func (ctx Context) WithConsensusAddressCodec(consensusAddressCodec address.Codec) Context { + ctx.ConsensusAddressCodec = consensusAddressCodec + return ctx +} + +// WithAddressPrefix returns the context with the provided address bech32 prefix. +func (ctx Context) WithAddressPrefix(addressPrefix string) Context { + ctx.AddressPrefix = addressPrefix + return ctx +} + +// WithValidatorPrefix returns the context with the provided validator bech32 prefix. +func (ctx Context) WithValidatorPrefix(validatorPrefix string) Context { + ctx.ValidatorPrefix = validatorPrefix + 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)) +} + +// PrintBytes prints the raw bytes to ctx.Output if it's defined, otherwise to os.Stdout. +// NOTE: for printing a complex state object, you should use ctx.PrintOutput +func (ctx Context) PrintBytes(o []byte) error { + writer := ctx.Output + if writer == nil { + writer = os.Stdout + } + + _, err := writer.Write(o) + return err +} + +// PrintProto outputs toPrint to the ctx.Output based on ctx.OutputFormat which is +// either text or json. If text, toPrint will be YAML encoded. Otherwise, toPrint +// will be JSON encoded using ctx.Codec. An error is returned upon failure. +func (ctx Context) PrintProto(toPrint proto.Message) error { + // always serialize JSON initially because proto json can't be directly YAML encoded + out, err := ctx.Codec.MarshalJSON(toPrint) + if err != nil { + return err + } + return ctx.printOutput(out) +} + +// PrintRaw is a variant of PrintProto that doesn't require a proto.Message type +// and uses a raw JSON message. No marshaling is performed. +func (ctx Context) PrintRaw(toPrint json.RawMessage) error { + return ctx.printOutput(toPrint) +} + +func (ctx Context) printOutput(out []byte) error { + var err error + if ctx.OutputFormat == "text" { + out, err = yaml.JSONToYAML(out) + if err != nil { + return err + } + } + + writer := ctx.Output + if writer == nil { + writer = os.Stdout + } + + _, err = writer.Write(out) + if err != nil { + return err + } + + if ctx.OutputFormat != "text" { + // append new-line for formats besides YAML + _, err = writer.Write([]byte("\n")) + if err != nil { + return err + } + } + + return nil +} + +// GetFromFields returns a from account address, account name and keyring type, given either an address or key name. +// If clientCtx.Simulate is true the keystore is not accessed and a valid address must be provided +// If clientCtx.GenerateOnly is true the keystore is only accessed if a key name is provided +// If from is empty, the default key if specified in the context will be used +func GetFromFields(clientCtx Context, kr keyring.Keyring, from string) (sdk.AccAddress, string, keyring.KeyType, error) { + if from == "" && clientCtx.KeyringDefaultKeyName != "" { + from = clientCtx.KeyringDefaultKeyName + _ = clientCtx.PrintString(fmt.Sprintf("No key name or address provided; using the default key: %s\n", clientCtx.KeyringDefaultKeyName)) + } + + if from == "" { + return nil, "", 0, nil + } + + addr, err := clientCtx.AddressCodec.StringToBytes(from) + switch { + case clientCtx.Simulate: + if err != nil { + return nil, "", 0, fmt.Errorf("a valid address must be provided in simulation mode: %w", err) + } + + return addr, "", 0, nil + + case clientCtx.GenerateOnly: + if err == nil { + return addr, "", 0, nil + } + } + + var k *keyring.Record + if err == nil { + k, err = kr.KeyByAddress(addr) + if err != nil { + return nil, "", 0, err + } + } else { + k, err = kr.Key(from) + if err != nil { + return nil, "", 0, err + } + } + + addr, err = k.GetAddress() + if err != nil { + return nil, "", 0, err + } + + return addr, k.Name, k.GetType(), nil +} diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index f8a1c1a2507c..d13435286c69 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -2,17 +2,18 @@ package tx import ( "context" - apibase "cosmossdk.io/api/cosmos/base/v1beta1" + apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/client/v2/offchain" + "cosmossdk.io/x/tx/signing" + "google.golang.org/protobuf/types/known/anypb" + "cosmossdk.io/client/v2/autocli/keyring" "cosmossdk.io/math" - authsigning "cosmossdk.io/x/auth/signing" + "errors" "fmt" "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/types/tx/signing" - "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/client/flags" @@ -35,7 +36,7 @@ type Factory struct { txParams TxParameters } -func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, error) { +func NewFactoryCLI(clientCtx Context, flagSet *pflag.FlagSet) (Factory, error) { if clientCtx.Viper == nil { clientCtx = clientCtx.WithViper("") } @@ -62,7 +63,7 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, e return Factory{}, errors.New("chain ID required but not specified") } - signMode := flags.ParseSignMode(clientCtx.SignModeStr) + signMode := flags.ParseSignModeStr(clientCtx.SignModeStr) memo := clientCtx.Viper.GetString(flags.FlagNote) timeoutHeight := clientCtx.Viper.GetUint64(flags.FlagTimeoutHeight) unordered := clientCtx.Viper.GetBool(flags.FlagUnordered) @@ -86,15 +87,15 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, e accountNumber: accNum, sequence: accSeq, fromName: clientCtx.FromName, - fromAddress: clientCtx.FromAddress, + fromAddress: sdk.MustAccAddressFromBech32(clientCtx.FromAddress), }, GasConfig: GasConfig{ gas: gasSetting.Gas, gasAdjustment: gasAdj, }, FeeConfig: FeeConfig{ - feeGranter: clientCtx.FeeGranter, - feePayer: clientCtx.FeePayer, + feeGranter: sdk.MustAccAddressFromBech32(clientCtx.FeeGranter), + feePayer: sdk.MustAccAddressFromBech32(clientCtx.FeePayer), }, ExecutionOptions: ExecutionOptions{ unordered: unordered, @@ -174,27 +175,25 @@ func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (TxBuilder, error) { // Derive the fees based on the provided gas prices, where // fee = ceil(gasPrice * gasLimit). - apiFees := make([]apibase.Coin, len(f.txParams.gasPrices)) + fees = make([]sdk.Coin, len(f.txParams.gasPrices)) for i, gp := range f.txParams.gasPrices { fee := gp.Amount.Mul(glDec) - apiFees[i] = apibase.Coin{Denom: gp.Denom, Amount: fee.Ceil().RoundInt().String()} + fees[i] = sdk.Coin{Denom: gp.Denom, Amount: fee.Ceil().RoundInt()} } } - // Prevent simple inclusion of a valid mnemonic in the memo field - if f.txParams.memo != "" && bip39.IsMnemonicValid(strings.ToLower(f.txParams.memo)) { - return nil, errors.New("cannot provide a valid mnemonic seed in the memo field") + if err := ValidateMemo(f.txParams.memo); err != nil { + return nil, err } txBuilder := f.txConfig.NewTxBuilder() - if err := txBuilder.SetMsgs(msgs...); err != nil { return nil, err } txBuilder.SetMemo(f.txParams.memo) - txBuilder.SetFeeAmount(apiFees) + txBuilder.SetFeeAmount(fees) txBuilder.SetGasLimit(f.txParams.gas) txBuilder.SetFeeGranter(f.txParams.feeGranter.String()) txBuilder.SetFeePayer(f.txParams.feePayer.String()) @@ -229,7 +228,7 @@ func (f Factory) PrintUnsignedTx(clientCtx client.Context, msgs ...sdk.Msg) erro return err } - f = f.WithGas(adjusted) + f.WithGas(adjusted) _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: f.Gas()}) } @@ -254,7 +253,7 @@ func (f Factory) PrintUnsignedTx(clientCtx client.Context, msgs ...sdk.Msg) erro // 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.MsgV2) ([]byte, error) { +func (f Factory) BuildSimTx(msgs ...sdk.Msg) ([]byte, error) { txb, err := f.BuildUnsignedTx(msgs...) if err != nil { return nil, err @@ -267,7 +266,7 @@ func (f Factory) BuildSimTx(msgs ...sdk.MsgV2) ([]byte, error) { // Create an empty signature literal as the ante handler will populate with a // sentinel pubkey. - sig := signing.SignatureV2{ + sig := offchain.OffchainSignature{ PubKey: pk, Data: f.getSimSignatureData(pk), Sequence: f.Sequence(), @@ -284,6 +283,153 @@ func (f Factory) BuildSimTx(msgs ...sdk.MsgV2) ([]byte, error) { return encoder(txb.GetTx()) } +// Sign signs a given tx with a named key. The bytes signed over are canonical. +// The resulting signature will be added to the transaction builder overwriting the previous +// ones if overwrite=true (otherwise, the signature will be appended). +// Signing a transaction with multiple signers in the DIRECT mode is not supported and will +// return an error. +// An error is returned upon failure. +func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, overwriteSig bool) error { + if f.keybase == nil { + return errors.New("keybase must be set prior to signing a transaction") + } + + var err error + signMode := f.txParams.signMode + if signMode == apitxsigning.SignMode_SIGN_MODE_UNSPECIFIED { + signMode = f.txConfig.SignModeHandler().DefaultMode() + } + + pubKey, err := f.keybase.GetPubKey(name) + if err != nil { + return err + } + + signerData := offchain.SignerData{ + ChainID: f.txParams.chainID, + AccountNumber: f.txParams.accountNumber, + Sequence: f.txParams.sequence, + PubKey: pubKey, + Address: sdk.AccAddress(pubKey.Address()).String(), + } + + tx := txBuilder.GetTx() + txWrap := TxWrapper{Tx: &tx} + + // For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on + // TxBuilder under the hood, and SignerInfos is needed to be generated the + // sign bytes. This is the reason for setting SetSignatures here, with a + // nil signature. + // + // Note: this line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it + // also doesn't affect its generated sign bytes, so for code's simplicity + // sake, we put it here. + sigData := offchain.SingleSignatureData{ + SignMode: signMode, + Signature: nil, + } + sig := offchain.OffchainSignature{ + PubKey: pubKey, + Data: &sigData, + Sequence: f.txParams.sequence, + } + + var prevSignatures []offchain.OffchainSignature + if !overwriteSig { + prevSignatures, err = txWrap.GetSignatures() + if err != nil { + return err + } + } + // Overwrite or append signer infos. + var sigs []offchain.OffchainSignature + if overwriteSig { + sigs = []offchain.OffchainSignature{sig} + } else { + sigs = append(sigs, prevSignatures...) + sigs = append(sigs, sig) + } + if err := txBuilder.SetSignatures(sigs...); err != nil { + return err + } + + if err := checkMultipleSigners(txWrap); err != nil { + return err + } + + bytesToSign, err := f.GetSignBytesAdapter(ctx, signerData, txBuilder) + if err != nil { + return err + } + + // Sign those bytes + sigBytes, err := f.keybase.Sign(name, bytesToSign, signMode) + if err != nil { + return err + } + + // Construct the SignatureV2 struct + sigData = offchain.SingleSignatureData{ + SignMode: signMode, + Signature: sigBytes, + } + sig = offchain.OffchainSignature{ + PubKey: pubKey, + Data: &sigData, + Sequence: f.txParams.sequence, + } + + if overwriteSig { + err = txBuilder.SetSignatures(sig) + } else { + prevSignatures = append(prevSignatures, sig) + err = 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 f.PreprocessTx(name, txBuilder) +} + +// GetSignBytesAdapter returns the sign bytes for a given transaction and sign mode. +func (f Factory) GetSignBytesAdapter(ctx context.Context, signerData offchain.SignerData, builder TxBuilder) ([]byte, error) { + var pubKey *anypb.Any + if signerData.PubKey != nil { + anyPk, err := codectypes.NewAnyWithValue(signerData.PubKey) + if err != nil { + return nil, err + } + + pubKey = &anypb.Any{ + TypeUrl: anyPk.TypeUrl, + Value: anyPk.Value, + } + } + + txSignerData := signing.SignerData{ + ChainID: signerData.ChainID, + AccountNumber: signerData.AccountNumber, + Sequence: signerData.Sequence, + Address: signerData.Address, + PubKey: pubKey, + } + // Generate the bytes to be signed. + return f.txConfig.SignModeHandler().GetSignBytes(ctx, f.SignMode(), txSignerData, builder.GetSigningTxData()) +} + +func ValidateMemo(memo string) error { + // Prevent simple inclusion of a valid mnemonic in the memo field + if memo != "" && bip39.IsMnemonicValid(strings.ToLower(memo)) { + return errors.New("cannot provide a valid mnemonic seed in the memo field") + } + + return nil +} + // WithAccountRetriever returns a copy of the Factory with an updated AccountRetriever. func (f Factory) WithAccountRetriever(ar client.AccountRetriever) Factory { f.accountRetriever = ar @@ -369,7 +515,7 @@ func (f Factory) WithSimulateAndExecute(sim bool) Factory { } // WithSignMode returns a copy of the Factory with an updated sign mode value. -func (f Factory) WithSignMode(mode signing.SignMode) Factory { +func (f Factory) WithSignMode(mode apitxsigning.SignMode) Factory { f.txParams.signMode = mode return f } @@ -433,7 +579,7 @@ func (f Factory) AccountRetriever() client.AccountRetriever { return f.accountRe func (f Factory) TimeoutHeight() uint64 { return f.txParams.timeoutHeight } func (f Factory) FromName() string { return f.txParams.fromName } func (f Factory) SimulateAndExecute() bool { return f.txParams.simulateAndExecute } -func (f Factory) SignMode() signing.SignMode { return f.txParams.signMode } +func (f Factory) SignMode() apitxsigning.SignMode { return f.txParams.signMode } // getSimPK gets the public key to use for building a simulation tx. // Note, we should only check for keys in the keybase if we are in simulate and execute mode, @@ -443,7 +589,7 @@ func (f Factory) SignMode() signing.SignMode { return f.txParams. func (f Factory) getSimPK() (cryptotypes.PubKey, error) { var ( err error - pk cryptotypes.PubKey = &secp256k1.PubKey{} // use default public key type + pk cryptotypes.PubKey = cryptotypes.EmptyPubKey{} ) if f.txParams.simulateAndExecute && f.keybase != nil { @@ -458,133 +604,20 @@ func (f Factory) getSimPK() (cryptotypes.PubKey, error) { // getSimSignatureData based on the pubKey type gets the correct SignatureData type // to use for building a simulation tx. -func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) signing.SignatureData { +func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) offchain.SignatureData { multisigPubKey, ok := pk.(*multisig.LegacyAminoPubKey) // TODO: abstract out multisig pubkey if !ok { - return &signing.SingleSignatureData{SignMode: f.txParams.signMode} + return &offchain.SingleSignatureData{SignMode: f.txParams.signMode} } - multiSignatureData := make([]signing.SignatureData, 0, multisigPubKey.Threshold) + multiSignatureData := make([]offchain.SignatureData, 0, multisigPubKey.Threshold) for i := uint32(0); i < multisigPubKey.Threshold; i++ { - multiSignatureData = append(multiSignatureData, &signing.SingleSignatureData{ + multiSignatureData = append(multiSignatureData, &offchain.SingleSignatureData{ SignMode: f.SignMode(), }) } - return &signing.MultiSignatureData{ + return &offchain.MultiSignatureData{ Signatures: multiSignatureData, } } - -// Sign signs a given tx with a named key. The bytes signed over are canconical. -// The resulting signature will be added to the transaction builder overwriting the previous -// ones if overwrite=true (otherwise, the signature will be appended). -// Signing a transaction with mutltiple signers in the DIRECT mode is not supported and will -// return an error. -// An error is returned upon failure. -func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, overwriteSig bool) error { - if f.keybase == nil { - return errors.New("keybase must be set prior to signing a transaction") - } - - var err error - signMode := f.txParams.signMode - if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { - // use the SignModeHandler's default mode if unspecified - signMode, err = authsigning.APISignModeToInternal(f.txConfig.SignModeHandler().DefaultMode()) - if err != nil { - return err - } - } - - pubKey, err := f.keybase.GetPubKey(name) - if err != nil { - return err - } - - signerData := authsigning.SignerData{ - ChainID: f.txParams.chainID, - AccountNumber: f.txParams.accountNumber, - Sequence: f.txParams.sequence, - PubKey: pubKey, - Address: sdk.AccAddress(pubKey.Address()).String(), - } - - // For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on - // TxBuilder under the hood, and SignerInfos is needed to generated the - // sign bytes. This is the reason for setting SetSignatures here, with a - // nil signature. - // - // Note: this line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it - // also doesn't affect its generated sign bytes, so for code's simplicity - // sake, we put it here. - sigData := signing.SingleSignatureData{ - SignMode: signMode, - Signature: nil, - } - sig := signing.SignatureV2{ - PubKey: pubKey, - Data: &sigData, - Sequence: f.txParams.sequence, - } - - var prevSignatures []signing.SignatureV2 - if !overwriteSig { - prevSignatures, err = txBuilder.GetTx().GetSignaturesV2() - if err != nil { - return err - } - } - // Overwrite or append signer infos. - var sigs []signing.SignatureV2 - if overwriteSig { - sigs = []signing.SignatureV2{sig} - } else { - sigs = append(sigs, prevSignatures...) - sigs = append(sigs, sig) - } - if err := txBuilder.SetSignatures(sigs...); err != nil { - return err - } - - if err := checkMultipleSigners(txBuilder.GetTx()); err != nil { - return err - } - - bytesToSign, err := authsigning.GetSignBytesAdapter(ctx, f.txConfig.SignModeHandler(), signMode, signerData, txBuilder.GetTx()) - if err != nil { - return err - } - - // Sign those bytes - sigBytes, err := f.keybase.Sign(name, bytesToSign, signMode) - if err != nil { - return err - } - - // Construct the SignatureV2 struct - sigData = signing.SingleSignatureData{ - SignMode: signMode, - Signature: sigBytes, - } - sig = signing.SignatureV2{ - PubKey: pubKey, - Data: &sigData, - Sequence: f.txParams.sequence, - } - - if overwriteSig { - err = txBuilder.SetSignatures(sig) - } else { - prevSignatures = append(prevSignatures, sig) - err = 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 f.PreprocessTx(name, txBuilder) -} diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index 4ef6ba74b2c2..00db84d04e8e 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -3,12 +3,13 @@ package tx import ( "bufio" "context" + apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/client/v2/offchain" "errors" "fmt" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/input" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/types/tx/signing" gogogrpc "github.com/cosmos/gogoproto/grpc" sdk "github.com/cosmos/cosmos-sdk/types" @@ -66,7 +67,7 @@ func GenerateOrBroadcastTxWithFactory(clientCtx client.Context, txf Factory, msg // 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.MsgV2) error { +func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { txf, err := txf.Prepare(clientCtx) if err != nil { return err @@ -122,7 +123,7 @@ func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.MsgV2) error } } - if err = Sign(clientCtx.CmdContext, txf, clientCtx.FromName, tx, true); err != nil { + if err = txf.Sign(clientCtx.CmdContext, clientCtx.FromName, tx, true); err != nil { return err } @@ -219,9 +220,9 @@ func makeAuxSignerData(clientCtx client.Context, f Factory, msgs ...sdk.Msg) (tx // checkMultipleSigners checks that there can be maximum one DIRECT signer in // a tx. -func checkMultipleSigners(tx sdk.Tx) error { +func checkMultipleSigners(tx TxWrapper) error { directSigners := 0 - sigsV2, err := tx.GetSignaturesV2() + sigsV2, err := tx.GetSignatures() if err != nil { return err } @@ -236,15 +237,15 @@ func checkMultipleSigners(tx sdk.Tx) error { } // countDirectSigners counts the number of DIRECT signers in a signature data. -func countDirectSigners(data signing.SignatureData) int { - switch data := data.(type) { - case *signing.SingleSignatureData: - if data.SignMode == signing.SignMode_SIGN_MODE_DIRECT { +func countDirectSigners(sigData offchain.SignatureData) int { + switch data := sigData.(type) { + case *offchain.SingleSignatureData: + if data.SignMode == apitxsigning.SignMode_SIGN_MODE_DIRECT { return 1 } return 0 - case *signing.MultiSignatureData: + case *offchain.MultiSignatureData: directSigners := 0 for _, d := range data.Signatures { directSigners += countDirectSigners(d) diff --git a/client/v2/tx/types.go b/client/v2/tx/types.go index 2cdee1fd222f..6f3bbb081c5f 100644 --- a/client/v2/tx/types.go +++ b/client/v2/tx/types.go @@ -1,6 +1,11 @@ package tx -import "fmt" +import ( + "cosmossdk.io/client/v2/offchain" + "fmt" + typestx "github.com/cosmos/cosmos-sdk/types/tx" + protov2 "google.golang.org/protobuf/proto" +) // GasEstimateResponse defines a response definition for tx gas estimation. type GasEstimateResponse struct { @@ -10,3 +15,17 @@ type GasEstimateResponse struct { func (gr GasEstimateResponse) String() string { return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) } + +type TxWrapper struct { + Tx *typestx.Tx +} + +func (tx TxWrapper) GetMsgs() ([]protov2.Message, error) { + //TODO implement me + panic("implement me") +} + +func (tx TxWrapper) GetSignatures() ([]offchain.OffchainSignature, error) { + //TODO implement me + panic("implement me") +} diff --git a/codec/types/util.go b/codec/types/util.go index f8a0f9bed969..b29fb33b5034 100644 --- a/codec/types/util.go +++ b/codec/types/util.go @@ -1,18 +1,15 @@ package types import ( - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" + protov2 "google.golang.org/protobuf/proto" ) // MsgTypeURL returns the TypeURL of a `sdk.Msg`. -func MsgTypeURL[T sdk.ProtoMessage](v T) string { - switch msg := any(v).(type) { - case sdk.Msg: - return "/" + proto.MessageName(msg) - case sdk.MsgV2: - return "/" + string(msg.ProtoReflect().Descriptor().FullName()) - default: - return "" +func MsgTypeURL(msg proto.Message) string { + if m, ok := msg.(protov2.Message); ok { + return "/" + string(m.ProtoReflect().Descriptor().FullName()) } + + return "/" + proto.MessageName(msg) } diff --git a/crypto/types/types.go b/crypto/types/types.go index 12fc6e39541f..a906eb7a1785 100644 --- a/crypto/types/types.go +++ b/crypto/types/types.go @@ -52,3 +52,21 @@ type PrivKey interface { type ( Address = cmtcrypto.Address ) + +type EmptyPubKey struct{} + +func (e EmptyPubKey) Reset() {} + +func (e EmptyPubKey) String() string { return "" } + +func (e EmptyPubKey) ProtoMessage() {} + +func (e EmptyPubKey) VerifySignature(msg, sig []byte) bool { return false } + +func (e EmptyPubKey) Equals(PubKey) bool { return false } + +func (e EmptyPubKey) Address() Address { return nil } + +func (e EmptyPubKey) Bytes() []byte { return nil } + +func (e EmptyPubKey) Type() string { return "EmptyPubKey" } From b8bfa8d352391c11314c9da1e9ed63e56a6c305a Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Mon, 15 Apr 2024 10:14:44 -0300 Subject: [PATCH 09/14] Add TxApiCodec --- client/v2/tx/config.go | 8 ++++---- types/tx_msg.go | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index bb99840aee01..7473a5d8ca49 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -20,10 +20,10 @@ type TxConfig interface { // TxEncodingConfig defines an interface that contains transaction // encoders and decoders type TxEncodingConfig interface { - TxEncoder() sdk.TxEncoder - TxDecoder() sdk.TxDecoder - TxJSONEncoder() sdk.TxEncoder - TxJSONDecoder() sdk.TxDecoder + TxEncoder() sdk.TxApiEncoder + TxDecoder() sdk.TxApiDecoder + TxJSONEncoder() sdk.TxApiEncoder + TxJSONDecoder() sdk.TxApiDecoder } type TxSigningConfig interface { diff --git a/types/tx_msg.go b/types/tx_msg.go index de8d7233b925..c3c91529fda6 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -3,6 +3,7 @@ package types import ( "encoding/json" fmt "fmt" + typestx "github.com/cosmos/cosmos-sdk/types/tx" strings "strings" "github.com/cosmos/gogoproto/proto" @@ -105,6 +106,12 @@ type TxDecoder func(txBytes []byte) (Tx, error) // TxEncoder marshals transaction to bytes type TxEncoder func(tx Tx) ([]byte, error) +// TxApiDecoder unmarshals transaction bytes into API Tx type +type TxApiDecoder func(txBytes []byte) (typestx.Tx, error) + +// TxApiEncoder marshals transaction to bytes +type TxApiEncoder func(tx typestx.Tx) ([]byte, error) + // MsgTypeURL returns the TypeURL of a `sdk.Msg`. var MsgTypeURL = codectypes.MsgTypeURL From a92653918b05eaaf6e372d062d627e88216bbc64 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Mon, 15 Apr 2024 10:15:10 -0300 Subject: [PATCH 10/14] Remove multisig import from factory --- client/v2/tx/factory.go | 4 ++-- crypto/types/types.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index d13435286c69..2754a87e40f0 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -13,7 +13,7 @@ import ( "errors" "fmt" "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/client/flags" @@ -605,7 +605,7 @@ func (f Factory) getSimPK() (cryptotypes.PubKey, error) { // getSimSignatureData based on the pubKey type gets the correct SignatureData type // to use for building a simulation tx. func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) offchain.SignatureData { - multisigPubKey, ok := pk.(*multisig.LegacyAminoPubKey) // TODO: abstract out multisig pubkey + multisigPubKey, ok := pk.(*cryptotypes.DummyMultiSig) if !ok { return &offchain.SingleSignatureData{SignMode: f.txParams.signMode} } diff --git a/crypto/types/types.go b/crypto/types/types.go index a906eb7a1785..85cf2f269cc3 100644 --- a/crypto/types/types.go +++ b/crypto/types/types.go @@ -2,6 +2,7 @@ package types import ( cmtcrypto "github.com/cometbft/cometbft/crypto" + "github.com/cosmos/cosmos-sdk/codec/types" proto "github.com/cosmos/gogoproto/proto" ) @@ -70,3 +71,24 @@ func (e EmptyPubKey) Address() Address { return nil } func (e EmptyPubKey) Bytes() []byte { return nil } func (e EmptyPubKey) Type() string { return "EmptyPubKey" } + +type DummyMultiSig struct { + Threshold uint32 + PubKeys []*types.Any +} + +func (e DummyMultiSig) Reset() {} + +func (e DummyMultiSig) String() string { return "" } + +func (e DummyMultiSig) ProtoMessage() {} + +func (e DummyMultiSig) VerifySignature(msg, sig []byte) bool { return false } + +func (e DummyMultiSig) Equals(PubKey) bool { return false } + +func (e DummyMultiSig) Address() Address { return nil } + +func (e DummyMultiSig) Bytes() []byte { return nil } + +func (e DummyMultiSig) Type() string { return "DummyMultiSig" } From 903f3750dea239f2b6560412d314a48fdc422464 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Wed, 17 Apr 2024 10:15:31 -0300 Subject: [PATCH 11/14] Continue removing unwanted dependencies from client/v2 --- client/v2/offchain/builder.go | 10 +- client/v2/offchain/handler_map.go | 69 ++++++ client/v2/offchain/sign.go | 47 ++-- client/v2/offchain/sign_context.go | 377 +++++++++++++++++++++++++++++ client/v2/tx/account_retriever.go | 49 ++++ client/v2/tx/aux_builder.go | 9 +- client/v2/tx/aux_builder_test.go | 17 +- client/v2/tx/builder.go | 3 +- client/v2/tx/config.go | 6 +- client/v2/tx/context.go | 6 +- client/v2/tx/factory.go | 66 ++--- client/v2/tx/tx.go | 13 +- 12 files changed, 577 insertions(+), 95 deletions(-) create mode 100644 client/v2/offchain/handler_map.go create mode 100644 client/v2/offchain/sign_context.go create mode 100644 client/v2/tx/account_retriever.go diff --git a/client/v2/offchain/builder.go b/client/v2/offchain/builder.go index c3a8f924ed01..0a78b6341142 100644 --- a/client/v2/offchain/builder.go +++ b/client/v2/offchain/builder.go @@ -13,8 +13,6 @@ import ( basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" apitx "cosmossdk.io/api/cosmos/tx/v1beta1" - txsigning "cosmossdk.io/x/tx/signing" - "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -49,7 +47,7 @@ func (b *builder) GetTx() *apitx.Tx { } // GetSigningTxData returns the necessary data to generate sign bytes. -func (b *builder) GetSigningTxData() (txsigning.TxData, error) { +func (b *builder) GetSigningTxData() (TxData, error) { body := b.tx.Body authInfo := b.tx.AuthInfo @@ -118,13 +116,13 @@ func (b *builder) GetSigningTxData() (txsigning.TxData, error) { } authInfoBz, err := protov2.Marshal(b.tx.AuthInfo) if err != nil { - return txsigning.TxData{}, err + return TxData{}, err } bodyBz, err := protov2.Marshal(b.tx.Body) if err != nil { - return txsigning.TxData{}, err + return TxData{}, err } - txData := txsigning.TxData{ + txData := TxData{ AuthInfo: txAuthInfo, AuthInfoBytes: authInfoBz, Body: txBody, diff --git a/client/v2/offchain/handler_map.go b/client/v2/offchain/handler_map.go new file mode 100644 index 000000000000..4afda527a935 --- /dev/null +++ b/client/v2/offchain/handler_map.go @@ -0,0 +1,69 @@ +package offchain + +import ( + "context" + "fmt" + + signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" +) + +// SignModeHandler is the interface that handlers for each sign mode should implement to generate sign bytes. +type SignModeHandler interface { + // Mode is the sign mode supported by this handler + Mode() signingv1beta1.SignMode + + // GetSignBytes returns the sign bytes for the provided SignerData and TxData, or an error. + GetSignBytes(ctx context.Context, signerData SignerData, txData TxData) ([]byte, error) +} + +// HandlerMap aggregates several sign mode handlers together for convenient generation of sign bytes +// based on sign mode. +type HandlerMap struct { + signModeHandlers map[signingv1beta1.SignMode]SignModeHandler + defaultMode signingv1beta1.SignMode + modes []signingv1beta1.SignMode +} + +// NewHandlerMap constructs a new sign mode handler map. The first handler is used as the default. +func NewHandlerMap(handlers ...SignModeHandler) *HandlerMap { + if len(handlers) == 0 { + panic("no handlers") + } + res := &HandlerMap{ + signModeHandlers: map[signingv1beta1.SignMode]SignModeHandler{}, + } + + for i, handler := range handlers { + if handler == nil { + panic("nil handler") + } + mode := handler.Mode() + if i == 0 { + res.defaultMode = mode + } + res.signModeHandlers[mode] = handler + res.modes = append(res.modes, mode) + } + + return res +} + +// SupportedModes lists the modes supported by this handler map. +func (h *HandlerMap) SupportedModes() []signingv1beta1.SignMode { + return h.modes +} + +// DefaultMode returns the default mode for this handler map. +func (h *HandlerMap) DefaultMode() signingv1beta1.SignMode { + return h.defaultMode +} + +// GetSignBytes returns the sign bytes for the transaction for the requested mode. +func (h *HandlerMap) GetSignBytes(ctx context.Context, signMode signingv1beta1.SignMode, signerData SignerData, txData TxData) ([]byte, error) { + handler, ok := h.signModeHandlers[signMode] + if !ok { + return nil, fmt.Errorf("unsuppored sign mode %s", signMode) + } + + return handler.GetSignBytes(ctx, signerData, txData) +} diff --git a/client/v2/offchain/sign.go b/client/v2/offchain/sign.go index 7171140c1884..428cda3fa2a5 100644 --- a/client/v2/offchain/sign.go +++ b/client/v2/offchain/sign.go @@ -2,18 +2,13 @@ package offchain import ( "context" - - "google.golang.org/protobuf/types/known/anypb" - apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" apitx "cosmossdk.io/api/cosmos/tx/v1beta1" "cosmossdk.io/client/v2/internal/offchain" - txsigning "cosmossdk.io/x/tx/signing" + "github.com/cosmos/cosmos-sdk/client/V2/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/client" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/version" ) @@ -28,6 +23,26 @@ const ( signMode = apisigning.SignMode_SIGN_MODE_TEXTUAL ) +// TxData is the data about a transaction that is necessary to generate sign bytes. +type TxData struct { + // Body is the TxBody that will be part of the transaction. + Body *apitx.TxBody + + // AuthInfo is the AuthInfo that will be part of the transaction. + AuthInfo *apitx.AuthInfo + + // BodyBytes is the marshaled body bytes that will be part of TxRaw. + BodyBytes []byte + + // AuthInfoBytes is the marshaled AuthInfo bytes that will be part of TxRaw. + AuthInfoBytes []byte + + // BodyHasUnknownNonCriticals should be set to true if the transaction has been + // decoded and found to have unknown non-critical fields. This is only needed + // for amino JSON signing. + BodyHasUnknownNonCriticals bool +} + type SignerData struct { Address string ChainID string @@ -37,7 +52,7 @@ type SignerData struct { } // Sign signs given bytes using the specified encoder and SignMode. -func Sign(ctx client.Context, rawBytes []byte, fromName, indent, encoding, output string, emitUnpopulated bool) (string, error) { +func Sign(ctx tx.Context, rawBytes []byte, fromName, indent, encoding, output string, emitUnpopulated bool) (string, error) { encoder, err := getEncoder(encoding) if err != nil { return "", err @@ -62,7 +77,7 @@ func Sign(ctx client.Context, rawBytes []byte, fromName, indent, encoding, outpu } // sign signs a digest with provided key and SignMode. -func sign(ctx client.Context, fromName, digest string) (*apitx.Tx, error) { +func sign(ctx tx.Context, fromName, digest string) (*apitx.Tx, error) { keybase, err := keyring.NewAutoCLIKeyring(ctx.Keyring) if err != nil { return nil, err @@ -138,7 +153,7 @@ func sign(ctx client.Context, fromName, digest string) (*apitx.Tx, error) { // getSignBytes gets the bytes to be signed for the given Tx and SignMode. func getSignBytes(ctx context.Context, - handlerMap *txsigning.HandlerMap, + handlerMap *HandlerMap, signerData SignerData, tx *builder, ) ([]byte, error) { @@ -147,20 +162,12 @@ func getSignBytes(ctx context.Context, return nil, err } - anyPk, err := codectypes.NewAnyWithValue(signerData.PubKey) - if err != nil { - return nil, err - } - - txSignerData := txsigning.SignerData{ + txSignerData := SignerData{ ChainID: signerData.ChainID, AccountNumber: signerData.AccountNumber, Sequence: signerData.Sequence, Address: signerData.Address, - PubKey: &anypb.Any{ - TypeUrl: anyPk.TypeUrl, - Value: anyPk.Value, - }, + PubKey: signerData.PubKey, } return handlerMap.GetSignBytes(ctx, signMode, txSignerData, txData) diff --git a/client/v2/offchain/sign_context.go b/client/v2/offchain/sign_context.go new file mode 100644 index 000000000000..b43d70d3be61 --- /dev/null +++ b/client/v2/offchain/sign_context.go @@ -0,0 +1,377 @@ +package offchain + +import ( + "errors" + "fmt" + + cosmos_proto "github.com/cosmos/cosmos-proto" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + + msgv1 "cosmossdk.io/api/cosmos/msg/v1" + "cosmossdk.io/core/address" +) + +type TypeResolver interface { + protoregistry.MessageTypeResolver + protoregistry.ExtensionTypeResolver +} + +// SignContext is a context for retrieving the list of signers from a +// message where signers are specified by the cosmos.msg.v1.signer protobuf +// option. It also contains the ProtoFileResolver and address.Codec's used +// for resolving message descriptors and converting addresses. +type SignContext struct { + fileResolver ProtoFileResolver + typeResolver protoregistry.MessageTypeResolver + addressCodec address.Codec + validatorAddressCodec address.Codec + getSignersFuncs map[protoreflect.FullName]GetSignersFunc + customGetSignerFuncs map[protoreflect.FullName]GetSignersFunc + maxRecursionDepth int +} + +// Options are options for creating Context which will be used for signing operations. +type Options struct { + // FileResolver is the protobuf file resolver to use for resolving message descriptors. + // If it is nil, the global protobuf registry will be used. + FileResolver ProtoFileResolver + + // TypeResolver is the protobuf type resolver to use for resolving message types. + TypeResolver TypeResolver + + // AddressCodec is the codec for converting addresses between strings and bytes. + AddressCodec address.Codec + + // ValidatorAddressCodec is the codec for converting validator addresses between strings and bytes. + ValidatorAddressCodec address.Codec + + // CustomGetSigners is a map of message types to custom GetSignersFuncs. + CustomGetSigners map[protoreflect.FullName]GetSignersFunc + + // MaxRecursionDepth is the maximum depth of nested messages that will be traversed + MaxRecursionDepth int +} + +// DefineCustomGetSigners defines a custom GetSigners function for a given +// message type. +// +// NOTE: if a custom signers function is defined, the message type used to +// define this function MUST be the concrete type passed to GetSigners, +// otherwise a runtime type error will occur. +func (o *Options) DefineCustomGetSigners(typeName protoreflect.FullName, f GetSignersFunc) { + if o.CustomGetSigners == nil { + o.CustomGetSigners = map[protoreflect.FullName]GetSignersFunc{} + } + o.CustomGetSigners[typeName] = f +} + +// ProtoFileResolver is a protodesc.Resolver that also allows iterating over all +// files descriptors. It is a subset of the methods supported by protoregistry.Files. +type ProtoFileResolver interface { + protodesc.Resolver + RangeFiles(func(protoreflect.FileDescriptor) bool) +} + +// NewContext creates a new Context using the provided options. +func NewContext(options Options) (*SignContext, error) { + protoFiles := options.FileResolver + if protoFiles == nil { + protoFiles = protoregistry.GlobalFiles + } + + protoTypes := options.TypeResolver + if protoTypes == nil { + protoTypes = protoregistry.GlobalTypes + } + + if options.AddressCodec == nil { + return nil, errors.New("address codec is required") + } + + if options.ValidatorAddressCodec == nil { + return nil, errors.New("validator address codec is required") + } + + if options.MaxRecursionDepth <= 0 { + options.MaxRecursionDepth = 32 + } + + customGetSignerFuncs := map[protoreflect.FullName]GetSignersFunc{} + for k := range options.CustomGetSigners { + customGetSignerFuncs[k] = options.CustomGetSigners[k] + } + + c := &SignContext{ + fileResolver: protoFiles, + typeResolver: protoTypes, + addressCodec: options.AddressCodec, + validatorAddressCodec: options.ValidatorAddressCodec, + getSignersFuncs: map[protoreflect.FullName]GetSignersFunc{}, + customGetSignerFuncs: customGetSignerFuncs, + maxRecursionDepth: options.MaxRecursionDepth, + } + + return c, nil +} + +// GetSignersFunc returns the signers for a given message. +type GetSignersFunc func(proto.Message) ([][]byte, error) + +// CustomGetSigner is a custom GetSignersFunc that is defined for a specific message type. +type CustomGetSigner struct { + MsgType protoreflect.FullName + Fn GetSignersFunc +} + +func (c CustomGetSigner) IsManyPerContainerType() {} + +func getSignersFieldNames(descriptor protoreflect.MessageDescriptor) ([]string, error) { + signersFields := proto.GetExtension(descriptor.Options(), msgv1.E_Signer).([]string) + if len(signersFields) == 0 { + return nil, fmt.Errorf("no cosmos.msg.v1.signer option found for message %s; use DefineCustomGetSigners to specify a custom getter", descriptor.FullName()) + } + + return signersFields, nil +} + +// Validate performs a dry run of getting all msg's signers. This has 2 benefits: +// - it will error if any Msg has forgotten the "cosmos.msg.v1.signer" +// annotation +// - it will pre-populate the context's internal cache for getSignersFuncs +// so that calling it in antehandlers will be faster. +func (c *SignContext) Validate() error { + var errs []error + c.fileResolver.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + for i := 0; i < fd.Services().Len(); i++ { + sd := fd.Services().Get(i) + + // Skip services that are not annotated with the "cosmos.msg.v1.service" option. + if ext := proto.GetExtension(sd.Options(), msgv1.E_Service); ext == nil || !ext.(bool) { + continue + } + + for j := 0; j < sd.Methods().Len(); j++ { + md := sd.Methods().Get(j).Input() + _, hasCustomSigner := c.customGetSignerFuncs[md.FullName()] + if _, err := getSignersFieldNames(md); err == nil && hasCustomSigner { + errs = append(errs, fmt.Errorf("a custom signer function as been defined for message %s which already has a signer field defined with (cosmos.msg.v1.signer)", md.FullName())) + continue + } + _, err := c.getGetSignersFn(md) + if err != nil { + errs = append(errs, err) + } + } + } + + return true + }) + + return errors.Join(errs...) +} + +func (c *SignContext) makeGetSignersFunc(descriptor protoreflect.MessageDescriptor) (GetSignersFunc, error) { + signersFields, err := getSignersFieldNames(descriptor) + if err != nil { + return nil, err + } + + fieldGetters := make([]func(proto.Message, [][]byte) ([][]byte, error), len(signersFields)) + for i, fieldName := range signersFields { + field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) + if field == nil { + return nil, fmt.Errorf("field %s not found in message %s", fieldName, descriptor.FullName()) + } + + if field.IsMap() || field.HasOptionalKeyword() { + return nil, fmt.Errorf("cosmos.msg.v1.signer field %s in message %s must not be a map or optional", fieldName, descriptor.FullName()) + } + + switch field.Kind() { + case protoreflect.StringKind: + addrCdc := c.getAddressCodec(field) + if field.IsList() { + fieldGetters[i] = func(msg proto.Message, arr [][]byte) ([][]byte, error) { + signers := msg.ProtoReflect().Get(field).List() + n := signers.Len() + for i := 0; i < n; i++ { + addrStr := signers.Get(i).String() + addrBz, err := addrCdc.StringToBytes(addrStr) + if err != nil { + return nil, err + } + arr = append(arr, addrBz) + } + return arr, nil + } + } else { + fieldGetters[i] = func(msg proto.Message, arr [][]byte) ([][]byte, error) { + addrStr := msg.ProtoReflect().Get(field).String() + addrBz, err := addrCdc.StringToBytes(addrStr) + if err != nil { + return nil, err + } + return append(arr, addrBz), nil + } + } + case protoreflect.MessageKind: + var fieldGetter func(protoreflect.Message, int) ([][]byte, error) + fieldGetter = func(msg protoreflect.Message, depth int) ([][]byte, error) { + if depth > c.maxRecursionDepth { + return nil, errors.New("maximum recursion depth exceeded") + } + desc := msg.Descriptor() + signerFields, err := getSignersFieldNames(desc) + if err != nil { + return nil, err + } + if len(signerFields) != 1 { + return nil, fmt.Errorf("nested cosmos.msg.v1.signer option in message %s must contain only one value", desc.FullName()) + } + signerFieldName := signerFields[0] + childField := desc.Fields().ByName(protoreflect.Name(signerFieldName)) + switch { + case childField.Kind() == protoreflect.MessageKind: + if childField.IsList() { + childMsgs := msg.Get(childField).List() + var arr [][]byte + for i := 0; i < childMsgs.Len(); i++ { + res, err := fieldGetter(childMsgs.Get(i).Message(), depth+1) + if err != nil { + return nil, err + } + arr = append(arr, res...) + } + return arr, nil + } else { + return fieldGetter(msg.Get(childField).Message(), depth+1) + } + case childField.IsMap() || childField.HasOptionalKeyword(): + return nil, fmt.Errorf("cosmos.msg.v1.signer field %s in message %s must not be a map or optional", signerFieldName, desc.FullName()) + case childField.Kind() == protoreflect.StringKind: + addrCdc := c.getAddressCodec(childField) + if childField.IsList() { + childMsgs := msg.Get(childField).List() + n := childMsgs.Len() + var res [][]byte + for i := 0; i < n; i++ { + addrStr := childMsgs.Get(i).String() + addrBz, err := addrCdc.StringToBytes(addrStr) + if err != nil { + return nil, err + } + res = append(res, addrBz) + } + return res, nil + } else { + addrStr := msg.Get(childField).String() + addrBz, err := addrCdc.StringToBytes(addrStr) + if err != nil { + return nil, err + } + return [][]byte{addrBz}, nil + } + } + return nil, fmt.Errorf("unexpected field type %s for field %s in message %s, only string and message type are supported", + childField.Kind(), signerFieldName, desc.FullName()) + } + + fieldGetters[i] = func(msg proto.Message, arr [][]byte) ([][]byte, error) { + if field.IsList() { + signers := msg.ProtoReflect().Get(field).List() + n := signers.Len() + for i := 0; i < n; i++ { + res, err := fieldGetter(signers.Get(i).Message(), 0) + if err != nil { + return nil, err + } + arr = append(arr, res...) + } + } else { + res, err := fieldGetter(msg.ProtoReflect().Get(field).Message(), 0) + if err != nil { + return nil, err + } + arr = append(arr, res...) + } + return arr, nil + } + default: + return nil, fmt.Errorf("unexpected field type %s for field %s in message %s", field.Kind(), fieldName, descriptor.FullName()) + } + } + + return func(message proto.Message) ([][]byte, error) { + var signers [][]byte + for _, getter := range fieldGetters { + signers, err = getter(message, signers) + if err != nil { + return nil, err + } + } + return signers, nil + }, nil +} + +func (c *SignContext) getAddressCodec(field protoreflect.FieldDescriptor) address.Codec { + scalarOpt := proto.GetExtension(field.Options(), cosmos_proto.E_Scalar) + addrCdc := c.addressCodec + if scalarOpt != nil { + if scalarOpt.(string) == "cosmos.ValidatorAddressString" { + addrCdc = c.validatorAddressCodec + } + } + + return addrCdc +} + +func (c *SignContext) getGetSignersFn(messageDescriptor protoreflect.MessageDescriptor) (GetSignersFunc, error) { + f, ok := c.customGetSignerFuncs[messageDescriptor.FullName()] + if ok { + return f, nil + } + f, ok = c.getSignersFuncs[messageDescriptor.FullName()] + if !ok { + var err error + f, err = c.makeGetSignersFunc(messageDescriptor) + if err != nil { + return nil, err + } + c.getSignersFuncs[messageDescriptor.FullName()] = f + } + + return f, nil +} + +// GetSigners returns the signers for a given message. +func (c *SignContext) GetSigners(msg proto.Message) ([][]byte, error) { + f, err := c.getGetSignersFn(msg.ProtoReflect().Descriptor()) + if err != nil { + return nil, err + } + + return f(msg) +} + +// AddressCodec returns the address codec used by the context. +func (c *SignContext) AddressCodec() address.Codec { + return c.addressCodec +} + +// ValidatorAddressCodec returns the validator address codec used by the context. +func (c *SignContext) ValidatorAddressCodec() address.Codec { + return c.validatorAddressCodec +} + +// FileResolver returns the protobuf file resolver used by the context. +func (c *SignContext) FileResolver() ProtoFileResolver { + return c.fileResolver +} + +// TypeResolver returns the protobuf type resolver used by the context. +func (c *SignContext) TypeResolver() protoregistry.MessageTypeResolver { + return c.typeResolver +} diff --git a/client/v2/tx/account_retriever.go b/client/v2/tx/account_retriever.go new file mode 100644 index 000000000000..4c7aa31e42a4 --- /dev/null +++ b/client/v2/tx/account_retriever.go @@ -0,0 +1,49 @@ +package tx + +import ( + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Account defines a read-only version of the auth module's AccountI. +type Account interface { + GetAddress() sdk.AccAddress + GetPubKey() cryptotypes.PubKey // can return nil. + GetAccountNumber() uint64 + GetSequence() uint64 +} + +// AccountRetriever defines the interfaces required by transactions to +// ensure an account exists and to be able to query for account fields necessary +// for signing. +type AccountRetriever interface { + GetAccount(clientCtx Context, addr sdk.AccAddress) (Account, error) + GetAccountWithHeight(clientCtx Context, addr sdk.AccAddress) (Account, int64, error) + EnsureExists(clientCtx Context, addr sdk.AccAddress) error + GetAccountNumberSequence(clientCtx Context, addr sdk.AccAddress) (accNum, accSeq uint64, err error) +} + +var _ AccountRetriever = (*MockAccountRetriever)(nil) + +// MockAccountRetriever defines a no-op basic AccountRetriever that can be used +// in mocked contexts. Tests or context that need more sophisticated testing +// state should implement their own mock AccountRetriever. +type MockAccountRetriever struct { + ReturnAccNum, ReturnAccSeq uint64 +} + +func (mar MockAccountRetriever) GetAccount(_ Context, _ sdk.AccAddress) (Account, error) { + return nil, nil +} + +func (mar MockAccountRetriever) GetAccountWithHeight(_ Context, _ sdk.AccAddress) (Account, int64, error) { + return nil, 0, nil +} + +func (mar MockAccountRetriever) EnsureExists(_ Context, _ sdk.AccAddress) error { + return nil +} + +func (mar MockAccountRetriever) GetAccountNumberSequence(_ Context, _ sdk.AccAddress) (uint64, uint64, error) { + return mar.ReturnAccNum, mar.ReturnAccSeq, nil +} diff --git a/client/v2/tx/aux_builder.go b/client/v2/tx/aux_builder.go index cded148bfbc0..951f1e7f6c1d 100644 --- a/client/v2/tx/aux_builder.go +++ b/client/v2/tx/aux_builder.go @@ -3,12 +3,11 @@ package tx import ( "context" apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" - + "cosmossdk.io/client/v2/offchain" "github.com/cosmos/gogoproto/proto" "google.golang.org/protobuf/types/known/anypb" apitx "cosmossdk.io/api/cosmos/tx/v1beta1" - txsigning "cosmossdk.io/x/tx/signing" "cosmossdk.io/x/tx/signing/aminojson" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -126,7 +125,7 @@ func (b *AuxTxBuilder) SetSignMode(mode apitxsigning.SignMode) error { signing.SignMode_SIGN_MODE_DIRECT_AUX, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) } - b.auxSignerData.Mode = mode + b.auxSignerData.Mode = signing.SignMode(mode) return nil } @@ -223,14 +222,14 @@ func (b *AuxTxBuilder) GetSignBytes() ([]byte, error) { signBz, err = handler.GetSignBytes( context.Background(), - txsigning.SignerData{ + offchain.SignerData{ Address: b.auxSignerData.Address, ChainID: b.auxSignerData.SignDoc.ChainId, AccountNumber: b.auxSignerData.SignDoc.AccountNumber, Sequence: b.auxSignerData.SignDoc.Sequence, PubKey: nil, }, - txsigning.TxData{ + offchain.TxData{ Body: auxBody, AuthInfo: &apitx.AuthInfo{ SignerInfos: nil, diff --git a/client/v2/tx/aux_builder_test.go b/client/v2/tx/aux_builder_test.go index b0c7e3e874eb..2118932a3ca4 100644 --- a/client/v2/tx/aux_builder_test.go +++ b/client/v2/tx/aux_builder_test.go @@ -1,11 +1,12 @@ package tx_test import ( + apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/client/v2/tx" "testing" "github.com/stretchr/testify/require" - "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/testutil" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -52,7 +53,7 @@ func TestAuxTxBuilder(t *testing.T) { { "cannot set SIGN_MODE_DIRECT", func() error { - return b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT) + return b.SetSignMode(apisigning.SignMode_SIGN_MODE_DIRECT) }, true, "AuxTxBuilder can only sign with SIGN_MODE_DIRECT_AUX or SIGN_MODE_LEGACY_AMINO_JSON", }, @@ -104,7 +105,7 @@ func TestAuxTxBuilder(t *testing.T) { func() error { require.NoError(t, b.SetMsgs(msg1)) require.NoError(t, b.SetPubKey(pub1)) - require.NoError(t, b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX)) + require.NoError(t, b.SetSignMode(apisigning.SignMode_SIGN_MODE_DIRECT_AUX)) _, err := b.GetSignBytes() return err @@ -116,7 +117,7 @@ func TestAuxTxBuilder(t *testing.T) { func() error { require.NoError(t, b.SetMsgs(msg1)) require.NoError(t, b.SetPubKey(pub1)) - require.NoError(t, b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX)) + require.NoError(t, b.SetSignMode(apisigning.SignMode_SIGN_MODE_DIRECT_AUX)) _, err := b.GetSignBytes() require.NoError(t, err) @@ -132,7 +133,7 @@ func TestAuxTxBuilder(t *testing.T) { require.NoError(t, b.SetMsgs(msg1)) require.NoError(t, b.SetPubKey(pub1)) b.SetAddress(addr1.String()) - require.NoError(t, b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX)) + require.NoError(t, b.SetSignMode(apisigning.SignMode_SIGN_MODE_DIRECT_AUX)) _, err := b.GetSignBytes() require.NoError(t, err) @@ -153,7 +154,7 @@ func TestAuxTxBuilder(t *testing.T) { require.NoError(t, b.SetMsgs(msg1)) require.NoError(t, b.SetPubKey(pub1)) b.SetAddress(addr1.String()) - err := b.SetSignMode(signing.SignMode_SIGN_MODE_DIRECT_AUX) + err := b.SetSignMode(apisigning.SignMode_SIGN_MODE_DIRECT_AUX) require.NoError(t, err) _, err = b.GetSignBytes() @@ -175,7 +176,7 @@ func TestAuxTxBuilder(t *testing.T) { require.NoError(t, b.SetMsgs(msg1)) require.NoError(t, b.SetPubKey(pub1)) b.SetAddress(addr1.String()) - err := b.SetSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) + err := b.SetSignMode(apisigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) require.NoError(t, err) _, err = b.GetSignBytes() @@ -194,7 +195,7 @@ func TestAuxTxBuilder(t *testing.T) { require.NoError(t, b.SetMsgs(msg1)) require.NoError(t, b.SetPubKey(pub1)) b.SetAddress(addr1.String()) - err := b.SetSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) + err := b.SetSignMode(apisigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) require.NoError(t, err) _, err = b.GetSignBytes() diff --git a/client/v2/tx/builder.go b/client/v2/tx/builder.go index e09f9c814c65..d91f7b27bf5b 100644 --- a/client/v2/tx/builder.go +++ b/client/v2/tx/builder.go @@ -2,7 +2,6 @@ package tx import ( "cosmossdk.io/client/v2/offchain" - txsigning "cosmossdk.io/x/tx/signing" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" typestx "github.com/cosmos/cosmos-sdk/types/tx" @@ -18,7 +17,7 @@ type ExtendedTxBuilder interface { // also know how to encode itself. type TxBuilder interface { GetTx() typestx.Tx - GetSigningTxData() txsigning.TxData + GetSigningTxData() offchain.TxData SetMsgs(...sdk.Msg) error SetMemo(string) diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index 7473a5d8ca49..823c6260aee5 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -2,7 +2,7 @@ package tx import ( apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" - txsigning "cosmossdk.io/x/tx/signing" + "cosmossdk.io/client/v2/offchain" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -27,8 +27,8 @@ type TxEncodingConfig interface { } type TxSigningConfig interface { - SignModeHandler() *txsigning.HandlerMap - SigningContext() *txsigning.Context + SignModeHandler() *offchain.HandlerMap + SigningContext() *offchain.SignContext MarshalSignatureJSON([]signingtypes.SignatureV2) ([]byte, error) UnmarshalSignatureJSON([]byte) ([]signingtypes.SignatureV2, error) } diff --git a/client/v2/tx/context.go b/client/v2/tx/context.go index 0a7edeb9fa07..4c807cb620cd 100644 --- a/client/v2/tx/context.go +++ b/client/v2/tx/context.go @@ -3,13 +3,13 @@ package tx import ( "bufio" "context" - "cosmossdk.io/client/v2/autocli/keyring" "cosmossdk.io/core/address" "encoding/json" "fmt" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" "github.com/spf13/viper" @@ -56,7 +56,7 @@ type Context struct { Offline bool SkipConfirm bool TxConfig TxConfig - AccountRetriever client.AccountRetriever + AccountRetriever AccountRetriever NodeURI string FeePayer string FeeGranter string @@ -263,7 +263,7 @@ func (ctx Context) WithTxConfig(generator TxConfig) Context { } // WithAccountRetriever returns the context with an updated AccountRetriever -func (ctx Context) WithAccountRetriever(retriever client.AccountRetriever) Context { +func (ctx Context) WithAccountRetriever(retriever AccountRetriever) Context { ctx.AccountRetriever = retriever return ctx } diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 2754a87e40f0..6252c3286f81 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -3,17 +3,14 @@ package tx import ( "context" apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" - "cosmossdk.io/client/v2/offchain" - "cosmossdk.io/x/tx/signing" - "google.golang.org/protobuf/types/known/anypb" - "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/client/v2/offchain" "cosmossdk.io/math" + //"cosmossdk.io/x/tx/signing" + cryptokeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" "errors" "fmt" - "github.com/cosmos/cosmos-sdk/client" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/client/flags" @@ -30,8 +27,8 @@ import ( // Factory defines a client transaction factory that facilitates generating and // signing an application-specific transaction. type Factory struct { - keybase keyring.Keyring - accountRetriever client.AccountRetriever + keybase cryptokeyring.Keyring + accountRetriever AccountRetriever txConfig TxConfig txParams TxParameters } @@ -117,7 +114,7 @@ func NewFactoryCLI(clientCtx Context, flagSet *pflag.FlagSet) (Factory, error) { // they will be queried for and set on the provided Factory. // A new Factory with the updated fields will be returned. // Note: When in offline mode, the Prepare does nothing and returns the original factory. -func (f Factory) Prepare(clientCtx client.Context) (Factory, error) { +func (f Factory) Prepare(clientCtx Context) (Factory, error) { if f.txParams.ExecutionOptions.offline { return f, nil } @@ -210,7 +207,7 @@ func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (TxBuilder, error) { // 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 { +func (f Factory) PrintUnsignedTx(clientCtx Context, msgs ...sdk.Msg) error { if f.SimulateAndExecute() { if clientCtx.Offline { return errors.New("cannot estimate gas in offline mode") @@ -397,25 +394,12 @@ func (f Factory) Sign(ctx context.Context, name string, txBuilder TxBuilder, ove // GetSignBytesAdapter returns the sign bytes for a given transaction and sign mode. func (f Factory) GetSignBytesAdapter(ctx context.Context, signerData offchain.SignerData, builder TxBuilder) ([]byte, error) { - var pubKey *anypb.Any - if signerData.PubKey != nil { - anyPk, err := codectypes.NewAnyWithValue(signerData.PubKey) - if err != nil { - return nil, err - } - - pubKey = &anypb.Any{ - TypeUrl: anyPk.TypeUrl, - Value: anyPk.Value, - } - } - - txSignerData := signing.SignerData{ + txSignerData := offchain.SignerData{ ChainID: signerData.ChainID, AccountNumber: signerData.AccountNumber, Sequence: signerData.Sequence, Address: signerData.Address, - PubKey: pubKey, + PubKey: signerData.PubKey, } // Generate the bytes to be signed. return f.txConfig.SignModeHandler().GetSignBytes(ctx, f.SignMode(), txSignerData, builder.GetSigningTxData()) @@ -431,7 +415,7 @@ func ValidateMemo(memo string) error { } // WithAccountRetriever returns a copy of the Factory with an updated AccountRetriever. -func (f Factory) WithAccountRetriever(ar client.AccountRetriever) Factory { +func (f Factory) WithAccountRetriever(ar AccountRetriever) Factory { f.accountRetriever = ar return f } @@ -563,23 +547,23 @@ func (f Factory) PreprocessTx(keyname string, builder TxBuilder) error { return fmt.Errorf("error retrieving key from keyring: %w", err) } - return f.txParams.preprocessTxHook(f.txParams.chainID, f.Keybase().GetRecordType(key), builder) + return f.txParams.preprocessTxHook(f.txParams.chainID, cryptokeyring.KeyType(f.Keybase().GetRecordType(key)), builder) } -func (f Factory) AccountNumber() uint64 { return f.txParams.accountNumber } -func (f Factory) Sequence() uint64 { return f.txParams.sequence } -func (f Factory) Gas() uint64 { return f.txParams.gas } -func (f Factory) GasAdjustment() float64 { return f.txParams.gasAdjustment } -func (f Factory) Keybase() keyring.Keyring { return f.keybase } -func (f Factory) ChainID() string { return f.txParams.chainID } -func (f Factory) Memo() string { return f.txParams.memo } -func (f Factory) Fees() sdk.Coins { return f.txParams.fees } -func (f Factory) GasPrices() sdk.DecCoins { return f.txParams.gasPrices } -func (f Factory) AccountRetriever() client.AccountRetriever { return f.accountRetriever } -func (f Factory) TimeoutHeight() uint64 { return f.txParams.timeoutHeight } -func (f Factory) FromName() string { return f.txParams.fromName } -func (f Factory) SimulateAndExecute() bool { return f.txParams.simulateAndExecute } -func (f Factory) SignMode() apitxsigning.SignMode { return f.txParams.signMode } +func (f Factory) AccountNumber() uint64 { return f.txParams.accountNumber } +func (f Factory) Sequence() uint64 { return f.txParams.sequence } +func (f Factory) Gas() uint64 { return f.txParams.gas } +func (f Factory) GasAdjustment() float64 { return f.txParams.gasAdjustment } +func (f Factory) Keybase() keyring.Keyring { return f.keybase } +func (f Factory) ChainID() string { return f.txParams.chainID } +func (f Factory) Memo() string { return f.txParams.memo } +func (f Factory) Fees() sdk.Coins { return f.txParams.fees } +func (f Factory) GasPrices() sdk.DecCoins { return f.txParams.gasPrices } +func (f Factory) AccountRetriever() AccountRetriever { return f.accountRetriever } +func (f Factory) TimeoutHeight() uint64 { return f.txParams.timeoutHeight } +func (f Factory) FromName() string { return f.txParams.fromName } +func (f Factory) SimulateAndExecute() bool { return f.txParams.simulateAndExecute } +func (f Factory) SignMode() apitxsigning.SignMode { return f.txParams.signMode } // getSimPK gets the public key to use for building a simulation tx. // Note, we should only check for keys in the keybase if we are in simulate and execute mode, diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index 00db84d04e8e..bb9ffc1fcf7b 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -7,7 +7,6 @@ import ( "cosmossdk.io/client/v2/offchain" "errors" "fmt" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/input" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" gogogrpc "github.com/cosmos/gogoproto/grpc" @@ -20,7 +19,7 @@ import ( // GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxCLI(clientCtx client.Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { +func GenerateOrBroadcastTxCLI(clientCtx Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { txf, err := NewFactoryCLI(clientCtx, flagSet) if err != nil { return err @@ -31,7 +30,7 @@ func GenerateOrBroadcastTxCLI(clientCtx client.Context, flagSet *pflag.FlagSet, // GenerateOrBroadcastTxWithFactory will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxWithFactory(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { +func GenerateOrBroadcastTxWithFactory(clientCtx Context, txf Factory, msgs ...sdk.Msg) error { // Validate all msgs before generating or broadcasting the tx. // We were calling ValidateBasic separately in each CLI handler before. // Right now, we're factorizing that call inside this function. @@ -67,7 +66,7 @@ func GenerateOrBroadcastTxWithFactory(clientCtx client.Context, txf Factory, msg // 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 { +func BroadcastTx(clientCtx Context, txf Factory, msgs ...sdk.Msg) error { txf, err := txf.Prepare(clientCtx) if err != nil { return err @@ -144,7 +143,7 @@ func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error { // 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.MsgV2, + clientCtx gogogrpc.ClientConn, txf Factory, msgs ...sdk.Msg, ) (*tx.SimulateResponse, uint64, error) { txBytes, err := txf.BuildSimTx(msgs...) if err != nil { @@ -163,9 +162,9 @@ func CalculateGas( } // makeAuxSignerData generates an AuxSignerData from the client inputs. -func makeAuxSignerData(clientCtx client.Context, f Factory, msgs ...sdk.Msg) (tx.AuxSignerData, error) { +func makeAuxSignerData(clientCtx Context, f Factory, msgs ...sdk.Msg) (tx.AuxSignerData, error) { b := NewAuxTxBuilder() - fromAddress, name, _, err := client.GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) + fromAddress, name, _, err := GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) if err != nil { return tx.AuxSignerData{}, err } From 9aa7ec7fd27c405f574e17690c74d27efb4fa515 Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Wed, 17 Apr 2024 10:22:52 -0300 Subject: [PATCH 12/14] Move new context struct to internal package --- client/v2/{ => internal}/tx/context.go | 11 ++++++----- client/v2/tx/account_retriever.go | 17 +++++++++-------- client/v2/tx/config.go | 3 ++- client/v2/tx/factory.go | 9 +++++---- client/v2/tx/tx.go | 11 ++++++----- 5 files changed, 28 insertions(+), 23 deletions(-) rename client/v2/{ => internal}/tx/context.go (98%) diff --git a/client/v2/tx/context.go b/client/v2/internal/tx/context.go similarity index 98% rename from client/v2/tx/context.go rename to client/v2/internal/tx/context.go index 4c807cb620cd..2a89b523cdcf 100644 --- a/client/v2/tx/context.go +++ b/client/v2/internal/tx/context.go @@ -3,6 +3,7 @@ package tx import ( "bufio" "context" + "cosmossdk.io/client/v2/tx" "cosmossdk.io/core/address" "encoding/json" "fmt" @@ -22,7 +23,7 @@ import ( ) // PreprocessTxFn defines a hook by which chains can preprocess transactions before broadcasting -type PreprocessTxFn func(chainID string, key keyring.KeyType, tx TxBuilder) error +type PreprocessTxFn func(chainID string, key keyring.KeyType, tx tx.TxBuilder) error // Context implements a typical context created in SDK modules for transaction // handling and queries. @@ -55,8 +56,8 @@ type Context struct { GenerateOnly bool Offline bool SkipConfirm bool - TxConfig TxConfig - AccountRetriever AccountRetriever + TxConfig tx.TxConfig + AccountRetriever tx.AccountRetriever NodeURI string FeePayer string FeeGranter string @@ -257,13 +258,13 @@ func (ctx Context) WithSkipConfirmation(skip bool) Context { } // WithTxConfig returns the context with an updated TxConfig -func (ctx Context) WithTxConfig(generator TxConfig) Context { +func (ctx Context) WithTxConfig(generator tx.TxConfig) Context { ctx.TxConfig = generator return ctx } // WithAccountRetriever returns the context with an updated AccountRetriever -func (ctx Context) WithAccountRetriever(retriever AccountRetriever) Context { +func (ctx Context) WithAccountRetriever(retriever tx.AccountRetriever) Context { ctx.AccountRetriever = retriever return ctx } diff --git a/client/v2/tx/account_retriever.go b/client/v2/tx/account_retriever.go index 4c7aa31e42a4..e9db5ba20135 100644 --- a/client/v2/tx/account_retriever.go +++ b/client/v2/tx/account_retriever.go @@ -1,6 +1,7 @@ package tx import ( + "cosmossdk.io/client/v2/internal/tx" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -17,10 +18,10 @@ type Account interface { // ensure an account exists and to be able to query for account fields necessary // for signing. type AccountRetriever interface { - GetAccount(clientCtx Context, addr sdk.AccAddress) (Account, error) - GetAccountWithHeight(clientCtx Context, addr sdk.AccAddress) (Account, int64, error) - EnsureExists(clientCtx Context, addr sdk.AccAddress) error - GetAccountNumberSequence(clientCtx Context, addr sdk.AccAddress) (accNum, accSeq uint64, err error) + GetAccount(clientCtx tx.Context, addr sdk.AccAddress) (Account, error) + GetAccountWithHeight(clientCtx tx.Context, addr sdk.AccAddress) (Account, int64, error) + EnsureExists(clientCtx tx.Context, addr sdk.AccAddress) error + GetAccountNumberSequence(clientCtx tx.Context, addr sdk.AccAddress) (accNum, accSeq uint64, err error) } var _ AccountRetriever = (*MockAccountRetriever)(nil) @@ -32,18 +33,18 @@ type MockAccountRetriever struct { ReturnAccNum, ReturnAccSeq uint64 } -func (mar MockAccountRetriever) GetAccount(_ Context, _ sdk.AccAddress) (Account, error) { +func (mar MockAccountRetriever) GetAccount(_ tx.Context, _ sdk.AccAddress) (Account, error) { return nil, nil } -func (mar MockAccountRetriever) GetAccountWithHeight(_ Context, _ sdk.AccAddress) (Account, int64, error) { +func (mar MockAccountRetriever) GetAccountWithHeight(_ tx.Context, _ sdk.AccAddress) (Account, int64, error) { return nil, 0, nil } -func (mar MockAccountRetriever) EnsureExists(_ Context, _ sdk.AccAddress) error { +func (mar MockAccountRetriever) EnsureExists(_ tx.Context, _ sdk.AccAddress) error { return nil } -func (mar MockAccountRetriever) GetAccountNumberSequence(_ Context, _ sdk.AccAddress) (uint64, uint64, error) { +func (mar MockAccountRetriever) GetAccountNumberSequence(_ tx.Context, _ sdk.AccAddress) (uint64, uint64, error) { return mar.ReturnAccNum, mar.ReturnAccSeq, nil } diff --git a/client/v2/tx/config.go b/client/v2/tx/config.go index 823c6260aee5..500daec76754 100644 --- a/client/v2/tx/config.go +++ b/client/v2/tx/config.go @@ -2,6 +2,7 @@ package tx import ( apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/client/v2/internal/tx" "cosmossdk.io/client/v2/offchain" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -74,7 +75,7 @@ type ExecutionOptions struct { offline bool generateOnly bool simulateAndExecute bool - preprocessTxHook PreprocessTxFn + preprocessTxHook tx.PreprocessTxFn } type ExtensionOptions struct { diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 6252c3286f81..c06921b395d6 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -4,6 +4,7 @@ import ( "context" apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/client/v2/internal/tx" "cosmossdk.io/client/v2/offchain" "cosmossdk.io/math" //"cosmossdk.io/x/tx/signing" @@ -33,7 +34,7 @@ type Factory struct { txParams TxParameters } -func NewFactoryCLI(clientCtx Context, flagSet *pflag.FlagSet) (Factory, error) { +func NewFactoryCLI(clientCtx tx.Context, flagSet *pflag.FlagSet) (Factory, error) { if clientCtx.Viper == nil { clientCtx = clientCtx.WithViper("") } @@ -114,7 +115,7 @@ func NewFactoryCLI(clientCtx Context, flagSet *pflag.FlagSet) (Factory, error) { // they will be queried for and set on the provided Factory. // A new Factory with the updated fields will be returned. // Note: When in offline mode, the Prepare does nothing and returns the original factory. -func (f Factory) Prepare(clientCtx Context) (Factory, error) { +func (f Factory) Prepare(clientCtx tx.Context) (Factory, error) { if f.txParams.ExecutionOptions.offline { return f, nil } @@ -207,7 +208,7 @@ func (f Factory) BuildUnsignedTx(msgs ...sdk.Msg) (TxBuilder, error) { // 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 Context, msgs ...sdk.Msg) error { +func (f Factory) PrintUnsignedTx(clientCtx tx.Context, msgs ...sdk.Msg) error { if f.SimulateAndExecute() { if clientCtx.Offline { return errors.New("cannot estimate gas in offline mode") @@ -524,7 +525,7 @@ func (f Factory) WithFeePayer(fp sdk.AccAddress) Factory { // 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 PreprocessTxFn) Factory { +func (f Factory) WithPreprocessTxHook(preprocessFn tx.PreprocessTxFn) Factory { f.txParams.preprocessTxHook = preprocessFn return f } diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index bb9ffc1fcf7b..b842401e9159 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -4,6 +4,7 @@ import ( "bufio" "context" apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + tx2 "cosmossdk.io/client/v2/internal/tx" "cosmossdk.io/client/v2/offchain" "errors" "fmt" @@ -19,7 +20,7 @@ import ( // GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxCLI(clientCtx Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { +func GenerateOrBroadcastTxCLI(clientCtx tx2.Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { txf, err := NewFactoryCLI(clientCtx, flagSet) if err != nil { return err @@ -30,7 +31,7 @@ func GenerateOrBroadcastTxCLI(clientCtx Context, flagSet *pflag.FlagSet, msgs .. // GenerateOrBroadcastTxWithFactory will either generate and print an unsigned transaction // or sign it and broadcast it returning an error upon failure. -func GenerateOrBroadcastTxWithFactory(clientCtx Context, txf Factory, msgs ...sdk.Msg) error { +func GenerateOrBroadcastTxWithFactory(clientCtx tx2.Context, txf Factory, msgs ...sdk.Msg) error { // Validate all msgs before generating or broadcasting the tx. // We were calling ValidateBasic separately in each CLI handler before. // Right now, we're factorizing that call inside this function. @@ -66,7 +67,7 @@ func GenerateOrBroadcastTxWithFactory(clientCtx Context, txf Factory, msgs ...sd // 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 Context, txf Factory, msgs ...sdk.Msg) error { +func BroadcastTx(clientCtx tx2.Context, txf Factory, msgs ...sdk.Msg) error { txf, err := txf.Prepare(clientCtx) if err != nil { return err @@ -162,9 +163,9 @@ func CalculateGas( } // makeAuxSignerData generates an AuxSignerData from the client inputs. -func makeAuxSignerData(clientCtx Context, f Factory, msgs ...sdk.Msg) (tx.AuxSignerData, error) { +func makeAuxSignerData(clientCtx tx2.Context, f Factory, msgs ...sdk.Msg) (tx.AuxSignerData, error) { b := NewAuxTxBuilder() - fromAddress, name, _, err := GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) + fromAddress, name, _, err := tx2.GetFromFields(clientCtx, clientCtx.Keyring, clientCtx.From) if err != nil { return tx.AuxSignerData{}, err } From 14ac0772d363f79ed4922d4a49cdb11db0d8f88c Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Mon, 29 Apr 2024 16:47:26 -0300 Subject: [PATCH 13/14] wip --- client/v2/internal/{tx => }/context.go | 54 +-- {x/tx => client/v2}/internal/testpb/1.proto | 0 .../v2}/internal/testpb/1.pulsar.go | 0 .../v2}/internal/testpb/buf.gen.yaml | 0 {x/tx => client/v2}/internal/testpb/buf.lock | 0 {x/tx => client/v2}/internal/testpb/buf.yaml | 0 .../v2}/internal/testpb/signers.proto | 0 .../v2}/internal/testpb/signers.pulsar.go | 0 .../v2}/internal/testpb/unknownproto.proto | 0 .../internal/testpb/unknownproto.pulsar.go | 0 .../v2/offchain}/handler_map_test.go | 20 +- client/v2/offchain/sign.go | 3 +- .../v2/offchain/sign_context_test.go | 4 +- x/tx/signing/context.go | 378 ------------------ x/tx/signing/handler_map.go | 60 --- x/tx/signing/sign_mode_handler.go | 16 - 16 files changed, 32 insertions(+), 503 deletions(-) rename client/v2/internal/{tx => }/context.go (92%) rename {x/tx => client/v2}/internal/testpb/1.proto (100%) rename {x/tx => client/v2}/internal/testpb/1.pulsar.go (100%) rename {x/tx => client/v2}/internal/testpb/buf.gen.yaml (100%) rename {x/tx => client/v2}/internal/testpb/buf.lock (100%) rename {x/tx => client/v2}/internal/testpb/buf.yaml (100%) rename {x/tx => client/v2}/internal/testpb/signers.proto (100%) rename {x/tx => client/v2}/internal/testpb/signers.pulsar.go (100%) rename {x/tx => client/v2}/internal/testpb/unknownproto.proto (100%) rename {x/tx => client/v2}/internal/testpb/unknownproto.pulsar.go (100%) rename {x/tx/signing => client/v2/offchain}/handler_map_test.go (62%) rename x/tx/signing/context_test.go => client/v2/offchain/sign_context_test.go (99%) delete mode 100644 x/tx/signing/context.go delete mode 100644 x/tx/signing/handler_map.go delete mode 100644 x/tx/signing/sign_mode_handler.go diff --git a/client/v2/internal/tx/context.go b/client/v2/internal/context.go similarity index 92% rename from client/v2/internal/tx/context.go rename to client/v2/internal/context.go index 2a89b523cdcf..f198574e390d 100644 --- a/client/v2/internal/tx/context.go +++ b/client/v2/internal/context.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "cosmossdk.io/client/v2/tx" + "cosmossdk.io/core/address" "encoding/json" "fmt" @@ -38,29 +39,30 @@ type Context struct { KeyringOptions []keyring.Option KeyringDir string KeyringDefaultKeyName string - Output io.Writer - OutputFormat string - Height int64 - HomeDir string + + Output io.Writer + OutputFormat string + Height int64 + HomeDir string // From is a name or an address of a keyring account used to set FromName and FromAddress fields. // Should be set by the "from" flag. From string // Name of a keyring account used to sign transactions. FromName string // Address of a keyring account used to sign transactions. - FromAddress string - BroadcastMode string - SignModeStr string - UseLedger bool - Simulate bool - GenerateOnly bool - Offline bool - SkipConfirm bool - TxConfig tx.TxConfig - AccountRetriever tx.AccountRetriever - NodeURI string - FeePayer string - FeeGranter string + FromAddress string + BroadcastMode string + SignModeStr string + + Simulate bool + GenerateOnly bool + Offline bool + SkipConfirm bool + NodeURI string + + FeePayer string + FeeGranter string + Viper *viper.Viper LedgerHasProtobuf bool PreprocessTxHook PreprocessTxFn @@ -159,12 +161,6 @@ func (ctx Context) WithGRPCClient(grpcClient *grpc.ClientConn) Context { return ctx } -// WithUseLedger returns a copy of the context with an updated UseLedger flag. -func (ctx Context) WithUseLedger(useLedger bool) Context { - ctx.UseLedger = useLedger - return ctx -} - // WithChainID returns a copy of the context with an updated chain ID. func (ctx Context) WithChainID(chainID string) Context { ctx.ChainID = chainID @@ -257,18 +253,6 @@ func (ctx Context) WithSkipConfirmation(skip bool) Context { return ctx } -// WithTxConfig returns the context with an updated TxConfig -func (ctx Context) WithTxConfig(generator tx.TxConfig) Context { - ctx.TxConfig = generator - return ctx -} - -// WithAccountRetriever returns the context with an updated AccountRetriever -func (ctx Context) WithAccountRetriever(retriever tx.AccountRetriever) Context { - ctx.AccountRetriever = retriever - return ctx -} - // WithInterfaceRegistry returns the context with an updated InterfaceRegistry func (ctx Context) WithInterfaceRegistry(interfaceRegistry codectypes.InterfaceRegistry) Context { ctx.InterfaceRegistry = interfaceRegistry diff --git a/x/tx/internal/testpb/1.proto b/client/v2/internal/testpb/1.proto similarity index 100% rename from x/tx/internal/testpb/1.proto rename to client/v2/internal/testpb/1.proto diff --git a/x/tx/internal/testpb/1.pulsar.go b/client/v2/internal/testpb/1.pulsar.go similarity index 100% rename from x/tx/internal/testpb/1.pulsar.go rename to client/v2/internal/testpb/1.pulsar.go diff --git a/x/tx/internal/testpb/buf.gen.yaml b/client/v2/internal/testpb/buf.gen.yaml similarity index 100% rename from x/tx/internal/testpb/buf.gen.yaml rename to client/v2/internal/testpb/buf.gen.yaml diff --git a/x/tx/internal/testpb/buf.lock b/client/v2/internal/testpb/buf.lock similarity index 100% rename from x/tx/internal/testpb/buf.lock rename to client/v2/internal/testpb/buf.lock diff --git a/x/tx/internal/testpb/buf.yaml b/client/v2/internal/testpb/buf.yaml similarity index 100% rename from x/tx/internal/testpb/buf.yaml rename to client/v2/internal/testpb/buf.yaml diff --git a/x/tx/internal/testpb/signers.proto b/client/v2/internal/testpb/signers.proto similarity index 100% rename from x/tx/internal/testpb/signers.proto rename to client/v2/internal/testpb/signers.proto diff --git a/x/tx/internal/testpb/signers.pulsar.go b/client/v2/internal/testpb/signers.pulsar.go similarity index 100% rename from x/tx/internal/testpb/signers.pulsar.go rename to client/v2/internal/testpb/signers.pulsar.go diff --git a/x/tx/internal/testpb/unknownproto.proto b/client/v2/internal/testpb/unknownproto.proto similarity index 100% rename from x/tx/internal/testpb/unknownproto.proto rename to client/v2/internal/testpb/unknownproto.proto diff --git a/x/tx/internal/testpb/unknownproto.pulsar.go b/client/v2/internal/testpb/unknownproto.pulsar.go similarity index 100% rename from x/tx/internal/testpb/unknownproto.pulsar.go rename to client/v2/internal/testpb/unknownproto.pulsar.go diff --git a/x/tx/signing/handler_map_test.go b/client/v2/offchain/handler_map_test.go similarity index 62% rename from x/tx/signing/handler_map_test.go rename to client/v2/offchain/handler_map_test.go index a937ac643ee0..77e2b6c6c7bd 100644 --- a/x/tx/signing/handler_map_test.go +++ b/client/v2/offchain/handler_map_test.go @@ -1,18 +1,16 @@ -package signing_test +package offchain import ( "context" + signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" "testing" "github.com/stretchr/testify/require" - - signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" - "cosmossdk.io/x/tx/signing" ) var ( - _ signing.SignModeHandler = directHandler{} - _ signing.SignModeHandler = aminoJSONHandler{} + _ SignModeHandler = directHandler{} + _ SignModeHandler = aminoJSONHandler{} ) type directHandler struct{} @@ -21,7 +19,7 @@ func (s directHandler) Mode() signingv1beta1.SignMode { return signingv1beta1.SignMode_SIGN_MODE_DIRECT } -func (s directHandler) GetSignBytes(_ context.Context, _ signing.SignerData, _ signing.TxData) ([]byte, error) { +func (s directHandler) GetSignBytes(_ context.Context, _ SignerData, _ TxData) ([]byte, error) { panic("not implemented") } @@ -31,20 +29,20 @@ func (s aminoJSONHandler) Mode() signingv1beta1.SignMode { return signingv1beta1.SignMode_SIGN_MODE_LEGACY_AMINO_JSON } -func (s aminoJSONHandler) GetSignBytes(_ context.Context, _ signing.SignerData, _ signing.TxData) ([]byte, error) { +func (s aminoJSONHandler) GetSignBytes(_ context.Context, _ SignerData, _ TxData) ([]byte, error) { panic("not implemented") } func TestNewHandlerMap(t *testing.T) { require.PanicsWithValue(t, "nil handler", func() { - signing.NewHandlerMap(nil) + NewHandlerMap(nil) }) require.PanicsWithValue(t, "no handlers", func() { - signing.NewHandlerMap() + NewHandlerMap() }) dh := directHandler{} ah := aminoJSONHandler{} - handlerMap := signing.NewHandlerMap(dh, aminoJSONHandler{}) + handlerMap := NewHandlerMap(dh, aminoJSONHandler{}) require.Equal(t, dh.Mode(), handlerMap.DefaultMode()) require.NotEqual(t, ah.Mode(), handlerMap.DefaultMode()) } diff --git a/client/v2/offchain/sign.go b/client/v2/offchain/sign.go index 428cda3fa2a5..08ae182a5335 100644 --- a/client/v2/offchain/sign.go +++ b/client/v2/offchain/sign.go @@ -4,8 +4,9 @@ import ( "context" apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" apitx "cosmossdk.io/api/cosmos/tx/v1beta1" + tx "cosmossdk.io/client/v2/internal" "cosmossdk.io/client/v2/internal/offchain" - "github.com/cosmos/cosmos-sdk/client/V2/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" diff --git a/x/tx/signing/context_test.go b/client/v2/offchain/sign_context_test.go similarity index 99% rename from x/tx/signing/context_test.go rename to client/v2/offchain/sign_context_test.go index d8c2b370333a..686d2f64f168 100644 --- a/x/tx/signing/context_test.go +++ b/client/v2/offchain/sign_context_test.go @@ -1,6 +1,7 @@ -package signing +package offchain import ( + "cosmossdk.io/client/v2/internal/testpb" "encoding/hex" "strings" "testing" @@ -11,7 +12,6 @@ import ( bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" groupv1 "cosmossdk.io/api/cosmos/group/v1" "cosmossdk.io/core/address" - "cosmossdk.io/x/tx/internal/testpb" ) var deeplyNestedRepeatedSigner = &testpb.DeeplyNestedRepeatedSigner{ diff --git a/x/tx/signing/context.go b/x/tx/signing/context.go deleted file mode 100644 index 7aca6fd2b1cd..000000000000 --- a/x/tx/signing/context.go +++ /dev/null @@ -1,378 +0,0 @@ -package signing - -import ( - "errors" - "fmt" - - cosmos_proto "github.com/cosmos/cosmos-proto" - gogoproto "github.com/cosmos/gogoproto/proto" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protodesc" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/reflect/protoregistry" - - msgv1 "cosmossdk.io/api/cosmos/msg/v1" - "cosmossdk.io/core/address" -) - -type TypeResolver interface { - protoregistry.MessageTypeResolver - protoregistry.ExtensionTypeResolver -} - -// Context is a context for retrieving the list of signers from a -// message where signers are specified by the cosmos.msg.v1.signer protobuf -// option. It also contains the ProtoFileResolver and address.Codec's used -// for resolving message descriptors and converting addresses. -type Context struct { - fileResolver ProtoFileResolver - typeResolver protoregistry.MessageTypeResolver - addressCodec address.Codec - validatorAddressCodec address.Codec - getSignersFuncs map[protoreflect.FullName]GetSignersFunc - customGetSignerFuncs map[protoreflect.FullName]GetSignersFunc - maxRecursionDepth int -} - -// Options are options for creating Context which will be used for signing operations. -type Options struct { - // FileResolver is the protobuf file resolver to use for resolving message descriptors. - // If it is nil, the global protobuf registry will be used. - FileResolver ProtoFileResolver - - // TypeResolver is the protobuf type resolver to use for resolving message types. - TypeResolver TypeResolver - - // AddressCodec is the codec for converting addresses between strings and bytes. - AddressCodec address.Codec - - // ValidatorAddressCodec is the codec for converting validator addresses between strings and bytes. - ValidatorAddressCodec address.Codec - - // CustomGetSigners is a map of message types to custom GetSignersFuncs. - CustomGetSigners map[protoreflect.FullName]GetSignersFunc - - // MaxRecursionDepth is the maximum depth of nested messages that will be traversed - MaxRecursionDepth int -} - -// DefineCustomGetSigners defines a custom GetSigners function for a given -// message type. -// -// NOTE: if a custom signers function is defined, the message type used to -// define this function MUST be the concrete type passed to GetSigners, -// otherwise a runtime type error will occur. -func (o *Options) DefineCustomGetSigners(typeName protoreflect.FullName, f GetSignersFunc) { - if o.CustomGetSigners == nil { - o.CustomGetSigners = map[protoreflect.FullName]GetSignersFunc{} - } - o.CustomGetSigners[typeName] = f -} - -// ProtoFileResolver is a protodesc.Resolver that also allows iterating over all -// files descriptors. It is a subset of the methods supported by protoregistry.Files. -type ProtoFileResolver interface { - protodesc.Resolver - RangeFiles(func(protoreflect.FileDescriptor) bool) -} - -// NewContext creates a new Context using the provided options. -func NewContext(options Options) (*Context, error) { - protoFiles := options.FileResolver - if protoFiles == nil { - protoFiles = gogoproto.HybridResolver - } - - protoTypes := options.TypeResolver - if protoTypes == nil { - protoTypes = protoregistry.GlobalTypes - } - - if options.AddressCodec == nil { - return nil, errors.New("address codec is required") - } - - if options.ValidatorAddressCodec == nil { - return nil, errors.New("validator address codec is required") - } - - if options.MaxRecursionDepth <= 0 { - options.MaxRecursionDepth = 32 - } - - customGetSignerFuncs := map[protoreflect.FullName]GetSignersFunc{} - for k := range options.CustomGetSigners { - customGetSignerFuncs[k] = options.CustomGetSigners[k] - } - - c := &Context{ - fileResolver: protoFiles, - typeResolver: protoTypes, - addressCodec: options.AddressCodec, - validatorAddressCodec: options.ValidatorAddressCodec, - getSignersFuncs: map[protoreflect.FullName]GetSignersFunc{}, - customGetSignerFuncs: customGetSignerFuncs, - maxRecursionDepth: options.MaxRecursionDepth, - } - - return c, nil -} - -// GetSignersFunc returns the signers for a given message. -type GetSignersFunc func(proto.Message) ([][]byte, error) - -// CustomGetSigner is a custom GetSignersFunc that is defined for a specific message type. -type CustomGetSigner struct { - MsgType protoreflect.FullName - Fn GetSignersFunc -} - -func (c CustomGetSigner) IsManyPerContainerType() {} - -func getSignersFieldNames(descriptor protoreflect.MessageDescriptor) ([]string, error) { - signersFields := proto.GetExtension(descriptor.Options(), msgv1.E_Signer).([]string) - if len(signersFields) == 0 { - return nil, fmt.Errorf("no cosmos.msg.v1.signer option found for message %s; use DefineCustomGetSigners to specify a custom getter", descriptor.FullName()) - } - - return signersFields, nil -} - -// Validate performs a dry run of getting all msg's signers. This has 2 benefits: -// - it will error if any Msg has forgotten the "cosmos.msg.v1.signer" -// annotation -// - it will pre-populate the context's internal cache for getSignersFuncs -// so that calling it in antehandlers will be faster. -func (c *Context) Validate() error { - var errs []error - c.fileResolver.RangeFiles(func(fd protoreflect.FileDescriptor) bool { - for i := 0; i < fd.Services().Len(); i++ { - sd := fd.Services().Get(i) - - // Skip services that are not annotated with the "cosmos.msg.v1.service" option. - if ext := proto.GetExtension(sd.Options(), msgv1.E_Service); ext == nil || !ext.(bool) { - continue - } - - for j := 0; j < sd.Methods().Len(); j++ { - md := sd.Methods().Get(j).Input() - _, hasCustomSigner := c.customGetSignerFuncs[md.FullName()] - if _, err := getSignersFieldNames(md); err == nil && hasCustomSigner { - errs = append(errs, fmt.Errorf("a custom signer function as been defined for message %s which already has a signer field defined with (cosmos.msg.v1.signer)", md.FullName())) - continue - } - _, err := c.getGetSignersFn(md) - if err != nil { - errs = append(errs, err) - } - } - } - - return true - }) - - return errors.Join(errs...) -} - -func (c *Context) makeGetSignersFunc(descriptor protoreflect.MessageDescriptor) (GetSignersFunc, error) { - signersFields, err := getSignersFieldNames(descriptor) - if err != nil { - return nil, err - } - - fieldGetters := make([]func(proto.Message, [][]byte) ([][]byte, error), len(signersFields)) - for i, fieldName := range signersFields { - field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) - if field == nil { - return nil, fmt.Errorf("field %s not found in message %s", fieldName, descriptor.FullName()) - } - - if field.IsMap() || field.HasOptionalKeyword() { - return nil, fmt.Errorf("cosmos.msg.v1.signer field %s in message %s must not be a map or optional", fieldName, descriptor.FullName()) - } - - switch field.Kind() { - case protoreflect.StringKind: - addrCdc := c.getAddressCodec(field) - if field.IsList() { - fieldGetters[i] = func(msg proto.Message, arr [][]byte) ([][]byte, error) { - signers := msg.ProtoReflect().Get(field).List() - n := signers.Len() - for i := 0; i < n; i++ { - addrStr := signers.Get(i).String() - addrBz, err := addrCdc.StringToBytes(addrStr) - if err != nil { - return nil, err - } - arr = append(arr, addrBz) - } - return arr, nil - } - } else { - fieldGetters[i] = func(msg proto.Message, arr [][]byte) ([][]byte, error) { - addrStr := msg.ProtoReflect().Get(field).String() - addrBz, err := addrCdc.StringToBytes(addrStr) - if err != nil { - return nil, err - } - return append(arr, addrBz), nil - } - } - case protoreflect.MessageKind: - var fieldGetter func(protoreflect.Message, int) ([][]byte, error) - fieldGetter = func(msg protoreflect.Message, depth int) ([][]byte, error) { - if depth > c.maxRecursionDepth { - return nil, errors.New("maximum recursion depth exceeded") - } - desc := msg.Descriptor() - signerFields, err := getSignersFieldNames(desc) - if err != nil { - return nil, err - } - if len(signerFields) != 1 { - return nil, fmt.Errorf("nested cosmos.msg.v1.signer option in message %s must contain only one value", desc.FullName()) - } - signerFieldName := signerFields[0] - childField := desc.Fields().ByName(protoreflect.Name(signerFieldName)) - switch { - case childField.Kind() == protoreflect.MessageKind: - if childField.IsList() { - childMsgs := msg.Get(childField).List() - var arr [][]byte - for i := 0; i < childMsgs.Len(); i++ { - res, err := fieldGetter(childMsgs.Get(i).Message(), depth+1) - if err != nil { - return nil, err - } - arr = append(arr, res...) - } - return arr, nil - } else { - return fieldGetter(msg.Get(childField).Message(), depth+1) - } - case childField.IsMap() || childField.HasOptionalKeyword(): - return nil, fmt.Errorf("cosmos.msg.v1.signer field %s in message %s must not be a map or optional", signerFieldName, desc.FullName()) - case childField.Kind() == protoreflect.StringKind: - addrCdc := c.getAddressCodec(childField) - if childField.IsList() { - childMsgs := msg.Get(childField).List() - n := childMsgs.Len() - var res [][]byte - for i := 0; i < n; i++ { - addrStr := childMsgs.Get(i).String() - addrBz, err := addrCdc.StringToBytes(addrStr) - if err != nil { - return nil, err - } - res = append(res, addrBz) - } - return res, nil - } else { - addrStr := msg.Get(childField).String() - addrBz, err := addrCdc.StringToBytes(addrStr) - if err != nil { - return nil, err - } - return [][]byte{addrBz}, nil - } - } - return nil, fmt.Errorf("unexpected field type %s for field %s in message %s, only string and message type are supported", - childField.Kind(), signerFieldName, desc.FullName()) - } - - fieldGetters[i] = func(msg proto.Message, arr [][]byte) ([][]byte, error) { - if field.IsList() { - signers := msg.ProtoReflect().Get(field).List() - n := signers.Len() - for i := 0; i < n; i++ { - res, err := fieldGetter(signers.Get(i).Message(), 0) - if err != nil { - return nil, err - } - arr = append(arr, res...) - } - } else { - res, err := fieldGetter(msg.ProtoReflect().Get(field).Message(), 0) - if err != nil { - return nil, err - } - arr = append(arr, res...) - } - return arr, nil - } - default: - return nil, fmt.Errorf("unexpected field type %s for field %s in message %s", field.Kind(), fieldName, descriptor.FullName()) - } - } - - return func(message proto.Message) ([][]byte, error) { - var signers [][]byte - for _, getter := range fieldGetters { - signers, err = getter(message, signers) - if err != nil { - return nil, err - } - } - return signers, nil - }, nil -} - -func (c *Context) getAddressCodec(field protoreflect.FieldDescriptor) address.Codec { - scalarOpt := proto.GetExtension(field.Options(), cosmos_proto.E_Scalar) - addrCdc := c.addressCodec - if scalarOpt != nil { - if scalarOpt.(string) == "cosmos.ValidatorAddressString" { - addrCdc = c.validatorAddressCodec - } - } - - return addrCdc -} - -func (c *Context) getGetSignersFn(messageDescriptor protoreflect.MessageDescriptor) (GetSignersFunc, error) { - f, ok := c.customGetSignerFuncs[messageDescriptor.FullName()] - if ok { - return f, nil - } - f, ok = c.getSignersFuncs[messageDescriptor.FullName()] - if !ok { - var err error - f, err = c.makeGetSignersFunc(messageDescriptor) - if err != nil { - return nil, err - } - c.getSignersFuncs[messageDescriptor.FullName()] = f - } - - return f, nil -} - -// GetSigners returns the signers for a given message. -func (c *Context) GetSigners(msg proto.Message) ([][]byte, error) { - f, err := c.getGetSignersFn(msg.ProtoReflect().Descriptor()) - if err != nil { - return nil, err - } - - return f(msg) -} - -// AddressCodec returns the address codec used by the context. -func (c *Context) AddressCodec() address.Codec { - return c.addressCodec -} - -// ValidatorAddressCodec returns the validator address codec used by the context. -func (c *Context) ValidatorAddressCodec() address.Codec { - return c.validatorAddressCodec -} - -// FileResolver returns the protobuf file resolver used by the context. -func (c *Context) FileResolver() ProtoFileResolver { - return c.fileResolver -} - -// TypeResolver returns the protobuf type resolver used by the context. -func (c *Context) TypeResolver() protoregistry.MessageTypeResolver { - return c.typeResolver -} diff --git a/x/tx/signing/handler_map.go b/x/tx/signing/handler_map.go deleted file mode 100644 index 98238a51d047..000000000000 --- a/x/tx/signing/handler_map.go +++ /dev/null @@ -1,60 +0,0 @@ -package signing - -import ( - "context" - "fmt" - - signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" -) - -// HandlerMap aggregates several sign mode handlers together for convenient generation of sign bytes -// based on sign mode. -type HandlerMap struct { - signModeHandlers map[signingv1beta1.SignMode]SignModeHandler - defaultMode signingv1beta1.SignMode - modes []signingv1beta1.SignMode -} - -// NewHandlerMap constructs a new sign mode handler map. The first handler is used as the default. -func NewHandlerMap(handlers ...SignModeHandler) *HandlerMap { - if len(handlers) == 0 { - panic("no handlers") - } - res := &HandlerMap{ - signModeHandlers: map[signingv1beta1.SignMode]SignModeHandler{}, - } - - for i, handler := range handlers { - if handler == nil { - panic("nil handler") - } - mode := handler.Mode() - if i == 0 { - res.defaultMode = mode - } - res.signModeHandlers[mode] = handler - res.modes = append(res.modes, mode) - } - - return res -} - -// SupportedModes lists the modes supported by this handler map. -func (h *HandlerMap) SupportedModes() []signingv1beta1.SignMode { - return h.modes -} - -// DefaultMode returns the default mode for this handler map. -func (h *HandlerMap) DefaultMode() signingv1beta1.SignMode { - return h.defaultMode -} - -// GetSignBytes returns the sign bytes for the transaction for the requested mode. -func (h *HandlerMap) GetSignBytes(ctx context.Context, signMode signingv1beta1.SignMode, signerData SignerData, txData TxData) ([]byte, error) { - handler, ok := h.signModeHandlers[signMode] - if !ok { - return nil, fmt.Errorf("unsuppored sign mode %s", signMode) - } - - return handler.GetSignBytes(ctx, signerData, txData) -} diff --git a/x/tx/signing/sign_mode_handler.go b/x/tx/signing/sign_mode_handler.go deleted file mode 100644 index 682dbc1941d2..000000000000 --- a/x/tx/signing/sign_mode_handler.go +++ /dev/null @@ -1,16 +0,0 @@ -package signing - -import ( - "context" - - signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" -) - -// SignModeHandler is the interface that handlers for each sign mode should implement to generate sign bytes. -type SignModeHandler interface { - // Mode is the sign mode supported by this handler - Mode() signingv1beta1.SignMode - - // GetSignBytes returns the sign bytes for the provided SignerData and TxData, or an error. - GetSignBytes(ctx context.Context, signerData SignerData, txData TxData) ([]byte, error) -} From d8fc1195096fc26dfd7ea88b30c09e558c7df0fc Mon Sep 17 00:00:00 2001 From: Ezequiel Raynaudo Date: Mon, 6 May 2024 10:13:40 -0300 Subject: [PATCH 14/14] Fix import --- client/v2/tx/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index b842401e9159..c4bb95b71411 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -4,7 +4,7 @@ import ( "bufio" "context" apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" - tx2 "cosmossdk.io/client/v2/internal/tx" + tx2 "cosmossdk.io/client/v2/internal" "cosmossdk.io/client/v2/offchain" "errors" "fmt"