Skip to content
Merged
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
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,10 @@ _ = packetInfo

Common helpers:

- `DefaultKeyringParams` / `NewKeyring` for consistent keyring setup.
- `LoadKeyringFromMnemonic` / `ImportKeyFromMnemonic` for mnemonic-based flows.
- `DefaultKeyringParams` / `NewKeyring` for consistent keyring setup. A single keyring supports both Cosmos (`secp256k1`) and EVM (`eth_secp256k1`) key types.
- `KeyType` enum (`KeyTypeCosmos`, `KeyTypeEVM`) selects the key algorithm and BIP44 derivation path. Controller and host chains can each use a different `KeyType`.
- `LoadKeyring(keyName, mnemonicFile, keyType)` creates a test keyring from a mnemonic.
- `ImportKey(kr, keyName, mnemonicFile, hrp, keyType)` imports a mnemonic into an existing keyring.
- `AddressFromKey` to derive HRP-specific addresses without mutating global config.
- `NewDefaultTxConfig` and `SignTxWithKeyring` for signing with Cosmos SDK builders.

Expand All @@ -221,6 +223,32 @@ addr, _ := sdkcrypto.AddressFromKey(kr, "alice", constants.LumeraAccountHRP)
_ = addr
```

#### Using different key types per chain

When controller and host chains use different cryptographic key types, import keys
under separate names and pass them independently:

```go
kr, _ := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams())

// Controller chain: standard Cosmos key (secp256k1, coin type 118)
sdkcrypto.ImportKey(kr, "controller-key", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos)

// Host chain: EVM-compatible key (eth_secp256k1, coin type 60)
sdkcrypto.ImportKey(kr, "host-key", "mnemonic.txt", "inj", sdkcrypto.KeyTypeEVM)
```

The ICA controller supports this via the `HostKeyName` config field:

```go
cfg := ica.Config{
Keyring: kr,
KeyName: "controller-key", // signs on the controller chain
HostKeyName: "host-key", // used for host chain operations
// ...
}
```

### Multi-Account Usage

Reuse the same configuration and transports for multiple local accounts via the client factory:
Expand Down
26 changes: 26 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ This is a concise map of the exported Go surface. For full GoDoc see `pkg.go.dev
- Results: `ActionResult` (tx hash, height, action ID), `CascadeResult` (action result + task ID), `DownloadResult` (action ID, task ID, output path).
- Errors: `ErrInvalidConfig`, `ErrNotFound`, `ErrTimeout`, `ErrInvalidSignature`, `ErrTaskFailed`.

## Package `pkg/crypto`

Crypto helpers for keyring management, key import, address derivation, and transaction signing. A single keyring supports both Cosmos (`secp256k1`) and EVM (`eth_secp256k1`) key types.

- `KeyType` enum: `KeyTypeCosmos` (secp256k1, BIP44 coin type 118) and `KeyTypeEVM` (eth_secp256k1, BIP44 coin type 60). Helper methods: `String()`, `HDPath()`, `SigningAlgo()`.
- `KeyringParams` / `DefaultKeyringParams()`: configuration for keyring initialization (app name, backend, directory).
- `NewKeyring(KeyringParams) (keyring.Keyring, error)`: creates a keyring supporting both Cosmos and EVM key algorithms.
- `LoadKeyring(keyName, mnemonicFile string, keyType KeyType) (keyring.Keyring, []byte, string, error)`: creates a test keyring and imports a mnemonic with the given key type; returns the keyring, pubkey bytes, and Lumera address.
- `ImportKey(kr keyring.Keyring, keyName, mnemonicFile, hrp string, keyType KeyType) ([]byte, string, error)`: imports a mnemonic into an existing keyring under the given key name and key type; returns pubkey bytes and address for the specified HRP.
- `AddressFromKey(kr, keyName, hrp) (string, error)`: derives an HRP-specific bech32 address from a keyring key without mutating global config.
- `NewDefaultTxConfig() client.TxConfig`: builds a protobuf tx config with Lumera action and crypto interfaces registered.
- `SignTxWithKeyring(kr, keyName, chainID string, txBuilder, txConfig) ([]byte, error)`: signs a transaction using Cosmos SDK builders.

## Package `ica`

ICA (Interchain Accounts / ICS-27) controller for registering interchain accounts and executing messages across chains.

- `Config`: controller/host chain configuration (`Controller`, `Host` as `base.Config`), `Keyring`, `KeyName`, optional `HostKeyName` (separate key for host chain operations), IBC settings (`ConnectionID`, `CounterpartyConnectionID`, `Ordering`, `RelativeTimeout`), and polling parameters (`PollDelay`, `PollRetries`, `AckRetries`).
- `NewController(ctx, Config) (*Controller, error)`: creates a gRPC-based ICA controller. When `HostKeyName` is set, host chain operations use a different key than the controller chain signer.
- `Controller.EnsureICAAddress(ctx)`: resolves or registers an ICA address and polls until available.
- `Controller.SendRequestAction(ctx, *MsgRequestAction) (*ActionResult, error)`: sends a request action over ICA, waits for the ack, and returns the action ID.
- `Controller.SendApproveAction(ctx, *MsgApproveAction) (string, error)`: sends an approve action over ICA.
- Packet helpers: `PackRequestAny`, `PackApproveAny`, `BuildICAPacketData`, `BuildMsgSendTx`.
- Ack extraction: `ExtractRequestActionIDsFromAck`, `ExtractRequestActionIDsFromTxMsgData`.
- CLI helpers: `ParseTxHashJSON`, `ExtractPacketInfoFromTxJSON`, `DecodePacketAcknowledgementJSON`.

## Logging

Logging uses `go.uber.org/zap`. Use `client.WithLogLevel` to set the default level (error by default), or pass a custom `*zap.Logger` via `client.WithLogger`.
115 changes: 115 additions & 0 deletions docs/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,67 @@ defer alice.Close()
defer bob.Close()
```

## Crypto Helpers (`pkg/crypto`)

The `pkg/crypto` package provides keyring creation, key import, and address derivation. A single keyring supports both Cosmos (`secp256k1`) and EVM (`eth_secp256k1`) key types.

### Key types

`KeyType` selects the cryptographic algorithm and BIP44 derivation path:

| KeyType | Algorithm | BIP44 Coin Type | HD Path |
|---------|-----------|----------------|---------|
| `KeyTypeCosmos` | `secp256k1` | 118 | `m/44'/118'/0'/0/0` |
| `KeyTypeEVM` | `eth_secp256k1` | 60 | `m/44'/60'/0'/0/0` |

### Creating a keyring

`NewKeyring` creates a keyring that accepts both key types. The algorithm is selected when importing or creating keys, not at keyring creation time.

```go
import sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"

kr, err := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams())
```

### Importing keys from a mnemonic

Use `LoadKeyring` to create a test keyring and import a key in one step:

```go
kr, pubBytes, addr, err := sdkcrypto.LoadKeyring("alice", "mnemonic.txt", sdkcrypto.KeyTypeCosmos)
```

Use `ImportKey` to add a key to an existing keyring:

```go
pubBytes, addr, err := sdkcrypto.ImportKey(kr, "bob", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos)
```

### Using different key types per chain

When controller and host chains use different cryptographic key types, import keys under separate names:

```go
kr, _ := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams())

// Controller chain: standard Cosmos key (secp256k1, coin type 118)
sdkcrypto.ImportKey(kr, "controller-key", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos)

// Host chain: EVM-compatible key (eth_secp256k1, coin type 60)
sdkcrypto.ImportKey(kr, "host-key", "mnemonic.txt", "inj", sdkcrypto.KeyTypeEVM)
```

The ICA controller supports this via the `HostKeyName` config field (see Tutorial 6 below).

### Deriving addresses

`AddressFromKey` derives a bech32 address for any HRP without mutating global SDK config:

```go
addr, err := sdkcrypto.AddressFromKey(kr, "alice", "lumera")
```

## Tutorials

### 1) Query actions (read-only)
Expand Down Expand Up @@ -140,6 +201,7 @@ Key points:
- For ICA, set the ICA creator address and app pubkey on the request message.
- The Cascade client uses `ICAOwnerKeyName` + `ICAOwnerHRP` to derive the controller owner address.
`appPubkey` should be the controller key's pubkey bytes from the keyring.
- When controller and host chains use different key types, import keys under separate names into the same keyring and set `HostKeyName` on the ICA `Config` (see the Crypto Helpers section above).

```go
ctx := context.Background()
Expand Down Expand Up @@ -187,6 +249,59 @@ if err != nil { log.Fatal(err) }

Query helpers include `GetSuperNode`, `ListSuperNodes`, and `GetTopSuperNodesForBlock`.

## ICA Controller Overview

The `ica` package provides a production-ready ICA (Interchain Accounts / ICS-27) controller that manages the full lifecycle of cross-chain message execution against Lumera.

### What it does

`ica.Controller` connects to both a controller chain and the Lumera host chain over gRPC. It handles ICA registration, IBC packet construction, transaction broadcasting, acknowledgement polling, and action ID extraction — all behind a small set of methods:

```go
ctrl, _ := ica.NewController(ctx, ica.Config{
Controller: controllerBaseConfig,
Host: hostBaseConfig,
Keyring: kr,
KeyName: "controller-key",
HostKeyName: "host-key", // optional: separate key for host chain
ConnectionID: "connection-0",
})
defer ctrl.Close()

addr, _ := ctrl.EnsureICAAddress(ctx) // register + poll until ready
result, _ := ctrl.SendRequestAction(ctx, msg) // send, wait for ack, return action ID
txHash, _ := ctrl.SendApproveAction(ctx, approveMsg)
```

For lower-level or offline workflows, packet-building helpers are available separately: `PackRequestAny`, `BuildICAPacketData`, `BuildMsgSendTx`.

### Strengths

- **Minimal setup** — only gRPC endpoints, a keyring, and an IBC connection ID are required. No Docker, no relayer binary, no chain binaries.
- **End-to-end in one call** — `SendRequestAction` builds the ICA packet, broadcasts on the controller chain, waits for tx inclusion, resolves the counterparty channel, polls for the host-chain acknowledgement, and extracts the action ID.
- **Mixed key type support** — controller and host chains can use different cryptographic key types (`KeyTypeCosmos` / `KeyTypeEVM`) by setting `HostKeyName` to a separate key in the same keyring.
- **Resilient polling** — configurable retry counts and delays for both ICA registration (`PollRetries` / `PollDelay`) and acknowledgement waiting (`AckRetries`).
- **Tight Lumera integration** — purpose-built for `MsgRequestAction` and `MsgApproveAction`, with typed results (`ActionResult`) and Cascade metadata compatibility.

### Limitations

- **Requires running chains** — the controller connects to live gRPC endpoints. It does not spin up chains or relayers; infrastructure must already be deployed.
- **Lumera-specific high-level methods** — `SendRequestAction` and `SendApproveAction` are tailored to Lumera action messages. Generic ICA message execution requires using the lower-level packet helpers directly.
- **No chain lifecycle management** — unlike e2e testing frameworks (e.g., interchaintest), there is no built-in chain provisioning, genesis configuration, or relayer orchestration.
- **Relayer dependency** — IBC packet relay between controller and host chains depends on an external relayer (e.g., Hermes). The controller does not relay packets itself.

### When to use this vs. interchaintest

| Aspect | `ica.Controller` | interchaintest |
| --- | --- | --- |
| **Purpose** | Production client / scripting | E2E integration testing |
| **Infrastructure** | Connects to running chains | Spins up chains + relayers in Docker |
| **Setup effort** | Config struct + keyring | Docker, chain binaries, genesis config |
| **Iteration speed** | Fast (gRPC calls) | Slower (container lifecycle + block production) |
| **Scope** | Lumera ICA operations | Any IBC flow, any chain |

Use `ica.Controller` when you have running chains and need to execute ICA operations in production or automation scripts. Use interchaintest when you need to validate the full ICA flow in CI from scratch without external infrastructure.

## Examples and testing

- Run tests: `make test`
Expand Down
6 changes: 5 additions & 1 deletion examples/ica-request-tx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ func main() {
appName = "injectived"
}

kr, err := sdkcrypto.NewMultiChainKeyring(appName, *keyringBackend, *keyringDir)
kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{
AppName: appName,
Backend: *keyringBackend,
Dir: *keyringDir,
})
if err != nil {
fmt.Printf("open keyring: %v\n", err)
os.Exit(1)
Expand Down
6 changes: 5 additions & 1 deletion examples/ica-request-verify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ func main() {
}
fmt.Printf("Using keyring app name: %s\n", appName)

kr, err := sdkcrypto.NewMultiChainKeyring(appName, *keyringBackend, *keyringDir)
kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{
AppName: appName,
Backend: *keyringBackend,
Dir: *keyringDir,
})
if err != nil {
fmt.Printf("open keyring: %v\n", err)
os.Exit(1)
Expand Down
49 changes: 6 additions & 43 deletions ica/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,13 @@ import (
sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
sdktypes "github.com/LumeraProtocol/sdk-go/types"
"github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
controllertypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/controller/types"
icatypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/types"
clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
connectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types"
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
)

const (
defaultRelativeTimeout = 10 * time.Minute
defaultPollDelay = 2 * time.Second
defaultPollRetries = 120
defaultAckRetries = 120
)

// Config configures the ICA controller for a controller/host chain pair.
type Config struct {
Controller base.Config
Host base.Config
Keyring keyring.Keyring
KeyName string

ConnectionID string
CounterpartyConnectionID string
Ordering channeltypes.Order
RelativeTimeout time.Duration
PollDelay time.Duration
PollRetries int
AckRetries int
}

// Controller manages ICA registration and message execution via gRPC.
type Controller struct {
cfg Config
controllerBC *base.Client
hostBC *base.Client
ownerAddr string
appPubkey []byte
}

// NewController creates a new ICA controller using gRPC-based queries and txs.
func NewController(ctx context.Context, cfg Config) (*Controller, error) {
if cfg.Keyring == nil {
Expand Down Expand Up @@ -89,11 +56,16 @@ func NewController(ctx context.Context, cfg Config) (*Controller, error) {
cfg.Ordering = channeltypes.ORDERED
}

hostKeyName := cfg.HostKeyName
if hostKeyName == "" {
hostKeyName = cfg.KeyName
}

controllerBC, err := base.New(ctx, cfg.Controller, cfg.Keyring, cfg.KeyName)
if err != nil {
return nil, fmt.Errorf("create controller blockchain client: %w", err)
}
hostBC, err := base.New(ctx, cfg.Host, cfg.Keyring, cfg.KeyName)
hostBC, err := base.New(ctx, cfg.Host, cfg.Keyring, hostKeyName)
if err != nil {
_ = controllerBC.Close()
return nil, fmt.Errorf("create host blockchain client: %w", err)
Expand Down Expand Up @@ -393,12 +365,3 @@ func (c *Controller) queryAcknowledgement(ctx context.Context, port, channel str
}
return ack, nil
}

// ErrAckNotFound is returned when no acknowledgement event is present for the packet.
var ErrAckNotFound = errors.New("acknowledgement event not found")

// ErrPacketInfoNotFound is returned when no send_packet event is found in a tx.
var ErrPacketInfoNotFound = errors.New("send_packet event not found")

// ErrICAAddressNotFound is returned when no ICA address is registered yet.
var ErrICAAddressNotFound = errors.New("ica address not found")
80 changes: 80 additions & 0 deletions ica/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ica

import (
"errors"
"time"

"github.com/LumeraProtocol/sdk-go/blockchain/base"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
)

const (
// defaultRelativeTimeout is the default ICA packet timeout (10 minutes).
defaultRelativeTimeout = 10 * time.Minute
// defaultPollDelay is the default interval between polling attempts (2 seconds).
defaultPollDelay = 2 * time.Second
// defaultPollRetries is the default max attempts when polling for ICA registration.
defaultPollRetries = 120
// defaultAckRetries is the default max attempts when polling for an IBC acknowledgement.
defaultAckRetries = 120
)

// Config configures the ICA controller for a controller/host chain pair.
// Controller and host chains can use different key types (Cosmos or EVM)
// by importing keys under separate names into the same keyring and setting
// KeyName / HostKeyName independently.
type Config struct {
// Controller holds the gRPC/chain configuration for the controller chain
// (the chain that signs and broadcasts MsgSendTx).
Controller base.Config
// Host holds the gRPC/chain configuration for the host chain
// (the chain where the ICA executes messages).
Host base.Config
// Keyring provides access to signing keys for both chains.
Keyring keyring.Keyring
// KeyName is the key name used for signing on the controller chain.
KeyName string
// HostKeyName is an optional separate key name for host chain operations.
// When empty, KeyName is used for both chains.
HostKeyName string

// ConnectionID is the IBC connection identifier on the controller chain.
ConnectionID string
// CounterpartyConnectionID is the IBC connection identifier on the host chain.
// When set, it is included in the ICA registration metadata.
CounterpartyConnectionID string
// Ordering specifies the IBC channel ordering (ORDERED or UNORDERED).
// Defaults to ORDERED.
Ordering channeltypes.Order
// RelativeTimeout is the timeout duration for ICA packets, relative to
// the current block time. Defaults to 10 minutes.
RelativeTimeout time.Duration
// PollDelay is the interval between polling attempts when waiting for
// ICA registration or acknowledgements. Defaults to 2 seconds.
PollDelay time.Duration
// PollRetries is the maximum number of polling attempts when waiting
// for ICA address registration. Defaults to 120.
PollRetries int
// AckRetries is the maximum number of polling attempts when waiting
// for an IBC packet acknowledgement. Defaults to 120.
AckRetries int
}

// Controller manages ICA registration and message execution via gRPC.
type Controller struct {
cfg Config // full configuration snapshot
controllerBC *base.Client // gRPC client for the controller chain
hostBC *base.Client // gRPC client for the host chain
ownerAddr string // bech32 owner address on the controller chain
appPubkey []byte // compressed public key bytes for ICA creator validation
}

// ErrAckNotFound is returned when no acknowledgement event is present for the packet.
var ErrAckNotFound = errors.New("acknowledgement event not found")

// ErrPacketInfoNotFound is returned when no send_packet event is found in a tx.
var ErrPacketInfoNotFound = errors.New("send_packet event not found")

// ErrICAAddressNotFound is returned when no ICA address is registered yet.
var ErrICAAddressNotFound = errors.New("ica address not found")
Loading