Skip to content
starskiff

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 open

Transfer 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 channels tuples: first pair → channel-0, next → channel-1, …
  • Mixed pairs with EVM chains ([wasmd, evmd]) work out of the box — EVM instances advertise relayerHints and hermes configures ethermint derivation automatically.
  • Handshakes are the slow part; budget 1–3 minutes of relayer start timeout for multi-channel setups.