快速开始

运行你的第一个 DApp#

本方法演示了如何直接使用 Trade API 端点进行代币兑换。你将在以太坊网络上将 USDC 兑换为 ETH。

1. 设置开发环境#

// --------------------- npm 依赖包 ---------------------
import { Web3 } from 'web3';
import axios from 'axios';
import * as dotenv from 'dotenv';
import CryptoJS from 'crypto-js';
// 你要连接的以太坊节点 URL
const web3 = new Web3('https://......com');
// --------------------- 环境变量 ---------------------

// 加载隐藏的环境变量
dotenv.config();

// 你的钱包信息 - 请替换为你自己的值
const WALLET_ADDRESS: string = process.env.EVM_WALLET_ADDRESS || '0xYourWalletAddress';
const PRIVATE_KEY: string = process.env.EVM_PRIVATE_KEY || 'YourPrivateKey'; 

// Base 链上用于兑换的代币地址
const ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // 原生 ETH
const USDC_ADDRESS: string = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base 上的 USDC

// Base 链的 Chain ID
const chainIndex: string = '8453';

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

// 兑换金额(最小单位)(0.0005 ETH)
const SWAP_AMOUNT: string = '500000000000000'; // 0.0005 ETH
const SLIPPAGEPERCENT: string = '0.5'; // 0.5% 滑点容差

// --------------------- 工具函数 ---------------------
export function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
// 请参阅 https://web3.okx.com/zh-hans/web3/build/docs/waas/rest-authentication 获取 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 等原生代币。

/**
 * 检查 DEX 的代币授权额度
 * @param tokenAddress - 代币合约地址
 * @param ownerAddress - 你的钱包地址
 * @param spenderAddress - DEX 消费者地址
 * @returns 授权额度
 */
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. 检查授权参数并发起授权#

如果授权额度低于你要兑换的金额,则需要对代币进行授权。

3.1 定义交易授权参数#

const getApproveTransactionParams = {
  chainIndex: chainIndex,
  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 = {
      chainIndex: chainIndex,
      tokenContractAddress: tokenAddress,
      approveAmount: amount
    };

    // 准备身份验证
    const timestamp = new Date().toISOString();
    const requestPath = `/api/v6/${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 创建 Gas Limit 计算工具函数#

使用链上网关 API 获取 gas limit。

/**
 * 通过链上网关 API 获取交易 gas limit
 * @param fromAddress - 发送方地址
 * @param toAddress - 目标合约地址
 * @param txAmount - 交易金额(授权时为 0)
 * @param inputData - 交易 calldata
 * @returns 预估 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/v6/${path}`;

    const body = {
      chainIndex: chainIndex,
      fromAddress: fromAddress,
      toAddress: toAddress,
      txAmount: txAmount,
      extJson: {
        inputData: inputData
      }
    };

    // 准备身份验证,签名中包含请求体
    const bodyString = JSON.stringify(body);
    const timestamp = new Date().toISOString();
    const requestPath = `/api/v6/${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;
  }
}

使用 RPC 获取 gas limit。

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

// 增加 20% 缓冲
const gasLimit = (BigInt(gasLimit) * BigInt(12) / BigInt(10)).toString();

3.4 获取交易信息并发送授权交易#

/**
 * 签名并发送授权交易
 * @param tokenAddress - 要授权的代币
 * @param amount - 授权金额
 * @returns 授权交易的交易哈希
 */
async function approveToken(tokenAddress: string, amount: string): Promise<string | null> {
  const spenderAddress = '0x3b3ae790Df4F312e745D270119c6052904FB6790'; // 以太坊主网 DEX 消费者地址
  // 查看路由合约地址: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...');

  // 从 OKX Trade API 获取授权交易数据
  const approveData = await getApproveTransaction(tokenAddress, amount);

  // 使用 RPC 获取准确的 gas limit
  const gasLimit = await web3.eth.estimateGas({
    from: WALLET_ADDRESS,
    to: tokenAddress,
    value: '0',
    data: approveData.data
  });

  // 使用链上网关 API 获取准确的 gas limit
//   const gasLimit = await getGasLimit(WALLET_ADDRESS, tokenAddress, '0', approveData.data);

  // 获取当前 gas 价格
  const gasPrice = await web3.eth.getGasPrice();
  const adjustedGasPrice = BigInt(gasPrice) * BigInt(15) / BigInt(10); // 1.5 倍以加速确认

  // 获取当前 nonce
  const nonce = await web3.eth.getTransactionCount(WALLET_ADDRESS, 'latest');

  // 创建交易对象
  const txObject = {
    from: WALLET_ADDRESS,
    to: tokenAddress,
    data: approveData.data,
    value: '0',
    gas: gasLimit,
    gasPrice: adjustedGasPrice.toString(),
    nonce: nonce
  };

  // 签名并广播交易
  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,
  chainIndex: chainIndex,
  toTokenAddress: toTokenAddress,
  fromTokenAddress: fromTokenAddress,
};

4.2 定义辅助函数#

/**
 * 从 Trade API 获取兑换报价
 * @param fromTokenAddress - 源代币地址
 * @param toTokenAddress - 目标代币地址
 * @param amount - 兑换金额
 * @param slippagePercent - 最大滑点(例如 "0.5" 表示 0.5%)
 * @returns 兑换报价
 */
async function getSwapQuote(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  slippagePercent: string = '0.5'
): Promise<any> {
  try {
    const path = 'dex/aggregator/quote';
    const url = `${baseUrl}${path}`;

    const params = {
      chainIndex: chainIndex,
      fromTokenAddress,
      toTokenAddress,
      amount,
      slippagePercent
    };

    // 准备身份验证
    const timestamp = new Date().toISOString();
    const requestPath = `/api/v6/${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 = {
      chainIndex: chainIndex,
      fromTokenAddress,
      toTokenAddress,
      amount,
      userWalletAddress: userAddress,
      slippagePercent
};

5.2 请求兑换交易数据#

/**
 * 从 Trade API 获取兑换交易数据
 * @param fromTokenAddress - 源代币地址
 * @param toTokenAddress - 目标代币地址
 * @param amount - 兑换金额
 * @param userAddress - 用户钱包地址
 * @param slippagePercent - 最大滑点(例如 "0.5" 表示 0.5%)
 * @returns 兑换交易数据
 */
async function getSwapTransaction(
  fromTokenAddress: string,
  toTokenAddress: string,
  amount: string,
  userAddress: string,
  slippagePercent: string = '0.5'
): Promise<any> {
  try {
    const path = 'dex/aggregator/swap';
    const url = `${baseUrl}${path}`;

    const params = {
      chainIndex: chainIndex,
      fromTokenAddress,
      toTokenAddress,
      amount,
      userWalletAddress: userAddress,
      slippagePercent
    };

    // 准备身份验证
    const timestamp = new Date().toISOString();
    const requestPath = `/api/v6/${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 仅对白名单用户开放。请联系 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: chainIndex,
            extJson: {
                inputData: tx.data
            },
            includeDebug: true
        };

        const timestamp = new Date().toISOString();
        const requestPath = "/api/v6/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];
        
        // 检查模拟结果
        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. 广播交易#

使用交易上链 API 进行 Gas 估算,该方式利用了交易上链 API,相比标准方法能提供更准确的 gas 估算。

/**
 * 通过链上网关 API 获取交易 gas limit
 * @param fromAddress - 发送方地址
 * @param toAddress - 目标合约地址
 * @param txAmount - 交易金额(授权时为 0)
 * @param inputData - 交易 calldata
 * @returns 预估 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/v6/${path}`;

    const body = {
      chainIndex: chainIndex,
      fromAddress: fromAddress,
      toAddress: toAddress,
      txAmount: txAmount,
      extJson: {
        inputData: inputData
      }
    };

    // 准备身份验证,签名中包含请求体
    const bodyString = JSON.stringify(body);
    const timestamp = new Date().toISOString();
    const requestPath = `/api/v6/${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;
  }
}