运行你的第一个 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;
}
}
