DEX API

在 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#

提示
Solana 的 NativeTokenAddress 为 11111111111111111111111111111111
  • 通过 /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. 反序列化并签名#

提示
此处 callData 是从 /swap 接口中获取
// 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}`);

最佳实践和注意事项#

在实现交换指令时,请记住以下关键点:

  1. 您可以在此处查看完整的 Typescript 实现示例
  2. 错误处理: 始终为 API 响应和交易模拟结果实施适当的错误处理。
  3. 滑点保护: 根据您的实际情况和行情选择适当的滑点参数。
  4. Gas 优化: 可用时,使用地址查找表以减少交易大小和成本。
  5. 交易模拟: 在发送交易之前始终模拟交易,以尽早发现潜在问题。您也可以在不执行交易的情况下使用此功能进行测试。
  6. 重试逻辑: 为失败交易实施适当的重试机制,并采用适当的止盈止损退出策略。

通过遵循这些实践并理解兑换指令的过程,您可以在 Solana 应用程序中构建可靠且高效的代币兑换功能。