DEX API

在 Sui 链上搭建兑换应用#

在本指南中,我们将通过欧易 DEX 提供一个用例来进行 Sui 代币兑换。这个过程包括

  • 设置你的环境
  • 获取 toTokenAddress 的代币账户地址
  • 获取兑换路径
  • 组装交易并签名
  • 执行交易

1. 设置你的环境#

你可以直接复制并使用 OKX DEX API 库来完成必要的环境配置:

git clone https://github.com/okx/dex-api-library.git
cd dex-api-library
npm install

此外,你还需要创建一个 .env 文件,配置如下:

OKX_API_KEY=你的 API key
OKX_SECRET_KEY=你的 Secret key
OKX_API_PASSPHRASE=你的 Passphrase
OKX_PROJECT_ID=你的 Projecr ID
WALLET_ADDRESS=你的 Sui 钱包地址
PRIVATE_KEY=你的 Sui 钱包私钥

你可以从以下链接获取你的 API Key:https://www.okx.com/web3/build/dev-portal。

同时,本示例使用 hexWithoutFlag 格式的 Sui 私钥。

该格式可以按照以下步骤获取:

1.导出并保存你的 Sui 钱包私钥

2.下载并安装 SUI CLI

3.使用以下命令将你的 Sui 钱包私钥转换为 hexWithoutFlag 格式

sui keytool convert <你的Sui私钥>

4.将输出值作为 .env 文件中的 PRIVATE_KEY

2. 获取代币信息和兑换报价#

使用 /dex/aggregator/quote 端点获取代币信息,使用 /dex/aggregator/swap 端点获取兑换数据。

以下是将 SUI 兑换为 USDC 的示例:

function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
    if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
        throw new Error("缺少必要的环境变量");
    }
    const timestamp = new Date().toISOString();
    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/swap";
    const params = {
        chainId: SUI_CHAIN_ID,
        fromTokenAddress,
        toTokenAddress,
        amount: "1000000", 
        slippage: "0.5",
        userWalletAddress: normalizedWalletAddress,
    };

    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 Data = data.data[0]
    return Data
}

3. 组装交易并签名#

提示
此处 txData 是从 /swap 接口中获取
async function executeSwap(txData: string, privateKey: string) {
    // 创建交易块
    const txBlock = Transaction.from(txData);
    txBlock.setSender(normalizedWalletAddress);

    // 设置 Gas 参数
    const referenceGasPrice = await client.getReferenceGasPrice();
    txBlock.setGasPrice(BigInt(referenceGasPrice));
    txBlock.setGasBudget(BigInt(CONFIG.DEFAULT_GAS_BUDGET));

    // 构建并签名交易
    const builtTx = await txBlock.build({ client });
    const txBytes = Buffer.from(builtTx).toString('base64');

    const signedTx = await wallet.signTransaction({
        privateKey,
        data: {
            type: 'raw',
            data: txBytes
        }
    });

    return signedTx;
}

4. 执行交易#

const result = await client.executeTransactionBlock({
    transactionBlock: builtTx,
    signature: [signedTx.signature],
    options: {
        showEffects: true,
        showEvents: true,
    }
});

// 验证交易
const confirmation = await client.waitForTransaction({
    digest: result.digest,
    options: {
        showEffects: true,
        showEvents: true,
    }
});

console.log("交易 ID:", result.digest);
console.log("浏览器 URL:", `https://suiscan.xyz/mainnet/tx/${result.digest}`);

5.使用 TypeScript 完成完整 Swap 实现#

以下实现提供了一个完整的兑换解决方案:

// swap.ts
import { SuiWallet } from "@okxweb3/coin-sui";
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
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 SUI_CHAIN_ID = "784";
const DEFAULT_GAS_BUDGET = 50000000;
const MAX_RETRIES = 3;

// 初始化客户端
const wallet = new SuiWallet();
const client = new SuiClient({
    url: getFullnodeUrl('mainnet')
});

// 标准化钱包地址
const normalizedWalletAddress = normalizeSuiAddress(userAddress);

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: SUI_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 (BigInt(Math.floor(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 <金额> <询价代币地址> <目标代币地址>");
            console.log("示例: ts-node swap.ts 1.5 0x2::sui::SUI 0xdba...::usdc::USDC");
            process.exit(1);
        }

        const [amount, fromTokenAddress, toTokenAddress] = args;

        if (!userPrivateKey || !userAddress) {
            throw new Error("未找到私钥或用户地址");
        }

        // 获取代币信息
        console.log("获取代币信息...");
        const tokenInfo = await getTokenInfo(fromTokenAddress, toTokenAddress);
        console.log(`询价代币: ${tokenInfo.fromToken.symbol} (${tokenInfo.fromToken.decimals} 位小数)`);
        console.log(`目标代币: ${tokenInfo.toToken.symbol} (${tokenInfo.toToken.decimals} 位小数)`);

        // 使用获取的小数位数转换金额
        const rawAmount = convertAmount(amount, tokenInfo.fromToken.decimals);
        console.log(`金额以 ${tokenInfo.fromToken.symbol} 基础单位表示:`, rawAmount);

        // 获取兑换报价
        const quoteParams = {
            chainId: SUI_CHAIN_ID,
            amount: rawAmount,
            fromTokenAddress,
            toTokenAddress,
            slippage: "0.5",
            userWalletAddress: normalizedWalletAddress,
        };

        // 获取兑换数据
        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 {
                // 创建交易块
                const txBlock = Transaction.from(swapData.tx.data);
                txBlock.setSender(normalizedWalletAddress);

                // 设置 Gas 参数
                const referenceGasPrice = await client.getReferenceGasPrice();
                txBlock.setGasPrice(BigInt(referenceGasPrice));
                txBlock.setGasBudget(BigInt(DEFAULT_GAS_BUDGET));

                // 构建并签名交易
                const builtTx = await txBlock.build({ client });
                const txBytes = Buffer.from(builtTx).toString('base64');

                const signedTx = await wallet.signTransaction({
                    privateKey: userPrivateKey,
                    data: {
                        type: 'raw',
                        data: txBytes
                    }
                });

                if (!signedTx?.signature) {
                    throw new Error("签名交易失败");
                }

                // 执行交易
                const result = await client.executeTransactionBlock({
                    transactionBlock: builtTx,
                    signature: [signedTx.signature],
                    options: {
                        showEffects: true,
                        showEvents: true,
                    }
                });

                // 等待确认
                const confirmation = await client.waitForTransaction({
                    digest: result.digest,
                    options: {
                        showEffects: true,
                        showEvents: true,
                    }
                });

                console.log("\n兑换成功完成!");
                console.log("交易 ID:", result.digest);
                console.log("浏览器 URL:", `https://suiscan.xyz/mainnet/tx/${result.digest}`);

                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();
}

使用示例#

要执行兑换,请使用以下参数运行脚本:

npx ts-node swap.ts <金额> <询价代币地址> <目标代币地址>

例如

# 示例:将 1.5 SUI 兑换为 USDC
npx ts-node swap.ts 1.5 0x2::sui::SUI 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC

将返回类似如下的响应:

获取代币信息...
询价代币: SUI (9 位小数)
目标代币: USDC (6 位小数)
金额以 SUI 基础单位表示: 1500000000

兑换报价:
输入: 1.5 SUI ($2.73)
输出: 2.73 USDC ($2.73)

执行兑换交易...
签名交易...
执行交易...

兑换成功完成!
交易 ID: 5LncQyzK7YmcodcsQMYwnjYBAYBkKJAaS1XR2RLiCVyPyA5nwHjUNuSQos4VGk4CJm5spRPngdnv8cQYjYYwCAVu
浏览器 URL: https://suiscan.xyz/mainnet/tx/5LncQyzK7Ym