Skip to content

Commit 7867ed9

Browse files
authored
feat(connect): introduce ignite connect (#102)
1 parent df7f61a commit 7867ed9

File tree

24 files changed

+2754
-5
lines changed

24 files changed

+2754
-5
lines changed

app.ignite.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ apps:
2424
wasm:
2525
description: Scaffold a CosmosWasm-enabled chain with ease
2626
path: ./wasm
27+
connect:
28+
description: Interact with any Cosmos SDK based blockchain using Ignite Connect
29+
path: ./connect
2730
cca:
2831
description: Scaffold a blockchain frontend in seconds with Ignite
2932
path: ./cca

connect/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Connect App Changelog
2+
3+
## [`v0.1.0`](https://github.com/ignite/apps/releases/tag/connect/v0.1.0)
4+
5+
* First release of the Connect app compatible with Ignite >= v28.x.y

connect/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Connect
2+
3+
This Ignite App extends [Ignite CLI](https://github.com/ignite/cli) to let a user interact with any Cosmos SDK based chain.
4+
5+
## Installation
6+
7+
```shell
8+
ignite app install -g github.com/ignite/apps/connect
9+
```
10+
11+
### Usage
12+
13+
* Discover available chains
14+
15+
```shell
16+
ignite connect discover
17+
```
18+
19+
* Add a chain to interact with
20+
21+
```shell
22+
ignite connect add atomone
23+
```
24+
25+
* (Or) Add a local chain to interact with
26+
27+
```shell
28+
ignite connect add simapp localhost:9090
29+
```
30+
31+
* List all connected chains
32+
33+
```shell
34+
ignite connect
35+
```
36+
37+
* Remove a connected chain
38+
39+
```shell
40+
ignite connect rm atomone
41+
```

connect/chains/config.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package chains
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path"
7+
8+
"gopkg.in/yaml.v3"
9+
10+
igniteconfig "github.com/ignite/cli/v28/ignite/config"
11+
)
12+
13+
var (
14+
configName = "connect.yaml"
15+
16+
// ErrConfigNotFound is returned when the config file is not found
17+
ErrConfigNotFound = fmt.Errorf("config file not found")
18+
)
19+
20+
type Config struct {
21+
Chains map[string]*ChainConfig `yaml:"chains"`
22+
}
23+
24+
type ChainConfig struct {
25+
ChainID string `yaml:"chain_id"`
26+
Bech32Prefix string `yaml:"bech32_prefix"`
27+
GRPCEndpoint string `yaml:"grpc_endpoint"`
28+
}
29+
30+
func (c *Config) Save() error {
31+
out, err := yaml.Marshal(c)
32+
if err != nil {
33+
return fmt.Errorf("failed to marshal config: %w", err)
34+
}
35+
36+
configDir, err := ConfigDir()
37+
if err != nil {
38+
return err
39+
}
40+
41+
connectConfigPath := path.Join(configDir, configName)
42+
if err := os.WriteFile(connectConfigPath, out, 0o644); err != nil {
43+
return fmt.Errorf("error saving config: %w", err)
44+
}
45+
46+
return nil
47+
}
48+
49+
func ReadConfig() (*Config, error) {
50+
configDir, err := ConfigDir()
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
connectConfigPath := path.Join(configDir, configName)
56+
if _, err := os.Stat(connectConfigPath); os.IsNotExist(err) {
57+
return &Config{map[string]*ChainConfig{}}, ErrConfigNotFound
58+
} else if err != nil {
59+
return nil, fmt.Errorf("failed to check config file: %w", err)
60+
}
61+
62+
data, err := os.ReadFile(connectConfigPath)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to read config: %w", err)
65+
}
66+
67+
var c Config
68+
if err := yaml.Unmarshal(data, &c); err != nil {
69+
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
70+
}
71+
72+
return &c, nil
73+
}
74+
75+
func ConfigDir() (string, error) {
76+
igniteConfigDir, err := igniteconfig.DirPath()
77+
if err != nil {
78+
return "", fmt.Errorf("failed to get ignite config directory: %w", err)
79+
}
80+
81+
dir := path.Join(igniteConfigDir, "apps", "connect")
82+
if err := os.MkdirAll(dir, 0o755); err != nil {
83+
return "", fmt.Errorf("failed to create config directory: %w", err)
84+
}
85+
86+
return dir, nil
87+
}

connect/chains/descriptors.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package chains
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"os"
8+
"path"
9+
10+
authv1betav1 "cosmossdk.io/api/cosmos/auth/v1beta1"
11+
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
12+
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
13+
"google.golang.org/grpc"
14+
"google.golang.org/grpc/credentials"
15+
"google.golang.org/grpc/credentials/insecure"
16+
"google.golang.org/protobuf/proto"
17+
"google.golang.org/protobuf/reflect/protodesc"
18+
"google.golang.org/protobuf/reflect/protoregistry"
19+
"google.golang.org/protobuf/types/descriptorpb"
20+
)
21+
22+
type Conn struct {
23+
chainName string
24+
config *ChainConfig
25+
configDir string
26+
client *grpc.ClientConn
27+
28+
ProtoFiles *protoregistry.Files
29+
ModuleOptions map[string]*autocliv1.ModuleOptions
30+
}
31+
32+
func NewConn(chainName string, cfg *ChainConfig) (*Conn, error) {
33+
configDir, err := ConfigDir()
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
return &Conn{
39+
chainName: chainName,
40+
config: cfg,
41+
configDir: configDir,
42+
}, nil
43+
}
44+
45+
// fdsCacheFilename returns the filename for the cached file descriptor set.
46+
func (c *Conn) fdsCacheFilename() string {
47+
return path.Join(c.configDir, fmt.Sprintf("%s.fds", c.chainName))
48+
}
49+
50+
// appOptsCacheFilename returns the filename for the app options cache file.
51+
func (c *Conn) appOptsCacheFilename() string {
52+
return path.Join(c.configDir, fmt.Sprintf("%s.autocli", c.chainName))
53+
}
54+
55+
func (c *Conn) Load(ctx context.Context) error {
56+
var err error
57+
fdSet := &descriptorpb.FileDescriptorSet{}
58+
fdsFilename := c.fdsCacheFilename()
59+
60+
if _, err := os.Stat(fdsFilename); os.IsNotExist(err) {
61+
client, err := c.Connect()
62+
if err != nil {
63+
return err
64+
}
65+
66+
reflectionClient := reflectionv1.NewReflectionServiceClient(client)
67+
fdRes, err := reflectionClient.FileDescriptors(ctx, &reflectionv1.FileDescriptorsRequest{})
68+
if err != nil {
69+
return fmt.Errorf("error getting file descriptors: %w", err)
70+
}
71+
72+
fdSet = &descriptorpb.FileDescriptorSet{File: fdRes.Files}
73+
bz, err := proto.Marshal(fdSet)
74+
if err != nil {
75+
return err
76+
}
77+
78+
if err = os.WriteFile(fdsFilename, bz, 0o600); err != nil {
79+
return err
80+
}
81+
} else {
82+
bz, err := os.ReadFile(fdsFilename)
83+
if err != nil {
84+
return err
85+
}
86+
87+
if err = proto.Unmarshal(bz, fdSet); err != nil {
88+
return err
89+
}
90+
}
91+
92+
c.ProtoFiles, err = protodesc.FileOptions{AllowUnresolvable: true}.NewFiles(fdSet)
93+
if err != nil {
94+
return fmt.Errorf("error building protoregistry.Files: %w", err)
95+
}
96+
97+
appOptsFilename := c.appOptsCacheFilename()
98+
if _, err := os.Stat(appOptsFilename); os.IsNotExist(err) {
99+
client, err := c.Connect()
100+
if err != nil {
101+
return err
102+
}
103+
104+
autocliQueryClient := autocliv1.NewQueryClient(client)
105+
appOptsRes, err := autocliQueryClient.AppOptions(ctx, &autocliv1.AppOptionsRequest{})
106+
if err != nil {
107+
return fmt.Errorf("error getting autocli config: %w", err)
108+
}
109+
110+
bz, err := proto.Marshal(appOptsRes)
111+
if err != nil {
112+
return err
113+
}
114+
115+
if err := os.WriteFile(appOptsFilename, bz, 0o600); err != nil {
116+
return err
117+
}
118+
119+
c.ModuleOptions = appOptsRes.ModuleOptions
120+
} else {
121+
bz, err := os.ReadFile(appOptsFilename)
122+
if err != nil {
123+
return err
124+
}
125+
126+
var appOptsRes autocliv1.AppOptionsResponse
127+
if err := proto.Unmarshal(bz, &appOptsRes); err != nil {
128+
return err
129+
}
130+
131+
c.ModuleOptions = appOptsRes.ModuleOptions
132+
}
133+
134+
return nil
135+
}
136+
137+
func (c *Conn) Connect() (*grpc.ClientConn, error) {
138+
if c.client != nil {
139+
return c.client, nil
140+
}
141+
142+
var err error
143+
creds := credentials.NewTLS(&tls.Config{
144+
MinVersion: tls.VersionTLS12,
145+
})
146+
147+
c.client, err = grpc.NewClient(c.config.GRPCEndpoint, grpc.WithTransportCredentials(creds))
148+
if err != nil {
149+
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
150+
}
151+
152+
// try connection by querying an endpoint
153+
// fallback to insecure if it doesn't work
154+
authClient := authv1betav1.NewQueryClient(c.client)
155+
if _, err = authClient.Params(context.Background(), &authv1betav1.QueryParamsRequest{}); err != nil {
156+
creds = insecure.NewCredentials()
157+
c.client, err = grpc.NewClient(c.config.GRPCEndpoint, grpc.WithTransportCredentials(creds))
158+
if err != nil {
159+
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
160+
}
161+
}
162+
163+
return c.client, nil
164+
}

connect/chains/keyring.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package chains
2+
3+
// TODO(@julienrbrt): Implement in follow-up.

0 commit comments

Comments
 (0)