Skip to content

Commit 4ee2f35

Browse files
committed
Initial lumera‑ica‑client CLI with ICA workflows, docs, and CI
1 parent a1e34eb commit 4ee2f35

21 files changed

Lines changed: 3194 additions & 33 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Setup Go from go.mod
2+
description: Detect the Go toolchain version from go.mod and install it with caching enabled
3+
outputs:
4+
version:
5+
description: Detected Go version
6+
value: ${{ steps.determine.outputs.version }}
7+
runs:
8+
using: composite
9+
steps:
10+
- id: determine
11+
name: Determine Go version
12+
shell: bash
13+
run: |
14+
set -euo pipefail
15+
16+
VERSION=""
17+
TOOLCHAIN_VERSION=$(grep -E '^toolchain go[0-9]+\.[0-9]+(\.[0-9]+)?$' go.mod | cut -d ' ' -f 2 | sed 's/^go//' || true)
18+
if [ -n "$TOOLCHAIN_VERSION" ]; then
19+
VERSION="$TOOLCHAIN_VERSION"
20+
echo "Detected toolchain directive: go$VERSION"
21+
else
22+
VERSION=$(grep -E '^go [0-9]+\.[0-9]+(\.[0-9]+)?$' go.mod | cut -d ' ' -f 2 || true)
23+
if [ -n "$VERSION" ]; then
24+
echo "Detected go directive: $VERSION"
25+
fi
26+
fi
27+
28+
if [ -z "$VERSION" ]; then
29+
echo "Unable to determine Go version from go.mod" >&2
30+
exit 1
31+
fi
32+
33+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
34+
35+
- name: Setup Go
36+
uses: actions/setup-go@v6
37+
with:
38+
go-version: ${{ steps.determine.outputs.version }}
39+
cache: true

.github/workflows/build.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main, develop]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v6.0.1
15+
16+
- name: Set up Go
17+
uses: ./.github/actions/setup-go
18+
19+
- name: Download dependencies
20+
run: go mod download
21+
22+
- name: Build
23+
run: make build

.gitignore

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1-
# If you prefer the allow list template instead of the deny list, see community template:
2-
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3-
#
4-
# Binaries for programs and plugins
5-
*.exe
6-
*.exe~
7-
*.dll
8-
*.so
9-
*.dylib
10-
11-
# Test binary, built with `go test -c`
12-
*.test
13-
14-
# Code coverage profiles and other test artifacts
15-
*.out
16-
coverage.*
17-
*.coverprofile
18-
profile.cov
19-
20-
# Dependency directories (remove the comment below to include it)
21-
# vendor/
22-
23-
# Go workspace file
24-
go.work
25-
go.work.sum
26-
27-
# env file
28-
.env
29-
30-
# Editor/IDE
31-
# .idea/
32-
# .vscode/
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Code coverage profiles and other test artifacts
15+
*.out
16+
coverage.*
17+
*.coverprofile
18+
profile.cov
19+
20+
# Dependency directories (remove the comment below to include it)
21+
# vendor/
22+
23+
# Go workspace file
24+
go.work
25+
go.work.sum
26+
27+
# env file
28+
.env
29+
30+
# Editor/IDE
31+
# .idea/
32+
# .vscode/
33+
34+
build/

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
APP ?= lumera-ica-client
2+
BIN_DIR ?= build
3+
4+
.PHONY: build
5+
build:
6+
@mkdir -p $(BIN_DIR)
7+
go build -o $(BIN_DIR)/$(APP) .

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# lumera-ica-client
2-
Lumera reference client - ICA flow for Cascade actions
2+
Lumera reference client for ICA-based Cascade actions.
3+
4+
Developer guide: `docs/DEVELOPER_GUIDE.md`

build/config.toml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[lumera]
2+
# Chain ID of the Lumera network
3+
chain_id = "lumera-testnet-2"
4+
5+
# Address of the gRPC server for the Lumera node
6+
grpc_endpoint = "grpc.testnet.lumera.io:443"
7+
8+
# Address of the RPC HTTP server for the Lumera node
9+
rpc_endpoint = "https://rpc.testnet.lumera.io"
10+
11+
# SDK log level: debug, info, warn, error
12+
log_level = "info"
13+
14+
# Key name in the controller keyring to use for Lumera signing.
15+
key_name = "lumera-ibc-test"
16+
17+
[controller]
18+
# Chain ID of the controller network
19+
chain_id = "osmo-test-5"
20+
# human-readable part of the chain addresses
21+
hrp = "osmo"
22+
rpc_endpoint = "https://rpc.testnet.osmosis.zone:443"
23+
binary = "osmosisd"
24+
home = "~/.osmosisd-testnet"
25+
gas_prices = "0.03uosmo"
26+
27+
key_name = "osmosis-ibc-test"
28+
# This keyring is used for ICA signing and cascade metadata (no separate Lumera keyring).
29+
# KeyRing backend for storing keys: "file", "test", or "os" (default: test)
30+
keyring_backend = "test"
31+
# keyring_dir is used for "file" backend; for "test" it defaults to controller home when unset.
32+
#keyring_dir = "~/.osmosisd-testnet"
33+
34+
# Keyring passphrase in a plain text
35+
#keyring_passphrase_plain = ""
36+
37+
# Keyring passphrase in a text file
38+
#keyring_passphrase_file = ""
39+
40+
# controller-side ibc connection id
41+
connection_id = "connection-4370"
42+
# counterparty_connection_id is optional; needed when building ICA version metadata.
43+
counterparty_connection_id = "connection-4"

build/lumera-ica-client

84.8 MB
Binary file not shown.

build/nature-417074.jpg

1.22 MB
Loading

client/cascade_client.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"time"
11+
12+
"github.com/LumeraProtocol/sdk-go/cascade"
13+
sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
14+
"github.com/cosmos/cosmos-sdk/crypto/keyring"
15+
)
16+
17+
const defaultCascadeTimeout = 30 * time.Second
18+
19+
// Client bundles the cascade client with its backing keyring and owner address.
20+
// The keyring is the controller chain keyring; the Lumera address is derived from it.
21+
type Client struct {
22+
Cascade *cascade.Client
23+
Keyring keyring.Keyring
24+
OwnerAddress string
25+
}
26+
27+
// NewCascadeClient initializes the SDK cascade client using controller keyring settings.
28+
// It derives a Lumera bech32 address from the same key name for action registration.
29+
func NewCascadeClient(ctx context.Context, cfg *Config) (*Client, error) {
30+
if cfg == nil {
31+
return nil, fmt.Errorf("config is nil")
32+
}
33+
// Create the controller keyring (used for ICA signing and metadata signing).
34+
controllerKR, err := newControllerKeyring(cfg.Controller)
35+
if err != nil {
36+
return nil, err
37+
}
38+
// Resolve controller owner address using the configured controller account HRP.
39+
ownerAddr, err := sdkcrypto.AddressFromKey(controllerKR, cfg.Controller.KeyName, cfg.Controller.AccountHRP)
40+
if err != nil {
41+
return nil, fmt.Errorf("derive controller address: %w", err)
42+
}
43+
// Resolve Lumera address with the Lumera HRP for on-chain action registration.
44+
lumeraAddr, err := sdkcrypto.AddressFromKey(controllerKR, cfg.Lumera.KeyName, "lumera")
45+
if err != nil {
46+
return nil, fmt.Errorf("derive lumera address: %w", err)
47+
}
48+
// Initialize cascade SDK client with Lumera connection settings and log level.
49+
casc, err := cascade.New(ctx, cascade.Config{
50+
ChainID: cfg.Lumera.ChainID,
51+
GRPCAddr: cfg.Lumera.GRPCEndpoint,
52+
Address: lumeraAddr,
53+
KeyName: cfg.Lumera.KeyName,
54+
ICAOwnerKeyName: cfg.Controller.KeyName,
55+
ICAOwnerHRP: cfg.Controller.AccountHRP,
56+
Timeout: defaultCascadeTimeout,
57+
LogLevel: cfg.Lumera.LogLevel,
58+
}, controllerKR)
59+
if err != nil {
60+
return nil, err
61+
}
62+
return &Client{Cascade: casc, Keyring: controllerKR, OwnerAddress: ownerAddr}, nil
63+
}
64+
65+
// newControllerKeyring constructs the Cosmos keyring for the controller chain.
66+
func newControllerKeyring(cfg ControllerConfig) (keyring.Keyring, error) {
67+
passphrase, err := resolvePassphrase(cfg.KeyringPassphrasePlain, cfg.KeyringPassphraseFile)
68+
if err != nil {
69+
return nil, err
70+
}
71+
// For test backend, fall back to controller.home when keyring_dir is unset.
72+
dir := strings.TrimSpace(cfg.KeyringDir)
73+
if dir == "" && strings.EqualFold(cfg.KeyringBackend, "test") {
74+
dir = strings.TrimSpace(cfg.Home)
75+
}
76+
params := sdkcrypto.KeyringParams{
77+
AppName: keyringAppName(cfg),
78+
Backend: cfg.KeyringBackend,
79+
Dir: dir,
80+
Input: passphraseReader(passphrase),
81+
}
82+
kr, err := sdkcrypto.NewKeyring(params)
83+
if err != nil {
84+
return nil, fmt.Errorf("init keyring: %w", err)
85+
}
86+
return kr, nil
87+
}
88+
89+
// resolvePassphrase selects a single passphrase source or returns empty for interactive prompts.
90+
func resolvePassphrase(plain, filePath string) (string, error) {
91+
plain = strings.TrimSpace(plain)
92+
filePath = strings.TrimSpace(filePath)
93+
if plain != "" && filePath != "" {
94+
return "", fmt.Errorf("only one of keyring passphrase plain/file may be set")
95+
}
96+
if plain != "" {
97+
return plain, nil
98+
}
99+
if filePath == "" {
100+
return "", nil
101+
}
102+
data, err := os.ReadFile(filePath)
103+
if err != nil {
104+
return "", fmt.Errorf("read keyring passphrase file: %w", err)
105+
}
106+
pass := strings.TrimSpace(string(data))
107+
if pass == "" {
108+
return "", fmt.Errorf("keyring passphrase file is empty")
109+
}
110+
return pass, nil
111+
}
112+
113+
func passphraseReader(passphrase string) io.Reader {
114+
if passphrase == "" {
115+
return nil
116+
}
117+
// Repeat the passphrase to satisfy multiple keyring prompts.
118+
return &repeatReader{data: []byte(passphrase + "\n")}
119+
}
120+
121+
// keyringAppName selects a stable keyring application name for the controller chain.
122+
func keyringAppName(cfg ControllerConfig) string {
123+
if cfg.Binary != "" {
124+
return filepath.Base(cfg.Binary)
125+
}
126+
if cfg.ChainID != "" {
127+
return cfg.ChainID
128+
}
129+
return "lumera"
130+
}
131+
132+
type repeatReader struct {
133+
data []byte
134+
pos int
135+
}
136+
137+
// Read loops the underlying data to satisfy repeated keyring reads.
138+
func (r *repeatReader) Read(p []byte) (int, error) {
139+
if len(r.data) == 0 {
140+
return 0, io.EOF
141+
}
142+
n := 0
143+
for n < len(p) {
144+
remaining := len(r.data) - r.pos
145+
if remaining == 0 {
146+
r.pos = 0
147+
remaining = len(r.data)
148+
}
149+
chunk := remaining
150+
if chunk > len(p)-n {
151+
chunk = len(p) - n
152+
}
153+
copy(p[n:n+chunk], r.data[r.pos:r.pos+chunk])
154+
n += chunk
155+
r.pos += chunk
156+
}
157+
return n, nil
158+
}

0 commit comments

Comments
 (0)