feat(hubl): support keyring (#17107)

This commit is contained in:
Julien Robert 2023-08-24 14:49:57 +02:00 committed by GitHub
parent 2f8247d2d5
commit 7c10b2482c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 352 additions and 153 deletions

View File

@ -90,7 +90,7 @@ modules:
"@type": cosmos.staking.module.v1.Module
- name: tx
config:
"@type": cosmos.tx.module.v1.Module
"@type": cosmos.tx.config.v1.Config
```
A more complete example of `app.yaml` can be found [here](https://github.com/cosmos/cosmos-sdk/blob/91b1d83f1339e235a1dfa929ecc00084101a19e3/simapp/app.yaml).

View File

@ -7,7 +7,7 @@ require (
cosmossdk.io/client/v2 v2.0.0-20230815130322-dded2e9921f0
cosmossdk.io/errors v1.0.0
github.com/cockroachdb/errors v1.10.0
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230815152400-f42d52f7e531
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230823104347-c6b0bb62ac70
github.com/manifoldco/promptui v0.9.0
github.com/pelletier/go-toml/v2 v2.0.9
github.com/spf13/cobra v1.7.0

View File

@ -184,8 +184,8 @@ github.com/cosmos/cosmos-db v1.0.0 h1:EVcQZ+qYag7W6uorBKFPvX6gRjw6Uq2hIh4hCWjuQ0
github.com/cosmos/cosmos-db v1.0.0/go.mod h1:iBvi1TtqaedwLdcrZVYRSSCb6eSy61NLj4UNmdIgs0U=
github.com/cosmos/cosmos-proto v1.0.0-beta.3 h1:VitvZ1lPORTVxkmF2fAp3IiA61xVwArQYKXTdEcpW6o=
github.com/cosmos/cosmos-proto v1.0.0-beta.3/go.mod h1:t8IASdLaAq+bbHbjq4p960BvcTqtwuAxid3b/2rOD6I=
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230815152400-f42d52f7e531 h1:wMqsKQzHof2ikpTlM5O1PZG4yr+15am+0ROtD/5wJLA=
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230815152400-f42d52f7e531/go.mod h1:yQzBSxaplSkNZd34HOEqF4NhOHikbpFwWhWAQSteXrw=
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230823104347-c6b0bb62ac70 h1:sK7w1WJAThRUXXtnpn7B8dBoSG8yFbYeuXNC+S69ANI=
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230823104347-c6b0bb62ac70/go.mod h1:lUps0DsgRmykXPyjoEBIkJa0Ybq666DFdubg6AB+WCg=
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw=
github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE=

View File

@ -1,68 +0,0 @@
package internal
import (
"bytes"
"os"
"path"
"github.com/pelletier/go-toml/v2"
"cosmossdk.io/errors"
)
type Config struct {
Chains map[string]*ChainConfig `toml:"chains"`
}
type ChainConfig struct {
GRPCEndpoints []GRPCEndpoint `toml:"trusted-grpc-endpoints"`
Bech32Prefix string `toml:"bech32-prefix"`
}
type GRPCEndpoint struct {
Endpoint string `toml:"endpoint"`
Insecure bool `toml:"insecure"`
}
func LoadConfig(configDir string) (*Config, error) {
configPath := configFilename(configDir)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return &Config{Chains: map[string]*ChainConfig{}}, nil
}
bz, err := os.ReadFile(configPath)
if err != nil {
return nil, errors.Wrapf(err, "can't read config file: %s", configPath)
}
config := &Config{}
if err = toml.Unmarshal(bz, config); err != nil {
return nil, errors.Wrapf(err, "can't load config file: %s", configPath)
}
return config, err
}
func SaveConfig(configDir string, config *Config) error {
buf := &bytes.Buffer{}
enc := toml.NewEncoder(buf)
if err := enc.Encode(config); err != nil {
return err
}
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}
configPath := configFilename(configDir)
if err := os.WriteFile(configPath, buf.Bytes(), 0o600); err != nil {
return err
}
return nil
}
func configFilename(configDir string) string {
return path.Join(configDir, "config.toml")
}

View File

@ -0,0 +1,108 @@
package config
import (
"bytes"
"os"
"path"
"github.com/pelletier/go-toml/v2"
"cosmossdk.io/errors"
"cosmossdk.io/tools/hubl/internal/flags"
)
const (
DefaultConfigDirName = ".hubl"
GlobalKeyringDirName = "global"
)
type Config struct {
Chains map[string]*ChainConfig `toml:"chains"`
KeyringBackend string `toml:"keyring-backend"`
}
type ChainConfig struct {
GRPCEndpoints []GRPCEndpoint `toml:"trusted-grpc-endpoints"`
AddressPrefix string `toml:"address-prefix"`
KeyringBackend string `toml:"keyring-backend"`
}
type GRPCEndpoint struct {
Endpoint string `toml:"endpoint"`
Insecure bool `toml:"insecure"`
}
var EmptyConfig = &Config{
Chains: map[string]*ChainConfig{},
KeyringBackend: flags.DefaultKeyringBackend,
}
func (cfg *Config) GetKeyringBackend(chainName string) (string, error) {
if chainName == GlobalKeyringDirName {
return cfg.KeyringBackend, nil
} else {
chainCfg, ok := cfg.Chains[chainName]
if ok {
return chainCfg.KeyringBackend, nil
}
}
return flags.DefaultKeyringBackend, nil
}
func GetConfigDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := path.Join(homeDir, DefaultConfigDirName)
if _, err := os.Stat(configDir); os.IsNotExist(err) {
return configDir, os.MkdirAll(configDir, 0o750)
}
return configDir, nil
}
func Load(configDir string) (*Config, error) {
configPath := configFilename(configDir)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return EmptyConfig, nil
}
bz, err := os.ReadFile(configPath)
if err != nil {
return nil, errors.Wrapf(err, "can't read config file: %s", configPath)
}
config := &Config{}
if err = toml.Unmarshal(bz, config); err != nil {
return nil, errors.Wrapf(err, "can't load config file: %s", configPath)
}
return config, err
}
func Save(configDir string, config *Config) error {
buf := &bytes.Buffer{}
enc := toml.NewEncoder(buf)
if err := enc.Encode(config); err != nil {
return err
}
if err := os.MkdirAll(configDir, 0o750); err != nil {
return err
}
configPath := configFilename(configDir)
if err := os.WriteFile(configPath, buf.Bytes(), 0o600); err != nil {
return err
}
return nil
}
func configFilename(configDir string) string {
return path.Join(configDir, "config.toml")
}

View File

@ -0,0 +1,18 @@
package flags
const (
FlagInsecure = "insecure"
FlagUpdate = "update"
FlagConfig = "config"
FlagLong = "long"
FlagOutput = "output"
FlagKeyringBackend = "keyring-backend"
)
const (
OutputFormatText = "text"
OutputFormatJSON = "json"
DefaultKeyringBackend = "os"
)

View File

@ -0,0 +1,109 @@
package internal
import (
"bufio"
"context"
"fmt"
"path"
"github.com/spf13/cobra"
_ "cosmossdk.io/api/cosmos/crypto/ed25519"
_ "cosmossdk.io/api/cosmos/crypto/secp256k1"
_ "cosmossdk.io/api/cosmos/crypto/secp256r1"
"cosmossdk.io/tools/hubl/internal/config"
"cosmossdk.io/tools/hubl/internal/flags"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
)
func KeyringCmd(chainName string) *cobra.Command {
shortDesc := fmt.Sprintf("Keyring management for %s", chainName)
if chainName == "" {
chainName = config.GlobalKeyringDirName
shortDesc = "Global keyring management for Hubl"
}
keyringCmd := &cobra.Command{
Use: "keys",
Short: shortDesc,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
registry := codectypes.NewInterfaceRegistry()
cryptocodec.RegisterInterfaces(registry)
cdc := codec.NewProtoCodec(registry)
configDir, err := config.GetConfigDir()
if err != nil {
return err
}
cfg, err := config.Load(configDir)
if err != nil {
return err
}
backend, err := cfg.GetKeyringBackend(chainName)
if err != nil {
return err
}
if changed := cmd.Flags().Changed(flags.FlagKeyringBackend); changed {
b, err := cmd.Flags().GetString(flags.FlagKeyringBackend)
if err != nil {
return err
}
backend = b
}
keyringDir := path.Join(configDir, "keyring", chainName)
inBuf := bufio.NewReader(cmd.InOrStdin())
kr, err := keyring.New(chainName, backend, keyringDir, inBuf, cdc)
if err != nil {
return err
}
addressCodec, validatorAddressCodec, consensusAddressCodec, err := getAddressCodecFromConfig(cfg, chainName)
if err != nil {
return err
}
clientCtx := client.Context{}.
WithKeyring(kr).
WithCodec(cdc).
WithKeyringDir(keyringDir).
WithInput(inBuf).
WithAddressCodec(addressCodec).
WithValidatorAddressCodec(validatorAddressCodec).
WithConsensusAddressCodec(consensusAddressCodec)
cmd.SetContext(context.WithValue(context.Background(), client.ClientContextKey, &clientCtx))
if err := client.SetCmdClientContext(cmd, clientCtx); err != nil {
return err
}
return nil
},
}
keyringCmd.AddCommand(
keys.AddKeyCommand(),
keys.DeleteKeyCommand(),
keys.ExportKeyCommand(),
keys.ImportKeyCommand(),
keys.ImportKeyHexCommand(),
keys.ListKeysCmd(),
keys.ParseKeyStringCommand(),
keys.RenameKeyCommand(),
keys.ShowKeysCmd(),
)
keyringCmd.PersistentFlags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|kwallet|pass|test|memory)")
keyringCmd.PersistentFlags().String(flags.FlagOutput, flags.OutputFormatText, "Output format (text|json)")
return keyringCmd
}

View File

@ -20,23 +20,22 @@ import (
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
reflectionv2alpha1 "cosmossdk.io/api/cosmos/base/reflection/v2alpha1"
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
"cosmossdk.io/tools/hubl/internal/config"
)
const DefaultConfigDirName = ".hubl"
type ChainInfo struct {
client *grpc.ClientConn
Context context.Context
ConfigDir string
Chain string
Config *ChainConfig
Config *config.ChainConfig
ProtoFiles *protoregistry.Files
ModuleOptions map[string]*autocliv1.ModuleOptions
}
func NewChainInfo(configDir, chain string, config *ChainConfig) *ChainInfo {
func NewChainInfo(configDir, chain string, config *config.ChainConfig) *ChainInfo {
return &ChainInfo{
Context: context.Background(),
Config: config,
@ -47,7 +46,7 @@ func NewChainInfo(configDir, chain string, config *ChainConfig) *ChainInfo {
func (c *ChainInfo) getCacheDir() (string, error) {
cacheDir := path.Join(c.ConfigDir, "cache")
return cacheDir, os.MkdirAll(cacheDir, 0o755)
return cacheDir, os.MkdirAll(cacheDir, 0o750)
}
func (c *ChainInfo) fdsCacheFilename() (string, error) {

View File

@ -11,11 +11,9 @@ import (
)
type ChainRegistryEntry struct {
APIs ChainRegistryAPIs `json:"apis"`
}
type ChainRegistryAPIs struct {
GRPC []*APIEntry `json:"grpc"`
APIs struct {
GRPC []*APIEntry `json:"grpc"`
} `json:"apis"`
}
type APIEntry struct {

View File

@ -3,8 +3,6 @@ package internal
import (
"context"
"fmt"
"os"
"path"
"strings"
"github.com/spf13/cobra"
@ -15,74 +13,33 @@ import (
"cosmossdk.io/client/v2/autocli"
"cosmossdk.io/client/v2/autocli/flag"
"cosmossdk.io/tools/hubl/internal/config"
"cosmossdk.io/tools/hubl/internal/flags"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
)
var (
flagInsecure = "insecure"
flagUpdate = "update"
flagConfig = "config"
flagLong = "long"
)
func RootCommand() (*cobra.Command, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
configDir := path.Join(homeDir, DefaultConfigDirName)
config, err := LoadConfig(configDir)
if err != nil {
return nil, err
}
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),
VersionCmd(),
)
cmd.AddCommand(commands...)
return cmd, nil
}
func InitCommand(config *Config, configDir string) *cobra.Command {
func InitCmd(config *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).
Long: `To configure a new chain, 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.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chainName := strings.ToLower(args[0])
return reconfigure(cmd, config, configDir, chainName)
},
}
cmd.Flags().BoolVar(&insecure, flagInsecure, false, "allow setting up insecure gRPC connection")
cmd.Flags().BoolVar(&insecure, flags.FlagInsecure, false, "allow setting up insecure gRPC connection")
return cmd
}
func RemoteCommand(config *Config, configDir string) ([]*cobra.Command, error) {
func RemoteCommand(config *config.Config, configDir string) ([]*cobra.Command, error) {
commands := []*cobra.Command{}
for chain, chainConfig := range config.Chains {
@ -103,11 +60,16 @@ func RemoteCommand(config *Config, configDir string) ([]*cobra.Command, error) {
ModuleOptions: chainInfo.ModuleOptions,
}
addressCodec, validatorAddressCodec, consensusAddressCodec, err := getAddressCodecFromConfig(config, chain)
if err != nil {
return nil, err
}
builder := &autocli.Builder{
Builder: flag.Builder{
AddressCodec: addresscodec.NewBech32Codec(chainConfig.Bech32Prefix),
ValidatorAddressCodec: addresscodec.NewBech32Codec(fmt.Sprintf("%svaloper", chainConfig.Bech32Prefix)),
ConsensusAddressCodec: addresscodec.NewBech32Codec(fmt.Sprintf("%svalcons", chainConfig.Bech32Prefix)),
AddressCodec: addressCodec,
ValidatorAddressCodec: validatorAddressCodec,
ConsensusAddressCodec: consensusAddressCodec,
TypeResolver: &dynamicTypeResolver{chainInfo},
FileResolver: chainInfo.ProtoFiles,
},
@ -132,17 +94,20 @@ func RemoteCommand(config *Config, configDir string) ([]*cobra.Command, error) {
case reconfig:
return reconfigure(cmd, config, configDir, chain)
case update:
cmd.Printf("Updating autocli data for %s\n", chain)
cmd.Printf("Updating AutoCLI data for %s\n", chain)
return chainInfo.Load(true)
default:
return cmd.Help()
}
},
}
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")
chainCmd.PersistentFlags().StringVar(&output, flags.FlagOutput, flags.OutputFormatJSON, "output format (text|json)")
chainCmd.Flags().BoolVar(&update, flags.FlagUpdate, false, "update the CLI commands for the selected chain (should be used after every chain upgrade)")
chainCmd.Flags().BoolVar(&reconfig, flags.FlagConfig, false, "re-configure the selected chain (allows choosing a new gRPC endpoint and refreshes data")
chainCmd.Flags().BoolVar(&insecure, flags.FlagInsecure, false, "allow re-configuring the selected chain using an insecure gRPC connection")
chainCmd.PersistentFlags().StringVar(&output, flags.FlagOutput, flags.OutputFormatJSON, fmt.Sprintf("output format (%s|%s)", flags.OutputFormatText, flags.OutputFormatJSON))
// add chain specific keyring
chainCmd.AddCommand(KeyringCmd(chainInfo.Chain))
if err := appOpts.EnhanceRootCommandWithBuilder(chainCmd, builder); err != nil {
return nil, err
@ -154,25 +119,24 @@ func RemoteCommand(config *Config, configDir string) ([]*cobra.Command, error) {
return commands, nil
}
func RemoteErrorCommand(config *Config, configDir, chain string, chainConfig *ChainConfig, err error) *cobra.Command {
func RemoteErrorCommand(cfg *config.Config, configDir, chain string, chainConfig *config.ChainConfig, err error) *cobra.Command {
cmd := &cobra.Command{
Use: chain,
Short: "Unable to load data",
Long: "Unable to load data, reconfiguration needed.",
Short: fmt.Sprintf("Unable to load %s data", chain),
Long: fmt.Sprintf("Unable to load %s data, reconfiguration needed.", chain),
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)
return reconfigure(cmd, cfg, configDir, chain)
},
}
cmd.Flags().Bool(flagInsecure, chainConfig.GRPCEndpoints[0].Insecure, "allow setting up insecure gRPC connection")
cmd.Flags().Bool(flags.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)
func reconfigure(cmd *cobra.Command, cfg *config.Config, configDir, chain string) error {
insecure, _ := cmd.Flags().GetBool(flags.FlagInsecure)
cmd.Printf("Configuring %s\n", chain)
endpoint, err := SelectGRPCEndpoints(chain)
@ -181,8 +145,8 @@ func reconfigure(cmd *cobra.Command, config *Config, configDir, chain string) er
}
cmd.Printf("%s endpoint selected\n", endpoint)
chainConfig := &ChainConfig{
GRPCEndpoints: []GRPCEndpoint{
chainConfig := &config.ChainConfig{
GRPCEndpoints: []config.GRPCEndpoint{
{
Endpoint: endpoint,
Insecure: insecure,
@ -205,10 +169,11 @@ func reconfigure(cmd *cobra.Command, config *Config, configDir, chain string) er
return err
}
chainConfig.Bech32Prefix = addressPrefix
config.Chains[chain] = chainConfig
chainConfig.KeyringBackend = flags.DefaultKeyringBackend
chainConfig.AddressPrefix = addressPrefix
cfg.Chains[chain] = chainConfig
if err := SaveConfig(configDir, config); err != nil {
if err := config.Save(configDir, cfg); err != nil {
return err
}

View File

@ -0,0 +1,39 @@
package internal
import (
"github.com/spf13/cobra"
"cosmossdk.io/tools/hubl/internal/config"
)
func RootCommand() (*cobra.Command, error) {
configDir, err := config.GetConfigDir()
if err != nil {
return nil, err
}
cfg, err := config.Load(configDir)
if err != nil {
return nil, err
}
cmd := &cobra.Command{
Use: "hubl",
Short: "Hubl is a CLI for interacting with Cosmos SDK chains",
}
// add commands
commands, err := RemoteCommand(cfg, configDir)
if err != nil {
return nil, err
}
commands = append(
commands,
InitCmd(cfg, configDir),
KeyringCmd(""),
VersionCmd(),
)
cmd.AddCommand(commands...)
return cmd, nil
}

View File

@ -0,0 +1,29 @@
package internal
import (
"fmt"
"cosmossdk.io/core/address"
"cosmossdk.io/tools/hubl/internal/config"
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
)
// getAddressCodecFromConfig returns the address codecs for the given chain name
func getAddressCodecFromConfig(cfg *config.Config, chainName string) (address.Codec, address.Codec, address.Codec, error) {
addressPrefix := "cosmos"
if chainName != config.GlobalKeyringDirName {
chainConfig, ok := cfg.Chains[chainName]
if !ok {
return nil, nil, nil, fmt.Errorf("chain %s not found in config", chainName)
}
addressPrefix = chainConfig.AddressPrefix
}
return addresscodec.NewBech32Codec(addressPrefix),
addresscodec.NewBech32Codec(fmt.Sprintf("%svaloper", addressPrefix)),
addresscodec.NewBech32Codec(fmt.Sprintf("%svalcons", addressPrefix)),
nil
}

View File

@ -6,6 +6,8 @@ import (
"strings"
"github.com/spf13/cobra"
"cosmossdk.io/tools/hubl/internal/flags"
)
func VersionCmd() *cobra.Command {
@ -34,7 +36,7 @@ func VersionCmd() *cobra.Command {
},
}
versionCmd.Flags().BoolVar(&long, flagLong, false, "display long version information")
versionCmd.Flags().BoolVar(&long, flags.FlagLong, false, "display long version information")
return versionCmd
}