diff --git a/tools/hubl/README.md b/tools/hubl/README.md index 2905ea7223..97d02921dd 100644 --- a/tools/hubl/README.md +++ b/tools/hubl/README.md @@ -32,14 +32,13 @@ hubl --help ### Add chain -To configure a new chain just run this command using the --init flag and the name of the chain as it's listed in the chain registry (https://github.com/cosmos/chain-registry). +To configure a new chain just run this command using the --init flag and the name of the chain as it's listed in the chain registry (). If the chain is not listed in the chain registry, you can use any unique name. - ```shell -hubl --init [chain-name] -hubl --init regen +hubl init [chain-name] +hubl init regen ``` The chain configuration is stored in `~/.hubl/config.toml`. @@ -56,6 +55,12 @@ endpoint = 'localhost:9090' insecure = true ``` +Or use the `--insecure` flag: + +```shell +hubl init regen --insecure +``` + ::: ### Query diff --git a/tools/hubl/cmd/hubl/main.go b/tools/hubl/cmd/hubl/main.go index dbb6c04eb0..61aaaad6e6 100644 --- a/tools/hubl/cmd/hubl/main.go +++ b/tools/hubl/cmd/hubl/main.go @@ -10,8 +10,7 @@ func main() { panic(err) } - err = cmd.Execute() - if err != nil { + if err = cmd.Execute(); err != nil { panic(err) } } diff --git a/tools/hubl/internal/compat.go b/tools/hubl/internal/compat.go index 4f4bf10c29..d3df822dac 100644 --- a/tools/hubl/internal/compat.go +++ b/tools/hubl/internal/compat.go @@ -19,7 +19,7 @@ import ( // loadFileDescriptorsGRPCReflection attempts to load the file descriptor set using gRPC reflection when cosmos.reflection.v1 // is unavailable. func loadFileDescriptorsGRPCReflection(ctx context.Context, client *grpc.ClientConn) (*descriptorpb.FileDescriptorSet, error) { - fmt.Printf("This chain does not support cosmos.reflection.v1 yet, we will attempt to use a fallback. Some features may be unsupported and it may not be possible to read all data.\n") + fmt.Printf("This chain does not support cosmos.reflection.v1 yet... attempting to use a fallback. Some features may be unsupported and it may not be possible to read all data.\n") var interfaceImplNames []string cosmosReflectBetaClient := reflectionv1beta1.NewReflectionServiceClient(client) @@ -30,9 +30,7 @@ func loadFileDescriptorsGRPCReflection(ctx context.Context, client *grpc.ClientC InterfaceName: iface, }) if err == nil { - for _, name := range implRes.ImplementationMessageNames { - interfaceImplNames = append(interfaceImplNames, name) - } + interfaceImplNames = append(interfaceImplNames, implRes.ImplementationMessageNames...) } } } @@ -66,10 +64,9 @@ func loadFileDescriptorsGRPCReflection(ctx context.Context, client *grpc.ClientC } }() - err = reflectClient.Send(&grpc_reflection_v1alpha.ServerReflectionRequest{ + if err = reflectClient.Send(&grpc_reflection_v1alpha.ServerReflectionRequest{ MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{}, - }) - if err != nil { + }); err != nil { return nil, err } @@ -95,11 +92,9 @@ func loadFileDescriptorsGRPCReflection(ctx context.Context, client *grpc.ClientC if err != nil { return nil, err } - } - err = reflectClient.CloseSend() - if err != nil { + if err = reflectClient.CloseSend(); err != nil { return nil, err } @@ -209,7 +204,7 @@ func addMissingFileDescriptors(ctx context.Context, client *grpc.ClientConn, fdM } func guessAutocli(files *protoregistry.Files) *autocliv1.AppOptionsResponse { - fmt.Printf("This chain does not support autocli directly yet. We will use some default mappings in the meantime to support a subset of the available services.\n") + fmt.Printf("This chain does not support autocli directly yet. Using some default mappings in the meantime to support a subset of the available services.\n") res := map[string]*autocliv1.ModuleOptions{} files.RangeFiles(func(descriptor protoreflect.FileDescriptor) bool { services := descriptor.Services() diff --git a/tools/hubl/internal/config.go b/tools/hubl/internal/config.go index be13afdc3b..4a921381db 100644 --- a/tools/hubl/internal/config.go +++ b/tools/hubl/internal/config.go @@ -2,7 +2,6 @@ package internal import ( "bytes" - "fmt" "os" "path" @@ -25,8 +24,8 @@ type GRPCEndpoint struct { func LoadConfig(configDir string) (*Config, error) { configPath := configFilename(configDir) + if _, err := os.Stat(configPath); os.IsNotExist(err) { - // file doesn't exist return &Config{Chains: map[string]*ChainConfig{}}, nil } @@ -36,8 +35,7 @@ func LoadConfig(configDir string) (*Config, error) { } config := &Config{} - err = toml.Unmarshal(bz, config) - if err != nil { + if err = toml.Unmarshal(bz, config); err != nil { return nil, errors.Wrapf(err, "can't load config file: %s", configPath) } @@ -45,26 +43,21 @@ func LoadConfig(configDir string) (*Config, error) { } func SaveConfig(configDir string, config *Config) error { - configPath := configFilename(configDir) buf := &bytes.Buffer{} enc := toml.NewEncoder(buf) - err := enc.Encode(config) - if err != nil { + if err := enc.Encode(config); err != nil { return err } - err = os.MkdirAll(configDir, 0755) - if err != nil { + if err := os.MkdirAll(configDir, 0755); err != nil { return err } - err = os.WriteFile(configPath, buf.Bytes(), 0644) - if err != nil { + configPath := configFilename(configDir) + if err := os.WriteFile(configPath, buf.Bytes(), 0644); err != nil { return err } - fmt.Printf("Saved config in %s\n", configPath) - return nil } diff --git a/tools/hubl/internal/load.go b/tools/hubl/internal/load.go index a190fbd59e..14d4218c82 100644 --- a/tools/hubl/internal/load.go +++ b/tools/hubl/internal/load.go @@ -20,16 +20,17 @@ import ( "google.golang.org/protobuf/types/descriptorpb" ) -const DefaultDirName = ".hubl" +const DefaultConfigDirName = ".hubl" type ChainInfo struct { + client *grpc.ClientConn + ConfigDir string Chain string ModuleOptions map[string]*autocliv1.ModuleOptions ProtoFiles *protoregistry.Files Context context.Context Config *ChainConfig - client *grpc.ClientConn } func NewChainInfo(configDir string, chain string, config *ChainConfig) *ChainInfo { @@ -91,8 +92,7 @@ func (c *ChainInfo) Load(reload bool) error { return err } - err = os.WriteFile(fdsFilename, bz, 0644) - if err != nil { + if err = os.WriteFile(fdsFilename, bz, 0644); err != nil { return err } } else { @@ -101,15 +101,14 @@ func (c *ChainInfo) Load(reload bool) error { return err } - err = proto.Unmarshal(bz, fdSet) - if err != nil { + if err = proto.Unmarshal(bz, fdSet); err != nil { return err } } c.ProtoFiles, err = protodesc.FileOptions{AllowUnresolvable: true}.NewFiles(fdSet) if err != nil { - return errors.Wrapf(err, "error building protoregistry.Files") + return fmt.Errorf("error building protoregistry.Files: %w", err) } appOptsFilename, err := c.appOptsCacheFilename() @@ -165,7 +164,6 @@ func (c *ChainInfo) OpenClient() (*grpc.ClientConn, error) { var res error for _, endpoint := range c.Config.GRPCEndpoints { - var err error var creds credentials.TransportCredentials if endpoint.Insecure { creds = insecure.NewCredentials() @@ -174,6 +172,8 @@ func (c *ChainInfo) OpenClient() (*grpc.ClientConn, error) { MinVersion: tls.VersionTLS12, }) } + + var err error c.client, err = grpc.Dial(endpoint.Endpoint, grpc.WithTransportCredentials(creds)) if err != nil { res = multierror.Append(res, err) diff --git a/tools/hubl/internal/registry.go b/tools/hubl/internal/registry.go index 3930aa33b2..eb170d7319 100644 --- a/tools/hubl/internal/registry.go +++ b/tools/hubl/internal/registry.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/manifoldco/promptui" ) @@ -34,20 +35,40 @@ func GetChainRegistryEntry(chain string) (*ChainRegistryEntry, error) { } data := &ChainRegistryEntry{} - err = json.Unmarshal(bz, data) - if err != nil { + if err = json.Unmarshal(bz, data); err != nil { return nil, err } - fmt.Printf("Found data for %s in the chain registry\n", chain) + // clean-up the URL + cleanEntries := make([]*APIEntry, 0) + for i, apiEntry := range data.APIs.GRPC { + // clean-up the http(s):// prefix + if strings.Contains(apiEntry.Address, "https://") { + data.APIs.GRPC[i].Address = strings.Replace(apiEntry.Address, "https://", "", 1) + } else if strings.Contains(apiEntry.Address, "http://") { + data.APIs.GRPC[i].Address = strings.Replace(apiEntry.Address, "http://", "", 1) + } + // remove trailing slashes + data.APIs.GRPC[i].Address = strings.TrimSuffix(data.APIs.GRPC[i].Address, "/") + + // remove addresses without a port + if !strings.Contains(data.APIs.GRPC[i].Address, ":") { + continue + } + + cleanEntries = append(cleanEntries, data.APIs.GRPC[i]) + } + + data.APIs.GRPC = cleanEntries + fmt.Printf("Found data for %s in the chain registry\n", chain) return data, nil } func SelectGRPCEndpoints(chain string) (string, error) { entry, err := GetChainRegistryEntry(chain) if err != nil { - fmt.Printf("Unable to load data for %s in the chain registry. You'll have to specify a gRPC endpoint manually.\n", chain) + fmt.Printf("Unable to load data for %s in the chain registry. Specify a custom gRPC endpoint manually.\n", chain) prompt := &promptui.Prompt{ Label: "Enter a gRPC endpoint that you trust", } diff --git a/tools/hubl/internal/remote.go b/tools/hubl/internal/remote.go index 945f8e394e..1b89f233e8 100644 --- a/tools/hubl/internal/remote.go +++ b/tools/hubl/internal/remote.go @@ -13,52 +13,75 @@ import ( "google.golang.org/protobuf/types/dynamicpb" "cosmossdk.io/client/v2/autocli" - "cosmossdk.io/client/v2/autocli/flag" ) +var ( + flagInsecure string = "insecure" + flagUpdate string = "update" + flagConfig string = "config" +) + func RootCommand() (*cobra.Command, error) { - userCfgDir, err := os.UserHomeDir() + homeDir, err := os.UserHomeDir() if err != nil { return nil, err } - configDir := path.Join(userCfgDir, DefaultDirName) - + configDir := path.Join(homeDir, DefaultConfigDirName) config, err := LoadConfig(configDir) if err != nil { return nil, err } - var initChain string cmd := &cobra.Command{ + Use: "hubl", + Short: "Hubl is a CLI for interacting with Cosmos SDK chains", + Long: "Hubl is a CLI for interacting with Cosmos SDK chains", + } + + // add commands + commands, err := RemoteCommand(config, configDir) + if err != nil { + return nil, err + } + commands = append(commands, InitCommand(config, configDir)) + + cmd.AddCommand(commands...) + return cmd, nil +} + +func InitCommand(config *Config, configDir string) *cobra.Command { + var insecure bool + + cmd := &cobra.Command{ + Use: "init [foochain]", + Short: "Initialize a new chain", Long: `To configure a new chain just run this command using the --init flag and the name of the chain as it's listed in the chain registry (https://github.com/cosmos/chain-registry). If the chain is not listed in the chain registry, you can use any unique name.`, - Example: "hubl --init foochain", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if initChain != "" { - return reconfigure(configDir, initChain, config) - } + chainName := strings.ToLower(args[0]) - return cmd.Help() + return reconfigure(cmd, config, configDir, chainName) }, } - cmd.Flags().StringVar(&initChain, "init", "", "initialize a new chain with the specified name") + cmd.Flags().BoolVar(&insecure, flagInsecure, false, "allow setting up insecure gRPC connection") + + return cmd +} + +func RemoteCommand(config *Config, configDir string) ([]*cobra.Command, error) { + commands := []*cobra.Command{} for chain, chainConfig := range config.Chains { + chain, chainConfig := chain, chainConfig + + // load chain info chainInfo := NewChainInfo(configDir, chain, chainConfig) - err = chainInfo.Load(false) - if err != nil { - cmd.AddCommand(&cobra.Command{ - Use: chain, - Short: "Unable to load data", - Long: "Unable to load data, reconfiguration needed.", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Error loading chain data for %s: %+v\n", chain, err) - return reconfigure(configDir, chain, config) - }, - }) + if err := chainInfo.Load(false); err != nil { + commands = append(commands, RemoteErrorCommand(config, configDir, chain, chainConfig, err)) continue } @@ -77,62 +100,87 @@ If the chain is not listed in the chain registry, you can use any unique name.`, AddQueryConnFlags: func(command *cobra.Command) {}, } - var update bool - var reconfig bool + var ( + update bool + reconfig bool + insecure bool + ) chainCmd := &cobra.Command{ Use: chain, Short: fmt.Sprintf("Commands for the %s chain", chain), RunE: func(cmd *cobra.Command, args []string) error { if reconfig { - return reconfigure(configDir, chain, config) + return reconfigure(cmd, config, configDir, chain) } else if update { - fmt.Printf("Updating autocli data for %s\n", chain) - chainInfo := NewChainInfo(configDir, chain, chainConfig) - err := chainInfo.Load(true) - return err + cmd.Printf("Updating autocli data for %s\n", chain) + return chainInfo.Load(true) } else { return cmd.Help() } }, } - chainCmd.Flags().BoolVar(&update, "update", false, "update the CLI commands for the selected chain (should be used after every chain upgrade)") - chainCmd.Flags().BoolVar(&reconfig, "config", false, "re-configure the selected chain (allows choosing a new gRPC endpoint and refreshes data)") + chainCmd.Flags().BoolVar(&update, flagUpdate, false, "update the CLI commands for the selected chain (should be used after every chain upgrade)") + chainCmd.Flags().BoolVar(&reconfig, flagConfig, false, "re-configure the selected chain (allows choosing a new gRPC endpoint and refreshes data") + chainCmd.Flags().BoolVar(&insecure, flagInsecure, false, "allow re-configuring the selected chain using an insecure gRPC connection") - err = appOpts.EnhanceRootCommandWithBuilder(chainCmd, builder) - if err != nil { + if err := appOpts.EnhanceRootCommandWithBuilder(chainCmd, builder); err != nil { return nil, err } - cmd.AddCommand(chainCmd) + commands = append(commands, chainCmd) } - return cmd, nil + return commands, nil } -func reconfigure(configDir, chain string, config *Config) error { - fmt.Printf("Configuring %s\n", chain) +func RemoteErrorCommand(config *Config, configDir string, chain string, chainConfig *ChainConfig, err error) *cobra.Command { + cmd := &cobra.Command{ + Use: chain, + Short: "Unable to load data", + Long: "Unable to load data, reconfiguration needed.", + RunE: func(cmd *cobra.Command, args []string) error { + cmd.Printf("Error loading chain data for %s: %+v\n", chain, err) + + return reconfigure(cmd, config, configDir, chain) + }, + } + + cmd.Flags().Bool(flagInsecure, chainConfig.GRPCEndpoints[0].Insecure, "allow setting up insecure gRPC connection") + + return cmd +} + +func reconfigure(cmd *cobra.Command, config *Config, configDir, chain string) error { + insecure, _ := cmd.Flags().GetBool(flagInsecure) + + cmd.Printf("Configuring %s\n", chain) endpoint, err := SelectGRPCEndpoints(chain) if err != nil { return err } - fmt.Printf("Selected: %s\n", endpoint) + cmd.Printf("%s endpoint selected\n", endpoint) chainConfig := &ChainConfig{ GRPCEndpoints: []GRPCEndpoint{ { Endpoint: endpoint, + Insecure: insecure, }, }, } - config.Chains[chain] = chainConfig chainInfo := NewChainInfo(configDir, chain, chainConfig) - err = chainInfo.Load(true) - if err != nil { + if err = chainInfo.Load(true); err != nil { return err } - return SaveConfig(configDir, config) + config.Chains[chain] = chainConfig + if err := SaveConfig(configDir, config); err != nil { + return err + } + + cmd.Printf("Configuration saved to %s\n", configDir) + return nil } type dynamicTypeResolver struct {