EVM Integration
This example demonstrates a complete cross-chain transfer from Base to Optimism using TypeScript and viem.
Prerequisites
- Node.js 18+
- An API key from LayerZero
- A funded wallet on Base (source chain)
Installation
- pnpm
- npm
- yarn
pnpm add viem dotenv
npm install viem dotenv
yarn add viem dotenv
Environment setup
Create a .env file in your project root:
VT_API_KEY=your_api_key_here
EVM_PRIVATE_KEY=0xyour_private_key_here
Complete Transfer Flow
Validate transfer path
Before requesting a quote, verify that the destination token is reachable from your source token.
Query the tokens endpoint with filters to check if your destination exists in the list of transferrable tokens:
- TypeScript (viem)
const response = await fetch(
'https://transfer.layerzero-api.com/v1/tokens?' +
new URLSearchParams({
transferrableFromChainKey: 'base',
transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
}),
);
const {tokens} = await response.json();
// Check if destination token exists in reachable tokens
const isSupported = tokens.some(
(t) => t.chainKey === 'optimism' && t.address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
);
if (!isSupported) {
throw new Error('Transfer path not supported');
}
console.log(`Found ${tokens.length} reachable destinations`);
Validating the transfer path before requesting quotes prevents unnecessary API calls and provides immediate feedback if a route doesn't exist.
Get quotes
Request a quote for your cross-chain transfer. The API returns available routes with fees, estimated duration, and the steps needed to execute.
- TypeScript (viem)
const quoteResponse = await fetch('https://transfer.layerzero-api.com/v1/quotes', {
method: 'POST',
headers: {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
srcChainKey: 'base',
dstChainKey: 'optimism',
srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
srcWalletAddress: '0xYourWallet',
dstWalletAddress: '0xYourWallet',
amount: '100000000000000', // 0.0001 ETH in wei
options: {
amountType: 'EXACT_SRC_AMOUNT',
feeTolerance: {type: 'PERCENT', amount: 2},
},
}),
});
const {quotes} = await quoteResponse.json();
const quote = quotes[0];
console.log('Quote ID:', quote.id);
console.log('Route:', quote.routeSteps[0].type);
console.log('Fee:', quote.feeUsd, 'USD');
Quote response structure:
{
id: 'quote_abc123def456',
routeSteps: [
{
type: 'STARGATE_V2_TAXI',
srcChainKey: 'base',
description: 'Bridge ETH via Stargate V2',
},
],
feeUsd: '0.42',
feePercent: '0.42',
srcAmount: '100000000000000',
dstAmount: '99580000000000',
duration: {estimated: '60000'},
userSteps: [
{
type: 'TRANSACTION',
chainKey: 'base',
chainType: 'EVM',
signerAddress: '0xYourWallet',
transaction: {
encoded: {
chainId: 8453,
to: '0xStargateContract',
data: '0x...',
value: '100024887844265667',
},
},
},
],
}
Execute user steps
Process each user step in the quote. The quote contains an array of steps—execute them in order.
- TypeScript (viem)
import {createWalletClient, createPublicClient, http} from 'viem';
import {privateKeyToAccount} from 'viem/accounts';
import {base} from 'viem/chains';
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
const wallet = createWalletClient({account, chain: base, transport: http()});
const client = createPublicClient({chain: base, transport: http()});
let txHash;
for (const step of quote.userSteps) {
if (step.type === 'TRANSACTION') {
const tx = step.transaction.encoded;
txHash = await wallet.sendTransaction({
account,
to: tx.to,
data: tx.data,
value: BigInt(tx.value ?? 0n),
});
await client.waitForTransactionReceipt({hash: txHash});
console.log('Transaction sent:', txHash);
} else if (step.type === 'SIGNATURE') {
const typed = step.signature.typedData;
const signature = await wallet.signTypedData({
account,
domain: typed.domain,
types: typed.types,
primaryType: typed.primaryType,
message: typed.message,
});
await fetch('https://transfer.layerzero-api.com/v1/submit-signature', {
method: 'POST',
headers: {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
quoteId: quote.id,
signatures: [signature],
}),
});
console.log('Signature submitted');
}
}
Most EVM routes use TRANSACTION steps. Intent-based routes (like Aori) may include SIGNATURE steps for EIP-712 signed messages.
Track transfer status
Poll the status endpoint until the transfer completes. The API returns the current status and explorer link.
- TypeScript (viem)
async function pollStatus(quoteId: string, txHash?: string) {
const deadline = Date.now() + 5 * 60_000; // 5 minute timeout
while (Date.now() < deadline) {
const query = txHash ? `?txHash=${txHash}` : '';
const response = await fetch(
`https://transfer.layerzero-api.com/v1/status/${encodeURIComponent(quoteId)}${query}`,
{headers: {'x-api-key': 'YOUR_API_KEY'}},
);
const {status, explorerUrl} = await response.json();
console.log('Status:', status);
if (status === 'SUCCEEDED') {
console.log('Transfer complete!');
console.log('Explorer:', explorerUrl);
return status;
}
if (status === 'FAILED') {
throw new Error('Transfer failed');
}
await new Promise((r) => setTimeout(r, 4000));
}
throw new Error('Transfer timed out');
}
await pollStatus(quote.id, txHash);
Status values:
| Status | Description |
|---|---|
PENDING | Transfer initiated, waiting for confirmation |
PROCESSING | Transfer in progress across chains |
SUCCEEDED | Transfer completed successfully |
FAILED | Transfer failed |
UNKNOWN | Status cannot be determined |
Complete example
import {createWalletClient, createPublicClient, http, type Hex} from 'viem';
import {privateKeyToAccount} from 'viem/accounts';
import {base} from 'viem/chains';
import * as dotenv from 'dotenv';
dotenv.config();
const API = 'https://transfer.layerzero-api.com/v1';
const API_KEY = process.env.VT_API_KEY!;
const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as Hex;
const account = privateKeyToAccount(PRIVATE_KEY);
const wallet = createWalletClient({account, chain: base, transport: http()});
const client = createPublicClient({chain: base, transport: http()});
async function main() {
// Step 1: Validate transfer path
const tokensRes = await fetch(
`${API}/tokens?` +
new URLSearchParams({
transferrableFromChainKey: 'base',
transferrableFromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
}),
);
const {tokens} = await tokensRes.json();
const isSupported = tokens.some(
(t) => t.chainKey === 'optimism' && t.address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
);
if (!isSupported) throw new Error('Transfer path not supported');
console.log(`Found ${tokens.length} reachable destinations`);
// Step 2: Get quote
const quoteRes = await fetch(`${API}/quotes`, {
method: 'POST',
headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'},
body: JSON.stringify({
srcChainKey: 'base',
dstChainKey: 'optimism',
srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
srcWalletAddress: account.address,
dstWalletAddress: account.address,
amount: '100000000000000',
options: {amountType: 'EXACT_SRC_AMOUNT', feeTolerance: {type: 'PERCENT', amount: 2}},
}),
});
const {quotes} = await quoteRes.json();
const quote = quotes[0];
if (!quote) throw new Error('No quote available');
console.log('Quote received:', quote.id);
console.log('Fee:', quote.feeUsd, 'USD');
// Step 3: Execute user steps
let txHash: Hex | undefined;
for (const step of quote.userSteps) {
if (step.type === 'SIGNATURE') {
const typed = step.signature.typedData;
const signature = await wallet.signTypedData({
account,
domain: typed.domain,
types: typed.types,
primaryType: typed.primaryType,
message: typed.message,
});
await fetch(`${API}/submit-signature`, {
method: 'POST',
headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'},
body: JSON.stringify({quoteId: quote.id, signatures: [signature]}),
});
console.log('Signature submitted');
} else if (step.type === 'TRANSACTION') {
const tx = step.transaction.encoded;
txHash = await wallet.sendTransaction({
account,
to: tx.to,
data: tx.data,
value: BigInt(tx.value ?? 0n),
});
await client.waitForTransactionReceipt({hash: txHash});
console.log('Transaction sent:', txHash);
}
}
// Step 4: Poll status
const deadline = Date.now() + 5 * 60_000;
while (Date.now() < deadline) {
const statusRes = await fetch(
`${API}/status/${encodeURIComponent(quote.id)}${txHash ? `?txHash=${txHash}` : ''}`,
{headers: {'x-api-key': API_KEY}},
);
const {status, explorerUrl} = await statusRes.json();
console.log('Status:', status);
if (status === 'SUCCEEDED') {
console.log('Explorer:', explorerUrl);
break;
}
if (status === 'FAILED') throw new Error('Transfer failed');
await new Promise((r) => setTimeout(r, 4000));
}
}
main().catch(console.error);
Next steps
- Solana Example — Transfer tokens from Solana
- API Reference — Explore all endpoints