IBC Relaying
Two chains + hermes, end to end
A full local IBC setup is three instances: two chains and a hermes relayer.
Boot chains and relayer
import { findFreePorts, Instance, testAccounts } from 'starskiff';
const relayer = testAccounts[1]; // funded on BOTH chains
const [portsA, portsB] = await Promise.all([findFreePorts(), findFreePorts()]);
const chainA = Instance.wasmd({
chainId: 'ibc-a',
prefix: 'wasm',
...portsA,
accounts: [{ mnemonic: relayer.mnemonic, coins: '1000000000stake', name: 'relayer' }],
});
const chainB = Instance.wasmd({
chainId: 'ibc-b',
prefix: 'wasm',
...portsB,
accounts: [{ mnemonic: relayer.mnemonic, coins: '1000000000stake', name: 'relayer' }],
});
await Promise.all([chainA.start(), chainB.start()]);
const hermes = Instance.hermes(
{ channels: [[chainA, chainB]], mnemonic: relayer.mnemonic },
{ timeout: 180_000 },
);
await hermes.start(); // resolves once channel-0 is openTransfer across the channel
import { SigningStargateClient, StargateClient, GasPrice } from '@cosmjs/stargate';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx';
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(relayer.mnemonic, { prefix: 'wasm' });
const [sender] = await wallet.getAccounts();
const client = await SigningStargateClient.connectWithSigner(chainA.rpcUrl, wallet, {
gasPrice: GasPrice.fromString('0stake'),
});
await client.signAndBroadcast(sender.address, [{
typeUrl: '/ibc.applications.transfer.v1.MsgTransfer',
value: MsgTransfer.fromPartial({
sourcePort: 'transfer',
sourceChannel: 'channel-0',
token: { denom: 'stake', amount: '1000000' },
sender: sender.address,
receiver: receiverAddress, // same mnemonic, chain B prefix
timeoutTimestamp: BigInt((Math.floor(Date.now() / 1000) + 600) * 1_000_000_000),
}),
}], 'auto');Relaying isn't instant — poll the receiving chain for the ibc/… denom:
const query = await StargateClient.connect(chainB.rpcUrl);
let ibcBalance;
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 2_000));
const balances = await query.getAllBalances(receiverAddress);
ibcBalance = balances.find((b) => b.denom.startsWith('ibc/'));
if (ibcBalance) break;
}Notes
- Channel numbering follows the order of the
channelstuples: first pair →channel-0, next →channel-1, … - Mixed pairs with EVM chains (
[wasmd, evmd]) work out of the box — EVM instances advertiserelayerHintsand hermes configures ethermint derivation automatically. - Handshakes are the slow part; budget 1–3 minutes of relayer start timeout for multi-channel setups.