From fab2f8a0a368d34692001cfa48a56bb3bc484a85 Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Sun, 9 Jul 2023 22:57:21 -0700 Subject: [PATCH 01/13] set codec registry when constructing grpc client --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index 9585743..71dcecb 100644 --- a/client.go +++ b/client.go @@ -80,7 +80,7 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { 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())), + grpc.WithDefaultCallOptions(grpc.ForceCodec(codec.NewProtoCodec(c.Codec.InterfaceRegistry).GRPCCodec())), ) if err != nil { initErr = fmt.Errorf("failed to dial grpc server node %s", err) From 336c019ac8a3ebe11512e42d3228c4fb5b156af2 Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Sun, 9 Jul 2023 23:11:46 -0700 Subject: [PATCH 02/13] create transaction factory with helper function --- client.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 71dcecb..378aa19 100644 --- a/client.go +++ b/client.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/spf13/pflag" "go.uber.org/zap" "google.golang.org/grpc" ) @@ -22,6 +23,7 @@ type Client struct { cfg *ClientConfig RPC *rpchttp.HTTP GRPC *grpc.ClientConn + Factory tx.Factory Keyring keyring.Keyring Codec Codec @@ -76,11 +78,15 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { initErr = fmt.Errorf("failed to construct client from node %s", err) return } - + factory, err := tx.NewFactoryCLI(c.ClientContext(), pflag.NewFlagSet("", pflag.ExitOnError)) + if err != nil { + initErr = fmt.Errorf("failed to initialize tx factory %s", err) + return + } 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(c.Codec.InterfaceRegistry).GRPCCodec())), + grpc.WithDefaultCallOptions(grpc.ForceCodec(codec.NewProtoCodec(nil).GRPCCodec())), ) if err != nil { initErr = fmt.Errorf("failed to dial grpc server node %s", err) @@ -90,14 +96,20 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { c.RPC = rpc c.GRPC = grpcConn c.Keyring = keyInfo + c.Factory = factory c.log.Info("initialized client") }) return initErr } -// Returns an instance of tx.Factory which can be used to broadcast transactions +// Returns previously initialized transaction factory func (c *Client) TxFactory() tx.Factory { + return c.Factory +} + +// Returns an instance of tx.Factory which can be used to broadcast transactions +func (c *Client) TxFactory2() tx.Factory { return tx.Factory{}. WithAccountRetriever(c). WithChainID(c.cfg.ChainID). From 1c45abb0689b28e4bae305832d5fc8493b4872d6 Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 15:26:44 -0700 Subject: [PATCH 03/13] cleanup client initialization --- client.go | 55 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/client.go b/client.go index 378aa19..7315be9 100644 --- a/client.go +++ b/client.go @@ -9,9 +9,9 @@ import ( "github.com/cosmos/cosmos-sdk/client" cclient "github.com/cosmos/cosmos-sdk/client" "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" @@ -73,30 +73,42 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { initErr = fmt.Errorf("failed to initialize keyring %s", err) return } + c.Keyring = keyInfo rpc, err := cclient.NewClientFromNode(c.cfg.RPCAddr) if err != nil { initErr = fmt.Errorf("failed to construct client from node %s", err) return } - factory, err := tx.NewFactoryCLI(c.ClientContext(), pflag.NewFlagSet("", pflag.ExitOnError)) - if err != nil { - initErr = fmt.Errorf("failed to initialize tx factory %s", err) - return - } + 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())), + //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 - c.Factory = factory + factory, err := tx.NewFactoryCLI(c.ClientContext(), pflag.NewFlagSet("", pflag.ExitOnError)) + if err != nil { + initErr = fmt.Errorf("failed to initialize tx factory %s", err) + return + } + 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.Factory = c.configTxFactory(factory.WithTxConfig(txCfg)) + c.log.Info("initialized client") }) @@ -108,17 +120,6 @@ func (c *Client) TxFactory() tx.Factory { return c.Factory } -// Returns an instance of tx.Factory which can be used to broadcast transactions -func (c *Client) TxFactory2() tx.Factory { - return tx.Factory{}. - WithAccountRetriever(c). - WithChainID(c.cfg.ChainID). - WithGasAdjustment(c.cfg.GasAdjustment). - WithGasPrices(c.cfg.GasPrices). - WithKeybase(c.Keyring). - WithSignMode(signing.SignMode_SIGN_MODE_DIRECT) -} - // Returns an instance of client.Context, used widely throughout cosmos-sdk func (c *Client) ClientContext() client.Context { return client.Context{}. @@ -131,3 +132,13 @@ func (c *Client) ClientContext() client.Context { WithCodec(c.Codec.Marshaler). WithInterfaceRegistry(c.Codec.InterfaceRegistry) } + +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) +} From f57891cb9ba1ad2f8da2046c1e4296eb3aff88a6 Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 15:55:14 -0700 Subject: [PATCH 04/13] fix invalid configurations for simd config --- config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 7ff89f5..7f56b33 100644 --- a/config.go +++ b/config.go @@ -133,13 +133,13 @@ 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, From a8185db9025128242e40e0d5a735ea3f457707ee Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 16:46:30 -0700 Subject: [PATCH 05/13] cleanup private/public functions, etc.. --- client.go | 88 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/client.go b/client.go index 7315be9..32f2a2c 100644 --- a/client.go +++ b/client.go @@ -5,6 +5,8 @@ import ( "os" "sync" + sdktypes "github.com/cosmos/cosmos-sdk/types" + rpchttp "github.com/cometbft/cometbft/rpc/client/http" "github.com/cosmos/cosmos-sdk/client" cclient "github.com/cosmos/cosmos-sdk/client" @@ -23,6 +25,7 @@ type Client struct { cfg *ClientConfig RPC *rpchttp.HTTP GRPC *grpc.ClientConn + CCtx client.Context Factory tx.Factory Keyring keyring.Keyring @@ -30,6 +33,8 @@ type Client struct { initFn sync.Once closeFn sync.Once + + factoryLock sync.Mutex } // Returns a new compass client used to interact with the cosmos blockchain @@ -83,18 +88,12 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { 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.GRPC = grpcConn - factory, err := tx.NewFactoryCLI(c.ClientContext(), pflag.NewFlagSet("", pflag.ExitOnError)) - if err != nil { - initErr = fmt.Errorf("failed to initialize tx factory %s", err) - return - } signOpts, err := authtx.NewDefaultSigningOptions() if err != nil { initErr = fmt.Errorf("failed to get tx opts %s", err) @@ -107,8 +106,15 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { initErr = fmt.Errorf("failed to initialize tx config %s", err) return } - c.Factory = c.configTxFactory(factory.WithTxConfig(txCfg)) + c.CCtx = c.configClientContext(client.Context{}.WithTxConfig(txCfg)) + + factory, err := tx.NewFactoryCLI(c.ClientContext(), 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") }) @@ -122,15 +128,7 @@ func (c *Client) TxFactory() tx.Factory { // Returns an instance of client.Context, used widely throughout cosmos-sdk func (c *Client) ClientContext() client.Context { - return client.Context{}. - WithViper("breaker"). - WithAccountRetriever(c). - WithChainID(c.cfg.ChainID). - WithKeyring(c.Keyring). - WithGRPCClient(c.GRPC). - WithClient(c.RPC). - WithCodec(c.Codec.Marshaler). - WithInterfaceRegistry(c.Codec.InterfaceRegistry) + return c.CCtx } func (c *Client) configTxFactory(input tx.Factory) tx.Factory { @@ -140,5 +138,61 @@ func (c *Client) configTxFactory(input tx.Factory) tx.Factory { WithGasAdjustment(c.cfg.GasAdjustment). WithGasPrices(c.cfg.GasPrices). WithKeybase(c.Keyring). - WithSignMode(signing.SignMode_SIGN_MODE_DIRECT) + WithSignMode(signing.SignMode_SIGN_MODE_DIRECT). + WithSimulateAndExecute(true) +} + +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) //.WithOutput(os.Stdout) +} + +func (c *Client) PrepareClientContext(cctx client.Context) error { + c.factoryLock.Lock() + defer c.factoryLock.Unlock() + factory, err := c.Factory.Prepare(cctx) + if err != nil { + c.log.Error("failed to prepare factory", zap.Error(err)) + return err + } + c.Factory = factory + return nil +} + +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) + } + 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 } From 8b4f86cc6960bf930121f442421470580ccdab4e Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 17:08:57 -0700 Subject: [PATCH 06/13] set default modules --- config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.go b/config.go index 7f56b33..b2cf0b2 100644 --- a/config.go +++ b/config.go @@ -102,6 +102,7 @@ func GetCosmosHubConfig(keyHome string, debug bool) *ClientConfig { Timeout: "20s", OutputFormat: "json", SignModeStr: "direct", + Modules: ModuleBasics, } cfg.SetKeysDir(keyHome) return cfg @@ -124,6 +125,7 @@ func GetOsmosisConfig(keyHome string, debug bool) *ClientConfig { Timeout: "20s", OutputFormat: "json", SignModeStr: "direct", + Modules: ModuleBasics, } cfg.SetKeysDir(keyHome) return cfg @@ -146,6 +148,7 @@ func GetSimdConfig() *ClientConfig { Timeout: "20s", OutputFormat: "json", SignModeStr: "direct", + Modules: ModuleBasics, } cfg.SetKeysDir("keyring-test") return cfg From 1e9d4b3abdf88055e60300e7b66df3f0b26e08ce Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 20:10:54 -0700 Subject: [PATCH 07/13] fix transaction sending --- client.go | 40 +++++++++++++++++++++++++++++++++++++--- config.go | 1 + 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 32f2a2c..9b5a150 100644 --- a/client.go +++ b/client.go @@ -121,11 +121,26 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { return initErr } +// 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 +} + // Returns previously initialized transaction factory func (c *Client) TxFactory() tx.Factory { return c.Factory } +func (c *Client) UpdateFromName(name string) { + c.CCtx = c.CCtx.WithFromName(name) +} + // Returns an instance of client.Context, used widely throughout cosmos-sdk func (c *Client) ClientContext() client.Context { return c.CCtx @@ -139,6 +154,7 @@ func (c *Client) configTxFactory(input tx.Factory) tx.Factory { WithGasPrices(c.cfg.GasPrices). WithKeybase(c.Keyring). WithSignMode(signing.SignMode_SIGN_MODE_DIRECT). + // prevents some runtime panics due to misconfigured clients causing the error messages to be logged WithSimulateAndExecute(true) } @@ -149,9 +165,14 @@ func (c *Client) configClientContext(cctx client.Context) client.Context { WithChainID(c.cfg.ChainID). WithKeyring(c.Keyring). WithGRPCClient(c.GRPC). - WithClient(c.RPC).WithSignModeStr(signing.SignMode_SIGN_MODE_DIRECT.String()). + WithClient(c.RPC). + WithSignModeStr(signing.SignMode_SIGN_MODE_DIRECT.String()). WithCodec(c.Codec.Marshaler). - WithInterfaceRegistry(c.Codec.InterfaceRegistry) //.WithOutput(os.Stdout) + 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) } func (c *Client) PrepareClientContext(cctx client.Context) error { @@ -175,7 +196,7 @@ func (c *Client) SetFromAddress() error { 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) + c.CCtx = c.CCtx.WithFromAddress(*activeKp).WithFromName(c.cfg.Key) } return nil } @@ -196,3 +217,16 @@ func (c *Client) GetActiveKeypair() (*sdktypes.AccAddress, error) { } 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 +} diff --git a/config.go b/config.go index b2cf0b2..5e9b206 100644 --- a/config.go +++ b/config.go @@ -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"` From da45b1eb0e92312555ff8290c4c2b2c2eefc7d3e Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 21:13:41 -0700 Subject: [PATCH 08/13] set sequence number when preparing client context --- client.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client.go b/client.go index 9b5a150..93a9fda 100644 --- a/client.go +++ b/client.go @@ -178,12 +178,21 @@ func (c *Client) configClientContext(cctx client.Context) client.Context { func (c *Client) PrepareClientContext(cctx client.Context) error { c.factoryLock.Lock() defer c.factoryLock.Unlock() + kp, err := c.GetActiveKeypair() + if err != nil { + return err + } factory, err := c.Factory.Prepare(cctx) if err != nil { c.log.Error("failed to prepare factory", zap.Error(err)) return err } c.Factory = factory + _, seq, err := c.Factory.AccountRetriever().GetAccountNumberSequence(c.CCtx, *kp) + if err != nil { + return err + } + c.Factory = c.Factory.WithSequence(seq) return nil } From 80fe0525fa035ed6e6709f5f98ff28f370a0984d Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 22:09:47 -0700 Subject: [PATCH 09/13] add helper functions to send transaction & sync sequence number --- client.go | 114 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/client.go b/client.go index 93a9fda..8d00b96 100644 --- a/client.go +++ b/client.go @@ -5,14 +5,14 @@ import ( "os" "sync" + "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" cclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" "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" @@ -34,7 +34,12 @@ type Client struct { initFn sync.Once closeFn sync.Once - factoryLock sync.Mutex + // 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 that + // runs in a goroutine, accepting messages to send through a channel + txLock sync.Mutex } // Returns a new compass client used to interact with the cosmos blockchain @@ -145,54 +150,16 @@ func (c *Client) UpdateFromName(name string) { func (c *Client) ClientContext() client.Context { return c.CCtx } - -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). - // prevents some runtime panics due to misconfigured clients causing the error messages to be logged - WithSimulateAndExecute(true) -} - -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). - 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) -} - -func (c *Client) PrepareClientContext(cctx client.Context) error { - c.factoryLock.Lock() - defer c.factoryLock.Unlock() - kp, err := c.GetActiveKeypair() - if err != nil { - return err +func (c *Client) SendTransaction(msg sdktypes.Msg) error { + c.txLock.Lock() + defer c.txLock.Unlock() + if err := c.prepare(); err != nil { + return fmt.Errorf("transaction preparation failed %v", err) } - factory, err := c.Factory.Prepare(cctx) - if err != nil { - c.log.Error("failed to prepare factory", zap.Error(err)) - return err + c.log.Info("sending transaction") + if err := tx.GenerateOrBroadcastTxWithFactory(c.CCtx, c.Factory, msg); err != nil { + return fmt.Errorf("failed to send transaction %v", err) } - c.Factory = factory - _, seq, err := c.Factory.AccountRetriever().GetAccountNumberSequence(c.CCtx, *kp) - if err != nil { - return err - } - c.Factory = c.Factory.WithSequence(seq) return nil } @@ -239,3 +206,52 @@ func (c *Client) KeyringRecordAt(idx int) (*keyring.Record, error) { } return keys[idx], nil } + +// prepartion is susceptible to race conditions so its an private function +func (c *Client) prepare() error { + kp, err := c.GetActiveKeypair() + if err != nil { + return err + } + factory, err := c.Factory.Prepare(c.CCtx) + if err != nil { + c.log.Error("failed to prepare factory", zap.Error(err)) + return err + } + c.Factory = factory + _, seq, err := c.Factory.AccountRetriever().GetAccountNumberSequence(c.CCtx, *kp) + if err != nil { + return err + } + c.Factory = c.Factory.WithSequence(seq) + return nil +} + +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). + // prevents some runtime panics due to misconfigured clients causing the error messages to be logged + WithSimulateAndExecute(true) +} + +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). + 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) +} From 6fb0e4f390b9d2f7aaee29d04f563108459dd2b8 Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Fri, 14 Jul 2023 22:35:11 -0700 Subject: [PATCH 10/13] fix transaction signing --- client.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index 8d00b96..c77b66e 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "sync" + "time" "github.com/cosmos/cosmos-sdk/client" sdktypes "github.com/cosmos/cosmos-sdk/types" @@ -40,6 +41,8 @@ type Client struct { // NOTE: this isn't very performant, so should probably move to a goroutine that // runs in 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 @@ -150,6 +153,16 @@ func (c *Client) UpdateFromName(name string) { func (c *Client) ClientContext() client.Context { return c.CCtx } + +// Sends the given transaction, ensuring that it is signed, and that +// sequence numbers and all other required fields are set +// +// Note: this sleeps for 5 seconds after sending the transaction as a hacky workaround +// for avoiding sequence number race conditions as the cosmos-sdk when confirming a transaction +// after sending does not actually appear to check that the transaction has been confirmed, only +// that it has been received without error +// +// TODO: return the hash of the signed transaction func (c *Client) SendTransaction(msg sdktypes.Msg) error { c.txLock.Lock() defer c.txLock.Unlock() @@ -160,6 +173,7 @@ func (c *Client) SendTransaction(msg sdktypes.Msg) error { if err := tx.GenerateOrBroadcastTxWithFactory(c.CCtx, c.Factory, msg); err != nil { return fmt.Errorf("failed to send transaction %v", err) } + time.Sleep(time.Second * 5) return nil } @@ -215,15 +229,14 @@ func (c *Client) prepare() error { } factory, err := c.Factory.Prepare(c.CCtx) if err != nil { - c.log.Error("failed to prepare factory", zap.Error(err)) return err } - c.Factory = factory - _, seq, err := c.Factory.AccountRetriever().GetAccountNumberSequence(c.CCtx, *kp) + _, seq, err := factory.AccountRetriever().GetAccountNumberSequence(c.CCtx, *kp) if err != nil { return err } - c.Factory = c.Factory.WithSequence(seq) + c.seqNum = seq + c.Factory = factory.WithSequence(c.seqNum) return nil } From 303058475cfd7083179ff20c0c5d950705e8a10b Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Sat, 15 Jul 2023 22:49:59 -0700 Subject: [PATCH 11/13] wip custom transaction broadcast function --- client.go | 24 +++++++++++++++++------- utils.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index c77b66e..6cfe5fe 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,7 @@ package compass import ( + "context" "fmt" "os" "sync" @@ -11,7 +12,6 @@ import ( "github.com/cosmos/cosmos-sdk/types/tx/signing" rpchttp "github.com/cometbft/cometbft/rpc/client/http" - cclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/keyring" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" @@ -87,11 +87,15 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { return } c.Keyring = keyInfo - rpc, err := cclient.NewClientFromNode(c.cfg.RPCAddr) + 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) } + //rpc, err := cclient.NewClientFromNode(c.cfg.RPCAddr) + //if err != nil { + // initErr = fmt.Errorf("failed to construct client from node %s", err) + // return + //} c.RPC = rpc grpcConn, err := grpc.Dial( c.cfg.GRPCAddr, // your gRPC server address. @@ -163,16 +167,22 @@ func (c *Client) ClientContext() client.Context { // that it has been received without error // // TODO: return the hash of the signed transaction -func (c *Client) SendTransaction(msg sdktypes.Msg) error { +func (c *Client) SendTransaction(ctx context.Context, msg sdktypes.Msg) error { c.txLock.Lock() defer c.txLock.Unlock() if err := c.prepare(); err != nil { return fmt.Errorf("transaction preparation failed %v", err) } c.log.Info("sending transaction") - if err := tx.GenerateOrBroadcastTxWithFactory(c.CCtx, c.Factory, msg); err != nil { - return fmt.Errorf("failed to send transaction %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)) + //if err := tx.GenerateOrBroadcastTxWithFactory(c.CCtx, c.Factory, msg); err != nil { + // return fmt.Errorf("failed to send transaction %v", err) + //} + //tx.Sign() time.Sleep(time.Second * 5) return nil } diff --git a/utils.go b/utils.go index 1ad563d..fb0a4ba 100644 --- a/utils.go +++ b/utils.go @@ -1,13 +1,18 @@ package compass import ( + "context" + "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" + "go.uber.org/zap" ) // Returns a Cosmos JSON-RPC websocket client @@ -44,3 +49,52 @@ func CreateMnemonic() (string, error) { } return mnemonic, nil } + +// Broadcasts a transaction, returning the transaction hash +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) + } + c.log.Debug("building unsigned tx") + unsignedTx, err := factory.BuildUnsignedTx(msgs...) + if err != nil { + c.log.Debug("building tx failed", zap.Error(err)) + return "", fmt.Errorf("failed to build unsigned transaction %s", err) + } + c.log.Debug("signing transaction") + if err := tx.Sign(c.CCtx.CmdContext, factory, c.CCtx.GetFromName(), unsignedTx, true); err != nil { + c.log.Debug("signing failed", zap.Error(err)) + return "", fmt.Errorf("failed to sign transaction %s", err) + } + c.log.Debug("encoding transaction") + txBytes, err := c.CCtx.TxConfig.TxEncoder()(unsignedTx.GetTx()) + if err != nil { + c.log.Debug("encoding failed", zap.Error(err)) + return "", fmt.Errorf("failed to get transaction encoder %s", err) + } + c.log.Debug("broadcasting transaction") + res, err := c.CCtx.BroadcastTx(txBytes) + if err != nil { + c.log.Debug("broadcast failed", zap.Error(err)) + return "", fmt.Errorf("failed to broadcast transaction %s", err) + } + c.log.Debug("confirming transaction", zap.String("tx.hash", res.TxHash)) + 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: + txRes, err := c.CCtx.Client.Tx(ctx, []byte(res.TxHash), false) + if err != nil { + c.log.Debug("status check failed", zap.Error(err), zap.String("tx.hash", res.TxHash)) + continue + } + c.log.Info("confirmed transaction", zap.String("tx.hash", res.TxHash), zap.Any("response", txRes)) + return res.TxHash, nil + } + } +} From 84c02e0ce76c60989e13ce9e6e1281a9e39e46d8 Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Mon, 17 Jul 2023 16:28:20 -0700 Subject: [PATCH 12/13] cleanup various unused, and confusing functions --- client.go | 84 ++++++++++++++++++++------------------------------ client_test.go | 1 - utils.go | 43 ++++++++++++++------------ 3 files changed, 58 insertions(+), 70 deletions(-) diff --git a/client.go b/client.go index 6cfe5fe..e35b11d 100644 --- a/client.go +++ b/client.go @@ -26,8 +26,6 @@ type Client struct { cfg *ClientConfig RPC *rpchttp.HTTP GRPC *grpc.ClientConn - CCtx client.Context - Factory tx.Factory Keyring keyring.Keyring Codec Codec @@ -35,11 +33,13 @@ 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 that - // runs in a goroutine, accepting messages to send through a channel + // 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 @@ -81,22 +81,20 @@ 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 } c.Keyring = keyInfo + rpc, err := NewRPCClient(c.cfg.RPCAddr, time.Second*30) if err != nil { initErr = fmt.Errorf("failed to construct rpc client %v", err) } - //rpc, err := cclient.NewClientFromNode(c.cfg.RPCAddr) - //if err != nil { - // initErr = fmt.Errorf("failed to construct client from node %s", err) - // return - //} 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 @@ -106,6 +104,7 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { return } c.GRPC = grpcConn + signOpts, err := authtx.NewDefaultSigningOptions() if err != nil { initErr = fmt.Errorf("failed to get tx opts %s", err) @@ -118,15 +117,15 @@ func (c *Client) Initialize(keyringOptions []keyring.Option) error { initErr = fmt.Errorf("failed to initialize tx config %s", err) return } + c.cctx = c.configClientContext(client.Context{}.WithTxConfig(txCfg)) - c.CCtx = c.configClientContext(client.Context{}.WithTxConfig(txCfg)) - - factory, err := tx.NewFactoryCLI(c.ClientContext(), pflag.NewFlagSet("", pflag.ExitOnError)) + 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.factory = c.configTxFactory(factory.WithTxConfig(txCfg)) + c.log.Info("initialized client") }) @@ -139,54 +138,35 @@ func (c *Client) MigrateKeyring() error { if err != nil { return err } - c.Factory = c.Factory.WithKeybase(c.Keyring) - c.CCtx = c.CCtx.WithKeyring(c.Keyring) + c.factory = c.factory.WithKeybase(c.Keyring) + c.cctx = c.cctx.WithKeyring(c.Keyring) return nil } -// Returns previously initialized transaction factory -func (c *Client) TxFactory() tx.Factory { - return c.Factory -} - +// 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) -} - -// Returns an instance of client.Context, used widely throughout cosmos-sdk -func (c *Client) ClientContext() client.Context { - return c.CCtx + c.cctx = c.cctx.WithFromName(name) } -// Sends the given transaction, ensuring that it is signed, and that -// sequence numbers and all other required fields are set -// -// Note: this sleeps for 5 seconds after sending the transaction as a hacky workaround -// for avoiding sequence number race conditions as the cosmos-sdk when confirming a transaction -// after sending does not actually appear to check that the transaction has been confirmed, only -// that it has been received without error -// -// TODO: return the hash of the signed transaction -func (c *Client) SendTransaction(ctx context.Context, msg sdktypes.Msg) error { +// 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) + return "", fmt.Errorf("transaction preparation failed %v", err) } - c.log.Info("sending transaction") txHash, err := c.BroadcastTx(ctx, msg) if err != nil { - return fmt.Errorf("failed to broadcast transaction %v", err) + return "", fmt.Errorf("failed to broadcast transaction %v", err) } c.log.Info("sent transaction", zap.String("tx.hash", txHash)) - //if err := tx.GenerateOrBroadcastTxWithFactory(c.CCtx, c.Factory, msg); err != nil { - // return fmt.Errorf("failed to send transaction %v", err) - //} - //tx.Sign() - time.Sleep(time.Second * 5) - return nil + 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 { @@ -196,7 +176,7 @@ func (c *Client) SetFromAddress() error { 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) + c.cctx = c.cctx.WithFromAddress(*activeKp).WithFromName(c.cfg.Key) } return nil } @@ -231,25 +211,28 @@ func (c *Client) KeyringRecordAt(idx int) (*keyring.Record, error) { return keys[idx], nil } -// prepartion is susceptible to race conditions so its an private function +// 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) + factory, err := c.factory.Prepare(c.cctx) if err != nil { return err } - _, seq, err := factory.AccountRetriever().GetAccountNumberSequence(c.CCtx, *kp) + _, seq, err := factory.AccountRetriever().GetAccountNumberSequence(c.cctx, *kp) if err != nil { return err } c.seqNum = seq - c.Factory = factory.WithSequence(c.seqNum) + 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). @@ -262,6 +245,7 @@ func (c *Client) configTxFactory(input tx.Factory) tx.Factory { WithSimulateAndExecute(true) } +// helper function which applies configuration against the client context func (c *Client) configClientContext(cctx client.Context) client.Context { return cctx. //WithViper("breaker"). diff --git a/client_test.go b/client_test.go index fc99007..17865de 100644 --- a/client_test.go +++ b/client_test.go @@ -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()) } diff --git a/utils.go b/utils.go index fb0a4ba..47cb452 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,7 @@ package compass import ( "context" + "encoding/hex" "fmt" "time" @@ -12,7 +13,6 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/go-bip39" - "go.uber.org/zap" ) // Returns a Cosmos JSON-RPC websocket client @@ -50,36 +50,44 @@ func CreateMnemonic() (string, error) { return mnemonic, nil } -// Broadcasts a transaction, returning the transaction hash +// 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) + factory, err := c.factory.Prepare(c.cctx) if err != nil { return "", fmt.Errorf("failed to prepare transaction %s", err) } - c.log.Debug("building unsigned tx") + unsignedTx, err := factory.BuildUnsignedTx(msgs...) if err != nil { - c.log.Debug("building tx failed", zap.Error(err)) return "", fmt.Errorf("failed to build unsigned transaction %s", err) } - c.log.Debug("signing transaction") - if err := tx.Sign(c.CCtx.CmdContext, factory, c.CCtx.GetFromName(), unsignedTx, true); err != nil { - c.log.Debug("signing failed", zap.Error(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) } - c.log.Debug("encoding transaction") - txBytes, err := c.CCtx.TxConfig.TxEncoder()(unsignedTx.GetTx()) + + txBytes, err := c.cctx.TxConfig.TxEncoder()(unsignedTx.GetTx()) if err != nil { - c.log.Debug("encoding failed", zap.Error(err)) return "", fmt.Errorf("failed to get transaction encoder %s", err) } - c.log.Debug("broadcasting transaction") - res, err := c.CCtx.BroadcastTx(txBytes) + + res, err := c.cctx.BroadcastTx(txBytes) if err != nil { - c.log.Debug("broadcast failed", zap.Error(err)) return "", fmt.Errorf("failed to broadcast transaction %s", err) } - c.log.Debug("confirming transaction", zap.String("tx.hash", res.TxHash)) + + 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() @@ -88,12 +96,9 @@ func (c *Client) BroadcastTx(ctx context.Context, msgs ...sdk.Msg) (string, erro case <-exitTicker: return "", fmt.Errorf("failed to confirm transaction") case <-checkTicker.C: - txRes, err := c.CCtx.Client.Tx(ctx, []byte(res.TxHash), false) - if err != nil { - c.log.Debug("status check failed", zap.Error(err), zap.String("tx.hash", res.TxHash)) + if _, err := c.cctx.Client.Tx(ctx, txBytes, false); err != nil { continue } - c.log.Info("confirmed transaction", zap.String("tx.hash", res.TxHash), zap.Any("response", txRes)) return res.TxHash, nil } } From 46940e3e50c9d655d026648618d018bbe8fdf37c Mon Sep 17 00:00:00 2001 From: bonedaddy Date: Mon, 17 Jul 2023 16:48:46 -0700 Subject: [PATCH 13/13] add helper functions to retrieve configured keypair info --- utils.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/utils.go b/utils.go index 47cb452..1f11d4d 100644 --- a/utils.go +++ b/utils.go @@ -103,3 +103,13 @@ func (c *Client) BroadcastTx(ctx context.Context, msgs ...sdk.Msg) (string, erro } } } + +// 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() +}