diff --git a/README.md b/README.md index 9fa06de..244605b 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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: diff --git a/docs/API.md b/docs/API.md index ae03cfd..7c7bd6f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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`. diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index eb515f0..4c256f5 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -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) @@ -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() @@ -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` diff --git a/examples/ica-request-tx/main.go b/examples/ica-request-tx/main.go index 6057478..26b6c3b 100644 --- a/examples/ica-request-tx/main.go +++ b/examples/ica-request-tx/main.go @@ -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) diff --git a/examples/ica-request-verify/main.go b/examples/ica-request-verify/main.go index 439e882..9896269 100644 --- a/examples/ica-request-verify/main.go +++ b/examples/ica-request-verify/main.go @@ -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) diff --git a/ica/controller.go b/ica/controller.go index 6d7e172..3befae8 100644 --- a/ica/controller.go +++ b/ica/controller.go @@ -13,7 +13,6 @@ 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" @@ -21,38 +20,6 @@ import ( 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 { @@ -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) @@ -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") diff --git a/ica/types.go b/ica/types.go new file mode 100644 index 0000000..fa39892 --- /dev/null +++ b/ica/types.go @@ -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") diff --git a/pkg/crypto/crypto_test.go b/pkg/crypto/crypto_test.go index 63d1e7f..276664a 100644 --- a/pkg/crypto/crypto_test.go +++ b/pkg/crypto/crypto_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/LumeraProtocol/sdk-go/constants" + sdkethsecp256k1 "github.com/LumeraProtocol/sdk-go/pkg/crypto/ethsecp256k1" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" @@ -24,6 +25,29 @@ var testMnemonic = func() string { return mnemonic }() +// --------------------------------------------------------------------------- +// KeyType tests +// --------------------------------------------------------------------------- + +func TestKeyType_String(t *testing.T) { + require.Equal(t, "cosmos", KeyTypeCosmos.String()) + require.Equal(t, "evm", KeyTypeEVM.String()) +} + +func TestKeyType_HDPath(t *testing.T) { + require.Equal(t, sdk.FullFundraiserPath, KeyTypeCosmos.HDPath()) + require.Equal(t, EVMBIP44HDPath, KeyTypeEVM.HDPath()) +} + +func TestKeyType_SigningAlgo(t *testing.T) { + require.Equal(t, hd.Secp256k1.Name(), KeyTypeCosmos.SigningAlgo().Name()) + require.Equal(t, hd.PubKeyType(sdkethsecp256k1.KeyType), KeyTypeEVM.SigningAlgo().Name()) +} + +// --------------------------------------------------------------------------- +// Keyring creation +// --------------------------------------------------------------------------- + func TestDefaultKeyringParams(t *testing.T) { params := DefaultKeyringParams() require.Equal(t, "lumera", params.AppName) @@ -40,6 +64,34 @@ func TestNewKeyring(t *testing.T) { require.Error(t, err) } +func TestNewKeyring_SupportsBothAlgos(t *testing.T) { + kr := newTestKeyring(t) + + // Cosmos key + _, err := kr.NewAccount("cosmos-key", testMnemonic, "", KeyTypeCosmos.HDPath(), KeyTypeCosmos.SigningAlgo()) + require.NoError(t, err) + + rec, err := kr.Key("cosmos-key") + require.NoError(t, err) + pk, err := rec.GetPubKey() + require.NoError(t, err) + require.Equal(t, string(hd.Secp256k1Type), pk.Type()) + + // EVM key (different name, same mnemonic) + _, err = kr.NewAccount("evm-key", testMnemonic, "", KeyTypeEVM.HDPath(), KeyTypeEVM.SigningAlgo()) + require.NoError(t, err) + + rec2, err := kr.Key("evm-key") + require.NoError(t, err) + pk2, err := rec2.GetPubKey() + require.NoError(t, err) + require.Equal(t, sdkethsecp256k1.KeyType, pk2.Type()) +} + +// --------------------------------------------------------------------------- +// Address derivation +// --------------------------------------------------------------------------- + func TestAddressFromKey(t *testing.T) { kr := newTestKeyring(t) _, err := kr.NewAccount("alice", testMnemonic, "", sdk.FullFundraiserPath, hd.Secp256k1) @@ -57,33 +109,379 @@ func TestAddressFromKey(t *testing.T) { require.Error(t, err) } -func TestLoadKeyringFromMnemonic(t *testing.T) { +// --------------------------------------------------------------------------- +// Unified LoadKeyring / ImportKey +// --------------------------------------------------------------------------- + +func TestLoadKeyring_Cosmos(t *testing.T) { mnemonicFile := writeMnemonicFile(t) - kr, pub, addr, err := LoadKeyringFromMnemonic("alice", mnemonicFile) + kr, pub, addr, err := LoadKeyring("alice", mnemonicFile, KeyTypeCosmos) require.NoError(t, err) require.NotNil(t, kr) require.NotEmpty(t, pub) require.True(t, strings.HasPrefix(addr, constants.LumeraAccountHRP)) - _, err = kr.Key("alice") + rec, err := kr.Key("alice") + require.NoError(t, err) + pk, err := rec.GetPubKey() require.NoError(t, err) + require.Equal(t, string(hd.Secp256k1Type), pk.Type()) } -func TestImportKeyFromMnemonic(t *testing.T) { +func TestLoadKeyring_EVM(t *testing.T) { + mnemonicFile := writeMnemonicFile(t) + kr, pub, addr, err := LoadKeyring("alice", mnemonicFile, KeyTypeEVM) + require.NoError(t, err) + require.NotNil(t, kr) + require.NotEmpty(t, pub) + require.True(t, strings.HasPrefix(addr, constants.LumeraAccountHRP)) + + rec, err := kr.Key("alice") + require.NoError(t, err) + pk, err := rec.GetPubKey() + require.NoError(t, err) + require.Equal(t, sdkethsecp256k1.KeyType, pk.Type()) +} + +func TestLoadKeyring_DifferentKeyTypesDerivesDifferentAddress(t *testing.T) { + mnemonicFile := writeMnemonicFile(t) + + _, _, cosmosAddr, err := LoadKeyring("alice", mnemonicFile, KeyTypeCosmos) + require.NoError(t, err) + + _, _, evmAddr, err := LoadKeyring("alice", mnemonicFile, KeyTypeEVM) + require.NoError(t, err) + + require.NotEqual(t, cosmosAddr, evmAddr) +} + +func TestImportKey_Cosmos(t *testing.T) { kr := newTestKeyring(t) mnemonicFile := writeMnemonicFile(t) - pub, addr, err := ImportKeyFromMnemonic(kr, "alice", mnemonicFile, "cosmos") + pub, addr, err := ImportKey(kr, "alice", mnemonicFile, "cosmos", KeyTypeCosmos) require.NoError(t, err) require.NotEmpty(t, pub) require.True(t, strings.HasPrefix(addr, "cosmos")) - pub2, addr2, err := ImportKeyFromMnemonic(kr, "alice", mnemonicFile, "cosmos") + // Idempotent: second import returns the same key. + pub2, addr2, err := ImportKey(kr, "alice", mnemonicFile, "cosmos", KeyTypeCosmos) require.NoError(t, err) - require.Equal(t, addr, addr2) require.Equal(t, pub, pub2) + require.Equal(t, addr, addr2) +} + +func TestImportKey_EVM(t *testing.T) { + kr := newTestKeyring(t) + mnemonicFile := writeMnemonicFile(t) + + pub, addr, err := ImportKey(kr, "alice", mnemonicFile, "cosmos", KeyTypeEVM) + require.NoError(t, err) + require.NotEmpty(t, pub) + require.True(t, strings.HasPrefix(addr, "cosmos")) + + rec, err := kr.Key("alice") + require.NoError(t, err) + pk, err := rec.GetPubKey() + require.NoError(t, err) + require.Equal(t, sdkethsecp256k1.KeyType, pk.Type()) +} + +// TestImportKey_MultiChain imports both Cosmos and EVM keys into a single +// keyring under different names (simulating independent controller/host +// chain configuration). +func TestImportKey_MultiChain(t *testing.T) { + kr := newTestKeyring(t) + mnemonicFile := writeMnemonicFile(t) + + // Controller chain: Cosmos key + cosmosPub, cosmosAddr, err := ImportKey(kr, "controller", mnemonicFile, constants.LumeraAccountHRP, KeyTypeCosmos) + require.NoError(t, err) + require.NotEmpty(t, cosmosPub) + + // Host chain: EVM key + evmPub, evmAddr, err := ImportKey(kr, "host", mnemonicFile, constants.LumeraAccountHRP, KeyTypeEVM) + require.NoError(t, err) + require.NotEmpty(t, evmPub) + + // Keys must differ (different algo + derivation path). + require.NotEqual(t, cosmosPub, evmPub) + require.NotEqual(t, cosmosAddr, evmAddr) + + // Verify key types stored in the keyring. + cosmosRec, err := kr.Key("controller") + require.NoError(t, err) + cosmosPK, err := cosmosRec.GetPubKey() + require.NoError(t, err) + require.Equal(t, string(hd.Secp256k1Type), cosmosPK.Type()) + + evmRec, err := kr.Key("host") + require.NoError(t, err) + evmPK, err := evmRec.GetPubKey() + require.NoError(t, err) + require.Equal(t, sdkethsecp256k1.KeyType, evmPK.Type()) +} + +// --------------------------------------------------------------------------- +// Behavior matrix +// --------------------------------------------------------------------------- + +func TestKeyBehaviorMatrix(t *testing.T) { + type keyMode struct { + name string + keyType KeyType + expectedAlg string + } + + mnemonicFile := writeMnemonicFile(t) + modes := []keyMode{ + { + name: "cosmos", + keyType: KeyTypeCosmos, + expectedAlg: string(hd.Secp256k1Type), + }, + { + name: "evm", + keyType: KeyTypeEVM, + expectedAlg: sdkethsecp256k1.KeyType, + }, + } + + loadAddrs := make(map[string]string, len(modes)) + loadPubs := make(map[string][]byte, len(modes)) + + for _, mode := range modes { + t.Run(mode.name, func(t *testing.T) { + // LoadKeyring should be deterministic and set the expected key algorithm. + kr1, pub1, addr1, err := LoadKeyring("alice", mnemonicFile, mode.keyType) + require.NoError(t, err) + _, pub2, addr2, err := LoadKeyring("alice", mnemonicFile, mode.keyType) + require.NoError(t, err) + require.Equal(t, pub1, pub2) + require.Equal(t, addr1, addr2) + require.True(t, strings.HasPrefix(addr1, constants.LumeraAccountHRP)) + + rec1, err := kr1.Key("alice") + require.NoError(t, err) + pk1, err := rec1.GetPubKey() + require.NoError(t, err) + require.Equal(t, mode.expectedAlg, pk1.Type()) + + loadAddrs[mode.name] = addr1 + loadPubs[mode.name] = pub1 + + // ImportKey should be deterministic in a single keyring. + krImport := newTestKeyring(t) + pubImp1, addrImp1, err := ImportKey(krImport, "alice", mnemonicFile, "cosmos", mode.keyType) + require.NoError(t, err) + pubImp2, addrImp2, err := ImportKey(krImport, "alice", mnemonicFile, "cosmos", mode.keyType) + require.NoError(t, err) + require.Equal(t, pubImp1, pubImp2) + require.Equal(t, addrImp1, addrImp2) + require.True(t, strings.HasPrefix(addrImp1, "cosmos")) + + recImp, err := krImport.Key("alice") + require.NoError(t, err) + pkImp, err := recImp.GetPubKey() + require.NoError(t, err) + require.Equal(t, mode.expectedAlg, pkImp.Type()) + + // ImportKey with Lumera HRP should match LoadKeyring outputs. + krLumera := newTestKeyring(t) + pubLumera, addrLumera, err := ImportKey(krLumera, "alice", mnemonicFile, constants.LumeraAccountHRP, mode.keyType) + require.NoError(t, err) + require.Equal(t, pub1, pubLumera) + require.Equal(t, addr1, addrLumera) + }) + } + + require.NotEqual(t, loadAddrs["cosmos"], loadAddrs["evm"]) + require.NotEqual(t, loadPubs["cosmos"], loadPubs["evm"]) +} + +// --------------------------------------------------------------------------- +// Error paths +// --------------------------------------------------------------------------- + +func TestLoadKeyring_Errors(t *testing.T) { + mnemonicFile := writeMnemonicFile(t) + + // Empty key name. + _, _, _, err := LoadKeyring("", mnemonicFile, KeyTypeCosmos) + require.Error(t, err) + require.Contains(t, err.Error(), "key name is required") + + // Non-existent mnemonic file. + _, _, _, err = LoadKeyring("alice", "/no/such/file.txt", KeyTypeCosmos) + require.Error(t, err) + + // Empty mnemonic file. + emptyFile := filepath.Join(t.TempDir(), "empty.txt") + require.NoError(t, os.WriteFile(emptyFile, []byte(" \n"), 0o600)) + _, _, _, err = LoadKeyring("alice", emptyFile, KeyTypeCosmos) + require.Error(t, err) + require.Contains(t, err.Error(), "mnemonic file is empty") +} + +func TestImportKey_Errors(t *testing.T) { + mnemonicFile := writeMnemonicFile(t) + kr := newTestKeyring(t) + + // Nil keyring. + _, _, err := ImportKey(nil, "alice", mnemonicFile, "cosmos", KeyTypeCosmos) + require.Error(t, err) + require.Contains(t, err.Error(), "keyring is nil") + + // Empty key name. + _, _, err = ImportKey(kr, "", mnemonicFile, "cosmos", KeyTypeCosmos) + require.Error(t, err) + require.Contains(t, err.Error(), "key name is required") + + // Non-existent mnemonic file. + _, _, err = ImportKey(kr, "alice", "/no/such/file.txt", "cosmos", KeyTypeCosmos) + require.Error(t, err) +} + +func TestImportKey_KeyTypeMismatch(t *testing.T) { + kr := newTestKeyring(t) + mnemonicFile := writeMnemonicFile(t) + + // Import as Cosmos first. + _, _, err := ImportKey(kr, "alice", mnemonicFile, "cosmos", KeyTypeCosmos) + require.NoError(t, err) + + // Re-import same name with EVM should fail. + _, _, err = ImportKey(kr, "alice", mnemonicFile, "cosmos", KeyTypeEVM) + require.Error(t, err) + require.Contains(t, err.Error(), "already exists with algorithm") } +func TestAddressFromKey_Errors(t *testing.T) { + kr := newTestKeyring(t) + + _, err := AddressFromKey(nil, "alice", "cosmos") + require.Error(t, err) + + _, err = AddressFromKey(kr, "", "cosmos") + require.Error(t, err) + + _, err = AddressFromKey(kr, "nonexistent", "cosmos") + require.Error(t, err) +} + +// --------------------------------------------------------------------------- +// GetKey +// --------------------------------------------------------------------------- + +func TestGetKey(t *testing.T) { + kr := newTestKeyring(t) + _, err := kr.NewAccount("bob", testMnemonic, "", KeyTypeCosmos.HDPath(), KeyTypeCosmos.SigningAlgo()) + require.NoError(t, err) + + rec, err := GetKey(kr, "bob") + require.NoError(t, err) + require.Equal(t, "bob", rec.Name) + + _, err = GetKey(kr, "missing") + require.Error(t, err) +} + +// --------------------------------------------------------------------------- +// Cross-HRP address derivation +// --------------------------------------------------------------------------- + +func TestAddressFromKey_MultipleHRPs(t *testing.T) { + kr := newTestKeyring(t) + _, err := kr.NewAccount("alice", testMnemonic, "", KeyTypeCosmos.HDPath(), KeyTypeCosmos.SigningAlgo()) + require.NoError(t, err) + + lumeraAddr, err := AddressFromKey(kr, "alice", constants.LumeraAccountHRP) + require.NoError(t, err) + require.True(t, strings.HasPrefix(lumeraAddr, constants.LumeraAccountHRP+"1")) + + cosmosAddr, err := AddressFromKey(kr, "alice", "cosmos") + require.NoError(t, err) + require.True(t, strings.HasPrefix(cosmosAddr, "cosmos1")) + + osmosisAddr, err := AddressFromKey(kr, "alice", "osmo") + require.NoError(t, err) + require.True(t, strings.HasPrefix(osmosisAddr, "osmo1")) + + // Same key, different HRPs produce different bech32 strings but all are valid. + require.NotEqual(t, lumeraAddr, cosmosAddr) + require.NotEqual(t, cosmosAddr, osmosisAddr) +} + +// --------------------------------------------------------------------------- +// NewKeyring with custom params +// --------------------------------------------------------------------------- + +func TestNewKeyring_CustomParams(t *testing.T) { + kr, err := NewKeyring(KeyringParams{ + AppName: "custom-app", + Backend: "test", + Dir: t.TempDir(), + }) + require.NoError(t, err) + require.NotNil(t, kr) + + // Keyring should support both key types. + _, err = kr.NewAccount("c", testMnemonic, "", KeyTypeCosmos.HDPath(), KeyTypeCosmos.SigningAlgo()) + require.NoError(t, err) +} + +func TestNewKeyring_DefaultsApplied(t *testing.T) { + // All-zero params: defaults should be applied without panic. + // We use "test" backend to avoid OS keyring interaction. + kr, err := NewKeyring(KeyringParams{Backend: "test", Dir: t.TempDir()}) + require.NoError(t, err) + require.NotNil(t, kr) +} + +// --------------------------------------------------------------------------- +// Unified keyring sign-and-verify round trip +// --------------------------------------------------------------------------- + +func TestUnifiedKeyring_SignVerifyRoundTrip(t *testing.T) { + kr := newTestKeyring(t) + msg := []byte("hello unified keyring") + + tests := []struct { + name string + keyType KeyType + }{ + {"cosmos", KeyTypeCosmos}, + {"evm", KeyTypeEVM}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keyName := "sign-" + tt.name + _, err := kr.NewAccount(keyName, testMnemonic, "", tt.keyType.HDPath(), tt.keyType.SigningAlgo()) + require.NoError(t, err) + + rec, err := kr.Key(keyName) + require.NoError(t, err) + pub, err := rec.GetPubKey() + require.NoError(t, err) + + addr, err := rec.GetAddress() + require.NoError(t, err) + + sig, _, err := kr.SignByAddress(addr, msg, 0) + require.NoError(t, err) + require.NotEmpty(t, sig) + + valid := pub.VerifySignature(msg, sig) + require.True(t, valid, "signature verification should pass for %s key", tt.name) + }) + } +} + +// --------------------------------------------------------------------------- +// TxConfig and signing +// --------------------------------------------------------------------------- + func TestNewDefaultTxConfig(t *testing.T) { txCfg := NewDefaultTxConfig() require.NotNil(t, txCfg) @@ -107,6 +505,25 @@ func TestSignTxWithKeyring(t *testing.T) { require.Len(t, sigs, 1) } +func TestSignTxWithKeyring_EVM(t *testing.T) { + kr := newTestKeyring(t) + _, err := kr.NewAccount("alice", testMnemonic, "", KeyTypeEVM.HDPath(), KeyTypeEVM.SigningAlgo()) + require.NoError(t, err) + + txCfg := NewDefaultTxConfig() + builder := txCfg.NewTxBuilder() + err = SignTxWithKeyring(context.Background(), txCfg, kr, "alice", builder, "chain-id", 1, 0, false) + require.NoError(t, err) + + sigs, err := builder.GetTx().GetSignaturesV2() + require.NoError(t, err) + require.Len(t, sigs, 1) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + func newTestKeyring(t *testing.T) keyring.Keyring { t.Helper() kr, err := NewKeyring(KeyringParams{ diff --git a/pkg/crypto/keyring.go b/pkg/crypto/keyring.go index d209486..f9df828 100644 --- a/pkg/crypto/keyring.go +++ b/pkg/crypto/keyring.go @@ -9,12 +9,14 @@ import ( "strings" "github.com/LumeraProtocol/sdk-go/constants" + sdkethsecp256k1 "github.com/LumeraProtocol/sdk-go/pkg/crypto/ethsecp256k1" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/std" sdk "github.com/cosmos/cosmos-sdk/types" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" @@ -22,6 +24,57 @@ import ( actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" ) +const ( + // EVMBIP44HDPath is the default Ethereum derivation path (coin type 60). + EVMBIP44HDPath = "m/44'/60'/0'/0/0" +) + +// KeyType represents the cryptographic key algorithm and HD derivation path +// to use for a chain. Controller and host chains can each be configured with +// an independent KeyType. +type KeyType int + +const ( + // KeyTypeCosmos uses secp256k1 with BIP44 coin type 118 (standard Cosmos). + KeyTypeCosmos KeyType = iota + // KeyTypeEVM uses eth_secp256k1 with BIP44 coin type 60 (Ethereum-compatible). + KeyTypeEVM +) + +// String returns the string representation of the key type. +func (kt KeyType) String() string { + switch kt { + case KeyTypeEVM: + return "evm" + default: + return "cosmos" + } +} + +// HDPath returns the BIP44 HD derivation path for this key type. +func (kt KeyType) HDPath() string { + switch kt { + case KeyTypeEVM: + return EVMBIP44HDPath + default: + return sdk.FullFundraiserPath + } +} + +// SigningAlgo returns the keyring signing algorithm for this key type. +func (kt KeyType) SigningAlgo() keyring.SignatureAlgo { + switch kt { + case KeyTypeEVM: + return ethSecp256k1Alg + default: + return hd.Secp256k1 + } +} + +var ( + ethSecp256k1Alg = ethSecp256k1Algo{} +) + // KeyringParams holds configuration for initializing a Cosmos keyring. type KeyringParams struct { // AppName names the keyring namespace. Default: "lumera" @@ -48,7 +101,9 @@ func DefaultKeyringParams() KeyringParams { } } -// NewKeyring creates a new Cosmos keyring with the provided parameters. +// NewKeyring creates a new keyring that supports both Cosmos (secp256k1) +// and EVM (eth_secp256k1) key types. The key type used is determined when +// importing or creating keys, not at keyring creation time. func NewKeyring(p KeyringParams) (keyring.Keyring, error) { app := p.AppName if app == "" { @@ -68,12 +123,12 @@ func NewKeyring(p KeyringParams) (keyring.Keyring, error) { in = bufio.NewReader(os.Stdin) } - // Create a proto codec for keyring operations - reg := codectypes.NewInterfaceRegistry() - std.RegisterInterfaces(reg) - cdc := codec.NewProtoCodec(reg) + registry := codectypes.NewInterfaceRegistry() + std.RegisterInterfaces(registry) + sdkethsecp256k1.RegisterInterfaces(registry) + cdc := codec.NewProtoCodec(registry) - return keyring.New(app, backend, dir, in, cdc) + return keyring.New(app, backend, dir, in, cdc, ethSecp256k1Option()) } // GetKey returns metadata for the named key in the provided keyring. @@ -81,9 +136,14 @@ func GetKey(kr keyring.Keyring, keyName string) (*keyring.Record, error) { return kr.Key(keyName) } -// LoadKeyringFromMnemonic creates a test keyring, imports the mnemonic, and -// returns the keyring, pubkey bytes, and Lumera address. -func LoadKeyringFromMnemonic(keyName, mnemonicFile string) (keyring.Keyring, []byte, string, error) { +// LoadKeyring creates a test keyring in a temporary directory under +// os.TempDir(), imports the mnemonic using the specified key type, and returns +// the keyring, pubkey bytes, and Lumera address. +// +// The temporary directory is cleaned up by the OS on reboot. For production +// use, prefer NewKeyring with an explicit directory and import keys via +// kr.NewAccount directly. +func LoadKeyring(keyName, mnemonicFile string, keyType KeyType) (keyring.Keyring, []byte, string, error) { if keyName == "" { return nil, nil, "", fmt.Errorf("key name is required") } @@ -96,15 +156,23 @@ func LoadKeyringFromMnemonic(keyName, mnemonicFile string) (keyring.Keyring, []b if err != nil { return nil, nil, "", fmt.Errorf("create keyring dir: %w", err) } + ok := false + defer func() { + if !ok { + _ = os.RemoveAll(krDir) + } + }() - registry := codectypes.NewInterfaceRegistry() - cryptocodec.RegisterInterfaces(registry) - krCodec := codec.NewProtoCodec(registry) - kr, err := keyring.New("lumera", "test", krDir, strings.NewReader(""), krCodec) + kr, err := NewKeyring(KeyringParams{ + AppName: "lumera", + Backend: "test", + Dir: krDir, + Input: strings.NewReader(""), + }) if err != nil { return nil, nil, "", fmt.Errorf("create keyring: %w", err) } - if _, err := kr.NewAccount(keyName, mnemonic, "", sdk.FullFundraiserPath, hd.Secp256k1); err != nil { + if _, err := kr.NewAccount(keyName, mnemonic, "", keyType.HDPath(), keyType.SigningAlgo()); err != nil { return nil, nil, "", fmt.Errorf("import key: %w", err) } @@ -125,12 +193,16 @@ func LoadKeyringFromMnemonic(keyName, mnemonicFile string) (keyring.Keyring, []b return nil, nil, "", fmt.Errorf("pubkey is nil") } + ok = true return kr, pub.Bytes(), addr, nil } -// ImportKeyFromMnemonic imports the mnemonic into an existing keyring (if needed), -// returning the pubkey bytes and address for the provided HRP. -func ImportKeyFromMnemonic(kr keyring.Keyring, keyName, mnemonicFile, hrp string) ([]byte, string, error) { +// ImportKey imports a mnemonic into an existing keyring using the specified +// key type, returning the pubkey bytes and address for the provided HRP. +// +// If a key with the same name already exists, ImportKey verifies that its +// algorithm matches the requested keyType and returns an error on mismatch. +func ImportKey(kr keyring.Keyring, keyName, mnemonicFile, hrp string, keyType KeyType) ([]byte, string, error) { if kr == nil { return nil, "", fmt.Errorf("keyring is nil") } @@ -142,10 +214,23 @@ func ImportKeyFromMnemonic(kr keyring.Keyring, keyName, mnemonicFile, hrp string return nil, "", err } - if _, err := kr.Key(keyName); err != nil { - if _, err := kr.NewAccount(keyName, mnemonic, "", sdk.FullFundraiserPath, hd.Secp256k1); err != nil { + existing, err := kr.Key(keyName) + if err != nil { + // Key does not exist — import it. + if _, err := kr.NewAccount(keyName, mnemonic, "", keyType.HDPath(), keyType.SigningAlgo()); err != nil { return nil, "", fmt.Errorf("import key: %w", err) } + } else { + // Key exists — verify the algorithm matches the requested key type. + pub, err := existing.GetPubKey() + if err != nil { + return nil, "", fmt.Errorf("get existing pubkey: %w", err) + } + wantAlgo := string(keyType.SigningAlgo().Name()) + if pub.Type() != wantAlgo { + return nil, "", fmt.Errorf("key %q already exists with algorithm %s, but %s (%s) was requested", + keyName, pub.Type(), keyType.String(), wantAlgo) + } } addr, err := AddressFromKey(kr, keyName, hrp) @@ -172,6 +257,7 @@ func NewDefaultTxConfig() client.TxConfig { reg := codectypes.NewInterfaceRegistry() // Register crypto and module interfaces cryptocodec.RegisterInterfaces(reg) + sdkethsecp256k1.RegisterInterfaces(reg) actiontypes.RegisterInterfaces(reg) proto := codec.NewProtoCodec(reg) @@ -189,3 +275,29 @@ func readMnemonicFile(mnemonicFile string) (string, error) { } return mnemonic, nil } + +func ethSecp256k1Option() keyring.Option { + return func(options *keyring.Options) { + options.SupportedAlgos = keyring.SigningAlgoList{ethSecp256k1Alg, hd.Secp256k1} + options.SupportedAlgosLedger = keyring.SigningAlgoList{ethSecp256k1Alg, hd.Secp256k1} + } +} + +type ethSecp256k1Algo struct{} + +func (s ethSecp256k1Algo) Name() hd.PubKeyType { + return hd.PubKeyType(sdkethsecp256k1.KeyType) +} + +func (s ethSecp256k1Algo) Derive() hd.DeriveFn { + // Reuse Cosmos derivation function with Ethereum BIP44 path. + return hd.Secp256k1.Derive() +} + +func (s ethSecp256k1Algo) Generate() hd.GenerateFn { + return func(bz []byte) cryptotypes.PrivKey { + bzArr := make([]byte, sdkethsecp256k1.PrivKeySize) + copy(bzArr, bz) + return &sdkethsecp256k1.PrivKey{Key: bzArr} + } +} diff --git a/pkg/crypto/multichain_keyring.go b/pkg/crypto/multichain_keyring.go deleted file mode 100644 index 827b2e8..0000000 --- a/pkg/crypto/multichain_keyring.go +++ /dev/null @@ -1,53 +0,0 @@ -package crypto - -import ( - "bufio" - "os" - "path/filepath" - "strings" - - "github.com/cosmos/cosmos-sdk/codec" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "github.com/cosmos/cosmos-sdk/crypto/keyring" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - cosmoscrypto "github.com/cosmos/cosmos-sdk/crypto/types" - - injethsecp256k1 "github.com/LumeraProtocol/sdk-go/pkg/crypto/ethsecp256k1" -) - -func NewMultiChainKeyring(appName, backend, dir string) (keyring.Keyring, error) { - // Expand ~ in path - if strings.HasPrefix(dir, "~/") { - home, _ := os.UserHomeDir() - dir = filepath.Join(home, dir[2:]) - } - - registry := codectypes.NewInterfaceRegistry() - - // Standard Cosmos - registry.RegisterInterface("cosmos.crypto.PubKey", (*cosmoscrypto.PubKey)(nil)) - registry.RegisterInterface("cosmos.crypto.PrivKey", (*cosmoscrypto.PrivKey)(nil)) - registry.RegisterImplementations((*cosmoscrypto.PubKey)(nil), - &secp256k1.PubKey{}, - ) - registry.RegisterImplementations((*cosmoscrypto.PrivKey)(nil), - &secp256k1.PrivKey{}, - ) - - // Injective - registry.RegisterImplementations((*cosmoscrypto.PubKey)(nil), - &injethsecp256k1.PubKey{}, - ) - registry.RegisterImplementations((*cosmoscrypto.PrivKey)(nil), - &injethsecp256k1.PrivKey{}, - ) - - cdc := codec.NewProtoCodec(registry) - // For file backend, provide stdin for password input - var userInput *bufio.Reader - if backend == keyring.BackendFile { - userInput = bufio.NewReader(os.Stdin) - } - - return keyring.New(appName, backend, dir, userInput, cdc) -}