Use the Cosmos implementation (crates/chains/core/cosmos/) as reference throughout.
cargo init crates/chains/core/mychain --libAdd to workspace Cargo.toml members, then add mercury-chain-traits and mercury-core as dependencies.
Mirror the Cosmos crate structure:
chain.rs- main struct + constructor,ChainTypesandIbcTypesimplsconfig.rs- TOML-deserializable configkeys.rs- signingtypes.rs- events, packets, proofs, chain statusqueries.rs-ChainStatusQuery,ClientQuery<Self>,PacketStateQueryevents.rs-PacketEvents(parse SendPacket/WriteAck from raw events, query block events)builders.rs-ClientPayloadBuilder<Self>,ClientMessageBuilder<Self>,PacketMessageBuilder<Self>tx.rs-MessageSender, transaction building, signing, fee estimation, submission
All traits live in mercury-chain-traits. Implement them in order:
- Type traits -
ChainTypes(height, timestamp, chain ID, events, messages, chain status, revision number, increment height),IbcTypes(client/consensus state, proofs, packets, acknowledgements) - Query traits -
ChainStatusQuery,ClientQuery<Self>(client state, consensus state, trusting period, client latest height),PacketStateQuery(packet commitment/receipt/ack with Merkle proofs) - Events -
PacketEvents(extract SendPacket/WriteAck from raw events, query block events) - Builder traits -
ClientPayloadBuilder<Self>(create/update client payloads),ClientMessageBuilder<Self>(create/update client, register counterparty),PacketMessageBuilder<Self>(recv/ack/timeout packets) - Messaging -
MessageSenderwith batching and nonce retry - Transaction internals - fee estimation, nonce queries, tx submission, polling (concrete methods, not traits)
Once all traits are implemented, RelayChain is automatically satisfied via a blanket impl.
In your counterparty crate (e.g., crates/chains/counterparties/mychain/src/plugin.rs):
-
ChainPlugin- implementchain_type(),validate_config(),connect(),parse_client_id(),query_status(),chain_id_from_config(),rpc_addr_from_config(). Theconnect()method creates your chain, wraps it inCachedChain, and returns it asAnyChain(Arc<dyn Any + Send + Sync>). -
register()function - register your chain plugin with theChainRegistry:
pub fn register(registry: &mut ChainRegistry) {
registry.register_chain(MyChainPlugin);
}All relay pairs (same-chain and cross-chain) live in dedicated relay crates under crates/chains/relay-pairs/ (e.g., cosmos-cosmos/, cosmos-ethereum/). This keeps counterparty crates focused on adapter types and trait impls.
-
RelayPairPlugin- for each supported relay direction, implementsrc_type(),dst_type(), andbuild_relay(). Thebuild_relay()method downcastsAnyChainback to your concrete types, creates aRelayContext, and returns forward + reverseDynRelayinstances. -
register()function - register relay pair plugins:
pub fn register(registry: &mut ChainRegistry) {
registry.register_pair(MyChainToOtherChainRelay);
registry.register_pair(OtherChainToMyChainRelay);
}Add your register() calls in bin/src/registry.rs:
pub fn build_registry() -> ChainRegistry {
let mut r = ChainRegistry::new();
mercury_cosmos_counterparties::plugin::register(&mut r);
mercury_ethereum_counterparties::plugin::register(&mut r);
mercury_mychain_counterparties::plugin::register(&mut r); // chain plugin
mercury_mychain_relay::register(&mut r); // relay pairs
r
}No enum variants, match arms, or CLI code changes needed beyond these lines.
To relay between your chain and an existing chain, you need cross-chain trait impls on both sides. Each side lives in its respective counterparty crate, behind a feature flag.
For a new chain MyChain relaying against Cosmos:
In crates/chains/counterparties/mychain/ (your counterparty crate):
ClientPayloadBuilder<CosmosChain<S>>- builds your chain's light client payloads.build_create_client_payloadis typically counterparty-agnostic.build_update_client_payloadreceivesCosmosClientState, which is an enum - match on the variant that wraps your light client (usuallyWasmfor non-Tendermint clients).ClientMessageBuilder<CosmosChain<S>>- builds on-chain messages from Cosmos payloadsPacketMessageBuilder<CosmosChain<S>>- builds recv/ack/timeout messagesClientQuery<CosmosChain<S>>- queries your chain for Cosmos client/consensus stateMisbehaviourDetector<CosmosChain<S>>+MisbehaviourQuery+MisbehaviourMessageBuilder- can be no-op stubs initially
In crates/chains/counterparties/cosmos/ (the Cosmos counterparty crate):
ClientPayloadBuilder<MyChain>- Cosmos's impl is fully generic (impl<C: ChainTypes> ClientPayloadBuilder<C>), so this is automatic via the blanket forwardClientMessageBuilder<MyChain>- buildsMsgCreateClient/MsgUpdateClienton Cosmos targeting your chain's light clientPacketMessageBuilder<MyChain>- builds Cosmos packet messages from your chain's proof typesClientQuery<MyChain>- queries Cosmos for your chain's client state (dispatches onCosmosClientStateenum)
Each chain has an adapter type (MyChainAdapter) in its counterparty crate that forwards same-chain traits from the core type and adds cross-chain impls. The delegate_chain! macro handles all boilerplate delegation:
// Generates Deref, HasCore, ChainTypes, IbcTypes, and all operational trait delegations.
// Also generates a blanket ClientPayloadBuilder<C> delegation by default.
mercury_chain_traits::delegate_chain! {
impl[S: MySigner] MyChainAdapter<S> => MyChain<S>
}
// Use skip_cpb if your adapter needs a custom ClientPayloadBuilder impl:
mercury_chain_traits::delegate_chain! {
impl[] MyChainAdapter => MyChain; skip_cpb
}Cross-chain trait impls (ClientMessageBuilder<OtherChain>, PacketMessageBuilder<OtherChain>) are still written manually on the adapter, since they contain counterparty-specific logic (e.g., Ethereum must unwrap CosmosClientState::Wasm to extract beacon bytes).
Non-native light clients on Cosmos are deployed as CosmWasm contracts, so their state is wrapped in CosmosClientState::Wasm. The data field contains the inner light client state bytes (e.g., JSON-serialized beacon client state for Ethereum). When implementing cross-chain traits against Cosmos, match on this enum to extract your chain's inner state. The exhaustive match ensures a compile error if a new variant is added, forcing explicit handling.
Add your counterparty and relay crate register() calls to bin/src/registry.rs. The plugin system takes it from there - no enum variants or match arms to touch.