feat: add autocli multi-chain command (#14696)

Co-authored-by: Marko <marbar3778@yahoo.com>
This commit is contained in:
Aaron Craelius
2023-01-23 19:23:37 +00:00
committed by GitHub
co-authored by Marko
parent 9b5ac6abf0
commit d86ba04629
12 changed files with 2177 additions and 13 deletions
+17
View File
@@ -0,0 +1,17 @@
package main
import (
"cosmossdk.io/tools/hubl/internal"
)
func main() {
cmd, err := internal.RootCommand()
if err != nil {
panic(err)
}
err = cmd.Execute()
if err != nil {
panic(err)
}
}
+126
View File
@@ -0,0 +1,126 @@
module cosmossdk.io/tools/hubl
go 1.19
require (
cosmossdk.io/api v0.2.6
cosmossdk.io/client/v2 v2.0.0-20230120150717-4f6f6c00021f
github.com/cockroachdb/errors v1.9.0
github.com/hashicorp/go-multierror v1.1.1
github.com/manifoldco/promptui v0.9.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.6.1
google.golang.org/grpc v1.52.0
google.golang.org/protobuf v1.28.1
)
require (
cosmossdk.io/core v0.4.1 // indirect
cosmossdk.io/depinject v1.0.0-alpha.3 // indirect
cosmossdk.io/errors v1.0.0-beta.7 // indirect
cosmossdk.io/math v1.0.0-beta.4 // indirect
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.1 // indirect
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
github.com/cockroachdb/redact v1.1.3 // indirect
github.com/confio/ics23/go v0.9.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.1 // indirect
github.com/cosmos/cosmos-sdk v0.47.0-rc1 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogoproto v1.4.3 // indirect
github.com/cosmos/gorocksdb v1.2.0 // indirect
github.com/cosmos/iavl v0.19.4 // indirect
github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/getsentry/sentry-go v0.12.0 // indirect
github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.1.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.15.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.15.0 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tendermint/btcd v0.1.1 // indirect
github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tendermint/tendermint v0.37.0-rc2 // indirect
github.com/tendermint/tm-db v0.6.7 // indirect
github.com/tidwall/btree v1.5.2 // indirect
github.com/zondax/hid v0.9.1 // indirect
github.com/zondax/ledger-go v0.14.1 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/exp v0.0.0-20221019170559-20944726eadf // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/term v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
pgregory.net/rapid v0.5.5 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace cosmossdk.io/client/v2 => ../../client/v2
+1055
View File
File diff suppressed because it is too large Load Diff
+285
View File
@@ -0,0 +1,285 @@
package internal
import (
"context"
"fmt"
"io"
"strings"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
reflectionv1beta1 "cosmossdk.io/api/cosmos/base/reflection/v1beta1"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
"google.golang.org/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
)
// 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")
var interfaceImplNames []string
cosmosReflectBetaClient := reflectionv1beta1.NewReflectionServiceClient(client)
interfacesRes, err := cosmosReflectBetaClient.ListAllInterfaces(ctx, &reflectionv1beta1.ListAllInterfacesRequest{})
if err == nil {
for _, iface := range interfacesRes.InterfaceNames {
implRes, err := cosmosReflectBetaClient.ListImplementations(ctx, &reflectionv1beta1.ListImplementationsRequest{
InterfaceName: iface,
})
if err == nil {
for _, name := range implRes.ImplementationMessageNames {
interfaceImplNames = append(interfaceImplNames, name)
}
}
}
}
reflectClient, err := grpc_reflection_v1alpha.NewServerReflectionClient(client).ServerReflectionInfo(ctx)
if err != nil {
return nil, err
}
fdMap := map[string]*descriptorpb.FileDescriptorProto{}
waitListServiceRes := make(chan *grpc_reflection_v1alpha.ListServiceResponse)
waitc := make(chan struct{})
go func() {
for {
in, err := reflectClient.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
panic(err)
}
switch res := in.MessageResponse.(type) {
case *grpc_reflection_v1alpha.ServerReflectionResponse_ListServicesResponse:
waitListServiceRes <- res.ListServicesResponse
case *grpc_reflection_v1alpha.ServerReflectionResponse_FileDescriptorResponse:
processFileDescriptorsResponse(res, fdMap)
}
}
}()
err = reflectClient.Send(&grpc_reflection_v1alpha.ServerReflectionRequest{
MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{},
})
if err != nil {
return nil, err
}
listServiceRes := <-waitListServiceRes
for _, response := range listServiceRes.Service {
err = reflectClient.Send(&grpc_reflection_v1alpha.ServerReflectionRequest{
MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{
FileContainingSymbol: response.Name,
},
})
if err != nil {
return nil, err
}
}
for _, msgName := range interfaceImplNames {
err = reflectClient.Send(&grpc_reflection_v1alpha.ServerReflectionRequest{
MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{
FileContainingSymbol: msgName,
},
})
if err != nil {
return nil, err
}
}
err = reflectClient.CloseSend()
if err != nil {
return nil, err
}
<-waitc
// we loop through all the file descriptor dependencies to capture any file descriptors we haven't loaded yet
cantFind := map[string]bool{}
for {
missing := missingFileDescriptors(fdMap, cantFind)
if len(missing) == 0 {
break
}
err = addMissingFileDescriptors(ctx, client, fdMap, missing)
if err != nil {
return nil, err
}
// mark all deps that we aren't able to resolve as can't find, so we don't keep looping and get a 429 error
for _, dep := range missing {
if fdMap[dep] == nil {
cantFind[dep] = true
}
}
}
for dep := range cantFind {
fmt.Printf("Warning: can't find %s.\n", dep)
}
fdSet := &descriptorpb.FileDescriptorSet{}
for _, descriptorProto := range fdMap {
fdSet.File = append(fdSet.File, descriptorProto)
}
return fdSet, nil
}
func processFileDescriptorsResponse(res *grpc_reflection_v1alpha.ServerReflectionResponse_FileDescriptorResponse, fdMap map[string]*descriptorpb.FileDescriptorProto) {
for _, bz := range res.FileDescriptorResponse.FileDescriptorProto {
fd := &descriptorpb.FileDescriptorProto{}
err := proto.Unmarshal(bz, fd)
if err != nil {
panic(err)
}
fdMap[fd.GetName()] = fd
}
}
func missingFileDescriptors(fdMap map[string]*descriptorpb.FileDescriptorProto, cantFind map[string]bool) []string {
var missing []string
for _, descriptorProto := range fdMap {
for _, dep := range descriptorProto.Dependency {
if fdMap[dep] == nil && !cantFind[dep] /* skip deps we've marked as can't find */ {
missing = append(missing, dep)
}
}
}
return missing
}
func addMissingFileDescriptors(ctx context.Context, client *grpc.ClientConn, fdMap map[string]*descriptorpb.FileDescriptorProto, missingFiles []string) error {
reflectClient, err := grpc_reflection_v1alpha.NewServerReflectionClient(client).ServerReflectionInfo(ctx)
if err != nil {
return err
}
waitc := make(chan struct{})
go func() {
for {
in, err := reflectClient.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
panic(err)
}
switch res := in.MessageResponse.(type) {
case *grpc_reflection_v1alpha.ServerReflectionResponse_FileDescriptorResponse:
processFileDescriptorsResponse(res, fdMap)
}
}
}()
for _, file := range missingFiles {
err = reflectClient.Send(&grpc_reflection_v1alpha.ServerReflectionRequest{
MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileByFilename{
FileByFilename: file,
},
})
if err != nil {
return err
}
}
err = reflectClient.CloseSend()
if err != nil {
return err
}
<-waitc
return nil
}
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")
res := map[string]*autocliv1.ModuleOptions{}
files.RangeFiles(func(descriptor protoreflect.FileDescriptor) bool {
services := descriptor.Services()
n := services.Len()
for i := 0; i < n; i++ {
service := services.Get(i)
serviceName := service.FullName()
mapping, ok := defaultAutocliMappings[serviceName]
if ok {
parts := strings.Split(mapping, " ")
numParts := len(parts)
if numParts < 2 || numParts > 3 {
fmt.Printf("Warning: bad mapping %q found for %q\n", mapping, serviceName)
continue
}
modOpts := res[parts[0]]
if modOpts == nil {
modOpts = &autocliv1.ModuleOptions{}
res[parts[0]] = modOpts
}
switch parts[1] {
case "query":
if modOpts.Query == nil {
modOpts.Query = &autocliv1.ServiceCommandDescriptor{
SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{},
}
}
if numParts == 3 {
modOpts.Query.SubCommands[parts[2]] = &autocliv1.ServiceCommandDescriptor{Service: string(serviceName)}
} else {
modOpts.Query.Service = string(serviceName)
}
case "tx":
if modOpts.Tx == nil {
modOpts.Tx = &autocliv1.ServiceCommandDescriptor{
SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{},
}
}
if numParts == 3 {
modOpts.Tx.SubCommands[parts[2]] = &autocliv1.ServiceCommandDescriptor{Service: string(serviceName)}
} else {
modOpts.Tx.Service = string(serviceName)
}
default:
fmt.Printf("Warning: bad mapping %q found for %q\n", mapping, serviceName)
continue
}
}
}
return true
})
return &autocliv1.AppOptionsResponse{ModuleOptions: res}
}
var defaultAutocliMappings = map[protoreflect.FullName]string{
"cosmos.auth.v1beta1.Query": "auth query",
"cosmos.authz.v1beta1.Query": "authz query",
"cosmos.bank.v1beta1.Query": "bank query",
"cosmos.distribution.v1beta1.Query": "distribution query",
"cosmos.evidence.v1.Query": "evidence query",
"cosmos.feegrant.v1beta1.Query": "feegrant query",
"cosmos.gov.v1.Query": "gov query",
"cosmos.gov.v1beta1.Query": "gov query v1beta1",
"cosmos.group.v1.Query": "group query",
"cosmos.mint.v1beta1.Query": "mint query",
"cosmos.params.v1beta1.Query": "params query",
"cosmos.slashing.v1beta1.Query": "slashing query",
"cosmos.staking.v1beta1.Query": "staking query",
"cosmos.upgrade.v1.Query": "upgrade query",
}
+73
View File
@@ -0,0 +1,73 @@
package internal
import (
"bytes"
"fmt"
"os"
"path"
"github.com/pelletier/go-toml/v2"
"github.com/pkg/errors"
)
type Config struct {
Chains map[string]*ChainConfig `toml:"chains"`
}
type ChainConfig struct {
GRPCEndpoints []GRPCEndpoint `toml:"trusted-grpc-endpoints"`
}
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) {
// file doesn't exist
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{}
err = toml.Unmarshal(bz, config)
if err != nil {
return nil, errors.Wrapf(err, "can't load config file: %s", configPath)
}
return config, err
}
func SaveConfig(configDir string, config *Config) error {
configPath := configFilename(configDir)
buf := &bytes.Buffer{}
enc := toml.NewEncoder(buf)
err := enc.Encode(config)
if err != nil {
return err
}
err = os.MkdirAll(configDir, 0755)
if err != nil {
return err
}
err = os.WriteFile(configPath, buf.Bytes(), 0644)
if err != nil {
return err
}
fmt.Printf("Saved config in %s\n", configPath)
return nil
}
func configFilename(configDir string) string {
return path.Join(configDir, "config.toml")
}
+187
View File
@@ -0,0 +1,187 @@
package internal
import (
"context"
"crypto/tls"
"fmt"
"os"
"path"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
"github.com/cockroachdb/errors"
"github.com/hashicorp/go-multierror"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
)
const DefaultDirName = ".hubl"
type ChainInfo struct {
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 {
return &ChainInfo{
ConfigDir: configDir,
Chain: chain,
Config: config,
Context: context.Background(),
}
}
func (c *ChainInfo) getCacheDir() (string, error) {
cacheDir := path.Join(c.ConfigDir, "cache")
return cacheDir, os.MkdirAll(cacheDir, 0755)
}
func (c *ChainInfo) fdsCacheFilename() (string, error) {
cacheDir, err := c.getCacheDir()
if err != nil {
return "", err
}
return path.Join(cacheDir, fmt.Sprintf("%s.fds", c.Chain)), nil
}
func (c *ChainInfo) appOptsCacheFilename() (string, error) {
cacheDir, err := c.getCacheDir()
if err != nil {
return "", err
}
return path.Join(cacheDir, fmt.Sprintf("%s.autocli", c.Chain)), nil
}
func (c *ChainInfo) Load(reload bool) error {
fdSet := &descriptorpb.FileDescriptorSet{}
fdsFilename, err := c.fdsCacheFilename()
if err != nil {
return err
}
if _, err := os.Stat(fdsFilename); os.IsNotExist(err) || reload {
client, err := c.OpenClient()
if err != nil {
return err
}
reflectionClient := reflectionv1.NewReflectionServiceClient(client)
fdRes, err := reflectionClient.FileDescriptors(c.Context, &reflectionv1.FileDescriptorsRequest{})
if err != nil {
fdSet, err = loadFileDescriptorsGRPCReflection(c.Context, client)
if err != nil {
return err
}
} else {
fdSet = &descriptorpb.FileDescriptorSet{File: fdRes.Files}
}
bz, err := proto.Marshal(fdSet)
if err != nil {
return err
}
err = os.WriteFile(fdsFilename, bz, 0644)
if err != nil {
return err
}
} else {
bz, err := os.ReadFile(fdsFilename)
if err != nil {
return err
}
err = proto.Unmarshal(bz, fdSet)
if err != nil {
return err
}
}
c.ProtoFiles, err = protodesc.FileOptions{AllowUnresolvable: true}.NewFiles(fdSet)
if err != nil {
return errors.Wrapf(err, "error building protoregistry.Files")
}
appOptsFilename, err := c.appOptsCacheFilename()
if err != nil {
return err
}
if _, err := os.Stat(appOptsFilename); os.IsNotExist(err) || reload {
client, err := c.OpenClient()
if err != nil {
return err
}
autocliQueryClient := autocliv1.NewQueryClient(client)
appOptionsRes, err := autocliQueryClient.AppOptions(c.Context, &autocliv1.AppOptionsRequest{})
if err != nil {
appOptionsRes = guessAutocli(c.ProtoFiles)
}
bz, err := proto.Marshal(appOptionsRes)
if err != nil {
return err
}
err = os.WriteFile(appOptsFilename, bz, 0644)
if err != nil {
return err
}
c.ModuleOptions = appOptionsRes.ModuleOptions
} else {
bz, err := os.ReadFile(appOptsFilename)
if err != nil {
return err
}
var appOptsRes autocliv1.AppOptionsResponse
err = proto.Unmarshal(bz, &appOptsRes)
if err != nil {
return err
}
c.ModuleOptions = appOptsRes.ModuleOptions
}
return nil
}
func (c *ChainInfo) OpenClient() (*grpc.ClientConn, error) {
if c.client != nil {
return c.client, nil
}
var res error
for _, endpoint := range c.Config.GRPCEndpoints {
var err error
var creds credentials.TransportCredentials
if endpoint.Insecure {
creds = insecure.NewCredentials()
} else {
creds = credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
})
}
c.client, err = grpc.Dial(endpoint.Endpoint, grpc.WithTransportCredentials(creds))
if err != nil {
res = multierror.Append(res, err)
continue
}
return c.client, nil
}
return nil, errors.Wrapf(res, "error loading gRPC client")
}
+80
View File
@@ -0,0 +1,80 @@
package internal
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/manifoldco/promptui"
)
type ChainRegistryEntry struct {
APIs ChainRegistryAPIs `json:"apis"`
}
type ChainRegistryAPIs struct {
GRPC []*APIEntry `json:"grpc"`
}
type APIEntry struct {
Address string
Provider string
}
func GetChainRegistryEntry(chain string) (*ChainRegistryEntry, error) {
res, err := http.Get(fmt.Sprintf("https://raw.githubusercontent.com/cosmos/chain-registry/master/%v/chain.json", chain))
if err != nil {
return nil, err
}
bz, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
data := &ChainRegistryEntry{}
err = json.Unmarshal(bz, data)
if err != nil {
return nil, err
}
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)
prompt := &promptui.Prompt{
Label: "Enter a gRPC endpoint that you trust",
}
return prompt.Run()
}
var items []string
if entry != nil {
for _, apiEntry := range entry.APIs.GRPC {
items = append(items, fmt.Sprintf("%s: %s", apiEntry.Provider, apiEntry.Address))
}
}
prompt := promptui.SelectWithAdd{
Label: fmt.Sprintf("Select a gRPC endpoint that you trust for the %s network", chain),
Items: items,
AddLabel: "Custom endpoint:",
}
i, ep, err := prompt.Run()
if err != nil {
return "", err
}
// user selected a custom endpoint
if i == -1 {
return ep, nil
}
return entry.APIs.GRPC[i].Address, nil
}
+188
View File
@@ -0,0 +1,188 @@
package internal
import (
"fmt"
"os"
"path"
"strings"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/dynamicpb"
"cosmossdk.io/client/v2/autocli"
"cosmossdk.io/client/v2/autocli/flag"
)
func RootCommand() (*cobra.Command, error) {
userCfgDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
configDir := path.Join(userCfgDir, DefaultDirName)
config, err := LoadConfig(configDir)
if err != nil {
return nil, err
}
var initChain string
cmd := &cobra.Command{
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",
RunE: func(cmd *cobra.Command, args []string) error {
if initChain != "" {
return reconfigure(configDir, initChain, config)
}
return cmd.Help()
},
}
cmd.Flags().StringVar(&initChain, "init", "", "initialize a new chain with the specified name")
for chain, chainConfig := range config.Chains {
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)
},
})
continue
}
appOpts := autocli.AppOptions{
ModuleOptions: chainInfo.ModuleOptions,
}
builder := &autocli.Builder{
Builder: flag.Builder{
TypeResolver: &dynamicTypeResolver{chainInfo},
FileResolver: chainInfo.ProtoFiles,
},
GetClientConn: func(command *cobra.Command) (grpc.ClientConnInterface, error) {
return chainInfo.OpenClient()
},
AddQueryConnFlags: func(command *cobra.Command) {},
}
var update bool
var reconfig 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)
} else if update {
fmt.Printf("Updating autocli data for %s\n", chain)
chainInfo := NewChainInfo(configDir, chain, chainConfig)
err := chainInfo.Load(true)
return err
} 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)")
err = appOpts.EnhanceRootCommandWithBuilder(chainCmd, builder)
if err != nil {
return nil, err
}
cmd.AddCommand(chainCmd)
}
return cmd, nil
}
func reconfigure(configDir, chain string, config *Config) error {
fmt.Printf("Configuring %s\n", chain)
endpoint, err := SelectGRPCEndpoints(chain)
if err != nil {
return err
}
fmt.Printf("Selected: %s\n", endpoint)
chainConfig := &ChainConfig{
GRPCEndpoints: []GRPCEndpoint{
{
Endpoint: endpoint,
},
},
}
config.Chains[chain] = chainConfig
chainInfo := NewChainInfo(configDir, chain, chainConfig)
err = chainInfo.Load(true)
if err != nil {
return err
}
return SaveConfig(configDir, config)
}
type dynamicTypeResolver struct {
*ChainInfo
}
var _ protoregistry.MessageTypeResolver = dynamicTypeResolver{}
var _ protoregistry.ExtensionTypeResolver = dynamicTypeResolver{}
func (d dynamicTypeResolver) FindMessageByName(message protoreflect.FullName) (protoreflect.MessageType, error) {
desc, err := d.ProtoFiles.FindDescriptorByName(message)
if err != nil {
return nil, err
}
return dynamicpb.NewMessageType(desc.(protoreflect.MessageDescriptor)), nil
}
func (d dynamicTypeResolver) FindMessageByURL(url string) (protoreflect.MessageType, error) {
if i := strings.LastIndexByte(url, '/'); i >= 0 {
url = url[i+len("/"):]
}
return d.FindMessageByName(protoreflect.FullName(url))
}
func (d dynamicTypeResolver) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
desc, err := d.ProtoFiles.FindDescriptorByName(field)
if err != nil {
return nil, err
}
return dynamicpb.NewExtensionType(desc.(protoreflect.ExtensionTypeDescriptor)), nil
}
func (d dynamicTypeResolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
desc, err := d.ProtoFiles.FindDescriptorByName(message)
if err != nil {
return nil, err
}
messageDesc := desc.(protoreflect.MessageDescriptor)
exts := messageDesc.Extensions()
n := exts.Len()
for i := 0; i < n; i++ {
ext := exts.Get(i)
if ext.Number() == field {
return dynamicpb.NewExtensionType(ext), nil
}
}
return nil, protoregistry.NotFound
}