Bridging
EVM to EVM
Full worked example moving USDC from Base Sepolia to Arbitrum Sepolia.
What happens
- If your USDC allowance for the source
TokenMessengerV2is short, sign anapprove. - Call
depositForBurnon the source chain. USDC is burned and Circle emits the message. - Poll Iris for the attestation.
- Call
receiveMessageon the destination chain. USDC is minted to your recipient.
Steps 1, 2, and 4 return unsigned RawEvmTransaction objects. You sign and broadcast them yourself.
Setup
import {
ZetHubBridge,
Environment,
NetworkId,
AssetSymbol,
} from "@zethub/bridge";
import { createWalletClient, custom, http, createPublicClient } from "viem";
import { baseSepolia, arbitrumSepolia } from "viem/chains";
// Accounts
const senderAddress = "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d";
const recipientAddress = "0x9F70008A83912b19B3E64B58D5f4A08bBd7b3F0e";
const AMOUNT = "5.0";
// Wallet and public clients you already have in your app
const wallet = createWalletClient({
transport: custom(window.ethereum),
account: senderAddress,
});
const sourcePublic = createPublicClient({
chain: baseSepolia,
transport: http(),
});
const destPublic = createPublicClient({
chain: arbitrumSepolia,
transport: http(),
});Discover the tokens
const sdk = new ZetHubBridge({ environment: Environment.TESTNET });
const chains = await sdk.chainDetailsMap();
const sourceToken = chains[NetworkId.BASE_SEPOLIA].tokens.find(
(t) => t.symbol === AssetSymbol.USDC,
)!;
const destinationToken = chains[NetworkId.ARBITRUM_SEPOLIA].tokens.find(
(t) => t.symbol === AssetSymbol.USDC,
)!;Step 1: approve (only when allowance is short)
const enough = await sdk.bridge.checkAllowance({
token: sourceToken,
owner: senderAddress,
amount: AMOUNT,
});
if (!enough) {
const approveTx = await sdk.bridge.rawTxBuilder.approve({
token: sourceToken,
owner: senderAddress,
});
const approveHash = await wallet.sendTransaction({
to: approveTx.to,
data: approveTx.data,
chain: baseSepolia,
});
await sourcePublic.waitForTransactionReceipt({ hash: approveHash });
}Step 2: burn on the source
const burnTx = await sdk.bridge.rawTxBuilder.send({
sourceToken,
destinationToken,
amount: AMOUNT,
fromAccountAddress: senderAddress,
toAccountAddress: recipientAddress,
});
const burnHash = await wallet.sendTransaction({
to: burnTx.to,
data: burnTx.data,
chain: baseSepolia,
});
await sourcePublic.waitForTransactionReceipt({ hash: burnHash });Step 3: wait for the attestation
const att = await sdk.attestation.waitFor(NetworkId.BASE_SEPOLIA, burnHash);Step 4: mint on the destination
const mintTx = await sdk.bridge.rawTxBuilder.receive({
destinationToken,
toAccountAddress: recipientAddress,
message: att.message,
attestation: att.attestation,
});
const mintHash = await wallet.sendTransaction({
to: mintTx.to,
data: mintTx.data,
chain: arbitrumSepolia,
});
await destPublic.waitForTransactionReceipt({ hash: mintHash });
console.log("Done:", { burnHash, mintHash });Full script
import {
ZetHubBridge,
Environment,
NetworkId,
AssetSymbol,
} from "@zethub/bridge";
import { createWalletClient, custom, http, createPublicClient } from "viem";
import { baseSepolia, arbitrumSepolia } from "viem/chains";
async function bridge() {
const senderAddress = "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d";
const recipientAddress = "0x9F70008A83912b19B3E64B58D5f4A08bBd7b3F0e";
const AMOUNT = "5.0";
const wallet = createWalletClient({
transport: custom(window.ethereum),
account: senderAddress,
});
const sourcePublic = createPublicClient({ chain: baseSepolia, transport: http() });
const destPublic = createPublicClient({ chain: arbitrumSepolia, transport: http() });
const sdk = new ZetHubBridge({ environment: Environment.TESTNET });
const chains = await sdk.chainDetailsMap();
const sourceToken = chains[NetworkId.BASE_SEPOLIA].tokens.find(
(t) => t.symbol === AssetSymbol.USDC,
)!;
const destinationToken = chains[NetworkId.ARBITRUM_SEPOLIA].tokens.find(
(t) => t.symbol === AssetSymbol.USDC,
)!;
if (!(await sdk.bridge.checkAllowance({
token: sourceToken,
owner: senderAddress,
amount: AMOUNT,
}))) {
const approveTx = await sdk.bridge.rawTxBuilder.approve({
token: sourceToken,
owner: senderAddress,
});
const h = await wallet.sendTransaction({
to: approveTx.to,
data: approveTx.data,
chain: baseSepolia,
});
await sourcePublic.waitForTransactionReceipt({ hash: h });
}
const burnTx = await sdk.bridge.rawTxBuilder.send({
sourceToken,
destinationToken,
amount: AMOUNT,
fromAccountAddress: senderAddress,
toAccountAddress: recipientAddress,
});
const burnHash = await wallet.sendTransaction({
to: burnTx.to,
data: burnTx.data,
chain: baseSepolia,
});
await sourcePublic.waitForTransactionReceipt({ hash: burnHash });
const att = await sdk.attestation.waitFor(NetworkId.BASE_SEPOLIA, burnHash);
const mintTx = await sdk.bridge.rawTxBuilder.receive({
destinationToken,
toAccountAddress: recipientAddress,
message: att.message,
attestation: att.attestation,
});
const mintHash = await wallet.sendTransaction({
to: mintTx.to,
data: mintTx.data,
chain: arbitrumSepolia,
});
await destPublic.waitForTransactionReceipt({ hash: mintHash });
return { burnHash, mintHash };
}Notes
- The recipient is an EVM address on the destination chain.
- Your wallet must be able to switch chains between the burn and the mint. Every mainstream browser wallet handles this.
- All EVM chains share the same
TokenMessengerV2andMessageTransmitterV2addresses. See Contracts.