在 Solana 链上兑换的高级用法#
当你需要对兑换过程进行更多控制和组装定制时,可使用 swap-instruction 接口。 已有的 /swap 兑换接口的作用是,直接返回了构建好的交易数据,可直接签名执行。但 swap-instruction 兑换指令接口允许你:
- 构建自定义的交易签名流程
- 按照你的需要处理指令
- 在已构建的交易添加自己的指令
- 直接使用查找表来优化交易数据大小
本指南将逐步介绍,如何使用兑换指令接口发起一笔完整的兑换交易。 你将了解如何从 API 接口中获取指令、组装处理它们并将其构建成一个可用的交易。
1. 设置环境#
导入必要的库并配置 你的环境:
// 与 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();
2. 初始化连接和钱包#
设置 你的连接和钱包实例:
// 注意:在生产环境中,请考虑使用具有高速率限制的可靠 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() || ""))
);
3. 配置兑换参数#
设置 你的兑换参数:
// 配置交换参数
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" // 考虑的最大路由数
}
4. 处理兑换指令#
获取并处理兑换指令:
// 将 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));
}
5. 处理地址查找表#
使用地址查找表优化交易数据优化大小
// 使用查找表以优化交易数据大小
// 查找表对于与许多账户交互的复杂兑换至关重要
// 它们显著减少了交易大小和成本
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);
}
6. 创建并签名交易#
创建交易消息并签名:
// 获取最近的 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])
7. 执行交易#
最后,模拟并发送交易:
// 将交易发送到 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}`);
最佳实践和注意事项#
在实施交换指令时,请记住以下关键点:
- 错误处理:始终为API响应和事务模拟结果实现正确的错误处理。
- 防滑保护:根据 你的用例和市场条件选择适当的防滑参数。
- Gas优化:在可用时使用地址查找表来减少事务大小和成本。
- 事务模拟:在发送事务之前始终模拟事务,以便及早发现潜在问题。
- 重试逻辑:使用适当的退避策略为失败的事务实现适当的重试机制。