Skip to content

Commit

Permalink
feat(client/v2): factory (backport #20623) (#22416)
Browse files Browse the repository at this point in the history
Co-authored-by: Julián Toledano <JulianToledano@users.noreply.github.com>
  • Loading branch information
mergify[bot] and JulianToledano authored Nov 4, 2024
1 parent 878e306 commit c74f7e8
Show file tree
Hide file tree
Showing 25 changed files with 4,411 additions and 5 deletions.
2 changes: 2 additions & 0 deletions client/v2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion client/v2/autocli/flag/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
6 changes: 6 additions & 0 deletions client/v2/autocli/keyring/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
10 changes: 10 additions & 0 deletions client/v2/autocli/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 8 additions & 0 deletions client/v2/autocli/keyring/no_keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion client/v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions client/v2/internal/account/retriever.go
Original file line number Diff line number Diff line change
@@ -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 changes: 66 additions & 0 deletions client/v2/internal/coins/util.go
Original file line number Diff line number Diff line change
@@ -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 changes: 83 additions & 0 deletions client/v2/internal/coins/util_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
2 changes: 1 addition & 1 deletion client/v2/offchain/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit c74f7e8

Please sign in to comment.