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(connect): introduce ignite connect #102

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app.ignite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ apps:
wasm:
description: Scaffold a CosmosWasm-enabled chain with ease
path: ./wasm
connect:
description: Interact with any Cosmos SDK based blockchain using Ignite Connect
path: ./connect
cca:
description: Scaffold a blockchain frontend in seconds with Ignite
path: ./cca
Expand Down
5 changes: 5 additions & 0 deletions connect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Connect App Changelog

## [`v0.1.0`](https://github.com/ignite/apps/releases/tag/connect/v0.1.0)

* First release of the Connect app compatible with Ignite >= v28.x.y
41 changes: 41 additions & 0 deletions connect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Connect

This Ignite App extends [Ignite CLI](https://github.com/ignite/cli) to let a user interact with any Cosmos SDK based chain.

## Installation

```shell
ignite app install -g github.com/ignite/apps/connect
```

### Usage

* Discover available chains

```shell
ignite connect discover
```

* Add a chain to interact with

```shell
ignite connect add atomone
```

* (Or) Add a local chain to interact with

```shell
ignite connect add simapp localhost:9090
```

* List all connected chains

```shell
ignite connect
```

* Remove a connected chain

```shell
ignite connect rm atomone
```
87 changes: 87 additions & 0 deletions connect/chains/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package chains

import (
"fmt"
"os"
"path"

igniteconfig "github.com/ignite/cli/v28/ignite/config"

"gopkg.in/yaml.v3"
)

var (
configName = "connect.yaml"

// ErrConfigNotFound is returned when the config file is not found
ErrConfigNotFound = fmt.Errorf("config file not found")
)

type Config struct {
Chains map[string]*ChainConfig `yaml:"chains"`
}

type ChainConfig struct {
ChainID string `yaml:"chain_id"`
Bech32Prefix string `yaml:"bech32_prefix"`
GRPCEndpoint string `yaml:"grpc_endpoint"`
}

func (c *Config) Save() error {
out, err := yaml.Marshal(c)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

configDir, err := ConfigDir()
if err != nil {
return err
}

connectConfigPath := path.Join(configDir, configName)
if err := os.WriteFile(connectConfigPath, out, 0644); err != nil {
return fmt.Errorf("error saving config: %w", err)
}

return nil
}

func ReadConfig() (*Config, error) {
configDir, err := ConfigDir()
if err != nil {
return nil, err
}

connectConfigPath := path.Join(configDir, configName)
if _, err := os.Stat(connectConfigPath); os.IsNotExist(err) {
return &Config{map[string]*ChainConfig{}}, ErrConfigNotFound
} else if err != nil {
return nil, fmt.Errorf("failed to check config file: %w", err)
}

data, err := os.ReadFile(connectConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}

var c Config
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}

return &c, nil
}

func ConfigDir() (string, error) {
igniteConfigDir, err := igniteconfig.DirPath()
if err != nil {
return "", fmt.Errorf("failed to get ignite config directory: %w", err)
}

dir := path.Join(igniteConfigDir, "connect")
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("failed to create config directory: %w", err)
}

return dir, nil
}
165 changes: 165 additions & 0 deletions connect/chains/descriptors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package chains

import (
"context"
"crypto/tls"
"fmt"
"os"
"path"

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

authv1betav1 "cosmossdk.io/api/cosmos/auth/v1beta1"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
)

type Conn struct {
chainName string
config *ChainConfig
configDir string
client *grpc.ClientConn

ProtoFiles *protoregistry.Files
ModuleOptions map[string]*autocliv1.ModuleOptions
}

func NewConn(chainName string, cfg *ChainConfig) (*Conn, error) {
configDir, err := ConfigDir()
if err != nil {
return nil, err
}

return &Conn{
chainName: chainName,
config: cfg,
configDir: configDir,
}, nil
}

// fdsCacheFilename returns the filename for the cached file descriptor set.
func (c *Conn) fdsCacheFilename() string {
return path.Join(c.configDir, fmt.Sprintf("%s.fds", c.chainName))
}

// appOptsCacheFilename returns the filename for the app options cache file.
func (c *Conn) appOptsCacheFilename() string {
return path.Join(c.configDir, fmt.Sprintf("%s.autocli", c.chainName))
}

func (c *Conn) Load(ctx context.Context) error {
var err error
fdSet := &descriptorpb.FileDescriptorSet{}
fdsFilename := c.fdsCacheFilename()

if _, err := os.Stat(fdsFilename); os.IsNotExist(err) {
client, err := c.Connect()
if err != nil {
return err
}

reflectionClient := reflectionv1.NewReflectionServiceClient(client)
fdRes, err := reflectionClient.FileDescriptors(ctx, &reflectionv1.FileDescriptorsRequest{})
if err != nil {
return fmt.Errorf("error getting file descriptors: %w", err)
}

fdSet = &descriptorpb.FileDescriptorSet{File: fdRes.Files}
bz, err := proto.Marshal(fdSet)
if err != nil {
return err
}

if err = os.WriteFile(fdsFilename, bz, 0o600); err != nil {
return err
}
} else {
bz, err := os.ReadFile(fdsFilename)
if err != nil {
return err
}

if err = proto.Unmarshal(bz, fdSet); err != nil {
return err
}
}

c.ProtoFiles, err = protodesc.FileOptions{AllowUnresolvable: true}.NewFiles(fdSet)
if err != nil {
return fmt.Errorf("error building protoregistry.Files: %w", err)
}

appOptsFilename := c.appOptsCacheFilename()
if _, err := os.Stat(appOptsFilename); os.IsNotExist(err) {
client, err := c.Connect()
if err != nil {
return err
}

autocliQueryClient := autocliv1.NewQueryClient(client)
appOptsRes, err := autocliQueryClient.AppOptions(ctx, &autocliv1.AppOptionsRequest{})
if err != nil {
return fmt.Errorf("error getting autocli config: %w", err)
}

bz, err := proto.Marshal(appOptsRes)
if err != nil {
return err
}

if err := os.WriteFile(appOptsFilename, bz, 0o600); err != nil {
return err
}

c.ModuleOptions = appOptsRes.ModuleOptions
} else {
bz, err := os.ReadFile(appOptsFilename)
if err != nil {
return err
}

var appOptsRes autocliv1.AppOptionsResponse
if err := proto.Unmarshal(bz, &appOptsRes); err != nil {
return err
}

c.ModuleOptions = appOptsRes.ModuleOptions
}

return nil
}

func (c *Conn) Connect() (*grpc.ClientConn, error) {
if c.client != nil {
return c.client, nil
}

var err error
creds := credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
})

c.client, err = grpc.NewClient(c.config.GRPCEndpoint, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
}

// try connection by querying an endpoint
// fallback to insecure if it doesn't work
authClient := authv1betav1.NewQueryClient(c.client)
if _, err = authClient.Params(context.Background(), &authv1betav1.QueryParamsRequest{}); err != nil {
creds = insecure.NewCredentials()
c.client, err = grpc.NewClient(c.config.GRPCEndpoint, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
}
}

return c.client, nil
}
3 changes: 3 additions & 0 deletions connect/chains/keyring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package chains

// TODO(@julienrbrt): Implement in follow-up.
Loading
Loading