Skip to main content
Version: Endpoint V2

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 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

1

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:

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`);
Why validate first?

Validating the transfer path before requesting quotes prevents unnecessary API calls and provides immediate feedback if a route doesn't exist.

2

Get quotes

Request a quote for your cross-chain transfer. The API returns available routes with fees, estimated duration, and quote ID.

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'},
}

3

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:

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:

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');
}
Solana requires build-user-steps

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.

4

Track transfer status

Poll the status endpoint until the transfer completes. The API returns the current status and explorer link.

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:

StatusDescription
PENDINGTransfer initiated, waiting for confirmation
PROCESSINGTransfer in progress across chains
SUCCEEDEDTransfer completed successfully
FAILEDTransfer failed
UNKNOWNStatus 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

AspectEVMSolana
Transaction formatJSON with to, data, valueBase64-encoded versioned transaction
User stepsIncluded in quote responseRequires /build-user-steps call
SigningEIP-191 or EIP-712Ed25519
ConfirmationwaitForTransactionReceiptconfirmTransaction with blockhash

Next steps