feat(client/v2): factory (#20623)
Co-authored-by: Julien Robert <julien@rbrt.fr>
This commit is contained in:
parent
8bbf51c5ca
commit
c8f4cf787b
@ -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
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
116
client/v2/internal/account/retriever.go
Normal file
116
client/v2/internal/account/retriever.go
Normal 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
|
||||
}
|
||||
66
client/v2/internal/coins/util.go
Normal file
66
client/v2/internal/coins/util.go
Normal 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
|
||||
}
|
||||
83
client/v2/internal/coins/util_test.go
Normal file
83
client/v2/internal/coins/util_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
465
client/v2/tx/README.md
Normal 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
116
client/v2/tx/common_test.go
Normal 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
338
client/v2/tx/config.go
Normal 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
293
client/v2/tx/config_test.go
Normal 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
119
client/v2/tx/encoder.go
Normal 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,
|
||||
})
|
||||
}
|
||||
107
client/v2/tx/encoder_test.go
Normal file
107
client/v2/tx/encoder_test.go
Normal 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
761
client/v2/tx/factory.go
Normal 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
|
||||
}
|
||||
921
client/v2/tx/factory_test.go
Normal file
921
client/v2/tx/factory_test.go
Normal 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
52
client/v2/tx/flags.go
Normal 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
197
client/v2/tx/signature.go
Normal 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)
|
||||
}
|
||||
143
client/v2/tx/signature_test.go
Normal file
143
client/v2/tx/signature_test.go
Normal 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
229
client/v2/tx/tx.go
Normal 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
214
client/v2/tx/types.go
Normal 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
115
client/v2/tx/wrapper.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user