在 Solana 链上搭建兑换应用#
在本指南中,我们将通过欧易 DEX 提供一个用例来进行 Solana 代币兑换。这个过程包括:
- 设置你的环境
- 获取 toTokenAddress 的代币账户地址
- 获取兑换路径
- 反序列化并签名
- 执行交易
1. 设置你的环境#
导入必要的 Node.js 库并设置你的环境变量以及定义辅助函数和组装参数 Node.js 环境设置
在以上基础上需要导入以下库
const bs58 = require('bs58');
const solanaWeb3 = require('@solana/web3.js');
const {Connection} = require("@solana/web3.js");
npm i bs58
npm i @solana/web3.js
2. 获取兑换路径及 callData#
- 通过 /swap 接口,获取详细兑换路径及 callData 数据,以 Solana 链 SOL 到 wSOL 兑换为例。
curl --location --request GET 'https://www.okx.com/api/v5/dex/aggregator/swap?amount=1000&chainId=501&fromTokenAddress=11111111111111111111111111111111&toTokenAddress=So11111111111111111111111111111111111111112&userWalletAddress=3cUbuUEJkcgtzGxvsukksNzmgqaUK9jwFS5pqxxxxxxx&slippage=0.05' \
3. 反序列化并签名#
// rpc
const connection = new Connection("xxxxxxxxxxx")
async function signTransaction(callData, privateKey) {
// decode
const transaction = bs58.decode(callData)
let tx
// There are two types of callData, one is the old version and the other is the new version.
try {
tx = solanaWeb3.Transaction.from(transaction)
} catch (error) {
tx = solanaWeb3.VersionedTransaction.deserialize(transaction)
}
// Replace the latest block hash
const recentBlockHash = await connection.getLatestBlockhash();
if (tx instanceof solanaWeb3.VersionedTransaction) {
tx.message.recentBlockhash = recentBlockHash.blockhash;
} else {
tx.recentBlockhash = recentBlockHash.blockhash
}
let feePayer = solanaWeb3.Keypair.fromSecretKey(bs58.decode(privateKey))
// sign
if (tx instanceof solanaWeb3.VersionedTransaction) {
// v0 callData
tx.sign([feePayer])
} else {
// legacy callData
tx.partialSign(feePayer)
}
console.log(tx)
}
// 'xxxxxxx' means your privateKey
signTransaction(callData,'xxxxxxx')
4. 执行交易#
const txId = await connection.sendRawTransaction(tx.serialize());
console.log('txId:', txId)
// Verify whether it has been broadcast on the chain.
await connection.confirmTransaction(txId);
console.log(`https://solscan.io/tx/${txId}`);
5.使用 TypeScript 来完成完整实现#
我们提供了一个实现 solana 代币兑换的 typescript 搭建实例库,它是我们 OKX DEX API 库的一部分。
RPC 配置:在继续之前,你需要选择一个 RPC 端点。虽然这个示例使用的是 Helius(https://mainnet.helius-rpc.com
), 你也可以选择任何你喜欢的 Solana RPC 提供商。建议使用第三方服务商,因为公共的 Solana RPC 端点可能会有调用服务限制。
免责声明:RPC 端点的选择完全由你决定。OKX 不对任何第三方 RPC 服务负责。请始终确保在生产环境中使用可靠和安全的 RPC 提供商。
环境设置#
创建一个 .env
文件,包含以下配置:
OKX_API_KEY=your_api_key
OKX_SECRET_KEY=your_secret_key
OKX_API_PASSPHRASE=your_passphrase
OKX_PROJECT_ID=your_project_id
WALLET_ADDRESS=your_wallet_address
PRIVATE_KEY=your_private_key
SOLANA_RPC_URL=your_rpc_url
可选 WS 节点
WS_ENDPONT=
完整的兑换实现#
以下实现提供了一个功能齐全的代币兑换解决方案:
// swap.ts
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 dotenv from 'dotenv';
dotenv.config();
// 环境变量
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;
// 常量
const SOLANA_CHAIN_ID = "501";
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;
const connection = new Connection(`${solanaRpcUrl}`, {
confirmTransactionInitialTimeout: 5000
// wsEndpoint: solanaWsUrl,
});
function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
throw new Error("缺少必要的环境变量");
}
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,
};
}
async function getTokenInfo(fromTokenAddress: string, toTokenAddress: string) {
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/quote";
const params = {
chainId: SOLANA_CHAIN_ID,
fromTokenAddress,
toTokenAddress,
amount: "1000000", // 小额金额用于获取代币信息
slippage: "0.5",
};
const queryString = "?" + new URLSearchParams(params).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
const response = await fetch(
`https://www.okx.com${requestPath}${queryString}`,
{ method: "GET", headers }
);
if (!response.ok) {
throw new Error(`获取报价失败: ${await response.text()}`);
}
const data = await response.json();
if (data.code !== "0" || !data.data?.[0]) {
throw new Error("获取代币信息失败");
}
const quoteData = data.data[0];
return {
fromToken: {
symbol: quoteData.fromToken.tokenSymbol,
decimals: parseInt(quoteData.fromToken.decimal),
price: quoteData.fromToken.tokenUnitPrice
},
toToken: {
symbol: quoteData.toToken.tokenSymbol,
decimals: parseInt(quoteData.toToken.decimal),
price: quoteData.toToken.tokenUnitPrice
}
};
}
function convertAmount(amount: string, decimals: number) {
try {
if (!amount || isNaN(parseFloat(amount))) {
throw new Error("无效的金额");
}
const value = parseFloat(amount);
if (value <= 0) {
throw new Error("金额必须大于0");
}
return new BN(value * Math.pow(10, decimals)).toString();
} catch (err) {
console.error("金额转换错误:", err);
throw new Error("无效的金额格式");
}
}
async function main() {
try {
const args = process.argv.slice(2);
if (args.length < 3) {
console.log("用法: ts-node swap.ts <amount> <fromTokenAddress> <toTokenAddress>");
console.log("示例: ts-node swap.ts 1.5 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
process.exit(1);
}
const [amount, fromTokenAddress, toTokenAddress] = args;
if (!userPrivateKey || !userAddress) {
throw new Error("未找到私钥或用户地址");
}
// 获取代币信息
console.log("获取代币信息...");
const tokenInfo = await getTokenInfo(fromTokenAddress, toTokenAddress);
console.log(`From: ${tokenInfo.fromToken.symbol} (${tokenInfo.fromToken.decimals} decimals)`);
console.log(`To: ${tokenInfo.toToken.symbol} (${tokenInfo.toToken.decimals} decimals)`);
// 使用获取的decimals转换金额
const rawAmount = convertAmount(amount, tokenInfo.fromToken.decimals);
console.log(`以${tokenInfo.fromToken.symbol}为单位的金额:`, rawAmount);
// 获取交换报价
const quoteParams = {
chainId: SOLANA_CHAIN_ID,
amount: rawAmount,
fromTokenAddress,
toTokenAddress,
slippage: "0.5",
userWalletAddress: userAddress,
} as Record<string, string>;
// 获取交换数据
const timestamp = new Date().toISOString();
const requestPath = "/api/v5/dex/aggregator/swap";
const queryString = "?" + new URLSearchParams(quoteParams).toString();
const headers = getHeaders(timestamp, "GET", requestPath, queryString);
console.log("请求交换报价...");
const response = await fetch(
`https://www.okx.com${requestPath}${queryString}`,
{ method: "GET", headers }
);
const data = await response.json();
if (data.code !== "0") {
throw new Error(`API错误: ${data.msg}`);
}
const swapData = data.data[0];
// 显示估计输出和价格影响
const outputAmount = parseFloat(swapData.routerResult.toTokenAmount) / Math.pow(10, tokenInfo.toToken.decimals);
console.log("\n交换报价:");
console.log(`输入: ${amount} ${tokenInfo.fromToken.symbol} ($${(parseFloat(amount) * parseFloat(tokenInfo.fromToken.price)).toFixed(2)})`);
console.log(`输出: ${outputAmount.toFixed(tokenInfo.toToken.decimals)} ${tokenInfo.toToken.symbol} ($${(outputAmount * parseFloat(tokenInfo.toToken.price)).toFixed(2)})`);
if (swapData.priceImpactPercentage) {
console.log(`价格影响: ${swapData.priceImpactPercentage}%`);
}
console.log("\n执行交换交易...");
let retryCount = 0;
while (retryCount < MAX_RETRIES) {
try {
if (!swapData || (!swapData.tx && !swapData.data)) {
throw new Error("无效的交换数据结构");
}
const transactionData = swapData.tx?.data || swapData.data;
if (!transactionData || typeof transactionData !== 'string') {
throw new Error("无效的交易数据");
}
const recentBlockHash = await connection.getLatestBlockhash();
console.log("获取到的区块哈希:", recentBlockHash.blockhash);
const decodedTransaction = base58.decode(transactionData);
let tx;
try {
tx = solanaWeb3.VersionedTransaction.deserialize(decodedTransaction);
console.log("成功创建版本化交易");
tx.message.recentBlockhash = recentBlockHash.blockhash;
} catch (e) {
console.log("版本化交易失败,尝试使用传统交易:", e);
tx = solanaWeb3.Transaction.from(decodedTransaction);
console.log("成功创建传统交易");
tx.recentBlockhash = recentBlockHash.blockhash;
}
const computeBudgetIx = solanaWeb3.ComputeBudgetProgram.setComputeUnitLimit({
units: COMPUTE_UNITS
});
const feePayer = solanaWeb3.Keypair.fromSecretKey(
base58.decode(userPrivateKey)
);
if (tx instanceof solanaWeb3.VersionedTransaction) {
tx.sign([feePayer]);
} else {
tx.partialSign(feePayer);
}
const txId = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
maxRetries: 5
});
const confirmation = await connection.confirmTransaction({
signature: txId,
blockhash: recentBlockHash.blockhash,
lastValidBlockHeight: recentBlockHash.lastValidBlockHeight
}, 'confirmed');
if (confirmation?.value?.err) {
throw new Error(`交易失败: ${JSON.stringify(confirmation.value.err)}`);
}
console.log("\n交换成功完成!");
console.log("交易 ID:", txId);
console.log("浏览器 URL:", `https://solscan.io/tx/${txId}`);
process.exit(0);
} catch (error) {
console.error(`尝试 ${retryCount + 1} 失败:`, error);
retryCount++;
if (retryCount === MAX_RETRIES) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, 2000 * retryCount));
}
}
} catch (error) {
console.error("错误:", error instanceof Error ? error.message : "未知错误");
process.exit(1);
}
}
if (require.main === module) {
main();
}
使用示例#
要执行兑换,请使用以下参数运行脚本:
// 例如 : Swap 0.01 SOL to USDC
npx ts-node swap.ts .01 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
将返回类似如下的响应:
From: SOL (9 位精度)
To: USDC (6 位精度)
基于 Sol 的最小单位,数量为:10000000
请求兑换报价
兑换询价
输入: 0.01 SOL ($1.82)
输出: 1.820087 USDC ($1.82)
执行兑换交易...
获得最新区块哈希: J7cWaf9UQJyN6SqatDHhmdAtP3skN7YKFCJnbaLeKf3r
成功创建版本化交易
兑换成功!
交易哈希:5LncQyzK7YmcodcsQMYwnjYBAYBkKJAaS1XR2RLiCVyPyA5nwHjUNuSQos4VGk4CJm5spRPngdnv8cQYjYYwCAVu
浏览器链接:https://solscan.io/tx/5LncQyzK7YmcodcsQMYwnjYBAYBkKJAaS1XR2RLiCVyPyA5nwHjUNuSQos4VGk4CJm5spRPngdnv8cQYjYYwCAVu
6. 开启 MEV 保护#
在任何链上进行交易都伴随着 MEV(最大可提取价值)风险,但这里有一些方法可以潜在地保护 Solana 上用户的交易。这种实现包括几个方法,开发者可以使用这些方法来最小化用户的 MEV 风险。
设置 MEV 保护的自动化方案#
第一个方案是通过设置动态优先费用来开启 MEV 保护,可以把它看作是你与 MEV 机器人竞争出价,让你的交易比 MEV 机器人更快被打包:
static async getPriorityFee(): Promise<number> {
const recentFees = await connection.getRecentPrioritizationFees();
const maxFee = Math.max(...recentFees.map(fee => fee.prioritizationFee));
return Math.min(maxFee * 1.5, MEV_PROTECTION.MAX_PRIORITY_FEE);
}
对于较大的交易,可以启用 TWAP(时间加权平均价格)。这样就不会命中 MEV 机器人的策略,他们通常会寻找大额交易,而我们将交易拆分成更小的部分避开他们的寻找策略:
if (MEV_PROTECTION.TWAP_ENABLED) {
const chunks = await TWAPExecution.splitTrade(
rawAmount,
fromTokenAddress,
toTokenAddress
);
}
MEV 自动化方案的具体策略#
当你使用此实现 执行交易时,实际有以下的策略发生
(1)交易前检查:
- 购买的代币会被检查是否存在貔貅盘特征
- 会检查你的网络费用来设置有竞争力的优先费用
- 查看你的交易额度来确认是否要拆单
(2)交易中:
- 大额交易将拆分成不同金额的单子,并在随机时间发出
- 每单将根据市场条件设置优先费用
- 使用特定的区块来减少暴露风险
(3)交易的安全性保障:
- 每笔交易都将进行预执行模拟
- 将对区块确认状态进行内置的追踪
- 如果交易出现问题会自动重试
需要说明的是,即使使用了上述策略,MEV 攻击仍然有可能发生,不能完全被阻止。但我们的这些保护措施会使得对交易进行 MEV 攻击成本更高。
特别提醒
我们也列出了一些开发过程中你需要注意的内容:
(1)对于适用 TWAP 的交易,请在代码中启用它:
MEV_PROTECTION.TWAP_ENABLED = true;
(2)在市场行情比较热的时候,你可能需要提高优先费用:
MEV_PROTECTION.PRIORITY_MULTIPLIER = 3; // More competitive
(3)根据交易的代币设置合适的滑点
CONFIG.SLIPPAGE = "0.5" // Standard setting
另外,通常更好的 MEV 保护意味着更慢的执行。如果你的最高优先级是交易速度,那么就需要接受更多的 MEV 风险,可以根据对应的用户需求进行平衡。
实际示例#
执行如下代码:
npx ts-node solana-swap-mev.ts .02 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
将会返回:
兑换成功!
交易哈希: 669uQvX6wRRGo3mUMvyPG5s9kFN9pCZsKER5kbByfWUKptWHTCUMpfycwMXC2RFMJpzYBKaPAMfCbxr3886fzkQY, 51nvyyGWQU3Nw8jo7g1Suq2sAZSUkgA7bSJ8upbFtR8bSsibs896R6Bifi6ucFmhuTP63cmsM8bKJiFz6AA14LxA, 5ov1cVk64adFVnnXZpizrdRFd4BvpASMwkkVTohRWtig5Fu519iQSahVbddvjRAtfcimNGg6XhN8cTaneVddc63j, 2ySKQq5gmfYZ1sJuCrz72aNFMknu943PBAw9ebRFtFeLpW4Q9PXjNTHwY1uiREVmvDiYGJZu9piKvBNDLorx5zi5
浏览器链接:
https://solscan.io/tx/669uQvX6wRRGo3mUMvyPG5s9kFN9pCZsKER5kbByfWUKptWHTCUMpfycwMXC2RFMJpzYBKaPAMfCbxr3886fzkQY
https://solscan.io/tx/51nvyyGWQU3Nw8jo7g1Suq2sAZSUkgA7bSJ8upbFtR8bSsibs896R6Bifi6ucFmhuTP63cmsM8bKJiFz6AA14LxA
https://solscan.io/tx/5ov1cVk64adFVnnXZpizrdRFd4BvpASMwkkVTohRWtig5Fu519iQSahVbddvjRAtfcimNGg6XhN8cTaneVddc63j
https://solscan.io/tx/2ySKQq5gmfYZ1sJuCrz72aNFMknu943PBAw9ebRFtFeLpW4Q9PXjNTHwY1uiREVmvDiYGJZu9piKvBNDLorx5zi5
其他可供参考的保护措施和思路#
虽然文档中的实现也为 MEV 攻击提供了较为坚实的防御机制,但实际上 Solana 上最有效的 MEV 保护发生在节点验证层。
节点验证者层解决方案可以在交易进入内存池之前拦截并保护交易,提供更底层的防御。然而,这些解决方案通常需要专门的基础设施,无法通过常规的 RPC 端点访问实现。
现实是:最有效的 MEV 保护结合了多种方法——通过智能合约保护,以及本实现中的设置交易策略保护,再到节点验证者层解决方案。每一层都有其独特的优势和特点,在与 MEV 的斗争中发挥作用。
开发者们可以根据自己应用的需要,来选择和实现。
7. 添加兑换指令#
当你需要对兑换过程进行更多控制和组装定制时,可使用 swap-instruction 接口。 已有的 /swap 兑换接口的作用是,直接返回了构建好的交易数据,可直接签名执行。但 swap-instruction 兑换指令接口允许你:
- 构建自定义的交易签名流程
- 按照你的需要处理指令
- 在已构建的交易添加自己的指令
- 直接使用查找表来优化交易数据大小
本指南将逐步介绍,如何使用兑换指令接口发起一笔完整的兑换交易。您将了解如何从 API 接口中获取指令、组装处理它们并将其构建成一个可用的交易。
设置您的环境#
导入必要的库并配置您的环境:
// 与 DEX 交互所需的 Solana 依赖项
import {
Connection, // 处理与 Solana 网络的 RPC 连接
Keypair, // 管理用于签名的钱包密钥对
PublicKey, // 处理 Solana 公钥的转换和验证
TransactionInstruction, // 核心交易指令类型
TransactionMessage, // 构建交易消息(v0 格式)
VersionedTransaction, // 支持带有查找表的新交易格式
RpcResponseAndContext, // RPC 响应包装类型
SimulatedTransactionResponse, // 模拟结果类型
AddressLookupTableAccount, // 用于交易大小优化
PublicKeyInitData // 公钥输入类型
} from "@solana/web3.js";
import base58 from "bs58"; // 用于私钥解码
import dotenv from "dotenv"; // 环境变量管理
dotenv.config();
初始化连接和钱包#
设置您的连接和钱包实例:
// 注意:在生产环境中,请考虑使用具有高速率限制的可靠 RPC 端点
const connection = new Connection(
process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);
// 初始化用于签名的钱包
// 该钱包将作为费用支付者和交易签名者
// 确保它有足够的 SOL 来支付交易费用
const wallet = Keypair.fromSecretKey(
Uint8Array.from(base58.decode(process.env.PRIVATE_KEY?.toString() || ""))
);
配置兑换参数#
设置您的兑换参数:
// 配置交换参数
const baseUrl = "https://beta.okex.org/api/v5/dex/aggregator/swap-instruction";
const params = {
chainId: "501", // Solana 主网链 ID
feePercent: "1", // 你计划收取的分佣费用百分比
amount: "1000000", // 最小单位金额(例如,SOL 的 lamports)
fromTokenAddress: "11111111111111111111111111111111", // SOL 铸币地址
toTokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC 铸币地址
slippage: "0.1", // 滑点容忍百分比
userWalletAddress: process.env.WALLET_ADDRESS || "", // 执行交换的钱包
priceTolerance: "0", // 允许的最大价格影响
autoSlippage: "false", // 使用固定滑点而非自动滑点
fromTokenReferrerWalletAddress: process.env.WALLET_ADDRESS || "", // 用于推荐费用
pathNum: "3" // 考虑的最大路由数
}
处理兑换指令#
获取并处理兑换指令:
// 将 DEX API 指令转换为 Solana 格式的辅助函数
// DEX 返回的指令是自定义格式,需要转换
function createTransactionInstruction(instruction: any): TransactionInstruction {
return new TransactionInstruction({
programId: new PublicKey(instruction.programId), // DEX 程序 ID
keys: instruction.accounts.map((key: any) => ({ pubkey: new PublicKey(key.pubkey), // Account address
isSigner: key.isSigner, // 如果账户必须签名则为 true
isWritable: key.isWritable // 如果指令涉及到修改账户则为 true
})),
data: Buffer.from(instruction.data, 'base64') // 指令参数
});
}
// 从 DEX 获取最佳交换路由和指令
// 此调用会找到不同 DEX 流动性池中的最佳价格
const url = `${baseUrl}?${new URLSearchParams(params).toString()}`;
const { data: { instructionLists, addressLookupTableAccount } } =
await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
// 将 DEX 指令处理为 Solana 兼容格式
const instructions: TransactionInstruction[] = [];
// 移除 DEX 返回的重复查找表地址
const addressLookupTableAccount2 = Array.from(new Set(addressLookupTableAccount));
console.log("要加载的查找表:", addressLookupTableAccount2);
// 将每个 DEX 指令转换为 Solana 格式
if (instructionLists?.length) {
instructions.push(...instructionLists.map(createTransactionInstruction));
}
处理地址查找表#
使用地址查找表优化交易数据优化大小
// 使用查找表以优化交易数据大小
// 查找表对于与许多账户交互的复杂兑换至关重要
// 它们显著减少了交易大小和成本
const addressLookupTableAccounts: AddressLookupTableAccount[] = [];
if (addressLookupTableAccount2?.length > 0) {
console.log("加载地址查找表...");
// 并行获取所有查找表以提高性能
const lookupTableAccounts = await Promise.all(
addressLookupTableAccount2.map(async (address: unknown) => {
const pubkey = new PublicKey(address as PublicKeyInitData);
// 从 Solana 获取查找表账户数据
const account = await connection
.getAddressLookupTable(pubkey)
.then((res) => res.value);
if (!account) {
throw new Error(`无法获取查找表账户 ${address}`);
}
return account;
})
);
addressLookupTableAccounts.push(...lookupTableAccounts);
}
创建并签名交易#
创建交易消息并签名:
// 获取最近的 blockhash 以确定交易时间和唯一性
// 交易在此 blockhash 之后的有限时间内有效
const latestBlockhash = await connection.getLatestBlockhash('finalized');
// 创建版本化交易消息
// V0 消息格式需要支持查找表
const messageV0 = new TransactionMessage({
payerKey: wallet.publicKey, // 费用支付者地址
recentBlockhash: latestBlockhash.blockhash, // 交易时间
instructions // 来自 DEX 的兑换指令
}).compileToV0Message(addressLookupTableAccounts); // 包含查找表
// 创建带有优化的新版本化交易
const transaction = new VersionedTransaction(messageV0);
// 模拟交易以检查错误
// 这有助于在支付费用之前发现问题
const result: RpcResponseAndContext<SimulatedTransactionResponse> =
await connection.simulateTransaction(transaction);
// 使用费用支付者钱包签名交易
const feePayer = Keypair.fromSecretKey(
base58.decode(process.env.PRIVATE_KEY?.toString() || "")
);
transaction.sign([feePayer])
执行交易#
最后,模拟并发送交易:
// 将交易发送到 Solana
// skipPreflight=false 确保额外的验证
// maxRetries 帮助处理网络问题
const txId = await connection.sendRawTransaction(transaction.serialize(), {
skipPreflight: false, // 运行预验证
maxRetries: 5 // 失败时重试
});
// 记录交易详情
console.log("Raw transaction:", transaction.serialize());
console.log("Base58 transaction:", base58.encode(transaction.serialize()));
// 记录模拟结果以供调试
console.log("=========模拟结果=========");
result.value.logs?.forEach((log) => {
console.log(log);
});
// 记录交易结果
console.log("Transaction ID:", txId);
console.log("Explorer URL:", `https://solscan.io/tx/${txId}`);
最佳实践和注意事项#
在实现交换指令时,请记住以下关键点:
- 您可以在此处查看完整的 Typescript 实现示例
- 错误处理: 始终为 API 响应和交易模拟结果实施适当的错误处理。
- 滑点保护: 根据您的实际情况和行情选择适当的滑点参数。
- Gas 优化: 可用时,使用地址查找表以减少交易大小和成本。
- 交易模拟: 在发送交易之前始终模拟交易,以尽早发现潜在问题。您也可以在不执行交易的情况下使用此功能进行测试。
- 重试逻辑: 为失败交易实施适当的重试机制,并采用适当的止盈止损退出策略。
通过遵循这些实践并理解兑换指令的过程,您可以在 Solana 应用程序中构建可靠且高效的代币兑换功能。