package client import ( "context" "crypto/tls" "errors" "fmt" "slices" "strings" cmtcfg "github.com/cometbft/cometbft/config" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" corectx "cosmossdk.io/core/context" "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" ) // ClientContextKey defines the context key used to retrieve a client.Context from // a command's Context. const ClientContextKey = sdk.ContextKey("client.context") // SetCmdClientContextHandler is to be used in a command pre-hook execution to // read flags that populate a Context and sets that to the command's Context. func SetCmdClientContextHandler(clientCtx Context, cmd *cobra.Command) (err error) { clientCtx, err = ReadPersistentCommandFlags(clientCtx, cmd.Flags()) if err != nil { return err } return SetCmdClientContext(cmd, clientCtx) } // ValidateCmd returns unknown command error or Help display if help flag set func ValidateCmd(cmd *cobra.Command, args []string) error { var unknownCmd string var skipNext bool for _, arg := range args { // search for help flag if arg == "--help" || arg == "-h" { return cmd.Help() } // check if the current arg is a flag switch { case len(arg) > 0 && (arg[0] == '-'): // the next arg should be skipped if the current arg is a // flag and does not use "=" to assign the flag's value if !strings.Contains(arg, "=") { skipNext = true } else { skipNext = false } case skipNext: // skip current arg skipNext = false case unknownCmd == "": // unknown command found // continue searching for help flag unknownCmd = arg } } // return the help screen if no unknown command is found if unknownCmd != "" { err := fmt.Sprintf("unknown command \"%s\" for \"%s\"", unknownCmd, cmd.CalledAs()) // build suggestions for unknown argument if suggestions := cmd.SuggestionsFor(unknownCmd); len(suggestions) > 0 { err += "\n\nDid you mean this?\n" for _, s := range suggestions { err += fmt.Sprintf("\t%v\n", s) } } return errors.New(err) } return cmd.Help() } // ReadPersistentCommandFlags returns a Context with fields set for "persistent" // or common flags that do not necessarily change with context. // // Note, the provided clientCtx may have field pre-populated. The following order // of precedence occurs: // // - client.Context field not pre-populated & flag not set: uses default flag value // - client.Context field not pre-populated & flag set: uses set flag value // - client.Context field pre-populated & flag not set: uses pre-populated value // - client.Context field pre-populated & flag set: uses set flag value func ReadPersistentCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, error) { if clientCtx.OutputFormat == "" || flagSet.Changed(flags.FlagOutput) { output, _ := flagSet.GetString(flags.FlagOutput) clientCtx = clientCtx.WithOutputFormat(output) } if clientCtx.HomeDir == "" || flagSet.Changed(flags.FlagHome) { homeDir, _ := flagSet.GetString(flags.FlagHome) clientCtx = clientCtx.WithHomeDir(homeDir) } if !clientCtx.Simulate || flagSet.Changed(flags.FlagDryRun) { dryRun, _ := flagSet.GetBool(flags.FlagDryRun) clientCtx = clientCtx.WithSimulation(dryRun) } if clientCtx.KeyringDir == "" || flagSet.Changed(flags.FlagKeyringDir) { keyringDir, _ := flagSet.GetString(flags.FlagKeyringDir) // The keyring directory is optional and falls back to the home directory // if omitted. if keyringDir == "" { keyringDir = clientCtx.HomeDir } clientCtx = clientCtx.WithKeyringDir(keyringDir) } if clientCtx.ChainID == "" || flagSet.Changed(flags.FlagChainID) { chainID, _ := flagSet.GetString(flags.FlagChainID) clientCtx = clientCtx.WithChainID(chainID) } if clientCtx.Keyring == nil || flagSet.Changed(flags.FlagKeyringBackend) { keyringBackend, _ := flagSet.GetString(flags.FlagKeyringBackend) if keyringBackend != "" { kr, err := NewKeyringFromBackend(clientCtx, keyringBackend) if err != nil { return clientCtx, err } clientCtx = clientCtx.WithKeyring(kr) } } if clientCtx.Client == nil || flagSet.Changed(flags.FlagNode) { rpcURI, _ := flagSet.GetString(flags.FlagNode) if rpcURI != "" { clientCtx = clientCtx.WithNodeURI(rpcURI) client, err := NewClientFromNode(rpcURI) if err != nil { return clientCtx, err } clientCtx = clientCtx.WithClient(client) } } if clientCtx.GRPCClient == nil || flagSet.Changed(flags.FlagGRPC) { grpcURI, _ := flagSet.GetString(flags.FlagGRPC) if grpcURI != "" { var dialOpts []grpc.DialOption useInsecure, _ := flagSet.GetBool(flags.FlagGRPCInsecure) if useInsecure { dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } else { dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ MinVersion: tls.VersionTLS12, }))) } grpcClient, err := grpc.NewClient(grpcURI, dialOpts...) if err != nil { return Context{}, err } clientCtx = clientCtx.WithGRPCClient(grpcClient) } } return clientCtx, nil } // readQueryCommandFlags returns an updated Context with fields set based on flags // defined in AddQueryFlagsToCmd. An error is returned if any flag query fails. // // Note, the provided clientCtx may have field pre-populated. The following order // of precedence occurs: // // - client.Context field not pre-populated & flag not set: uses default flag value // - client.Context field not pre-populated & flag set: uses set flag value // - client.Context field pre-populated & flag not set: uses pre-populated value // - client.Context field pre-populated & flag set: uses set flag value func readQueryCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, error) { if clientCtx.Height == 0 || flagSet.Changed(flags.FlagHeight) { height, _ := flagSet.GetInt64(flags.FlagHeight) clientCtx = clientCtx.WithHeight(height) } if !clientCtx.UseLedger || flagSet.Changed(flags.FlagUseLedger) { useLedger, _ := flagSet.GetBool(flags.FlagUseLedger) clientCtx = clientCtx.WithUseLedger(useLedger) } return ReadPersistentCommandFlags(clientCtx, flagSet) } // readTxCommandFlags returns an updated Context with fields set based on flags // defined in AddTxFlagsToCmd. An error is returned if any flag query fails. // // Note, the provided clientCtx may have field pre-populated. The following order // of precedence occurs: // // - client.Context field not pre-populated & flag not set: uses default flag value // - client.Context field not pre-populated & flag set: uses set flag value // - client.Context field pre-populated & flag not set: uses pre-populated value // - client.Context field pre-populated & flag set: uses set flag value func readTxCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, error) { clientCtx, err := ReadPersistentCommandFlags(clientCtx, flagSet) if err != nil { return clientCtx, err } if !clientCtx.GenerateOnly || flagSet.Changed(flags.FlagGenerateOnly) { genOnly, _ := flagSet.GetBool(flags.FlagGenerateOnly) clientCtx = clientCtx.WithGenerateOnly(genOnly) } if !clientCtx.Offline || flagSet.Changed(flags.FlagOffline) { offline, _ := flagSet.GetBool(flags.FlagOffline) clientCtx = clientCtx.WithOffline(offline) } if !clientCtx.UseLedger || flagSet.Changed(flags.FlagUseLedger) { useLedger, _ := flagSet.GetBool(flags.FlagUseLedger) clientCtx = clientCtx.WithUseLedger(useLedger) } if clientCtx.BroadcastMode == "" || flagSet.Changed(flags.FlagBroadcastMode) { bMode, _ := flagSet.GetString(flags.FlagBroadcastMode) clientCtx = clientCtx.WithBroadcastMode(bMode) } if !clientCtx.SkipConfirm || flagSet.Changed(flags.FlagSkipConfirmation) { skipConfirm, _ := flagSet.GetBool(flags.FlagSkipConfirmation) clientCtx = clientCtx.WithSkipConfirmation(skipConfirm) } if clientCtx.SignModeStr == "" || flagSet.Changed(flags.FlagSignMode) { signModeStr, _ := flagSet.GetString(flags.FlagSignMode) clientCtx = clientCtx.WithSignModeStr(signModeStr) } if clientCtx.FeePayer == nil || flagSet.Changed(flags.FlagFeePayer) { payer, _ := flagSet.GetString(flags.FlagFeePayer) if payer != "" { payerAcc, err := clientCtx.AddressCodec.StringToBytes(payer) if err != nil { return clientCtx, err } clientCtx = clientCtx.WithFeePayerAddress(payerAcc) } } if clientCtx.FeeGranter == nil || flagSet.Changed(flags.FlagFeeGranter) { granter, _ := flagSet.GetString(flags.FlagFeeGranter) if granter != "" { granterAcc, err := clientCtx.AddressCodec.StringToBytes(granter) if err != nil { return clientCtx, err } clientCtx = clientCtx.WithFeeGranterAddress(granterAcc) } } if clientCtx.From == "" || flagSet.Changed(flags.FlagFrom) { from, _ := flagSet.GetString(flags.FlagFrom) fromAddr, fromName, keyType, err := GetFromFields(clientCtx, clientCtx.Keyring, from) if err != nil { return clientCtx, fmt.Errorf("failed to convert address field to address: %w", err) } clientCtx = clientCtx.WithFrom(from).WithFromAddress(fromAddr).WithFromName(fromName) if keyType == keyring.TypeLedger && clientCtx.SignModeStr == flags.SignModeTextual { if !slices.Contains(clientCtx.TxConfig.SignModeHandler().SupportedModes(), signingv1beta1.SignMode_SIGN_MODE_TEXTUAL) { return clientCtx, errors.New("SIGN_MODE_TEXTUAL is not available") } } // If the `from` signer account is a ledger key, we need to use // SIGN_MODE_AMINO_JSON, because ledger doesn't support proto yet. // ref: https://github.com/cosmos/cosmos-sdk/issues/8109 if keyType == keyring.TypeLedger && clientCtx.SignModeStr != flags.SignModeLegacyAminoJSON && clientCtx.SignModeStr != flags.SignModeTextual && !clientCtx.LedgerHasProtobuf { fmt.Println("Default sign-mode 'direct' not supported by Ledger, using sign-mode 'amino-json'.") clientCtx = clientCtx.WithSignModeStr(flags.SignModeLegacyAminoJSON) } } if !clientCtx.IsAux || flagSet.Changed(flags.FlagAux) { isAux, _ := flagSet.GetBool(flags.FlagAux) clientCtx = clientCtx.WithAux(isAux) if isAux { // If the user didn't explicitly set an --output flag, use JSON by default. if clientCtx.OutputFormat == "" || !flagSet.Changed(flags.FlagOutput) { clientCtx = clientCtx.WithOutputFormat(flags.OutputFormatJSON) } // If the user didn't explicitly set a --sign-mode flag, use DIRECT_AUX by default. if clientCtx.SignModeStr == "" || !flagSet.Changed(flags.FlagSignMode) { clientCtx = clientCtx.WithSignModeStr(flags.SignModeDirectAux) } } } return clientCtx, nil } // GetClientQueryContext returns a Context from a command with fields set based on flags // defined in AddQueryFlagsToCmd. An error is returned if any flag query fails. // // - client.Context field not pre-populated & flag not set: uses default flag value // - client.Context field not pre-populated & flag set: uses set flag value // - client.Context field pre-populated & flag not set: uses pre-populated value // - client.Context field pre-populated & flag set: uses set flag value func GetClientQueryContext(cmd *cobra.Command) (Context, error) { ctx := GetClientContextFromCmd(cmd) return readQueryCommandFlags(ctx, cmd.Flags()) } // GetClientTxContext returns a Context from a command with fields set based on flags // defined in AddTxFlagsToCmd. An error is returned if any flag query fails. // // - client.Context field not pre-populated & flag not set: uses default flag value // - client.Context field not pre-populated & flag set: uses set flag value // - client.Context field pre-populated & flag not set: uses pre-populated value // - client.Context field pre-populated & flag set: uses set flag value func GetClientTxContext(cmd *cobra.Command) (Context, error) { ctx := GetClientContextFromCmd(cmd) return readTxCommandFlags(ctx, cmd.Flags()) } // GetClientContextFromCmd returns a Context from a command or an empty Context // if it has not been set. func GetClientContextFromCmd(cmd *cobra.Command) Context { if v := cmd.Context().Value(ClientContextKey); v != nil { clientCtxPtr := v.(*Context) return *clientCtxPtr } return Context{} } // SetCmdClientContext sets a command's Context value to the provided argument. // If the context has not been set, set the given context as the default. func SetCmdClientContext(cmd *cobra.Command, clientCtx Context) error { cmdCtx := cmd.Context() if cmdCtx == nil { cmdCtx = context.Background() } v := cmd.Context().Value(ClientContextKey) if clientCtxPtr, ok := v.(*Context); ok { *clientCtxPtr = clientCtx } else { cmd.SetContext(context.WithValue(cmdCtx, ClientContextKey, &clientCtx)) } return nil } func GetViperFromCmd(cmd *cobra.Command) *viper.Viper { value := cmd.Context().Value(corectx.ViperContextKey) v, ok := value.(*viper.Viper) if !ok { return viper.New() } return v } func GetConfigFromCmd(cmd *cobra.Command) *cmtcfg.Config { v := cmd.Context().Value(corectx.ViperContextKey) viper, ok := v.(*viper.Viper) if !ok { return cmtcfg.DefaultConfig() } return GetConfigFromViper(viper) } func GetLoggerFromCmd(cmd *cobra.Command) log.Logger { v := cmd.Context().Value(corectx.LoggerContextKey) logger, ok := v.(log.Logger) if !ok { return log.NewLogger(cmd.OutOrStdout()) } return logger } func GetConfigFromViper(v *viper.Viper) *cmtcfg.Config { conf := cmtcfg.DefaultConfig() err := v.Unmarshal(conf) rootDir := v.GetString(flags.FlagHome) if err != nil { return cmtcfg.DefaultConfig().SetRoot(rootDir) } return conf.SetRoot(rootDir) }