区块链 区块链技术 比特币公众号手机端

使用0x Swap API实现智能订单路由的高效代币兑换

概述

流动性碎片化是 DeFi 中最大的挑战之一。对于开发者来说,要为 Token 交易找到最佳价格,通常意味着要逐个查询多个去中心化交易所(DEX),这既低效又难以维护。

通过 Quicknode 集成 0x Swap API,你可以使用智能订单路由来解决这个问题,该路由聚合了多个受支持链上超过 150 个来源的流动性。在本指南中,你将学习如何将这一强大的路由逻辑直接集成到自己的交易机器人或 dApp 中,从而能够获取指示性价格、管理 Token 授权,并以尽可能最优的价格执行交易。

我们将使用 Base 主网上的 Token 交换来演示这一集成,但相同的概念适用于所有受支持的链。

你将做什么

  • 查询 0x Swap API 以获取 Token 对的指示性定价
  • 分析 API 响应以处理 Token 授权和余额检查
  • 生成已签名的交易报价
  • 使用 Viem 执行交换

你需要什么

  • 一个已启用 0x Swap API Add‑on 的 Quicknode 端点
  • 已安装 Node.js(建议 v20+)
  • 已安装 tsx
  • 一个代码编辑器(例如 VS Code)
  • 一个持有资金的钱包(以及其私钥)用于测试交易(确保你有原生 Gas Token 来支付 Gas,以及你想要出售的 Token)

为什么使用 0x Swap API?

在编写代码之前,先了解一下为什么许多 DeFi 团队都依赖 0x Swap API,会很有帮助。它提供的不仅是价格发现,还有一组面向开发者的功能,可简化多条链上的报价、模拟、路由和执行。

  • 多链支持:该 API 支持主要的 EVM 链,包括 Ethereum、Base、Polygon、BNB Smart Chain、Avalanche、Arbitrum、Optimism 等。你的集成在不同链上的工作方式相同,这有助于减少代码库中的碎片化。

  • 广泛的流动性覆盖:路由引擎聚合了来自 150 多个来源的流动性,包括 Uniswap、Curve、Balancer、Aerodrome、PancakeSwap、Trader Joe 等去中心化交易所,以及专业做市商和 RFQ 提供商。

  • 内置“预检”检查(issues:API 会先运行模拟,在响应的 issues 字段中预先警告你可能遇到的阻碍,而不是让你的交易在链上失败并浪费 Gas。关键检查包括:

    • issues.balance:返回用户出售 Token 的实际余额和预期余额。
    • issues.allowance:返回指定 spender 对用户 Token 的实际授权额度。
  • 智能订单路由:API 不仅返回价格,还会返回 route(路由)。你可以清楚地看到交易如何被拆分,以尽量减少滑点。

  • 支持变现:如果你正在构建钱包或仪表盘,可以轻松添加自己的费用。API 支持收取联盟费用和交易盈余。

  • Gas 优化:路由逻辑在寻找最佳路径时会考虑 Gas 成本。响应提供精确的 gasgasPrice 估算,确保你的交易更有可能快速执行,同时避免多付费用。

支持的方法

Quicknode 通过你的端点开放 0x Swap API,你将在本指南中使用的两个核心方法如下:

GET /swap/allowance-holder/price

使用这个端点获取交易的指示性价格。传入 chainIdsellTokenbuyTokensellAmount 等参数后,API 会返回:

  • 预期输出数量(buyAmount)和考虑滑点后的最低输出(minBuyAmount
  • 路由详情和 Gas 估算
  • 可能阻碍执行的 issues
  • 费用明细(协议费、联盟费、Gas 等)

这个端点非常适合构建报价预览、在用户输入金额时更新 UI,或在不提交交易的情况下进行假设性检查。

GET /swap/allowance-holder/quote

当你准备好执行交换时,使用这个端点。它返回一个确定报价,以及 transaction 字段中构造完整的交易负载(包括 todatavalue 和 Gas 参数)。你可以直接将这个负载传给钱包客户端或签名流程,从而在链上发送交易。

这两个方法配合使用,可以将用户体验分为使用 /price预览步骤和使用 /quote执行步骤,同时让你的集成在不同链上保持简单且一致。

项目设置

步骤 1:初始化项目

首先,你需要搭建一个简单的 Node.js 环境并安装必要的依赖。我们将使用 viem,这是一个轻量级且类型安全的以太坊接口。

  1. 使用你喜欢的包管理器初始化一个新的 Node.js 项目
mkdir quicknode-0x-swap-api
cd quicknode-0x-swap-api
npm init -y
  1. 安装所需的包:
npm install viem dotenv
npm install --save-dev @types/node

步骤 2:配置环境变量

  1. 在根目录中创建一个 .env 文件来存储你的凭据。
touch .env
  1. 将你的 Quicknode 端点 URL、特定的路径扩展名和私钥添加到 .env 文件中。

注意:0x Swap API 需要通过 Quicknode HTTP 端点 URL 上的特定路径扩展名(addon/1117)来访问。

## .env
## Quicknode 端点 URL(不含 addon 路径)
QUICKNODE_HTTP_URL=https://your-base-endpoint.quiknode.pro/your-key

## 0x Swap API addon 路径
ADD_ON_PATH=addon/1117

## 你钱包的私钥(请保密!)
PRIVATE_KEY=0x...your-private-key-here

结合 Viem 使用 0x Swap API

步骤 1:核心导入与设置

在你的项目根目录中创建一个名为 swap.ts 的新文件。

touch swap.ts

然后,在 swap.ts 文件的开头添加必要的导入和配置。

本节导入用于区块链交互的 viem 库函数,设置环境变量,并创建所需的钱包客户端和公共客户端连接,用于读取区块链状态和发送交易。

import {
  createWalletClient,
  createPublicClient,
  http,
  parseUnits,
  formatUnits,
  maxUint256,
  erc20Abi,
  Address,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import dotenv from "dotenv";

// 加载环境变量
dotenv.config();

// ============================================
// 1. 配置与设置
// ============================================

// 环境变量校验
const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const ADD_ON_PATH = process.env.ADD_ON_PATH;
const PRIVATE_KEY = process.env.PRIVATE_KEY as Address;

if (!QUICKNODE_HTTP_URL || !ADD_ON_PATH || !PRIVATE_KEY) {
  throw new Error(
    "Missing required environment variables: QUICKNODE_HTTP_URL, ADD_ON_PATH, and PRIVATE_KEY"
  );
}

// 用于标准 Ethereum RPC 调用的 Base RPC URL
const BASE_RPC_URL = QUICKNODE_HTTP_URL;

// 带有 addon 路径的完整 URL,用于 0x Swap API 调用
const SWAP_API_URL = `${QUICKNODE_HTTP_URL}/${ADD_ON_PATH}`;

// Base 链的 Token 地址
const TOKENS = {
  NATIVE: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Base 上的原生 ETH
  WETH: "0x4200000000000000000000000000000000000006", // Base 上的封装 ETH
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base 上的 USDC
  USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", // Base 上的 USDe
  WBTC: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // Base 上的 WBTC
} as const;

// 链配置
const CHAIN_ID = "8453"; // Base 链 ID

// 从私钥初始化账户
const account = privateKeyToAccount(PRIVATE_KEY);

// 初始化 Viem 客户端
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(BASE_RPC_URL),
});

const publicClient = createPublicClient({
  chain: base,
  transport: http(BASE_RPC_URL),
});

步骤 2:类型定义

定义 API 响应的 TypeScript 接口以确保类型安全。

这些类型定义让 API 响应有了清晰的结构,有助于在编译时捕获错误,并提供更好的 IDE 自动补全支持。它们定义了我们将从 0x API 接收的数据的具体结构。查看 0x Swap API 文档 了解更多详情。

// ============================================
// 2. 类型定义
// ============================================

interface SwapPriceResponse {
  allowanceTarget: Address;
  buyAmount: string;
  buyToken: Address;
  sellAmount: string;
  sellToken: Address;
  gas: string;
  gasPrice: string;
  issues: {
    allowance?: {
      actual: string;
      spender: Address;
    };
    balance?: {
      token: Address;
      actual: string;
      expected: string;
    };
    simulationIncomplete?: boolean;
  };
  liquidityAvailable: boolean;
  route: any;
  fees: any;
  minBuyAmount?: string;
}

interface SwapQuoteResponse extends SwapPriceResponse {
  transaction: {
    to: Address;
    data: string;
    gas: string;
    gasPrice: string;
    value: string;
  };
}

步骤 3:获取价格

实现一个从 0x API 获取指示性价格的函数。

此函数查询 0x API 以获取当前的交换价格,而不会创建实际订单。它用于在用户提交交易之前展示预期的交换结果并检查潜在问题。

这一步至关重要,能够验证用户余额是否充足、Token 授权是否足够,以及市场是否有足够的流动性。

了解 API 响应

API 响应包含几个重要字段,可帮助你有效管理交换过程:

  • issues:立即检查这个对象。如果 issues.balance.actual 低于你的出售数量,你可以在用户尝试交换之前提示他们充值。
  • liquidityAvailable:一个布尔值,用于确认你的交易是否有足够的市场深度。
  • route:一个数组,显示交易是如何被拆分的。你可以利用它在 UI 中展示“智能路由”的可视化效果。
// ============================================
// 3. 获取价格
// ============================================

/**
 * 从 0x API 获取指示性价格
 * 这是一个只读端点,用于在不提交的情况下检查价格
 */
async function getPrice(
  sellToken: Address,
  buyToken: Address,
  sellAmount: string,
  decimals: number = 18,
  slippageBps: number = 100
): Promise<SwapPriceResponse> {
  const sellAmountWei = parseUnits(sellAmount, decimals).toString();

  const params = new URLSearchParams({
    chainId: CHAIN_ID,
    sellToken,
    buyToken,
    sellAmount: sellAmountWei,
    taker: account.address,
    slippageBps: slippageBps.toString(),
  });

  const headers: HeadersInit = {
    "Content-Type": "application/json",
  };

  const url = `${SWAP_API_URL}/swap/allowance-holder/price?${params.toString()}`;
  console.log(`   Fetching price from: ${url}`);

  const response = await fetch(url, { headers });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Price fetch failed: ${error}`);
  }

  return response.json();
}

步骤 4:余额与授权检查

将这些辅助函数添加到脚本中,以标准化检查余额和授权的方式,自动处理原生 ETH(需要 getBalance)和 ERC20 Token(需要 balanceOfallowance)之间的逻辑差异。

虽然 0x Swap API 已经在 issues 字段中包含了预检检查,但我们仍可以借助这些函数来显示余额和检查授权。

// ============================================
// 4. 余额检查
// ============================================

/**
 * 检查 Token 是否为原生 ETH Token
 */
function isNativeToken(tokenAddress: Address): boolean {
  return tokenAddress.toLowerCase() === TOKENS.NATIVE.toLowerCase();
}

/**
 * 检查特定地址的 Token 余额
 */
async function checkBalance(
  tokenAddress: Address,
  userAddress: Address
): Promise<bigint> {
  // 对于原生 ETH,使用 getBalance 而不是 ERC20 balanceOf
  if (isNativeToken(tokenAddress)) {
    const balance = await publicClient.getBalance({
      address: userAddress,
    });
    return balance;
  }

  const balance = await publicClient.readContract({
    address: tokenAddress as Address,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [userAddress as Address],
  });

  return balance;
}

/**
 * 获取 Token 精度
 */
async function getTokenDecimals(tokenAddress: Address): Promise<number> {
  // 原生 ETH 始终有 18 位精度
  if (isNativeToken(tokenAddress)) {
    return 18;
  }

  const decimals = await publicClient.readContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "decimals",
  });

  return decimals;
}

/**
 * 检查 spender 的当前授权额度
 */
async function checkAllowance(
  tokenAddress: Address,
  owner: Address,
  spender: Address
): Promise<bigint> {
  const allowance = await publicClient.readContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "allowance",
    args: [owner, spender],
  });

  return allowance;
}

步骤 5:问题检测

实现检测和处理常见交换问题的逻辑。

此函数分析 API 响应中是否存在余额不足、缺少 Token 授权等潜在问题。它会提供清晰的错误信息,并在需要时自动触发授权流程,让交换过程更加顺畅。API 响应中的 issues 字段用于确定问题的类型以及应采取的措施。

// ============================================
// 5. 问题检测与处理
// ============================================

/**
 * 分析并处理价格响应中的问题
 */
async function handleIssues(priceData: SwapPriceResponse): Promise<void> {
  console.log(" Analyzing potential issues...");

  // 检查余额问题
  if (priceData.issues.balance) {
    const { token, actual, expected } = priceData.issues.balance;

    // 跳过原生 ETH 的余额检查,因为它的处理方式不同
    if (!isNativeToken(token)) {
      const decimals = await getTokenDecimals(token);
      const actualFormatted = formatUnits(BigInt(actual), decimals);
      const expectedFormatted = formatUnits(BigInt(expected), decimals);

      console.log(`\n️  BALANCE ISSUE DETECTED:`);
      console.log(`   Token: ${token}`);
      console.log(`   Current balance: ${actualFormatted}`);
      console.log(`   Required: ${expectedFormatted}`);
      console.log(
        `   Shortfall: ${formatUnits(
          BigInt(expected) - BigInt(actual),
          decimals
        )}`
      );

      throw new Error(
        `Insufficient balance. Need ${expectedFormatted} but have ${actualFormatted}`
      );
    } else {
      // 对于原生 ETH,仍然检查,但使用更简单的格式化
      const decimals = 18; // 原生 ETH 有 18 位精度
      const actualFormatted = formatUnits(BigInt(actual), decimals);
      const expectedFormatted = formatUnits(BigInt(expected), decimals);

      console.log(`\n️  BALANCE ISSUE DETECTED:`);
      console.log(`   Token: Native ETH`);
      console.log(`   Current balance: ${actualFormatted} ETH`);
      console.log(`   Required: ${expectedFormatted} ETH`);
      console.log(
        `   Shortfall: ${formatUnits(
          BigInt(expected) - BigInt(actual),
          decimals
        )} ETH`
      );

      throw new Error(
        `Insufficient balance. Need ${expectedFormatted} ETH but have ${actualFormatted} ETH`
      );
    }
  }

  // 检查授权问题 - 跳过原生 ETH(无需授权)
  if (priceData.issues.allowance && !isNativeToken(priceData.sellToken)) {
    const { spender, actual } = priceData.issues.allowance;
    const requiredAmount = BigInt(priceData.sellAmount);
    const currentAllowance = BigInt(actual);

    console.log(`\n️  ALLOWANCE ISSUE DETECTED:`);
    console.log(`   Current allowance: ${currentAllowance.toString()}`);
    console.log(`   Required allowance: ${requiredAmount.toString()}`);
    console.log(`   Spender (AllowanceHolder): ${spender}`);

    // 检查当前授权额度是否足以完成此交换
    if (currentAllowance >= requiredAmount) {
      console.log(`    Current allowance is sufficient for this swap`);
    } else {
      console.log(`   Action: Setting approval for exact swap amount...`);
      await setTokenApprovalForAmount(
        priceData.sellToken,
        spender,
        requiredAmount
      );
      console.log(` Token approval completed successfully`);
    }
  } else if (isNativeToken(priceData.sellToken)) {
    console.log(" Native ETH selected - no approval needed");
  } else {
    console.log(" No issues detected - ready to proceed");
  }

  // 检查模拟问题
  if (priceData.issues.simulationIncomplete) {
    console.log("️  Warning: Simulation incomplete - transaction may fail");
  }
}

步骤 6:Token 授权

实现金额精确且安全的 Token 授权。

此函数处理交换前所需的 ERC20 Token 授权。它不使用无限授权(存在安全风险),而是只为每次交换授权所需的确切金额,并会先检查现有的授权额度(作为 API 响应之后的Layer2检查),以避免不必要的交易。

// ============================================
// 6. Token 授权
// ============================================

/**
 * 为交换设置所需确切金额的 Token 授权
 * 这比无限授权更安全
 */
async function setTokenApprovalForAmount(
  tokenAddress: Address,
  spender: Address,
  amount: bigint
): Promise<void> {
  try {
    // 检查当前授权额度
    const currentAllowance = await checkAllowance(
      tokenAddress,
      account.address,
      spender
    );

    console.log(`   Current allowance: ${currentAllowance.toString()}`);
    console.log(`   Required amount: ${amount.toString()}`);

    // 仅当当前授权额度不足时才授权
    if (currentAllowance >= amount) {
      console.log(
        `   ℹ️  Current allowance is already sufficient for this swap`
      );
      return;
    }

    // 计算需要多少额外授权
    // 我们将授权所需的确切金额
    const approvalAmount = amount;

    console.log(`   Approving exact amount: ${approvalAmount.toString()}`);

    // 模拟授权交易
    console.log(`   Simulating approval transaction...`);
    const { request } = await publicClient.simulateContract({
      account,
      address: tokenAddress,
      abi: erc20Abi,
      functionName: "approve",
      args: [spender, approvalAmount],
    });

    // 执行授权
    console.log(`   Sending approval transaction...`);
    const hash = await walletClient.writeContract(request);
    console.log(`   Approval tx hash: ${hash}`);

    // 等待确认
    console.log(`   Waiting for confirmation...`);
    const receipt = await publicClient.waitForTransactionReceipt({ hash });

    if (receipt.status !== "success") {
      throw new Error("Approval transaction failed");
    }

    console.log(`   Approval confirmed in block ${receipt.blockNumber}`);

    // 验证新的授权额度
    const newAllowance = await checkAllowance(
      tokenAddress,
      account.address,
      spender
    );
    console.log(`   New allowance: ${newAllowance.toString()}`);
  } catch (error) {
    console.error(" Approval failed:", error);
    throw error;
  }
}

步骤 7:获取报价

从 0x API 获取确定且可执行的报价。

price 端点不同,quote 端点返回的是完整、可立即执行的交易。这相当于向市场表明交易意图,因此通常能获得更好的价格。它还带有滑点保护,确保得到最低输出数量。

了解 API 响应

响应中包含一个 transaction 对象。这就是你将传递给钱包或 web3 库的内容:

  • transaction.to:交易将与之交互的合约地址(通常是 0x Exchange Proxy 或 AllowanceHolder)。
  • transaction.data:包含交换逻辑的经过编码的十六进制数据。
  • transaction.value:要发送的原生 ETH(以 wei 为单位)数量。对于 ERC-20 交换,这通常为 0;如果交换的是原生 ETH,则为非零。
// ============================================
// 7. 获取报价
// ============================================

/**
 * 从 0x API 获取确定报价
 * 这将返回一个可执行的交易
 */
async function getQuote(
  sellToken: Address,
  buyToken: Address,
  sellAmount: string,
  slippageBps: number,
  decimals: number = 18
): Promise<SwapQuoteResponse> {
  const sellAmountWei = parseUnits(sellAmount, decimals).toString();

  const params = new URLSearchParams({
    chainId: CHAIN_ID,
    sellToken,
    buyToken,
    sellAmount: sellAmountWei,
    taker: account.address,
    slippageBps: slippageBps.toString(),
  });

  const headers: HeadersInit = {
    "Content-Type": "application/json",
  };

  const url = `${SWAP_API_URL}/swap/allowance-holder/quote?${params.toString()}`;
  console.log(`   Requesting firm quote...`);

  const response = await fetch(url, { headers });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Quote fetch failed: ${error}`);
  }

  return response.json();
}

步骤 8:交易执行

将交换交易提交到区块链。

此函数从报价中提取交易数据,并将其提交到区块链。它同时处理 Token 到 Token 的交换(value = 0)和原生 ETH 交换(value > 0),并记录重要细节以确保透明。

// ============================================
// 8. 交易执行
// ============================================

/**
 * 将交换交易提交到区块链
 */
async function submitTransaction(
  transaction: SwapQuoteResponse["transaction"]
): Promise<Address> {
  console.log(" Submitting transaction to the blockchain...");
  console.log(`   To: ${transaction.to}`);
  console.log(`   Gas limit: ${transaction.gas}`);
  console.log(
    `   Gas price: ${formatUnits(BigInt(transaction.gasPrice), 9)} Gwei`
  );
  console.log(`   Value: ${transaction.value} wei`);

  const hash = await walletClient.sendTransaction({
    to: transaction.to,
    data: transaction.data as `0x${string}`,
    gas: BigInt(transaction.gas),
    gasPrice: BigInt(transaction.gasPrice),
    value: transaction.value ? BigInt(transaction.value) : 0n,
  });

  return hash;
}

步骤 9:主流程编排

将所有步骤组合成一个完整的交换流程。

这是编排整个交换过程的主函数。它按正确的顺序协调前面定义的所有函数:获取价格、处理问题、获取报价、执行交换,并在每一步记录详细信息。

// ============================================
// 9. 主交换编排
// ============================================

/**
 * 执行完整的 Token 交换
 */
async function executeSwap(
  sellToken: Address,
  buyToken: Address,
  sellAmount: string,
  slippageBps: number = 100
): Promise<any> {
  console.log("\n" + "=".repeat(60));
  console.log(" STARTING TOKEN SWAP");
  console.log("=".repeat(60));

  const sellDecimals = await getTokenDecimals(sellToken);
  const buyDecimals = await getTokenDecimals(buyToken);

  console.log(`\n Swap Parameters:`);
  console.log(
    `   Sell: ${sellAmount} tokens (${sellToken}...)`
  );
  console.log(`   Buy: ${buyToken}...`);
  console.log(
    `   Slippage: ${slippageBps / 100}% (${slippageBps} bps)`
  );
  console.log(`   User: ${account.address}`);

  try {
    // 步骤 1:获取价格
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 1: FETCHING INDICATIVE PRICE");
    console.log("-".repeat(60));
    const priceData = await getPrice(
      sellToken,
      buyToken,
      sellAmount,
      sellDecimals,
      slippageBps
    );

    if (!priceData.liquidityAvailable) {
      throw new Error(" Insufficient liquidity for this trade");
    }

    const buyAmountFormatted = formatUnits(
      BigInt(priceData.buyAmount),
      buyDecimals
    );
    const minBuyAmountFormatted = priceData.minBuyAmount
      ? formatUnits(BigInt(priceData.minBuyAmount), buyDecimals)
      : "N/A";

    console.log(` Price fetched successfully:`);
    console.log(`   Expected output: ${buyAmountFormatted}`);
    console.log(`   Minimum output: ${minBuyAmountFormatted}`);
    console.log(`   Estimated gas: ${priceData.gas} units`);

    // 步骤 2:处理问题
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 2: CHECKING FOR ISSUES");
    console.log("-".repeat(60));

    // 处理问题(包括授权)
    await handleIssues(priceData);

    // 步骤 3:获取报价
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 3: FETCHING FIRM QUOTE");
    console.log("-".repeat(60));
    const quoteData = await getQuote(
      sellToken,
      buyToken,
      sellAmount,
      slippageBps,
      sellDecimals
    );

    const quoteBuyAmount = formatUnits(
      BigInt(quoteData.buyAmount),
      buyDecimals
    );
    console.log(` Quote received:`);
    console.log(`   Final output: ${quoteBuyAmount}`);
    console.log(`   Transaction to: ${quoteData.transaction.to}`);

    // 步骤 4:执行交换
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 4: EXECUTING SWAP");
    console.log("-".repeat(60));
    const txHash = await submitTransaction(quoteData.transaction);

    console.log(` Transaction submitted!`);
    console.log(`   Hash: ${txHash}`);
    console.log("\n⏳ Waiting for confirmation...");

    const receipt = await publicClient.waitForTransactionReceipt({
      hash: txHash,
    });

    console.log("\n" + "=".repeat(60));
    if (receipt.status === "success") {
      console.log(` SWAP SUCCESSFUL!`);
      console.log(`   Block: ${receipt.blockNumber}`);
      console.log(`   Gas used: ${receipt.gasUsed.toString()}`);
    } else {
      console.log(` TRANSACTION FAILED`);
    }
    console.log("=".repeat(60) + "\n");

    return receipt;
  } catch (error) {
    console.error("\n Swap failed:", error);
    throw error;
  }
}

步骤 10:实用函数

添加用于显示余额和运行脚本的辅助函数。

这些实用函数提供了便捷的方式,可以一次检查所有 Token 余额并设置主执行流程。displayBalances 函数通过显示交换前后的余额,帮助你确认交换是否正确执行。

// ============================================
// 10. 实用函数
// ============================================

/**
 * 显示 Token 余额
 */
async function displayBalances(): Promise<void> {
  console.log("\n Current Balances:");

  for (const [symbol, address] of Object.entries(TOKENS)) {
    const balance = await checkBalance(address, account.address);
    const decimals = await getTokenDecimals(address);
    const formatted = formatUnits(balance, decimals);

    // 使用 ETH 后缀显示原生 ETH
    if (isNativeToken(address)) {
      console.log(`   ${symbol}: ${formatted} ETH`);
    } else {
      console.log(`   ${symbol}: ${formatted}`);
    }
  }
}

步骤 11:主执行

最后,我们将所有内容组合到一个主执行函数中。此函数会获取价格以验证条件,确保授权已就绪,获取确定报价,然后将交易提交到区块链。

// ============================================
// 11. 主执行
// ============================================

async function main() {
  try {
    console.log(" Wallet Configuration:");
    console.log(`   Address: ${account.address}`);
    console.log(`   Network: Base (Chain ID: ${CHAIN_ID})`);

    // 显示当前余额
    await displayBalances();

    // 示例交换:0.000001 ETH -> USDC,滑点 1%(100 bps)
    await executeSwap(
      TOKENS.NATIVE, // 出售原生 ETH
      TOKENS.USDC, // 购买 USDC
      "0.000001", // 交换金额
      100 // 1% 滑点(以 bps 为单位)
    );

    // 显示更新后的余额
    await displayBalances();
  } catch (error) {
    console.error("Error in main:", error);
    process.exit(1);
  }
}

// 如果直接调用则运行
if (require.main === module) {
  main();
}

完整代码

查看下面的完整 swap.ts 文件:

点击展开

import {
  createWalletClient,
  createPublicClient,
  http,
  parseUnits,
  formatUnits,
  maxUint256,
  erc20Abi,
  Address,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import dotenv from "dotenv";

// 加载环境变量
dotenv.config();

// ============================================
// 1. 配置与设置
// ============================================

// 环境变量校验
const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const ADD_ON_PATH = process.env.ADD_ON_PATH;
const PRIVATE_KEY = process.env.PRIVATE_KEY as Address;

if (!QUICKNODE_HTTP_URL || !ADD_ON_PATH || !PRIVATE_KEY) {
  throw new Error(
    "Missing required environment variables: QUICKNODE_HTTP_URL, ADD_ON_PATH, and PRIVATE_KEY"
  );
}

// 用于标准 Ethereum RPC 调用的 Base RPC URL
const BASE_RPC_URL = QUICKNODE_HTTP_URL;

// 带有 addon 路径的完整 URL,用于 0x Swap API 调用
const SWAP_API_URL = `${QUICKNODE_HTTP_URL}/${ADD_ON_PATH}`;

// Base 链的 Token 地址
const TOKENS = {
  NATIVE: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // Base 上的原生 ETH
  WETH: "0x4200000000000000000000000000000000000006", // Base 上的封装 ETH
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base 上的 USDC
  USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", // Base 上的 USDe
  WBTC: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // Base 上的 WBTC
} as const;

// 链配置
const CHAIN_ID = "8453"; // Base 链 ID

// 从私钥初始化账户
const account = privateKeyToAccount(PRIVATE_KEY);

// 初始化 Viem 客户端
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(BASE_RPC_URL),
});

const publicClient = createPublicClient({
  chain: base,
  transport: http(BASE_RPC_URL),
});

// ============================================
// 2. 类型定义
// ============================================

interface SwapPriceResponse {
  allowanceTarget: Address;
  buyAmount: string;
  buyToken: Address;
  sellAmount: string;
  sellToken: Address;
  gas: string;
  gasPrice: string;
  issues: {
    allowance?: {
      actual: string;
      spender: Address;
    };
    balance?: {
      token: Address;
      actual: string;
      expected: string;
    };
    simulationIncomplete?: boolean;
  };
  liquidityAvailable: boolean;
  route: any;
  fees: any;
  minBuyAmount?: string;
}

interface SwapQuoteResponse extends SwapPriceResponse {
  transaction: {
    to: Address;
    data: string;
    gas: string;
    gasPrice: string;
    value: string;
  };
}

// ============================================
// 3. 获取价格
// ============================================

/**
 * 从 0x API 获取指示性价格
 * 这是一个只读端点,用于在不提交的情况下检查价格
 */
async function getPrice(
  sellToken: Address,
  buyToken: Address,
  sellAmount: string,
  decimals: number = 18,
  slippageBps: number = 100
): Promise<SwapPriceResponse> {
  const sellAmountWei = parseUnits(sellAmount, decimals).toString();

  const params = new URLSearchParams({
    chainId: CHAIN_ID,
    sellToken,
    buyToken,
    sellAmount: sellAmountWei,
    taker: account.address,
    slippageBps: slippageBps.toString(),
  });

  const headers: HeadersInit = {
    "Content-Type": "application/json",
  };

  const url = `${SWAP_API_URL}/swap/allowance-holder/price?${params.toString()}`;
  console.log(`   Fetching price from: ${url}`);

  const response = await fetch(url, { headers });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Price fetch failed: ${error}`);
  }

  return response.json();
}

// ============================================
// 4. 余额检查
// ============================================

/**
 * 检查 Token 是否为原生 ETH Token
 */
function isNativeToken(tokenAddress: Address): boolean {
  return tokenAddress.toLowerCase() === TOKENS.NATIVE.toLowerCase();
}

/**
 * 检查特定地址的 Token 余额
 */
async function checkBalance(
  tokenAddress: Address,
  userAddress: Address
): Promise<bigint> {
  // 对于原生 ETH,使用 getBalance 而不是 ERC20 balanceOf
  if (isNativeToken(tokenAddress)) {
    const balance = await publicClient.getBalance({
      address: userAddress,
    });
    return balance;
  }

  const balance = await publicClient.readContract({
    address: tokenAddress as Address,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [userAddress as Address],
  });

  return balance;
}

/**
 * 获取 Token 精度
 */
async function getTokenDecimals(tokenAddress: Address): Promise<number> {
  // 原生 ETH 始终有 18 位精度
  if (isNativeToken(tokenAddress)) {
    return 18;
  }

  const decimals = await publicClient.readContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "decimals",
  });

  return decimals;
}

/**
 * 检查 spender 的当前授权额度
 */
async function checkAllowance(
  tokenAddress: Address,
  owner: Address,
  spender: Address
): Promise<bigint> {
  const allowance = await publicClient.readContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "allowance",
    args: [owner, spender],
  });

  return allowance;
}

// ============================================
// 5. 问题检测与处理
// ============================================

/**
 * 分析并处理价格响应中的问题
 */
async function handleIssues(priceData: SwapPriceResponse): Promise<void> {
  console.log(" Analyzing potential issues...");

  // 检查余额问题
  if (priceData.issues.balance) {
    const { token, actual, expected } = priceData.issues.balance;

    // 跳过原生 ETH 的余额检查,因为它的处理方式不同
    if (!isNativeToken(token)) {
      const decimals = await getTokenDecimals(token);
      const actualFormatted = formatUnits(BigInt(actual), decimals);
      const expectedFormatted = formatUnits(BigInt(expected), decimals);

      console.log(`\n️  BALANCE ISSUE DETECTED:`);
      console.log(`   Token: ${token}`);
      console.log(`   Current balance: ${actualFormatted}`);
      console.log(`   Required: ${expectedFormatted}`);
      console.log(
        `   Shortfall: ${formatUnits(
          BigInt(expected) - BigInt(actual),
          decimals
        )}`
      );

      throw new Error(
        `Insufficient balance. Need ${expectedFormatted} but have ${actualFormatted}`
      );
    } else {
      // 对于原生 ETH,仍然检查,但使用更简单的格式化
      const decimals = 18; // 原生 ETH 有 18 位精度
      const actualFormatted = formatUnits(BigInt(actual), decimals);
      const expectedFormatted = formatUnits(BigInt(expected), decimals);

      console.log(`\n️  BALANCE ISSUE DETECTED:`);
      console.log(`   Token: Native ETH`);
      console.log(`   Current balance: ${actualFormatted} ETH`);
      console.log(`   Required: ${expectedFormatted} ETH`);
      console.log(
        `   Shortfall: ${formatUnits(
          BigInt(expected) - BigInt(actual),
          decimals
        )} ETH`
      );

      throw new Error(
        `Insufficient balance. Need ${expectedFormatted} ETH but have ${actualFormatted} ETH`
      );
    }
  }

  // 检查授权问题 - 跳过原生 ETH(无需授权)
  if (priceData.issues.allowance && !isNativeToken(priceData.sellToken)) {
    const { spender, actual } = priceData.issues.allowance;
    const requiredAmount = BigInt(priceData.sellAmount);
    const currentAllowance = BigInt(actual);

    console.log(`\n️  ALLOWANCE ISSUE DETECTED:`);
    console.log(`   Current allowance: ${currentAllowance.toString()}`);
    console.log(`   Required allowance: ${requiredAmount.toString()}`);
    console.log(`   Spender (AllowanceHolder): ${spender}`);

    // 检查当前授权额度是否足以完成此交换
    if (currentAllowance >= requiredAmount) {
      console.log(`    Current allowance is sufficient for this swap`);
    } else {
      console.log(`   Action: Setting approval for exact swap amount...`);
      await setTokenApprovalForAmount(
        priceData.sellToken,
        spender,
        requiredAmount
      );
      console.log(` Token approval completed successfully`);
    }
  } else if (isNativeToken(priceData.sellToken)) {
    console.log(" Native ETH selected - no approval needed");
  } else {
    console.log(" No issues detected - ready to proceed");
  }

  // 检查模拟问题
  if (priceData.issues.simulationIncomplete) {
    console.log("️  Warning: Simulation incomplete - transaction may fail");
  }
}

// ============================================
// 6. Token 授权
// ============================================

/**
 * 为交换设置所需确切金额的 Token 授权
 * 这比无限授权更安全
 */
async function setTokenApprovalForAmount(
  tokenAddress: Address,
  spender: Address,
  amount: bigint
): Promise<void> {
  try {
    // 检查当前授权额度
    const currentAllowance = await checkAllowance(
      tokenAddress,
      account.address,
      spender
    );

    console.log(`   Current allowance: ${currentAllowance.toString()}`);
    console.log(`   Required amount: ${amount.toString()}`);

    // 仅当当前授权额度不足时才授权
    if (currentAllowance >= amount) {
      console.log(
        `   ℹ️  Current allowance is already sufficient for this swap`
      );
      return;
    }

    // 计算需要多少额外授权
    // 我们将授权所需的确切金额
    const approvalAmount = amount;

    console.log(`   Approving exact amount: ${approvalAmount.toString()}`);

    // 模拟授权交易
    console.log(`   Simulating approval transaction...`);
    const { request } = await publicClient.simulateContract({
      account,
      address: tokenAddress,
      abi: erc20Abi,
      functionName: "approve",
      args: [spender, approvalAmount],
    });

    // 执行授权
    console.log(`   Sending approval transaction...`);
    const hash = await walletClient.writeContract(request);
    console.log(`   Approval tx hash: ${hash}`);

    // 等待确认
    console.log(`   Waiting for confirmation...`);
    const receipt = await publicClient.waitForTransactionReceipt({ hash });

    if (receipt.status !== "success") {
      throw new Error("Approval transaction failed");
    }

    console.log(`   Approval confirmed in block ${receipt.blockNumber}`);

    // 验证新的授权额度
    const newAllowance = await checkAllowance(
      tokenAddress,
      account.address,
      spender
    );
    console.log(`   New allowance: ${newAllowance.toString()}`);
  } catch (error) {
    console.error(" Approval failed:", error);
    throw error;
  }
}

// ============================================
// 7. 获取报价
// ============================================

/**
 * 从 0x API 获取确定报价
 * 这将返回一个可执行的交易
 */
async function getQuote(
  sellToken: Address,
  buyToken: Address,
  sellAmount: string,
  slippageBps: number,
  decimals: number = 18
): Promise<SwapQuoteResponse> {
  const sellAmountWei = parseUnits(sellAmount, decimals).toString();

  const params = new URLSearchParams({
    chainId: CHAIN_ID,
    sellToken,
    buyToken,
    sellAmount: sellAmountWei,
    taker: account.address,
    slippageBps: slippageBps.toString(),
  });

  const headers: HeadersInit = {
    "Content-Type": "application/json",
  };

  const url = `${SWAP_API_URL}/swap/allowance-holder/quote?${params.toString()}`;
  console.log(`   Requesting firm quote...`);

  const response = await fetch(url, { headers });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Quote fetch failed: ${error}`);
  }

  return response.json();
}

// ============================================
// 8. 交易执行
// ============================================

/**
 * 将交换交易提交到区块链
 */
async function submitTransaction(
  transaction: SwapQuoteResponse["transaction"]
): Promise<Address> {
  console.log(" Submitting transaction to the blockchain...");
  console.log(`   To: ${transaction.to}`);
  console.log(`   Gas limit: ${transaction.gas}`);
  console.log(
    `   Gas price: ${formatUnits(BigInt(transaction.gasPrice), 9)} Gwei`
  );
  console.log(`   Value: ${transaction.value} wei`);

  const hash = await walletClient.sendTransaction({
    to: transaction.to,
    data: transaction.data as `0x${string}`,
    gas: BigInt(transaction.gas),
    gasPrice: BigInt(transaction.gasPrice),
    value: transaction.value ? BigInt(transaction.value) : 0n,
  });

  return hash;
}

// ============================================
// 9. 主交换编排
// ============================================

/**
 * 执行完整的 Token 交换
 */
async function executeSwap(
  sellToken: Address,
  buyToken: Address,
  sellAmount: string,
  slippageBps: number = 100
): Promise<any> {
  console.log("\n" + "=".repeat(60));
  console.log(" STARTING TOKEN SWAP");
  console.log("=".repeat(60));

  const sellDecimals = await getTokenDecimals(sellToken);
  const buyDecimals = await getTokenDecimals(buyToken);

  console.log(`\n Swap Parameters:`);
  console.log(
    `   Sell: ${sellAmount} tokens (${sellToken}...)`
  );
  console.log(`   Buy: ${buyToken}...`);
  console.log(
    `   Slippage: ${slippageBps / 100}% (${slippageBps} bps)`
  );
  console.log(`   User: ${account.address}`);

  try {
    // 步骤 1:获取价格
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 1: FETCHING INDICATIVE PRICE");
    console.log("-".repeat(60));
    const priceData = await getPrice(
      sellToken,
      buyToken,
      sellAmount,
      sellDecimals,
      slippageBps
    );

    if (!priceData.liquidityAvailable) {
      throw new Error(" Insufficient liquidity for this trade");
    }

    const buyAmountFormatted = formatUnits(
      BigInt(priceData.buyAmount),
      buyDecimals
    );
    const minBuyAmountFormatted = priceData.minBuyAmount
      ? formatUnits(BigInt(priceData.minBuyAmount), buyDecimals)
      : "N/A";

    console.log(` Price fetched successfully:`);
    console.log(`   Expected output: ${buyAmountFormatted}`);
    console.log(`   Minimum output: ${minBuyAmountFormatted}`);
    console.log(`   Estimated gas: ${priceData.gas} units`);

    // 步骤 2:处理问题
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 2: CHECKING FOR ISSUES");
    console.log("-".repeat(60));

    // 处理问题(包括授权)
    await handleIssues(priceData);

    // 步骤 3:获取报价
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 3: FETCHING FIRM QUOTE");
    console.log("-".repeat(60));
    const quoteData = await getQuote(
      sellToken,
      buyToken,
      sellAmount,
      slippageBps,
      sellDecimals
    );

    const quoteBuyAmount = formatUnits(
      BigInt(quoteData.buyAmount),
      buyDecimals
    );
    console.log(` Quote received:`);
    console.log(`   Final output: ${quoteBuyAmount}`);
    console.log(`   Transaction to: ${quoteData.transaction.to}`);

    // 步骤 4:执行交换
    console.log("\n" + "-".repeat(60));
    console.log(" STEP 4: EXECUTING SWAP");
    console.log("-".repeat(60));
    const txHash = await submitTransaction(quoteData.transaction);

    console.log(` Transaction submitted!`);
    console.log(`   Hash: ${txHash}`);
    console.log("\n⏳ Waiting for confirmation...");

    const receipt = await publicClient.waitForTransactionReceipt({
      hash: txHash,
    });

    console.log("\n" + "=".repeat(60));
    if (receipt.status === "success") {
      console.log(` SWAP SUCCESSFUL!`);
      console.log(`   Block: ${receipt.blockNumber}`);
      console.log(`   Gas used: ${receipt.gasUsed.toString()}`);
    } else {
      console.log(` TRANSACTION FAILED`);
    }
    console.log("=".repeat(60) + "\n");

    return receipt;
  } catch (error) {
    console.error("\n Swap failed:", error);
    throw error;
  }
}

// ============================================
// 10. 实用函数
// ============================================

/**
 * 显示 Token 余额
 */
async function displayBalances(): Promise<void> {
  console.log("\n Current Balances:");

  for (const [symbol, address] of Object.entries(TOKENS)) {
    const balance = await checkBalance(address, account.address);
    const decimals = await getTokenDecimals(address);
    const formatted = formatUnits(balance, decimals);

    // 使用 ETH 后缀显示原生 ETH
    if (isNativeToken(address)) {
      console.log(`   ${symbol}: ${formatted} ETH`);
    } else {
      console.log(`   ${symbol}: ${formatted}`);
    }
  }
}

// ============================================
// 11. 主执行
// ============================================

async function main() {
  try {
    console.log(" Wallet Configuration:");
    console.log(`   Address: ${account.address}`);
    console.log(`   Network: Base (Chain ID: ${CHAIN_ID})`);

    // 显示当前余额
    await displayBalances();

    // 示例交换:0.000001 ETH -> USDC,滑点 1%(100 bps)
    await executeSwap(
      TOKENS.NATIVE, // 出售原生 ETH
      TOKENS.USDC, // 购买 USDC
      "0.000001", // 交换金额
      100 // 1% 滑点(以 bps 为单位)
    );

    // 显示更新后的余额
    await displayBalances();
  } catch (error) {
    console.error("Error in main:", error);
    process.exit(1);
  }
}

// 如果直接调用则运行
if (require.main === module) {
  main();
}

运行脚本

要运行你的机器人,请在 main 函数中调整交换参数,然后运行脚本:

tsx swap.ts

输出应大致如下:

 Wallet Configuration:
   Address: 0x0a417DDB75Dc491C90F044Ea725E8329A1592d00
   Network: Base (Chain ID: 8453)

 Current Balances:
   NATIVE: 0.007370389645673835 ETH
   WETH: 0
   USDC: 0
   USDe: 0
   WBTC: 0

============================================================
 STARTING TOKEN SWAP
============================================================

 Swap Parameters:
   Sell: 0.000001 tokens (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE...)
   Buy: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913...
   Slippage: 1% (100 bps)
   User: 0x0a417DDB75Dc491C90F044Ea725E8329A1592d00

   # ... 显示每个步骤的其余输出 ...

============================================================
 SWAP SUCCESSFUL!
   Block: 38479262
   Gas used: 483760
============================================================

 Current Balances:
   NATIVE: 0.007364615567936407 ETH
   WETH: 0
   USDC: 0.00278
   USDe: 0
   WBTC: 0

结论

你已经成功集成了 0x Swap API,实现了程序化的 Token 交换。通过这一流程,你的应用现在可以从超过 150 个流动性场所获取最高效的定价,而无需自己管理复杂的路由逻辑。

后续步骤

  • 探索高级路由:查看 includedSources 参数,将交换限制在特定的 DEX(例如,仅 Uniswap 或 Curve)。
  • 使用滑点保护:修改 /quote 请求中的 slippageBps 参数(基点),保护你的交易在执行期间免受市场波动的影响(例如,设置为 200 表示 2%)。
  • 构建前端:使用 wagmi 将此逻辑连接到 React 前端,让用户可以直接从你的 UI 交换 Token。

有关可用参数的更多详情,请查看官方 0x API 文档。

订阅我们的通讯,获取更多关于 Web3 和区块链的文章与指南。如有任何问题或需要进一步帮助,欢迎加入我们的 Discord 服务器,或在本页末尾的反馈部分提交反馈。关注我们的 X(@Quicknode)和 Telegram 公告频道,随时掌握最新动态。

  • 原文链接: quicknode.com/guides/qui...
  • 鸿途知科网 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
版权声明

本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门