Build Swap Applications on Solana#
There are two approaches to building swap applications with OKX DEX on Solana:
- The API-first approach - directly interacting with OKX DEX API endpoints
- The SDK approach - using the
@okx-dex/okx-dex-sdk
package for a simplified developer experience
This guide covers both methods to help you choose the approach that best fits your needs.
Method 1: API-First Approach#
This approach demonstrates a token swap using the OKX DEX API endpoints directly. You will swap SOL to USDC on Solana Mainnet.
1. Set Up Your Environment#
Import the necessary Node.js libraries and set up your environment variables:
// Required libraries
import base58 from "bs58";
import BN from "bn.js";
import * as solanaWeb3 from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import cryptoJS from "crypto-js";
import axios from "axios";
import dotenv from 'dotenv';
dotenv.config();
// Environment variables
const apiKey = process.env.OKX_API_KEY;
const secretKey = process.env.OKX_SECRET_KEY;
const apiPassphrase = process.env.OKX_API_PASSPHRASE;
const projectId = process.env.OKX_PROJECT_ID;
const userAddress = process.env.WALLET_ADDRESS;
const userPrivateKey = process.env.PRIVATE_KEY;
const solanaRpcUrl = process.env.SOLANA_RPC_URL;
// Constants
const SOLANA_CHAIN_ID = "501";
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;
// Initialize Solana connection
const connection = new Connection(`${solanaRpcUrl}`, {
confirmTransactionInitialTimeout: 5000
});
// Utility function for OKX API authentication
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
const stringToSign = timestamp + method + requestPath + (queryString || body);
return {
"Content-Type": "application/json",
"OK-ACCESS-KEY": apiKey,
"OK-ACCESS-SIGN": cryptoJS.enc.Base64.stringify(
cryptoJS.HmacSHA256(stringToSign, secretKey)
),
"OK-ACCESS-TIMESTAMP": timestamp,
"OK-ACCESS-PASSPHRASE": apiPassphrase,
"OK-ACCESS-PROJECT": projectId,
};
}
2. Get Swap Data#
Solana's native token address is 11111111111111111111111111111111. Use the /swap endpoint to retrieve detailed swap information:
async function getSwapData(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage = '0.005' // 0.5% slippage
) {
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const params = {
amount: amount,
chainId: SOLANA_CHAIN_ID,
fromTokenAddress: fromTokenAddress,
toTokenAddress: toTokenAddress,
userWalletAddress: userAddress,
slippage: slippage
};
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
try {
const response = await axios.get(
`https://web3.okx.com${requestPath}${queryString}`,
{ headers }
);
if (response.data.code !== "0" || !response.data.data?.[0]) {
throw new Error(`API Error: ${response.data.msg || "Failed to get swap data"}`);
}
return response.data.data[0];
} catch (error) {
console.error("Error fetching swap data:", error);
throw error;
}
}
3. Prepare Transaction#
async function prepareTransaction(callData: string) {
try {
// Decode the base58 encoded transaction data
const decodedTransaction = base58.decode(callData);
// Get the latest blockhash
const recentBlockHash = await connection.getLatestBlockhash();
console.log("Got blockhash:", recentBlockHash.blockhash);
let tx;
// Try to deserialize as a versioned transaction first
try {
tx = solanaWeb3.VersionedTransaction.deserialize(decodedTransaction);
console.log("Successfully created versioned transaction");
tx.message.recentBlockhash = recentBlockHash.blockhash;
} catch (e) {
// Fall back to legacy transaction if versioned fails
console.log("Versioned transaction failed, trying legacy:", e);
tx = solanaWeb3.Transaction.from(decodedTransaction);
console.log("Successfully created legacy transaction");
tx.recentBlockhash = recentBlockHash.blockhash;
}
return {
transaction: tx,
recentBlockHash
};
} catch (error) {
console.error("Error preparing transaction:", error);
throw error;
}
}
4. Simulate Transaction#
Before executing the actual swap, it's crucial to simulate the transaction to ensure it will succeed and to identify any potential issues:
This function uses the Onchain gateway API. This API is available to our enterprise customers only. If you are interested, please contact us dexapi@okx.com.
async function simulateTransaction(swapData: any) {
try {
if (!swapData.tx) {
throw new Error('Invalid swap data format - missing transaction data');
}
const tx = swapData.tx;
const params: any = {
fromAddress: tx.from,
toAddress: tx.to,
txAmount: tx.value,
chainIndex: SOLANA_CHAIN_ID,
extJson: {
inputData: tx.data
},
includeDebug: true
};
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/pre-transaction/simulate";
const requestBody = JSON.stringify(params);
const headers = getHeaders(timestamp, "POST", requestPath, "", requestBody);
console.log('Simulating transaction...');
const response = await axios.post(
`https://web3.okx.com${requestPath}`,
params,
{ headers }
);
if (response.data.code !== "0") {
throw new Error(`Simulation failed: ${response.data.msg || "Unknown simulation error"}`);
}
const simulationResult = response.data.data[0];
// Check simulation success
if (simulationResult.success === false) {
console.error('Transaction simulation failed:', simulationResult.error);
throw new Error(`Transaction would fail: ${simulationResult.error}`);
}
console.log('Transaction simulation successful');
console.log(`Estimated gas used: ${simulationResult.gasUsed || 'N/A'}`);
if (simulationResult.logs) {
console.log('Simulation logs:', simulationResult.logs);
}
return simulationResult;
} catch (error) {
console.error("Error simulating transaction:", error);
throw error;
}
}
5. Broadcast Transaction#
5.1 Create a Compute Unit Estimation Utility Function
Solana uses compute units instead of gas to measure transaction complexity. There are two approaches to estimate compute units for your transactions: using standard RPC calls or leveraging the Onchain Gateway API.
Method 1: Using the Onchain Gateway API for Compute Unit Estimation
The first approach leverages OKX's proprietary Onchain Gateway API, which provides more accurate compute unit estimations. This API is exclusively available to enterprise customers. For access inquiries, please contact dexapi@okx.com.
/**
* Get transaction compute units from Onchain Gateway API
* @param fromAddress - Sender address
* @param toAddress - Target program address
* @param inputData - Transaction data (base58 encoded)
* @returns Estimated compute units
*/
async function getComputeUnits(
fromAddress: string,
toAddress: string,
inputData: string
): Promise<number> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: "501", // Solana chain ID
fromAddress: fromAddress,
toAddress: toAddress,
txAmount: "0",
extJson: {
inputData: inputData
}
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post(url, body, { headers });
if (response.data.code === '0') {
const computeUnits = parseInt(response.data.data[0].gasLimit);
console.log(`API estimated compute units: ${computeUnits}`);
return computeUnits;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get compute units from API:', (error as Error).message);
throw error;
}
}
Method 2: Using RPC to Estimate Compute Units
The second approach utilizes standard Solana RPC calls to simulate and estimate the required compute units for your transaction.
/**
* Estimate compute units for a transaction
*/
async function getComputeUnits(transaction: VersionedTransaction): Promise<number> {
try {
// Simulate the transaction to get compute unit usage
const simulationResult = await connection.simulateTransaction(transaction, {
replaceRecentBlockhash: true,
commitment: 'processed'
});
if (simulationResult.value.err) {
throw new Error(`Simulation failed: ${JSON.stringify(simulationResult.value.err)}`);
}
// Get the compute units consumed from simulation
const computeUnitsConsumed = simulationResult.value.unitsConsumed || 200000;
// Add 20% buffer for safety
const computeUnitsWithBuffer = Math.ceil(computeUnitsConsumed * 1.2);
console.log(`Estimated compute units: ${computeUnitsConsumed}`);
console.log(`With 20% buffer: ${computeUnitsWithBuffer}`);
return computeUnitsWithBuffer;
} catch (error) {
console.warn('Failed to estimate compute units, using default:', error);
return 300000; // Default fallback
}
}
5.2 Transaction Preparation with Compute Units
Before broadcasting, prepare your transaction with the estimated compute units and latest blockhash:
Method 1: Transaction Using Compute Units from Gas-Limit API
For enterprise customers, prepare your transaction with compute units estimated from the Onchain Gateway API: Note: This functionality requires enterprise API access. Contact dexapi@okx.com for more information.
// Simple connection setup
const connection = new Connection(
process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);
/**
* Get transaction compute units from Onchain Gateway API
*/
async function getComputeUnitsFromAPI(
fromAddress: string,
inputData: string
): Promise<number> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: "501", // Solana chain ID
fromAddress: fromAddress,
toAddress: "", // Can be empty for Solana
txAmount: "0",
extJson: {
inputData: inputData
}
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await fetch(url, {
method: 'POST',
headers,
body: bodyString
});
const data = await response.json();
if (data.code === '0') {
const computeUnits = parseInt(data.data[0].gasLimit);
console.log(`API estimated compute units: ${computeUnits}`);
return computeUnits;
} else {
throw new Error(`API Error: ${data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get compute units from API:', (error as Error).message);
throw error;
}
}
/**
* Prepare transaction with compute units from API
*/
async function prepareTransactionWithAPIComputeUnits(
transaction: VersionedTransaction,
fromAddress: string,
transactionData: string
): Promise<{
transaction: VersionedTransaction;
gasData: {
estimatedComputeUnits: number;
priorityFee: number;
blockhash: string;
};
}> {
try {
// Get fresh blockhash
const { blockhash } = await connection.getLatestBlockhash('confirmed');
console.log(`Using blockhash: ${blockhash}`);
// Update the transaction's blockhash
transaction.message.recentBlockhash = blockhash;
// Check if transaction already has compute budget instructions
const hasComputeBudgetIx = transaction.message.compiledInstructions.some(ix => {
const programId = transaction.message.staticAccountKeys[ix.programIdIndex];
return programId.equals(ComputeBudgetProgram.programId);
});
if (hasComputeBudgetIx) {
console.log('Transaction already contains compute budget instructions, skipping addition');
return {
transaction,
gasData: {
estimatedComputeUnits: 300000,
priorityFee: 1000,
blockhash
}
};
}
// Get compute units from API
const estimatedComputeUnits = await getComputeUnitsFromAPI(fromAddress, transactionData);
// Set priority fee
const priorityFee = 1000; // microLamports
const gasData = {
estimatedComputeUnits,
priorityFee,
blockhash
};
console.log(`Priority fee: ${gasData.priorityFee} microLamports`);
// Create compute unit limit instruction
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: gasData.estimatedComputeUnits
});
// Create compute unit price instruction for priority
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: gasData.priorityFee
});
// Get existing instructions and account keys
const existingInstructions = [...transaction.message.compiledInstructions];
const existingAccountKeys = [...transaction.message.staticAccountKeys];
// Add compute budget program to account keys if not present
let computeBudgetProgramIndex = existingAccountKeys.findIndex(
key => key.equals(ComputeBudgetProgram.programId)
);
if (computeBudgetProgramIndex === -1) {
computeBudgetProgramIndex = existingAccountKeys.length;
existingAccountKeys.push(ComputeBudgetProgram.programId);
}
// Create new instructions array with compute budget instructions
const newInstructions = [
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computeBudgetIx.data
},
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computePriceIx.data
},
...existingInstructions
];
// Create new versioned message with proper instruction mapping
const newMessage = new TransactionMessage({
payerKey: existingAccountKeys[0],
recentBlockhash: gasData.blockhash,
instructions: newInstructions.map(ix => ({
programId: existingAccountKeys[ix.programIdIndex],
keys: ix.accountKeyIndexes.map(idx => ({
pubkey: existingAccountKeys[idx],
isSigner: false,
isWritable: false
})).filter(key => key.pubkey),
data: Buffer.from(ix.data)
})).filter(ix => ix.programId)
}).compileToV0Message();
// Create and return new transaction
const preparedTransaction = new VersionedTransaction(newMessage);
return { transaction: preparedTransaction, gasData };
} catch (error) {
console.error('Error preparing transaction:', error);
throw error;
}
}
Method 2: Transaction Using Compute Units from RPC
// Simple connection setup
const connection = new Connection(
process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);
/**
* Prepare transaction with compute units
*/
async function prepareTransactionWithComputeUnits(
transaction: VersionedTransaction
): Promise<{
transaction: VersionedTransaction;
gasData: {
estimatedComputeUnits: number;
priorityFee: number;
blockhash: string;
};
}> {
try {
// Get fresh blockhash
const { blockhash } = await connection.getLatestBlockhash('confirmed');
console.log(`Using blockhash: ${blockhash}`);
// Update the transaction's blockhash
transaction.message.recentBlockhash = blockhash;
// Check if transaction already has compute budget instructions
const hasComputeBudgetIx = transaction.message.compiledInstructions.some(ix => {
const programId = transaction.message.staticAccountKeys[ix.programIdIndex];
return programId.equals(ComputeBudgetProgram.programId);
});
if (hasComputeBudgetIx) {
console.log('Transaction already contains compute budget instructions, skipping addition');
return {
transaction,
gasData: {
estimatedComputeUnits: 300000,
priorityFee: 1000,
blockhash
}
};
}
// Estimate compute units
const estimatedComputeUnits = await getComputeUnits(transaction);
// Set priority fee
const priorityFee = 1000; // microLamports
const gasData = {
estimatedComputeUnits,
priorityFee,
blockhash
};
console.log(`Priority fee: ${gasData.priorityFee} microLamports`);
// Create compute unit limit instruction
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: gasData.estimatedComputeUnits
});
// Create compute unit price instruction for priority
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: gasData.priorityFee
});
// Get existing instructions and account keys
const existingInstructions = [...transaction.message.compiledInstructions];
const existingAccountKeys = [...transaction.message.staticAccountKeys];
// Add compute budget program to account keys if not present
let computeBudgetProgramIndex = existingAccountKeys.findIndex(
key => key.equals(ComputeBudgetProgram.programId)
);
if (computeBudgetProgramIndex === -1) {
computeBudgetProgramIndex = existingAccountKeys.length;
existingAccountKeys.push(ComputeBudgetProgram.programId);
}
// Create new instructions array with compute budget instructions
const newInstructions = [
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computeBudgetIx.data
},
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computePriceIx.data
},
...existingInstructions
];
// Create new versioned message with proper instruction mapping
const newMessage = new TransactionMessage({
payerKey: existingAccountKeys[0],
recentBlockhash: gasData.blockhash,
instructions: newInstructions.map(ix => ({
programId: existingAccountKeys[ix.programIdIndex],
keys: ix.accountKeyIndexes.map(idx => ({
pubkey: existingAccountKeys[idx],
isSigner: false,
isWritable: false
})).filter(key => key.pubkey),
data: Buffer.from(ix.data)
})).filter(ix => ix.programId)
}).compileToV0Message();
// Create and return new transaction
const preparedTransaction = new VersionedTransaction(newMessage);
return { transaction: preparedTransaction, gasData };
} catch (error) {
console.error('Error preparing transaction:', error);
throw error;
}
}
5.3 Broadcasting Transactions
Using Onchain Gateway API
This functionality requires enterprise API access. To learn more about enterprise features, please contact dexapi@okx.com.
async function broadcastTransaction(
signedTx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction
) {
try {
const serializedTx = signedTx.serialize();
const encodedTx = base58.encode(serializedTx);
const path = "dex/pre-transaction/broadcast-transaction";
const url = `https://web3.okx.com/api/v5/${path}`;
const broadcastData = {
signedTx: encodedTx,
chainIndex: SOLANA_CHAIN_ID,
address: userAddress
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(broadcastData);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post(url, broadcastData, { headers });
if (response.data.code === '0') {
const orderId = response.data.data[0].orderId;
console.log(`Transaction broadcast successfully, Order ID: ${orderId}`);
return orderId;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to broadcast transaction:', error);
throw error;
}
}
Using Standard RPC
async function signAndBroadcastTransaction(
tx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction,
connection: Connection
) {
if (!userPrivateKey) {
throw new Error("Private key not found");
}
const feePayer = solanaWeb3.Keypair.fromSecretKey(
base58.decode(userPrivateKey)
);
// Sign the transaction
if (tx instanceof solanaWeb3.VersionedTransaction) {
tx.sign([feePayer]);
} else {
tx.partialSign(feePayer);
}
// Send the transaction with retry logic
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const txId = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
preflightCommitment: 'processed',
maxRetries: 0 // Handle retries manually
});
console.log(`Transaction sent: ${txId}`);
// Wait for confirmation with timeout
const confirmation = await connection.confirmTransaction({
signature: txId,
blockhash: tx instanceof solanaWeb3.VersionedTransaction
? tx.message.recentBlockhash
: tx.recentBlockhash!,
lastValidBlockHeight: tx instanceof solanaWeb3.VersionedTransaction
? undefined
: tx.lastValidBlockHeight!
}, 'confirmed');
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log(`Transaction confirmed: https://solscan.io/tx/${txId}`);
return txId;
} catch (error) {
attempt++;
console.warn(`Attempt ${attempt} failed:`, error);
if (attempt >= maxRetries) {
throw new Error(`Transaction failed after ${maxRetries} attempts: ${error}`);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
5.4 Transaction Execution Flow Using Compute Unit Data
Here's a complete example that demonstrates the full flow from getting swap data to preparing transactions with proper compute unit estimation:
import { getHeaders } from '../../shared';
import {
Connection,
VersionedTransaction,
ComputeBudgetProgram,
TransactionMessage
} from "@solana/web3.js";
import base58 from 'bs58';
import dotenv from 'dotenv';
dotenv.config();
// Simple connection to one RPC endpoint
const connection = new Connection(
process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);
async function getQuote(params: any) {
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const queryString = "?" + new URLSearchParams({
...params,
}).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
const response = await fetch(`https://web3.okx.com${requestPath}${queryString}`, {
method: "GET",
headers
});
const data = await response.json();
return data;
}
/**
* Get compute units using Onchain Gateway API (Enterprise only)
*/
async function getComputeUnitsFromAPI(
fromAddress: string,
inputData: string
): Promise<number> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: "501", // Solana chain ID
fromAddress: fromAddress,
toAddress: "", // Can be empty for Solana
txAmount: "0",
extJson: {
inputData: inputData
}
};
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await fetch(url, {
method: 'POST',
headers,
body: bodyString
});
const data = await response.json();
if (data.code === '0') {
const computeUnits = parseInt(data.data[0].gasLimit);
console.log(`API estimated compute units: ${computeUnits}`);
return computeUnits;
} else {
throw new Error(`API Error: ${data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get compute units from API, falling back to simulation:', error);
// Fallback to RPC simulation
return 300000;
}
}
/**
* Execute a complete Solana transaction with proper compute unit estimation
*/
async function executeTransaction(): Promise<{
quote: any;
gasData: {
estimatedComputeUnits: number;
priorityFee: number;
blockhash: string;
};
preparedTransaction: string;
}> {
try {
console.log('Getting Solana swap data...');
// Step 1: Get swap data from OKX DEX API
const quote = await getQuote({
chainId: '501', // Solana chain ID
amount: '10000000', // 0.01 SOL in lamports
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
userWalletAddress: "YOUR_WALLET_ADDRESS",
slippage: '0.1',
autoSlippage: "true",
maxAutoSlippageBps: "100"
});
console.log('Quote response:', JSON.stringify(quote, null, 2));
// Step 2: Process transaction data if available
if (quote.data && quote.data[0] && quote.data[0].tx && quote.data[0].tx.data) {
console.log('\nGetting gas data for transaction...');
// Step 3: Create transaction from the data
const decodedTransaction = base58.decode(quote.data[0].tx.data);
const transaction = VersionedTransaction.deserialize(decodedTransaction);
// Step 4: Get compute units using API (Enterprise) or fallback to RPC
const userWalletAddress = "YOUR_WALLET_ADDRESS";
let estimatedComputeUnits: number;
try {
// Try API first (Enterprise customers)
estimatedComputeUnits = await getComputeUnitsFromAPI(
userWalletAddress,
quote.data[0].tx.data
);
console.log('Using API estimate for compute units');
} catch (error) {
// Fallback to RPC simulation
estimatedComputeUnits = await getComputeUnits(transaction);
console.log('Using RPC simulation for compute units');
}
// Step 5: Prepare transaction with compute units
const { transaction: preparedTransaction, gasData } = await prepareTransactionWithComputeUnits(transaction);
// Override with API estimate if we got one
gasData.estimatedComputeUnits = estimatedComputeUnits;
console.log('\nGas Data Summary:');
console.log('Blockhash:', gasData.blockhash);
console.log('Estimated Compute Units:', gasData.estimatedComputeUnits);
console.log('Priority Fee:', gasData.priorityFee, 'microLamports');
console.log('Transaction prepared successfully');
// Return the complete result
const result = {
quote: quote.data[0],
gasData,
preparedTransaction: Buffer.from(preparedTransaction.serialize()).toString('base64')
};
console.log('\nFinal Result:', JSON.stringify(result, null, 2));
return result;
} else {
throw new Error('No transaction data received from swap API');
}
} catch (error) {
console.error("Error executing transaction:", error);
throw error;
}
}
// Example usage
async function main() {
try {
await executeTransaction();
} catch (error) {
console.error('Failed to prepare transaction:', error);
process.exit(1);
}
}
// Run if this file is executed directly
if (require.main === module) {
main();
}
export { executeTransaction, prepareTransactionWithComputeUnits, getComputeUnits, getComputeUnitsFromAPI };
6. Track Transaction#
Finally, create a transaction tracking system:
With the Onchain gateway API
This function uses the Onchain gateway API. This API is available to our enterprise customers only. If you are interested, please contact us dexapi@okx.com.
// Define transaction status interface
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
/**
* Tracking transaction confirmation status using the Onchain gateway API
* @param orderId - Order ID from broadcast response
* @param intervalMs - Polling interval in milliseconds
* @param timeoutMs - Maximum time to wait
* @returns Final transaction confirmation status
*/
async function trackTransaction(
orderId: string,
intervalMs: number = 5000,
timeoutMs: number = 300000
): Promise<any> {
console.log(`Tracking transaction with Order ID: ${orderId}`);
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
// Get transaction status
try {
const path = 'dex/post-transaction/orders';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
orderId: orderId,
chainIndex: SOLANA_CHAIN_ID,
address: userAddress,
limit: '1'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0' && response.data.data && response.data.data.length > 0) {
if (response.data.data[0].orders && response.data.data[0].orders.length > 0) {
const txData = response.data.data[0].orders[0];
// Use txStatus to match the API response
const status = txData.txStatus;
// Only log when status changes
if (status !== lastStatus) {
lastStatus = status;
if (status === '1') {
console.log(`Transaction pending: ${txData.txHash || 'Hash not available yet'}`);
} else if (status === '2') {
console.log(`Transaction successful: https://solscan.io/tx/${txData.txHash}`);
return txData;
} else if (status === '3') {
const failReason = txData.failReason || 'Unknown reason';
const errorMessage = `Transaction failed: ${failReason}`;
console.error(errorMessage);
const errorInfo = handleTransactionError(txData);
console.log(`Error type: ${errorInfo.error}`);
console.log(`Suggested action: ${errorInfo.action}`);
throw new Error(errorMessage);
}
}
} else {
console.log(`No orders found for Order ID: ${orderId}`);
}
}
} catch (error) {
console.warn('Error checking transaction status:', (error instanceof Error ? error.message : "Unknown error"));
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Transaction tracking timed out');
}
/**
* Comprehensive error handling with failReason
* @param txData - Transaction data from post-transaction/orders
* @returns Structured error information
*/
function handleTransactionError(txData: any): TxErrorInfo {
const failReason = txData.failReason || 'Unknown reason';
// Log the detailed error
console.error(`Transaction failed with reason: ${failReason}`);
// Default error info
let errorInfo: TxErrorInfo = {
error: 'TRANSACTION_FAILED',
message: failReason,
action: 'Try again or contact support'
};
// More specific error handling based on the failure reason
if (failReason.includes('insufficient funds')) {
errorInfo = {
error: 'INSUFFICIENT_FUNDS',
message: 'Your wallet does not have enough funds to complete this transaction',
action: 'Add more SOL to your wallet to cover the transaction'
};
} else if (failReason.includes('blockhash')) {
errorInfo = {
error: 'BLOCKHASH_EXPIRED',
message: 'The transaction blockhash has expired',
action: 'Try again with a fresh transaction'
};
} else if (failReason.includes('compute budget')) {
errorInfo = {
error: 'COMPUTE_BUDGET_EXCEEDED',
message: 'Transaction exceeded compute budget',
action: 'Increase compute units or simplify the transaction'
};
}
return errorInfo;
}
For more detailed swap-specific information, you can use the SWAP API:
/**
* Track transaction using SWAP API
* @param chainId - Chain ID (e.g., 501 for Solana)
* @param txHash - Transaction hash
* @returns Transaction details
*/
async function trackTransactionWithSwapAPI(
txHash: string
): Promise<any> {
try {
const path = 'dex/aggregator/history';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
chainId: SOLANA_CHAIN_ID,
txHash: txHash,
isFromMyProject: 'true'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
const txData = response.data.data[0];
const status = txData.status;
if (status === 'pending') {
console.log(`Transaction is still pending: ${txHash}`);
return { status: 'pending', details: txData };
} else if (status === 'success') {
console.log(`Transaction successful!`);
console.log(`From: ${txData.fromTokenDetails.symbol} - Amount: ${txData.fromTokenDetails.amount}`);
console.log(`To: ${txData.toTokenDetails.symbol} - Amount: ${txData.toTokenDetails.amount}`);
console.log(`Transaction Fee: ${txData.txFee}`);
console.log(`Explorer URL: https://solscan.io/tx/${txHash}`);
return { status: 'success', details: txData };
} else if (status === 'failure') {
console.error(`Transaction failed: ${txData.errorMsg || 'Unknown reason'}`);
return { status: 'failure', details: txData };
}
return txData;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to track transaction status:', (error instanceof Error ? error.message : "Unknown error"));
throw error;
}
}
The Onchain gateway API provides transaction tracking capabilities through the /dex/post-transaction/orders
endpoint. This functionality enables tracking transactions as they progress through OKX's systems using order IDs and simple status codes (1: Pending, 2: Success, 3: Failed).
SWAP API transaction tracking provides comprehensive swap execution details using the /dex/aggregator/history
endpoint. It offers token-specific information (symbols, amounts), fees paid, and detailed blockchain data. Use this when you need complete swap insight with token-level details.
Choose the first for basic transaction confirmation status, and the second when you need detailed information about the swap execution itself.
7. Complete Implementation#
Here's a complete implementation example:
import { getHeaders } from '../../shared';
import { Connection, PublicKey, Transaction, Keypair, VersionedTransaction, SystemProgram } from '@solana/web3.js';
import * as axios from 'axios';
import bs58 from 'bs58';
// // Utility function for OKX API authentication
// function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
// const stringToSign = timestamp + method + requestPath + (queryString || body);
// return {
// "Content-Type": "application/json",
// "OK-ACCESS-KEY": apiKey,
// "OK-ACCESS-SIGN": cryptoJS.enc.Base64.stringify(
// cryptoJS.HmacSHA256(stringToSign, secretKey)
// ),
// "OK-ACCESS-TIMESTAMP": timestamp,
// "OK-ACCESS-PASSPHRASE": apiPassphrase,
// "OK-ACCESS-PROJECT": projectId,
// };
// Environment variables
const WALLET_ADDRESS = process.env.SOLANA_WALLET_ADDRESS;
const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY;
const chainId = '501'; // Solana Mainnet
const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
// Constants
const SOL_ADDRESS = '11111111111111111111111111111111'; // Native SOL
const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC
// Initialize Solana connection
const connection = new Connection(rpcUrl, 'confirmed');
// Type definitions
interface GasLimitApiResponse {
code: string;
msg?: string;
data: Array<{
gasLimit: string;
}>;
}
interface SimulationApiResponse {
code: string;
msg?: string;
data: Array<{
intention: string;
gasUsed?: string;
failReason?: string;
assetChange?: Array<{
assetType: string;
name: string;
symbol: string;
decimals: number;
address: string;
imageUrl: string;
rawValue: string;
}>;
risks?: Array<any>;
}>;
}
interface BroadcastApiResponse {
code: string;
msg?: string;
data: Array<{
orderId: string;
}>;
}
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
// ============================================================================
// API Functions
// ============================================================================
/**
* Get gas limit from Onchain Gateway API
*/
async function getGasLimit(
fromAddress: string,
toAddress: string,
txAmount: string = '0',
inputData: string = ''
): Promise<string> {
try {
console.log('Getting gas limit from Onchain Gateway API...');
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: chainId,
fromAddress: fromAddress,
toAddress: toAddress,
txAmount: txAmount,
extJson: {
inputData: inputData
}
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post<GasLimitApiResponse>(url, body, { headers });
if (response.data.code === '0') {
const gasLimit = response.data.data[0].gasLimit;
console.log(`Gas Limit obtained: ${gasLimit}`);
return gasLimit;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get gas limit:', (error as Error).message);
throw error;
}
}
/**
* Get swap data from OKX API
*/
async function getSwapData(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage = '0.5'
) {
try {
console.log('Getting swap data from OKX API...');
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const queryString = "?" + new URLSearchParams({
chainIndex: chainId,
fromTokenAddress,
toTokenAddress,
amount,
slippage,
userWalletAddress: WALLET_ADDRESS!,
autoSlippage: "false",
maxAutoSlippageBps: "0"
}).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
const response = await fetch(`https://web3.okx.com${requestPath}${queryString}`, {
method: "GET",
headers
});
if (!response.ok) {
throw new Error(`Failed to get swap data: ${response.status} ${await response.text()}`);
}
const data = await response.json();
console.log('Swap data obtained');
return data.data[0]; // Return only the first swap data object
} catch (error) {
console.error('Failed to get swap data:', (error as Error).message);
throw error;
}
}
/**
* Simulate transaction using Onchain Gateway API
*/
async function simulateTransaction(swapData: any) {
try {
console.log('Simulating transaction with Onchain Gateway API...');
const path = 'dex/pre-transaction/simulate';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: chainId,
fromAddress: swapData.tx.from,
toAddress: swapData.tx.to,
txAmount: swapData.tx.value,
extJson: {
inputData: swapData.tx.data
}
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post<SimulationApiResponse>(url, body, { headers });
if (response.data.code === '0') {
const simulationData = response.data.data[0];
if (simulationData.failReason) {
throw new Error(`Simulation failed: ${simulationData.failReason}`);
}
console.log(`Transaction simulation successful. Gas used: ${simulationData.gasUsed}`);
console.log('Simulation API Response:', simulationData);
return simulationData;
} else {
throw new Error(`Simulation API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Transaction simulation failed:', (error as Error).message);
throw error;
}
}
/**
* Broadcast transaction using Onchain Gateway API with RPC fallback
*/
async function broadcastTransaction(
signedTx: string,
chainId: string,
walletAddress: string
): Promise<string> {
try {
console.log('Broadcasting transaction via Onchain Gateway API...');
const path = 'dex/pre-transaction/broadcast-transaction';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
signedTx: signedTx,
chainIndex: chainId,
address: walletAddress
};
console.log('Broadcast request body:', JSON.stringify(body, null, 2));
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(body);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post<BroadcastApiResponse>(url, body, { headers });
if (response.data.code === '0') {
const orderId = response.data.data[0].orderId;
console.log(`Transaction broadcast successful. Order ID: ${orderId}`);
return orderId;
} else {
throw new Error(`Broadcast API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('OKX API broadcast failed:', (error as Error).message);
// Fallback to direct RPC broadcast
try {
console.log('Attempting direct RPC broadcast as fallback...');
// Decode the signed transaction
const txBytes = bs58.decode(signedTx);
// Send directly to Solana RPC
const signature = await connection.sendRawTransaction(txBytes, {
skipPreflight: false,
preflightCommitment: 'processed'
});
console.log(`Direct RPC broadcast successful. Signature: ${signature}`);
// Wait for confirmation
const confirmation = await connection.confirmTransaction(signature, 'confirmed');
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log(`Transaction confirmed: https://solscan.io/tx/${signature}`);
return signature;
} catch (rpcError) {
console.error('RPC broadcast also failed:', (rpcError as Error).message);
throw new Error(`Both OKX API and RPC broadcast failed. OKX Error: ${(error as Error).message}, RPC Error: ${(rpcError as Error).message}`);
}
}
}
/**
* Track transaction status using Onchain Gateway API
*/
async function trackTransaction(
orderId: string,
intervalMs: number = 5000,
timeoutMs: number = 180000 // Reduced timeout to 3 minutes
): Promise<any> {
console.log(`Tracking transaction with Order ID: ${orderId}`);
const startTime = Date.now();
let lastStatus = '';
let pendingCount = 0;
while (Date.now() - startTime < timeoutMs) {
try {
const path = 'dex/post-transaction/orders';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
orderId: orderId,
chainIndex: chainId,
address: WALLET_ADDRESS!,
limit: '1'
};
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
const responseData = response.data as any;
if (responseData.code === '0' && responseData.data && responseData.data.length > 0) {
if (responseData.data[0].orders && responseData.data[0].orders.length > 0) {
const txData = responseData.data[0].orders[0];
const status = txData.txStatus;
if (status !== lastStatus) {
lastStatus = status;
if (status === '1') {
pendingCount++;
console.log(`Transaction pending (${pendingCount}): ${txData.txHash || 'Hash not available yet'}`);
// If pending too long without a hash, something is wrong
if (pendingCount > 12 && !txData.txHash) { // 1 minute of pending without hash
console.warn('Transaction has been pending for too long without a transaction hash. This may indicate an issue.');
}
} else if (status === '2') {
console.log(`Transaction successful: https://web3.okx.com/explorer/solana/tx/${txData.txHash}`);
return txData;
} else if (status === '3') {
const failReason = txData.failReason || 'Unknown reason';
const errorMessage = `Transaction failed: ${failReason}`;
console.error(errorMessage);
const errorInfo = handleTransactionError(txData);
console.log(`Error type: ${errorInfo.error}`);
console.log(`Suggested action: ${errorInfo.action}`);
throw new Error(errorMessage);
}
} else if (status === '1') {
pendingCount++;
// Show progress for long pending transactions
if (pendingCount % 6 === 0) { // Every 30 seconds
const elapsed = Math.round((Date.now() - startTime) / 1000);
console.log(`Still pending... (${elapsed}s elapsed)`);
}
}
} else {
console.log(`No orders found for Order ID: ${orderId}`);
}
} else {
console.log('No response data from tracking API');
}
} catch (error) {
console.warn('Error checking transaction status:', (error as Error).message);
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error(`Transaction tracking timed out after ${timeoutMs/1000} seconds. The transaction may still be processing.`);
}
// ============================================================================
// Transaction Signing Functions
// ============================================================================
/**
* Sign transaction with private key - Fixed OKX approach with gas limit analysis
*/
async function signTransaction(swapData: any, gasLimit: string): Promise<string> {
try {
console.log('Signing transaction...');
if (!PRIVATE_KEY) {
throw new Error('Private key not found in environment variables');
}
// Create keypair from private key
const privateKeyBytes = bs58.decode(PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);
if (!swapData.tx || !swapData.tx.data) {
throw new Error('No transaction data found in swap response');
}
const callData = swapData.tx.data;
console.log('Transaction data length:', callData.length);
console.log('Gas limit from API:', gasLimit);
try {
// Decode the base58 encoded transaction data (this is the correct approach)
const decodedTransaction = bs58.decode(callData);
console.log('Decoded transaction bytes length:', decodedTransaction.length);
// Get the latest blockhash (CRITICAL!)
const recentBlockHash = await connection.getLatestBlockhash();
console.log('Got recent blockhash:', recentBlockHash.blockhash);
let transaction: Transaction | VersionedTransaction;
// Try VersionedTransaction first (more common for modern Solana programs)
try {
transaction = VersionedTransaction.deserialize(decodedTransaction);
console.log('Successfully deserialized as VersionedTransaction');
// DEBUGGING: Let's see what instructions are already in the transaction
console.log('Number of instructions in OKX transaction:', transaction.message.compiledInstructions.length);
// Check if there are already ComputeBudget instructions
const computeBudgetProgram = new PublicKey('ComputeBudget111111111111111111111111111111');
const computeBudgetIndex = transaction.message.staticAccountKeys.findIndex(
key => key.equals(computeBudgetProgram)
);
if (computeBudgetIndex !== -1) {
console.log('ComputeBudget program found at index:', computeBudgetIndex);
// Check which instructions use the ComputeBudget program
const computeBudgetInstructions = transaction.message.compiledInstructions.filter(
ix => ix.programIdIndex === computeBudgetIndex
);
console.log('Number of ComputeBudget instructions:', computeBudgetInstructions.length);
// Analyze each ComputeBudget instruction
computeBudgetInstructions.forEach((ix, i) => {
const data = ix.data;
if (data.length > 0) {
const instructionType = data[0];
console.log(`ComputeBudget instruction ${i}: type ${instructionType}`);
if (instructionType === 0 && data.length >= 5) {
// SetComputeUnitLimit instruction
const computeUnits = new Uint32Array(data.slice(1, 5).buffer)[0];
console.log(` - Current compute unit limit: ${computeUnits}`);
console.log(` - Gas limit from API: ${gasLimit}`);
// Check if we need to update it
const apiGasLimit = parseInt(gasLimit);
if (computeUnits !== apiGasLimit) {
console.log(` - Compute units mismatch! OKX: ${computeUnits}, API: ${apiGasLimit}`);
// We could potentially update this here
}
} else if (instructionType === 1 && data.length >= 9) {
// SetComputeUnitPrice instruction
const microLamports = new BigUint64Array(data.slice(1, 9).buffer)[0];
console.log(` - Current compute unit price: ${microLamports} microlamports`);
}
}
});
} else {
console.log('No ComputeBudget program found - OKX transaction may not have compute budget instructions');
console.log('We should add ComputeBudget instruction with gas limit:', gasLimit);
// Add ComputeBudget instruction since OKX didn't include one
const setComputeUnitLimitData = Buffer.alloc(5);
setComputeUnitLimitData[0] = 0; // SetComputeUnitLimit instruction
setComputeUnitLimitData.writeUInt32LE(parseInt(gasLimit), 1);
// Add the ComputeBudget program to static accounts
transaction.message.staticAccountKeys.push(computeBudgetProgram);
const programIndex = transaction.message.staticAccountKeys.length - 1;
// Add the compute budget instruction at the beginning
transaction.message.compiledInstructions.unshift({
programIdIndex: programIndex,
accountKeyIndexes: [],
data: setComputeUnitLimitData
});
console.log('Added ComputeBudget instruction with gas limit:', gasLimit);
}
// CRITICAL: Update the blockhash in the transaction message
transaction.message.recentBlockhash = recentBlockHash.blockhash;
// Sign the versioned transaction
transaction.sign([keypair]);
console.log('Signed VersionedTransaction');
} catch (versionedError) {
console.log('VersionedTransaction failed, trying legacy Transaction');
try {
transaction = Transaction.from(decodedTransaction);
console.log('Successfully deserialized as legacy Transaction');
// DEBUGGING: Check legacy transaction instructions
console.log('Number of instructions in legacy transaction:', transaction.instructions.length);
// Check for ComputeBudget instructions in legacy format
const computeBudgetProgram = new PublicKey('ComputeBudget111111111111111111111111111111');
const computeBudgetInstructions = transaction.instructions.filter(
ix => ix.programId.equals(computeBudgetProgram)
);
if (computeBudgetInstructions.length === 0) {
console.log('No ComputeBudget instructions found in legacy transaction');
console.log('Adding ComputeBudget instruction with gas limit:', gasLimit);
// Add ComputeBudget instruction
const setComputeUnitLimitData = Buffer.alloc(5);
setComputeUnitLimitData[0] = 0; // SetComputeUnitLimit instruction
setComputeUnitLimitData.writeUInt32LE(parseInt(gasLimit), 1);
const computeBudgetIx = {
programId: computeBudgetProgram,
keys: [],
data: setComputeUnitLimitData
};
// Add at the beginning
transaction.instructions.unshift(computeBudgetIx);
console.log('Added ComputeBudget instruction to legacy transaction');
} else {
console.log('Found existing ComputeBudget instructions:', computeBudgetInstructions.length);
}
// CRITICAL: Update the blockhash in the transaction
transaction.recentBlockhash = recentBlockHash.blockhash;
// Sign the legacy transaction
transaction.sign(keypair);
console.log('Signed legacy Transaction');
} catch (legacyError) {
console.log('Both transaction types failed to deserialize');
console.log('VersionedTransaction error:', (versionedError as Error).message);
console.log('Legacy Transaction error:', (legacyError as Error).message);
// This should not happen with proper OKX data
throw new Error('Failed to deserialize OKX transaction data. Data may be corrupted.');
}
}
// Serialize and encode the signed transaction
const serializedTx = transaction.serialize();
const encodedTx = bs58.encode(serializedTx);
console.log('Transaction signed and encoded successfully');
return encodedTx;
} catch (error) {
console.log('Failed to process OKX transaction data:', (error as Error).message);
// If we reach here, the OKX data is not in expected format
throw new Error(`Cannot process OKX transaction data: ${(error as Error).message}`);
}
} catch (error) {
console.error('Failed to sign transaction:', (error as Error).message);
throw error;
}
}
// ============================================================================
// Error Handling
// ============================================================================
/**
* Comprehensive error handling with failReason
*/
function handleTransactionError(txData: any): TxErrorInfo {
const failReason = txData.failReason || 'Unknown reason';
console.error(`Transaction failed with reason: ${failReason}`);
return {
error: 'TRANSACTION_FAILED',
message: failReason,
action: 'Try again or contact support'
};
}
// ============================================================================
// Main Execution Functions
// ============================================================================
/**
* Execute swap with full transaction flow
*/
async function executeSwap(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.05'
): Promise<string> {
try {
console.log('Starting swap execution...');
// Step 1: Get swap data
const swapData = await getSwapData(fromTokenAddress, toTokenAddress, amount, slippage);
console.log('Swap data obtained');
// Step 2: Simulate transaction
const simulationResult = await simulateTransaction(swapData);
console.log('Transaction simulation completed');
console.log('Simulation result', simulationResult.intention);
// Step 3: Get gas limit
const gasLimit = await getGasLimit(
swapData.tx.from,
swapData.tx.to,
swapData.tx.value || '0',
swapData.tx.data
);
console.log('Gas limit obtained');
// Step 4: Check account balance
if (!(swapData.tx && swapData.tx.data)) {
throw new Error('No valid transaction data found in swap API response (tx.data missing)');
}
console.log('Checking account balance...');
const fromPubkey = new PublicKey(swapData.tx.from);
const balance = await connection.getBalance(fromPubkey);
console.log(`Account balance: ${balance / 1e9} SOL`);
// Check if we have enough balance for the transaction
const requiredAmount = parseInt(swapData.tx.value || '0');
console.log(`Required amount: ${requiredAmount / 1e9} SOL`);
if (balance < requiredAmount) {
throw new Error(`Insufficient balance. Required: ${requiredAmount / 1e9} SOL, Available: ${balance / 1e9} SOL`);
}
// Step 5: Sign the transaction with private key
console.log('Signing transaction with private key...');
const signedTx = await signTransaction(swapData, gasLimit);
console.log('Transaction signed successfully');
// Step 6: Broadcast transaction
console.log('Broadcasting signed transaction via Onchain Gateway API...');
const txHash = await broadcastTransaction(signedTx, chainId, WALLET_ADDRESS!);
console.log(`Transaction broadcast successful. Hash: ${txHash}`);
// Step 7: Track transaction
console.log('Tracking transaction status...');
const trackingResult = await trackTransaction(txHash);
console.log('Transaction tracking completed');
console.log('Tracking result', trackingResult);
return txHash;
} catch (error) {
console.error('Swap execution failed:', (error as Error).message);
throw error;
}
}
/**
* Execute swap with simulation and detailed logging
*/
async function executeSwapWithSimulation(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.05'
): Promise<any> {
try {
console.log('Starting swap execution with simulation...');
const txHash = await executeSwap(fromTokenAddress, toTokenAddress, amount, slippage);
console.log('Swap execution completed successfully!');
console.log(`Transaction Hash: ${txHash}`);
return { success: true, txHash };
} catch (error) {
console.error('Swap execution failed:', (error as Error).message);
return { success: false, error: (error as Error).message };
}
}
/**
* Simulation-only mode
*/
async function simulateOnly(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.05'
): Promise<any> {
try {
console.log('Starting simulation-only mode...');
console.log(`Simulation Details:`);
console.log(` From Token: ${fromTokenAddress}`);
console.log(` To Token: ${toTokenAddress}`);
console.log(` Amount: ${amount}`);
console.log(` Slippage: ${slippage}%`);
// Step 1: Get swap data
const swapData = await getSwapData(fromTokenAddress, toTokenAddress, amount, slippage);
console.log('Swap data obtained');
// Step 2: Simulate transaction
const simulationResult = await simulateTransaction(swapData);
console.log('Transaction simulation completed');
// Step 3: Get gas limit
const gasLimit = await getGasLimit(
swapData.tx.from,
swapData.tx.to,
swapData.tx.value || '0',
swapData.tx.data
);
console.log('Gas limit obtained');
return {
success: true,
swapData,
simulationResult,
gasLimit,
estimatedGasUsed: simulationResult.gasUsed,
};
} catch (error) {
console.error('Simulation failed:', (error as Error).message);
return { success: false, error: (error as Error).message };
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
async function main() {
try {
console.log('Solana Swap Tools with Onchain Gateway API');
console.log('=====================================');
// Validate environment variables
if (!WALLET_ADDRESS || !PRIVATE_KEY) {
throw new Error('Missing wallet address or private key in environment variables');
}
console.log(`Wallet Address: ${WALLET_ADDRESS}`);
console.log(`Chain ID: ${chainId}`);
console.log(`RPC URL: ${rpcUrl}`);
// Parse command line arguments
const args = process.argv.slice(2);
const mode = args[0] || 'simulate'; // Default to simulate mode
// Example parameters
const fromToken = SOL_ADDRESS;
const toToken = USDC_ADDRESS;
const amount = '10000000'; // 0.01 SOL in lamports
const slippage = '0.05'; // 0.5%
console.log('\nConfiguration:');
console.log(` From: ${fromToken} (SOL)`);
console.log(` To: ${toToken} (USDC)`);
console.log(` Amount: ${parseInt(amount) / 1e9} SOL`);
console.log(` Slippage: ${slippage}%`);
console.log(` Mode: ${mode}`);
let result;
switch (mode.toLowerCase()) {
case 'simulate':
case 'sim':
result = await simulateOnly(fromToken, toToken, amount, slippage);
break;
case 'execute':
case 'exec':
result = await executeSwapWithSimulation(fromToken, toToken, amount, slippage);
break;
default:
console.log('\nAvailable modes:');
console.log(' simulate/sim - Only simulate the transaction');
console.log(' execute/exec - Execute the full swap');
console.log('\nExample: npm run solana-swap simulate');
return;
}
if (result.success) {
console.log('\nOperation completed successfully!');
if (mode === 'simulate' || mode === 'sim') {
console.log(`Gas Limit: ${result.gasLimit}`);
} else {
console.log(`Transaction Hash: ${result.txHash}`);
}
} else {
console.log('\nOperation failed!');
console.log(`Error: ${result.error}`);
}
} catch (error) {
console.error('Main execution failed:', (error as Error).message);
process.exit(1);
}
}
// Run the script
if (require.main === module) {
main();
}
// ============================================================================
// Exports
// ============================================================================
export {
executeSwap,
executeSwapWithSimulation,
simulateOnly,
getSwapData,
simulateTransaction,
getGasLimit,
broadcastTransaction,
trackTransaction,
signTransaction
};
You can run this script using solana-swap-executor.ts sim
or solana-swap-executor.ts exec
.
sim
simulates a transaction using swap data using the transaction simulation API and retruns gasLimit
info
exec
executes a transaction using the broadcast API
8. MEV Protection#
The first line of defense uses dynamic priority fees - think of it as your bid in an auction against MEV bots:
const MEV_PROTECTION = {
// Trade Protection
MAX_PRICE_IMPACT: "0.05", // 5% max price impact
SLIPPAGE: "0.05", // 5% slippage tolerance
MIN_ROUTES: 2, // Minimum DEX routes
// Priority Fees
MIN_PRIORITY_FEE: 10_000,
MAX_PRIORITY_FEE: 1_000_000,
PRIORITY_MULTIPLIER: 2,
// TWAP Settings
TWAP_ENABLED: true,
TWAP_INTERVALS: 4, // Split into 4 parts
TWAP_DELAY_MS: 2000, // 2s between trades
// Transaction Settings
COMPUTE_UNITS: 300_000,
MAX_RETRIES: 3,
CONFIRMATION_TIMEOUT: 60_000,
// Block Targeting
TARGET_SPECIFIC_BLOCKS: true,
PREFERRED_SLOT_OFFSET: 2, // Target blocks with slot % 4 == 2
} as const;
static async getPriorityFee() {
const recentFees = await connection.getRecentPrioritizationFees();
const maxFee = Math.max(...recentFees.map(fee => fee.prioritizationFee));
return Math.min(maxFee * 1.5, MEV_PROTECTION.MAX_PRIORITY_FEE);
}
For larger trades, you can enable TWAP (Time-Weighted Average Price). Instead of making one big splash that MEV bots love to target, your trade gets split into smaller pieces:
// Define the TWAPExecution class outside of the if block
class TWAPExecution {
static async splitTrade(
totalAmount: string,
fromTokenAddress: string,
toTokenAddress: string
): Promise<TradeChunk[]> {
const amount = new BN(totalAmount);
const chunkSize = amount.divn(MEV_PROTECTION.TWAP_INTERVALS);
return Array(MEV_PROTECTION.TWAP_INTERVALS)
.fill(null)
.map(() => ({
amount: chunkSize.toString(),
fromTokenAddress,
toTokenAddress,
minAmountOut: "0" // Will be calculated per chunk
}));
}
}
// Then use it in the if block
if (MEV_PROTECTION.TWAP_ENABLED) {
const chunks = await TWAPExecution.splitTrade(
rawAmount,
fromTokenAddress,
toTokenAddress
);
}
Protection in Action#
When you execute a trade with this implementation, several things happen:
- Pre-Trade Checks:
- The token you're buying gets checked for honeypot characteristics
- Network fees are analyzed to set competitive priority
- Your trade size determines if it should be split up
- During the Trade:
- Large trades can be split into parts with randomized timing
- Each piece gets its own priority fee based on market conditions
- Specific block targeting helps reduce exposure
- Transaction Safety:
- Each transaction runs through simulation first
- Built-in confirmation tracking
- Automatic retry logic if something goes wrong
While MEV on Solana can't be completely eliminated, these protections make life harder for MEV bots.
Method 2: SDK approach#
Using the OKX DEX SDK provides a much simpler developer experience while retaining all the functionality of the API-first approach. The SDK handles many implementation details for you, including retry logic, error handling, and transaction management.
1. Install the SDK#
npm install @okx-dex/okx-dex-sdk
# or
yarn add @okx-dex/okx-dex-sdk
# or
pnpm add @okx-dex/okx-dex-sdk
2. Setup Your Environment#
Create a .env file with your API credentials and wallet information:
# OKX API Credentials
OKX_API_KEY=your_api_key
OKX_SECRET_KEY=your_secret_key
OKX_API_PASSPHRASE=your_passphrase
OKX_PROJECT_ID=your_project_id
# Solana Configuration
SOLANA_RPC_URL=your_solana_rpc_url
SOLANA_WALLET_ADDRESS=your_solana_wallet_address
SOLANA_PRIVATE_KEY=your_solana_private_key
3. Initialize the Client#
Create a file for your DEX client (e.g., DexClient.ts):
// DexClient.ts
import { OKXDexClient } from '@okx-dex/okx-dex-sdk';
import 'dotenv/config';
// Validate environment variables
const requiredEnvVars = [
'OKX_API_KEY',
'OKX_SECRET_KEY',
'OKX_API_PASSPHRASE',
'OKX_PROJECT_ID',
'SOLANA_WALLET_ADDRESS',
'SOLANA_PRIVATE_KEY',
'SOLANA_RPC_URL'
];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
// Initialize the client
export const client = new OKXDexClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
apiPassphrase: process.env.OKX_API_PASSPHRASE!,
projectId: process.env.OKX_PROJECT_ID!,
solana: {
connection: {
rpcUrl: process.env.SOLANA_RPC_URL!,
wsEndpoint: process.env.SOLANA_WS_URL,
confirmTransactionInitialTimeout: 5000
},
walletAddress: process.env.SOLANA_WALLET_ADDRESS!,
privateKey: process.env.SOLANA_PRIVATE_KEY!,
computeUnits: 300000,
maxRetries: 3
}
});
4. Execute a Swap With the SDK#
Create a swap execution file:
// swap.ts
import { client } from './DexClient';
/**
* Example: Execute a swap from SOL to USDC
*/
async function executeSwap() {
try {
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('Missing SOLANA_PRIVATE_KEY in .env file');
}
// Get quote to fetch token information
console.log("Getting token information...");
const quote = await client.dex.getQuote({
chainId: '501',
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: '1000000', // Small amount for quote
slippage: '0.005' // 0.5% slippage
});
const tokenInfo = {
fromToken: {
symbol: quote.data[0].fromToken.tokenSymbol,
decimals: parseInt(quote.data[0].fromToken.decimal),
price: quote.data[0].fromToken.tokenUnitPrice
},
toToken: {
symbol: quote.data[0].toToken.tokenSymbol,
decimals: parseInt(quote.data[0].toToken.decimal),
price: quote.data[0].toToken.tokenUnitPrice
}
};
// Convert amount to base units (for display purposes)
const humanReadableAmount = 0.1; // 0.1 SOL
const rawAmount = (humanReadableAmount * Math.pow(10, tokenInfo.fromToken.decimals)).toString();
console.log("\nSwap Details:");
console.log("--------------------");
console.log(`From: ${tokenInfo.fromToken.symbol}`);
console.log(`To: ${tokenInfo.toToken.symbol}`);
console.log(`Amount: ${humanReadableAmount} ${tokenInfo.fromToken.symbol}`);
console.log(`Amount in base units: ${rawAmount}`);
console.log(`Approximate USD value: $${(humanReadableAmount * parseFloat(tokenInfo.fromToken.price)).toFixed(2)}`);
// Execute the swap
console.log("\nExecuting swap...");
const swapResult = await client.dex.executeSwap({
chainId: '501', // Solana chain ID
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: rawAmount,
slippage: '0.005', // 0.5% slippage
userWalletAddress: process.env.SOLANA_WALLET_ADDRESS!
});
console.log('Swap executed successfully:');
console.log(JSON.stringify(swapResult, null, 2));
return swapResult;
} catch (error) {
if (error instanceof Error) {
console.error('Error executing swap:', error.message);
// API errors include details in the message
if (error.message.includes('API Error:')) {
const match = error.message.match(/API Error: (.*)/);
if (match) console.error('API Error Details:', match[1]);
}
}
throw error;
}
}
// Run if this file is executed directly
if (require.main === module) {
executeSwap()
.then(() => process.exit(0))
.catch((error) => {
console.error('Error:', error);
process.exit(1);
});
}
export { executeSwap };
5. Additional SDK Functionality#
The SDK provides additional methods that simplify development:
Get a quote for a token pair
const quote = await client.dex.getQuote({
chainId: '501', // Solana
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: '100000000', // 0.1 SOL (in lamports)
slippage: '0.005' // 0.5% slippage
});
- Method 1: API-First Approach1. Set Up Your Environment2. Get Swap Data3. Prepare Transaction4. Simulate Transaction5. Broadcast Transaction6. Track Transaction7. Complete Implementation8. MEV ProtectionMethod 2: SDK approach1. Install the SDK2. Setup Your Environment3. Initialize the Client4. Execute a Swap With the SDK5. Additional SDK Functionality