feat: add autocli multi-chain command (#14696)
Co-authored-by: Marko <marbar3778@yahoo.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user