Solana Integration
This example demonstrates a complete cross-chain transfer from Solana to Arbitrum using TypeScript and the Solana Web3.js library.
Prerequisites
- Node.js 18+
- An API key from LayerZero
- A funded Solana wallet with the token you want to transfer
Installation
- pnpm
- npm
- yarn
pnpm add @solana/web3.js bs58 dotenv
npm install @solana/web3.js bs58 dotenv
yarn add @solana/web3.js bs58 dotenv
Environment setup
Create a .env file in your project root:
VT_API_KEY=your_api_key_here
SOLANA_PRIVATE_KEY=your_base58_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
const response = await fetch(
'https://transfer.layerzero-api.com/v1/tokens?' +
new URLSearchParams({
transferrableFromChainKey: 'solana',
transferrableFromTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8',
}),
);
const {tokens} = await response.json();
// Check if destination token exists in reachable tokens
const isSupported = tokens.some(
(t) => t.chainKey === 'arbitrum' && t.address === '0x16f1967565aaD72DD77588a332CE445e7cEF752b',
);
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 quote ID.
- TypeScript
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: 'solana',
dstChainKey: 'arbitrum',
srcTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8',
dstTokenAddress: '0x16f1967565aaD72DD77588a332CE445e7cEF752b',
srcWalletAddress: 'YourSolanaPublicKey',
dstWalletAddress: '0xYourEVMWallet',
amount: '1000000000000',
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_solana_abc123',
routeSteps: [
{
type: 'OFT_V2',
srcChainKey: 'solana',
description: 'Transfer OFT from Solana to Arbitrum',
},
],
feeUsd: '0.15',
feePercent: '0.015',
srcAmount: '1000000000000',
dstAmount: '999850000000',
duration: {estimated: '120000'},
}
Build and execute user steps
For Solana transfers, call /build-user-steps to get fresh transaction data, then sign and submit the transaction.
Build user steps
Solana transactions have short blockhash validity, so you must generate transaction data immediately before signing:
- TypeScript
const stepsResponse = await fetch('https://transfer.layerzero-api.com/v1/build-user-steps', {
method: 'POST',
headers: {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({quoteId: quote.id}),
});
const {body} = await stepsResponse.json();
const userSteps = body.userSteps;
User steps response:
[
{
type: 'TRANSACTION',
chainKey: 'solana',
chainType: 'SOLANA',
description: 'Send OFT tokens via LayerZero',
signerAddress: 'YourSolanaPublicKey',
transaction: {
encoded: {
encoding: 'base64',
data: 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAf...',
},
},
},
];
Execute the transaction
Deserialize, sign, and submit the transaction using Solana Web3.js:
- TypeScript
import * as web3 from '@solana/web3.js';
import bs58 from 'bs58';
const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed');
// Parse private key (supports hex or base58)
function parseSolanaSecretKey(raw: string): Uint8Array {
const isHex = /^0x[0-9a-fA-F]+$/.test(raw) || /^[0-9a-fA-F]+$/.test(raw);
if (isHex) {
const hex = raw.replace(/^0x/, '');
return new Uint8Array(Buffer.from(hex, 'hex'));
}
return bs58.decode(raw);
}
const keypair = web3.Keypair.fromSecretKey(parseSolanaSecretKey(process.env.SOLANA_PRIVATE_KEY!));
let lastSignature;
for (const step of userSteps) {
if (step.type !== 'TRANSACTION') continue;
const tx = step.transaction.encoded;
if (tx.encoding !== 'base64') continue;
// Deserialize the transaction
const raw = Buffer.from(tx.data, 'base64');
const msg = web3.VersionedMessage.deserialize(raw);
const vtx = new web3.VersionedTransaction(msg);
// Sign the transaction
vtx.sign([keypair]);
// Send the transaction
lastSignature = await connection.sendTransaction(vtx);
console.log('Transaction sent:', lastSignature);
// Wait for confirmation
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction({
signature: lastSignature,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
});
console.log('Transaction confirmed');
}
Unlike EVM chains where userSteps are included in the quote response, Solana requires calling /build-user-steps to generate fresh transaction data with a valid blockhash.
Track transfer status
Poll the status endpoint until the transfer completes. The API returns the current status and explorer link.
- TypeScript
async function pollStatus(quoteId: string, txSignature?: string) {
const deadline = Date.now() + 5 * 60_000; // 5 minute timeout
while (Date.now() < deadline) {
const query = txSignature ? `?txHash=${encodeURIComponent(txSignature)}` : '';
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, lastSignature);
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 * as web3 from '@solana/web3.js';
import bs58 from 'bs58';
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.SOLANA_PRIVATE_KEY!;
const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed');
function parseSolanaSecretKey(raw: string): Uint8Array {
const isHex = /^0x[0-9a-fA-F]+$/.test(raw) || /^[0-9a-fA-F]+$/.test(raw);
if (isHex) {
const hex = raw.replace(/^0x/, '');
return new Uint8Array(Buffer.from(hex, 'hex'));
}
return bs58.decode(raw);
}
const keypair = web3.Keypair.fromSecretKey(parseSolanaSecretKey(PRIVATE_KEY));
async function main() {
// Step 1: Validate transfer path
const tokensRes = await fetch(
`${API}/tokens?` +
new URLSearchParams({
transferrableFromChainKey: 'solana',
transferrableFromTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8',
}),
);
const {tokens} = await tokensRes.json();
const isSupported = tokens.some(
(t) => t.chainKey === 'arbitrum' && t.address === '0x16f1967565aaD72DD77588a332CE445e7cEF752b',
);
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: 'solana',
dstChainKey: 'arbitrum',
srcTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8',
dstTokenAddress: '0x16f1967565aaD72DD77588a332CE445e7cEF752b',
srcWalletAddress: keypair.publicKey.toBase58(),
dstWalletAddress: '0x6d9798053f498451bec79c0397f7f95b079bdcd6',
amount: '1000000000000',
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);
// Step 3: Build user steps (required for Solana)
const stepsRes = await fetch(`${API}/build-user-steps`, {
method: 'POST',
headers: {'x-api-key': API_KEY, 'Content-Type': 'application/json'},
body: JSON.stringify({quoteId: quote.id}),
});
const {body} = await stepsRes.json();
const userSteps = body.userSteps;
// Execute transactions
let lastSignature: string | undefined;
for (const step of userSteps) {
if (step.type !== 'TRANSACTION') continue;
const tx = step.transaction.encoded;
if (tx.encoding !== 'base64') continue;
const raw = Buffer.from(tx.data, 'base64');
const msg = web3.VersionedMessage.deserialize(raw);
const vtx = new web3.VersionedTransaction(msg);
vtx.sign([keypair]);
lastSignature = await connection.sendTransaction(vtx);
console.log('Transaction sent:', lastSignature);
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction({
signature: lastSignature,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
});
}
// Step 4: Poll status
const deadline = Date.now() + 5 * 60_000;
while (Date.now() < deadline) {
const query = lastSignature ? `?txHash=${encodeURIComponent(lastSignature)}` : '';
const statusRes = await fetch(`${API}/status/${encodeURIComponent(quote.id)}${query}`, {
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);
Key differences from EVM
| Aspect | EVM | Solana |
|---|---|---|
| Transaction format | JSON with to, data, value | Base64-encoded versioned transaction |
| User steps | Included in quote response | Requires /build-user-steps call |
| Signing | EIP-191 or EIP-712 | Ed25519 |
| Confirmation | waitForTransactionReceipt | confirmTransaction with blockhash |
Next steps
- EVM Example — Transfer tokens between EVM chains
- API Reference — Explore all endpoints