Skip to main content

Local development with Anvil

Run Recoil Link on two local chains and send messages between them. No faucets, no testnet, deliveries in seconds. You need Foundry (for anvil, cast, forge) and the rlink CLI.

1. Start two chains​

In two terminals:

anvil --port 8545 --chain-id 31337
anvil --port 8546 --chain-id 31338

2. Describe them in a local registry​

rlink reads chain metadata from a registry. Create one in a folder of your project:

for n in 1 2; do
mkdir -p local-registry/chains/anvil$n
cat > local-registry/chains/anvil$n/metadata.yaml <<EOF
name: anvil$n
displayName: Anvil $n
chainId: $((31336 + n))
domainId: $((31336 + n))
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:$((8544 + n))
nativeToken:
name: Ether
symbol: ETH
decimals: 18
blocks:
confirmations: 1
reorgPeriod: 0
estimateBlockTime: 1
isTestnet: true
EOF
done

3. Write a local core config​

On a local chain there are no validators, so use a test security module that accepts every message. Never use testIsm outside local development.

core-config.yaml
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' # Anvil account 0
defaultIsm:
type: testIsm
defaultHook:
type: merkleTreeHook
requiredHook:
type: protocolFee
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
protocolFee: '0'
maxProtocolFee: '1000000000000000000'

There is no gas paymaster in this config, so messages cost nothing locally.

4. Deploy to both chains​

Sign with Anvil's first default account:

export HYP_KEY=$(cast wallet private-key --mnemonic "test test test test test test test test test test test junk")
rlink core deploy --chain anvil1 --config core-config.yaml --registry ./local-registry --yes
rlink core deploy --chain anvil2 --config core-config.yaml --registry ./local-registry --yes

Each takes a few seconds. The CLI writes the addresses to local-registry/chains/anvil<n>/addresses.yaml. On fresh Anvil chains the Mailbox gets the same address on both (same deployer, same nonces); that is expected.

5. Send a message​

rlink send message --origin anvil1 --destination anvil2 --body "Hello local" --relay --registry ./local-registry --yes
Message ID: 0xeff00ee2…c62139
Attempting self-relay of message
Message was self-relayed!
Delivery time: 6s

--relay makes the CLI deliver the message itself instead of waiting for a relayer, which you do not run locally.

6. Your own contracts​

Deploy the Sender on anvil1 and the Receiver on anvil2 with the Mailbox address from addresses.yaml, send through the Sender, then deliver with rlink status:

MAILBOX=$(grep '^mailbox:' local-registry/chains/anvil1/addresses.yaml | cut -d'"' -f2)
SENDER=$(forge create src/Sender.sol:Sender --rpc-url http://127.0.0.1:8545 --private-key $HYP_KEY \
--broadcast --constructor-args $MAILBOX | awk '/Deployed to:/ {print $3}')
RECEIVER=$(forge create src/Receiver.sol:Receiver --rpc-url http://127.0.0.1:8546 --private-key $HYP_KEY \
--broadcast --constructor-args $MAILBOX 31337 $SENDER | awk '/Deployed to:/ {print $3}')

TX=$(cast send --rpc-url http://127.0.0.1:8545 --private-key $HYP_KEY --json $SENDER \
'send(uint32,address,bytes)' 31338 $RECEIVER $(cast from-utf8 "hello from Sender") | jq -r .transactionHash)

rlink status --origin anvil1 --dispatchTx $TX --relay --registry ./local-registry --yes

The Receiver emits Received(31337, <Sender as bytes32>, "hello from Sender") on anvil2.

Next​

  • Testing with MockMailbox: the same flow inside Foundry tests, without running chains.
  • Move to testnet: same contracts, real Mailbox addresses, a fee from quoteDispatch.