交易 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 Ethereum Mainnet
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Native ETH
const USDC_ADDRESS: string = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC

// Chain ID for Ethereum Mainnet
const chainId: string = '1';

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

// Amount to swap in smallest unit (approx $1 of ETH)
// 1 ETH = 10^18 wei, so 0.0005 ETH
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH (approx $1)
const SLIPPAGE: string = '0.5'; // 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;
  }
}

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

/**
 * Sign and send approve transaction
 * @param tokenAddress - Token to approve
 * @param amount - Amount to approve
 * @returns Order ID 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 Onchain gateway API
  const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);

  // Get current gas price (can also use Onchain gateway API)
  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 transaction
  const {signedTx} = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  await web3.eth.sendSignedTransaction(signedTx);
}

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.5" for 0.5%)
 * @returns Swap quote
 */
async function getSwapQuote(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.5'
): 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 quote 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.5" for 0.5%)
 * @returns Swap quote
 */
async function getSwapQuote(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  userAddress: string,
  slippage: string = '0.5'
): 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 quote:', (error as Error).message);
    throw error;
  }
}

5.3 请求兑换接口拿到 tx 信息

/**
 * Execute token swap
 * @param fromTokenAddress - Source token address
 * @param toTokenAddress - Destination token address
 * @param amount - Amount to swap
 * @param slippage - Maximum slippage
 */
async function executeSwap(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.5'
): 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 using Onchain gateway API
  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 transaction
  const {signedTx} = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  console.log("TX signed");
  const chainTxInfo = await web3.eth.sendSignedTransaction(signedTx);
  console.log('chainTxInfo:', chainTxInfo);
}

6. 广播交易#

6.1 用 RPC 广播交易

const chainTxInfo = await web3.eth.sendSignedTransaction(signedTx);
  console.log('chainTxInfo:', chainTxInfo);

6.2 用交易上链 API广播交易

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

    const broadcastData = {
      signedTx: signedTx.rawTransaction,
      chainIndex: chainId,
      address: WALLET_ADDRESS
    };

    // 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(`Swap transaction broadcast successfully, Order ID: ${orderId}`);

      // 9. Monitor transaction status
      await monitorTransaction(orderId);

      return orderId;
    } else {
      throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
    }
  } catch (error) {
    console.error('Failed to broadcast swap transaction:', error);
    throw error;
  }

7. 追踪交易#

使用交易上链 API

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

/**
 * Monitor transaction status
 * @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 = 5000,
  timeoutMs: number = 300000
): 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/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 monitoring 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'
  };
}

使用 查询 Dex 交易状态 API

/**
 * Track transaction status using DEX API
 * @param chainId - Chain ID (e.g., 1 for Ethereum Mainnet)
 * @param txHash - Transaction hash
 * @returns Transaction details
 */
async function trackTransactionStatus(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;
      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://web3.okx.com/explorer/base/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;
  }
}

交易上链 API 监控使用 /dex/post-transaction/orders 跟踪内部订单处理状态。它有助于监控交易在 OKX 系统中的移动情况,并提供订单 ID 和 基本状态(1:待处理,2:成功,3:失败)。
Swap API 交易跟踪使用 /dex/aggregator/history 提供全面的兑换执行详情。它提供特定于代币的信息(代码、金额)、已支付的费用以及详细的区块链数据。如果您需要完整的兑换验证并提供代币级详细信息,请使用此选项。 选择前者用于基本交易状态更新,而选择后者用于获取兑换执行本身的详细信息。

8. 完整实现#

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

// Token Swap using OKX Onchain gateway API on Base
// Required packages
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('https://mainnet.base.org');

// 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'; // Without 0x prefix

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

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

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

// Amount to swap in smallest unit (approx $1 of ETH)
// 1 ETH = 10^18 wei, so 0.0005 ETH = 5 * 10^14 wei at ~$2000/ETH
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH (approx $1)
const SLIPPAGE: string = '0.5'; // 0.5% slippage tolerance

// Define type for error handling
interface TxErrorInfo {
  error: string;
  message: string;
  action: string;
}

// ===== Get Header Function =====

export function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
    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,
    };
}

// ===== Token Approval Functions =====

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

/**
 * Get approve transaction data from OKX DEX API
 * @param tokenAddress - Token contract address
 * @param amount - Amount to approve
 * @returns Approval transaction data
 */
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;
  }
}

/**
 * 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}`;
    console.log({path: path, url:url})

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

/**
 * Sign and send approve transaction
 * @param tokenAddress - Token to approve
 * @param amount - Amount to approve
 * @returns Order ID of the approval transaction
 */
async function approveToken(tokenAddress: string, amount: string): Promise<string | null> {
  const spenderAddress = '0x6b2C0c7be2048Daa9b5527982C29f48062B34D58'; // Base DEX spender
  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 Onchain gateway API
  const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);

  // Get current gas price (can also use Onchain gateway API)
  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 transaction
  const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);

  // Broadcast transaction using Onchain gateway API
  try {
    const path = 'dex/pre-transaction/broadcast-transaction';
    const url = `${baseUrl}${path}`;

    const broadcastData = {
      signedTx: signedTx.rawTransaction,
      chainIndex: chainId,
      address: WALLET_ADDRESS
    };

    // 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(`Approval transaction broadcast successfully, Order ID: ${orderId}`);

      // Monitor transaction status
      await monitorTransaction(orderId);

      return orderId;
    } else {
      throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
    }
  } catch (error) {
    console.error('Failed to broadcast approval transaction:', error);
    throw error;
  }
}

// ===== Swap Functions =====

/**
 * Get swap quote 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.5" for 0.5%)
 * @returns Swap quote
 */
async function getSwapQuote(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  userAddress: string,
  slippage: string = '0.5'
): 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 quote:', (error as Error).message);
    throw error;
  }
}

/**
 * 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
 * @returns Swap transaction data
 */
async function getSwapTransaction(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  userAddress: string,
  slippage: string = '0.5'
): 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;
  }
}

/**
 * Execute token swap
 * @param fromTokenAddress - Source token address
 * @param toTokenAddress - Destination token address
 * @param amount - Amount to swap
 * @param slippage - Maximum slippage
 * @returns Order ID of the swap transaction
 */
async function executeSwap(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippage: string = '0.5'
): 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 using Onchain gateway API
  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 transaction
  const signedTx = await web3.eth.accounts.signTransaction(txObject, PRIVATE_KEY);
  console.log("TX signed");

  // 8. Broadcast transaction using Onchain gateway API
  try {
    const path = 'dex/pre-transaction/broadcast-transaction';
    const url = `https://web3.okx.com/api/v5/${path}`;

    const broadcastData = {
      signedTx: signedTx.rawTransaction,
      chainIndex: chainId,
      address: WALLET_ADDRESS
    };

    // 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(`Swap transaction broadcast successfully, Order ID: ${orderId}`);

      // 9. Monitor transaction status
      await monitorTransaction(orderId);

      return orderId;
    } else {
      throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
    }
  } catch (error) {
    console.error('Failed to broadcast swap transaction:', error);
    throw error;
  }
}

// ===== Transaction Monitoring =====

/**
 * Monitor transaction status
 * @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 = 5000,
  timeoutMs: number = 300000
): 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/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 monitoring 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'
  };
}

// ===== Main Function =====

/**
 * Main function to execute ETH to USDC swap on Base
 */
async function main(): Promise<void> {
  try {
    console.log('Starting ETH to USDC swap on Base using Onchain gateway API');

    // Execute swap from ETH to USDC on Base
    const orderId = await executeSwap(
      ETH_ADDRESS,    // From ETH on Base
      USDC_ADDRESS,   // To USDC on Base
      SWAP_AMOUNT,    // Amount in ETH's smallest unit (0.0005 ETH ≈ $1)
      SLIPPAGE        // 0.5% slippage
    );

    // Get final transaction details
    try {
      const path = 'dex/post-transaction/orders';
      const url = `https://web3.okx.com/api/v5/${path}`;

      const params = {
        orderId: orderId,
        chainIndex: chainId
      };

      // 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) {
        console.log('Transaction Hash:', response.data.data[0].txHash);
      }
    } catch (error) {
      console.error('Failed to get final transaction details:', (error as Error).message);
    }
  } catch (error) {
    console.error('Swap failed:', (error as Error).message);
  }
}

// Execute the main function
main();

方法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 'dotenv/config';
// Validate environment variables
const requiredEnvVars = [
    'OKX_API_KEY',
    'OKX_SECRET_KEY',
    'OKX_API_PASSPHRASE',
    'OKX_PROJECT_ID',
    'EVM_WALLET_ADDRESS',
    'EVM_PRIVATE_KEY',
    'EVM_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!,
    evm: {
        connection: {
            rpcUrl: process.env.EVM_RPC_URL!,
        },
        walletAddress: process.env.EVM_WALLET_ADDRESS!,
        privateKey: process.env.EVM_PRIVATE_KEY!,
    }
});

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 we need more decimal places
    if (currentDecimals < decimals) {
        result = result + '0'.repeat(decimals - currentDecimals);
    }
    // Remove digits if we 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.5'
        });
        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.5', // 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.5'     // 0.5%
});