feat(client/v2): factory (#20623)

Co-authored-by: Julien Robert <julien@rbrt.fr>
This commit is contained in:
Julián Toledano 2024-10-03 14:45:10 +02:00 committed by GitHub
parent 8bbf51c5ca
commit c8f4cf787b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 4411 additions and 5 deletions

View File

@ -42,6 +42,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#18626](https://github.com/cosmos/cosmos-sdk/pull/18626) Support for off-chain signing and verification of a file.
* [#18461](https://github.com/cosmos/cosmos-sdk/pull/18461) Support governance proposals.
* [#20623](https://github.com/cosmos/cosmos-sdk/pull/20623) Introduce client/v2 tx factory.
* [#20623](https://github.com/cosmos/cosmos-sdk/pull/20623) Extend client/v2 keyring interface with `KeyType` and `KeyInfo`.
### Improvements

View File

@ -151,7 +151,7 @@ func getKeyringFromCtx(ctx *context.Context) keyring.Keyring {
dctx := *ctx
if dctx != nil {
if clientCtx := dctx.Value(client.ClientContextKey); clientCtx != nil {
k, err := sdkkeyring.NewAutoCLIKeyring(clientCtx.(*client.Context).Keyring)
k, err := sdkkeyring.NewAutoCLIKeyring(clientCtx.(*client.Context).Keyring, clientCtx.(*client.Context).AddressCodec)
if err != nil {
panic(fmt.Errorf("failed to create keyring: %w", err))
}

View File

@ -20,4 +20,10 @@ type Keyring interface {
// Sign signs the given bytes with the key with the given name.
Sign(name string, msg []byte, signMode signingv1beta1.SignMode) ([]byte, error)
// KeyType returns the type of the key.
KeyType(name string) (uint, error)
// KeyInfo given a key name or address returns key name, key address and key type.
KeyInfo(nameOrAddr string) (string, string, uint, error)
}

View File

@ -48,3 +48,13 @@ func (k *KeyringImpl) LookupAddressByKeyName(name string) ([]byte, error) {
func (k *KeyringImpl) Sign(name string, msg []byte, signMode signingv1beta1.SignMode) ([]byte, error) {
return k.k.Sign(name, msg, signMode)
}
// KeyType returns the type of the key.
func (k *KeyringImpl) KeyType(name string) (uint, error) {
return k.k.KeyType(name)
}
// KeyInfo given a key name or address returns key name, key address and key type.
func (k *KeyringImpl) KeyInfo(nameOrAddr string) (string, string, uint, error) {
return k.k.KeyInfo(nameOrAddr)
}

View File

@ -29,3 +29,11 @@ func (k NoKeyring) GetPubKey(name string) (cryptotypes.PubKey, error) {
func (k NoKeyring) Sign(name string, msg []byte, signMode signingv1beta1.SignMode) ([]byte, error) {
return nil, errNoKeyring
}
func (k NoKeyring) KeyType(name string) (uint, error) {
return 0, errNoKeyring
}
func (k NoKeyring) KeyInfo(name string) (string, string, uint, error) {
return "", "", 0, errNoKeyring
}

View File

@ -53,7 +53,7 @@ require (
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.0.3-0.20240911104526-ddc3f09bfc22 // indirect
github.com/cosmos/crypto v0.1.2 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/go-bip39 v1.0.0
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.7.0
github.com/cosmos/iavl v1.3.0 // indirect

View File

@ -0,0 +1,116 @@
package account
import (
"context"
"fmt"
"strconv"
gogogrpc "github.com/cosmos/gogoproto/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"cosmossdk.io/core/address"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
// GRPCBlockHeightHeader represents the gRPC header for block height.
const GRPCBlockHeightHeader = "x-cosmos-block-height"
var _ AccountRetriever = accountRetriever{}
// Account provides a read-only abstraction over the auth module's AccountI.
type Account interface {
GetAddress() sdk.AccAddress
GetPubKey() cryptotypes.PubKey // can return nil.
GetAccountNumber() uint64
GetSequence() uint64
}
// AccountRetriever defines methods required to retrieve account details necessary for transaction signing.
type AccountRetriever interface {
GetAccount(context.Context, []byte) (Account, error)
GetAccountWithHeight(context.Context, []byte) (Account, int64, error)
EnsureExists(context.Context, []byte) error
GetAccountNumberSequence(context.Context, []byte) (accNum, accSeq uint64, err error)
}
type accountRetriever struct {
ac address.Codec
conn gogogrpc.ClientConn
registry codectypes.InterfaceRegistry
}
// NewAccountRetriever creates a new instance of accountRetriever.
func NewAccountRetriever(ac address.Codec, conn gogogrpc.ClientConn, registry codectypes.InterfaceRegistry) *accountRetriever {
return &accountRetriever{
ac: ac,
conn: conn,
registry: registry,
}
}
// GetAccount retrieves an account using its address.
func (a accountRetriever) GetAccount(ctx context.Context, addr []byte) (Account, error) {
acc, _, err := a.GetAccountWithHeight(ctx, addr)
return acc, err
}
// GetAccountWithHeight retrieves an account and its associated block height using the account's address.
func (a accountRetriever) GetAccountWithHeight(ctx context.Context, addr []byte) (Account, int64, error) {
var header metadata.MD
qc := authtypes.NewQueryClient(a.conn)
addrStr, err := a.ac.BytesToString(addr)
if err != nil {
return nil, 0, err
}
res, err := qc.Account(ctx, &authtypes.QueryAccountRequest{Address: addrStr}, grpc.Header(&header))
if err != nil {
return nil, 0, err
}
blockHeight := header.Get(GRPCBlockHeightHeader)
if len(blockHeight) != 1 {
return nil, 0, fmt.Errorf("unexpected '%s' header length; got %d, expected 1", GRPCBlockHeightHeader, len(blockHeight))
}
nBlockHeight, err := strconv.Atoi(blockHeight[0])
if err != nil {
return nil, 0, fmt.Errorf("failed to parse block height: %w", err)
}
var acc Account
if err := a.registry.UnpackAny(res.Account, &acc); err != nil {
return nil, 0, err
}
return acc, int64(nBlockHeight), nil
}
// EnsureExists checks if an account exists using its address.
func (a accountRetriever) EnsureExists(ctx context.Context, addr []byte) error {
if _, err := a.GetAccount(ctx, addr); err != nil {
return err
}
return nil
}
// GetAccountNumberSequence retrieves the account number and sequence for an account using its address.
func (a accountRetriever) GetAccountNumberSequence(ctx context.Context, addr []byte) (accNum, accSeq uint64, err error) {
acc, err := a.GetAccount(ctx, addr)
if err != nil {
if status.Code(err) == codes.NotFound {
return 0, 0, nil
}
return 0, 0, err
}
return acc.GetAccountNumber(), acc.GetSequence(), nil
}

View File

@ -0,0 +1,66 @@
package coins
import (
"errors"
base "cosmossdk.io/api/cosmos/base/v1beta1"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
)
var (
_ withAmount = &base.Coin{}
_ withAmount = &base.DecCoin{}
)
type withAmount interface {
GetAmount() string
}
// IsZero check if given coins are zero.
func IsZero[T withAmount](coins []T) (bool, error) {
for _, coin := range coins {
amount, ok := math.NewIntFromString(coin.GetAmount())
if !ok {
return false, errors.New("invalid coin amount")
}
if !amount.IsZero() {
return false, nil
}
}
return true, nil
}
func ParseDecCoins(coins string) ([]*base.DecCoin, error) {
parsedGasPrices, err := sdk.ParseDecCoins(coins) // TODO: do it here to avoid sdk dependency
if err != nil {
return nil, err
}
finalGasPrices := make([]*base.DecCoin, len(parsedGasPrices))
for i, coin := range parsedGasPrices {
finalGasPrices[i] = &base.DecCoin{
Denom: coin.Denom,
Amount: coin.Amount.String(),
}
}
return finalGasPrices, nil
}
func ParseCoinsNormalized(coins string) ([]*base.Coin, error) {
parsedFees, err := sdk.ParseCoinsNormalized(coins) // TODO: do it here to avoid sdk dependency
if err != nil {
return nil, err
}
finalFees := make([]*base.Coin, len(parsedFees))
for i, coin := range parsedFees {
finalFees[i] = &base.Coin{
Denom: coin.Denom,
Amount: coin.Amount.String(),
}
}
return finalFees, nil
}

View File

@ -0,0 +1,83 @@
package coins
import (
"testing"
"github.com/stretchr/testify/require"
base "cosmossdk.io/api/cosmos/base/v1beta1"
)
func TestCoinIsZero(t *testing.T) {
type testCase[T withAmount] struct {
name string
coins []T
isZero bool
}
tests := []testCase[*base.Coin]{
{
name: "not zero coin",
coins: []*base.Coin{
{
Denom: "stake",
Amount: "100",
},
},
isZero: false,
},
{
name: "zero coin",
coins: []*base.Coin{
{
Denom: "stake",
Amount: "0",
},
},
isZero: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := IsZero(tt.coins)
require.NoError(t, err)
require.Equal(t, got, tt.isZero)
})
}
}
func TestDecCoinIsZero(t *testing.T) {
type testCase[T withAmount] struct {
name string
coins []T
isZero bool
}
tests := []testCase[*base.DecCoin]{
{
name: "not zero coin",
coins: []*base.DecCoin{
{
Denom: "stake",
Amount: "100",
},
},
isZero: false,
},
{
name: "zero coin",
coins: []*base.DecCoin{
{
Denom: "stake",
Amount: "0",
},
},
isZero: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := IsZero(tt.coins)
require.NoError(t, err)
require.Equal(t, got, tt.isZero)
})
}
}

View File

@ -63,7 +63,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) {
keybase, err := keyring.NewAutoCLIKeyring(ctx.Keyring)
keybase, err := keyring.NewAutoCLIKeyring(ctx.Keyring, ctx.AddressCodec)
if err != nil {
return nil, err
}

465
client/v2/tx/README.md Normal file
View File

@ -0,0 +1,465 @@
The tx package provides a robust set of tools for building, signing, and managing transactions in a Cosmos SDK-based blockchain application.
## Overview
This package includes several key components:
1. Transaction Factory
2. Transaction Config
3. Transaction Encoder/Decoder
4. Signature Handling
## Architecture
```mermaid
graph TD
A[Client] --> B[Factory]
B --> D[TxConfig]
D --> E[TxEncodingConfig]
D --> F[TxSigningConfig]
B --> G[Tx]
G --> H[Encoder]
G --> I[Decoder]
F --> J[SignModeHandler]
F --> K[SigningContext]
B --> L[AuxTxBuilder]
```
## Key Components
### TxConfig
`TxConfig` provides configuration for transaction handling, including:
- Encoding and decoding
- Sign mode handling
- Signature JSON marshaling/unmarshaling
```mermaid
classDiagram
class TxConfig {
<<interface>>
TxEncodingConfig
TxSigningConfig
}
class TxEncodingConfig {
<<interface>>
TxEncoder() txEncoder
TxDecoder() txDecoder
TxJSONEncoder() txEncoder
TxJSONDecoder() txDecoder
Decoder() Decoder
}
class TxSigningConfig {
<<interface>>
SignModeHandler() *signing.HandlerMap
SigningContext() *signing.Context
MarshalSignatureJSON([]Signature) ([]byte, error)
UnmarshalSignatureJSON([]byte) ([]Signature, error)
}
class txConfig {
TxEncodingConfig
TxSigningConfig
}
class defaultEncodingConfig {
cdc codec.BinaryCodec
decoder Decoder
TxEncoder() txEncoder
TxDecoder() txDecoder
TxJSONEncoder() txEncoder
TxJSONDecoder() txDecoder
}
class defaultTxSigningConfig {
signingCtx *signing.Context
handlerMap *signing.HandlerMap
cdc codec.BinaryCodec
SignModeHandler() *signing.HandlerMap
SigningContext() *signing.Context
MarshalSignatureJSON([]Signature) ([]byte, error)
UnmarshalSignatureJSON([]byte) ([]Signature, error)
}
TxConfig <|-- txConfig
TxEncodingConfig <|.. defaultEncodingConfig
TxSigningConfig <|.. defaultTxSigningConfig
txConfig *-- defaultEncodingConfig
txConfig *-- defaultTxSigningConfig
```
### Factory
The `Factory` is the main entry point for creating and managing transactions. It handles:
- Account preparation
- Gas calculation
- Unsigned transaction building
- Transaction signing
- Transaction simulation
- Transaction broadcasting
```mermaid
classDiagram
class Factory {
keybase keyring.Keyring
cdc codec.BinaryCodec
accountRetriever account.AccountRetriever
ac address.Codec
conn gogogrpc.ClientConn
txConfig TxConfig
txParams TxParameters
tx txState
NewFactory(keybase, cdc, accRetriever, txConfig, ac, conn, parameters) Factory
Prepare() error
BuildUnsignedTx(msgs ...transaction.Msg) error
BuildsSignedTx(ctx context.Context, msgs ...transaction.Msg) (Tx, error)
calculateGas(msgs ...transaction.Msg) error
Simulate(msgs ...transaction.Msg) (*apitx.SimulateResponse, uint64, error)
UnsignedTxString(msgs ...transaction.Msg) (string, error)
BuildSimTx(msgs ...transaction.Msg) ([]byte, error)
sign(ctx context.Context, overwriteSig bool) (Tx, error)
WithGas(gas uint64)
WithSequence(sequence uint64)
WithAccountNumber(accnum uint64)
getTx() (Tx, error)
getFee() (*apitx.Fee, error)
getSigningTxData() (signing.TxData, error)
setSignatures(...Signature) error
}
class TxParameters {
<<struct>>
chainID string
AccountConfig
GasConfig
FeeConfig
SignModeConfig
TimeoutConfig
MemoConfig
}
class TxConfig {
<<interface>>
}
class Tx {
<<interface>>
}
class txState {
<<struct>>
msgs []transaction.Msg
memo string
fees []*base.Coin
gasLimit uint64
feeGranter []byte
feePayer []byte
timeoutHeight uint64
unordered bool
timeoutTimestamp uint64
signatures []Signature
signerInfos []*apitx.SignerInfo
}
Factory *-- TxParameters
Factory *-- TxConfig
Factory *-- txState
Factory ..> Tx : creates
```
### Encoder/Decoder
The package includes functions for encoding and decoding transactions in both binary and JSON formats.
```mermaid
classDiagram
class Decoder {
<<interface>>
Decode(txBytes []byte) (*txdecode.DecodedTx, error)
}
class txDecoder {
<<function>>
decode(txBytes []byte) (Tx, error)
}
class txEncoder {
<<function>>
encode(tx Tx) ([]byte, error)
}
class EncoderUtils {
<<utility>>
decodeTx(cdc codec.BinaryCodec, decoder Decoder) txDecoder
encodeTx(tx Tx) ([]byte, error)
decodeJsonTx(cdc codec.BinaryCodec, decoder Decoder) txDecoder
encodeJsonTx(tx Tx) ([]byte, error)
protoTxBytes(tx *txv1beta1.Tx) ([]byte, error)
}
class MarshalOptions {
<<utility>>
Deterministic bool
}
class JSONMarshalOptions {
<<utility>>
Indent string
UseProtoNames bool
UseEnumNumbers bool
}
Decoder <.. EncoderUtils : uses
txDecoder <.. EncoderUtils : creates
txEncoder <.. EncoderUtils : implements
EncoderUtils ..> MarshalOptions : uses
EncoderUtils ..> JSONMarshalOptions : uses
```
### Sequence Diagrams
#### Generate Aux Signer Data
```mermaid
sequenceDiagram
participant User
participant GenerateOrBroadcastTxCLI
participant generateAuxSignerData
participant makeAuxSignerData
participant AuxTxBuilder
participant ctx.PrintProto
User->>GenerateOrBroadcastTxCLI: Call with isAux flag
GenerateOrBroadcastTxCLI->>generateAuxSignerData: Call
generateAuxSignerData->>makeAuxSignerData: Call
makeAuxSignerData->>AuxTxBuilder: NewAuxTxBuilder()
makeAuxSignerData->>AuxTxBuilder: SetAddress(f.txParams.fromAddress)
alt f.txParams.offline
makeAuxSignerData->>AuxTxBuilder: SetAccountNumber(f.AccountNumber())
makeAuxSignerData->>AuxTxBuilder: SetSequence(f.Sequence())
else
makeAuxSignerData->>f.accountRetriever: GetAccountNumberSequence()
makeAuxSignerData->>AuxTxBuilder: SetAccountNumber(accNum)
makeAuxSignerData->>AuxTxBuilder: SetSequence(seq)
end
makeAuxSignerData->>AuxTxBuilder: SetMsgs(msgs...)
makeAuxSignerData->>AuxTxBuilder: SetSignMode(f.SignMode())
makeAuxSignerData->>f.keybase: GetPubKey(f.txParams.fromName)
makeAuxSignerData->>AuxTxBuilder: SetPubKey(pubKey)
makeAuxSignerData->>AuxTxBuilder: SetChainID(f.txParams.chainID)
makeAuxSignerData->>AuxTxBuilder: GetSignBytes()
makeAuxSignerData->>f.keybase: Sign(f.txParams.fromName, signBz, f.SignMode())
makeAuxSignerData->>AuxTxBuilder: SetSignature(sig)
makeAuxSignerData->>AuxTxBuilder: GetAuxSignerData()
AuxTxBuilder-->>makeAuxSignerData: Return AuxSignerData
makeAuxSignerData-->>generateAuxSignerData: Return AuxSignerData
generateAuxSignerData->>ctx.PrintProto: Print AuxSignerData
ctx.PrintProto-->>GenerateOrBroadcastTxCLI: Return result
GenerateOrBroadcastTxCLI-->>User: Return result
```
#### Generate Only
```mermaid
sequenceDiagram
participant User
participant GenerateOrBroadcastTxCLI
participant generateOnly
participant Factory
participant ctx.PrintString
User->>GenerateOrBroadcastTxCLI: Call with generateOnly flag
GenerateOrBroadcastTxCLI->>generateOnly: Call
generateOnly->>Factory: Prepare()
alt Error in Prepare
Factory-->>generateOnly: Return error
generateOnly-->>GenerateOrBroadcastTxCLI: Return error
GenerateOrBroadcastTxCLI-->>User: Return error
end
generateOnly->>Factory: UnsignedTxString(msgs...)
Factory->>Factory: BuildUnsignedTx(msgs...)
Factory->>Factory: setMsgs(msgs...)
Factory->>Factory: setMemo(f.txParams.memo)
Factory->>Factory: setFees(f.txParams.gasPrices)
Factory->>Factory: setGasLimit(f.txParams.gas)
Factory->>Factory: setFeeGranter(f.txParams.feeGranter)
Factory->>Factory: setFeePayer(f.txParams.feePayer)
Factory->>Factory: setTimeoutHeight(f.txParams.timeoutHeight)
Factory->>Factory: getTx()
Factory->>Factory: txConfig.TxJSONEncoder()
Factory->>Factory: encoder(tx)
Factory-->>generateOnly: Return unsigned tx string
generateOnly->>ctx.PrintString: Print unsigned tx string
ctx.PrintString-->>generateOnly: Return result
generateOnly-->>GenerateOrBroadcastTxCLI: Return result
GenerateOrBroadcastTxCLI-->>User: Return result
```
#### DryRun
```mermaid
sequenceDiagram
participant User
participant GenerateOrBroadcastTxCLI
participant dryRun
participant Factory
participant os.Stderr
User->>GenerateOrBroadcastTxCLI: Call with dryRun flag
GenerateOrBroadcastTxCLI->>dryRun: Call
dryRun->>Factory: Prepare()
alt Error in Prepare
Factory-->>dryRun: Return error
dryRun-->>GenerateOrBroadcastTxCLI: Return error
GenerateOrBroadcastTxCLI-->>User: Return error
end
dryRun->>Factory: Simulate(msgs...)
Factory->>Factory: BuildSimTx(msgs...)
Factory->>Factory: BuildUnsignedTx(msgs...)
Factory->>Factory: getSimPK()
Factory->>Factory: getSimSignatureData(pk)
Factory->>Factory: setSignatures(sig)
Factory->>Factory: getTx()
Factory->>Factory: txConfig.TxEncoder()(tx)
Factory->>ServiceClient: Simulate(context.Background(), &apitx.SimulateRequest{})
ServiceClient->>Factory: Return result
Factory-->>dryRun: Return (simulation, gas, error)
alt Error in Simulate
dryRun-->>GenerateOrBroadcastTxCLI: Return error
GenerateOrBroadcastTxCLI-->>User: Return error
end
dryRun->>os.Stderr: Fprintf(GasEstimateResponse{GasEstimate: gas})
os.Stderr-->>dryRun: Return result
dryRun-->>GenerateOrBroadcastTxCLI: Return result
GenerateOrBroadcastTxCLI-->>User: Return result
```
#### Generate and Broadcast Tx
```mermaid
sequenceDiagram
participant User
participant GenerateOrBroadcastTxCLI
participant BroadcastTx
participant Factory
participant clientCtx
User->>GenerateOrBroadcastTxCLI: Call
GenerateOrBroadcastTxCLI->>BroadcastTx: Call
BroadcastTx->>Factory: Prepare()
alt Error in Prepare
Factory-->>BroadcastTx: Return error
BroadcastTx-->>GenerateOrBroadcastTxCLI: Return error
GenerateOrBroadcastTxCLI-->>User: Return error
end
alt SimulateAndExecute is true
BroadcastTx->>Factory: calculateGas(msgs...)
Factory->>Factory: Simulate(msgs...)
Factory->>Factory: WithGas(adjusted)
end
BroadcastTx->>Factory: BuildUnsignedTx(msgs...)
Factory->>Factory: setMsgs(msgs...)
Factory->>Factory: setMemo(f.txParams.memo)
Factory->>Factory: setFees(f.txParams.gasPrices)
Factory->>Factory: setGasLimit(f.txParams.gas)
Factory->>Factory: setFeeGranter(f.txParams.feeGranter)
Factory->>Factory: setFeePayer(f.txParams.feePayer)
Factory->>Factory: setTimeoutHeight(f.txParams.timeoutHeight)
alt !clientCtx.SkipConfirm
BroadcastTx->>Factory: getTx()
BroadcastTx->>Factory: txConfig.TxJSONEncoder()
BroadcastTx->>clientCtx: PrintRaw(txBytes)
BroadcastTx->>clientCtx: Input.GetConfirmation()
alt Not confirmed
BroadcastTx-->>GenerateOrBroadcastTxCLI: Return error
GenerateOrBroadcastTxCLI-->>User: Return error
end
end
BroadcastTx->>Factory: BuildsSignedTx(ctx, msgs...)
Factory->>Factory: sign(ctx, true)
Factory->>Factory: keybase.GetPubKey(fromName)
Factory->>Factory: getSignBytesAdapter()
Factory->>Factory: keybase.Sign(fromName, bytesToSign, signMode)
Factory->>Factory: setSignatures(sig)
Factory->>Factory: getTx()
BroadcastTx->>Factory: txConfig.TxEncoder()
BroadcastTx->>clientCtx: BroadcastTx(txBytes)
alt Error in BroadcastTx
clientCtx-->>BroadcastTx: Return error
BroadcastTx-->>GenerateOrBroadcastTxCLI: Return error
GenerateOrBroadcastTxCLI-->>User: Return error
end
BroadcastTx->>clientCtx: OutputTx(res)
clientCtx-->>BroadcastTx: Return result
BroadcastTx-->>GenerateOrBroadcastTxCLI: Return result
GenerateOrBroadcastTxCLI-->>User: Return result
```
## Usage
To use the `tx` package, typically you would:
1. Create a `Factory`
2. Simulate the transaction (optional)
3. Build a signed transaction
4. Encode the transaction
5. Broadcast the transaction
Here's a simplified example:
```go
// Create a Factory
factory, err := NewFactory(keybase, cdc, accountRetriever, txConfig, addressCodec, conn, txParameters)
if err != nil {
return err
}
// Simulate the transaction (optional)
simRes, gas, err := factory.Simulate(msgs...)
if err != nil {
return err
}
factory.WithGas(gas)
// Build a signed transaction
signedTx, err := factory.BuildsSignedTx(context.Background(), msgs...)
if err != nil {
return err
}
// Encode the transaction
txBytes, err := factory.txConfig.TxEncoder()(signedTx)
if err != nil {
return err
}
// Broadcast the transaction
// (This step depends on your specific client implementation)
```

116
client/v2/tx/common_test.go Normal file
View File

@ -0,0 +1,116 @@
package tx
import (
"context"
"google.golang.org/grpc"
abciv1beta1 "cosmossdk.io/api/cosmos/base/abci/v1beta1"
apitx "cosmossdk.io/api/cosmos/tx/v1beta1"
"cosmossdk.io/client/v2/autocli/keyring"
"cosmossdk.io/client/v2/internal/account"
txdecode "cosmossdk.io/x/tx/decode"
"cosmossdk.io/x/tx/signing"
"github.com/cosmos/cosmos-sdk/codec"
addrcodec "github.com/cosmos/cosmos-sdk/codec/address"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
codec2 "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/hd"
cryptoKeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/types"
)
var (
cdc = codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
ac = addrcodec.NewBech32Codec("cosmos")
valCodec = addrcodec.NewBech32Codec("cosmosval")
signingOptions = signing.Options{
AddressCodec: ac,
ValidatorAddressCodec: valCodec,
}
signingContext, _ = signing.NewContext(signingOptions)
decodeOptions = txdecode.Options{SigningContext: signingContext, ProtoCodec: cdc}
decoder, _ = txdecode.NewDecoder(decodeOptions)
k = cryptoKeyring.NewInMemory(cdc)
keybase, _ = cryptoKeyring.NewAutoCLIKeyring(k, ac)
txConf, _ = NewTxConfig(ConfigOptions{
AddressCodec: ac,
Cdc: cdc,
ValidatorAddressCodec: valCodec,
})
)
func setKeyring() keyring.Keyring {
registry := codectypes.NewInterfaceRegistry()
codec2.RegisterInterfaces(registry)
cdc := codec.NewProtoCodec(registry)
k := cryptoKeyring.NewInMemory(cdc)
_, err := k.NewAccount("alice", "equip will roof matter pink blind book anxiety banner elbow sun young", "", "m/44'/118'/0'/0/0", hd.Secp256k1)
if err != nil {
panic(err)
}
keybase, err := cryptoKeyring.NewAutoCLIKeyring(k, ac)
if err != nil {
panic(err)
}
return keybase
}
type mockAccount struct {
addr []byte
}
func (m mockAccount) GetAddress() types.AccAddress {
return m.addr
}
func (m mockAccount) GetPubKey() cryptotypes.PubKey {
return nil
}
func (m mockAccount) GetAccountNumber() uint64 {
return 1
}
func (m mockAccount) GetSequence() uint64 {
return 0
}
type mockAccountRetriever struct{}
func (m mockAccountRetriever) GetAccount(_ context.Context, address []byte) (account.Account, error) {
return mockAccount{addr: address}, nil
}
func (m mockAccountRetriever) GetAccountWithHeight(_ context.Context, address []byte) (account.Account, int64, error) {
return mockAccount{addr: address}, 0, nil
}
func (m mockAccountRetriever) EnsureExists(_ context.Context, _ []byte) error {
return nil
}
func (m mockAccountRetriever) GetAccountNumberSequence(_ context.Context, _ []byte) (accNum, accSeq uint64, err error) {
return accNum, accSeq, nil
}
type mockClientConn struct{}
func (m mockClientConn) Invoke(_ context.Context, _ string, _, reply interface{}, _ ...grpc.CallOption) error {
simResponse := apitx.SimulateResponse{
GasInfo: &abciv1beta1.GasInfo{
GasWanted: 10000,
GasUsed: 7500,
},
Result: nil,
}
*reply.(*apitx.SimulateResponse) = simResponse // nolint:govet // ignore linting error
return nil
}
func (m mockClientConn) NewStream(_ context.Context, _ *grpc.StreamDesc, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
}

338
client/v2/tx/config.go Normal file
View File

@ -0,0 +1,338 @@
package tx
import (
"errors"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"cosmossdk.io/core/address"
txdecode "cosmossdk.io/x/tx/decode"
"cosmossdk.io/x/tx/signing"
"cosmossdk.io/x/tx/signing/aminojson"
"cosmossdk.io/x/tx/signing/direct"
"cosmossdk.io/x/tx/signing/directaux"
"cosmossdk.io/x/tx/signing/textual"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)
var (
_ TxConfig = txConfig{}
_ TxEncodingConfig = defaultEncodingConfig{}
_ TxSigningConfig = defaultTxSigningConfig{}
defaultEnabledSignModes = []apitxsigning.SignMode{
apitxsigning.SignMode_SIGN_MODE_DIRECT,
apitxsigning.SignMode_SIGN_MODE_DIRECT_AUX,
apitxsigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
}
)
// TxConfig is an interface that a client can use to generate a concrete transaction type
// defined by the application.
type TxConfig interface {
TxEncodingConfig
TxSigningConfig
}
// TxEncodingConfig defines the interface for transaction encoding and decoding.
// It provides methods for both binary and JSON encoding/decoding.
type TxEncodingConfig interface {
// TxEncoder returns an encoder for binary transaction encoding.
TxEncoder() txEncoder
// TxDecoder returns a decoder for binary transaction decoding.
TxDecoder() txDecoder
// TxJSONEncoder returns an encoder for JSON transaction encoding.
TxJSONEncoder() txEncoder
// TxJSONDecoder returns a decoder for JSON transaction decoding.
TxJSONDecoder() txDecoder
// Decoder returns the Decoder interface for decoding transaction bytes into a DecodedTx.
Decoder() Decoder
}
// TxSigningConfig defines the interface for transaction signing configurations.
type TxSigningConfig interface {
// SignModeHandler returns a reference to the HandlerMap which manages the different signing modes.
SignModeHandler() *signing.HandlerMap
// SigningContext returns a reference to the Context which holds additional data required during signing.
SigningContext() *signing.Context
// MarshalSignatureJSON takes a slice of Signature objects and returns their JSON encoding.
MarshalSignatureJSON([]Signature) ([]byte, error)
// UnmarshalSignatureJSON takes a JSON byte slice and returns a slice of Signature objects.
UnmarshalSignatureJSON([]byte) ([]Signature, error)
}
// ConfigOptions defines the configuration options for transaction processing.
type ConfigOptions struct {
AddressCodec address.Codec
Decoder Decoder
Cdc codec.BinaryCodec
ValidatorAddressCodec address.Codec
FileResolver signing.ProtoFileResolver
TypeResolver signing.TypeResolver
CustomGetSigner map[protoreflect.FullName]signing.GetSignersFunc
MaxRecursionDepth int
EnablesSignModes []apitxsigning.SignMode
CustomSignModes []signing.SignModeHandler
TextualCoinMetadataQueryFn textual.CoinMetadataQueryFn
}
// validate checks the ConfigOptions for required fields and sets default values where necessary.
// It returns an error if any required field is missing.
func (c *ConfigOptions) validate() error {
if c.AddressCodec == nil {
return errors.New("address codec cannot be nil")
}
if c.Cdc == nil {
return errors.New("codec cannot be nil")
}
if c.ValidatorAddressCodec == nil {
return errors.New("validator address codec cannot be nil")
}
// set default signModes if none are provided
if len(c.EnablesSignModes) == 0 {
c.EnablesSignModes = defaultEnabledSignModes
}
return nil
}
// txConfig is a struct that embeds TxEncodingConfig and TxSigningConfig interfaces.
type txConfig struct {
TxEncodingConfig
TxSigningConfig
}
// NewTxConfig creates a new TxConfig instance using the provided ConfigOptions.
// It validates the options, initializes the signing context, and sets up the decoder if not provided.
func NewTxConfig(options ConfigOptions) (TxConfig, error) {
err := options.validate()
if err != nil {
return nil, err
}
signingCtx, err := newDefaultTxSigningConfig(options)
if err != nil {
return nil, err
}
if options.Decoder == nil {
options.Decoder, err = txdecode.NewDecoder(txdecode.Options{
SigningContext: signingCtx.SigningContext(),
ProtoCodec: options.Cdc,
})
if err != nil {
return nil, err
}
}
return &txConfig{
TxEncodingConfig: defaultEncodingConfig{
cdc: options.Cdc,
decoder: options.Decoder,
},
TxSigningConfig: signingCtx,
}, nil
}
// defaultEncodingConfig is an empty struct that implements the TxEncodingConfig interface.
type defaultEncodingConfig struct {
cdc codec.BinaryCodec
decoder Decoder
}
// TxEncoder returns the default transaction encoder.
func (t defaultEncodingConfig) TxEncoder() txEncoder {
return encodeTx
}
// TxDecoder returns the default transaction decoder.
func (t defaultEncodingConfig) TxDecoder() txDecoder {
return decodeTx(t.cdc, t.decoder)
}
// TxJSONEncoder returns the default JSON transaction encoder.
func (t defaultEncodingConfig) TxJSONEncoder() txEncoder {
return encodeJsonTx
}
// TxJSONDecoder returns the default JSON transaction decoder.
func (t defaultEncodingConfig) TxJSONDecoder() txDecoder {
return decodeJsonTx(t.cdc, t.decoder)
}
// Decoder returns the Decoder instance associated with this encoding configuration.
func (t defaultEncodingConfig) Decoder() Decoder {
return t.decoder
}
// defaultTxSigningConfig is a struct that holds the signing context and handler map.
type defaultTxSigningConfig struct {
signingCtx *signing.Context
handlerMap *signing.HandlerMap
cdc codec.BinaryCodec
}
// newDefaultTxSigningConfig creates a new defaultTxSigningConfig instance using the provided ConfigOptions.
// It initializes the signing context and handler map.
func newDefaultTxSigningConfig(opts ConfigOptions) (*defaultTxSigningConfig, error) {
signingCtx, err := newSigningContext(opts)
if err != nil {
return nil, err
}
handlerMap, err := newHandlerMap(opts, signingCtx)
if err != nil {
return nil, err
}
return &defaultTxSigningConfig{
signingCtx: signingCtx,
handlerMap: handlerMap,
cdc: opts.Cdc,
}, nil
}
// SignModeHandler returns the handler map that manages the different signing modes.
func (t defaultTxSigningConfig) SignModeHandler() *signing.HandlerMap {
return t.handlerMap
}
// SigningContext returns the signing context that holds additional data required during signing.
func (t defaultTxSigningConfig) SigningContext() *signing.Context {
return t.signingCtx
}
// MarshalSignatureJSON takes a slice of Signature objects and returns their JSON encoding.
// This method is not yet implemented and will panic if called.
func (t defaultTxSigningConfig) MarshalSignatureJSON(signatures []Signature) ([]byte, error) {
descriptor := make([]*apitxsigning.SignatureDescriptor, len(signatures))
for i, sig := range signatures {
descData, err := signatureDataToProto(sig.Data)
if err != nil {
return nil, err
}
anyPk, err := codectypes.NewAnyWithValue(sig.PubKey)
if err != nil {
return nil, err
}
descriptor[i] = &apitxsigning.SignatureDescriptor{
PublicKey: &anypb.Any{
TypeUrl: codectypes.MsgTypeURL(sig.PubKey),
Value: anyPk.Value,
},
Data: descData,
Sequence: sig.Sequence,
}
}
return jsonMarshalOptions.Marshal(&apitxsigning.SignatureDescriptors{Signatures: descriptor})
}
// UnmarshalSignatureJSON takes a JSON byte slice and returns a slice of Signature objects.
// This method is not yet implemented and will panic if called.
func (t defaultTxSigningConfig) UnmarshalSignatureJSON(bz []byte) ([]Signature, error) {
var descriptor apitxsigning.SignatureDescriptors
err := protojson.UnmarshalOptions{}.Unmarshal(bz, &descriptor)
if err != nil {
return nil, err
}
sigs := make([]Signature, len(descriptor.Signatures))
for i, desc := range descriptor.Signatures {
var pubkey cryptotypes.PubKey
anyPk := &codectypes.Any{
TypeUrl: desc.PublicKey.TypeUrl,
Value: desc.PublicKey.Value,
}
err = t.cdc.UnpackAny(anyPk, &pubkey)
if err != nil {
return nil, err
}
data, err := SignatureDataFromProto(desc.Data)
if err != nil {
return nil, err
}
sigs[i] = Signature{
PubKey: pubkey,
Data: data,
Sequence: desc.Sequence,
}
}
return sigs, nil
}
// newSigningContext creates a new signing context using the provided ConfigOptions.
// Returns a signing.Context instance or an error if initialization fails.
func newSigningContext(opts ConfigOptions) (*signing.Context, error) {
return signing.NewContext(signing.Options{
FileResolver: opts.FileResolver,
TypeResolver: opts.TypeResolver,
AddressCodec: opts.AddressCodec,
ValidatorAddressCodec: opts.ValidatorAddressCodec,
CustomGetSigners: opts.CustomGetSigner,
MaxRecursionDepth: opts.MaxRecursionDepth,
})
}
// newHandlerMap constructs a new HandlerMap based on the provided ConfigOptions and signing context.
// It initializes handlers for each enabled and custom sign mode specified in the options.
func newHandlerMap(opts ConfigOptions, signingCtx *signing.Context) (*signing.HandlerMap, error) {
lenSignModes := len(opts.EnablesSignModes)
handlers := make([]signing.SignModeHandler, lenSignModes+len(opts.CustomSignModes))
for i, m := range opts.EnablesSignModes {
var err error
switch m {
case apitxsigning.SignMode_SIGN_MODE_DIRECT:
handlers[i] = &direct.SignModeHandler{}
case apitxsigning.SignMode_SIGN_MODE_TEXTUAL:
if opts.TextualCoinMetadataQueryFn == nil {
return nil, errors.New("cannot enable SIGN_MODE_TEXTUAL without a TextualCoinMetadataQueryFn")
}
handlers[i], err = textual.NewSignModeHandler(textual.SignModeOptions{
CoinMetadataQuerier: opts.TextualCoinMetadataQueryFn,
FileResolver: signingCtx.FileResolver(),
TypeResolver: signingCtx.TypeResolver(),
})
if err != nil {
return nil, err
}
case apitxsigning.SignMode_SIGN_MODE_DIRECT_AUX:
handlers[i], err = directaux.NewSignModeHandler(directaux.SignModeHandlerOptions{
TypeResolver: signingCtx.TypeResolver(),
SignersContext: signingCtx,
})
if err != nil {
return nil, err
}
case apitxsigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON:
handlers[i] = aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{
FileResolver: signingCtx.FileResolver(),
TypeResolver: opts.TypeResolver,
})
}
}
for i, m := range opts.CustomSignModes {
handlers[i+lenSignModes] = m
}
handler := signing.NewHandlerMap(handlers...)
return handler, nil
}

293
client/v2/tx/config_test.go Normal file
View File

@ -0,0 +1,293 @@
package tx
import (
"context"
"testing"
"github.com/stretchr/testify/require"
apicrypto "cosmossdk.io/api/cosmos/crypto/multisig/v1beta1"
_ "cosmossdk.io/api/cosmos/crypto/secp256k1"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"cosmossdk.io/x/tx/signing"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/address"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
codec2 "github.com/cosmos/cosmos-sdk/crypto/codec"
kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)
type mockModeHandler struct{}
func (t mockModeHandler) Mode() apitxsigning.SignMode {
return apitxsigning.SignMode_SIGN_MODE_DIRECT
}
func (t mockModeHandler) GetSignBytes(_ context.Context, _ signing.SignerData, _ signing.TxData) ([]byte, error) {
return []byte{}, nil
}
func TestConfigOptions_validate(t *testing.T) {
tests := []struct {
name string
opts ConfigOptions
wantErr bool
}{
{
name: "valid options",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Decoder: decoder,
Cdc: cdc,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
},
},
{
name: "missing address codec",
opts: ConfigOptions{
Decoder: decoder,
Cdc: cdc,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
},
wantErr: true,
},
{
name: "missing decoder",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Cdc: cdc,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
},
},
{
name: "missing codec",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Decoder: decoder,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
},
wantErr: true,
},
{
name: "missing validator address codec",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Decoder: decoder,
Cdc: cdc,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.opts.validate(); (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_newHandlerMap(t *testing.T) {
tests := []struct {
name string
opts ConfigOptions
}{
{
name: "handler map with default sign modes",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Decoder: decoder,
Cdc: cdc,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
},
},
{
name: "handler map with just one sign mode",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Decoder: decoder,
Cdc: cdc,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
EnablesSignModes: []apitxsigning.SignMode{apitxsigning.SignMode_SIGN_MODE_DIRECT},
},
},
{
name: "handler map with custom sign modes",
opts: ConfigOptions{
AddressCodec: address.NewBech32Codec("cosmos"),
Decoder: decoder,
Cdc: cdc,
ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"),
CustomSignModes: []signing.SignModeHandler{mockModeHandler{}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.opts.validate()
require.NoError(t, err)
signingCtx, err := newSigningContext(tt.opts)
require.NoError(t, err)
handlerMap, err := newHandlerMap(tt.opts, signingCtx)
require.NoError(t, err)
require.NotNil(t, handlerMap)
require.Equal(t, len(handlerMap.SupportedModes()), len(tt.opts.EnablesSignModes)+len(tt.opts.CustomSignModes))
})
}
}
func TestNewTxConfig(t *testing.T) {
tests := []struct {
name string
options ConfigOptions
wantErr bool
}{
{
name: "valid options",
options: ConfigOptions{
AddressCodec: ac,
Cdc: cdc,
ValidatorAddressCodec: valCodec,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewTxConfig(tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("NewTxConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.NotNil(t, got)
})
}
}
func Test_defaultTxSigningConfig_MarshalSignatureJSON(t *testing.T) {
tests := []struct {
name string
options ConfigOptions
signatures func(t *testing.T) []Signature
}{
{
name: "single signature",
options: ConfigOptions{
AddressCodec: ac,
Cdc: cdc,
ValidatorAddressCodec: valCodec,
},
signatures: func(t *testing.T) []Signature {
t.Helper()
k := setKeyring()
pk, err := k.GetPubKey("alice")
require.NoError(t, err)
signature, err := k.Sign("alice", make([]byte, 10), apitxsigning.SignMode_SIGN_MODE_DIRECT)
require.NoError(t, err)
return []Signature{
{
PubKey: pk,
Data: &SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: signature,
},
},
}
},
},
{
name: "multisig signatures",
options: ConfigOptions{
AddressCodec: ac,
Cdc: cdc,
ValidatorAddressCodec: valCodec,
},
signatures: func(t *testing.T) []Signature {
t.Helper()
n := 2
pubKeys := make([]cryptotypes.PubKey, n)
sigs := make([]SignatureData, n)
for i := 0; i < n; i++ {
sk := secp256k1.GenPrivKey()
pubKeys[i] = sk.PubKey()
msg, err := sk.Sign(make([]byte, 10))
require.NoError(t, err)
sigs[i] = &SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: msg,
}
}
bitArray := cryptotypes.NewCompactBitArray(n)
mKey := kmultisig.NewLegacyAminoPubKey(n, pubKeys)
return []Signature{
{
PubKey: mKey,
Data: &MultiSignatureData{
BitArray: &apicrypto.CompactBitArray{
ExtraBitsStored: bitArray.ExtraBitsStored,
Elems: bitArray.Elems,
},
Signatures: sigs,
},
},
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := NewTxConfig(tt.options)
require.NoError(t, err)
got, err := config.MarshalSignatureJSON(tt.signatures(t))
require.NoError(t, err)
require.NotNil(t, got)
})
}
}
func Test_defaultTxSigningConfig_UnmarshalSignatureJSON(t *testing.T) {
registry := codectypes.NewInterfaceRegistry()
codec2.RegisterInterfaces(registry)
cdc := codec.NewProtoCodec(registry)
tests := []struct {
name string
options ConfigOptions
bz []byte
}{
{
name: "single signature",
options: ConfigOptions{
AddressCodec: ac,
Cdc: cdc,
ValidatorAddressCodec: valCodec,
},
bz: []byte(`{"signatures":[{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey", "key":"A0/vnNfExjWI07A/61KBudIyy6NNbz1xruWSEf+/4f6H"}, "data":{"single":{"mode":"SIGN_MODE_DIRECT", "signature":"usUTJwdc4PWPuox0Y0G/RuHoxyj+QpUcBGvXyNdDX1FOdoVj0tg4TGKT2NnM3QP6wCNbubjHuMOhTtqfW8SkYg=="}}}]}`),
},
{
name: "multisig signatures",
options: ConfigOptions{
AddressCodec: ac,
Cdc: cdc,
ValidatorAddressCodec: valCodec,
},
bz: []byte(`{"signatures":[{"public_key":{"@type":"/cosmos.crypto.multisig.LegacyAminoPubKey","threshold":2,"public_keys":[{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A4Bs9huvS/COpZNhVhTnhgc8YR6VrSQ8hLQIHgnA+m3w"},{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AuNz2lFkLn3sKNjC5r4OWhgkWg5DZpGUiR9OdpzXspnp"}]},"data":{"multi":{"bitarray":{"extra_bits_stored":2,"elems":"AA=="},"signatures":[{"single":{"mode":"SIGN_MODE_DIRECT","signature":"vng4IlPzLH3fDFpikM5y1SfXFGny4BcLGwIFU0Ty4yoWjIxjTS4m6fgDB61sxEkV5DK/CD7gUwenGuEpzJ2IGw=="}},{"single":{"mode":"SIGN_MODE_DIRECT","signature":"2dsGmr13bq/mPxbk9AgqcFpuvk4beszWu6uxkx+EhTMdVGp4J8FtjZc8xs/Pp3oTWY4ScAORYQHxwqN4qwMXGg=="}}]}}}]}`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := NewTxConfig(tt.options)
require.NoError(t, err)
got, err := config.UnmarshalSignatureJSON(tt.bz)
require.NoError(t, err)
require.NotNil(t, got)
})
}
}

119
client/v2/tx/encoder.go Normal file
View File

@ -0,0 +1,119 @@
package tx
import (
"fmt"
"google.golang.org/protobuf/encoding/protojson"
protov2 "google.golang.org/protobuf/proto"
txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1"
txdecode "cosmossdk.io/x/tx/decode"
"github.com/cosmos/cosmos-sdk/codec"
)
var (
// marshalOption configures protobuf marshaling to be deterministic.
marshalOption = protov2.MarshalOptions{Deterministic: true}
// jsonMarshalOptions configures JSON marshaling for protobuf messages.
jsonMarshalOptions = protojson.MarshalOptions{
Indent: "",
UseProtoNames: true,
UseEnumNumbers: false,
}
)
// Decoder defines the interface for decoding transaction bytes into a DecodedTx.
type Decoder interface {
Decode(txBytes []byte) (*txdecode.DecodedTx, error)
}
// txDecoder is a function type that unmarshals transaction bytes into an API Tx type.
type txDecoder func(txBytes []byte) (Tx, error)
// txEncoder is a function type that marshals a transaction into bytes.
type txEncoder func(tx Tx) ([]byte, error)
// decodeTx decodes transaction bytes into an apitx.Tx structure.
func decodeTx(cdc codec.BinaryCodec, decoder Decoder) txDecoder {
return func(txBytes []byte) (Tx, error) {
tx := new(txv1beta1.Tx)
err := protov2.Unmarshal(txBytes, tx)
if err != nil {
return nil, err
}
pTxBytes, err := protoTxBytes(tx)
if err != nil {
return nil, err
}
decodedTx, err := decoder.Decode(pTxBytes)
if err != nil {
return nil, err
}
return newWrapperTx(cdc, decodedTx), nil
}
}
// encodeTx encodes an apitx.Tx into bytes using protobuf marshaling options.
func encodeTx(tx Tx) ([]byte, error) {
wTx, ok := tx.(*wrappedTx)
if !ok {
return nil, fmt.Errorf("unexpected tx type: %T", tx)
}
return marshalOption.Marshal(wTx.Tx)
}
// decodeJsonTx decodes transaction bytes into an apitx.Tx structure using JSON format.
func decodeJsonTx(cdc codec.BinaryCodec, decoder Decoder) txDecoder {
return func(txBytes []byte) (Tx, error) {
jsonTx := new(txv1beta1.Tx)
err := protojson.UnmarshalOptions{
AllowPartial: false,
DiscardUnknown: false,
}.Unmarshal(txBytes, jsonTx)
if err != nil {
return nil, err
}
pTxBytes, err := protoTxBytes(jsonTx)
if err != nil {
return nil, err
}
decodedTx, err := decoder.Decode(pTxBytes)
if err != nil {
return nil, err
}
return newWrapperTx(cdc, decodedTx), nil
}
}
// encodeJsonTx encodes an apitx.Tx into bytes using JSON marshaling options.
func encodeJsonTx(tx Tx) ([]byte, error) {
wTx, ok := tx.(*wrappedTx)
if !ok {
return nil, fmt.Errorf("unexpected tx type: %T", tx)
}
return jsonMarshalOptions.Marshal(wTx.Tx)
}
func protoTxBytes(tx *txv1beta1.Tx) ([]byte, error) {
bodyBytes, err := marshalOption.Marshal(tx.Body)
if err != nil {
return nil, err
}
authInfoBytes, err := marshalOption.Marshal(tx.AuthInfo)
if err != nil {
return nil, err
}
return marshalOption.Marshal(&txv1beta1.TxRaw{
BodyBytes: bodyBytes,
AuthInfoBytes: authInfoBytes,
Signatures: tx.Signatures,
})
}

View File

@ -0,0 +1,107 @@
package tx
import (
"testing"
"github.com/stretchr/testify/require"
base "cosmossdk.io/api/cosmos/base/v1beta1"
countertypes "cosmossdk.io/api/cosmos/counter/v1"
apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"cosmossdk.io/core/transaction"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
)
func getWrappedTx(t *testing.T) *wrappedTx {
t.Helper()
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
pk := secp256k1.GenPrivKey().PubKey()
addr, _ := ac.BytesToString(pk.Address())
f.tx.msgs = []transaction.Msg{&countertypes.MsgIncreaseCounter{
Signer: addr,
Count: 0,
}}
require.NoError(t, err)
err = f.setFeePayer(addr)
require.NoError(t, err)
f.tx.fees = []*base.Coin{{
Denom: "cosmos",
Amount: "1000",
}}
err = f.setSignatures([]Signature{{
PubKey: pk,
Data: &SingleSignatureData{
SignMode: apisigning.SignMode_SIGN_MODE_DIRECT,
Signature: nil,
},
Sequence: 0,
}}...)
require.NoError(t, err)
wTx, err := f.getTx()
require.NoError(t, err)
return wTx
}
func Test_txEncoder_txDecoder(t *testing.T) {
wTx := getWrappedTx(t)
encodedTx, err := encodeTx(wTx)
require.NoError(t, err)
require.NotNil(t, encodedTx)
isDeterministic, err := encodeTx(wTx)
require.NoError(t, err)
require.NotNil(t, encodedTx)
require.Equal(t, encodedTx, isDeterministic)
f := decodeTx(cdc, decoder)
decodedTx, err := f(encodedTx)
require.NoError(t, err)
require.NotNil(t, decodedTx)
dTx, ok := decodedTx.(*wrappedTx)
require.True(t, ok)
require.Equal(t, wTx.TxRaw, dTx.TxRaw)
require.Equal(t, wTx.Tx.AuthInfo.String(), dTx.Tx.AuthInfo.String())
require.Equal(t, wTx.Tx.Body.String(), dTx.Tx.Body.String())
require.Equal(t, wTx.Tx.Signatures, dTx.Tx.Signatures)
}
func Test_txJsonEncoder_txJsonDecoder(t *testing.T) {
tests := []struct {
name string
}{
{
name: "json encode and decode tx",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wTx := getWrappedTx(t)
encodedTx, err := encodeJsonTx(wTx)
require.NoError(t, err)
require.NotNil(t, encodedTx)
f := decodeJsonTx(cdc, decoder)
decodedTx, err := f(encodedTx)
require.NoError(t, err)
require.NotNil(t, decodedTx)
dTx, ok := decodedTx.(*wrappedTx)
require.True(t, ok)
require.Equal(t, wTx.TxRaw, dTx.TxRaw)
require.Equal(t, wTx.Tx.AuthInfo.String(), dTx.Tx.AuthInfo.String())
require.Equal(t, wTx.Tx.Body.String(), dTx.Tx.Body.String())
require.Equal(t, wTx.Tx.Signatures, dTx.Tx.Signatures)
})
}
}

761
client/v2/tx/factory.go Normal file
View File

@ -0,0 +1,761 @@
package tx
import (
"context"
"errors"
"fmt"
"math/big"
"strings"
"github.com/cosmos/go-bip39"
gogogrpc "github.com/cosmos/gogoproto/grpc"
"github.com/spf13/pflag"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
base "cosmossdk.io/api/cosmos/base/v1beta1"
apicrypto "cosmossdk.io/api/cosmos/crypto/multisig/v1beta1"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
apitx "cosmossdk.io/api/cosmos/tx/v1beta1"
"cosmossdk.io/client/v2/autocli/keyring"
"cosmossdk.io/client/v2/internal/account"
"cosmossdk.io/client/v2/internal/coins"
"cosmossdk.io/core/address"
"cosmossdk.io/core/transaction"
"cosmossdk.io/math"
"cosmossdk.io/x/tx/signing"
flags2 "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)
// Factory defines a client transaction factory that facilitates generating and
// signing an application-specific transaction.
type Factory struct {
keybase keyring.Keyring
cdc codec.BinaryCodec
accountRetriever account.AccountRetriever
ac address.Codec
conn gogogrpc.ClientConn
txConfig TxConfig
txParams TxParameters
tx txState
}
func NewFactoryFromFlagSet(flags *pflag.FlagSet, keybase keyring.Keyring, cdc codec.BinaryCodec, accRetriever account.AccountRetriever,
txConfig TxConfig, ac address.Codec, conn gogogrpc.ClientConn,
) (Factory, error) {
offline, _ := flags.GetBool(flags2.FlagOffline)
if err := validateFlagSet(flags, offline); err != nil {
return Factory{}, err
}
params, err := txParamsFromFlagSet(flags, keybase, ac)
if err != nil {
return Factory{}, err
}
params, err = prepareTxParams(params, accRetriever, offline)
if err != nil {
return Factory{}, err
}
return NewFactory(keybase, cdc, accRetriever, txConfig, ac, conn, params)
}
// NewFactory returns a new instance of Factory.
func NewFactory(keybase keyring.Keyring, cdc codec.BinaryCodec, accRetriever account.AccountRetriever,
txConfig TxConfig, ac address.Codec, conn gogogrpc.ClientConn, parameters TxParameters,
) (Factory, error) {
return Factory{
keybase: keybase,
cdc: cdc,
accountRetriever: accRetriever,
ac: ac,
conn: conn,
txConfig: txConfig,
txParams: parameters,
tx: txState{},
}, nil
}
// validateFlagSet checks the provided flags for consistency and requirements based on the operation mode.
func validateFlagSet(flags *pflag.FlagSet, offline bool) error {
if offline {
if !flags.Changed(flags2.FlagAccountNumber) || !flags.Changed(flags2.FlagSequence) {
return errors.New("account-number and sequence must be set in offline mode")
}
gas, _ := flags.GetString(flags2.FlagGas)
gasSetting, _ := flags2.ParseGasSetting(gas)
if gasSetting.Simulate {
return errors.New("simulate and offline flags cannot be set at the same time")
}
}
generateOnly, _ := flags.GetBool(flags2.FlagGenerateOnly)
chainID, _ := flags.GetString(flags2.FlagChainID)
if offline && generateOnly && chainID != "" {
return errors.New("chain ID cannot be used when offline and generate-only flags are set")
}
if chainID == "" {
return errors.New("chain ID required but not specified")
}
dryRun, _ := flags.GetBool(flags2.FlagDryRun)
if offline && dryRun {
return errors.New("dry-run: cannot use offline mode")
}
return nil
}
// prepareTxParams 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.
func prepareTxParams(parameters TxParameters, accRetriever account.AccountRetriever, offline bool) (TxParameters, error) {
if offline {
return parameters, nil
}
if len(parameters.address) == 0 {
return parameters, errors.New("missing 'from address' field")
}
if parameters.accountNumber == 0 || parameters.sequence == 0 {
num, seq, err := accRetriever.GetAccountNumberSequence(context.Background(), parameters.address)
if err != nil {
return parameters, err
}
if parameters.accountNumber == 0 {
parameters.accountNumber = num
}
if parameters.sequence == 0 {
parameters.sequence = seq
}
}
return parameters, 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 ...transaction.Msg) error {
fees := f.txParams.fees
isGasPriceZero, err := coins.IsZero(f.txParams.gasPrices)
if err != nil {
return err
}
if !isGasPriceZero {
areFeesZero, err := coins.IsZero(fees)
if err != nil {
return err
}
if !areFeesZero {
return errors.New("cannot provide both fees and gas prices")
}
// f.gas is an uint64 and we should convert to LegacyDec
// without the risk of under/overflow via uint64->int64.
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([]*base.Coin, len(f.txParams.gasPrices))
for i, gp := range f.txParams.gasPrices {
fee, err := math.LegacyNewDecFromStr(gp.Amount)
if err != nil {
return err
}
fee = fee.Mul(glDec)
fees[i] = &base.Coin{Denom: gp.Denom, Amount: fee.Ceil().RoundInt().String()}
}
}
if err := validateMemo(f.txParams.memo); err != nil {
return err
}
f.tx.msgs = msgs
f.tx.memo = f.txParams.memo
f.tx.fees = fees
f.tx.gasLimit = f.txParams.gas
f.tx.unordered = f.txParams.unordered
f.tx.timeoutTimestamp = f.txParams.timeoutTimestamp
err = f.setFeeGranter(f.txParams.feeGranter)
if err != nil {
return err
}
err = f.setFeePayer(f.txParams.feePayer)
if err != nil {
return err
}
return nil
}
func (f *Factory) BuildsSignedTx(ctx context.Context, msgs ...transaction.Msg) (Tx, error) {
err := f.BuildUnsignedTx(msgs...)
if err != nil {
return nil, err
}
return f.sign(ctx, true)
}
// calculateGas calculates the gas required for the given messages.
func (f *Factory) calculateGas(msgs ...transaction.Msg) error {
_, adjusted, err := f.Simulate(msgs...)
if err != nil {
return err
}
f.WithGas(adjusted)
return nil
}
// Simulate simulates the execution of a transaction and returns the
// simulation response obtained by the query and the adjusted gas amount.
func (f *Factory) Simulate(msgs ...transaction.Msg) (*apitx.SimulateResponse, uint64, error) {
txBytes, err := f.BuildSimTx(msgs...)
if err != nil {
return nil, 0, err
}
txSvcClient := apitx.NewServiceClient(f.conn)
simRes, err := txSvcClient.Simulate(context.Background(), &apitx.SimulateRequest{
TxBytes: txBytes,
})
if err != nil {
return nil, 0, err
}
return simRes, uint64(f.gasAdjustment() * float64(simRes.GasInfo.GasUsed)), nil
}
// UnsignedTxString 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) UnsignedTxString(msgs ...transaction.Msg) (string, error) {
if f.simulateAndExecute() {
err := f.calculateGas(msgs...)
if err != nil {
return "", err
}
}
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")
}
tx, err := f.getTx()
if err != nil {
return "", err
}
json, err := encoder(tx)
if err != nil {
return "", err
}
return fmt.Sprintf("%s\n", json), nil
}
// BuildSimTx creates an unsigned tx with an empty single signature and returns
// the encoded transaction or an error if the unsigned transaction cannot be
// built.
func (f *Factory) BuildSimTx(msgs ...transaction.Msg) ([]byte, error) {
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 := Signature{
PubKey: pk,
Data: f.getSimSignatureData(pk),
Sequence: f.sequence(),
}
if err := f.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")
}
tx, err := f.getTx()
if err != nil {
return nil, err
}
return encoder(tx)
}
// 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.
func (f *Factory) sign(ctx context.Context, overwriteSig bool) (Tx, error) {
if f.keybase == nil {
return nil, errors.New("keybase must be set prior to signing a transaction")
}
var err error
if f.txParams.signMode == apitxsigning.SignMode_SIGN_MODE_UNSPECIFIED {
f.txParams.signMode = f.txConfig.SignModeHandler().DefaultMode()
}
pubKey, err := f.keybase.GetPubKey(f.txParams.fromName)
if err != nil {
return nil, err
}
addr, err := f.ac.BytesToString(pubKey.Address())
if err != nil {
return nil, err
}
signerData := signing.SignerData{
ChainID: f.txParams.chainID,
AccountNumber: f.txParams.accountNumber,
Sequence: f.txParams.sequence,
PubKey: &anypb.Any{
TypeUrl: codectypes.MsgTypeURL(pubKey),
Value: pubKey.Bytes(),
},
Address: addr,
}
// For SIGN_MODE_DIRECT, we need to set the SignerInfos before generating
// the sign bytes. This is done by calling setSignatures with a nil
// signature, which in turn calls setSignerInfos internally.
//
// For SIGN_MODE_LEGACY_AMINO, this step is not strictly necessary,
// but we include it for consistency across all sign modes.
// It does not affect the generated sign bytes for LEGACY_AMINO.
//
// By setting the signatures here, we ensure that the correct SignerInfos
// are in place for all subsequent operations, regardless of the sign mode.
sigData := SingleSignatureData{
SignMode: f.txParams.signMode,
Signature: nil,
}
sig := Signature{
PubKey: pubKey,
Data: &sigData,
Sequence: f.txParams.sequence,
}
var prevSignatures []Signature
if !overwriteSig {
tx, err := f.getTx()
if err != nil {
return nil, err
}
prevSignatures, err = tx.GetSignatures()
if err != nil {
return nil, err
}
}
// Overwrite or append signer infos.
var sigs []Signature
if overwriteSig {
sigs = []Signature{sig}
} else {
sigs = append(sigs, prevSignatures...)
sigs = append(sigs, sig)
}
if err := f.setSignatures(sigs...); err != nil {
return nil, err
}
tx, err := f.getTx()
if err != nil {
return nil, err
}
if err := checkMultipleSigners(tx); err != nil {
return nil, err
}
bytesToSign, err := f.getSignBytesAdapter(ctx, signerData)
if err != nil {
return nil, err
}
// Sign those bytes
sigBytes, err := f.keybase.Sign(f.txParams.fromName, bytesToSign, f.txParams.signMode)
if err != nil {
return nil, err
}
// Construct the SignatureV2 struct
sigData = SingleSignatureData{
SignMode: f.signMode(),
Signature: sigBytes,
}
sig = Signature{
PubKey: pubKey,
Data: &sigData,
Sequence: f.txParams.sequence,
}
if overwriteSig {
err = f.setSignatures(sig)
} else {
prevSignatures = append(prevSignatures, sig)
err = f.setSignatures(prevSignatures...)
}
if err != nil {
return nil, fmt.Errorf("unable to set signatures on payload: %w", err)
}
return f.getTx()
}
// getSignBytesAdapter returns the sign bytes for a given transaction and sign mode.
func (f *Factory) getSignBytesAdapter(ctx context.Context, signerData signing.SignerData) ([]byte, error) {
txData, err := f.getSigningTxData()
if err != nil {
return nil, err
}
// Generate the bytes to be signed.
return f.txConfig.SignModeHandler().GetSignBytes(ctx, f.signMode(), signerData, *txData)
}
// WithGas returns a copy of the Factory with an updated gas value.
func (f *Factory) WithGas(gas uint64) {
f.txParams.gas = gas
}
// WithSequence returns a copy of the Factory with an updated sequence number.
func (f *Factory) WithSequence(sequence uint64) {
f.txParams.sequence = sequence
}
// WithAccountNumber returns a copy of the Factory with an updated account number.
func (f *Factory) WithAccountNumber(accnum uint64) {
f.txParams.accountNumber = accnum
}
// sequence returns the sequence number.
func (f *Factory) sequence() uint64 { return f.txParams.sequence }
// gasAdjustment returns the gas adjustment value.
func (f *Factory) gasAdjustment() float64 { return f.txParams.gasAdjustment }
// simulateAndExecute returns whether to simulate and execute.
func (f *Factory) simulateAndExecute() bool { return f.txParams.simulateAndExecute }
// signMode returns the sign mode.
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,
// 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{}
)
if f.txParams.simulateAndExecute && f.keybase != nil {
pk, err = f.keybase.GetPubKey(f.txParams.fromName)
if err != nil {
return nil, err
}
} else {
// When in dry-run mode, attempt to retrieve the account using the provided address.
// If the account retrieval fails, the default public key is used.
acc, err := f.accountRetriever.GetAccount(context.Background(), f.txParams.address)
if err != nil {
// If there is an error retrieving the account, return the default public key.
return pk, nil
}
// If the account is successfully retrieved, use its public key.
pk = acc.GetPubKey()
}
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) SignatureData {
multisigPubKey, ok := pk.(*multisig.LegacyAminoPubKey)
if !ok {
return &SingleSignatureData{SignMode: f.txParams.signMode}
}
multiSignatureData := make([]SignatureData, 0, multisigPubKey.Threshold)
for i := uint32(0); i < multisigPubKey.Threshold; i++ {
multiSignatureData = append(multiSignatureData, &SingleSignatureData{
SignMode: f.signMode(),
})
}
return &MultiSignatureData{
BitArray: &apicrypto.CompactBitArray{},
Signatures: multiSignatureData,
}
}
func (f *Factory) getTx() (*wrappedTx, error) {
msgs, err := msgsV1toAnyV2(f.tx.msgs)
if err != nil {
return nil, err
}
body := &apitx.TxBody{
Messages: msgs,
Memo: f.tx.memo,
TimeoutHeight: f.tx.timeoutHeight,
TimeoutTimestamp: timestamppb.New(f.tx.timeoutTimestamp),
Unordered: f.tx.unordered,
ExtensionOptions: f.tx.extensionOptions,
NonCriticalExtensionOptions: f.tx.nonCriticalExtensionOptions,
}
fee, err := f.getFee()
if err != nil {
return nil, err
}
authInfo := &apitx.AuthInfo{
SignerInfos: f.tx.signerInfos,
Fee: fee,
}
bodyBytes, err := marshalOption.Marshal(body)
if err != nil {
return nil, err
}
authInfoBytes, err := marshalOption.Marshal(authInfo)
if err != nil {
return nil, err
}
txRawBytes, err := marshalOption.Marshal(&apitx.TxRaw{
BodyBytes: bodyBytes,
AuthInfoBytes: authInfoBytes,
Signatures: f.tx.signatures,
})
if err != nil {
return nil, err
}
decodedTx, err := f.txConfig.Decoder().Decode(txRawBytes)
if err != nil {
return nil, err
}
return newWrapperTx(f.cdc, decodedTx), nil
}
// getSigningTxData returns a TxData with the current txState info.
func (f *Factory) getSigningTxData() (*signing.TxData, error) {
tx, err := f.getTx()
if err != nil {
return nil, err
}
return &signing.TxData{
Body: tx.Tx.Body,
AuthInfo: tx.Tx.AuthInfo,
BodyBytes: tx.TxRaw.BodyBytes,
AuthInfoBytes: tx.TxRaw.AuthInfoBytes,
BodyHasUnknownNonCriticals: tx.TxBodyHasUnknownNonCriticals,
}, nil
}
// setSignatures sets the signatures for the transaction builder.
// It takes a variable number of Signature arguments and processes each one to extract the mode information and raw signature.
// It also converts the public key to the appropriate format and sets the signer information.
func (f *Factory) setSignatures(signatures ...Signature) error {
n := len(signatures)
signerInfos := make([]*apitx.SignerInfo, n)
rawSignatures := make([][]byte, n)
for i, sig := range signatures {
var (
modeInfo *apitx.ModeInfo
pubKey *codectypes.Any
err error
anyPk *anypb.Any
)
modeInfo, rawSignatures[i] = signatureDataToModeInfoAndSig(sig.Data)
if sig.PubKey != nil {
pubKey, err = codectypes.NewAnyWithValue(sig.PubKey)
if err != nil {
return err
}
anyPk = &anypb.Any{
TypeUrl: pubKey.TypeUrl,
Value: pubKey.Value,
}
}
signerInfos[i] = &apitx.SignerInfo{
PublicKey: anyPk,
ModeInfo: modeInfo,
Sequence: sig.Sequence,
}
}
f.tx.signerInfos = signerInfos
f.tx.signatures = rawSignatures
return nil
}
// getFee computes the transaction fee information.
// It returns a pointer to an apitx.Fee struct containing the fee amount, gas limit, payer, and granter information.
// If the granter or payer addresses are set, it converts them from bytes to string using the addressCodec.
func (f *Factory) getFee() (fee *apitx.Fee, err error) {
granterStr := ""
if f.tx.granter != nil {
granterStr, err = f.ac.BytesToString(f.tx.granter)
if err != nil {
return nil, err
}
}
payerStr := ""
if f.tx.payer != nil {
payerStr, err = f.ac.BytesToString(f.tx.payer)
if err != nil {
return nil, err
}
}
fee = &apitx.Fee{
Amount: f.tx.fees,
GasLimit: f.tx.gasLimit,
Payer: payerStr,
Granter: granterStr,
}
return fee, nil
}
// setFeePayer sets the fee payer for the transaction.
func (f *Factory) setFeePayer(feePayer string) error {
if feePayer == "" {
return nil
}
addr, err := f.ac.StringToBytes(feePayer)
if err != nil {
return err
}
f.tx.payer = addr
return nil
}
// setFeeGranter sets the fee granter's address in the transaction builder.
// If the feeGranter string is empty, the function returns nil without setting an address.
// It converts the feeGranter string to bytes using the address codec and sets it as the granter address.
// Returns an error if the conversion fails.
func (f *Factory) setFeeGranter(feeGranter string) error {
if feeGranter == "" {
return nil
}
addr, err := f.ac.StringToBytes(feeGranter)
if err != nil {
return err
}
f.tx.granter = addr
return nil
}
// msgsV1toAnyV2 converts a slice of transaction.Msg (v1) to a slice of anypb.Any (v2).
// It first converts each transaction.Msg into a codectypes.Any and then converts
// these into anypb.Any.
func msgsV1toAnyV2(msgs []transaction.Msg) ([]*anypb.Any, error) {
anys := make([]*codectypes.Any, len(msgs))
for i, msg := range msgs {
anyMsg, err := codectypes.NewAnyWithValue(msg)
if err != nil {
return nil, err
}
anys[i] = anyMsg
}
return intoAnyV2(anys), nil
}
// intoAnyV2 converts a slice of codectypes.Any (v1) to a slice of anypb.Any (v2).
func intoAnyV2(v1s []*codectypes.Any) []*anypb.Any {
v2s := make([]*anypb.Any, len(v1s))
for i, v1 := range v1s {
v2s[i] = &anypb.Any{
TypeUrl: v1.TypeUrl,
Value: v1.Value,
}
}
return v2s
}
// checkMultipleSigners checks that there can be maximum one DIRECT signer in
// a tx.
func checkMultipleSigners(tx Tx) error {
directSigners := 0
sigsV2, err := tx.GetSignatures()
if err != nil {
return err
}
for _, sig := range sigsV2 {
directSigners += countDirectSigners(sig.Data)
if directSigners > 1 {
return errors.New("txs signed with CLI can have maximum 1 DIRECT signer")
}
}
return nil
}
// validateMemo validates the memo field.
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
}

View File

@ -0,0 +1,921 @@
package tx
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/anypb"
base "cosmossdk.io/api/cosmos/base/v1beta1"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"cosmossdk.io/core/transaction"
"cosmossdk.io/x/tx/signing"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
countertypes "github.com/cosmos/cosmos-sdk/testutil/x/counter/types"
)
var (
signer = "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9"
addr, _ = ac.StringToBytes(signer)
)
func TestFactory_prepareTxParams(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
error bool
}{
{
name: "no error",
txParams: TxParameters{
AccountConfig: AccountConfig{
address: addr,
},
},
},
{
name: "without account",
txParams: TxParameters{},
error: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
tt.txParams, err = prepareTxParams(tt.txParams, mockAccountRetriever{}, false)
if (err != nil) != tt.error {
t.Errorf("Prepare() error = %v, wantErr %v", err, tt.error)
}
})
}
}
func TestFactory_BuildUnsignedTx(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
msgs []transaction.Msg
error bool
}{
{
name: "no error",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
address: addr,
},
},
msgs: []transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: signer,
Count: 0,
},
},
},
{
name: "fees and gas price provided",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
address: addr,
},
GasConfig: GasConfig{
gasPrices: []*base.DecCoin{
{
Amount: "1000",
Denom: "stake",
},
},
},
FeeConfig: FeeConfig{
fees: []*base.Coin{
{
Amount: "1000",
Denom: "stake",
},
},
},
},
msgs: []transaction.Msg{},
error: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
err = f.BuildUnsignedTx(tt.msgs...)
if tt.error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Nil(t, f.tx.signatures)
require.Nil(t, f.tx.signerInfos)
}
})
}
}
func TestFactory_calculateGas(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
msgs []transaction.Msg
error bool
}{
{
name: "no error",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
address: addr,
},
GasConfig: GasConfig{
gasAdjustment: 1,
},
},
msgs: []transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: signer,
Count: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
err = f.calculateGas(tt.msgs...)
if tt.error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotZero(t, f.txParams.GasConfig)
}
})
}
}
func TestFactory_Simulate(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
msgs []transaction.Msg
error bool
}{
{
name: "no error",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
address: addr,
},
GasConfig: GasConfig{
gasAdjustment: 1,
},
},
msgs: []transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: signer,
Count: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
got, got1, err := f.Simulate(tt.msgs...)
if tt.error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, got)
require.NotZero(t, got1)
}
})
}
}
func TestFactory_BuildSimTx(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
msgs []transaction.Msg
want []byte
error bool
}{
{
name: "no error",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
address: addr,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
got, err := f.BuildSimTx(tt.msgs...)
if tt.error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, got)
}
})
}
}
func TestFactory_Sign(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
wantErr bool
}{
{
name: "no error",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
fromName: "alice",
address: addr,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(setKeyring(), cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
err = f.BuildUnsignedTx([]transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: signer,
Count: 0,
},
}...)
require.NoError(t, err)
require.Nil(t, f.tx.signatures)
require.Nil(t, f.tx.signerInfos)
tx, err := f.sign(context.Background(), true)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
sigs, err := tx.GetSignatures()
require.NoError(t, err)
require.NotNil(t, sigs)
require.NotNil(t, f.tx.signerInfos)
}
})
}
}
func TestFactory_getSignBytesAdapter(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
error bool
}{
{
name: "no error",
txParams: TxParameters{
chainID: "demo",
signMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
AccountConfig: AccountConfig{
address: addr,
},
},
},
{
name: "signMode not specified",
txParams: TxParameters{
chainID: "demo",
AccountConfig: AccountConfig{
address: addr,
},
},
error: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(setKeyring(), cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
err = f.BuildUnsignedTx([]transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: signer,
Count: 0,
},
}...)
require.NoError(t, err)
pk, err := f.keybase.GetPubKey("alice")
require.NoError(t, err)
require.NotNil(t, pk)
addr, err := f.ac.BytesToString(pk.Address())
require.NoError(t, err)
require.NotNil(t, addr)
signerData := signing.SignerData{
Address: addr,
ChainID: f.txParams.chainID,
AccountNumber: 0,
Sequence: 0,
PubKey: &anypb.Any{
TypeUrl: codectypes.MsgTypeURL(pk),
Value: pk.Bytes(),
},
}
got, err := f.getSignBytesAdapter(context.Background(), signerData)
if tt.error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, got)
}
})
}
}
func Test_validateMemo(t *testing.T) {
tests := []struct {
name string
memo string
wantErr bool
}{
{
name: "empty memo",
memo: "",
},
{
name: "valid memo",
memo: "11245",
},
{
name: "invalid Memo",
memo: "echo echo echo echo echo echo echo echo echo echo echo echo echo echo echo",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateMemo(tt.memo); (err != nil) != tt.wantErr {
t.Errorf("validateMemo() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestFactory_WithFunctions(t *testing.T) {
tests := []struct {
name string
txParams TxParameters
withFunc func(*Factory)
checkFunc func(*Factory) bool
}{
{
name: "with gas",
txParams: TxParameters{
AccountConfig: AccountConfig{
address: addr,
},
},
withFunc: func(f *Factory) {
f.WithGas(1000)
},
checkFunc: func(f *Factory) bool {
return f.txParams.GasConfig.gas == 1000
},
},
{
name: "with sequence",
txParams: TxParameters{
AccountConfig: AccountConfig{
address: addr,
},
},
withFunc: func(f *Factory) {
f.WithSequence(10)
},
checkFunc: func(f *Factory) bool {
return f.txParams.AccountConfig.sequence == 10
},
},
{
name: "with account number",
txParams: TxParameters{
AccountConfig: AccountConfig{
address: addr,
},
},
withFunc: func(f *Factory) {
f.WithAccountNumber(123)
},
checkFunc: func(f *Factory) bool {
return f.txParams.AccountConfig.accountNumber == 123
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(setKeyring(), cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, tt.txParams)
require.NoError(t, err)
require.NotNil(t, f)
tt.withFunc(&f)
require.True(t, tt.checkFunc(&f))
})
}
}
func TestFactory_getTx(t *testing.T) {
tests := []struct {
name string
txSetter func(f *Factory)
checkResult func(Tx)
}{
{
name: "empty tx",
txSetter: func(f *Factory) {
},
checkResult: func(tx Tx) {
wTx, ok := tx.(*wrappedTx)
require.True(t, ok)
// require.Equal(t, []*anypb.Any(nil), wTx.Tx.Body.Messages)
require.Nil(t, wTx.Tx.Body.Messages)
require.Empty(t, wTx.Tx.Body.Memo)
require.Equal(t, uint64(0), wTx.Tx.Body.TimeoutHeight)
require.Equal(t, wTx.Tx.Body.Unordered, false)
require.Nil(t, wTx.Tx.Body.ExtensionOptions)
require.Nil(t, wTx.Tx.Body.NonCriticalExtensionOptions)
require.Nil(t, wTx.Tx.AuthInfo.SignerInfos)
require.Nil(t, wTx.Tx.AuthInfo.Fee.Amount)
require.Equal(t, uint64(0), wTx.Tx.AuthInfo.Fee.GasLimit)
require.Empty(t, wTx.Tx.AuthInfo.Fee.Payer)
require.Empty(t, wTx.Tx.AuthInfo.Fee.Granter)
require.Nil(t, wTx.Tx.Signatures)
},
},
{
name: "full tx",
txSetter: func(f *Factory) {
pk := secp256k1.GenPrivKey().PubKey()
addr, _ := f.ac.BytesToString(pk.Address())
f.tx.msgs = []transaction.Msg{&countertypes.MsgIncreaseCounter{
Signer: addr,
Count: 0,
}}
err := f.setFeePayer(addr)
require.NoError(t, err)
f.tx.fees = []*base.Coin{{
Denom: "cosmos",
Amount: "1000",
}}
err = f.setSignatures([]Signature{{
PubKey: pk,
Data: &SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: nil,
},
Sequence: 0,
}}...)
require.NoError(t, err)
},
checkResult: func(tx Tx) {
wTx, ok := tx.(*wrappedTx)
require.True(t, ok)
require.True(t, len(wTx.Tx.Body.Messages) == 1)
require.NotNil(t, wTx.Tx.AuthInfo.SignerInfos)
require.NotNil(t, wTx.Tx.AuthInfo.Fee.Amount)
require.NotNil(t, wTx.Tx.Signatures)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
tt.txSetter(&f)
got, err := f.getTx()
require.NoError(t, err)
require.NotNil(t, got)
tt.checkResult(got)
})
}
}
func TestFactory_getFee(t *testing.T) {
tests := []struct {
name string
feeAmount []*base.Coin
feeGranter string
feePayer string
}{
{
name: "get fee with payer",
feeAmount: []*base.Coin{
{
Denom: "cosmos",
Amount: "1000",
},
},
feeGranter: "",
feePayer: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
},
{
name: "get fee with granter",
feeAmount: []*base.Coin{
{
Denom: "cosmos",
Amount: "1000",
},
},
feeGranter: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
feePayer: "",
},
{
name: "get fee with granter and granter",
feeAmount: []*base.Coin{
{
Denom: "cosmos",
Amount: "1000",
},
},
feeGranter: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
feePayer: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
f.tx.fees = tt.feeAmount
err = f.setFeeGranter(tt.feeGranter)
require.NoError(t, err)
err = f.setFeePayer(tt.feePayer)
require.NoError(t, err)
fee, err := f.getFee()
require.NoError(t, err)
require.NotNil(t, fee)
require.Equal(t, fee.Amount, tt.feeAmount)
require.Equal(t, fee.Granter, tt.feeGranter)
require.Equal(t, fee.Payer, tt.feePayer)
})
}
}
func TestFactory_getSigningTxData(t *testing.T) {
tests := []struct {
name string
txSetter func(f *Factory)
}{
{
name: "empty tx",
txSetter: func(f *Factory) {},
},
{
name: "full tx",
txSetter: func(f *Factory) {
pk := secp256k1.GenPrivKey().PubKey()
addr, _ := ac.BytesToString(pk.Address())
f.tx.msgs = []transaction.Msg{&countertypes.MsgIncreaseCounter{
Signer: addr,
Count: 0,
}}
err := f.setFeePayer(addr)
require.NoError(t, err)
f.tx.fees = []*base.Coin{{
Denom: "cosmos",
Amount: "1000",
}}
err = f.setSignatures([]Signature{{
PubKey: pk,
Data: &SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
Sequence: 0,
}}...)
require.NoError(t, err)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
tt.txSetter(&f)
got, err := f.getSigningTxData()
require.NoError(t, err)
require.NotNil(t, got)
})
}
}
func TestFactoryr_setMsgs(t *testing.T) {
tests := []struct {
name string
msgs []transaction.Msg
wantErr bool
}{
{
name: "set msgs",
msgs: []transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
Count: 0,
},
&countertypes.MsgIncreaseCounter{
Signer: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
Count: 1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
f.tx.msgs = tt.msgs
require.NoError(t, err)
require.Equal(t, len(tt.msgs), len(f.tx.msgs))
for i, msg := range tt.msgs {
require.Equal(t, msg, f.tx.msgs[i])
}
})
}
}
func TestFactory_SetMemo(t *testing.T) {
tests := []struct {
name string
memo string
}{
{
name: "set memo",
memo: "test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
f.tx.memo = tt.memo
require.Equal(t, f.tx.memo, tt.memo)
})
}
}
func TestFactory_SetFeeAmount(t *testing.T) {
tests := []struct {
name string
coins []*base.Coin
}{
{
name: "set coins",
coins: []*base.Coin{
{
Denom: "cosmos",
Amount: "1000",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
f.tx.fees = tt.coins
require.Equal(t, len(tt.coins), len(f.tx.fees))
for i, coin := range tt.coins {
require.Equal(t, coin.Amount, f.tx.fees[i].Amount)
}
})
}
}
func TestFactory_SetGasLimit(t *testing.T) {
tests := []struct {
name string
gasLimit uint64
}{
{
name: "set gas limit",
gasLimit: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
f.tx.gasLimit = tt.gasLimit
require.Equal(t, f.tx.gasLimit, tt.gasLimit)
})
}
}
func TestFactory_SetUnordered(t *testing.T) {
tests := []struct {
name string
unordered bool
}{
{
name: "unordered",
unordered: true,
},
{
name: "not unordered",
unordered: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
f.tx.unordered = tt.unordered
require.Equal(t, f.tx.unordered, tt.unordered)
})
}
}
func TestFactory_setSignatures(t *testing.T) {
tests := []struct {
name string
signatures func() []Signature
}{
{
name: "set empty single signature",
signatures: func() []Signature {
return []Signature{{
PubKey: secp256k1.GenPrivKey().PubKey(),
Data: &SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: nil,
},
Sequence: 0,
}}
},
},
{
name: "set single signature",
signatures: func() []Signature {
return []Signature{{
PubKey: secp256k1.GenPrivKey().PubKey(),
Data: &SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
Sequence: 0,
}}
},
},
{
name: "set empty multi signature",
signatures: func() []Signature {
return []Signature{{
PubKey: multisig.NewLegacyAminoPubKey(1, []cryptotypes.PubKey{secp256k1.GenPrivKey().PubKey()}),
Data: &MultiSignatureData{
BitArray: nil,
Signatures: []SignatureData{
&SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: nil,
},
},
},
Sequence: 0,
}}
},
},
{
name: "set multi signature",
signatures: func() []Signature {
return []Signature{{
PubKey: multisig.NewLegacyAminoPubKey(1, []cryptotypes.PubKey{secp256k1.GenPrivKey().PubKey()}),
Data: &MultiSignatureData{
BitArray: nil,
Signatures: []SignatureData{
&SingleSignatureData{
SignMode: apitxsigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
},
},
Sequence: 0,
}}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cryptocodec.RegisterInterfaces(cdc.InterfaceRegistry())
f, err := NewFactory(keybase, cdc, mockAccountRetriever{}, txConf, ac, mockClientConn{}, TxParameters{})
require.NoError(t, err)
sigs := tt.signatures()
err = f.setSignatures(sigs...)
require.NoError(t, err)
tx, err := f.getTx()
require.NoError(t, err)
signatures, err := tx.GetSignatures()
require.NoError(t, err)
require.Equal(t, len(sigs), len(signatures))
for i := range signatures {
require.Equal(t, sigs[i].PubKey, signatures[i].PubKey)
}
})
}
}
///////////////////////
func Test_msgsV1toAnyV2(t *testing.T) {
tests := []struct {
name string
msgs []transaction.Msg
}{
{
name: "convert msgV1 to V2",
msgs: []transaction.Msg{
&countertypes.MsgIncreaseCounter{
Signer: "cosmos1zglwfu6xjzvzagqcmvzewyzjp9xwqw5qwrr8n9",
Count: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := msgsV1toAnyV2(tt.msgs)
require.NoError(t, err)
require.NotNil(t, got)
})
}
}
func Test_intoAnyV2(t *testing.T) {
tests := []struct {
name string
msgs []*codectypes.Any
}{
{
name: "any to v2",
msgs: []*codectypes.Any{
{
TypeUrl: "/random/msg",
Value: []byte("random message"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := intoAnyV2(tt.msgs)
require.NotNil(t, got)
require.Equal(t, len(got), len(tt.msgs))
for i, msg := range got {
require.Equal(t, msg.TypeUrl, tt.msgs[i].TypeUrl)
require.Equal(t, msg.Value, tt.msgs[i].Value)
}
})
}
}

52
client/v2/tx/flags.go Normal file
View File

@ -0,0 +1,52 @@
package tx
import (
"fmt"
"strconv"
)
// Flag constants for transaction-related flags
const (
defaultGasLimit = 200000
gasFlagAuto = "auto"
flagTimeoutTimestamp = "timeout-timestamp"
flagChainID = "chain-id"
flagNote = "note"
flagSignMode = "sign-mode"
flagAccountNumber = "account-number"
flagSequence = "sequence"
flagFrom = "from"
flagDryRun = "dry-run"
flagGas = "gas"
flagGasAdjustment = "gas-adjustment"
flagGasPrices = "gas-prices"
flagFees = "fees"
flagFeePayer = "fee-payer"
flagFeeGranter = "fee-granter"
flagUnordered = "unordered"
flagOffline = "offline"
flagGenerateOnly = "generate-only"
)
// parseGasSetting parses a string gas value. The value may either be 'auto',
// which indicates a transaction should be executed in simulate mode to
// automatically find a sufficient gas value, or a string integer. It returns an
// error if a string integer is provided which cannot be parsed.
func parseGasSetting(gasStr string) (bool, uint64, error) {
switch gasStr {
case "":
return false, defaultGasLimit, nil
case gasFlagAuto:
return true, 0, nil
default:
gas, err := strconv.ParseUint(gasStr, 10, 64)
if err != nil {
return false, 0, fmt.Errorf("gas must be either integer or %s", gasFlagAuto)
}
return false, gas, nil
}
}

197
client/v2/tx/signature.go Normal file
View File

@ -0,0 +1,197 @@
package tx
import (
"errors"
"fmt"
apicrypto "cosmossdk.io/api/cosmos/crypto/multisig/v1beta1"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
apitx "cosmossdk.io/api/cosmos/tx/v1beta1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)
// Signature holds the necessary components to verify transaction signatures.
type Signature struct {
PubKey cryptotypes.PubKey // Public key for signature verification.
Data SignatureData // Signature data containing the actual signatures.
Sequence uint64 // Account sequence, relevant for SIGN_MODE_DIRECT.
}
// SignatureData defines an interface for different signature data types.
type SignatureData interface {
isSignatureData()
}
// SingleSignatureData stores a single signer's signature and its mode.
type SingleSignatureData struct {
SignMode apitxsigning.SignMode // Mode of the signature.
Signature []byte // Actual binary signature.
}
// MultiSignatureData encapsulates signatures from a multisig transaction.
type MultiSignatureData struct {
BitArray *apicrypto.CompactBitArray // Bitmap of signers.
Signatures []SignatureData // Individual signatures.
}
func (m *SingleSignatureData) isSignatureData() {}
func (m *MultiSignatureData) isSignatureData() {}
// signatureDataToModeInfoAndSig converts SignatureData to ModeInfo and its corresponding raw signature.
func signatureDataToModeInfoAndSig(data SignatureData) (*apitx.ModeInfo, []byte) {
if data == nil {
return nil, nil
}
switch data := data.(type) {
case *SingleSignatureData:
return &apitx.ModeInfo{
Sum: &apitx.ModeInfo_Single_{
Single: &apitx.ModeInfo_Single{Mode: data.SignMode},
},
}, data.Signature
case *MultiSignatureData:
modeInfos := make([]*apitx.ModeInfo, len(data.Signatures))
sigs := make([][]byte, len(data.Signatures))
for i, d := range data.Signatures {
modeInfos[i], sigs[i] = signatureDataToModeInfoAndSig(d)
}
multisig := cryptotypes.MultiSignature{Signatures: sigs}
sig, err := multisig.Marshal()
if err != nil {
panic(err)
}
return &apitx.ModeInfo{
Sum: &apitx.ModeInfo_Multi_{
Multi: &apitx.ModeInfo_Multi{
Bitarray: data.BitArray,
ModeInfos: modeInfos,
},
},
}, sig
default:
panic(fmt.Sprintf("unexpected signature data type %T", data))
}
}
// modeInfoAndSigToSignatureData converts ModeInfo and a raw signature to SignatureData.
func modeInfoAndSigToSignatureData(modeInfo *apitx.ModeInfo, sig []byte) (SignatureData, error) {
switch mi := modeInfo.Sum.(type) {
case *apitx.ModeInfo_Single_:
return &SingleSignatureData{
SignMode: mi.Single.Mode,
Signature: sig,
}, nil
case *apitx.ModeInfo_Multi_:
multi := mi.Multi
sigs, err := decodeMultiSignatures(sig)
if err != nil {
return nil, err
}
sigsV2 := make([]SignatureData, len(sigs))
for i, mi := range multi.ModeInfos {
sigsV2[i], err = modeInfoAndSigToSignatureData(mi, sigs[i])
if err != nil {
return nil, err
}
}
return &MultiSignatureData{
BitArray: multi.Bitarray,
Signatures: sigsV2,
}, nil
}
return nil, fmt.Errorf("unsupported ModeInfo type %T", modeInfo)
}
// decodeMultiSignatures decodes a byte array into individual signatures.
func decodeMultiSignatures(bz []byte) ([][]byte, error) {
multisig := cryptotypes.MultiSignature{}
err := multisig.Unmarshal(bz)
if err != nil {
return nil, err
}
if len(multisig.XXX_unrecognized) > 0 {
return nil, errors.New("unrecognized fields in MultiSignature")
}
return multisig.Signatures, nil
}
// signatureDataToProto converts a SignatureData interface to a protobuf SignatureDescriptor_Data.
// This function supports both SingleSignatureData and MultiSignatureData types.
// For SingleSignatureData, it directly maps the signature mode and signature bytes to the protobuf structure.
// For MultiSignatureData, it recursively converts each signature in the collection to the corresponding protobuf structure.
func signatureDataToProto(data SignatureData) (*apitxsigning.SignatureDescriptor_Data, error) {
switch data := data.(type) {
case *SingleSignatureData:
// Handle single signature data conversion.
return &apitxsigning.SignatureDescriptor_Data{
Sum: &apitxsigning.SignatureDescriptor_Data_Single_{
Single: &apitxsigning.SignatureDescriptor_Data_Single{
Mode: data.SignMode,
Signature: data.Signature,
},
},
}, nil
case *MultiSignatureData:
var err error
descDatas := make([]*apitxsigning.SignatureDescriptor_Data, len(data.Signatures))
for i, j := range data.Signatures {
descDatas[i], err = signatureDataToProto(j)
if err != nil {
return nil, err
}
}
return &apitxsigning.SignatureDescriptor_Data{
Sum: &apitxsigning.SignatureDescriptor_Data_Multi_{
Multi: &apitxsigning.SignatureDescriptor_Data_Multi{
Bitarray: data.BitArray,
Signatures: descDatas,
},
},
}, nil
}
// Return an error if the data type is not supported.
return nil, fmt.Errorf("unexpected signature data type %T", data)
}
// SignatureDataFromProto converts a protobuf SignatureDescriptor_Data to a SignatureData interface.
// This function supports both Single and Multi signature data types.
func SignatureDataFromProto(descData *apitxsigning.SignatureDescriptor_Data) (SignatureData, error) {
switch descData := descData.Sum.(type) {
case *apitxsigning.SignatureDescriptor_Data_Single_:
return &SingleSignatureData{
SignMode: descData.Single.Mode,
Signature: descData.Single.Signature,
}, nil
case *apitxsigning.SignatureDescriptor_Data_Multi_:
var err error
multi := descData.Multi
data := make([]SignatureData, len(multi.Signatures))
for i, j := range multi.Signatures {
data[i], err = SignatureDataFromProto(j)
if err != nil {
return nil, err
}
}
return &MultiSignatureData{
BitArray: multi.Bitarray,
Signatures: data,
}, nil
}
return nil, fmt.Errorf("unexpected signature data type %T", descData)
}

View File

@ -0,0 +1,143 @@
package tx
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
apimultisig "cosmossdk.io/api/cosmos/crypto/multisig/v1beta1"
apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
apitx "cosmossdk.io/api/cosmos/tx/v1beta1"
)
func TestSignatureDataToModeInfoAndSig(t *testing.T) {
tests := []struct {
name string
data SignatureData
mIResult *apitx.ModeInfo
sigResult []byte
}{
{
name: "single signature",
data: &SingleSignatureData{
SignMode: apisigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
mIResult: &apitx.ModeInfo{
Sum: &apitx.ModeInfo_Single_{
Single: &apitx.ModeInfo_Single{Mode: apisigning.SignMode_SIGN_MODE_DIRECT},
},
},
sigResult: []byte("signature"),
},
{
name: "multi signature",
data: &MultiSignatureData{
BitArray: nil,
Signatures: []SignatureData{
&SingleSignatureData{
SignMode: apisigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
},
},
mIResult: &apitx.ModeInfo{
Sum: &apitx.ModeInfo_Multi_{
Multi: &apitx.ModeInfo_Multi{
Bitarray: nil,
ModeInfos: []*apitx.ModeInfo{
{
Sum: &apitx.ModeInfo_Single_{
Single: &apitx.ModeInfo_Single{Mode: apisigning.SignMode_SIGN_MODE_DIRECT},
},
},
},
},
},
},
sigResult: []byte("\n\tsignature"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
modeInfo, signature := signatureDataToModeInfoAndSig(tt.data)
require.Equal(t, tt.mIResult, modeInfo)
require.Equal(t, tt.sigResult, signature)
})
}
}
func TestModeInfoAndSigToSignatureData(t *testing.T) {
type args struct {
modeInfo func() *apitx.ModeInfo
sig []byte
}
tests := []struct {
name string
args args
want SignatureData
wantErr bool
}{
{
name: "to SingleSignatureData",
args: args{
modeInfo: func() *apitx.ModeInfo {
return &apitx.ModeInfo{
Sum: &apitx.ModeInfo_Single_{
Single: &apitx.ModeInfo_Single{Mode: apisigning.SignMode_SIGN_MODE_DIRECT},
},
}
},
sig: []byte("signature"),
},
want: &SingleSignatureData{
SignMode: apisigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
},
{
name: "to MultiSignatureData",
args: args{
modeInfo: func() *apitx.ModeInfo {
return &apitx.ModeInfo{
Sum: &apitx.ModeInfo_Multi_{
Multi: &apitx.ModeInfo_Multi{
Bitarray: &apimultisig.CompactBitArray{},
ModeInfos: []*apitx.ModeInfo{
{
Sum: &apitx.ModeInfo_Single_{
Single: &apitx.ModeInfo_Single{Mode: apisigning.SignMode_SIGN_MODE_DIRECT},
},
},
},
},
},
}
},
sig: []byte("\n\tsignature"),
},
want: &MultiSignatureData{ // Changed from SingleSignatureData to MultiSignatureData
BitArray: &apimultisig.CompactBitArray{},
Signatures: []SignatureData{
&SingleSignatureData{
SignMode: apisigning.SignMode_SIGN_MODE_DIRECT,
Signature: []byte("signature"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := modeInfoAndSigToSignatureData(tt.args.modeInfo(), tt.args.sig)
if (err != nil) != tt.wantErr {
t.Errorf("ModeInfoAndSigToSignatureData() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ModeInfoAndSigToSignatureData() got = %v, want %v", got, tt.want)
}
})
}
}

229
client/v2/tx/tx.go Normal file
View File

@ -0,0 +1,229 @@
package tx
import (
"bufio"
"errors"
"fmt"
"os"
"github.com/cosmos/gogoproto/proto"
"github.com/spf13/pflag"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"cosmossdk.io/client/v2/internal/account"
"cosmossdk.io/core/transaction"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/input"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
)
// GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction
// or sign it and broadcast it returning an error upon failure.
func GenerateOrBroadcastTxCLI(ctx client.Context, flagSet *pflag.FlagSet, msgs ...transaction.Msg) error {
if err := validateMessages(msgs...); err != nil {
return err
}
txf, err := newFactory(ctx, flagSet)
if err != nil {
return err
}
genOnly, _ := flagSet.GetBool(flagGenerateOnly)
if genOnly {
return generateOnly(ctx, txf, msgs...)
}
isDryRun, _ := flagSet.GetBool(flagDryRun)
if isDryRun {
return dryRun(txf, msgs...)
}
return BroadcastTx(ctx, txf, msgs...)
}
// newFactory creates a new transaction Factory based on the provided context and flag set.
// It initializes a new CLI keyring, extracts transaction parameters from the flag set,
// configures transaction settings, and sets up an account retriever for the transaction Factory.
func newFactory(ctx client.Context, flagSet *pflag.FlagSet) (Factory, error) {
k, err := keyring.NewAutoCLIKeyring(ctx.Keyring, ctx.AddressCodec)
if err != nil {
return Factory{}, err
}
txConfig, err := NewTxConfig(ConfigOptions{
AddressCodec: ctx.AddressCodec,
Cdc: ctx.Codec,
ValidatorAddressCodec: ctx.ValidatorAddressCodec,
EnablesSignModes: ctx.TxConfig.SignModeHandler().SupportedModes(),
})
if err != nil {
return Factory{}, err
}
accRetriever := account.NewAccountRetriever(ctx.AddressCodec, ctx, ctx.InterfaceRegistry)
txf, err := NewFactoryFromFlagSet(flagSet, k, ctx.Codec, accRetriever, txConfig, ctx.AddressCodec, ctx)
if err != nil {
return Factory{}, err
}
return txf, nil
}
// validateMessages validates 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
func validateMessages(msgs ...transaction.Msg) error {
for _, msg := range msgs {
m, ok := msg.(HasValidateBasic)
if !ok {
continue
}
if err := m.ValidateBasic(); err != nil {
return err
}
}
return nil
}
// generateOnly prepares the transaction and prints the unsigned transaction string.
// It first calls Prepare on the transaction factory to set up any necessary pre-conditions.
// If preparation is successful, it generates an unsigned transaction string using the provided messages.
func generateOnly(ctx client.Context, txf Factory, msgs ...transaction.Msg) error {
uTx, err := txf.UnsignedTxString(msgs...)
if err != nil {
return err
}
return ctx.PrintString(uTx)
}
// dryRun performs a dry run of the transaction to estimate the gas required.
// It prepares the transaction factory and simulates the transaction with the provided messages.
func dryRun(txf Factory, msgs ...transaction.Msg) error {
_, gas, err := txf.Simulate(msgs...)
if err != nil {
return err
}
_, err = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: gas})
return err
}
// SimulateTx simulates a tx and returns the simulation response obtained by the query.
func SimulateTx(ctx client.Context, flagSet *pflag.FlagSet, msgs ...transaction.Msg) (proto.Message, error) {
txf, err := newFactory(ctx, flagSet)
if err != nil {
return nil, err
}
simulation, _, err := txf.Simulate(msgs...)
return simulation, err
}
// 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 ...transaction.Msg) error {
if txf.simulateAndExecute() {
err := txf.calculateGas(msgs...)
if err != nil {
return err
}
}
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")
}
unsigTx, err := txf.getTx()
if err != nil {
return err
}
txBytes, err := encoder(unsigTx)
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
}
}
signedTx, err := txf.sign(clientCtx.CmdContext, true)
if err != nil {
return err
}
txBytes, err := txf.txConfig.TxEncoder()(signedTx)
if err != nil {
return err
}
// broadcast to a CometBFT node
res, err := clientCtx.BroadcastTx(txBytes)
if err != nil {
return err
}
return clientCtx.PrintProto(res)
}
// countDirectSigners counts the number of DIRECT signers in a signature data.
func countDirectSigners(sigData SignatureData) int {
switch data := sigData.(type) {
case *SingleSignatureData:
if data.SignMode == apitxsigning.SignMode_SIGN_MODE_DIRECT {
return 1
}
return 0
case *MultiSignatureData:
directSigners := 0
for _, d := range data.Signatures {
directSigners += countDirectSigners(d)
}
return directSigners
default:
panic("unreachable case")
}
}
// getSignMode returns the corresponding apitxsigning.SignMode based on the provided mode string.
func getSignMode(mode string) apitxsigning.SignMode {
switch mode {
case "direct":
return apitxsigning.SignMode_SIGN_MODE_DIRECT
case "direct-aux":
return apitxsigning.SignMode_SIGN_MODE_DIRECT_AUX
case "amino-json":
return apitxsigning.SignMode_SIGN_MODE_LEGACY_AMINO_JSON
case "textual":
return apitxsigning.SignMode_SIGN_MODE_TEXTUAL
}
return apitxsigning.SignMode_SIGN_MODE_UNSPECIFIED
}

214
client/v2/tx/types.go Normal file
View File

@ -0,0 +1,214 @@
package tx
import (
"fmt"
"time"
"github.com/spf13/pflag"
"google.golang.org/protobuf/types/known/anypb"
base "cosmossdk.io/api/cosmos/base/v1beta1"
apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
apitx "cosmossdk.io/api/cosmos/tx/v1beta1"
keyring2 "cosmossdk.io/client/v2/autocli/keyring"
"cosmossdk.io/client/v2/internal/coins"
"cosmossdk.io/core/address"
"cosmossdk.io/core/transaction"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)
// HasValidateBasic is a copy of types.HasValidateBasic to avoid sdk import.
type HasValidateBasic interface {
// ValidateBasic does a simple validation check that
// doesn't require access to any other information.
ValidateBasic() error
}
// TxParameters defines the parameters required for constructing a transaction.
type TxParameters struct {
timeoutTimestamp time.Time // timeoutTimestamp indicates a timestamp after which the transaction is no longer valid.
chainID string // chainID specifies the unique identifier of the blockchain where the transaction will be processed.
memo string // memo contains any arbitrary memo to be attached to the transaction.
signMode apitxsigning.SignMode // signMode determines the signing mode to be used for the transaction.
AccountConfig // AccountConfig includes information about the transaction originator's account.
GasConfig // GasConfig specifies the gas settings for the transaction.
FeeConfig // FeeConfig details the fee associated with the transaction.
ExecutionOptions // ExecutionOptions includes settings that modify how the transaction is executed.
}
// AccountConfig defines the 'account' related fields in a transaction.
type AccountConfig struct {
// accountNumber is the unique identifier for the account.
accountNumber uint64
// sequence is the sequence number of the transaction.
sequence uint64
// fromName is the name of the account sending the transaction.
fromName string
// fromAddress is the address of the account sending the transaction.
fromAddress string
// address is the byte representation of the account address.
address []byte
}
// GasConfig defines the 'gas' related fields in a transaction.
// GasConfig defines the gas-related settings for a transaction.
type GasConfig struct {
gas uint64 // gas is the amount of gas requested for the transaction.
gasAdjustment float64 // gasAdjustment is the factor by which the estimated gas is multiplied to calculate the final gas limit.
gasPrices []*base.DecCoin // gasPrices is a list of denominations of DecCoin used to calculate the fee paid for the gas.
}
// NewGasConfig creates a new GasConfig with the specified gas, gasAdjustment, and gasPrices.
// If the provided gas value is zero, it defaults to a predefined value (defaultGas).
// The gasPrices string is parsed into a slice of DecCoin.
func NewGasConfig(gas uint64, gasAdjustment float64, gasPrices string) (GasConfig, error) {
parsedGasPrices, err := coins.ParseDecCoins(gasPrices)
if err != nil {
return GasConfig{}, err
}
return GasConfig{
gas: gas,
gasAdjustment: gasAdjustment,
gasPrices: parsedGasPrices,
}, nil
}
// FeeConfig holds the fee details for a transaction.
type FeeConfig struct {
fees []*base.Coin // fees are the amounts paid for the transaction.
feePayer string // feePayer is the account responsible for paying the fees.
feeGranter string // feeGranter is the account granting the fee payment if different from the payer.
}
// NewFeeConfig creates a new FeeConfig with the specified fees, feePayer, and feeGranter.
// It parses the fees string into a slice of Coin, handling normalization.
func NewFeeConfig(fees, feePayer, feeGranter string) (FeeConfig, error) {
parsedFees, err := coins.ParseCoinsNormalized(fees)
if err != nil {
return FeeConfig{}, err
}
return FeeConfig{
fees: parsedFees,
feePayer: feePayer,
feeGranter: feeGranter,
}, nil
}
// ExecutionOptions defines the transaction execution options ran by the client
type ExecutionOptions struct {
unordered bool // unordered indicates if the transaction execution order is not guaranteed.
simulateAndExecute bool // simulateAndExecute indicates if the transaction should be simulated before execution.
}
// 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)
}
// txState represents the internal state of a transaction.
type txState struct {
msgs []transaction.Msg
timeoutHeight uint64
timeoutTimestamp time.Time
granter []byte
payer []byte
unordered bool
memo string
gasLimit uint64
fees []*base.Coin
signerInfos []*apitx.SignerInfo
signatures [][]byte
extensionOptions []*anypb.Any
nonCriticalExtensionOptions []*anypb.Any
}
// Tx defines the interface for transaction operations.
type Tx interface {
transaction.Tx
// GetSigners fetches the addresses of the signers of the transaction.
GetSigners() ([][]byte, error)
// GetPubKeys retrieves the public keys of the signers of the transaction.
GetPubKeys() ([]cryptotypes.PubKey, error)
// GetSignatures fetches the signatures attached to the transaction.
GetSignatures() ([]Signature, error)
}
// txParamsFromFlagSet extracts the transaction parameters from the provided FlagSet.
func txParamsFromFlagSet(flags *pflag.FlagSet, keybase keyring2.Keyring, ac address.Codec) (params TxParameters, err error) {
timestampUnix, _ := flags.GetInt64(flagTimeoutTimestamp)
timeoutTimestamp := time.Unix(timestampUnix, 0)
chainID, _ := flags.GetString(flagChainID)
memo, _ := flags.GetString(flagNote)
signMode, _ := flags.GetString(flagSignMode)
accNumber, _ := flags.GetUint64(flagAccountNumber)
sequence, _ := flags.GetUint64(flagSequence)
from, _ := flags.GetString(flagFrom)
var fromName, fromAddress string
var addr []byte
isDryRun, _ := flags.GetBool(flagDryRun)
if isDryRun {
addr, err = ac.StringToBytes(from)
} else {
fromName, fromAddress, _, err = keybase.KeyInfo(from)
if err == nil {
addr, err = ac.StringToBytes(fromAddress)
}
}
if err != nil {
return params, err
}
gas, _ := flags.GetString(flagGas)
simulate, gasValue, _ := parseGasSetting(gas)
gasAdjustment, _ := flags.GetFloat64(flagGasAdjustment)
gasPrices, _ := flags.GetString(flagGasPrices)
fees, _ := flags.GetString(flagFees)
feePayer, _ := flags.GetString(flagFeePayer)
feeGrater, _ := flags.GetString(flagFeeGranter)
unordered, _ := flags.GetBool(flagUnordered)
gasConfig, err := NewGasConfig(gasValue, gasAdjustment, gasPrices)
if err != nil {
return params, err
}
feeConfig, err := NewFeeConfig(fees, feePayer, feeGrater)
if err != nil {
return params, err
}
txParams := TxParameters{
timeoutTimestamp: timeoutTimestamp,
chainID: chainID,
memo: memo,
signMode: getSignMode(signMode),
AccountConfig: AccountConfig{
accountNumber: accNumber,
sequence: sequence,
fromName: fromName,
fromAddress: fromAddress,
address: addr,
},
GasConfig: gasConfig,
FeeConfig: feeConfig,
ExecutionOptions: ExecutionOptions{
unordered: unordered,
simulateAndExecute: simulate,
},
}
return txParams, nil
}

115
client/v2/tx/wrapper.go Normal file
View File

@ -0,0 +1,115 @@
package tx
import (
"fmt"
"reflect"
"strings"
"github.com/cosmos/gogoproto/proto"
"google.golang.org/protobuf/types/known/anypb"
"cosmossdk.io/core/transaction"
"cosmossdk.io/x/tx/decode"
"github.com/cosmos/cosmos-sdk/codec"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
)
var (
_ transaction.Tx = wrappedTx{}
_ Tx = wrappedTx{}
)
// wrappedTx wraps a transaction and provides a codec for binary encoding/decoding.
type wrappedTx struct {
*decode.DecodedTx
cdc codec.BinaryCodec
}
func newWrapperTx(cdc codec.BinaryCodec, decodedTx *decode.DecodedTx) *wrappedTx {
return &wrappedTx{
DecodedTx: decodedTx,
cdc: cdc,
}
}
// GetSigners fetches the addresses of the signers of the transaction.
func (w wrappedTx) GetSigners() ([][]byte, error) {
return w.Signers, nil
}
// GetPubKeys retrieves the public keys of the signers from the transaction's SignerInfos.
func (w wrappedTx) GetPubKeys() ([]cryptotypes.PubKey, error) {
signerInfos := w.Tx.AuthInfo.SignerInfos
pks := make([]cryptotypes.PubKey, len(signerInfos))
for i, si := range signerInfos {
// NOTE: it is okay to leave this nil if there is no PubKey in the SignerInfo.
// PubKey's can be left unset in SignerInfo.
if si.PublicKey == nil {
continue
}
maybePk, err := w.decodeAny(si.PublicKey)
if err != nil {
return nil, err
}
pk, ok := maybePk.(cryptotypes.PubKey)
if !ok {
return nil, fmt.Errorf("invalid public key type: %T", maybePk)
}
pks[i] = pk
}
return pks, nil
}
// GetSignatures fetches the signatures attached to the transaction.
func (w wrappedTx) GetSignatures() ([]Signature, error) {
signerInfos := w.Tx.AuthInfo.SignerInfos
sigs := w.Tx.Signatures
pubKeys, err := w.GetPubKeys()
if err != nil {
return nil, err
}
signatures := make([]Signature, len(sigs))
for i, si := range signerInfos {
if si.ModeInfo == nil || si.ModeInfo.Sum == nil {
signatures[i] = Signature{
PubKey: pubKeys[i],
}
} else {
sigData, err := modeInfoAndSigToSignatureData(si.ModeInfo, sigs[i])
if err != nil {
return nil, err
}
signatures[i] = Signature{
PubKey: pubKeys[i],
Data: sigData,
Sequence: si.GetSequence(),
}
}
}
return signatures, nil
}
// decodeAny decodes a protobuf Any message into a concrete proto.Message.
func (w wrappedTx) decodeAny(anyPb *anypb.Any) (proto.Message, error) {
name := anyPb.GetTypeUrl()
if i := strings.LastIndexByte(name, '/'); i >= 0 {
name = name[i+len("/"):]
}
typ := proto.MessageType(name)
if typ == nil {
return nil, fmt.Errorf("unknown type: %s", name)
}
v1 := reflect.New(typ.Elem()).Interface().(proto.Message)
err := w.cdc.Unmarshal(anyPb.GetValue(), v1)
if err != nil {
return nil, err
}
return v1, nil
}

View File

@ -2,6 +2,7 @@ package keyring
import (
signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"cosmossdk.io/core/address"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
@ -21,15 +22,22 @@ type autoCLIKeyring interface {
// Sign signs the given bytes with the key with the given name.
Sign(name string, msg []byte, signMode signingv1beta1.SignMode) ([]byte, error)
// KeyType returns the type of the key.
KeyType(name string) (uint, error)
// KeyInfo given a key name or address returns key name, key address and key type.
KeyInfo(name string) (string, string, uint, error)
}
// NewAutoCLIKeyring wraps the SDK keyring and make it compatible with the AutoCLI keyring interfaces.
func NewAutoCLIKeyring(kr Keyring) (autoCLIKeyring, error) {
return &autoCLIKeyringAdapter{kr}, nil
func NewAutoCLIKeyring(kr Keyring, ac address.Codec) (autoCLIKeyring, error) {
return &autoCLIKeyringAdapter{kr, ac}, nil
}
type autoCLIKeyringAdapter struct {
Keyring
ac address.Codec
}
func (a *autoCLIKeyringAdapter) List() ([]string, error) {
@ -84,3 +92,40 @@ func (a *autoCLIKeyringAdapter) Sign(name string, msg []byte, signMode signingv1
signBytes, _, err := a.Keyring.Sign(record.Name, msg, sdkSignMode)
return signBytes, err
}
func (a *autoCLIKeyringAdapter) KeyType(name string) (uint, error) {
record, err := a.Keyring.Key(name)
if err != nil {
return 0, err
}
return uint(record.GetType()), nil
}
func (a *autoCLIKeyringAdapter) KeyInfo(nameOrAddr string) (string, string, uint, error) {
addr, err := a.ac.StringToBytes(nameOrAddr)
if err != nil {
// If conversion fails, it's likely a name, not an address
record, err := a.Keyring.Key(nameOrAddr)
if err != nil {
return "", "", 0, err
}
addr, err = record.GetAddress()
if err != nil {
return "", "", 0, err
}
addrStr, err := a.ac.BytesToString(addr)
if err != nil {
return "", "", 0, err
}
return record.Name, addrStr, uint(record.GetType()), nil
}
// If conversion succeeds, it's an address, get the key info by address
record, err := a.Keyring.KeyByAddress(addr)
if err != nil {
return "", "", 0, err
}
return record.Name, nameOrAddr, uint(record.GetType()), nil
}