交易 API

在 EVM 链上搭建兑换应用#

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

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

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

方法1:API方法#

在这种方法中,我们将直接使用OKX DEX API演示代币兑换。我们将在以太坊网络上将USDC兑换为ETH。

1.设置环境#

// --------------------- npm package ---------------------
import { Web3 } from 'web3';
import axios from 'axios';
import * as dotenv from 'dotenv';
import CryptoJS from 'crypto-js';
// The URL for the Ethereum node you want to connect to
const web3 = new Web3('https://......com');
// --------------------- environment variable ---------------------

// Load hidden environment variables
dotenv.config();

// Your wallet information - REPLACE WITH YOUR OWN VALUES
const WALLET_ADDRESS: string = process.env.EVM_WALLET_ADDRESS || '0xYourWalletAddress';
const PRIVATE_KEY: string = process.env.EVM_PRIVATE_KEY || 'YourPrivateKey'; 

// Token addresses for swap on Base Chain
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH
const USDC_ADDRESS: string = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base

// Chain ID for Base Chain
const chainId: string = '8453';

// API URL
const baseUrl: string = 'https://web3.okx.com/api/v5/';

// Amount to swap in smallest unit (0.0005 ETH)
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH
const SLIPPAGE: string = '0.005'; // 0.5% slippage tolerance

// --------------------- util function ---------------------
export function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
// Check https://web3.okx.com/zh-hans/web3/build/docs/waas/rest-authentication for api-key
    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;

    if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
        throw new Error("Missing required environment variables");
    }

    const stringToSign = timestamp + method + requestPath + queryString;
    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.检查授权额度#

您需要检查代币是否已批准DEX进行支出。此步骤仅适用于ERC20代币,而不是像ETH这样的本地代币。

/**
 * Check token allowance for DEX
 * @param tokenAddress - Token contract address
 * @param ownerAddress - Your wallet address
 * @param spenderAddress - DEX spender address
 * @returns Allowance amount
 */
async function checkAllowance(
  tokenAddress: string,
  ownerAddress: string,
  spenderAddress: string
): Promise<bigint> {
  const tokenABI = [
    {
      "constant": true,
      "inputs": [
        { "name": "_owner", "type": "address" },
        { "name": "_spender", "type": "address" }
      ],
      "name": "allowance",
      "outputs": [{ "name": "", "type": "uint256" }],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    }
  ];

  const tokenContract = new web3.eth.Contract(tokenABI, tokenAddress);
  try {
    const allowance = await tokenContract.methods.allowance(ownerAddress, spenderAddress).call();
    return BigInt(String(allowance));
  } catch (error) {
    console.error('Failed to query allowance:', error);
    throw error;
  }
}

3.检查授权交易参数并发起授权#

由于 allowanceAmount 小于 fromTokenAmount,你需要对该币种进行授权。

3.1 定义授权交易参数

const getApproveTransactionParams = {
  chainId: chainId,
  tokenContractAddress: tokenAddress,
  approveAmount: amount
};

3.2 定义辅助函数

async function getApproveTransaction(
  tokenAddress: string,
  amount: string
): Promise<any> {
  try {
    const path = 'dex/aggregator/approve-transaction';
    const url = `${baseUrl}${path}`;
    const params = {
      chainId: chainId,
      tokenContractAddress: tokenAddress,
      approveAmount: amount
    };

    // 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') {
      return response.data.data[0];
    } else {
      throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
    }
  } catch (error) {
    console.error('Failed to get approval transaction data:', (error as Error).message);
    throw error;
  }
}

3.3 计算 gasLimit

/**
 * Get transaction gas limit from Onchain gateway API
 * @param fromAddress - Sender address
 * @param toAddress - Target contract address
 * @param txAmount - Transaction amount (0 for approvals)
 * @param inputData - Transaction calldata
 * @returns Estimated gas limit
 */
async function getGasLimit(
  fromAddress: string,
  toAddress: string,
  txAmount: string = '0',
  inputData: string = ''
): Promise<string> {
  try {
    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(url, body, { headers });

    if (response.data.code === '0') {
      return response.data.data[0].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;
  }
}

Using RPC to get the gas limit.

const gasLimit = await web3.eth.estimateGas({
  from: WALLET_ADDRESS,
  to: tokenAddress,
  value: '0',
  data: approveData.data
});

// Add 20% buffer
const gasLimit = (BigInt(gasLimit) * BigInt(12) / BigInt(10)).toString();

3.4 获取授权交易 tx 并且发送授权请求

/**
 * Sign and send approve transaction
 * @param tokenAddress - Token to approve
 * @param amount - Amount to approve
 * @returns Transaction hash of the approval transaction
 */
async function approveToken(tokenAddress: string, amount: string): Promise<string | null> {
  const spenderAddress = '0x3b3ae790Df4F312e745D270119c6052904FB6790'; // Ethereum Mainnet DEX spender
  // See Router addresses at:  https://web3.okx.com/build/docs/waas/dex-smart-contract
  const currentAllowance = await checkAllowance(tokenAddress, WALLET_ADDRESS, spenderAddress);

  if (currentAllowance >= BigInt(amount)) {
    console.log('Sufficient allowance already exists');
    return null;
  }

  console.log('Insufficient allowance, approving tokens...');

  // Get approve transaction data from OKX DEX API
  const approveData = await getApproveTransaction(tokenAddress, amount);

  // Get accurate gas limit using RPC
  const gasLimit = await web3.eth.estimateGas({
    from: WALLET_ADDRESS,
    to: tokenAddress,
    value: '0',
    data: approveData.data
  });
  // Get accurate gas limit using Onchain gateway API
//   const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);

  // Get current gas price
  const gasPrice = await web3.eth.getGasPrice();
  const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation

  // Get current nonce
  const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');

  // Create transaction object
  const txObject = {
    from: WALLET_ADDRESS,
    to: tokenAddress,
    data: approveData.data,
    value: '0',
    gas: gasLimit,
    gasPrice: adjustedGasPrice.toString(),
    nonce: nonce
  };

  // Sign and broadcast transaction
  const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  
  console.log(`Approval transaction successful: ${receipt.transactionHash}`);
  return receipt.transactionHash;
}

4.请求询价接口,拿到询价数据#

4.1定义报价参数

const quoteParams = {
  amount: fromAmount,
  chainId: chainId,
  toTokenAddress: toTokenAddress,
  fromTokenAddress: fromTokenAddress,
};

4.2 定义辅助函数

/**
 * Get swap quote from DEX API
 * @param fromTokenAddress - Source token address
 * @param toTokenAddress - Destination token address
 * @param amount - Amount to swap
 * @param slippage - Maximum slippage (e.g., "0.005" for 0.5%)
 * @returns Swap quote
 */
async function getSwapQuote(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.005'
): Promise<any> {
  try {
    const path = 'dex/aggregator/quote';
    const url = `${baseUrl}${path}`;

    const params = {
      chainId: chainId,
      fromTokenAddress,
      toTokenAddress,
      amount,
      slippage
    };

    // 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') {
      return response.data.data[0];
    } else {
      throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
    }
  } catch (error) {
    console.error('Failed to get swap quote:', (error as Error).message);
    throw error;
  }
}

5.请求兑换接口,发起交易#

5.1 定义兑换参数

const swapParams = {
      chainId: chainId,
      fromTokenAddress,
      toTokenAddress,
      amount,
      userWalletAddress: userAddress,
      slippage
};

5.2 定义辅助函数

/**
 * Get swap transaction data from DEX API
 * @param fromTokenAddress - Source token address
 * @param toTokenAddress - Destination token address
 * @param amount - Amount to swap
 * @param userAddress - User wallet address
 * @param slippage - Maximum slippage (e.g., "0.005" for 0.5%)
 * @returns Swap transaction data
 */
async function getSwapTransaction(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  userAddress: string,
  slippage: string = '0.005'
): Promise<any> {
  try {
    const path = 'dex/aggregator/swap';
    const url = `${baseUrl}${path}`;

    const params = {
      chainId: chainId,
      fromTokenAddress,
      toTokenAddress,
      amount,
      userWalletAddress: userAddress,
      slippage
    };

    // 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') {
      return response.data.data[0];
    } else {
      throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
    }
  } catch (error) {
    console.error('Failed to get swap transaction data:', (error as Error).message);
    throw error;
  }
}

6. 模拟交易#

在执行实际兑换之前,务必模拟交易以确保其成功并识别任何潜在问题:

此功能使用 交易上链 API。此 API 仅供我们的白名单客户使用。如您感兴趣,请联系我们 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 || '0',
            chainIndex: chainId,
            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;
    }
}

7. 广播交易#

创建计算 Gas 限制的实用函数 获取交易 Gas 限制的方法有两种:使用标准 RPC 调用或利用 交易上链 API。

方法 1:使用 交易上链 API 估算 Gas

第一种方法利用 OKX 的 交易上链 API,该 API 可以提供比标准方法更准确的 Gas 估算。

/**
 * Get transaction gas limit from Onchain gateway API
 * @param fromAddress - Sender address
 * @param toAddress - Target contract address
 * @param txAmount - Transaction amount (0 for approvals)
 * @param inputData - Transaction calldata
 * @returns Estimated gas limit
 */
async function getGasLimit(
  fromAddress: string,
  toAddress: string,
  txAmount: string = '0',
  inputData: string = ''
): Promise<string> {
  try {
    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(url, body, { headers });

    if (response.data.code === '0') {
      return response.data.data[0].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;
  }
}

方法 2:使用 RPC 估算 Gas 限制

第二种方法利用标准的 Web3 RPC 调用来估算交易所需的 Gas。

const gasLimit = await web3.eth.estimateGas({
  from: WALLET_ADDRESS,
  to: tokenAddress,
  value: '0',
  data: swapData.data
});

// Add 20% buffer
const gasLimit = (BigInt(gasLimit) * BigInt(12) / BigInt(10)).toString();

Broadcasting Transactions with the Onchain Gateway API

For developers with access to the Onchain Gateway API, you can broadcast transactions directly through OKX's infrastructure. This method provides enhanced reliability and monitoring capabilities for high-volume trading operations.

The Broadcast API requires API is available to our whitelisted customers only. Please reach out to dexapi@okx.com to request access.

import { Web3 } from 'web3';
import axios from 'axios';
import * as dotenv from 'dotenv';
import CryptoJS from 'crypto-js';

// Load environment variables
dotenv.config();

// Connect to Base network
const web3 = new Web3(process.env.EVM_RPC_URL || 'https://mainnet.base.org');

// Your wallet information - REPLACE WITH YOUR OWN VALUES
const WALLET_ADDRESS = process.env.EVM_WALLET_ADDRESS || '';
const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY || '';

// Token addresses for swap on Base Chain
const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH

// Chain ID for Base Chain
const chainId = '8453';

// API URL
const baseUrl = 'https://web3.okx.com/api/v5/';

// Define interfaces
interface GasLimitApiResponse {
  code: string;
  msg?: string;
  data: Array<{
    gasLimit: string;
  }>;
}

// Interface for broadcast API response
interface BroadcastApiResponse {
  code: string;
  msg?: string;
  data: Array<{
    orderId: string;
  }>;
}

/**
 * Generate API authentication headers
 */
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
    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;

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

/**
 * Get transaction gas limit from Onchain gateway API
 * @param fromAddress - Sender address
 * @param toAddress - Target contract address
 * @param txAmount - Transaction amount (0 for approvals)
 * @param inputData - Transaction calldata
 * @returns Estimated gas limit
 */
async function getGasLimit(
  fromAddress: string,
  toAddress: string,
  txAmount: string = '0',
  inputData: string = ''
): Promise<string> {
  const path = 'dex/pre-transaction/gas-limit';
  const url = `${baseUrl}${path}`;
  const body = { chainIndex: chainId, fromAddress, toAddress, txAmount, extJson: { inputData } };
  
  const bodyString = JSON.stringify(body);
  const timestamp = new Date().toISOString();
  const headers = getHeaders(timestamp, 'POST', `/api/v5/${path}`, "", bodyString);

  const response = await axios.post<GasLimitApiResponse>(url, body, { headers });
  if (response.data.code === '0') {
    return response.data.data[0].gasLimit;
  }
  throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}

/**
 * Get swap data from OKX API
 */
async function getSwapData(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage = '0.5'
) {
    const path = 'dex/aggregator/swap';
    const url = `${baseUrl}${path}`;
    const params = { chainIndex: chainId, fromTokenAddress, toTokenAddress, amount, slippage, userWalletAddress: WALLET_ADDRESS };
    
    const queryString = "?" + new URLSearchParams(params).toString();
    const timestamp = new Date().toISOString();
    const headers = getHeaders(timestamp, 'GET', `/api/v5/${path}`, queryString);

    const response = await axios.get(`${url}${queryString}`, { headers });
    const responseData = response.data as any;
    if (responseData.code === '0') {
        return responseData.data[0];
    }
    throw new Error(`Swap API Error: ${responseData.msg || 'Unknown error'}`);
}

/**
 * Build and sign transaction using gas limit
 */
async function buildAndSignTransaction(swapData: any, gasLimit: string): Promise<any> {
    const gasPrice = await web3.eth.getGasPrice();
    const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'pending');
    
    const transaction = {
        from: swapData.tx.from,
        to: swapData.tx.to,
        data: swapData.tx.data,
        value: swapData.tx.value || '0x0',
        gas: gasLimit,
        gasPrice: gasPrice.toString(),
        nonce: Number(nonce),
        chainId: parseInt(chainId)
    };

    return await web3.eth.accounts.signTransaction(transaction, PRIVATE_KEY);
}

/**
 * Broadcast transaction using Onchain Gateway API
 */
async function broadcastTransaction(signedTx: any, chainId: string, walletAddress: string): Promise<string> {
    const path = 'dex/pre-transaction/broadcast-transaction';
    const url = `${baseUrl}${path}`;
    const rawTxHex = typeof signedTx.rawTransaction === 'string' ? signedTx.rawTransaction : web3.utils.bytesToHex(signedTx.rawTransaction);
    const body = { signedTx: rawTxHex, chainIndex: chainId, address: walletAddress };
    
    const bodyString = JSON.stringify(body);
    const timestamp = new Date().toISOString();
    const headers = getHeaders(timestamp, 'POST', `/api/v5/${path}`, "", bodyString);

    const response = await axios.post<BroadcastApiResponse>(url, body, { headers });
    if (response.data.code === '0') {
        return response.data.data[0].orderId;
    }
    throw new Error(`Broadcast API Error: ${response.data.msg || 'Unknown error'}`);
}

async function main() {
    try {
        console.log('EVM Gas Limit and Broadcast');
        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: ${process.env.EVM_RPC_URL || 'https://mainnet.base.org'}`);

        // Example parameters
        const fromToken = ETH_ADDRESS;
        const toToken = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
        const amount = '100000000000000'; // 0.0001 ETH in wei
        const slippage = '0.05'; // 0.5%

        // Step 1: Get swap data
        const swapData = await getSwapData(fromToken, toToken, amount, slippage);
        console.log('Swap data obtained');

        // Step 2: 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', gasLimit);

        // Step 3: Build and sign transaction
        const signedTx = await buildAndSignTransaction(swapData, gasLimit);
        console.log('Transaction built and signed');

        // Step 4: Broadcast transaction
        try {
            const orderId = await broadcastTransaction(signedTx, chainId, swapData.tx.from);
            console.log(`Transaction broadcast successful. Order ID: ${orderId}`);
        } catch (broadcastError: any) {
            if (broadcastError.message.includes('API registration and whitelist required')) {
                console.log('Broadcast failed - API registration and whitelist required');
                console.log('Gas limit obtained successfully:', gasLimit);
            } else {
                throw broadcastError;
            }
        }
        
    } catch (error) {
        console.error('Main execution failed:', (error as Error).message);
        process.exit(1);
    }
}

// Run the script
if (require.main === module) {
    main();
}

export {
    getSwapData,
    getGasLimit,
    broadcastTransaction
}; 

Alternative: Broadcasting Transactions Using Standard RPC For developers who prefer using standard blockchain RPC methods or do not have yet requested API whitelisting, you can broadcast transactions directly to the network using Web3 RPC calls.

/**
 * Execute token swap
 * @param fromTokenAddress - Source token address
 * @param toTokenAddress - Destination token address
 * @param amount - Amount to swap
 * @param slippage - Maximum slippage
 * @returns Transaction hash
 */
async function executeSwap(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.005'
): Promise<string> {
  // 1. Check allowance and approve if necessary (skip for native token)
  if (fromTokenAddress !== ETH_ADDRESS) {
    await approveToken(fromTokenAddress, amount);
  }

  // 2. Get swap transaction data
  const swapData = await getSwapTransaction(fromTokenAddress, toTokenAddress, amount, WALLET_ADDRESS, slippage);
  
  const txData = swapData.tx;
  console.log("Swap TX data received");

  // 3. Get accurate gas limit
  const gasLimit = await getGasLimit(
    WALLET_ADDRESS,
    txData.to,
    txData.value || '0',
    txData.data
  );
  console.log("Gas limit received");

  // 4. Get current nonce
  const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');
  console.log("Nonce received");

  // 5. Get current gas price and adjust for faster confirmation
  const gasPrice = await web3.eth.getGasPrice();
  const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation
  console.log("Gas price received");

  // 6. Create transaction object
  const txObject = {
    from: WALLET_ADDRESS,
    to: txData.to,
    data: txData.data,
    value: txData.value || '0',
    gas: gasLimit,
    gasPrice: adjustedGasPrice.toString(),
    nonce: nonce
  };
  console.log("TX build complete");

  // 7. Sign and broadcast transaction using RPC
  const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  console.log("TX signed");
  
  const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(`Transaction successful: ${receipt.transactionHash}`);
  
  return receipt.transactionHash;
}

8. 追踪交易#

最后,创建一个交易追踪系统,对于基本交易的状态确认,请选择第一个(8.1 部分);当您需要有关兑换执行本身的详细信息时,请选择第二个(8.2部分)。

6.1 使用 交易上链 API 交易上链 API 通过 /dex/post-transaction/orders 提供交易追踪功能。使用广播 API 返回的订单ID,并设置简单的状态代码(1:待处理,2:成功,3:失败),即可追踪交易在 OKX 系统中的进展。

// Define error info 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: chainId,
        address: WALLET_ADDRESS,
        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://web3.okx.com/explorer/base/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 as Error).message);
    }

    // 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 handling
  return {
    error: 'TRANSACTION_FAILED',
    message: failReason,
    action: 'Try again or contact support'
  };
}

8.2 如需更详细的兑换信息,您可以使用 SWAP API:

SWAP API 使用 /dex/aggregator/history 提供全面交易跟踪

/**
 * Track transaction using SWAP API
 * @param chainId - Chain ID (e.g., 1 for Ethereum Mainnet)
 * @param txHash - Transaction hash
 * @returns Transaction details
 */
async function trackTransactionWithSwapAPI(chainId: string, txHash: string): Promise<any> {
  try {
    const path = 'dex/aggregator/history';
    const url = `${baseUrl}${path}`;

    const params = {
      chainId: chainId,
      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://basescan.org/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 as Error).message);
    throw error;
  }
}

9. 完整实现#

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

import { Web3 } from 'web3';
import * as axios from 'axios';
import * as dotenv from 'dotenv';
import * as CryptoJS from 'crypto-js';

// Load environment variables
dotenv.config();

// Connect to Base network
const web3 = new Web3(process.env.EVM_RPC_URL || 'https://mainnet.base.org');

// Your wallet information - REPLACE WITH YOUR OWN VALUES
const WALLET_ADDRESS: string = process.env.EVM_WALLET_ADDRESS || '';
const PRIVATE_KEY: string = process.env.EVM_PRIVATE_KEY || '';

// Token addresses for swap on Base Chain
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH

// Chain ID for Base Chain
const chainId: string = '8453';

// API URL
const baseUrl: string = 'https://web3.okx.com/api/v5/';

// Define interfaces
interface TokenInfo {
    tokenSymbol: string;
    decimal: string;
    tokenUnitPrice: string;
}

// Interface for gas limit API response
interface GasLimitApiResponse {
  code: string;
  msg?: string;
  data: Array<{
    gasLimit: string;
  }>;
}

// Interface for simulation API response
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 for broadcast API response
interface BroadcastApiResponse {
  code: string;
  msg?: string;
  data: Array<{
    orderId: string;
  }>;
}

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

/**
 * Generate API authentication headers
 */
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
    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;

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

/**
 * Get transaction gas limit from Onchain gateway API
 * @param fromAddress - Sender address
 * @param toAddress - Target contract address
 * @param txAmount - Transaction amount (0 for approvals)
 * @param inputData - Transaction calldata
 * @returns Estimated gas limit
 */
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 = `${baseUrl}${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 });

    console.log('Gas Limit API Response:');
    console.log(JSON.stringify(response.data, null, 2));

    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 path = 'dex/aggregator/swap';
        const url = `${baseUrl}${path}`;

        const params = {
            chainIndex: chainId,
            fromTokenAddress: fromTokenAddress,
            toTokenAddress: toTokenAddress,
            amount: amount,
            slippage: slippage,
            userWalletAddress: WALLET_ADDRESS
        };

        console.log('Swap API Request Parameters:');
        console.log(JSON.stringify(params, null, 2));

        // Prepare authentication with query string
        const queryString = "?" + new URLSearchParams(params).toString();
        const timestamp = new Date().toISOString();
        const requestPath = `/api/v5/${path}`;
        const headers = getHeaders(timestamp, 'GET', requestPath, queryString);

        const response = await axios.get(`${url}${queryString}`, { headers });

        console.log('Swap API Response:');
        console.log(JSON.stringify(response.data, null, 2));

        const responseData = response.data as any;
        if (responseData.code === '0') {
            return responseData.data[0];
        } else {
            throw new Error(`Swap API Error: ${responseData.msg || 'Unknown error'}`);
        }
    } 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 = `${baseUrl}${path}`;

        const body = {
            chainIndex: chainId,
            fromAddress: swapData.tx.from,
            toAddress: swapData.tx.to,
            txAmount: swapData.tx.value || '0',
            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 });

        console.log('Simulation API Response:');
        console.log(JSON.stringify(response.data, null, 2));

        if (response.data.code === '0') {
            const simulationResult = response.data.data[0];
            // Check if simulation was successful (no failReason or empty failReason)
            if (!simulationResult.failReason || simulationResult.failReason === '') {
                console.log(`Transaction simulation successful. Gas used: ${simulationResult.gasUsed}`);
                return simulationResult;
            } else {
                throw new Error(`Simulation failed: ${simulationResult.failReason}`);
            }
        } 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: any, chainId: string, walletAddress: string): Promise<string> {
    try {
        console.log('Broadcasting transaction via Onchain Gateway API...');
        
        const path = 'dex/pre-transaction/broadcast-transaction';
        const url = `${baseUrl}${path}`;

        // Convert rawTransaction to hex string
        const rawTxHex = typeof signedTx.rawTransaction === 'string' 
            ? signedTx.rawTransaction 
            : web3.utils.bytesToHex(signedTx.rawTransaction);

        const body = {
            signedTx: rawTxHex,
            chainIndex: chainId,
            address: walletAddress
            // See [MEV Section](#10-mev-protection) for MEV protection settings

        };

        console.log('Broadcast API Request Body:');
        console.log(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 });

        console.log('Broadcast API Response:');
        console.log(JSON.stringify(response.data, null, 2));

        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('API broadcast failed, trying RPC fallback:', (error as Error).message);
        
        // Fallback to RPC broadcast
        try {
            console.log('Broadcasting via RPC fallback...');
            const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
            console.log(`RPC broadcast successful. Transaction hash: ${receipt.transactionHash}`);
            return receipt.transactionHash.toString();
        } catch (rpcError) {
            console.error('RPC broadcast also failed:', (rpcError as Error).message);
            throw new Error(`Both API and RPC broadcast failed. API Error: ${(error as Error).message}, RPC Error: ${(rpcError as Error).message}`);
        }
    }
}

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

        // Step 4: Get current gas price
        const gasPrice = await web3.eth.getGasPrice();
        console.log(`Current gas price: ${web3.utils.fromWei(gasPrice, 'gwei')} gwei`);

        // Step 5: Get nonce
        const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'pending');
        console.log(`Nonce: ${nonce}`);

        // Step 6: Build transaction
        const transaction = {
            from: swapData.tx.from,
            to: swapData.tx.to,
            data: swapData.tx.data,
            value: swapData.tx.value || '0x0',
            gas: gasLimit,
            gasPrice: gasPrice.toString(),
            nonce: Number(nonce),
            chainId: parseInt(chainId)
        };

        console.log('Transaction object:');
        console.log(JSON.stringify(transaction, null, 2));

        // Step 7: Sign transaction
        console.log('Signing transaction...');
        const signedTx = await web3.eth.accounts.signTransaction(transaction, PRIVATE_KEY);
        console.log('Transaction signed');

        // Step 8: Broadcast transaction
        const txHash = await broadcastTransaction(signedTx, chainId, WALLET_ADDRESS);
        console.log(`Transaction broadcast successful. Hash: ${txHash}`);

        // Step 9: 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 };
    }
}

/**
 * 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) {
    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') {
              console.log(`Transaction pending: ${txData.txHash || 'Hash not available yet'}`);
            } else if (status === '2') {
              console.log(`Transaction successful: https://web3.okx.com/explorer/base/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 as Error).message);
    }

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

  console.error(`Transaction failed with reason: ${failReason}`);

  return {
    error: 'TRANSACTION_FAILED',
    message: failReason,
    action: 'Try again or contact support'
  };
}

// ======== Main Execution ========

async function simulateOnly(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage: string = '0.5'
): 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
        );

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

async function main() {
    try {
        console.log('EVM 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: ${process.env.EVM_RPC_URL || 'https://mainnet.base.org'}`);

        // Parse command line arguments
        const args = process.argv.slice(2);
        const mode = args[0] || 'simulate'; // Default to simulate mode
        
        // Example parameters
        const fromToken = ETH_ADDRESS;
        const toToken = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
        const amount = '100000000000000'; // 0.0001 ETH in wei
        const slippage = '0.05'; // 0.5%

        console.log('\nConfiguration:');
        console.log(`   From: ${fromToken} (ETH)`);
        console.log(`   To: ${toToken} (USDC)`);
        console.log(`   Amount: ${web3.utils.fromWei(amount, 'ether')} ETH`);
        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 evm-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();
}

export {
    executeSwap,
    executeSwapWithSimulation,
    simulateOnly,
    getSwapData,
    simulateTransaction,
    getGasLimit,
    broadcastTransaction,
    trackTransaction
};


You can run this script using evm-swap.ts sim or evm-swap.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

10. MEV Protection#

MEV Protection with Broadcast Transaction API#

The OKX Broadcast Transaction API provides built-in MEV protection capabilities to help safeguard your transactions from front-running and sandwich attacks. The Broadcast API is available to our whitelisted customers only. Please reach out to dexapi@okx.com to request access.

Disclaimer: Your end-user's transaction can only be covered by the MEV protection feature if you actually utilise OKX Build's API services for that particular transaction. MEV protection is currently an experimental feature provided by third-parties and OKX Build does not guarantee the effectiveness and quality of such MEV protection.

Adding MEV Protection#

To enable MEV protection on EVM chains, add the extraData field to your broadcast transaction request with enableMevProtection: true:

/**
 * Broadcast transaction with MEV protection enabled for EVM chains
 */
async function broadcastTransactionWithMEV(
    signedTx: string,
    chainId: string = "8453", // Base chain
    walletAddress: string,
    enableMevProtection: boolean = true
): Promise<string> {
    try {
        console.log('Broadcasting transaction with MEV protection...');

        const path = 'dex/pre-transaction/broadcast-transaction';
        const url = `https://web3.okx.com/api/v5/${path}`;

        const body = {
            signedTx: signedTx,
            chainIndex: chainId,
            address: walletAddress,
            extraData: JSON.stringify({
                enableMevProtection: enableMevProtection
            })
        };

        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 orderId = response.data.data[0].orderId;
            console.log(`Transaction broadcast with MEV protection. Order ID: ${orderId}`);
            return orderId;
        } else {
            throw new Error(`Broadcast API Error: ${response.data.msg || 'Unknown error'}`);
        }
    } catch (error) {
        console.error('MEV-protected broadcast failed:', error);
        throw error;
    }
}

Usage Examples#

Basic swap without MEV protection:

// Standard broadcast (no MEV protection) for Base chain
const orderId = await broadcastTransaction(signedTx, "8453", walletAddress);

Swap with MEV protection enabled:

// With MEV protection on Base chain
const orderId = await broadcastTransactionWithMEVOptions(signedTx, "8453", walletAddress, true);

Integration with Complete Swap Flow#

Here's how to integrate MEV protection into your complete EVM swap execution:

/**
 * Execute EVM swap with MEV protection
 */
async function executeSwapWithMEVProtection(
    fromTokenAddress: string,
    toTokenAddress: string,
    amount: string,
    slippage: string = '0.005',
    enableMevProtection: boolean = true,
    chainId: string = "8453" // Base chain
): Promise<string> {
    try {
        // Step 1: Check allowance and approve if necessary (skip for native token)
        if (fromTokenAddress !== ETH_ADDRESS) {
            await approveToken(fromTokenAddress, amount);
        }

        // Step 2: Get swap transaction data
        const swapData = await getSwapTransaction(fromTokenAddress, toTokenAddress, amount, WALLET_ADDRESS, slippage);
        
        const txData = swapData.tx;
        console.log("Swap TX data received");

        // Step 3: Get current gas price and nonce
        const gasPrice = await web3.eth.getGasPrice();
        const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5x for faster confirmation
        const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');

        // Step 4: Create and sign transaction object
        const txObject = {
            from: WALLET_ADDRESS,
            to: txData.to,
            data: txData.data,
            value: txData.value || '0',
            gas: '300000', // Default gas limit
            gasPrice: adjustedGasPrice.toString(),
            nonce: nonce
        };

        const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
        console.log("Transaction signed");

        // Step 5: Broadcast with MEV protection
        const orderId = await broadcastTransactionWithMEVOptions(
            signedTx.rawTransaction, 
            chainId, 
            WALLET_ADDRESS, 
            enableMevProtection
        );

        // Step 6: Track transaction
        const result = await trackTransaction(orderId);
        
        return result.txHash;
    } catch (error) {
        console.error("MEV-protected swap failed:", error);
        throw error;
    }
}

MEV 保护功能与您现有的 EVM 和 Solana 兑换实现无缝集成,并为 Solana、Base、ETH 和 BSC 提供额外的安全保护,以抵御 MEV 攻击。

方法2:SDK方法#

使用OKX DEX SDK提供了更简单的开发人员体验,同时保留了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
# EVM Configuration
EVM_RPC_URL=your_evm_rpc_url
EVM_WALLET_ADDRESS=your_evm_wallet_address
EVM_PRIVATE_KEY=your_evm_private_key

3.初始化客户端#

为您的DEX客户端创建一个文件(例如,DexClient. ts):

// DexClient.ts
import { OKXDexClient } from '@okx-dex/okx-dex-sdk';
import { createEVMWallet } from '@okx-dex/okx-dex-sdk/core/evm-wallet';
import { createWallet } from '@okx-dex/okx-dex-sdk/core/wallet';
import { Connection } from '@solana/web3.js';
import { ethers } from 'ethers';
import dotenv from 'dotenv';

dotenv.config();

// EVM setup (Ethereum, Base, Arbitrum, etc.)
const evmProvider = new ethers.JsonRpcProvider(process.env.EVM_RPC_URL!);
const evmWallet = createEVMWallet(process.env.EVM_PRIVATE_KEY!, evmProvider);

// Initialize the client
const client = new OKXDexClient({
    // API credentials (get from OKX Developer Portal)
    apiKey: process.env.OKX_API_KEY!,
    secretKey: process.env.OKX_SECRET_KEY!,
    apiPassphrase: process.env.OKX_API_PASSPHRASE!,
    projectId: process.env.OKX_PROJECT_ID!,
    
    // EVM configuration (works for all EVM chains)
    evm: {
        wallet: evmWallet
    },
})

4.调用SDK执行代币授权#

创建代币授权工具的功能:

// approval.ts
import { client } from './DexClient';
// Helper function to convert human-readable amounts to base units
export function toBaseUnits(amount: string, decimals: number): string {
    // Remove any decimal point and count the decimal places
    const [integerPart, decimalPart = ''] = amount.split('.');
    const currentDecimals = decimalPart.length;

    // Combine integer and decimal parts, removing the decimal point
    let result = integerPart + decimalPart;

    // Add zeros if you need more decimal places
    if (currentDecimals < decimals) {
        result = result + '0'.repeat(decimals - currentDecimals);
    }
    // Remove digits if you have too many decimal places
    else if (currentDecimals > decimals) {
        result = result.slice(0, result.length - (currentDecimals - decimals));
    }

    // Remove leading zeros
    result = result.replace(/^0+/, '') || '0';

    return result;
}
/**
 * Example: Approve a token for swapping
 */
async function executeApproval(tokenAddress: string, amount: string) {
    try {
        // Get token information using quote
        console.log("Getting token information...");
        const tokenInfo = await client.dex.getQuote({
            chainId: '8453',  // Base Chain
            fromTokenAddress: tokenAddress,
            toTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // Native token
            amount: '1000000', // Use a reasonable amount for quote
            slippage: '0.005'
        });
        const tokenDecimals = parseInt(tokenInfo.data[0].fromToken.decimal);
        const rawAmount = toBaseUnits(amount, tokenDecimals);
        console.log(`\nApproval Details:`);
        console.log(`--------------------`);
        console.log(`Token: ${tokenInfo.data[0].fromToken.tokenSymbol}`);
        console.log(`Amount: ${amount} ${tokenInfo.data[0].fromToken.tokenSymbol}`);
        console.log(`Amount in base units: ${rawAmount}`);
        // Execute the approval
        console.log("\nExecuting approval...");
        const result = await client.dex.executeApproval({
            chainId: '8453',  // Base Chain
            tokenContractAddress: tokenAddress,
            approveAmount: rawAmount
        });
        if ('alreadyApproved' in result) {
            console.log("\nToken already approved for the requested amount!");
            return { success: true, alreadyApproved: true };
        } else {
            console.log("\nApproval completed successfully!");
            console.log("Transaction Hash:", result.transactionHash);
            console.log("Explorer URL:", result.explorerUrl);
            return result;
        }
    } catch (error) {
        if (error instanceof Error) {
            console.error('Error executing approval:', error.message);
        }
        throw error;
    }
}
// Run if this file is executed directly
if (require.main === module) {
    // Example usage: ts-node approval.ts 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 1000
    const args = process.argv.slice(2);
    if (args.length !== 2) {
        console.log("Usage: ts-node approval.ts <tokenAddress> <amountToApprove>");
        console.log("\nExamples:");
        console.log("  # Approve 1000 USDC");
        console.log(`  ts-node approval.ts 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 1000`);
        process.exit(1);
    }
    const [tokenAddress, amount] = args;
    executeApproval(tokenAddress, amount)
        .then(() => process.exit(0))
        .catch((error) => {
            console.error('Error:', error);
            process.exit(1);
        });
}
export { executeApproval };

5.调用SDK执行兑换#

创建兑换执行的文件:

// swap.ts
import { client } from './DexClient';
/**
 * Example: Execute a swap from ETH to USDC on Base chain
 */
async function executeSwap() {
  try {
    if (!process.env.EVM_PRIVATE_KEY) {
      throw new Error('Missing EVM_PRIVATE_KEY in .env file');
    }
    // You can change this to any EVM chain
    // For example, for Base, use chainId: '8453'
    // For example, for baseSepolia, use chainId: '84532'
    // You can also use SUI, use chainId: '784'
    // When using another Chain, you need to change the fromTokenAddress and toTokenAddress to the correct addresses for that chain

    const swapResult = await client.dex.executeSwap({
      chainId: '8453', // Base chain ID
      fromTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // Native ETH
      toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
      amount: String(10 * 10 ** 14), // .0001 ETH
      slippage: '0.005', // 0.5% slippage
      userWalletAddress: process.env.EVM_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 };

6.附加的SDK功能#

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

const quote = await client.dex.getQuote({
    chainId: '8453',  // Base Chain
    fromTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
    toTokenAddress: '0x4200000000000000000000000000000000000006', // WETH
    amount: '1000000',  // 1 USDC (in smallest units)
    slippage: '0.005'     // 0.5%
});