diff --git a/client/v2/CHANGELOG.md b/client/v2/CHANGELOG.md index 5cff1928e437..27f312a99925 100644 --- a/client/v2/CHANGELOG.md +++ b/client/v2/CHANGELOG.md @@ -54,6 +54,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### API Breaking Changes * [#17709](https://github.com/cosmos/cosmos-sdk/pull/17709) Address codecs have been removed from `autocli.AppOptions` and `flag.Builder`. Instead client/v2 uses the address codecs present in the context (introduced in [#17503](https://github.com/cosmos/cosmos-sdk/pull/17503)). +* [#22493](https://github.com/cosmos/cosmos-sdk/pull/22493) Refactored `client/v2` package to remove v1 context dependencies, while introducing new packages for client configuration, context management, and formatted output with improved transaction handling and flag support. ### Bug Fixes diff --git a/client/v2/autocli/app.go b/client/v2/autocli/app.go index 30b5138c1ee3..5e1316b4127d 100644 --- a/client/v2/autocli/app.go +++ b/client/v2/autocli/app.go @@ -3,19 +3,20 @@ package autocli import ( "github.com/cosmos/gogoproto/proto" "github.com/spf13/cobra" - "google.golang.org/grpc" "google.golang.org/protobuf/reflect/protoregistry" autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" "cosmossdk.io/client/v2/autocli/flag" + "cosmossdk.io/core/address" "cosmossdk.io/core/appmodule" "cosmossdk.io/depinject" "cosmossdk.io/log" "cosmossdk.io/x/tx/signing" - "github.com/cosmos/cosmos-sdk/client" sdkflags "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" ) // AppOptions are input options for an autocli enabled app. These options can be built via depinject based on an app config. @@ -38,8 +39,15 @@ type AppOptions struct { // module or need to be improved. ModuleOptions map[string]*autocliv1.ModuleOptions `optional:"true"` - // ClientCtx contains the necessary information needed to execute the commands. - ClientCtx client.Context + AddressCodec address.Codec // AddressCodec is used to encode/decode account addresses. + ValidatorAddressCodec address.ValidatorAddressCodec // ValidatorAddressCodec is used to encode/decode validator addresses. + ConsensusAddressCodec address.ConsensusAddressCodec // ConsensusAddressCodec is used to encode/decode consensus addresses. + + // Cdc is the codec used for binary encoding/decoding of messages. + Cdc codec.Codec + + // TxConfigOpts contains options for configuring transaction handling. + TxConfigOpts authtx.ConfigOptions skipValidation bool } @@ -63,19 +71,19 @@ func (appOptions AppOptions) EnhanceRootCommand(rootCmd *cobra.Command) error { builder := &Builder{ Builder: flag.Builder{ TypeResolver: protoregistry.GlobalTypes, - FileResolver: appOptions.ClientCtx.InterfaceRegistry, - AddressCodec: appOptions.ClientCtx.AddressCodec, - ValidatorAddressCodec: appOptions.ClientCtx.ValidatorAddressCodec, - ConsensusAddressCodec: appOptions.ClientCtx.ConsensusAddressCodec, - }, - GetClientConn: func(cmd *cobra.Command) (grpc.ClientConnInterface, error) { - return client.GetClientQueryContext(cmd) + FileResolver: appOptions.Cdc.InterfaceRegistry(), + AddressCodec: appOptions.AddressCodec, + ValidatorAddressCodec: appOptions.ValidatorAddressCodec, + ConsensusAddressCodec: appOptions.ConsensusAddressCodec, }, + GetClientConn: getQueryClientConn(appOptions.Cdc), AddQueryConnFlags: func(c *cobra.Command) { sdkflags.AddQueryFlagsToCmd(c) sdkflags.AddKeyringFlags(c.Flags()) }, - AddTxConnFlags: sdkflags.AddTxFlagsToCmd, + AddTxConnFlags: sdkflags.AddTxFlagsToCmd, + Cdc: appOptions.Cdc, + EnabledSignModes: appOptions.TxConfigOpts.EnabledSignModes, } return appOptions.EnhanceRootCommandWithBuilder(rootCmd, builder) @@ -170,9 +178,9 @@ func NewAppOptionsFromConfig( return AppOptions{ Modules: cfg.Modules, - ClientCtx: client.Context{InterfaceRegistry: interfaceRegistry}, ModuleOptions: moduleOptions, skipValidation: true, + Cdc: codec.NewProtoCodec(interfaceRegistry), }, nil } diff --git a/client/v2/autocli/builder.go b/client/v2/autocli/builder.go index 81604f0d810b..475dac8af6d6 100644 --- a/client/v2/autocli/builder.go +++ b/client/v2/autocli/builder.go @@ -5,6 +5,9 @@ import ( "google.golang.org/grpc" "cosmossdk.io/client/v2/autocli/flag" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/tx/signing" ) // Builder manages options for building CLI commands. @@ -19,6 +22,9 @@ type Builder struct { // AddQueryConnFlags and AddTxConnFlags are functions that add flags to query and transaction commands AddQueryConnFlags func(*cobra.Command) AddTxConnFlags func(*cobra.Command) + + Cdc codec.Codec + EnabledSignModes []signing.SignMode } // ValidateAndComplete the builder fields. diff --git a/client/v2/autocli/common.go b/client/v2/autocli/common.go index 409198267cfd..ff3b5b184f5a 100644 --- a/client/v2/autocli/common.go +++ b/client/v2/autocli/common.go @@ -1,18 +1,29 @@ package autocli import ( + "context" + "crypto/tls" "fmt" - "strings" + "strconv" "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + grpcinsecure "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/reflect/protoreflect" - "sigs.k8s.io/yaml" autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" + apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/client/v2/autocli/config" + "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/client/v2/broadcast/comet" + clientcontext "cosmossdk.io/client/v2/context" "cosmossdk.io/client/v2/internal/flags" + "cosmossdk.io/client/v2/internal/print" "cosmossdk.io/client/v2/internal/util" - "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/tx/signing" ) type cmdType int @@ -62,8 +73,13 @@ func (b *Builder) buildMethodCommandCommon(descriptor protoreflect.MethodDescrip } cmd.Args = binder.CobraArgs + cmd.PreRunE = b.preRunE() + cmd.RunE = func(cmd *cobra.Command, args []string) error { - ctx = cmd.Context() + ctx, err = b.getContext(cmd) + if err != nil { + return err + } input, err := binder.BuildMessage(args) if err != nil { @@ -237,27 +253,132 @@ func enhanceCustomCmd(builder *Builder, cmd *cobra.Command, cmdType cmdType, mod // outOrStdoutFormat formats the output based on the output flag and writes it to the command's output stream. func (b *Builder) outOrStdoutFormat(cmd *cobra.Command, out []byte) error { - clientCtx := client.Context{} - if v := cmd.Context().Value(client.ClientContextKey); v != nil { - clientCtx = *(v.(*client.Context)) + p, err := print.NewPrinter(cmd) + if err != nil { + return err + } + return p.PrintBytes(out) +} + +// getContext creates and returns a new context.Context with an autocli.Context value. +// It initializes a printer and, if necessary, a keyring based on command flags. +func (b *Builder) getContext(cmd *cobra.Command) (context.Context, error) { + // if the command uses the keyring this must be set + var ( + k keyring.Keyring + err error + ) + if cmd.Flags().Lookup(flags.FlagKeyringDir) != nil && cmd.Flags().Lookup(flags.FlagKeyringBackend) != nil { + k, err = keyring.NewKeyringFromFlags(cmd.Flags(), b.AddressCodec, cmd.InOrStdin(), b.Cdc) + if err != nil { + return nil, err + } + } else { + k = keyring.NoKeyring{} } - flagSet := cmd.Flags() - if clientCtx.OutputFormat == "" || flagSet.Changed(flags.FlagOutput) { - output, _ := flagSet.GetString(flags.FlagOutput) - clientCtx = clientCtx.WithOutputFormat(output) + + clientCtx := clientcontext.Context{ + Flags: cmd.Flags(), + AddressCodec: b.AddressCodec, + ValidatorAddressCodec: b.ValidatorAddressCodec, + ConsensusAddressCodec: b.ConsensusAddressCodec, + Cdc: b.Cdc, + Keyring: k, + EnabledSignModes: signModesToApiSignModes(b.EnabledSignModes), } - var err error - outputType := clientCtx.OutputFormat - // if the output type is text, convert the json to yaml - // if output type is json or nil, default to json - if outputType == flags.OutputFormatText { - out, err = yaml.JSONToYAML(out) + return clientcontext.SetInContext(cmd.Context(), clientCtx), nil +} + +// preRunE returns a function that sets flags from the configuration before running a command. +// It is used as a PreRunE hook for cobra commands to ensure flags are properly initialized +// from the configuration before command execution. +func (b *Builder) preRunE() func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + err := b.setFlagsFromConfig(cmd) if err != nil { return err } + + return nil + } +} + +// setFlagsFromConfig sets command flags from the provided configuration. +// It only sets flags that haven't been explicitly changed by the user. +func (b *Builder) setFlagsFromConfig(cmd *cobra.Command) error { + conf, err := config.CreateClientConfigFromFlags(cmd.Flags()) + if err != nil { + return err + } + + flagsToSet := map[string]string{ + flags.FlagChainID: conf.ChainID, + flags.FlagKeyringBackend: conf.KeyringBackend, + flags.FlagFrom: conf.KeyringDefaultKeyName, + flags.FlagOutput: conf.Output, + flags.FlagNode: conf.Node, + flags.FlagBroadcastMode: conf.BroadcastMode, + flags.FlagGrpcAddress: conf.GRPC.Address, + flags.FlagGrpcInsecure: strconv.FormatBool(conf.GRPC.Insecure), + } + + for flagName, value := range flagsToSet { + if flag := cmd.Flags().Lookup(flagName); flag != nil && !cmd.Flags().Changed(flagName) { + if err := cmd.Flags().Set(flagName, value); err != nil { + return err + } + } } - cmd.Println(strings.TrimSpace(string(out))) return nil } + +// getQueryClientConn returns a function that creates a gRPC client connection based on command flags. +// It handles the creation of secure or insecure connections and falls back to a CometBFT broadcaster +// if no gRPC address is specified. +func getQueryClientConn(cdc codec.Codec) func(cmd *cobra.Command) (grpc.ClientConnInterface, error) { + return func(cmd *cobra.Command) (grpc.ClientConnInterface, error) { + var err error + creds := grpcinsecure.NewCredentials() + + insecure := true + if cmd.Flags().Lookup(flags.FlagGrpcInsecure) != nil { + insecure, err = cmd.Flags().GetBool(flags.FlagGrpcInsecure) + if err != nil { + return nil, err + } + } + if !insecure { + creds = credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}) + } + + var addr string + if cmd.Flags().Lookup(flags.FlagGrpcAddress) != nil { + addr, err = cmd.Flags().GetString(flags.FlagGrpcAddress) + if err != nil { + return nil, err + } + } + if addr == "" { + // if grpc-addr has not been set, use the default clientConn + // TODO: default is comet + node, err := cmd.Flags().GetString(flags.FlagNode) + if err != nil { + return nil, err + } + return comet.NewCometBFTBroadcaster(node, comet.BroadcastSync, cdc) + } + + return grpc.NewClient(addr, []grpc.DialOption{grpc.WithTransportCredentials(creds)}...) + } +} + +// signModesToApiSignModes converts a slice of signing.SignMode to a slice of apitxsigning.SignMode. +func signModesToApiSignModes(modes []signing.SignMode) []apitxsigning.SignMode { + r := make([]apitxsigning.SignMode, len(modes)) + for i, m := range modes { + r[i] = apitxsigning.SignMode(m) + } + return r +} diff --git a/client/v2/autocli/common_test.go b/client/v2/autocli/common_test.go index 30827fb3d278..b40f1a6dbc26 100644 --- a/client/v2/autocli/common_test.go +++ b/client/v2/autocli/common_test.go @@ -32,6 +32,10 @@ type fixture struct { conn *testClientConn b *Builder clientCtx client.Context + + home string + chainID string + kBackend string } func initFixture(t *testing.T) *fixture { @@ -85,7 +89,8 @@ func initFixture(t *testing.T) *fixture { return conn, nil }, AddQueryConnFlags: flags.AddQueryFlagsToCmd, - AddTxConnFlags: flags.AddTxFlagsToCmd, + AddTxConnFlags: addTxAndGlobalFlagsToCmd, + Cdc: encodingConfig.Codec, } assert.NilError(t, b.ValidateAndComplete()) @@ -93,9 +98,19 @@ func initFixture(t *testing.T) *fixture { conn: conn, b: b, clientCtx: clientCtx, + + home: home, + chainID: "autocli-test", + kBackend: sdkkeyring.BackendMemory, } } +func addTxAndGlobalFlagsToCmd(cmd *cobra.Command) { + f := cmd.Flags() + f.String("home", "", "home directory") + flags.AddTxFlagsToCmd(cmd) +} + func runCmd(fixture *fixture, command func(moduleName string, f *fixture) (*cobra.Command, error), args ...string) (*bytes.Buffer, error) { out := &bytes.Buffer{} cmd, err := command("test", fixture) diff --git a/client/v2/autocli/config/config.go b/client/v2/autocli/config/config.go new file mode 100644 index 000000000000..c775acc5ce18 --- /dev/null +++ b/client/v2/autocli/config/config.go @@ -0,0 +1,133 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/pelletier/go-toml/v2" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "cosmossdk.io/client/v2/internal/flags" +) + +type Config struct { + ChainID string `mapstructure:"chain-id" toml:"chain-id" comment:"The chain ID of the blockchain network"` + KeyringBackend string `mapstructure:"keyring-backend" toml:"keyring-backend" comment:"The keyring backend to use (os|file|kwallet|pass|test|memory)"` + KeyringDefaultKeyName string `mapstructure:"keyring-default-keyname" toml:"keyring-default-keyname" comment:"The default key name to use for signing transactions"` + Output string `mapstructure:"output" toml:"output" comment:"The output format for queries (text|json)"` + Node string `mapstructure:"node" toml:"node" comment:"The RPC endpoint URL for the node to connect to"` + BroadcastMode string `mapstructure:"broadcast-mode" toml:"broadcast-mode" comment:"How transactions are broadcast to the network (sync|async|block)"` + GRPC GRPCConfig `mapstructure:",squash" comment:"The gRPC client configuration"` +} + +// GRPCConfig holds the gRPC client configuration. +type GRPCConfig struct { + Address string `mapstructure:"grpc-address" toml:"grpc-address" comment:"The gRPC server address to connect to"` + Insecure bool `mapstructure:"grpc-insecure" toml:"grpc-insecure" comment:"Allow gRPC over insecure connections"` +} + +func DefaultConfig() *Config { + return &Config{ + ChainID: "", + KeyringBackend: "os", + KeyringDefaultKeyName: "", + Output: "text", + Node: "tcp://localhost:26657", + BroadcastMode: "sync", + } +} + +// CreateClientConfig creates a new client configuration or reads an existing one. +func CreateClientConfig(homeDir, chainID string, v *viper.Viper) (*Config, error) { + if homeDir == "" { + return nil, errors.New("home dir can't be empty") + } + + configPath := filepath.Join(homeDir, "config") + configFilePath := filepath.Join(configPath, "client.toml") + + // when client.toml does not exist create and init with default values + if _, err := os.Stat(configFilePath); os.IsNotExist(err) { + if err := os.MkdirAll(configPath, os.ModePerm); err != nil { + return nil, fmt.Errorf("couldn't make client config: %w", err) + } + + conf := DefaultConfig() + if chainID != "" { + // chain-id will be written to the client.toml while initiating the chain. + conf.ChainID = chainID + } + + if err := writeConfigFile(configFilePath, conf); err != nil { + return nil, fmt.Errorf("could not write client config to the file: %w", err) + } + } + + conf, err := readConfig(configPath, v) + if err != nil { + return nil, fmt.Errorf("couldn't get client config: %w", err) + } + + return conf, nil +} + +// CreateClientConfigFromFlags creates a client configuration from command-line flags. +func CreateClientConfigFromFlags(set *pflag.FlagSet) (*Config, error) { + homeDir, _ := set.GetString(flags.FlagHome) + if homeDir == "" { + return DefaultConfig(), nil + } + chainID, _ := set.GetString(flags.FlagChainID) + + v := viper.New() + executableName, err := os.Executable() + if err != nil { + return nil, err + } + + v.SetEnvPrefix(path.Base(executableName)) + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + v.AutomaticEnv() + + return CreateClientConfig(homeDir, chainID, v) +} + +// writeConfigFile renders config using the template and writes it to +// configFilePath. +func writeConfigFile(configFilePath string, config *Config) error { + b, err := toml.Marshal(config) + if err != nil { + return err + } + + if dir := filepath.Dir(configFilePath); dir != "" { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + } + + return os.WriteFile(configFilePath, b, 0o600) +} + +// readConfig reads values from client.toml file and unmarshalls them into ClientConfig +func readConfig(configPath string, v *viper.Viper) (*Config, error) { + v.AddConfigPath(configPath) + v.SetConfigName("client") + v.SetConfigType("toml") + + if err := v.ReadInConfig(); err != nil { + return nil, err + } + + conf := DefaultConfig() + if err := v.Unmarshal(conf); err != nil { + return nil, err + } + + return conf, nil +} diff --git a/client/v2/autocli/flag/address.go b/client/v2/autocli/flag/address.go index 454c30a317dd..f7ba4310675f 100644 --- a/client/v2/autocli/flag/address.go +++ b/client/v2/autocli/flag/address.go @@ -7,13 +7,12 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" "cosmossdk.io/client/v2/autocli/keyring" + clientcontext "cosmossdk.io/client/v2/context" "cosmossdk.io/core/address" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" - sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" ) @@ -44,19 +43,19 @@ type addressValue struct { value string } -func (a addressValue) Get(protoreflect.Value) (protoreflect.Value, error) { +func (a *addressValue) Get(protoreflect.Value) (protoreflect.Value, error) { return protoreflect.ValueOfString(a.value), nil } -func (a addressValue) String() string { +func (a *addressValue) String() string { return a.value } // Set implements the flag.Value interface for addressValue. func (a *addressValue) Set(s string) error { // we get the keyring on set, as in NewValue the context is the parent context (before RunE) - keyring := getKeyringFromCtx(a.ctx) - addr, err := keyring.LookupAddressByKeyName(s) + k := getKeyringFromCtx(a.ctx) + addr, err := k.LookupAddressByKeyName(s) if err == nil { addrStr, err := a.addressCodec.BytesToString(addr) if err != nil { @@ -77,7 +76,7 @@ func (a *addressValue) Set(s string) error { return nil } -func (a addressValue) Type() string { +func (a *addressValue) Type() string { return "account address or key name" } @@ -110,8 +109,8 @@ func (a consensusAddressValue) String() string { func (a *consensusAddressValue) Set(s string) error { // we get the keyring on set, as in NewValue the context is the parent context (before RunE) - keyring := getKeyringFromCtx(a.ctx) - addr, err := keyring.LookupAddressByKeyName(s) + k := getKeyringFromCtx(a.ctx) + addr, err := k.LookupAddressByKeyName(s) if err == nil { addrStr, err := a.addressCodec.BytesToString(addr) if err != nil { @@ -147,20 +146,18 @@ func (a *consensusAddressValue) Set(s string) error { return nil } +// getKeyringFromCtx retrieves the keyring from the provided context. +// If the context is nil or does not contain a valid client context, +// it returns a no-op keyring implementation. 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, clientCtx.(*client.Context).AddressCodec) - if err != nil { - panic(fmt.Errorf("failed to create keyring: %w", err)) - } - - return k - } else if k := dctx.Value(keyring.KeyringContextKey); k != nil { - return k.(*keyring.KeyringImpl) - } + if *ctx == nil { + return keyring.NoKeyring{} + } + + c, err := clientcontext.ClientContextFromGoContext(*ctx) + if err != nil { + return keyring.NoKeyring{} } - return keyring.NoKeyring{} + return c.Keyring } diff --git a/client/v2/autocli/keyring/keyring.go b/client/v2/autocli/keyring/keyring.go index f5dce25efceb..73c523a6e0f3 100644 --- a/client/v2/autocli/keyring/keyring.go +++ b/client/v2/autocli/keyring/keyring.go @@ -1,10 +1,15 @@ package keyring import ( - "context" + "io" + + "github.com/spf13/pflag" signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/core/address" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/crypto/types" ) @@ -20,9 +25,32 @@ type KeyringImpl struct { k Keyring } -// NewKeyringInContext returns a new context with the keyring set. -func NewKeyringInContext(ctx context.Context, k Keyring) context.Context { - return context.WithValue(ctx, KeyringContextKey, NewKeyringImpl(k)) +// NewKeyringFromFlags creates a new Keyring instance based on command-line flags. +// It retrieves the keyring backend and directory from flags, creates a new keyring, +// and wraps it with an AutoCLI-compatible interface. +func NewKeyringFromFlags(flagSet *pflag.FlagSet, ac address.Codec, input io.Reader, cdc codec.Codec, opts ...keyring.Option) (Keyring, error) { + backEnd, err := flagSet.GetString("keyring-backend") + if err != nil { + return nil, err + } + + keyringDir, err := flagSet.GetString("keyring-dir") + if err != nil { + return nil, err + } + if keyringDir == "" { + keyringDir, err = flagSet.GetString("home") + if err != nil { + return nil, err + } + } + + k, err := keyring.New("autoclikeyring", backEnd, keyringDir, input, cdc, opts...) + if err != nil { + return nil, err + } + + return keyring.NewAutoCLIKeyring(k, ac) } func NewKeyringImpl(k Keyring) *KeyringImpl { diff --git a/client/v2/autocli/msg.go b/client/v2/autocli/msg.go index 9eb4f0444bba..9b30a56fe375 100644 --- a/client/v2/autocli/msg.go +++ b/client/v2/autocli/msg.go @@ -1,6 +1,7 @@ package autocli import ( + "bufio" "context" "fmt" @@ -13,8 +14,11 @@ import ( autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" "cosmossdk.io/client/v2/autocli/flag" "cosmossdk.io/client/v2/internal/flags" + "cosmossdk.io/client/v2/internal/print" "cosmossdk.io/client/v2/internal/util" + v2tx "cosmossdk.io/client/v2/tx" addresscodec "cosmossdk.io/core/address" + "cosmossdk.io/core/transaction" // the following will be extracted to a separate module // https://github.com/cosmos/cosmos-sdk/issues/14403 @@ -23,6 +27,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/input" clienttx "github.com/cosmos/cosmos-sdk/client/tx" ) @@ -228,3 +233,76 @@ func (b *Builder) handleGovProposal( return clienttx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), proposal) } + +// generateOrBroadcastTxWithV2 generates or broadcasts a transaction with the provided messages using v2 transaction handling. +// +//nolint:unused // It'll be used once BuildMsgMethodCommand is updated to use factory v2. +func (b *Builder) generateOrBroadcastTxWithV2(cmd *cobra.Command, msgs ...transaction.Msg) error { + ctx, err := b.getContext(cmd) + if err != nil { + return err + } + + cConn, err := b.GetClientConn(cmd) + if err != nil { + return err + } + + var bz []byte + genOnly, _ := cmd.Flags().GetBool(v2tx.FlagGenerateOnly) + isDryRun, _ := cmd.Flags().GetBool(v2tx.FlagDryRun) + if genOnly { + bz, err = v2tx.GenerateOnly(ctx, cConn, msgs...) + } else if isDryRun { + bz, err = v2tx.DryRun(ctx, cConn, msgs...) + } else { + skipConfirm, _ := cmd.Flags().GetBool("yes") + if skipConfirm { + bz, err = v2tx.GenerateAndBroadcastTxCLI(ctx, cConn, msgs...) + } else { + bz, err = v2tx.GenerateAndBroadcastTxCLIWithPrompt(ctx, cConn, b.userConfirmation(cmd), msgs...) + } + } + if err != nil { + return err + } + + output, _ := cmd.Flags().GetString(flags.FlagOutput) + p := print.Printer{ + Output: cmd.OutOrStdout(), + OutputFormat: output, + } + + return p.PrintBytes(bz) +} + +// userConfirmation returns a function that prompts the user for confirmation +// before signing and broadcasting a transaction. +// +//nolint:unused // It is used in generateOrBroadcastTxWithV2 however linting is complaining. +func (b *Builder) userConfirmation(cmd *cobra.Command) func([]byte) (bool, error) { + format, _ := cmd.Flags().GetString(flags.FlagOutput) + printer := print.Printer{ + Output: cmd.OutOrStdout(), + OutputFormat: format, + } + + return func(bz []byte) (bool, error) { + err := printer.PrintBytes(bz) + if err != nil { + return false, err + } + buf := bufio.NewReader(cmd.InOrStdin()) + ok, err := input.GetConfirmation("confirm transaction before signing and broadcasting", buf, cmd.ErrOrStderr()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "error: %v\ncanceled transaction\n", err) + return false, err + } + if !ok { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "canceled transaction") + return false, nil + } + + return true, nil + } +} diff --git a/client/v2/autocli/msg_test.go b/client/v2/autocli/msg_test.go index 11e6fd2d2fce..a86fc8ebcca7 100644 --- a/client/v2/autocli/msg_test.go +++ b/client/v2/autocli/msg_test.go @@ -55,6 +55,7 @@ func TestMsg(t *testing.T) { "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "1foo", "--generate-only", "--output", "json", + "--chain-id", fixture.chainID, ) assert.NilError(t, err) assertNormalizedJSONEqual(t, out.Bytes(), goldenLoad(t, "msg-output.golden")) @@ -74,6 +75,7 @@ func TestMsg(t *testing.T) { "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "1foo", "--generate-only", "--output", "json", + "--chain-id", fixture.chainID, ) assert.NilError(t, err) assertNormalizedJSONEqual(t, out.Bytes(), goldenLoad(t, "msg-output.golden")) @@ -93,8 +95,10 @@ func TestMsg(t *testing.T) { }), "send", "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "1foo", "--from", "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", - "--generate-only", "--output", "json", + "--generate-only", + "--chain-id", fixture.chainID, + "--keyring-backend", fixture.kBackend, ) assert.NilError(t, err) assertNormalizedJSONEqual(t, out.Bytes(), goldenLoad(t, "msg-output.golden")) @@ -116,8 +120,9 @@ func TestMsg(t *testing.T) { }), "send", "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "1foo", "--sender", "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", - "--generate-only", "--output", "json", + "--generate-only", + "--chain-id", fixture.chainID, ) assert.NilError(t, err) assertNormalizedJSONEqual(t, out.Bytes(), goldenLoad(t, "msg-output.golden")) diff --git a/client/v2/autocli/query.go b/client/v2/autocli/query.go index d308bcd7633a..aaf648f3578d 100644 --- a/client/v2/autocli/query.go +++ b/client/v2/autocli/query.go @@ -8,15 +8,15 @@ import ( "strings" "time" - autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" - "cosmossdk.io/math" - "cosmossdk.io/x/tx/signing/aminojson" - "github.com/spf13/cobra" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/reflect/protoreflect" + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" "cosmossdk.io/client/v2/internal/flags" "cosmossdk.io/client/v2/internal/util" + "cosmossdk.io/math" + "cosmossdk.io/x/tx/signing/aminojson" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -116,7 +116,6 @@ func (b *Builder) AddQueryServiceCommands(cmd *cobra.Command, cmdDescriptor *aut // BuildQueryMethodCommand creates a gRPC query command for the given service method. This can be used to auto-generate // just a single command for a single service rpc method. func (b *Builder) BuildQueryMethodCommand(ctx context.Context, descriptor protoreflect.MethodDescriptor, options *autocliv1.RpcCommandOptions) (*cobra.Command, error) { - getClientConn := b.GetClientConn serviceDescriptor := descriptor.Parent().(protoreflect.ServiceDescriptor) methodName := fmt.Sprintf("/%s/%s", serviceDescriptor.FullName(), descriptor.Name()) outputType := util.ResolveMessageType(b.TypeResolver, descriptor.Output()) @@ -130,13 +129,13 @@ func (b *Builder) BuildQueryMethodCommand(ctx context.Context, descriptor protor } cmd, err := b.buildMethodCommandCommon(descriptor, options, func(cmd *cobra.Command, input protoreflect.Message) error { - clientConn, err := getClientConn(cmd) + clientConn, err := b.GetClientConn(cmd) if err != nil { return err } output := outputType.New() - if err := clientConn.Invoke(cmd.Context(), methodName, input.Interface(), output.Interface()); err != nil { + if err := clientConn.Invoke(b.queryContext(cmd.Context(), cmd), methodName, input.Interface(), output.Interface()); err != nil { return err } @@ -170,6 +169,25 @@ func (b *Builder) BuildQueryMethodCommand(ctx context.Context, descriptor protor return cmd, nil } +// queryContext returns a new context with metadata for block height if specified. +// If the context already has metadata, it is returned as-is. Otherwise, if a height +// flag is present on the command, it adds an x-cosmos-block-height metadata value +// with the specified height. +func (b *Builder) queryContext(ctx context.Context, cmd *cobra.Command) context.Context { + md, _ := metadata.FromOutgoingContext(ctx) + if md != nil { + return ctx + } + + md = map[string][]string{} + if cmd.Flags().Lookup("height") != nil { + h, _ := cmd.Flags().GetInt64("height") + md["x-cosmos-block-height"] = []string{fmt.Sprintf("%d", h)} + } + + return metadata.NewOutgoingContext(ctx, md) +} + func encoder(encoder aminojson.Encoder) aminojson.Encoder { return encoder.DefineTypeEncoding("google.protobuf.Duration", func(_ *aminojson.Encoder, msg protoreflect.Message, w io.Writer) error { var ( diff --git a/client/v2/autocli/testdata/help-echo-msg.golden b/client/v2/autocli/testdata/help-echo-msg.golden index 1307509569c9..0761494efde8 100644 --- a/client/v2/autocli/testdata/help-echo-msg.golden +++ b/client/v2/autocli/testdata/help-echo-msg.golden @@ -18,6 +18,7 @@ Flags: --gas-prices string Determine the transaction fee by multiplying max gas units by gas prices (e.g. 0.1uatom), rounding up to nearest denom unit --generate-only Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase only accessed when providing a key name) -h, --help help for send + --home string home directory --keyring-backend string Select keyring's backend (os|file|kwallet|pass|test|memory) (default "os") --keyring-dir string The client Keyring directory; if omitted, the default 'home' directory will be used --ledger Use a connected Ledger device diff --git a/client/v2/broadcast/comet/client_conn.go b/client/v2/broadcast/comet/client_conn.go new file mode 100644 index 000000000000..df93b1af86e3 --- /dev/null +++ b/client/v2/broadcast/comet/client_conn.go @@ -0,0 +1,146 @@ +package comet + +import ( + "context" + "errors" + "strconv" + + abci "github.com/cometbft/cometbft/api/cometbft/abci/v1" + rpcclient "github.com/cometbft/cometbft/rpc/client" + gogogrpc "github.com/cosmos/gogoproto/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const grpcBlockHeightHeader = "x-cosmos-block-height" + +var ( + _ gogogrpc.ClientConn = &CometBFTBroadcaster{} + _ grpc.ClientConnInterface = &CometBFTBroadcaster{} +) + +func (c *CometBFTBroadcaster) NewStream(_ context.Context, _ *grpc.StreamDesc, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, errors.New("not implemented") +} + +// Invoke implements the gRPC ClientConn interface by forwarding the RPC call to CometBFT's ABCI Query. +// It marshals the request, sends it as an ABCI query, and unmarshals the response. +func (c *CometBFTBroadcaster) Invoke(ctx context.Context, method string, req, reply interface{}, opts ...grpc.CallOption) (err error) { + reqBz, err := c.getRPCCodec().Marshal(req) + if err != nil { + return err + } + + // parse height header + md, _ := metadata.FromOutgoingContext(ctx) + var height int64 + if heights := md.Get(grpcBlockHeightHeader); len(heights) > 0 { + height, err = strconv.ParseInt(heights[0], 10, 64) + if err != nil { + return err + } + if height < 0 { + return errorsmod.Wrapf( + sdkerrors.ErrInvalidRequest, + "client.Context.Invoke: height (%d) from %q must be >= 0", height, grpcBlockHeightHeader) + } + } + + abciR := abci.QueryRequest{ + Path: method, + Data: reqBz, + Height: height, + } + + res, err := c.queryABCI(ctx, abciR) + if err != nil { + return err + } + + err = c.getRPCCodec().Unmarshal(res.Value, reply) + if err != nil { + return err + } + + // Create header metadata. For now the headers contain: + // - block height + // We then parse all the call options, if the call option is a + // HeaderCallOption, then we manually set the value of that header to the + // metadata. + md = metadata.Pairs(grpcBlockHeightHeader, strconv.FormatInt(res.Height, 10)) + for _, callOpt := range opts { + header, ok := callOpt.(grpc.HeaderCallOption) + if !ok { + continue + } + + *header.HeaderAddr = md + } + + if c.cdc.InterfaceRegistry() != nil { + return types.UnpackInterfaces(reply, c.cdc.InterfaceRegistry()) + } + + return nil +} + +// queryABCI performs an ABCI query request to the CometBFT RPC client. +// If the RPC query fails or returns a non-OK response, it will return an error. +// The response is converted from ABCI error codes to gRPC status errors. +func (c *CometBFTBroadcaster) queryABCI(ctx context.Context, req abci.QueryRequest) (abci.QueryResponse, error) { + opts := rpcclient.ABCIQueryOptions{ + Height: req.Height, + Prove: req.Prove, + } + + result, err := c.rpcClient.ABCIQueryWithOptions(ctx, req.Path, req.Data, opts) + if err != nil { + return abci.QueryResponse{}, err + } + + if !result.Response.IsOK() { + return abci.QueryResponse{}, sdkErrorToGRPCError(result.Response) + } + + return result.Response, nil +} + +// sdkErrorToGRPCError converts an ABCI query response error code to an appropriate gRPC status error. +// It maps common SDK error codes to their gRPC equivalents: +// - ErrInvalidRequest -> InvalidArgument +// - ErrUnauthorized -> Unauthenticated +// - ErrKeyNotFound -> NotFound +// Any other error codes are mapped to Unknown. +func sdkErrorToGRPCError(resp abci.QueryResponse) error { + switch resp.Code { + case sdkerrors.ErrInvalidRequest.ABCICode(): + return status.Error(codes.InvalidArgument, resp.Log) + case sdkerrors.ErrUnauthorized.ABCICode(): + return status.Error(codes.Unauthenticated, resp.Log) + case sdkerrors.ErrKeyNotFound.ABCICode(): + return status.Error(codes.NotFound, resp.Log) + default: + return status.Error(codes.Unknown, resp.Log) + } +} + +// getRPCCodec returns the gRPC codec for the CometBFT broadcaster. +// If the broadcaster's codec implements GRPCCodecProvider, it returns its gRPC codec. +// Otherwise, it creates a new ProtoCodec with the broadcaster's interface registry and returns its gRPC codec. +func (c *CometBFTBroadcaster) getRPCCodec() encoding.Codec { + cdc, ok := c.cdc.(codec.GRPCCodecProvider) + if !ok { + return codec.NewProtoCodec(c.cdc.InterfaceRegistry()).GRPCCodec() + } + + return cdc.GRPCCodec() +} diff --git a/client/v2/broadcast/comet/comet.go b/client/v2/broadcast/comet/comet.go index d6ab7f904477..6fee9fe27e85 100644 --- a/client/v2/broadcast/comet/comet.go +++ b/client/v2/broadcast/comet/comet.go @@ -66,11 +66,11 @@ var _ broadcast.Broadcaster = &CometBFTBroadcaster{} type CometBFTBroadcaster struct { rpcClient CometRPC mode string - cdc codec.JSONCodec + cdc codec.Codec } // NewCometBFTBroadcaster creates a new CometBFTBroadcaster. -func NewCometBFTBroadcaster(rpcURL, mode string, cdc codec.JSONCodec) (*CometBFTBroadcaster, error) { +func NewCometBFTBroadcaster(rpcURL, mode string, cdc codec.Codec) (*CometBFTBroadcaster, error) { if cdc == nil { return nil, errors.New("codec can't be nil") } diff --git a/client/v2/broadcast/comet/comet_test.go b/client/v2/broadcast/comet/comet_test.go index 0eb8b81685ed..69c032f2e12d 100644 --- a/client/v2/broadcast/comet/comet_test.go +++ b/client/v2/broadcast/comet/comet_test.go @@ -22,7 +22,7 @@ var cdc = testutil.CodecOptions{}.NewCodec() func TestNewCometBftBroadcaster(t *testing.T) { tests := []struct { name string - cdc codec.JSONCodec + cdc codec.Codec mode string want *CometBFTBroadcaster wantErr bool diff --git a/client/v2/context/context.go b/client/v2/context/context.go new file mode 100644 index 000000000000..fdb65b517498 --- /dev/null +++ b/client/v2/context/context.go @@ -0,0 +1,55 @@ +package context + +import ( + gocontext "context" + "errors" + + "github.com/spf13/pflag" + + apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/core/address" + + "github.com/cosmos/cosmos-sdk/codec" +) + +// ContextKey is a key used to store and retrieve Context from a Go context.Context. +var ContextKey contextKey + +// contextKey is an empty struct used as a key type for storing Context in a context.Context. +type contextKey struct{} + +// Context represents the client context used in autocli commands. +// It contains various components needed for command execution. +type Context struct { + Flags *pflag.FlagSet + + AddressCodec address.Codec + ValidatorAddressCodec address.ValidatorAddressCodec + ConsensusAddressCodec address.ConsensusAddressCodec + + Cdc codec.Codec + + Keyring keyring.Keyring + + EnabledSignModes []apisigning.SignMode +} + +// SetInContext stores the provided autocli.Context in the given Go context.Context. +// It returns a new context.Context containing the autocli.Context value. +func SetInContext(goCtx gocontext.Context, cliCtx Context) gocontext.Context { + return gocontext.WithValue(goCtx, ContextKey, cliCtx) +} + +// ClientContextFromGoContext returns the autocli.Context from a given Go context. +// It checks if the context contains a valid autocli.Context and returns it. +func ClientContextFromGoContext(ctx gocontext.Context) (*Context, error) { + if c := ctx.Value(ContextKey); c != nil { + cliCtx, ok := c.(Context) + if !ok { + return nil, errors.New("context value is not of type autocli.Context") + } + return &cliCtx, nil + } + return nil, errors.New("context does not contain autocli.Context value") +} diff --git a/client/v2/go.mod b/client/v2/go.mod index b39ea3a3eaf8..ced3d482adfc 100644 --- a/client/v2/go.mod +++ b/client/v2/go.mod @@ -36,7 +36,7 @@ require ( buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.35.2-20240130113600-88ef6483f90f.1 // indirect cosmossdk.io/collections v0.4.1-0.20241128094659-bd76b47e1d8b // indirect cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e // indirect - cosmossdk.io/errors v1.0.1 // indirect + cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.5.0 cosmossdk.io/math v1.4.0 cosmossdk.io/schema v0.3.1-0.20241128094659-bd76b47e1d8b // indirect @@ -60,7 +60,7 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/cometbft/cometbft v1.0.0-rc2.0.20241127125717-4ce33b646ac9 github.com/cometbft/cometbft-db v1.0.1 // indirect - github.com/cometbft/cometbft/api v1.0.0-rc2 // indirect + github.com/cometbft/cometbft/api v1.0.0-rc2 github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.0 // indirect github.com/cosmos/go-bip39 v1.0.0 @@ -127,7 +127,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -145,7 +145,7 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/viper v1.19.0 // indirect + github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.13 // indirect diff --git a/client/v2/internal/flags/flags.go b/client/v2/internal/flags/flags.go index d06cef708cad..7f684ac43544 100644 --- a/client/v2/internal/flags/flags.go +++ b/client/v2/internal/flags/flags.go @@ -2,6 +2,12 @@ package flags // This defines flag names that can be used in autocli. const ( + // FlagHome is the flag to specify the home dir of the app. + FlagHome = "home" + + // FlagChainID is the flag to specify the chain ID of the network. + FlagChainID = "chain-id" + // FlagFrom is the flag to set the from address with which to sign the transaction. FlagFrom = "from" @@ -14,9 +20,24 @@ const ( // FlagNoPrompt is the flag to not use a prompt for commands. FlagNoPrompt = "no-prompt" + // FlagKeyringDir is the flag to specify the directory where the keyring is stored. + FlagKeyringDir = "keyring-dir" + // FlagKeyringBackend is the flag to specify which backend to use for the keyring (e.g. os, file, test). + FlagKeyringBackend = "keyring-backend" + // FlagNoProposal is the flag convert a gov proposal command into a normal command. // This is used to allow user of chains with custom authority to not use gov submit proposals for usual proposal commands. FlagNoProposal = "no-proposal" + + // FlagNode is the flag to specify the node address to connect to. + FlagNode = "node" + // FlagBroadcastMode is the flag to specify the broadcast mode for transactions. + FlagBroadcastMode = "broadcast-mode" + + // FlagGrpcAddress is the flag to specify the gRPC server address to connect to. + FlagGrpcAddress = "grpc-addr" + // FlagGrpcInsecure is the flag to allow insecure gRPC connections. + FlagGrpcInsecure = "grpc-insecure" ) // List of supported output formats diff --git a/client/v2/internal/print/printer.go b/client/v2/internal/print/printer.go new file mode 100644 index 000000000000..631281bcb0a2 --- /dev/null +++ b/client/v2/internal/print/printer.go @@ -0,0 +1,84 @@ +package print + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "cosmossdk.io/client/v2/internal/flags" +) + +const ( + jsonOutput = flags.OutputFormatJSON + textOutput = flags.OutputFormatText +) + +// Printer handles formatted output of different types of data +type Printer struct { + Output io.Writer + OutputFormat string +} + +// NewPrinter creates a new Printer instance with default stdout +func NewPrinter(cmd *cobra.Command) (*Printer, error) { + outputFormat, err := cmd.Flags().GetString("output") + if err != nil { + return nil, err + } + + if outputFormat != jsonOutput && outputFormat != textOutput { + return nil, fmt.Errorf("unsupported output format: %s", outputFormat) + } + + return &Printer{ + Output: cmd.OutOrStdout(), + OutputFormat: outputFormat, + }, nil +} + +// PrintString prints the raw string +func (p *Printer) PrintString(str string) error { + return p.PrintBytes([]byte(str)) +} + +// PrintRaw prints raw JSON message without marshaling +func (p *Printer) PrintRaw(toPrint json.RawMessage) error { + return p.PrintBytes(toPrint) +} + +// PrintBytes prints and formats bytes +func (p *Printer) PrintBytes(out []byte) error { + var err error + if p.OutputFormat == textOutput { + if !json.Valid(out) { + return fmt.Errorf("invalid JSON") + } + out, err = yaml.JSONToYAML(out) + if err != nil { + return err + } + } + + writer := p.Output + if writer == nil { + writer = os.Stdout + } + + _, err = writer.Write(out) + if err != nil { + return err + } + + if p.OutputFormat != textOutput { + _, err = writer.Write([]byte("\n")) + if err != nil { + return err + } + } + + return nil +} diff --git a/client/v2/offchain/cli.go b/client/v2/offchain/cli.go index 7738a6204451..024df5663912 100644 --- a/client/v2/offchain/cli.go +++ b/client/v2/offchain/cli.go @@ -6,15 +6,24 @@ import ( "github.com/spf13/cobra" + "cosmossdk.io/client/v2/autocli/config" + "cosmossdk.io/client/v2/autocli/keyring" + "cosmossdk.io/client/v2/broadcast/comet" + clientcontext "cosmossdk.io/client/v2/context" v2flags "cosmossdk.io/client/v2/internal/flags" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" ) const ( flagEncoding = "encoding" flagFileFormat = "file-format" + flagBech32 = "bech32" ) // OffChain off-chain utilities. @@ -31,6 +40,7 @@ func OffChain() *cobra.Command { ) flags.AddKeyringFlags(cmd.PersistentFlags()) + cmd.PersistentFlags().String(flagBech32, "cosmos", "address bech32 prefix") return cmd } @@ -42,7 +52,19 @@ func SignFile() *cobra.Command { Long: "Sign a file using a given key.", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - clientCtx := client.GetClientContextFromCmd(cmd) + ir := types.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(ir) + cdc := codec.NewProtoCodec(ir) + + c, err := config.CreateClientConfigFromFlags(cmd.Flags()) + if err != nil { + return err + } + + keyringBackend := c.KeyringBackend + if !cmd.Flags().Changed(v2flags.FlagKeyringBackend) { + _ = cmd.Flags().Set(v2flags.FlagKeyringBackend, keyringBackend) + } bz, err := os.ReadFile(args[1]) if err != nil { @@ -53,8 +75,29 @@ func SignFile() *cobra.Command { outputFormat, _ := cmd.Flags().GetString(v2flags.FlagOutput) outputFile, _ := cmd.Flags().GetString(flags.FlagOutputDocument) signMode, _ := cmd.Flags().GetString(flags.FlagSignMode) + bech32Prefix, _ := cmd.Flags().GetString(flagBech32) + + ac := address.NewBech32Codec(bech32Prefix) + k, err := keyring.NewKeyringFromFlags(cmd.Flags(), ac, cmd.InOrStdin(), cdc) + if err != nil { + return err + } + + // off-chain does not need to query any information + conn, err := comet.NewCometBFTBroadcaster("", comet.BroadcastSync, cdc) + if err != nil { + return err + } - signedTx, err := Sign(clientCtx, bz, args[0], encoding, signMode, outputFormat) + ctx := clientcontext.Context{ + Flags: cmd.Flags(), + AddressCodec: ac, + ValidatorAddressCodec: address.NewBech32Codec(sdk.GetBech32PrefixValAddr(bech32Prefix)), + Cdc: cdc, + Keyring: k, + } + + signedTx, err := Sign(ctx, bz, conn, args[0], encoding, signMode, outputFormat) if err != nil { return err } @@ -87,10 +130,8 @@ func VerifyFile() *cobra.Command { Long: "Verify a previously signed file with the given key.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - clientCtx, err := client.GetClientQueryContext(cmd) - if err != nil { - return err - } + ir := types.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(ir) bz, err := os.ReadFile(args[0]) if err != nil { @@ -98,8 +139,18 @@ func VerifyFile() *cobra.Command { } fileFormat, _ := cmd.Flags().GetString(flagFileFormat) + bech32Prefix, _ := cmd.Flags().GetString(flagBech32) + + ac := address.NewBech32Codec(bech32Prefix) + + ctx := clientcontext.Context{ + Flags: cmd.Flags(), + AddressCodec: ac, + ValidatorAddressCodec: address.NewBech32Codec(sdk.GetBech32PrefixValAddr(bech32Prefix)), + Cdc: cdc, + } - err = Verify(clientCtx, bz, fileFormat) + err = Verify(ctx, bz, fileFormat) if err == nil { cmd.Println("Verification OK!") } diff --git a/client/v2/offchain/common_test.go b/client/v2/offchain/common_test.go index 5b862fcb20bb..d455fa74d102 100644 --- a/client/v2/offchain/common_test.go +++ b/client/v2/offchain/common_test.go @@ -2,32 +2,17 @@ package offchain import ( "context" - "testing" + "errors" - "github.com/stretchr/testify/require" + gogogrpc "github.com/cosmos/gogoproto/grpc" "google.golang.org/grpc" - bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" - "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/client" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/codec/address" "github.com/cosmos/cosmos-sdk/codec/testutil" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" ) -const ( - addressCodecPrefix = "cosmos" - validatorAddressCodecPrefix = "cosmosvaloper" - mnemonic = "have embark stumble card pistol fun gauge obtain forget oil awesome lottery unfold corn sure original exist siren pudding spread uphold dwarf goddess card" -) +const mnemonic = "have embark stumble card pistol fun gauge obtain forget oil awesome lottery unfold corn sure original exist siren pudding spread uphold dwarf goddess card" func getCodec() codec.Codec { registry := testutil.CodecOptions{}.NewInterfaceRegistry() @@ -36,111 +21,14 @@ func getCodec() codec.Codec { return codec.NewProtoCodec(registry) } -func newGRPCCoinMetadataQueryFn(grpcConn grpc.ClientConnInterface) textual.CoinMetadataQueryFn { - return func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) { - bankQueryClient := bankv1beta1.NewQueryClient(grpcConn) - res, err := bankQueryClient.DenomMetadata(ctx, &bankv1beta1.QueryDenomMetadataRequest{ - Denom: denom, - }) - if err != nil { - return nil, err - } - - return res.Metadata, nil - } -} - -// testConfig fulfills client.TxConfig although SignModeHandler is the only method implemented. -type testConfig struct { - handler *signing.HandlerMap -} - -func (t testConfig) SignModeHandler() *signing.HandlerMap { - return t.handler -} - -func (t testConfig) TxEncoder() sdk.TxEncoder { - return nil -} +var _ gogogrpc.ClientConn = mockClientConn{} -func (t testConfig) TxDecoder() sdk.TxDecoder { - return nil -} +type mockClientConn struct{} -func (t testConfig) TxJSONEncoder() sdk.TxEncoder { - return nil +func (c mockClientConn) Invoke(_ context.Context, _ string, _, _ interface{}, _ ...grpc.CallOption) error { + return errors.New("not implemented") } -func (t testConfig) TxJSONDecoder() sdk.TxDecoder { - return nil -} - -func (t testConfig) MarshalSignatureJSON(v2s []signingtypes.SignatureV2) ([]byte, error) { - return nil, nil -} - -func (t testConfig) UnmarshalSignatureJSON(bytes []byte) ([]signingtypes.SignatureV2, error) { - return nil, nil -} - -func (t testConfig) NewTxBuilder() client.TxBuilder { - return nil -} - -func (t testConfig) WrapTxBuilder(s sdk.Tx) (client.TxBuilder, error) { - return nil, nil -} - -func (t testConfig) SigningContext() *signing.Context { - return nil -} - -func newTestConfig(t *testing.T) *testConfig { - t.Helper() - - enabledSignModes := []signingtypes.SignMode{ - signingtypes.SignMode_SIGN_MODE_DIRECT, - signingtypes.SignMode_SIGN_MODE_DIRECT_AUX, - signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, - signingtypes.SignMode_SIGN_MODE_TEXTUAL, - } - - var err error - signingOptions := signing.Options{ - AddressCodec: address.NewBech32Codec(addressCodecPrefix), - ValidatorAddressCodec: address.NewBech32Codec(validatorAddressCodecPrefix), - } - signingContext, err := signing.NewContext(signingOptions) - require.NoError(t, err) - - lenSignModes := len(enabledSignModes) - handlers := make([]signing.SignModeHandler, lenSignModes) - for i, m := range enabledSignModes { - var err error - switch m { - case signingtypes.SignMode_SIGN_MODE_DIRECT: - handlers[i] = &direct.SignModeHandler{} - case signingtypes.SignMode_SIGN_MODE_DIRECT_AUX: - handlers[i], err = directaux.NewSignModeHandler(directaux.SignModeHandlerOptions{ - TypeResolver: signingOptions.TypeResolver, - SignersContext: signingContext, - }) - require.NoError(t, err) - case signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON: - handlers[i] = aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{ - FileResolver: signingOptions.FileResolver, - TypeResolver: signingOptions.TypeResolver, - }) - case signingtypes.SignMode_SIGN_MODE_TEXTUAL: - handlers[i], err = textual.NewSignModeHandler(textual.SignModeOptions{ - CoinMetadataQuerier: newGRPCCoinMetadataQueryFn(client.Context{}), - FileResolver: signingOptions.FileResolver, - TypeResolver: signingOptions.TypeResolver, - }) - require.NoError(t, err) - } - } - - handler := signing.NewHandlerMap(handlers...) - return &testConfig{handler: handler} +func (c mockClientConn) NewStream(_ context.Context, _ *grpc.StreamDesc, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, errors.New("not implemented") } diff --git a/client/v2/offchain/sign.go b/client/v2/offchain/sign.go index 8dfcb907c089..1ce41de31857 100644 --- a/client/v2/offchain/sign.go +++ b/client/v2/offchain/sign.go @@ -4,13 +4,14 @@ import ( "context" "fmt" + gogogrpc "github.com/cosmos/gogoproto/grpc" + apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + clientcontext "cosmossdk.io/client/v2/context" "cosmossdk.io/client/v2/internal/account" "cosmossdk.io/client/v2/internal/offchain" clitx "cosmossdk.io/client/v2/tx" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/version" ) @@ -29,20 +30,20 @@ var enabledSignModes = []apisigning.SignMode{ } // Sign signs given bytes using the specified encoder and SignMode. -func Sign(ctx client.Context, rawBytes []byte, fromName, encoding, signMode, output string) (string, error) { +func Sign( + ctx clientcontext.Context, + rawBytes []byte, + conn gogogrpc.ClientConn, + fromName, encoding, signMode, output string, +) (string, error) { digest, err := encodeDigest(encoding, rawBytes) if err != nil { return "", err } - keybase, err := keyring.NewAutoCLIKeyring(ctx.Keyring, ctx.AddressCodec) - if err != nil { - return "", err - } - txConfig, err := clitx.NewTxConfig(clitx.ConfigOptions{ AddressCodec: ctx.AddressCodec, - Cdc: ctx.Codec, + Cdc: ctx.Cdc, ValidatorAddressCodec: ctx.ValidatorAddressCodec, EnabledSignModes: enabledSignModes, }) @@ -50,7 +51,7 @@ func Sign(ctx client.Context, rawBytes []byte, fromName, encoding, signMode, out return "", err } - accRetriever := account.NewAccountRetriever(ctx.AddressCodec, ctx, ctx.InterfaceRegistry) + accRetriever := account.NewAccountRetriever(ctx.AddressCodec, conn, ctx.Cdc.InterfaceRegistry()) sm, err := getSignMode(signMode) if err != nil { @@ -66,12 +67,12 @@ func Sign(ctx client.Context, rawBytes []byte, fromName, encoding, signMode, out }, } - txf, err := clitx.NewFactory(keybase, ctx.Codec, accRetriever, txConfig, ctx.AddressCodec, ctx, params) + txf, err := clitx.NewFactory(ctx.Keyring, ctx.Cdc, accRetriever, txConfig, ctx.AddressCodec, conn, params) if err != nil { return "", err } - pubKey, err := keybase.GetPubKey(fromName) + pubKey, err := ctx.Keyring.GetPubKey(fromName) if err != nil { return "", err } diff --git a/client/v2/offchain/sign_test.go b/client/v2/offchain/sign_test.go index 839872866629..cb95b0485c39 100644 --- a/client/v2/offchain/sign_test.go +++ b/client/v2/offchain/sign_test.go @@ -5,23 +5,28 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/cosmos-sdk/client" + clientcontext "cosmossdk.io/client/v2/context" + "github.com/cosmos/cosmos-sdk/codec/address" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" ) func TestSign(t *testing.T) { + ac := address.NewBech32Codec("cosmos") + vc := address.NewBech32Codec("cosmosvaloper") k := keyring.NewInMemory(getCodec()) _, err := k.NewAccount("signVerify", mnemonic, "", "m/44'/118'/0'/0/0", hd.Secp256k1) require.NoError(t, err) - ctx := client.Context{ - TxConfig: newTestConfig(t), - Codec: getCodec(), - AddressCodec: address.NewBech32Codec("cosmos"), - ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"), - Keyring: k, + autoKeyring, err := keyring.NewAutoCLIKeyring(k, ac) + require.NoError(t, err) + + ctx := clientcontext.Context{ + AddressCodec: ac, + ValidatorAddressCodec: vc, + Cdc: getCodec(), + Keyring: autoKeyring, } tests := []struct { name string @@ -52,7 +57,7 @@ func TestSign(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Sign(ctx, tt.rawBytes, "signVerify", tt.encoding, tt.signMode, "json") + got, err := Sign(ctx, tt.rawBytes, mockClientConn{}, "signVerify", tt.encoding, tt.signMode, "json") if tt.wantErr { require.Error(t, err) } else { diff --git a/client/v2/offchain/verify.go b/client/v2/offchain/verify.go index 2c064faccc71..5e87cb90129b 100644 --- a/client/v2/offchain/verify.go +++ b/client/v2/offchain/verify.go @@ -8,19 +8,20 @@ import ( "google.golang.org/protobuf/types/known/anypb" + clientcontext "cosmossdk.io/client/v2/context" clitx "cosmossdk.io/client/v2/tx" + "cosmossdk.io/core/address" txsigning "cosmossdk.io/x/tx/signing" - "github.com/cosmos/cosmos-sdk/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" ) // Verify verifies a digest after unmarshalling it. -func Verify(ctx client.Context, digest []byte, fileFormat string) error { +func Verify(ctx clientcontext.Context, digest []byte, fileFormat string) error { txConfig, err := clitx.NewTxConfig(clitx.ConfigOptions{ AddressCodec: ctx.AddressCodec, - Cdc: ctx.Codec, + Cdc: ctx.Cdc, ValidatorAddressCodec: ctx.ValidatorAddressCodec, EnabledSignModes: enabledSignModes, }) @@ -33,12 +34,12 @@ func Verify(ctx client.Context, digest []byte, fileFormat string) error { return err } - return verify(ctx, dTx) + return verify(ctx.AddressCodec, txConfig, dTx) } // verify verifies given Tx. -func verify(ctx client.Context, dTx clitx.Tx) error { - signModeHandler := ctx.TxConfig.SignModeHandler() +func verify(addressCodec address.Codec, txConfig clitx.TxConfig, dTx clitx.Tx) error { + signModeHandler := txConfig.SignModeHandler() signers, err := dTx.GetSigners() if err != nil { @@ -60,7 +61,7 @@ func verify(ctx client.Context, dTx clitx.Tx) error { return errors.New("signature does not match its respective signer") } - addr, err := ctx.AddressCodec.BytesToString(pubKey.Address()) + addr, err := addressCodec.BytesToString(pubKey.Address()) if err != nil { return err } diff --git a/client/v2/offchain/verify_test.go b/client/v2/offchain/verify_test.go index 56345504d80e..d45648bd07c6 100644 --- a/client/v2/offchain/verify_test.go +++ b/client/v2/offchain/verify_test.go @@ -6,59 +6,53 @@ import ( "github.com/stretchr/testify/require" _ "cosmossdk.io/api/cosmos/crypto/secp256k1" + clientcontext "cosmossdk.io/client/v2/context" clitx "cosmossdk.io/client/v2/tx" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec/address" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" ) func Test_Verify(t *testing.T) { - ctx := client.Context{ - TxConfig: newTestConfig(t), - Codec: getCodec(), + ctx := clientcontext.Context{ AddressCodec: address.NewBech32Codec("cosmos"), ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"), + Cdc: getCodec(), } tests := []struct { name string digest []byte fileFormat string - ctx client.Context wantErr bool }{ { name: "verify json", digest: []byte("{\"body\":{\"messages\":[{\"@type\":\"/offchain.MsgSignArbitraryData\", \"app_domain\":\"\", \"signer\":\"cosmos16877zjk85kwlap3wclpmx34e0xllg2erc7u7m4\", \"data\":\"{\\n\\t\\\"name\\\": \\\"Sarah\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 29\\n}\\n\"}], \"timeout_timestamp\":\"0001-01-01T00:00:00Z\"}, \"auth_info\":{\"signer_infos\":[{\"public_key\":{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\", \"key\":\"Ahhu3idSSUAQXtDBvBjUlCPWH3od4rXyWgb7L4scSj4m\"}, \"mode_info\":{\"single\":{\"mode\":\"SIGN_MODE_DIRECT\"}}}], \"fee\":{}}, \"signatures\":[\"tdXsO5uNqIBFSBKEA1e3Wrcb6ejriP9HwlcBTkU7EUJzuezjg6Rvr1a+Kp6umCAN7MWoBHRT2cmqzDfg6RjaYA==\"]}"), fileFormat: "json", - ctx: ctx, }, { name: "wrong signer json", digest: []byte("{\"body\":{\"messages\":[{\"@type\":\"/offchain.MsgSignArbitraryData\", \"app_domain\":\"\", \"signer\":\"cosmos1xv9e39mkhhyg5aneu2myj82t7029sv48qu3pgj\", \"data\":\"{\\n\\t\\\"name\\\": \\\"Sarah\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 29\\n}\\n\"}], \"timeout_timestamp\":\"0001-01-01T00:00:00Z\"}, \"auth_info\":{\"signer_infos\":[{\"public_key\":{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\", \"key\":\"Ahhu3idSSUAQXtDBvBjUlCPWH3od4rXyWgb7L4scSj4m\"}, \"mode_info\":{\"single\":{\"mode\":\"SIGN_MODE_DIRECT\"}}}], \"fee\":{}}, \"signatures\":[\"tdXsO5uNqIBFSBKEA1e3Wrcb6ejriP9HwlcBTkU7EUJzuezjg6Rvr1a+Kp6umCAN7MWoBHRT2cmqzDfg6RjaYA==\"]}"), fileFormat: "json", - ctx: ctx, wantErr: true, }, { name: "verify text", digest: []byte("body:{messages:{[/offchain.MsgSignArbitraryData]:{app_domain:\"\" signer:\"cosmos16877zjk85kwlap3wclpmx34e0xllg2erc7u7m4\" data:\"{\\n\\t\\\"name\\\": \\\"Sarah\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 29\\n}\\n\"}} timeout_timestamp:{seconds:-62135596800}} auth_info:{signer_infos:{public_key:{[/cosmos.crypto.secp256k1.PubKey]:{key:\"\\x02\\x18n\\xde'RI@\\x10^\\xd0\\xc1\\xbc\\x18Ԕ#\\xd6\\x1fz\\x1d\\xe2\\xb5\\xf2Z\\x06\\xfb/\\x8b\\x1cJ>&\"}} mode_info:{single:{mode:SIGN_MODE_DIRECT}}} fee:{}} signatures:\"\\xb5\\xd5\\xec;\\x9b\\x8d\\xa8\\x80EH\\x12\\x84\\x03W\\xb7Z\\xb7\\x1b\\xe9\\xe8\\xeb\\x88\\xffG\\xc2W\\x01NE;\\x11Bs\\xb9\\xecヤo\\xafV\\xbe*\\x9e\\xae\\x98 \\r\\xecŨ\\x04tS\\xd9ɪ\\xcc7\\xe0\\xe9\\x18\\xda`\"\n"), fileFormat: "text", - ctx: ctx, }, { name: "wrong signer text", digest: []byte("body:{messages:{[/offchain.MsgSignArbitraryData]:{app_domain:\"\" signer:\"cosmos1xv9e39mkhhyg5aneu2myj82t7029sv48qu3pgj\" data:\"{\\n\\t\\\"name\\\": \\\"Sarah\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 29\\n}\\n\"}} timeout_timestamp:{seconds:-62135596800}} auth_info:{signer_infos:{public_key:{[/cosmos.crypto.secp256k1.PubKey]:{key:\"\\x02\\x18n\\xde'RI@\\x10^\\xd0\\xc1\\xbc\\x18Ԕ#\\xd6\\x1fz\\x1d\\xe2\\xb5\\xf2Z\\x06\\xfb/\\x8b\\x1cJ>&\"}} mode_info:{single:{mode:SIGN_MODE_DIRECT}}} fee:{}} signatures:\"\\xb5\\xd5\\xec;\\x9b\\x8d\\xa8\\x80EH\\x12\\x84\\x03W\\xb7Z\\xb7\\x1b\\xe9\\xe8\\xeb\\x88\\xffG\\xc2W\\x01NE;\\x11Bs\\xb9\\xecヤo\\xafV\\xbe*\\x9e\\xae\\x98 \\r\\xecŨ\\x04tS\\xd9ɪ\\xcc7\\xe0\\xe9\\x18\\xda`\"\n"), fileFormat: "text", - ctx: ctx, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := Verify(tt.ctx, tt.digest, tt.fileFormat) + err := Verify(ctx, tt.digest, tt.fileFormat) if tt.wantErr { require.Error(t, err) } else { @@ -69,19 +63,23 @@ func Test_Verify(t *testing.T) { } func Test_SignVerify(t *testing.T) { + ac := address.NewBech32Codec("cosmos") + k := keyring.NewInMemory(getCodec()) _, err := k.NewAccount("signVerify", mnemonic, "", "m/44'/118'/0'/0/0", hd.Secp256k1) require.NoError(t, err) - ctx := client.Context{ - TxConfig: newTestConfig(t), - Codec: getCodec(), + autoKeyring, err := keyring.NewAutoCLIKeyring(k, ac) + require.NoError(t, err) + + ctx := clientcontext.Context{ AddressCodec: address.NewBech32Codec("cosmos"), ValidatorAddressCodec: address.NewBech32Codec("cosmosvaloper"), - Keyring: k, + Cdc: getCodec(), + Keyring: autoKeyring, } - tx, err := Sign(ctx, []byte("Hello World!"), "signVerify", "no-encoding", "direct", "json") + tx, err := Sign(ctx, []byte("Hello World!"), mockClientConn{}, "signVerify", "no-encoding", "direct", "json") require.NoError(t, err) err = Verify(ctx, []byte(tx), "json") diff --git a/client/v2/tx/encoder.go b/client/v2/tx/encoder.go index 3e917b34b4c3..09011c8315e4 100644 --- a/client/v2/tx/encoder.go +++ b/client/v2/tx/encoder.go @@ -19,9 +19,10 @@ var ( // jsonMarshalOptions configures JSON marshaling for protobuf messages. jsonMarshalOptions = protojson.MarshalOptions{ - Indent: "", - UseProtoNames: true, - UseEnumNumbers: false, + Indent: "", + UseProtoNames: true, + UseEnumNumbers: false, + EmitUnpopulated: true, } // textMarshalOptions diff --git a/client/v2/tx/factory.go b/client/v2/tx/factory.go index 8007caaee4f8..6b18f492426c 100644 --- a/client/v2/tx/factory.go +++ b/client/v2/tx/factory.go @@ -44,7 +44,7 @@ type Factory struct { txConfig TxConfig txParams TxParameters - tx txState + tx *txState } func NewFactoryFromFlagSet(flags *pflag.FlagSet, keybase keyring.Keyring, cdc codec.BinaryCodec, accRetriever account.AccountRetriever, @@ -81,38 +81,37 @@ func NewFactory(keybase keyring.Keyring, cdc codec.BinaryCodec, accRetriever acc txConfig: txConfig, txParams: parameters, - tx: txState{}, + 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 { + dryRun, _ := flags.GetBool(flags2.FlagDryRun) + if offline && dryRun { + return errors.New("dry-run: cannot use offline mode") + } + + generateOnly, _ := flags.GetBool(flags2.FlagGenerateOnly) + chainID, _ := flags.GetString(flags2.FlagChainID) if offline { - if !flags.Changed(flags2.FlagAccountNumber) || !flags.Changed(flags2.FlagSequence) { + if !generateOnly && (!flags.Changed(flags2.FlagAccountNumber) || !flags.Changed(flags2.FlagSequence)) { return errors.New("account-number and sequence must be set in offline mode") } + if generateOnly && chainID != "" { + return errors.New("chain ID cannot be used when offline and generate-only flags are set") + } + 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 == "" { + } else 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 } diff --git a/client/v2/tx/flags.go b/client/v2/tx/flags.go index 6ef8584042f7..9d0a7c4a74d9 100644 --- a/client/v2/tx/flags.go +++ b/client/v2/tx/flags.go @@ -10,23 +10,23 @@ 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" + 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', diff --git a/client/v2/tx/tx.go b/client/v2/tx/tx.go index c6bb5a548f92..34278e48a024 100644 --- a/client/v2/tx/tx.go +++ b/client/v2/tx/tx.go @@ -1,91 +1,165 @@ package tx import ( - "bufio" "context" "errors" "fmt" - "os" + "github.com/cosmos/gogoproto/grpc" "github.com/cosmos/gogoproto/proto" "github.com/spf13/pflag" apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" "cosmossdk.io/client/v2/broadcast" "cosmossdk.io/client/v2/broadcast/comet" + clientcontext "cosmossdk.io/client/v2/context" "cosmossdk.io/client/v2/internal/account" + "cosmossdk.io/client/v2/internal/flags" "cosmossdk.io/core/transaction" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/input" - "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/codec" ) -// GenerateOrBroadcastTxCLIWithBroadcaster will either generate and print an unsigned transaction +// GenerateAndBroadcastTxCLIWithBroadcaster will either generate and print an unsigned transaction // or sign it and broadcast it with the specified broadcaster returning an error upon failure. -func GenerateOrBroadcastTxCLIWithBroadcaster(ctx client.Context, flagSet *pflag.FlagSet, broadcaster broadcast.Broadcaster, msgs ...transaction.Msg) error { - if err := validateMessages(msgs...); err != nil { - return err +func GenerateAndBroadcastTxCLIWithBroadcaster( + ctx context.Context, + conn grpc.ClientConn, + broadcaster broadcast.Broadcaster, + msgs ...transaction.Msg, +) ([]byte, error) { + txf, err := initFactory(ctx, conn, msgs...) + if err != nil { + return nil, err + } + + err = generateTx(txf, msgs...) + if err != nil { + return nil, err + } + + return BroadcastTx(ctx, txf, broadcaster) +} + +// GenerateAndBroadcastTxCLI will either generate and print an unsigned transaction +// or sign it and broadcast it using default CometBFT broadcaster, returning an error upon failure. +func GenerateAndBroadcastTxCLI(ctx context.Context, conn grpc.ClientConn, msgs ...transaction.Msg) ([]byte, error) { + cBroadcaster, err := cometBroadcaster(ctx) + if err != nil { + return nil, err } - txf, err := newFactory(ctx, flagSet) + return GenerateAndBroadcastTxCLIWithBroadcaster(ctx, conn, cBroadcaster, msgs...) +} + +// GenerateAndBroadcastTxCLIWithPrompt generates, signs and broadcasts a transaction after prompting the user for confirmation. +// It takes a context, gRPC client connection, prompt function for user confirmation, and transaction messages. +// The prompt function receives the unsigned transaction bytes and returns a boolean indicating user confirmation and any error. +// Returns the broadcast response bytes and any error encountered. +func GenerateAndBroadcastTxCLIWithPrompt( + ctx context.Context, + conn grpc.ClientConn, + prompt func([]byte) (bool, error), + msgs ...transaction.Msg, +) ([]byte, error) { + txf, err := initFactory(ctx, conn, msgs...) if err != nil { - return err + return nil, err } - genOnly, _ := flagSet.GetBool(flagGenerateOnly) - if genOnly { - return generateOnly(ctx, txf, msgs...) + err = generateTx(txf, msgs...) + if err != nil { + return nil, err } - isDryRun, _ := flagSet.GetBool(flagDryRun) - if isDryRun { - return dryRun(txf, msgs...) + confirmed, err := askConfirmation(txf, prompt) + if err != nil { + return nil, err + } + if !confirmed { + return nil, nil } - return BroadcastTx(ctx, txf, broadcaster, msgs...) + cBroadcaster, err := cometBroadcaster(ctx) + if err != nil { + return nil, err + } + + return BroadcastTx(ctx, txf, cBroadcaster) } -// GenerateOrBroadcastTxCLI will either generate and print an unsigned transaction -// or sign it and broadcast it using default CometBFT broadcaster, returning an error upon failure. -func GenerateOrBroadcastTxCLI(ctx client.Context, flagSet *pflag.FlagSet, msgs ...transaction.Msg) error { - cometBroadcaster, err := getCometBroadcaster(ctx, flagSet) +// GenerateOnly generates an unsigned transaction without broadcasting it. +// It initializes a transaction factory using the provided context, connection and messages, +// then generates an unsigned transaction. +// Returns the unsigned transaction bytes and any error encountered. +func GenerateOnly(ctx context.Context, conn grpc.ClientConn, msgs ...transaction.Msg) ([]byte, error) { + txf, err := initFactory(ctx, conn) if err != nil { - return err + return nil, err } - return GenerateOrBroadcastTxCLIWithBroadcaster(ctx, flagSet, cometBroadcaster, msgs...) + return generateOnly(txf, msgs...) } -// getCometBroadcaster returns a new CometBFT broadcaster based on the provided context and flag set. -func getCometBroadcaster(ctx client.Context, flagSet *pflag.FlagSet) (broadcast.Broadcaster, error) { - url, _ := flagSet.GetString("node") - mode, _ := flagSet.GetString("broadcast-mode") - return comet.NewCometBFTBroadcaster(url, mode, ctx.Codec) +// DryRun simulates a transaction without broadcasting it to the network. +// It initializes a transaction factory using the provided context, connection and messages, +// then performs a dry run simulation of the transaction. +// Returns the simulation response bytes and any error encountered. +func DryRun(ctx context.Context, conn grpc.ClientConn, msgs ...transaction.Msg) ([]byte, error) { + txf, err := initFactory(ctx, conn, msgs...) + if err != nil { + return nil, err + } + + return dryRun(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) +// initFactory initializes a new transaction Factory and validates the provided messages. +// It retrieves the client v2 context from the provided context, validates all messages, +// and creates a new transaction Factory using the client context and connection. +// Returns the initialized Factory and any error encountered. +func initFactory(ctx context.Context, conn grpc.ClientConn, msgs ...transaction.Msg) (Factory, error) { + clientCtx, err := clientcontext.ClientContextFromGoContext(ctx) if err != nil { return Factory{}, err } + if err := validateMessages(msgs...); err != nil { + return Factory{}, err + } + + txf, err := newFactory(*clientCtx, conn) + if err != nil { + return Factory{}, err + } + + return txf, nil +} + +// getCometBroadcaster returns a new CometBFT broadcaster based on the provided context and flag set. +func getCometBroadcaster(cdc codec.Codec, flagSet *pflag.FlagSet) (broadcast.Broadcaster, error) { + url, _ := flagSet.GetString(flags.FlagNode) + mode, _ := flagSet.GetString(flags.FlagBroadcastMode) + return comet.NewCometBFTBroadcaster(url, mode, cdc) +} + +// 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 clientcontext.Context, conn grpc.ClientConn) (Factory, error) { txConfig, err := NewTxConfig(ConfigOptions{ AddressCodec: ctx.AddressCodec, - Cdc: ctx.Codec, + Cdc: ctx.Cdc, ValidatorAddressCodec: ctx.ValidatorAddressCodec, - EnabledSignModes: ctx.TxConfig.SignModeHandler().SupportedModes(), + EnabledSignModes: ctx.EnabledSignModes, }) if err != nil { return Factory{}, err } - accRetriever := account.NewAccountRetriever(ctx.AddressCodec, ctx, ctx.InterfaceRegistry) + accRetriever := account.NewAccountRetriever(ctx.AddressCodec, conn, ctx.Cdc.InterfaceRegistry()) - txf, err := NewFactoryFromFlagSet(flagSet, k, ctx.Codec, accRetriever, txConfig, ctx.AddressCodec, ctx) + txf, err := NewFactoryFromFlagSet(ctx.Flags, ctx.Keyring, ctx.Cdc, accRetriever, txConfig, ctx.AddressCodec, conn) if err != nil { return Factory{}, err } @@ -115,30 +189,29 @@ func validateMessages(msgs ...transaction.Msg) error { // 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 { +func generateOnly(txf Factory, msgs ...transaction.Msg) ([]byte, error) { uTx, err := txf.UnsignedTxString(msgs...) if err != nil { - return err + return nil, err } - return ctx.PrintString(uTx) + return []byte(uTx), nil } // 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 { +func dryRun(txf Factory, msgs ...transaction.Msg) ([]byte, error) { _, gas, err := txf.Simulate(msgs...) if err != nil { - return err + return nil, err } - _, err = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: gas}) - return err + return []byte(fmt.Sprintf(`{"gas_estimate": %d}`, gas)), nil } // 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) +func SimulateTx(ctx clientcontext.Context, conn grpc.ClientConn, msgs ...transaction.Msg) (proto.Message, error) { + txf, err := newFactory(ctx, conn) if err != nil { return nil, err } @@ -147,10 +220,10 @@ func SimulateTx(ctx client.Context, flagSet *pflag.FlagSet, msgs ...transaction. 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, broadcaster broadcast.Broadcaster, msgs ...transaction.Msg) error { +// generateTx generates an unsigned transaction using the provided transaction factory and messages. +// If simulation and execution are enabled, it first calculates the gas requirements. +// It then builds the unsigned transaction with the provided messages. +func generateTx(txf Factory, msgs ...transaction.Msg) error { if txf.simulateAndExecute() { err := txf.calculateGas(msgs...) if err != nil { @@ -158,58 +231,29 @@ func BroadcastTx(clientCtx client.Context, txf Factory, broadcaster broadcast.Br } } - 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) - } + return txf.BuildUnsignedTx(msgs...) +} - 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 - } +// BroadcastTx attempts to sign and broadcast a transaction using the provided factory and broadcaster. +// GenerateTx must be called first to prepare the transaction for signing. +// This function then signs the transaction using the factory's signing capabilities, encodes it, +// and finally broadcasts it using the provided broadcaster. +func BroadcastTx(ctx context.Context, txf Factory, broadcaster broadcast.Broadcaster) ([]byte, error) { + if len(txf.tx.msgs) == 0 { + return nil, errors.New("no messages to broadcast") } - signedTx, err := txf.sign(clientCtx.CmdContext, true) + signedTx, err := txf.sign(ctx, true) if err != nil { - return err + return nil, err } txBytes, err := txf.txConfig.TxEncoder()(signedTx) if err != nil { - return err - } - - res, err := broadcaster.Broadcast(context.Background(), txBytes) - if err != nil { - return err + return nil, err } - return clientCtx.PrintString(string(res)) + return broadcaster.Broadcast(ctx, txBytes) } // countDirectSigners counts the number of DIRECT signers in a signature data. @@ -233,6 +277,38 @@ func countDirectSigners(sigData SignatureData) int { } } +// cometBroadcaster returns a broadcast.Broadcaster implementation that uses the CometBFT RPC client. +// It extracts the client context from the provided context and uses it to create a CometBFT broadcaster. +func cometBroadcaster(ctx context.Context) (broadcast.Broadcaster, error) { + c, err := clientcontext.ClientContextFromGoContext(ctx) + if err != nil { + return nil, err + } + + return getCometBroadcaster(c.Cdc, c.Flags) +} + +// askConfirmation encodes the transaction as JSON and prompts the user for confirmation using the provided prompter function. +// It returns the user's confirmation response and any error that occurred during the process. +func askConfirmation(txf Factory, prompter func([]byte) (bool, error)) (bool, error) { + encoder := txf.txConfig.TxJSONEncoder() + if encoder == nil { + return false, errors.New("failed to encode transaction: tx json encoder is nil") + } + + tx, err := txf.getTx() + if err != nil { + return false, err + } + + txBytes, err := encoder(tx) + if err != nil { + return false, fmt.Errorf("failed to encode transaction: %w", err) + } + + return prompter(txBytes) +} + // getSignMode returns the corresponding apitxsigning.SignMode based on the provided mode string. func getSignMode(mode string) apitxsigning.SignMode { switch mode { diff --git a/client/v2/tx/types.go b/client/v2/tx/types.go index a50b0b996b1d..801e246acae5 100644 --- a/client/v2/tx/types.go +++ b/client/v2/tx/types.go @@ -148,20 +148,21 @@ type Tx interface { // 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) + timestampUnix, _ := flags.GetInt64(FlagTimeoutTimestamp) timeoutTimestamp := time.Unix(timestampUnix, 0) - chainID, _ := flags.GetString(flagChainID) - memo, _ := flags.GetString(flagNote) - signMode, _ := flags.GetString(flagSignMode) + chainID, _ := flags.GetString(FlagChainID) + memo, _ := flags.GetString(FlagNote) + signMode, _ := flags.GetString(FlagSignMode) - accNumber, _ := flags.GetUint64(flagAccountNumber) - sequence, _ := flags.GetUint64(flagSequence) - from, _ := flags.GetString(flagFrom) + 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 { + isDryRun, _ := flags.GetBool(FlagDryRun) + generateOnly, _ := flags.GetBool(FlagGenerateOnly) + if isDryRun || generateOnly { addr, err = ac.StringToBytes(from) } else { fromName, fromAddress, _, err = keybase.KeyInfo(from) @@ -173,16 +174,16 @@ func txParamsFromFlagSet(flags *pflag.FlagSet, keybase keyring2.Keyring, ac addr return params, err } - gas, _ := flags.GetString(flagGas) + gas, _ := flags.GetString(FlagGas) simulate, gasValue, _ := parseGasSetting(gas) - gasAdjustment, _ := flags.GetFloat64(flagGasAdjustment) - gasPrices, _ := flags.GetString(flagGasPrices) + gasAdjustment, _ := flags.GetFloat64(FlagGasAdjustment) + gasPrices, _ := flags.GetString(FlagGasPrices) - fees, _ := flags.GetString(flagFees) - feePayer, _ := flags.GetString(flagFeePayer) - feeGrater, _ := flags.GetString(flagFeeGranter) + fees, _ := flags.GetString(FlagFees) + feePayer, _ := flags.GetString(FlagFeePayer) + feeGrater, _ := flags.GetString(FlagFeeGranter) - unordered, _ := flags.GetBool(flagUnordered) + unordered, _ := flags.GetBool(FlagUnordered) gasConfig, err := NewGasConfig(gasValue, gasAdjustment, gasPrices) if err != nil { diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index cc7cf47742c1..7851629e7716 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -121,7 +121,10 @@ func NewRootCmd() *cobra.Command { } autoCliOpts := tempApp.AutoCliOpts() - autoCliOpts.ClientCtx = initClientCtx + autoCliOpts.AddressCodec = initClientCtx.AddressCodec + autoCliOpts.ValidatorAddressCodec = initClientCtx.ValidatorAddressCodec + autoCliOpts.ConsensusAddressCodec = initClientCtx.ConsensusAddressCodec + autoCliOpts.Cdc = initClientCtx.Codec nodeCmds := nodeservice.NewNodeCommands() autoCliOpts.ModuleOptions[nodeCmds.Name()] = nodeCmds.AutoCLIOptions() diff --git a/tests/systemtests/mint_test.go b/tests/systemtests/mint_test.go index 1be1a713743b..d1c43bd9d863 100644 --- a/tests/systemtests/mint_test.go +++ b/tests/systemtests/mint_test.go @@ -1,3 +1,5 @@ +//go:build system_test + package systemtests import (