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.5'
) {
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. Broadcast Transaction#
With RPC
async function signTransaction(tx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction) {
if (!userPrivateKey) {
throw new Error("Private key not found");
}
const feePayer = solanaWeb3.Keypair.fromSecretKey(
base58.decode(userPrivateKey)
);
if (tx instanceof solanaWeb3.VersionedTransaction) {
tx.sign([feePayer]);
} else {
tx.partialSign(feePayer);
}
const txId = await connection.sendRawTransaction(tx.serialize());
console.log('Transaction ID:', txId);
// Wait for confirmation
await connection.confirmTransaction(txId);
console.log(`Transaction confirmed: https://solscan.io/tx/${txId}`);
return txId;
}
With Onchain gateway API
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;
}
}
5. Track Transaction#
Finally, create a transaction tracking system:
With the Onchain gateway API
// 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.
6. Complete Implementation#
Here's a complete implementation example:
// solana-swap.ts
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"; // Solana Mainnet
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;
const CONFIRMATION_TIMEOUT = 60000;
const POLLING_INTERVAL = 5000;
const BASE_URL = "https://web3.okx.com";
const DEX_PATH = "api/v5/dex";
// Initialize Solana connection
const connection = new Connection(solanaRpcUrl || "https://api.mainnet-beta.solana.com", {
confirmTransactionInitialTimeout: 30000
});
// ======== Utility Functions ========
/**
* Generate API authentication headers
*/
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
throw new Error("Missing required environment variables for API authentication");
}
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,
};
}
/**
* Convert human-readable amount to the smallest token units
*/
function convertAmount(amount: string, decimals: number): string {
try {
if (!amount || isNaN(parseFloat(amount))) {
throw new Error("Invalid amount");
}
const value = parseFloat(amount);
if (value <= 0) {
throw new Error("Amount must be greater than 0");
}
return new BN(value * Math.pow(10, decimals)).toString();
} catch (err) {
console.error("Amount conversion error:", err);
throw new Error("Invalid amount format");
}
}
/**
* Get token information from the API
*/
async function getTokenInfo(fromTokenAddress: string, toTokenAddress: string) {
const timestamp = new Date().toISOString();
const path = `dex/aggregator/quote`;
const requestPath = `/api/v5/${path}`;
const params: Record<string, string> = {
chainId: SOLANA_CHAIN_ID,
fromTokenAddress,
toTokenAddress,
amount: "1000000", // Small amount just to get token info
slippage: "0.5",
};
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
try {
const response = await axios.get(
`${BASE_URL}${requestPath}${queryString}`,
{ headers }
);
if (response.data.code !== "0" || !response.data.data?.[0]) {
throw new Error("Failed to get token information");
}
const quoteData = response.data.data[0];
return {
fromToken: {
symbol: quoteData.fromToken.tokenSymbol,
decimals: parseInt(quoteData.fromToken.decimal),
price: quoteData.fromToken.tokenUnitPrice
},
toToken: {
symbol: quoteData.toToken.tokenSymbol,
decimals: parseInt(quoteData.toToken.decimal),
price: quoteData.toToken.tokenUnitPrice
}
};
} catch (error) {
console.error("Error fetching token information:", error);
throw error;
}
}
// ===== Swap Functions =====
/**
* Get swap data from the API
*/
async function getSwapData(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage = '0.5'
) {
const timestamp = new Date().toISOString();
const path = `dex/aggregator/swap`;
const requestPath = `/api/v5/${path}`;
// Ensure all parameters are defined before creating URLSearchParams
const params: Record<string, string> = {
amount,
chainId: SOLANA_CHAIN_ID,
fromTokenAddress,
toTokenAddress,
slippage
};
// Only add userWalletAddress if it's defined
if (userAddress) {
params.userWalletAddress = userAddress;
}
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
try {
const response = await axios.get(
`${BASE_URL}${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;
}
}
/**
* Prepare the transaction with the latest blockhash and compute units
*/
async function prepareTransaction(callData: string) {
try {
// Decode the base58 encoded transaction data
const decodedTransaction = base58.decode(callData);
// Get the latest blockhash for transaction freshness
const recentBlockHash = await connection.getLatestBlockhash();
console.log("Got blockhash:", recentBlockHash.blockhash);
let tx;
// Try to deserialize as a versioned transaction first (Solana v0 transaction format)
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 format");
tx = solanaWeb3.Transaction.from(decodedTransaction);
console.log("Successfully created legacy transaction");
tx.recentBlockhash = recentBlockHash.blockhash;
// Add compute budget instruction for complex swaps (only for legacy transactions)
// For versioned transactions, this would already be included in the message
const computeBudgetIx = solanaWeb3.ComputeBudgetProgram.setComputeUnitLimit({
units: COMPUTE_UNITS
});
tx.add(computeBudgetIx);
}
return {
transaction: tx,
recentBlockHash
};
} catch (error) {
console.error("Error preparing transaction:", error);
throw error;
}
}
/**
* Sign the transaction with user's private key
*/
async function signTransaction(tx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction) {
if (!userPrivateKey) {
throw new Error("Private key not found");
}
const feePayer = solanaWeb3.Keypair.fromSecretKey(
base58.decode(userPrivateKey)
);
if (tx instanceof solanaWeb3.VersionedTransaction) {
tx.sign([feePayer]);
} else {
tx.partialSign(feePayer);
}
return tx;
}
/**
* Broadcast transaction using Onchain gateway API
*/
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 requestPath = `/api/v5/${path}`;
// Ensure all parameters are defined
const broadcastData: Record<string, string> = {
signedTx: encodedTx,
chainIndex: SOLANA_CHAIN_ID
};
// Only add address if it's defined
if (userAddress) {
broadcastData.address = userAddress;
}
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(broadcastData);
const timestamp = new Date().toISOString();
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post(`${BASE_URL}${requestPath}`, 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;
}
}
// ======== Transaction Tracking ========
// Define error info interface
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
/**
* Track transaction confirmation status using Onchain gateway API
* @param orderId - Order ID from broadcast response
* @param intervalMs - Polling interval in milliseconds
* @param timeoutMs - Maximum time to wait
* @returns Transaction confirmation status
*/
async function trackTransaction(
orderId: string,
intervalMs: number = POLLING_INTERVAL,
timeoutMs: number = CONFIRMATION_TIMEOUT
): Promise<any> {
console.log(`Track 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 requestPath = `/api/v5/${path}`;
// Ensure all parameters are defined before creating URLSearchParams
const params: Record<string, string> = {
orderId: orderId,
chainIndex: SOLANA_CHAIN_ID,
limit: '1'
};
// Only add address if it's defined
if (userAddress) {
params.address = userAddress;
}
// Prepare authentication
const timestamp = new Date().toISOString();
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(`${BASE_URL}${requestPath}${queryString}`, { 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');
}
/**
* 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 handling
return {
error: 'TRANSACTION_FAILED',
message: failReason,
action: 'Try again or contact support'
};
}
// ======== Main Swap Execution Function ========
/**
* Execute a token swap on Solana
*/
async function executeSwap(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.5'
): Promise<any> {
try {
// Validate inputs
if (!userPrivateKey) {
throw new Error("Missing private key");
}
if (!userAddress) {
throw new Error("Missing wallet address");
}
// Step 1: Get swap data from OKX DEX API
console.log("Getting swap data...");
const swapData = await getSwapData(fromTokenAddress, toTokenAddress, amount, slippage);
console.log("Swap route obtained");
// Step 2: Get the transaction data
const callData = swapData.tx.data;
if (!callData) {
throw new Error("Invalid transaction data received from API");
}
// Step 3: Prepare the transaction with compute units
const { transaction, recentBlockHash } = await prepareTransaction(callData);
console.log("Transaction prepared with compute unit limit:", COMPUTE_UNITS);
// Step 4: Sign the transaction
const signedTx = await signTransaction(transaction);
console.log("Transaction signed");
// Step 5: Broadcast the transaction using Onchain gateway API
const orderId = await broadcastTransaction(signedTx);
console.log(`Transaction broadcast successful with order ID: ${orderId}`);
// Step 6: Track transaction confirmation status
console.log("Tracking transaction...");
const txStatus = await trackTransaction(orderId);
return {
success: true,
orderId,
txHash: txStatus.txHash,
status: txStatus.txStatus === '2' ? 'SUCCESS' : 'PENDING'
};
} catch (error) {
console.error("Error during swap:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error"
};
}
}
// ======== Command Line Interface ========
async function main() {
try {
const args = process.argv.slice(2);
if (args.length < 3) {
console.log("Usage: ts-node solana-swap.ts <amount> <fromTokenAddress> <toTokenAddress> [<slippage>]");
console.log("Example: ts-node solana-swap.ts 0.1 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 0.5");
process.exit(1);
}
const [amountStr, fromTokenAddress, toTokenAddress, slippage = '0.5'] = args;
// Get token information
console.log("Getting token information...");
const tokenInfo = await getTokenInfo(fromTokenAddress, toTokenAddress);
console.log(`From: ${tokenInfo.fromToken.symbol} (${tokenInfo.fromToken.decimals} decimals)`);
console.log(`To: ${tokenInfo.toToken.symbol} (${tokenInfo.toToken.decimals} decimals)`);
// Convert amount using fetched decimals
const rawAmount = convertAmount(amountStr, tokenInfo.fromToken.decimals);
console.log(`Amount in ${tokenInfo.fromToken.symbol} base units:`, rawAmount);
// Execute swap
console.log("\nExecuting swap...");
const result = await executeSwap(fromTokenAddress, toTokenAddress, rawAmount, slippage);
if (result.success) {
console.log("\nSwap completed successfully!");
console.log("Order ID:", result.orderId);
if (result.txHash) {
console.log("Transaction ID:", result.txHash);
console.log("Explorer URL:", `https://solscan.io/tx/${result.txHash}`);
}
} else {
console.error("\nSwap failed:", result.error);
}
process.exit(result.success ? 0 : 1);
} catch (error) {
console.error("Error:", error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
}
// Execute main function if run directly
if (require.main === module) {
main();
}
// Export functions for modular usage
export {
executeSwap,
broadcastTransaction,
trackTransaction,
prepareTransaction
};
7. 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.5'
});
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.5', // 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.5' // 0.5%
});
- Method 1: API-First Approach1. Set Up Your Environment2. Get Swap Data3. Prepare Transaction4. Broadcast Transaction5. Track Transaction6. Complete Implementation7. MEV ProtectionMethod 2: SDK approach1. Install the SDK2. Setup Your Environment3. Initialize the Client4. Execute a Swap With the SDK5. Additional SDK Functionality