Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add autocli multi-chain command #14696

Merged
merged 16 commits into from
Jan 23, 2023
4 changes: 4 additions & 0 deletions client/v2/autocli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func (appOptions AppOptions) EnhanceRootCommand(rootCmd *cobra.Command) error {
AddQueryConnFlags: flags.AddQueryFlagsToCmd,
}

return appOptions.EnhanceRootCommandWithBuilder(rootCmd, builder)
}

func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Command, builder *Builder) error {
moduleOptions := appOptions.ModuleOptions
if moduleOptions == nil {
moduleOptions = map[string]*autocliv1.ModuleOptions{}
Expand Down
253 changes: 253 additions & 0 deletions client/v2/autocli/internal/remote/compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package remote

import (
"context"
"fmt"
"io"
"strings"

autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"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"
)

// loadFileDescriptorsCompat attempts to load the file descriptor set using gRPC reflection when cosmos.reflection.v1
// is unavailable.
func loadFileDescriptorsCompat(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.\n")

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
}
}

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)
}
Fixed Show fixed Hide fixed

fdSet := &descriptorpb.FileDescriptorSet{}
for _, descriptorProto := range fdMap {
fdSet.File = append(fdSet.File, descriptorProto)
}
Fixed Show fixed Hide fixed

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)
}
}
}
Fixed Show fixed Hide fixed
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.\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{
Query: &autocliv1.ServiceCommandDescriptor{
SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{},
},
Tx: &autocliv1.ServiceCommandDescriptor{
SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{},
},
}
res[parts[0]] = modOpts
}

switch parts[1] {
case "query":
if numParts == 3 {
modOpts.Query.SubCommands[parts[2]] = &autocliv1.ServiceCommandDescriptor{Service: string(serviceName)}
} else {
modOpts.Query.Service = string(serviceName)
}
case "tx":
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 changes: 73 additions & 0 deletions client/v2/autocli/internal/remote/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package remote

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)
Fixed Show fixed Hide fixed
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")
}
Loading