在 Solana 链上搭建兑换应用#
在 Solana 上使用OKX DEX构建兑换应用程序有两种方法:
- API的方法-直接与OKX DEX API交互
- SDK方法-使用
@okx-dex/okx-dex-sdk
简化开发人员体验
本指南涵盖了这两种方法,以帮助你选择最适合你需求的方法。
方法1:API方法#
在本指南中,我们将提供通过OKX DEX进行Solana代币兑换的用例。
1.设置环境#
导入必要的Node. js库并设置环境变量:
// Required libraries
import base58 from "bs58";
import BN from "bn.js";
import * as solanaWeb3 from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import cryptoJS from "crypto-js";
import axios from "axios";
import dotenv from 'dotenv';
dotenv.config();
// Environment variables
const apiKey = process.env.OKX_API_KEY;
const secretKey = process.env.OKX_SECRET_KEY;
const apiPassphrase = process.env.OKX_API_PASSPHRASE;
const projectId = process.env.OKX_PROJECT_ID;
const userAddress = process.env.WALLET_ADDRESS;
const userPrivateKey = process.env.PRIVATE_KEY;
const solanaRpcUrl = process.env.SOLANA_RPC_URL;
// Constants
const SOLANA_CHAIN_ID = "501";
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;
// Initialize Solana connection
const connection = new Connection(`${solanaRpcUrl}`, {
confirmTransactionInitialTimeout: 5000
});
// Utility function for OKX API authentication
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
const stringToSign = timestamp + method + requestPath + (queryString || body);
return {
"Content-Type": "application/json",
"OK-ACCESS-KEY": apiKey,
"OK-ACCESS-SIGN": cryptoJS.enc.Base64.stringify(
cryptoJS.HmacSHA256(stringToSign, secretKey)
),
"OK-ACCESS-TIMESTAMP": timestamp,
"OK-ACCESS-PASSPHRASE": apiPassphrase,
"OK-ACCESS-PROJECT": projectId,
};
}
2.获取兑换数据#
Solana的本机令牌地址11111111111111111111111111111111。使用/swap端点检索详细的兑换信息:
async function getSwapData(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage = '0.005' // 0.5% slippage
) {
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const params = {
amount: amount,
chainId: SOLANA_CHAIN_ID,
fromTokenAddress: fromTokenAddress,
toTokenAddress: toTokenAddress,
userWalletAddress: userAddress,
slippage: slippage
};
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
try {
const response = await axios.get(
`https://web3.okx.com${requestPath}${queryString}`,
{ headers }
);
if (response.data.code !== "0" || !response.data.data?.[0]) {
throw new Error(`API Error: ${response.data.msg || "Failed to get swap data"}`);
}
return response.data.data[0];
} catch (error) {
console.error("Error fetching swap data:", error);
throw error;
}
}
3.准备交易#
async function prepareTransaction(callData: string) {
try {
// Decode the base58 encoded transaction data
const decodedTransaction = base58.decode(callData);
// Get the latest blockhash
const recentBlockHash = await connection.getLatestBlockhash();
console.log("Got blockhash:", recentBlockHash.blockhash);
let tx;
// Try to deserialize as a versioned transaction first
try {
tx = solanaWeb3.VersionedTransaction.deserialize(decodedTransaction);
console.log("Successfully created versioned transaction");
tx.message.recentBlockhash = recentBlockHash.blockhash;
} catch (e) {
// Fall back to legacy transaction if versioned fails
console.log("Versioned transaction failed, trying legacy:", e);
tx = solanaWeb3.Transaction.from(decodedTransaction);
console.log("Successfully created legacy transaction");
tx.recentBlockhash = recentBlockHash.blockhash;
}
return {
transaction: tx,
recentBlockHash
};
} catch (error) {
console.error("Error preparing transaction:", error);
throw error;
}
}
4. 模拟交易#
在执行实际兑换之前,务必模拟交易以确保其成功并识别任何潜在问题:
此功能使用 交易上链 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,
chainIndex: SOLANA_CHAIN_ID,
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;
}
}
5. 广播交易#
5.1 使用 RPC
广播交易
Solana 使用计算单元(而非 Gas)来衡量交易复杂度。有两种方法可以估算交易的计算单元:使用标准 RPC 调用或利用 交易上链API。
方法 1:使用 交易上链 API 进行计算单元估算
第一种方法利用 OKX 的交易上链 API,该 API 提供比标准方法更准确的计算单元估算。
/**
* Get transaction compute units from 交易上链 API
* @param fromAddress - Sender address
* @param toAddress - Target program address
* @param inputData - Transaction data (base58 encoded)
* @returns Estimated compute units
*/
async function getComputeUnits(
fromAddress: string,
toAddress: string,
inputData: string
): Promise<number> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: "501", // Solana chain ID
fromAddress: fromAddress,
toAddress: toAddress,
txAmount: "0",
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') {
const computeUnits = parseInt(response.data.data[0].gasLimit);
console.log(`API estimated compute units: ${computeUnits}`);
return computeUnits;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get compute units from API:', (error as Error).message);
throw error;
}
}
方法 2:使用 RPC 估算计算单元
第二种方法利用标准 Solana RPC 调用来模拟和估算交易所需的计算单元。
/**
* Estimate compute units for a transaction
*/
async function getComputeUnits(transaction: VersionedTransaction): Promise<number> {
try {
// Simulate the transaction to get compute unit usage
const simulationResult = await connection.simulateTransaction(transaction, {
replaceRecentBlockhash: true,
commitment: 'processed'
});
if (simulationResult.value.err) {
throw new Error(`Simulation failed: ${JSON.stringify(simulationResult.value.err)}`);
}
// Get the compute units consumed from simulation
const computeUnitsConsumed = simulationResult.value.unitsConsumed || 200000;
// Add 20% buffer for safety
const computeUnitsWithBuffer = Math.ceil(computeUnitsConsumed * 1.2);
console.log(`Estimated compute units: ${computeUnitsConsumed}`);
console.log(`With 20% buffer: ${computeUnitsWithBuffer}`);
return computeUnitsWithBuffer;
} catch (error) {
console.warn('Failed to estimate compute units, using default:', error);
return 300000; // Default fallback
}
}
5.2 使用计算单元准备交易
广播前,请使用预估的计算单元和最新的区块哈希值准备您的交易:
方法 1:使用 Gas-Limit API 的计算单元准备交易
使用交易上链 API 中预估的计算单元准备您的交易:
/**
* Get transaction compute units from 交易上链 API
*/
async function getComputeUnitsFromAPI(
fromAddress: string,
inputData: string
): Promise<number> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: "501", // Solana chain ID
fromAddress: fromAddress,
toAddress: "", // Can be empty for Solana
txAmount: "0",
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 fetch(url, {
method: 'POST',
headers,
body: bodyString
});
const data = await response.json();
if (data.code === '0') {
const computeUnits = parseInt(data.data[0].gasLimit);
console.log(`API estimated compute units: ${computeUnits}`);
return computeUnits;
} else {
throw new Error(`API Error: ${data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get compute units from API:', (error as Error).message);
throw error;
}
}
/**
* Prepare transaction with compute units from API
*/
async function prepareTransactionWithAPIComputeUnits(
transaction: VersionedTransaction,
fromAddress: string,
transactionData: string
): Promise<{
transaction: VersionedTransaction;
gasData: {
estimatedComputeUnits: number;
priorityFee: number;
blockhash: string;
};
}> {
try {
// Get fresh blockhash
const { blockhash } = await connection.getLatestBlockhash('confirmed');
console.log(`Using blockhash: ${blockhash}`);
// Update the transaction's blockhash
transaction.message.recentBlockhash = blockhash;
// Check if transaction already has compute budget instructions
const hasComputeBudgetIx = transaction.message.compiledInstructions.some(ix => {
const programId = transaction.message.staticAccountKeys[ix.programIdIndex];
return programId.equals(ComputeBudgetProgram.programId);
});
if (hasComputeBudgetIx) {
console.log('Transaction already contains compute budget instructions, skipping addition');
return {
transaction,
gasData: {
estimatedComputeUnits: 300000,
priorityFee: 1000,
blockhash
}
};
}
// Get compute units from API
const estimatedComputeUnits = await getComputeUnitsFromAPI(fromAddress, transactionData);
// Set priority fee
const priorityFee = 1000; // microLamports
const gasData = {
estimatedComputeUnits,
priorityFee,
blockhash
};
console.log(`Priority fee: ${gasData.priorityFee} microLamports`);
// Create compute unit limit instruction
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: gasData.estimatedComputeUnits
});
// Create compute unit price instruction for priority
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: gasData.priorityFee
});
// Get existing instructions and account keys
const existingInstructions = [...transaction.message.compiledInstructions];
const existingAccountKeys = [...transaction.message.staticAccountKeys];
// Add compute budget program to account keys if not present
let computeBudgetProgramIndex = existingAccountKeys.findIndex(
key => key.equals(ComputeBudgetProgram.programId)
);
if (computeBudgetProgramIndex === -1) {
computeBudgetProgramIndex = existingAccountKeys.length;
existingAccountKeys.push(ComputeBudgetProgram.programId);
}
// Create new instructions array with compute budget instructions
const newInstructions = [
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computeBudgetIx.data
},
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computePriceIx.data
},
...existingInstructions
];
// Create new versioned message with proper instruction mapping
const newMessage = new TransactionMessage({
payerKey: existingAccountKeys[0],
recentBlockhash: gasData.blockhash,
instructions: newInstructions.map(ix => ({
programId: existingAccountKeys[ix.programIdIndex],
keys: ix.accountKeyIndexes.map(idx => ({
pubkey: existingAccountKeys[idx],
isSigner: false,
isWritable: false
})).filter(key => key.pubkey),
data: Buffer.from(ix.data)
})).filter(ix => ix.programId)
}).compileToV0Message();
// Create and return new transaction
const preparedTransaction = new VersionedTransaction(newMessage);
return { transaction: preparedTransaction, gasData };
} catch (error) {
console.error('Error preparing transaction:', error);
throw error;
}
}
方法 2:使用 RPC 的计算单元准备交易
// Simple connection setup
const connection = new Connection(
process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);
/**
* Prepare transaction with compute units
*/
async function prepareTransactionWithComputeUnits(
transaction: VersionedTransaction
): Promise<{
transaction: VersionedTransaction;
gasData: {
estimatedComputeUnits: number;
priorityFee: number;
blockhash: string;
};
}> {
try {
// Get fresh blockhash
const { blockhash } = await connection.getLatestBlockhash('confirmed');
console.log(`Using blockhash: ${blockhash}`);
// Update the transaction's blockhash
transaction.message.recentBlockhash = blockhash;
// Check if transaction already has compute budget instructions
const hasComputeBudgetIx = transaction.message.compiledInstructions.some(ix => {
const programId = transaction.message.staticAccountKeys[ix.programIdIndex];
return programId.equals(ComputeBudgetProgram.programId);
});
if (hasComputeBudgetIx) {
console.log('Transaction already contains compute budget instructions, skipping addition');
return {
transaction,
gasData: {
estimatedComputeUnits: 300000,
priorityFee: 1000,
blockhash
}
};
}
// Estimate compute units
const estimatedComputeUnits = await getComputeUnits(transaction);
// Set priority fee
const priorityFee = 1000; // microLamports
const gasData = {
estimatedComputeUnits,
priorityFee,
blockhash
};
console.log(`Priority fee: ${gasData.priorityFee} microLamports`);
// Create compute unit limit instruction
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: gasData.estimatedComputeUnits
});
// Create compute unit price instruction for priority
const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: gasData.priorityFee
});
// Get existing instructions and account keys
const existingInstructions = [...transaction.message.compiledInstructions];
const existingAccountKeys = [...transaction.message.staticAccountKeys];
// Add compute budget program to account keys if not present
let computeBudgetProgramIndex = existingAccountKeys.findIndex(
key => key.equals(ComputeBudgetProgram.programId)
);
if (computeBudgetProgramIndex === -1) {
computeBudgetProgramIndex = existingAccountKeys.length;
existingAccountKeys.push(ComputeBudgetProgram.programId);
}
// Create new instructions array with compute budget instructions
const newInstructions = [
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computeBudgetIx.data
},
{
programIdIndex: computeBudgetProgramIndex,
accountKeyIndexes: [],
data: computePriceIx.data
},
...existingInstructions
];
// Create new versioned message with proper instruction mapping
const newMessage = new TransactionMessage({
payerKey: existingAccountKeys[0],
recentBlockhash: gasData.blockhash,
instructions: newInstructions.map(ix => ({
programId: existingAccountKeys[ix.programIdIndex],
keys: ix.accountKeyIndexes.map(idx => ({
pubkey: existingAccountKeys[idx],
isSigner: false,
isWritable: false
})).filter(key => key.pubkey),
data: Buffer.from(ix.data)
})).filter(ix => ix.programId)
}).compileToV0Message();
// Create and return new transaction
const preparedTransaction = new VersionedTransaction(newMessage);
return { transaction: preparedTransaction, gasData };
} catch (error) {
console.error('Error preparing transaction:', error);
throw error;
}
}
5.3 使用交易上链 API
广播交易
使用 交易上链 API
对于拥有 交易上链 API 访问权限的开发者,您可以直接通过 OKX 的基础设施广播交易。此方法可为高交易量操作提供增强的可靠性和监控能力。
广播 API 针对白名单客户开放。请联系 dexapi@okx.com 申请访问权限。
async function broadcastTransaction(
signedTx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction
) {
try {
const serializedTx = signedTx.serialize();
const encodedTx = base58.encode(serializedTx);
const path = "dex/pre-transaction/broadcast-transaction";
const url = `https://web3.okx.com/api/v5/${path}`;
const broadcastData = {
signedTx: encodedTx,
chainIndex: SOLANA_CHAIN_ID,
address: userAddress
// See [MEV Section](#8-mev-protection) for MEV protection settings
};
// Prepare authentication with body included in signature
const bodyString = JSON.stringify(broadcastData);
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const headers = getHeaders(timestamp, 'POST', requestPath, "", bodyString);
const response = await axios.post(url, broadcastData, { headers });
if (response.data.code === '0') {
const orderId = response.data.data[0].orderId;
console.log(`Transaction broadcast successfully, Order ID: ${orderId}`);
return orderId;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to broadcast transaction:', error);
throw error;
}
}
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.
async function signAndBroadcastTransaction(
tx: solanaWeb3.Transaction | solanaWeb3.VersionedTransaction,
connection: Connection
) {
if (!userPrivateKey) {
throw new Error("Private key not found");
}
const feePayer = solanaWeb3.Keypair.fromSecretKey(
base58.decode(userPrivateKey)
);
// Sign the transaction
if (tx instanceof solanaWeb3.VersionedTransaction) {
tx.sign([feePayer]);
} else {
tx.partialSign(feePayer);
}
// Send the transaction with retry logic
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const txId = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
preflightCommitment: 'processed',
maxRetries: 0 // Handle retries manually
});
console.log(`Transaction sent: ${txId}`);
// Wait for confirmation with timeout
const confirmation = await connection.confirmTransaction({
signature: txId,
blockhash: tx instanceof solanaWeb3.VersionedTransaction
? tx.message.recentBlockhash
: tx.recentBlockhash!,
lastValidBlockHeight: tx instanceof solanaWeb3.VersionedTransaction
? undefined
: tx.lastValidBlockHeight!
}, 'confirmed');
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log(`Transaction confirmed: https://solscan.io/tx/${txId}`);
return txId;
} catch (error) {
attempt++;
console.warn(`Attempt ${attempt} failed:`, error);
if (attempt >= maxRetries) {
throw new Error(`Transaction failed after ${maxRetries} attempts: ${error}`);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
Swap Transaction Using Compute Unit Data#
Here's a complete example that demonstrates the full flow from getting swap data to preparing transactions with proper compute unit estimation:
import { getHeaders } from '../../shared';
import {
Connection,
VersionedTransaction,
ComputeBudgetProgram,
TransactionMessage
} from "@solana/web3.js";
import base58 from 'bs58';
import dotenv from 'dotenv';
dotenv.config();
// Simple connection to one RPC endpoint
const connection = new Connection(
process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);
async function getQuote(params: any) {
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const queryString = "?" + new URLSearchParams({
...params,
}).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
const response = await fetch(`https://web3.okx.com${requestPath}${queryString}`, {
method: "GET",
headers
});
const data = await response.json();
return data;
}
/**
* * Get compute units using 交易上链API (API registration and whitelist required)
*/
async function getComputeUnitsFromAPI(
fromAddress: string,
inputData: string
): Promise<number> {
try {
const path = 'dex/pre-transaction/gas-limit';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: "501", // Solana chain ID
fromAddress: fromAddress,
toAddress: "", // Can be empty for Solana
txAmount: "0",
extJson: {
inputData: inputData
}
};
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 fetch(url, {
method: 'POST',
headers,
body: bodyString
});
const data = await response.json();
if (data.code === '0') {
const computeUnits = parseInt(data.data[0].gasLimit);
console.log(`API estimated compute units: ${computeUnits}`);
return computeUnits;
} else {
throw new Error(`API Error: ${data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to get compute units from API, falling back to simulation:', error);
// Fallback to RPC simulation
return 300000;
}
}
/**
* Execute a complete Solana transaction with proper compute unit estimation
*/
async function executeTransaction(): Promise<{
quote: any;
gasData: {
estimatedComputeUnits: number;
priorityFee: number;
blockhash: string;
};
preparedTransaction: string;
}> {
try {
console.log('Getting Solana swap data...');
// Step 1: Get swap data from OKX DEX API
const quote = await getQuote({
chainId: '501', // Solana chain ID
amount: '10000000', // 0.01 SOL in lamports
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
userWalletAddress: "YOUR_WALLET_ADDRESS",
slippage: '0.1',
autoSlippage: "true",
maxAutoSlippageBps: "100"
});
console.log('Quote response:', JSON.stringify(quote, null, 2));
// Step 2: Process transaction data if available
if (quote.data && quote.data[0] && quote.data[0].tx && quote.data[0].tx.data) {
console.log('\nGetting gas data for transaction...');
// Step 3: Create transaction from the data
const decodedTransaction = base58.decode(quote.data[0].tx.data);
const transaction = VersionedTransaction.deserialize(decodedTransaction);
// Step 4: Get compute units using API (API whitelist required) or fallback to RPC
const userWalletAddress = "YOUR_WALLET_ADDRESS";
let estimatedComputeUnits: number;
try {
// Try API first (API whitelist required)
estimatedComputeUnits = await getComputeUnitsFromAPI(
userWalletAddress,
quote.data[0].tx.data
);
console.log('Using API estimate for compute units');
} catch (error) {
// Fallback to RPC simulation
estimatedComputeUnits = await getComputeUnits(transaction);
console.log('Using RPC simulation for compute units');
}
// Step 5: Prepare transaction with compute units
const { transaction: preparedTransaction, gasData } = await prepareTransactionWithComputeUnits(transaction);
// Override with API estimate if we got one
gasData.estimatedComputeUnits = estimatedComputeUnits;
console.log('\nGas Data Summary:');
console.log('Blockhash:', gasData.blockhash);
console.log('Estimated Compute Units:', gasData.estimatedComputeUnits);
console.log('Priority Fee:', gasData.priorityFee, 'microLamports');
console.log('Transaction prepared successfully');
// Return the complete result
const result = {
quote: quote.data[0],
gasData,
preparedTransaction: Buffer.from(preparedTransaction.serialize()).toString('base64')
};
console.log('\nFinal Result:', JSON.stringify(result, null, 2));
return result;
} else {
throw new Error('No transaction data received from swap API');
}
} catch (error) {
console.error("Error executing transaction:", error);
throw error;
}
}
// Example usage
async function main() {
try {
await executeTransaction();
} catch (error) {
console.error('Failed to prepare transaction:', error);
process.exit(1);
}
}
// Run if this file is executed directly
if (require.main === module) {
main();
}
export { executeTransaction, prepareTransactionWithComputeUnits, getComputeUnits, getComputeUnitsFromAPI };
6.追踪交易#
最后,创建一个交易追踪系统,对于基本交易的状态确认,请选择第一个(6.1 部分);当您需要有关兑换执行本身的详细信息时,请选择第二个(6.2部分)。
6.1 使用 交易上链 API
交易上链 API 通过 /dex/post-transaction/orders
提供交易追踪功能。使用广播 API 返回的订单ID,并设置简单的状态代码(1:待处理,2:成功,3:失败),即可追踪交易在 OKX 系统中的进展。
// Define transaction status interface
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
/**
* Tracking transaction confirmation status using the Onchain gateway API
* @param orderId - Order ID from broadcast response
* @param intervalMs - Polling interval in milliseconds
* @param timeoutMs - Maximum time to wait
* @returns Final transaction confirmation status
*/
async function trackTransaction(
orderId: string,
intervalMs: number = 5000,
timeoutMs: number = 300000
): Promise<any> {
console.log(`Tracking transaction with Order ID: ${orderId}`);
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
// Get transaction status
try {
const path = 'dex/post-transaction/orders';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
orderId: orderId,
chainIndex: SOLANA_CHAIN_ID,
address: userAddress,
limit: '1'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0' && response.data.data && response.data.data.length > 0) {
if (response.data.data[0].orders && response.data.data[0].orders.length > 0) {
const txData = response.data.data[0].orders[0];
// Use txStatus to match the API response
const status = txData.txStatus;
// Only log when status changes
if (status !== lastStatus) {
lastStatus = status;
if (status === '1') {
console.log(`Transaction pending: ${txData.txHash || 'Hash not available yet'}`);
} else if (status === '2') {
console.log(`Transaction successful: https://solscan.io/tx/${txData.txHash}`);
return txData;
} else if (status === '3') {
const failReason = txData.failReason || 'Unknown reason';
const errorMessage = `Transaction failed: ${failReason}`;
console.error(errorMessage);
const errorInfo = handleTransactionError(txData);
console.log(`Error type: ${errorInfo.error}`);
console.log(`Suggested action: ${errorInfo.action}`);
throw new Error(errorMessage);
}
}
} else {
console.log(`No orders found for Order ID: ${orderId}`);
}
}
} catch (error) {
console.warn('Error checking transaction status:', (error instanceof Error ? error.message : "Unknown error"));
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Transaction tracking timed out');
}
/**
* Comprehensive error handling with failReason
* @param txData - Transaction data from post-transaction/orders
* @returns Structured error information
*/
function handleTransactionError(txData: any): TxErrorInfo {
const failReason = txData.failReason || 'Unknown reason';
// Log the detailed error
console.error(`Transaction failed with reason: ${failReason}`);
// Default error info
let errorInfo: TxErrorInfo = {
error: 'TRANSACTION_FAILED',
message: failReason,
action: 'Try again or contact support'
};
// More specific error handling based on the failure reason
if (failReason.includes('insufficient funds')) {
errorInfo = {
error: 'INSUFFICIENT_FUNDS',
message: 'Your wallet does not have enough funds to complete this transaction',
action: 'Add more SOL to your wallet to cover the transaction'
};
} else if (failReason.includes('blockhash')) {
errorInfo = {
error: 'BLOCKHASH_EXPIRED',
message: 'The transaction blockhash has expired',
action: 'Try again with a fresh transaction'
};
} else if (failReason.includes('compute budget')) {
errorInfo = {
error: 'COMPUTE_BUDGET_EXCEEDED',
message: 'Transaction exceeded compute budget',
action: 'Increase compute units or simplify the transaction'
};
}
return errorInfo;
}
6.2 如需更详细的兑换信息,您可以使用 SWAP API:
SWAP API 使用 /dex/aggregator/history
提供全面交易跟踪
/**
* Track transaction using SWAP API
* @param chainId - Chain ID (e.g., 501 for Solana)
* @param txHash - Transaction hash
* @returns Transaction details
*/
async function trackTransactionWithSwapAPI(
txHash: string
): Promise<any> {
try {
const path = 'dex/aggregator/history';
const url = `https://web3.okx.com/api/v5/${path}`;
const params = {
chainId: SOLANA_CHAIN_ID,
txHash: txHash,
isFromMyProject: 'true'
};
// Prepare authentication
const timestamp = new Date().toISOString();
const requestPath = `/api/v5/${path}`;
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, 'GET', requestPath, queryString);
const response = await axios.get(url, { params, headers });
if (response.data.code === '0') {
const txData = response.data.data[0];
const status = txData.status;
if (status === 'pending') {
console.log(`Transaction is still pending: ${txHash}`);
return { status: 'pending', details: txData };
} else if (status === 'success') {
console.log(`Transaction successful!`);
console.log(`From: ${txData.fromTokenDetails.symbol} - Amount: ${txData.fromTokenDetails.amount}`);
console.log(`To: ${txData.toTokenDetails.symbol} - Amount: ${txData.toTokenDetails.amount}`);
console.log(`Transaction Fee: ${txData.txFee}`);
console.log(`Explorer URL: https://solscan.io/tx/${txHash}`);
return { status: 'success', details: txData };
} else if (status === 'failure') {
console.error(`Transaction failed: ${txData.errorMsg || 'Unknown reason'}`);
return { status: 'failure', details: txData };
}
return txData;
} else {
throw new Error(`API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to track transaction status:', (error instanceof Error ? error.message : "Unknown error"));
throw error;
}
}
7.完整实现#
这是一个完整的实现示例:
import { getHeaders } from '../../shared';
import { Connection, PublicKey, Transaction, Keypair, VersionedTransaction, SystemProgram } from '@solana/web3.js';
import * as axios from 'axios';
import bs58 from 'bs58';
// // Utility function for OKX API authentication
// function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "", body = "") {
// const stringToSign = timestamp + method + requestPath + (queryString || body);
// return {
// "Content-Type": "application/json",
// "OK-ACCESS-KEY": apiKey,
// "OK-ACCESS-SIGN": cryptoJS.enc.Base64.stringify(
// cryptoJS.HmacSHA256(stringToSign, secretKey)
// ),
// "OK-ACCESS-TIMESTAMP": timestamp,
// "OK-ACCESS-PASSPHRASE": apiPassphrase,
// "OK-ACCESS-PROJECT": projectId,
// };
// Environment variables
const WALLET_ADDRESS = process.env.SOLANA_WALLET_ADDRESS;
const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY;
const chainId = '501'; // Solana Mainnet
const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
// Constants
const SOL_ADDRESS = '11111111111111111111111111111111'; // Native SOL
const USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC
// Initialize Solana connection
const connection = new Connection(rpcUrl, 'confirmed');
// Type definitions
interface GasLimitApiResponse {
code: string;
msg?: string;
data: Array<{
gasLimit: string;
}>;
}
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 BroadcastApiResponse {
code: string;
msg?: string;
data: Array<{
orderId: string;
}>;
}
interface TxErrorInfo {
error: string;
message: string;
action: string;
}
// ============================================================================
// API Functions
// ============================================================================
/**
* Get gas limit from 交易上链 API
*/
async function getGasLimit(
fromAddress: string,
toAddress: string,
txAmount: string = '0',
inputData: string = ''
): Promise<string> {
try {
console.log('Getting gas limit from 交易上链 API...');
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<GasLimitApiResponse>(url, body, { headers });
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 timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const queryString = "?" + new URLSearchParams({
chainIndex: chainId,
fromTokenAddress,
toTokenAddress,
amount,
slippage,
userWalletAddress: WALLET_ADDRESS!,
autoSlippage: "false",
maxAutoSlippageBps: "0"
}).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
const response = await fetch(`https://web3.okx.com${requestPath}${queryString}`, {
method: "GET",
headers
});
if (!response.ok) {
throw new Error(`Failed to get swap data: ${response.status} ${await response.text()}`);
}
const data = await response.json();
console.log('Swap data obtained');
return data.data[0]; // Return only the first swap data object
} catch (error) {
console.error('Failed to get swap data:', (error as Error).message);
throw error;
}
}
/**
* Simulate transaction using 交易上链 API
*/
async function simulateTransaction(swapData: any) {
try {
console.log('Simulating transaction with 交易上链 API...');
const path = 'dex/pre-transaction/simulate';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
chainIndex: chainId,
fromAddress: swapData.tx.from,
toAddress: swapData.tx.to,
txAmount: swapData.tx.value,
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 });
if (response.data.code === '0') {
const simulationData = response.data.data[0];
if (simulationData.failReason) {
throw new Error(`Simulation failed: ${simulationData.failReason}`);
}
console.log(`Transaction simulation successful. Gas used: ${simulationData.gasUsed}`);
console.log('Simulation API Response:', simulationData);
return simulationData;
} 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 交易上链 API with RPC fallback
*/
async function broadcastTransaction(
signedTx: string,
chainId: string,
walletAddress: string
): Promise<string> {
try {
console.log('Broadcasting transaction via 交易上链 API...');
const path = 'dex/pre-transaction/broadcast-transaction';
const url = `https://web3.okx.com/api/v5/${path}`;
const body = {
signedTx: signedTx,
chainIndex: chainId,
address: walletAddress
};
console.log('Broadcast request body:', 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 });
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('OKX API broadcast failed:', (error as Error).message);
// Fallback to direct RPC broadcast
try {
console.log('Attempting direct RPC broadcast as fallback...');
// Decode the signed transaction
const txBytes = bs58.decode(signedTx);
// Send directly to Solana RPC
const signature = await connection.sendRawTransaction(txBytes, {
skipPreflight: false,
preflightCommitment: 'processed'
});
console.log(`Direct RPC broadcast successful. Signature: ${signature}`);
// Wait for confirmation
const confirmation = await connection.confirmTransaction(signature, 'confirmed');
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log(`Transaction confirmed: https://solscan.io/tx/${signature}`);
return signature;
} catch (rpcError) {
console.error('RPC broadcast also failed:', (rpcError as Error).message);
throw new Error(`Both OKX API and RPC broadcast failed. OKX Error: ${(error as Error).message}, RPC Error: ${(rpcError as Error).message}`);
}
}
}
/**
* Track transaction status using 交易上链 API
*/
async function trackTransaction(
orderId: string,
intervalMs: number = 5000,
timeoutMs: number = 180000 // Reduced timeout to 3 minutes
): Promise<any> {
console.log(`Tracking transaction with Order ID: ${orderId}`);
const startTime = Date.now();
let lastStatus = '';
let pendingCount = 0;
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') {
pendingCount++;
console.log(`Transaction pending (${pendingCount}): ${txData.txHash || 'Hash not available yet'}`);
// If pending too long without a hash, something is wrong
if (pendingCount > 12 && !txData.txHash) { // 1 minute of pending without hash
console.warn('Transaction has been pending for too long without a transaction hash. This may indicate an issue.');
}
} else if (status === '2') {
console.log(`Transaction successful: https://web3.okx.com/explorer/solana/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 if (status === '1') {
pendingCount++;
// Show progress for long pending transactions
if (pendingCount % 6 === 0) { // Every 30 seconds
const elapsed = Math.round((Date.now() - startTime) / 1000);
console.log(`Still pending... (${elapsed}s elapsed)`);
}
}
} else {
console.log(`No orders found for Order ID: ${orderId}`);
}
} else {
console.log('No response data from tracking API');
}
} 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 after ${timeoutMs/1000} seconds. The transaction may still be processing.`);
}
// ============================================================================
// Transaction Signing Functions
// ============================================================================
/**
* Sign transaction with private key - Fixed OKX approach with gas limit analysis
*/
async function signTransaction(swapData: any, gasLimit: string): Promise<string> {
try {
console.log('Signing transaction...');
if (!PRIVATE_KEY) {
throw new Error('Private key not found in environment variables');
}
// Create keypair from private key
const privateKeyBytes = bs58.decode(PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);
if (!swapData.tx || !swapData.tx.data) {
throw new Error('No transaction data found in swap response');
}
const callData = swapData.tx.data;
console.log('Transaction data length:', callData.length);
console.log('Gas limit from API:', gasLimit);
try {
// Decode the base58 encoded transaction data (this is the correct approach)
const decodedTransaction = bs58.decode(callData);
console.log('Decoded transaction bytes length:', decodedTransaction.length);
// Get the latest blockhash (CRITICAL!)
const recentBlockHash = await connection.getLatestBlockhash();
console.log('Got recent blockhash:', recentBlockHash.blockhash);
let transaction: Transaction | VersionedTransaction;
// Try VersionedTransaction first (more common for modern Solana programs)
try {
transaction = VersionedTransaction.deserialize(decodedTransaction);
console.log('Successfully deserialized as VersionedTransaction');
// DEBUGGING: Let's see what instructions are already in the transaction
console.log('Number of instructions in OKX transaction:', transaction.message.compiledInstructions.length);
// Check if there are already ComputeBudget instructions
const computeBudgetProgram = new PublicKey('ComputeBudget111111111111111111111111111111');
const computeBudgetIndex = transaction.message.staticAccountKeys.findIndex(
key => key.equals(computeBudgetProgram)
);
if (computeBudgetIndex !== -1) {
console.log('ComputeBudget program found at index:', computeBudgetIndex);
// Check which instructions use the ComputeBudget program
const computeBudgetInstructions = transaction.message.compiledInstructions.filter(
ix => ix.programIdIndex === computeBudgetIndex
);
console.log('Number of ComputeBudget instructions:', computeBudgetInstructions.length);
// Analyze each ComputeBudget instruction
computeBudgetInstructions.forEach((ix, i) => {
const data = ix.data;
if (data.length > 0) {
const instructionType = data[0];
console.log(`ComputeBudget instruction ${i}: type ${instructionType}`);
if (instructionType === 0 && data.length >= 5) {
// SetComputeUnitLimit instruction
const computeUnits = new Uint32Array(data.slice(1, 5).buffer)[0];
console.log(` - Current compute unit limit: ${computeUnits}`);
console.log(` - Gas limit from API: ${gasLimit}`);
// Check if we need to update it
const apiGasLimit = parseInt(gasLimit);
if (computeUnits !== apiGasLimit) {
console.log(` - Compute units mismatch! OKX: ${computeUnits}, API: ${apiGasLimit}`);
// We could potentially update this here
}
} else if (instructionType === 1 && data.length >= 9) {
// SetComputeUnitPrice instruction
const microLamports = new BigUint64Array(data.slice(1, 9).buffer)[0];
console.log(` - Current compute unit price: ${microLamports} microlamports`);
}
}
});
} else {
console.log('No ComputeBudget program found - OKX transaction may not have compute budget instructions');
console.log('We should add ComputeBudget instruction with gas limit:', gasLimit);
// Add ComputeBudget instruction since OKX didn't include one
const setComputeUnitLimitData = Buffer.alloc(5);
setComputeUnitLimitData[0] = 0; // SetComputeUnitLimit instruction
setComputeUnitLimitData.writeUInt32LE(parseInt(gasLimit), 1);
// Add the ComputeBudget program to static accounts
transaction.message.staticAccountKeys.push(computeBudgetProgram);
const programIndex = transaction.message.staticAccountKeys.length - 1;
// Add the compute budget instruction at the beginning
transaction.message.compiledInstructions.unshift({
programIdIndex: programIndex,
accountKeyIndexes: [],
data: setComputeUnitLimitData
});
console.log('Added ComputeBudget instruction with gas limit:', gasLimit);
}
// CRITICAL: Update the blockhash in the transaction message
transaction.message.recentBlockhash = recentBlockHash.blockhash;
// Sign the versioned transaction
transaction.sign([keypair]);
console.log('Signed VersionedTransaction');
} catch (versionedError) {
console.log('VersionedTransaction failed, trying legacy Transaction');
try {
transaction = Transaction.from(decodedTransaction);
console.log('Successfully deserialized as legacy Transaction');
// DEBUGGING: Check legacy transaction instructions
console.log('Number of instructions in legacy transaction:', transaction.instructions.length);
// Check for ComputeBudget instructions in legacy format
const computeBudgetProgram = new PublicKey('ComputeBudget111111111111111111111111111111');
const computeBudgetInstructions = transaction.instructions.filter(
ix => ix.programId.equals(computeBudgetProgram)
);
if (computeBudgetInstructions.length === 0) {
console.log('No ComputeBudget instructions found in legacy transaction');
console.log('Adding ComputeBudget instruction with gas limit:', gasLimit);
// Add ComputeBudget instruction
const setComputeUnitLimitData = Buffer.alloc(5);
setComputeUnitLimitData[0] = 0; // SetComputeUnitLimit instruction
setComputeUnitLimitData.writeUInt32LE(parseInt(gasLimit), 1);
const computeBudgetIx = {
programId: computeBudgetProgram,
keys: [],
data: setComputeUnitLimitData
};
// Add at the beginning
transaction.instructions.unshift(computeBudgetIx);
console.log('Added ComputeBudget instruction to legacy transaction');
} else {
console.log('Found existing ComputeBudget instructions:', computeBudgetInstructions.length);
}
// CRITICAL: Update the blockhash in the transaction
transaction.recentBlockhash = recentBlockHash.blockhash;
// Sign the legacy transaction
transaction.sign(keypair);
console.log('Signed legacy Transaction');
} catch (legacyError) {
console.log('Both transaction types failed to deserialize');
console.log('VersionedTransaction error:', (versionedError as Error).message);
console.log('Legacy Transaction error:', (legacyError as Error).message);
// This should not happen with proper OKX data
throw new Error('Failed to deserialize OKX transaction data. Data may be corrupted.');
}
}
// Serialize and encode the signed transaction
const serializedTx = transaction.serialize();
const encodedTx = bs58.encode(serializedTx);
console.log('Transaction signed and encoded successfully');
return encodedTx;
} catch (error) {
console.log('Failed to process OKX transaction data:', (error as Error).message);
// If we reach here, the OKX data is not in expected format
throw new Error(`Cannot process OKX transaction data: ${(error as Error).message}`);
}
} catch (error) {
console.error('Failed to sign transaction:', (error as Error).message);
throw error;
}
}
// ============================================================================
// Error Handling
// ============================================================================
/**
* Comprehensive error handling with failReason
*/
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 Functions
// ============================================================================
/**
* 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
);
console.log('Gas limit obtained');
// Step 4: Check account balance
if (!(swapData.tx && swapData.tx.data)) {
throw new Error('No valid transaction data found in swap API response (tx.data missing)');
}
console.log('Checking account balance...');
const fromPubkey = new PublicKey(swapData.tx.from);
const balance = await connection.getBalance(fromPubkey);
console.log(`Account balance: ${balance / 1e9} SOL`);
// Check if we have enough balance for the transaction
const requiredAmount = parseInt(swapData.tx.value || '0');
console.log(`Required amount: ${requiredAmount / 1e9} SOL`);
if (balance < requiredAmount) {
throw new Error(`Insufficient balance. Required: ${requiredAmount / 1e9} SOL, Available: ${balance / 1e9} SOL`);
}
// Step 5: Sign the transaction with private key
console.log('Signing transaction with private key...');
const signedTx = await signTransaction(swapData, gasLimit);
console.log('Transaction signed successfully');
// Step 6: Broadcast transaction
console.log('Broadcasting signed transaction via 交易上链 API...');
const txHash = await broadcastTransaction(signedTx, chainId, WALLET_ADDRESS!);
console.log(`Transaction broadcast successful. Hash: ${txHash}`);
// Step 7: 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 };
}
}
/**
* Simulation-only mode
*/
async function simulateOnly(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.05'
): 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
);
console.log('Gas limit obtained');
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 };
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
async function main() {
try {
console.log('Solana Swap Tools with 交易上链 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: ${rpcUrl}`);
// Parse command line arguments
const args = process.argv.slice(2);
const mode = args[0] || 'simulate'; // Default to simulate mode
// Example parameters
const fromToken = SOL_ADDRESS;
const toToken = USDC_ADDRESS;
const amount = '10000000'; // 0.01 SOL in lamports
const slippage = '0.05'; // 0.5%
console.log('\nConfiguration:');
console.log(` From: ${fromToken} (SOL)`);
console.log(` To: ${toToken} (USDC)`);
console.log(` Amount: ${parseInt(amount) / 1e9} SOL`);
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 solana-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();
}
// ============================================================================
// Exports
// ============================================================================
export {
executeSwap,
executeSwapWithSimulation,
simulateOnly,
getSwapData,
simulateTransaction,
getGasLimit,
broadcastTransaction,
trackTransaction,
signTransaction
};
You can run this script using solana-swap-executor.ts sim
or solana-swap-executor.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
8. MEV保护#
Solana交易存在MEV(最大可提取价值)风险。虽然MEV保护不直接包含在SDK中,但您可以使用API优先的方法自行实施。
第一道防线使用动态优先级费用-将其视为您在拍卖中对抗MEV机器人的出价。
/**
* Broadcast transaction with MEV protection enabled
*/
async function broadcastTransactionWithMEV(
signedTx: string,
chainId: string = "501",
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;
}
}
Jito Integration for Enhanced Protection#
For additional MEV protection on Solana, you can include Jito-specific parameters:
/**
* Broadcast transaction with Jito MEV protection
*/
async function broadcastTransactionWithJito(
signedTx: string,
jitoSignedTx: string,
chainId: string = "501",
walletAddress: string
): Promise<string> {
try {
console.log('Broadcasting transaction with Jito 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: true,
jitoSignedTx: jitoSignedTx
})
};
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 Jito protection. Order ID: ${orderId}`);
return orderId;
} else {
throw new Error(`Broadcast API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Jito-protected broadcast failed:', error);
throw error;
}
}
Updated Broadcast Function with MEV Parameters#
Here's the updated broadcastTransaction
function that includes MEV protection parameters:
/**
* Enhanced broadcast transaction with MEV protection parameters
*/
async function broadcastTransaction(
signedTx: string,
chainId: string = "501",
walletAddress: string,
enableMevProtection: boolean = false,
jitoSignedTx: string = ""
): Promise<string> {
try {
console.log(`Broadcasting transaction${enableMevProtection ? ' with MEV protection' : ''}...`);
const path = 'dex/pre-transaction/broadcast-transaction';
const url = `https://web3.okx.com/api/v5/${path}`;
const body: any = {
signedTx: signedTx,
chainIndex: chainId,
address: walletAddress
};
// Add MEV protection parameters if enabled
if (enableMevProtection || jitoSignedTx) {
const extraData: any = {};
if (enableMevProtection) {
extraData.enableMevProtection = true;
}
if (jitoSignedTx) {
extraData.jitoSignedTx = jitoSignedTx;
}
body.extraData = JSON.stringify(extraData);
}
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 successful. Order ID: ${orderId}`);
return orderId;
} else {
throw new Error(`Broadcast API Error: ${response.data.msg || 'Unknown error'}`);
}
} catch (error) {
console.error('Broadcast failed:', error);
throw error;
}
}
Usage Examples#
Basic swap without MEV protection:
// Standard broadcast (no MEV protection)
const orderId = await broadcastTransaction(signedTx, "501", walletAddress);
Swap with MEV protection enabled:
// With MEV protection
const orderId = await broadcastTransaction(signedTx, "501", walletAddress, true);
Swap with Jito MEV protection:
// With Jito protection
const orderId = await broadcastTransaction(signedTx, "501", walletAddress, true, jitoSignedTransaction);
Swap with only Jito (no general MEV protection):
// Only Jito protection
const orderId = await broadcastTransaction(signedTx, "501", walletAddress, false, jitoSignedTransaction);
Integration with Complete Swap Flow#
Here's how to integrate MEV protection into your complete swap execution:
/**
* Execute swap with MEV protection
*/
async function executeSwapWithMEVProtection(
fromTokenAddress: string,
toTokenAddress: string,
amount: string,
slippage: string = '0.005',
enableMevProtection: boolean = true
): Promise<string> {
try {
// Step 1: Get swap data
const swapData = await getSwapData(fromTokenAddress, toTokenAddress, amount, slippage);
// Step 2: Prepare and sign transaction
const { transaction } = await prepareTransaction(swapData.tx.data);
const signedTx = await signTransaction(transaction);
// Step 3: Broadcast with MEV protection
const orderId = await broadcastTransaction(signedTx, "501", userAddress, enableMevProtection);
// Step 4: 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、以太坊和 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
# Solana Configuration
SOLANA_RPC_URL=your_solana_rpc_url
SOLANA_WALLET_ADDRESS=your_solana_wallet_address
SOLANA_PRIVATE_KEY=your_solana_private_key
# Ethereum 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):
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);
// Solana setup
const solanaConnection = new Connection(process.env.SOLANA_RPC_URL!);
const solanaWallet = createWallet(process.env.SOLANA_PRIVATE_KEY!, solanaConnection);
// 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
},
// Solana configuration
solana: {
wallet: solanaWallet,
computeUnits: 300000, // Optional
maxRetries: 3 // Optional
},
})
4.调用SDK执行兑换#
创建兑换执行的文件:
// swap.ts
import { client } from './DexClient';
/**
* Example: Execute a swap from SOL to USDC
*/
async function executeSwap() {
try {
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('Missing SOLANA_PRIVATE_KEY in .env file');
}
// Get quote to fetch token information
console.log("Getting token information...");
const quote = await client.dex.getQuote({
chainId: '501',
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: '1000000', // Small amount for quote
slippage: '0.005' // 0.5% slippage
});
const tokenInfo = {
fromToken: {
symbol: quote.data[0].fromToken.tokenSymbol,
decimals: parseInt(quote.data[0].fromToken.decimal),
price: quote.data[0].fromToken.tokenUnitPrice
},
toToken: {
symbol: quote.data[0].toToken.tokenSymbol,
decimals: parseInt(quote.data[0].toToken.decimal),
price: quote.data[0].toToken.tokenUnitPrice
}
};
// Convert amount to base units (for display purposes)
const humanReadableAmount = 0.1; // 0.1 SOL
const rawAmount = (humanReadableAmount * Math.pow(10, tokenInfo.fromToken.decimals)).toString();
console.log("\nSwap Details:");
console.log("--------------------");
console.log(`From: ${tokenInfo.fromToken.symbol}`);
console.log(`To: ${tokenInfo.toToken.symbol}`);
console.log(`Amount: ${humanReadableAmount} ${tokenInfo.fromToken.symbol}`);
console.log(`Amount in base units: ${rawAmount}`);
console.log(`Approximate USD value: $${(humanReadableAmount * parseFloat(tokenInfo.fromToken.price)).toFixed(2)}`);
// Execute the swap
console.log("\nExecuting swap...");
const swapResult = await client.dex.executeSwap({
chainId: '501', // Solana chain ID
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: rawAmount,
slippage: '0.005', // 0.5% slippage
userWalletAddress: process.env.SOLANA_WALLET_ADDRESS!
});
console.log('Swap executed successfully:');
console.log(JSON.stringify(swapResult, null, 2));
return swapResult;
} catch (error) {
if (error instanceof Error) {
console.error('Error executing swap:', error.message);
// API errors include details in the message
if (error.message.includes('API Error:')) {
const match = error.message.match(/API Error: (.*)/);
if (match) console.error('API Error Details:', match[1]);
}
}
throw error;
}
}
// Run if this file is executed directly
if (require.main === module) {
executeSwap()
.then(() => process.exit(0))
.catch((error) => {
console.error('Error:', error);
process.exit(1);
});
}
export { executeSwap };
5.附加SDK功能#
SDK提供了简化开发的附加方法: 获取代币对的报价
const quote = await client.dex.getQuote({
chainId: '501', // Solana
fromTokenAddress: '11111111111111111111111111111111', // SOL
toTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: '100000000', // 0.1 SOL (in lamports)
slippage: '0.005' // 0.5% slippage
});