在 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 来搭建 swap 应用#
我们提供了一个实现 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 的斗争中发挥作用。
开发者们可以根据自己应用的需要,来选择和实现。