Skip to content

Commit

Permalink
Merge pull request #1 from teamscanworks/fix/panic
Browse files Browse the repository at this point in the history
Implementation Fixes
  • Loading branch information
bonedaddy authored Jul 19, 2023
2 parents 515b546 + 46940e3 commit 434d6ac
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 23 deletions.
183 changes: 163 additions & 20 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package compass

import (
"context"
"fmt"
"os"
"sync"
"time"

rpchttp "github.com/cometbft/cometbft/rpc/client/http"
"github.com/cosmos/cosmos-sdk/client"
cclient "github.com/cosmos/cosmos-sdk/client"
sdktypes "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"

rpchttp "github.com/cometbft/cometbft/rpc/client/http"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
"github.com/spf13/pflag"
"go.uber.org/zap"
"google.golang.org/grpc"
)
Expand All @@ -28,6 +32,17 @@ type Client struct {

initFn sync.Once
closeFn sync.Once

cctx client.Context
factory tx.Factory

// sequence number setting can potentially lead to race conditions, so lock access
// to the ability to send transactions to a single tx at a time
//
// NOTE: this isn't very performant, so should probably move to a goroutine accepting messages to send through a channel
txLock sync.Mutex

seqNum uint64
}

// Returns a new compass client used to interact with the cosmos blockchain
Expand Down Expand Up @@ -66,56 +81,184 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error {
initErr = fmt.Errorf("invalid client object: no config")
return
}

keyInfo, err := keyring.New(c.cfg.ChainID, c.cfg.KeyringBackend, c.cfg.KeyDirectory, os.Stdin, c.Codec.Marshaler, keyringOptions...)
if err != nil {
initErr = fmt.Errorf("failed to initialize keyring %s", err)
return
}
rpc, err := cclient.NewClientFromNode(c.cfg.RPCAddr)
c.Keyring = keyInfo

rpc, err := NewRPCClient(c.cfg.RPCAddr, time.Second*30)
if err != nil {
initErr = fmt.Errorf("failed to construct client from node %s", err)
return
initErr = fmt.Errorf("failed to construct rpc client %v", err)
}
c.RPC = rpc

grpcConn, err := grpc.Dial(
c.cfg.GRPCAddr, // your gRPC server address.
grpc.WithInsecure(), // The Cosmos SDK doesn't support any transport security mechanism
grpc.WithDefaultCallOptions(grpc.ForceCodec(codec.NewProtoCodec(nil).GRPCCodec())),
)
if err != nil {
initErr = fmt.Errorf("failed to dial grpc server node %s", err)
return
}

c.RPC = rpc
c.GRPC = grpcConn
c.Keyring = keyInfo

signOpts, err := authtx.NewDefaultSigningOptions()
if err != nil {
initErr = fmt.Errorf("failed to get tx opts %s", err)
return
}
txCfg, err := authtx.NewTxConfigWithOptions(c.Codec.Marshaler, authtx.ConfigOptions{
SigningOptions: signOpts,
})
if err != nil {
initErr = fmt.Errorf("failed to initialize tx config %s", err)
return
}
c.cctx = c.configClientContext(client.Context{}.WithTxConfig(txCfg))

factory, err := tx.NewFactoryCLI(c.cctx, pflag.NewFlagSet("", pflag.ExitOnError))
if err != nil {
initErr = fmt.Errorf("failed to initialize tx factory %s", err)
return
}
c.factory = c.configTxFactory(factory.WithTxConfig(txCfg))

c.log.Info("initialized client")
})

return initErr
}

// Returns an instance of tx.Factory which can be used to broadcast transactions
func (c *Client) TxFactory() tx.Factory {
return tx.Factory{}.
// Triggers keyring migration, ensuring that the factory, and client context are updated
func (c *Client) MigrateKeyring() error {
_, err := c.Keyring.MigrateAll()
if err != nil {
return err
}
c.factory = c.factory.WithKeybase(c.Keyring)
c.cctx = c.cctx.WithKeyring(c.Keyring)
return nil
}

// Sets the name of the key that is used for signing transactions, this is required
// to lookup the key material in the keyring
func (c *Client) UpdateFromName(name string) {
c.cctx = c.cctx.WithFromName(name)
}

// Sends and confirms the given message, returning the hex encoded transaction hash
// if the transaction was successfully confirmed.
func (c *Client) SendTransaction(ctx context.Context, msg sdktypes.Msg) (string, error) {
c.txLock.Lock()
defer c.txLock.Unlock()
if err := c.prepare(); err != nil {
return "", fmt.Errorf("transaction preparation failed %v", err)
}
txHash, err := c.BroadcastTx(ctx, msg)
if err != nil {
return "", fmt.Errorf("failed to broadcast transaction %v", err)
}
c.log.Info("sent transaction", zap.String("tx.hash", txHash))
return txHash, nil
}

// Updates the address used to sign transactions, using the first available
// key from the keyring
func (c *Client) SetFromAddress() error {
activeKp, err := c.GetActiveKeypair()
if err != nil {
return err
}
if activeKp == nil {
c.log.Warn("no keys found, you should create at least one")
} else {
c.log.Info("configured from address", zap.String("from.address", activeKp.String()))
c.cctx = c.cctx.WithFromAddress(*activeKp).WithFromName(c.cfg.Key)
}
return nil
}

// Returns the keypair actively in use for signing transactions (the first key in the keyring).
// If no address has been configured returns `nil, nil`
func (c *Client) GetActiveKeypair() (*sdktypes.AccAddress, error) {
keys, err := c.Keyring.List()
if err != nil {
return nil, err
}
if len(keys) == 0 {
return nil, nil
}
kp, err := keys[0].GetAddress()
if err != nil {
return nil, err
}
return &kp, nil
}

// Returns the keyring record located at the given index, returning an error
// if there are less than `idx` keys in the keyring
func (c *Client) KeyringRecordAt(idx int) (*keyring.Record, error) {
keys, err := c.Keyring.List()
if err != nil {
return nil, err
}
if len(keys) < idx-1 {
return nil, fmt.Errorf("key length %v less than index %v", len(keys), idx)
}
return keys[idx], nil
}

// ensures that all necessary configs are set to enable transaction sending
//
// TODO: likely not very performant
func (c *Client) prepare() error {
kp, err := c.GetActiveKeypair()
if err != nil {
return err
}
factory, err := c.factory.Prepare(c.cctx)
if err != nil {
return err
}
_, seq, err := factory.AccountRetriever().GetAccountNumberSequence(c.cctx, *kp)
if err != nil {
return err
}
c.seqNum = seq
c.factory = factory.WithSequence(c.seqNum)
return nil
}

// helper function which applies configuration against the transaction factory
func (c *Client) configTxFactory(input tx.Factory) tx.Factory {
return input.
WithAccountRetriever(c).
WithChainID(c.cfg.ChainID).
WithGasAdjustment(c.cfg.GasAdjustment).
WithGasPrices(c.cfg.GasPrices).
WithKeybase(c.Keyring).
WithSignMode(signing.SignMode_SIGN_MODE_DIRECT)
WithSignMode(signing.SignMode_SIGN_MODE_DIRECT).
// prevents some runtime panics due to misconfigured clients causing the error messages to be logged
WithSimulateAndExecute(true)
}

// Returns an instance of client.Context, used widely throughout cosmos-sdk
func (c *Client) ClientContext() client.Context {
return client.Context{}.
WithViper("breaker").
// helper function which applies configuration against the client context
func (c *Client) configClientContext(cctx client.Context) client.Context {
return cctx.
//WithViper("breaker").
WithAccountRetriever(c).
WithChainID(c.cfg.ChainID).
WithKeyring(c.Keyring).
WithGRPCClient(c.GRPC).
WithClient(c.RPC).
WithSignModeStr(signing.SignMode_SIGN_MODE_DIRECT.String()).
WithCodec(c.Codec.Marshaler).
WithInterfaceRegistry(c.Codec.InterfaceRegistry)
WithInterfaceRegistry(c.Codec.InterfaceRegistry).
WithBroadcastMode("sync").
// this is important to set otherwise it is not possible to programmatically sign
// transactions with cosmos-sdk as it will expect the user to provide input
WithSkipConfirmation(true)
}
1 change: 0 additions & 1 deletion client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@ func TestClient(t *testing.T) {
abcInfo, err := client.RPC.ABCIInfo(context.Background())
require.NoError(t, err)
require.GreaterOrEqual(t, abcInfo.Response.LastBlockHeight, int64(1))
require.NotNil(t, client.ClientContext())
}
8 changes: 6 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var (

// Allows configuration of the compass client
type ClientConfig struct {
// the name of that is used as the `FromName` for transaction signing
Key string `json:"key" yaml:"key"`
ChainID string `json:"chain-id" yaml:"chain-id"`
RPCAddr string `json:"rpc-addr" yaml:"rpc-addr"`
Expand Down Expand Up @@ -102,6 +103,7 @@ func GetCosmosHubConfig(keyHome string, debug bool) *ClientConfig {
Timeout: "20s",
OutputFormat: "json",
SignModeStr: "direct",
Modules: ModuleBasics,
}
cfg.SetKeysDir(keyHome)
return cfg
Expand All @@ -124,6 +126,7 @@ func GetOsmosisConfig(keyHome string, debug bool) *ClientConfig {
Timeout: "20s",
OutputFormat: "json",
SignModeStr: "direct",
Modules: ModuleBasics,
}
cfg.SetKeysDir(keyHome)
return cfg
Expand All @@ -133,19 +136,20 @@ func GetOsmosisConfig(keyHome string, debug bool) *ClientConfig {
func GetSimdConfig() *ClientConfig {
cfg := &ClientConfig{
Key: "default",
ChainID: "cosmoshub-4",
ChainID: "testing",
RPCAddr: "tcp://127.0.0.1:26657",
GRPCAddr: "127.0.0.1:9090",
AccountPrefix: "cosmos",
KeyringBackend: "test",
GasAdjustment: 1.2,
GasPrices: "0.01uatom",
GasPrices: "1stake",
MinGasAmount: 0,
KeyDirectory: "keyring-test",
Debug: true,
Timeout: "20s",
OutputFormat: "json",
SignModeStr: "direct",
Modules: ModuleBasics,
}
cfg.SetKeysDir("keyring-test")
return cfg
Expand Down
69 changes: 69 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package compass

import (
"context"
"encoding/hex"
"fmt"
"time"

rpchttp "github.com/cometbft/cometbft/rpc/client/http"
libclient "github.com/cometbft/cometbft/rpc/jsonrpc/client"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/go-bip39"
)

Expand Down Expand Up @@ -44,3 +49,67 @@ func CreateMnemonic() (string, error) {
}
return mnemonic, nil
}

// Broadcasts a transaction, returning the transaction hash. This is is not thread-safe, as the factory can
// return a sequence number that is behind X sequences depending on the amount of transactions that have been sent
// and the time to confirm transactions.
//
// To be as safe as possible it's recommended the caller use `SendTransaction`
func (c *Client) BroadcastTx(ctx context.Context, msgs ...sdk.Msg) (string, error) {
factory, err := c.factory.Prepare(c.cctx)
if err != nil {
return "", fmt.Errorf("failed to prepare transaction %s", err)
}

unsignedTx, err := factory.BuildUnsignedTx(msgs...)
if err != nil {
return "", fmt.Errorf("failed to build unsigned transaction %s", err)
}

if err := tx.Sign(c.cctx.CmdContext, factory, c.cctx.GetFromName(), unsignedTx, true); err != nil {
return "", fmt.Errorf("failed to sign transaction %s", err)
}

txBytes, err := c.cctx.TxConfig.TxEncoder()(unsignedTx.GetTx())
if err != nil {
return "", fmt.Errorf("failed to get transaction encoder %s", err)
}

res, err := c.cctx.BroadcastTx(txBytes)
if err != nil {
return "", fmt.Errorf("failed to broadcast transaction %s", err)
}

txBytes, err = hex.DecodeString(res.TxHash)
if err != nil {
return "", fmt.Errorf("failed to decode string %s", err)
}

// allow up to 10 seconds for the transaction to be confirmed before bailing
//
// TODO: allow this to be configurable
exitTicker := time.After(time.Second * 10)
checkTicker := time.NewTicker(time.Second)
defer checkTicker.Stop()
for {
select {
case <-exitTicker:
return "", fmt.Errorf("failed to confirm transaction")
case <-checkTicker.C:
if _, err := c.cctx.Client.Tx(ctx, txBytes, false); err != nil {
continue
}
return res.TxHash, nil
}
}
}

// Returns the address of the key used for transaction signing
func (c *Client) FromAddress() string {
return c.cctx.FromAddress.String()
}

// Returns the name of the key used for transaction signing
func (c *Client) FromName() string {
return c.cctx.GetFromName()
}

0 comments on commit 434d6ac

Please sign in to comment.