cast-interop is a cast-like CLI focused on zkSync interop workflows. It helps you extract bundles, fetch proofs, wait for roots, and execute/verify bundles across chains without wiring up the RPC or ABI plumbing every time.
Sending token from chain A to chain B:
cast-interop token send --token $TOKEN_ADDRESS --to $ADDRESS --rpc-src $RPC_A --rpc-dest $RPC_B --private-key $PRIVATE_KEY --amount-wei $AMOUNT(see examples/02_token/README.md for more details)
Viewing interop bundles/messages created by a given transaction:
cast-interop debug tx --rpc $RPC $TX_HASHRelaying all the bundles from transaction from chain A to chain B:
cast-interop bundle relay --rpc-src $RPC_A --rpc-dest $RPC_B --tx $TX_HASH --private-key $PRIVATE_KEYSending bundle with a single remote-call message:
cast-interop send message --to-chain $DESTINATION_CHAIN_ID --to $CONTRACT_ADDR --rpc $RPC_A --payload-file /tmp/message --private-key $PRIVATE_KEY(see examples/01_greeting/README.md for more details)
Automatically relay all the bundles between a set of chains:
cast-interop auto-relay --rpc $RPC_A $RPC_B $RPC_C --private-key $PRIVATE_KEYcargo install cast-interopOr build locally:
cargo build --releaseBinary path:
./target/release/cast-interop --helpConfig file location:
~/.config/cast-interop/config.toml
Add chains (RPC + chainId stored):
cast-interop chains add era --rpc https://mainnet.era.zksync.io
cast-interop chains add test --rpc https://sepolia.era.zksync.devList configured chains:
cast-interop chains listExample output:
alias chainId rpc
era 324 https://mainnet.era.zksync.io
test 300 https://sepolia.era.zksync.dev
You can still use the legacy [rpc] config for backwards compatibility:
[rpc]
default = "https://mainnet.era.zksync.io"Preferred new format:
[chains.era]
rpc = "https://mainnet.era.zksync.io"
chainId = 324
[chains.test]
rpc = "https://sepolia.era.zksync.dev"
chainId = 300
[addresses]
interop_center = "0x0000000000000000000000000000000000010010"
interop_handler = "0x000000000000000000000000000000000001000d"
interop_root_storage = "0x0000000000000000000000000000000000010008"RPC selection rules:
- Use
--rpc <URL>or--chain <alias>(not both). - If neither is provided, the CLI uses the default chain if configured.
Signer flags (required for sending transactions unless using --dry-run):
--private-key <hex>--private-key-env <ENV>(default:PRIVATE_KEY)
cast-interop bundle relay \
--chain-src era \
--chain-dest test \
--tx 0xSOURCE_TX_HASH \
--private-key $PRIVATE_KEYSample output (trimmed):
sent tx: 0x6b6c...e219
Relay summary output (trimmed, with --json):
cast-interop bundle relay \
--chain-src era \
--chain-dest test \
--tx 0xSOURCE_TX_HASH \
--mode execute \
--json{
"sourceChainId": "324",
"destinationChainId": "300",
"l1BatchNumber": 12345,
"l2MessageIndex": 7,
"bundleHash": "0x4f3c...a2b1",
"sourceTxHash": "0xabc...def",
"handlerTxHash": "0x6b6c...e219"
}cast-interop bundle relay \
--chain-src era \
--chain-dest test \
--tx 0xSOURCE_TX_HASH \
--mode verify \
--private-key $PRIVATE_KEYcast-interop bundle relay \
--chain-src era \
--chain-dest test \
--tx 0xSOURCE_TX_HASH \
--mode execute \
--dry-run- Extract bundle:
cast-interop bundle extract --chain era --tx 0xSOURCE_TX_HASH --out bundle.hex- Get proof:
cast-interop debug proof --chain era --tx 0xSOURCE_TX_HASH --msg-index 0 --out proof.json- Wait for root on destination:
cast-interop debug root \
--chain test \
--source-chain 324 \
--batch 12345 \
--expected-root 0xROOT- Execute bundle:
cast-interop bundle execute \
--chain test \
--bundle bundle.hex \
--proof proof.json \
--private-key $PRIVATE_KEYcast-interop send message \
--chain era \
--to-chain test \
--to 0xTargetAddress \
--payload 0xdeadbeef \
--interop-value 0 \
--execution-address permissionless \
--dry-runcalls.json:
{
"calls": [
{
"to": "0xTargetAddress",
"data": "0xabcdef",
"attributes": {
"interopValue": "0",
"indirect": null
}
}
]
}Send bundle:
cast-interop send bundle \
--chain era \
--to-chain test \
--calls calls.json \
--bundle-execution-address permissionless \
--bundle-unbundler 0xYourAddress \
--private-key $PRIVATE_KEYSend an ERC20 via interop (Type B flow):
cast-interop token send \
--chain-src era \
--chain-dest test \
--token 0xTokenOnSource \
--amount 100 \
--to 0xRecipientOnDest \
--private-key $PRIVATE_KEYDry-run (simulate only):
cast-interop token send \
--chain-src era \
--chain-dest test \
--token 0xTokenOnSource \
--amount-wei 1000000000000000000 \
--to 0xRecipientOnDest \
--dry-runCheck wrap info and destination balance:
cast-interop token info \
--chain-src era \
--chain-dest test \
--token 0xTokenOnSource
cast-interop token balance \
--chain-src era \
--chain-dest test \
--token 0xTokenOnSource \
--to 0xRecipientOnDestDebug checklist for stuck transfers:
cast-interop debug tx --chain era 0xSOURCE_TX_HASH
cast-interop debug proof --chain era --tx 0xSOURCE_TX_HASH
cast-interop debug root --chain test --source-chain 324 --batch <batch> --expected-root <root>
cast-interop bundle status --chain test --bundle-hash <bundleHash>
cast-interop bundle explain --chain test --bundle <bundle.hex> --proof <proof.json>
cast-interop debug doctor --chain testcast-interop debug watch \
--chain-src era \
--chain-dest test \
--tx 0xSOURCE_TX_HASH \
--until executed- txHash: The L2 transaction hash that emitted an
InteropBundleSentorMessageSentevent. - bundleHash: The hash of the interop bundle emitted by
InteropCenter.sendBundle. - sendId: A per-message ID emitted by
InteropCenter.sendMessage(bundleHash + index). - proof: Inclusion proof data returned by
zks_getL2ToL1LogProof(batch number, log index, proof nodes). - root wait: Checks
interopRoots(chainId, batchNumber)until the expected root is available on the destination chain.
Proof never appears
- Ensure the source RPC supports
zks_getL2ToL1LogProof. - Check that the transaction is finalized before polling.
Root mismatch
- Make sure
--source-chainuses the source chainId (not alias). - Verify you’re using the correct batch number from the proof.
Execute reverted
- Confirm the destination chainId matches the bundle’s destination.
- Validate permissions:
executionAddress/unbundlerAddressmust match the signer.
RPC missing finalized or getLogProof
- Use
cast-interop debug rpc --chain <alias>to confirm capabilities. - Switch to a zkSync-native RPC if the method is unsupported.
Most commands support --json for structured output.
Example (bundle status):
cast-interop bundle status --chain test --bundle-hash 0xBUNDLE --json{
"bundleHash": "0xBUNDLE",
"bundleStatus": "Verified",
"calls": [
{ "index": 0, "status": "Executed" }
]
}Example (chains list):
cast-interop chains list --json[
{
"alias": "era",
"rpc": "https://mainnet.era.zksync.io",
"chainId": "324"
}
]Example (debug tx, trimmed):
cast-interop debug tx --chain era 0xSOURCE_TX_HASHbundleHash: 0x4f3c...a2b1
interopEvents: 3