交易 API

在 Solana 链上搭建兑换应用#

在 Solana 上使用OKX DEX构建兑换应用程序有两种方法:

  1. API的方法-直接与OKX DEX API交互
  2. SDK方法-使用 @okx-dex/okx-dex-sdk简化开发人员体验

本指南涵盖了这两种方法,以帮助你选择最适合你需求的方法。

方法1:API方法#

在本指南中,我们将提供通过OKX DEX进行Solana代币兑换的用例。

1.设置环境#

导入必要的Node. js库并设置环境变量:

// 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.获取兑换数据#

Solana的本机令牌地址11111111111111111111111111111111。使用/swap端点检索详细的兑换信息:

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.准备交易#

从 /swap 获得callData后,你需要反序列化并签名交易:

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

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);
    }
}

4. 广播交易#

4.1 使用 RPC 广播交易

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}`);

4.2 使用交易上链 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.追踪交易#

使用交易上链 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;
}

详细的兑换信息,我们可以使用 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;
    }
}

交易上链 API 监控使用 /dex/post-transaction/orders 跟踪内部订单处理状态。它有助于监控交易在 OKX 系统中的移动情况,并提供订单 ID 和基本状态(1:待处理,2:成功,3:失败)。

Swap API 交易跟踪使用 /dex/aggregator/history 提供全面的兑换执行详情。它提供特定于代币的信息(代码、金额)、已支付的费用以及详细的区块链数据。如果您需要完整的兑换验证并提供代币级详细信息,请使用此选项。

选择前者用于基本交易状态更新,而选择后者用于获取兑换执行本身的详细信息。

6.完整实现#

这是一个完整的实现示例:

// 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_PATH}/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;
    }
}

// ======== Pre-Transaction Functionality ========

/**
 * 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_PATH}/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_PATH}/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;
    }
}

// ======== Post-Transaction Monitoring ========

// Define error info interface
interface TxErrorInfo {
    error: string;
    message: string;
    action: string;
}

/**
 * Monitor transaction 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 Final transaction status
 */
async function monitorTransaction(
    orderId: string,
    intervalMs: number = POLLING_INTERVAL,
    timeoutMs: number = CONFIRMATION_TIMEOUT
): Promise<any> {
    console.log(`Monitoring transaction with Order ID: ${orderId}`);

    const startTime = Date.now();
    let lastStatus = '';

    while (Date.now() - startTime < timeoutMs) {
        // Get transaction status
        try {
            const path = `${DEX_PATH}/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 monitoring 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: Monitor the transaction status
        console.log("Monitoring transaction status...");
        const txStatus = await monitorTransaction(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,
    monitorTransaction,
    prepareTransaction
};

7. MEV保护#

Solana交易存在MEV(最大可提取价值)风险。虽然MEV保护不直接包含在SDK中,但您可以使用API优先的方法自行实施。

第一道防线使用动态优先级费用-将其视为您在拍卖中对抗MEV机器人的出价。

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);
}

对于较大的交易,您可以启用TWAP(时间加权平均价格)。您的交易将被分成更小的部分,而不是MEV机器人喜欢瞄准的一个大的飞溅。

// 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
    );
}

运行中防护#

当您使用此实现执行交易时,会发生几件事情:

  1. 交易前检查:
  • 购买的代币会被检查是否存在貔貅盘特征
  • 检查你的网络费用来设置有竞争力的优先费用
  • 查看你的交易额度来确认是否要拆单
  1. 在交易期间:
  • 大额交易将拆分成不同金额的单子,并在随机时间发出
  • 每单将根据市场条件设置优先费用
  • 特定的区块来减少暴露风险
  1. 交易的安全性保障:
  • 每笔交易都将进行预执行模拟
  • 对区块确认状态进行内置的追踪
  • 出现问题自动重试逻辑

虽然Solana上的MEV不能完全消除,但这些保护措施使MEV机器人的生活更加困难。

方法2:SDK方法#

使用OKX DEXSDK提供了更简单的开发人员体验,同时保留了API方法的所有功能。SDK为您处理许多实现细节,包括重试逻辑、错误处理和事务管理。

1.安装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.设置环境#

使用您的API凭据和钱包信息创建一个. env文件:

# 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.初始化客户端#

为您的DEX客户端创建一个文件(例如,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.调用SDK执行兑换#

创建兑换执行的文件:

// 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.附加SDK功能#

SDK提供了简化开发的附加方法: 获取代币对的报价

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%
});