swap-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Swap Integration

兑换功能集成

Integrate Uniswap swaps into frontends, backends, and smart contracts.
将Uniswap代币兑换功能集成至前端、后端及智能合约中。

Prerequisites

前置条件

This skill assumes familiarity with viem basics (client setup, account management, contract interactions, transaction signing). Install the uniswap-viem plugin for comprehensive viem/wagmi guidance:
claude plugin add @uniswap/uniswap-viem
本指南假设你已熟悉viem的基础知识(客户端设置、账户管理、合约交互、交易签名)。安装uniswap-viem插件以获取完整的viem/wagmi使用指引:
claude plugin add @uniswap/uniswap-viem

Quick Decision Guide

快速决策指南

Building...Use This Method
Frontend with React/Next.jsTrading API
Backend script or botTrading API
Smart contract integrationUniversal Router direct calls
Need full control over routingUniversal Router SDK
开发场景推荐使用方法
React/Next.js前端应用Trading API
后端脚本或机器人Trading API
智能合约集成直接调用Universal Router
需要完全控制兑换路由逻辑Universal Router SDK

Routing Types Quick Reference

路由类型速查

TypeDescriptionChains
CLASSICStandard AMM swap through Uniswap poolsAll supported chains
DUTCH_V2UniswapX Dutch auction V2Ethereum, Arbitrum, Base, Unichain
PRIORITYMEV-protected priority orderBase, Unichain
WRAPETH to WETH conversionAll
UNWRAPWETH to ETH conversionAll
See Routing Types for the complete list including DUTCH_V3, DUTCH_LIMIT, LIMIT_ORDER, BRIDGE, and QUICKROUTE.
类型描述支持链
CLASSIC通过Uniswap资金池的标准AMM兑换所有支持的链
DUTCH_V2UniswapX荷兰式拍卖V2版本Ethereum、Arbitrum、Base、Unichain
PRIORITY受MEV保护的优先订单Base、Unichain
WRAPETH转WETH兑换所有支持的链
UNWRAPWETH转ETH兑换所有支持的链
查看路由类型章节获取完整列表,包括DUTCH_V3、DUTCH_LIMIT、LIMIT_ORDER、BRIDGE和QUICKROUTE。

Integration Methods

集成方法

1. Trading API (Recommended)

1. Trading API(推荐)

Best for: Frontends, backends, scripts. Handles routing optimization automatically.
Base URL:
https://trade-api.gateway.uniswap.org/v1
Authentication:
x-api-key: <your-api-key>
header required
Getting an API Key: The Trading API requires an API key for authentication. Visit the Uniswap Developer Portal to register and obtain your API key. Keys are typically available for immediate use after registration. Include it as an
x-api-key
header in all API requests.
Required Headers — Include these in ALL Trading API requests:
text
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0
3-Step Flow:
text
1. POST /check_approval  -> Check if token is approved
2. POST /quote           -> Get executable quote with routing
3. POST /swap            -> Get transaction to sign and submit
See the Trading API Reference section below for complete documentation.
最佳适用场景:前端应用、后端服务、脚本。自动处理路由优化。
基础URL
https://trade-api.gateway.uniswap.org/v1
认证方式:请求头中需包含
x-api-key: <your-api-key>
获取API密钥:Trading API需要API密钥进行认证。访问Uniswap开发者门户注册并获取你的API密钥。注册后密钥通常可立即使用。在所有API请求的请求头中添加
x-api-key
字段。
必填请求头 — 所有Trading API请求都需包含以下字段:
text
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0
三步集成流程
text
1. POST /check_approval  -> 检查代币是否已授权
2. POST /quote           -> 获取包含路由信息的可执行报价
3. POST /swap            -> 获取待签名并提交的交易数据
查看下方Trading API参考章节获取完整文档。

2. Universal Router SDK

2. Universal Router SDK

Best for: Direct control over transaction construction.
Installation:
bash
npm install @uniswap/universal-router-sdk @uniswap/sdk-core @uniswap/v3-sdk
Key Pattern:
typescript
import { SwapRouter } from '@uniswap/universal-router-sdk';

const { calldata, value } = SwapRouter.swapCallParameters(trade, options);
See the Universal Router Reference section below for complete documentation.
最佳适用场景:需要直接控制交易构建逻辑的场景。
安装命令
bash
npm install @uniswap/universal-router-sdk @uniswap/sdk-core @uniswap/v3-sdk
核心使用模式
typescript
import { SwapRouter } from '@uniswap/universal-router-sdk';

const { calldata, value } = SwapRouter.swapCallParameters(trade, options);
查看下方Universal Router参考章节获取完整文档。

3. Smart Contract Integration

3. 智能合约集成

Best for: On-chain integrations, DeFi composability.
Interface: Call
execute()
on Universal Router with encoded commands.
See the Universal Router Reference section below for command encoding.

最佳适用场景:链上集成、DeFi组合应用。
集成方式:调用Universal Router的
execute()
方法并传入编码后的命令。
查看下方Universal Router参考章节获取命令编码方式。

Trading API Reference

Trading API参考

Step 1: Check Token Approval

步骤1:检查代币授权

bash
POST /check_approval
Request:
json
{
  "walletAddress": "0x...",
  "token": "0x...",
  "amount": "1000000000",
  "chainId": 1
}
Response:
json
{
  "approval": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1
  }
}
If
approval
is
null
, token is already approved.
bash
POST /check_approval
请求示例
json
{
  "walletAddress": "0x...",
  "token": "0x...",
  "amount": "1000000000",
  "chainId": 1
}
响应示例
json
{
  "approval": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1
  }
}
如果
approval
null
,表示代币已完成授权。

Step 2: Get Quote

步骤2:获取报价

bash
POST /quote
Request:
json
{
  "swapper": "0x...",
  "tokenIn": "0x...",
  "tokenOut": "0x...",
  "tokenInChainId": "1",
  "tokenOutChainId": "1",
  "amount": "1000000000000000000",
  "type": "EXACT_INPUT",
  "slippageTolerance": 0.5,
  "routingPreference": "BEST_PRICE"
}
Note:
tokenInChainId
and
tokenOutChainId
must be strings (e.g.,
"1"
), not numbers.
Key Parameters:
ParameterDescription
type
EXACT_INPUT
or
EXACT_OUTPUT
slippageTolerance
0-100 percentage
protocols
Optional:
["V2", "V3", "V4"]
routingPreference
BEST_PRICE
,
FASTEST
,
CLASSIC
autoSlippage
true
to auto-calculate slippage (overrides
slippageTolerance
)
urgency
normal
or
fast
— affects UniswapX auction timing
Response:
json
{
  "routing": "CLASSIC",
  "quote": {
    "input": { "token": "0x...", "amount": "1000000000000000000" },
    "output": { "token": "0x...", "amount": "999000000" },
    "slippage": 0.5,
    "route": [],
    "gasFee": "5000000000000000",
    "gasFeeUSD": "0.01",
    "gasUseEstimate": "150000"
  },
  "permitData": {}
}
Display tip: Use
gasFeeUSD
(a string with the USD value) for gas cost display. Do not manually convert
gasFee
(wei) using a hardcoded ETH price — this leads to wildly inaccurate estimates (e.g., ~$87 instead of ~$0.01).
bash
POST /quote
请求示例
json
{
  "swapper": "0x...",
  "tokenIn": "0x...",
  "tokenOut": "0x...",
  "tokenInChainId": "1",
  "tokenOutChainId": "1",
  "amount": "1000000000000000000",
  "type": "EXACT_INPUT",
  "slippageTolerance": 0.5,
  "routingPreference": "BEST_PRICE"
}
注意
tokenInChainId
tokenOutChainId
必须为字符串类型(例如
"1"
),不能是数字。
关键参数说明
参数名称描述
type
EXACT_INPUT
(固定输入金额)或
EXACT_OUTPUT
(固定输出金额)
slippageTolerance
滑点容忍度,取值范围0-100(百分比)
protocols
可选参数:
["V2", "V3", "V4"]
routingPreference
BEST_PRICE
(最优价格)、
FASTEST
(最快速度)、
CLASSIC
(经典路由)
autoSlippage
设置为
true
时自动计算滑点(会覆盖
slippageTolerance
参数)
urgency
normal
(普通)或
fast
(加急)——影响UniswapX拍卖的时间参数
响应示例
json
{
  "routing": "CLASSIC",
  "quote": {
    "input": { "token": "0x...", "amount": "1000000000000000000" },
    "output": { "token": "0x...", "amount": "999000000" },
    "slippage": 0.5,
    "route": [],
    "gasFee": "5000000000000000",
    "gasFeeUSD": "0.01",
    "gasUseEstimate": "150000"
  },
  "permitData": {}
}
显示提示:使用
gasFeeUSD
(美元价值的字符串)展示Gas成本。请勿手动将
gasFee
(wei单位)通过硬编码的ETH价格转换——这会导致估算结果严重不准确(例如实际约0.01美元,估算却显示约87美元)。

Step 3: Execute Swap

步骤3:执行兑换

bash
POST /swap
Request - Spread the quote response directly into the body:
typescript
// CORRECT: Spread the quote response, strip null fields
const quoteResponse = await fetchQuote(params);

// Remove null permitData/permitTransaction (API rejects null values)
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

const swapRequest = {
  ...cleanQuote,
  // Only include permitData if it's a valid object (not null)
  ...(permitData && { permitData }),
};

// If using Permit2 signature, include BOTH signature and permitData
if (permit2Signature && permitData) {
  swapRequest.signature = permit2Signature;
  swapRequest.permitData = permitData;
}
Critical: Do NOT wrap the quote in
{quote: quoteResponse}
. The API expects the quote response fields spread into the request body.
Permit2 Rules:
  • signature
    and
    permitData
    must BOTH be present, or BOTH be absent
  • Never set
    permitData: null
    - omit the field entirely
  • The quote response often includes
    permitData: null
    - strip this before sending
Response (ready-to-sign transaction):
json
{
  "swap": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1,
    "gasLimit": "250000"
  }
}
Response Validation - Always validate before broadcasting:
typescript
function validateSwapResponse(response: SwapResponse): void {
  if (!response.swap?.data || response.swap.data === '' || response.swap.data === '0x') {
    throw new Error('swap.data is empty - quote may have expired');
  }
  if (!isAddress(response.swap.to) || !isAddress(response.swap.from)) {
    throw new Error('Invalid address in swap response');
  }
}
bash
POST /swap
请求示例 — 将报价响应直接展开到请求体中:
typescript
// 正确做法:展开报价响应,移除null字段
const quoteResponse = await fetchQuote(params);

// 移除null的permitData/permitTransaction字段(API会拒绝null值)
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

const swapRequest = {
  ...cleanQuote,
  // 仅当permitData是有效对象时才包含该字段(而非null)
  ...(permitData && { permitData }),
};

// 如果使用Permit2签名,需同时包含signature和permitData
if (permit2Signature && permitData) {
  swapRequest.signature = permit2Signature;
  swapRequest.permitData = permitData;
}
重要提示:请勿将报价包裹在
{quote: quoteResponse}
中。API期望将报价响应的字段直接展开到请求体中。
Permit2规则
  • signature
    permitData
    必须同时存在,或同时不存在
  • 绝对不要设置
    permitData: null
    — 完全省略该字段
  • 报价响应中通常会包含
    permitData: null
    — 发送请求前需移除该字段
响应示例(可直接签名的交易数据):
json
{
  "swap": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1,
    "gasLimit": "250000"
  }
}
响应验证 - 广播前必须进行验证:
typescript
function validateSwapResponse(response: SwapResponse): void {
  if (!response.swap?.data || response.swap.data === '' || response.swap.data === '0x') {
    throw new Error('swap.data为空 - 报价可能已过期');
  }
  if (!isAddress(response.swap.to) || !isAddress(response.swap.from)) {
    throw new Error('兑换响应中的地址无效');
  }
}

Supported Chains

支持的区块链

IDChainIDChain
1Ethereum8453Base
10Optimism42161Arbitrum
56BNB42220Celo
130Unichain43114Avalanche
137Polygon81457Blast
196X Layer7777777Zora
324zkSync480World Chain
1868Soneium143Monad
链ID区块链名称链ID区块链名称
1Ethereum8453Base
10Optimism42161Arbitrum
56BNB42220Celo
130Unichain43114Avalanche
137Polygon81457Blast
196X Layer7777777Zora
324zkSync480World Chain
1868Soneium143Monad

Routing Types

路由类型

TypeDescription
CLASSICStandard AMM swap through Uniswap pools
DUTCH_V2UniswapX Dutch auction V2
DUTCH_V3UniswapX Dutch auction V3
PRIORITYMEV-protected priority order (Base, Unichain)
DUTCH_LIMITUniswapX Dutch limit order
LIMIT_ORDERLimit order
WRAPETH to WETH conversion
UNWRAPWETH to ETH conversion
BRIDGECross-chain bridge
QUICKROUTEFast approximation quote
UniswapX availability: UniswapX V2 orders are supported on Ethereum (1), Arbitrum (42161), Base (8453), and Unichain (130). The auction mechanism varies by chain — see UniswapX Auction Types below.

类型描述
CLASSIC通过Uniswap资金池的标准AMM兑换
DUTCH_V2UniswapX荷兰式拍卖V2版本
DUTCH_V3UniswapX荷兰式拍卖V3版本
PRIORITY受MEV保护的优先订单(Base、Unichain)
DUTCH_LIMITUniswapX限价荷兰式拍卖
LIMIT_ORDER限价订单
WRAPETH转WETH兑换
UNWRAPWETH转ETH兑换
BRIDGE跨链桥接
QUICKROUTE快速估算报价
UniswapX可用性:UniswapX V2订单支持在Ethereum(1)、Arbitrum(42161)、Base(8453)和Unichain(130)上使用。不同链的拍卖机制有所差异——查看下方UniswapX拍卖类型章节了解详情。

Critical Implementation Notes

关键实现注意事项

These are common pitfalls discovered during real-world Trading API integration. Follow these rules to avoid on-chain reverts and API errors.
以下是在实际Trading API集成中发现的常见陷阱。请遵循这些规则以避免链上交易回滚和API错误。

1. Swap Request Body Format

1. 兑换请求体格式

The
/swap
endpoint expects the quote response spread into the request body, not wrapped in a
quote
field.
typescript
// WRONG - causes "quote does not match any of the allowed types"
const badRequest = {
  quote: quoteResponse, // Don't wrap!
  signature: '0x...',
};

// CORRECT - spread the quote response
const goodRequest = {
  ...quoteResponse,
  signature: '0x...', // Only if using Permit2
};
/swap
接口期望将报价响应直接展开到请求体中,而非包裹在
quote
字段里。
typescript
// 错误写法 - 会导致"quote does not match any of the allowed types"错误
const badRequest = {
  quote: quoteResponse, // 不要包裹!
  signature: '0x...',
};

// 正确写法 - 展开报价响应
const goodRequest = {
  ...quoteResponse,
  signature: '0x...', // 仅当使用Permit2时添加
};

2. Null Field Handling

2. Null字段处理

The API rejects
permitData: null
. Always strip null fields before sending:
typescript
function prepareSwapRequest(quoteResponse: QuoteResponse, signature?: string): object {
  // Strip null values that the API rejects
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

  const request: Record<string, unknown> = { ...cleanQuote };

  // Only include permitData if it's a valid object AND we have a signature
  if (signature && permitData && typeof permitData === 'object') {
    request.signature = signature;
    request.permitData = permitData;
  }

  return request;
}
API会拒绝
permitData: null
的请求。发送前务必移除所有null字段:
typescript
function prepareSwapRequest(quoteResponse: QuoteResponse, signature?: string): object {
  // 移除API会拒绝的null值
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

  const request: Record<string, unknown> = { ...cleanQuote };

  // 仅当同时拥有signature和有效的permitData对象时,才包含这两个字段
  if (signature && permitData && typeof permitData === 'object') {
    request.signature = signature;
    request.permitData = permitData;
  }

  return request;
}

3. Permit2 Field Rules

3. Permit2字段规则

When using Permit2 for gasless approvals:
Scenario
signature
permitData
Standard swap (no Permit2)OmitOmit
Permit2 swapRequiredRequired
InvalidPresentMissing
InvalidMissingPresent
Invalid (API error)Any
null
当使用Permit2实现无Gas授权时:
场景
signature
permitData
标准兑换(不使用Permit2)省略省略
使用Permit2的兑换必填必填
无效场景存在缺失
无效场景缺失存在
无效场景(API错误)任意值
null

4. Pre-Broadcast Validation

4. 广播前验证

Always validate the swap response before sending to the blockchain:
typescript
import { isAddress, isHex } from 'viem';

function validateSwapBeforeBroadcast(swap: SwapTransaction): void {
  // 1. data must be non-empty hex
  if (!swap.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.data is empty - this will revert on-chain. Re-fetch the quote.');
  }

  if (!isHex(swap.data)) {
    throw new Error('swap.data is not valid hex');
  }

  // 2. Addresses must be valid
  if (!isAddress(swap.to)) {
    throw new Error('swap.to is not a valid address');
  }

  if (!isAddress(swap.from)) {
    throw new Error('swap.from is not a valid address');
  }

  // 3. Value must be present (can be "0" for non-ETH swaps)
  if (swap.value === undefined || swap.value === null) {
    throw new Error('swap.value is missing');
  }
}
在将兑换交易发送到区块链前,务必验证响应数据:
typescript
import { isAddress, isHex } from 'viem';

function validateSwapBeforeBroadcast(swap: SwapTransaction): void {
  // 1. data必须是非空的十六进制字符串
  if (!swap.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.data为空 - 该交易在链上会回滚。请重新获取报价。');
  }

  if (!isHex(swap.data)) {
    throw new Error('swap.data不是有效的十六进制字符串');
  }

  // 2. 地址必须有效
  if (!isAddress(swap.to)) {
    throw new Error('swap.to不是有效的地址');
  }

  if (!isAddress(swap.from)) {
    throw new Error('swap.from不是有效的地址');
  }

  // 3. Value字段必须存在(非ETH兑换时可以为"0")
  if (swap.value === undefined || swap.value === null) {
    throw new Error('swap.value字段缺失');
  }
}

5. Browser Environment Setup

5. 浏览器环境配置

When using viem/wagmi in browser environments, you need Node.js polyfills:
Install buffer polyfill:
bash
npm install buffer
Add to your entry file (before other imports):
typescript
// src/main.tsx or src/index.tsx
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;

// Then your other imports
import React from 'react';
import { WagmiProvider } from 'wagmi';
// ...
Vite configuration (
vite.config.ts
):
typescript
export default defineConfig({
  define: {
    global: 'globalThis',
  },
  optimizeDeps: {
    include: ['buffer'],
  },
  resolve: {
    alias: {
      buffer: 'buffer',
    },
  },
});
Without this setup, you'll see:
ReferenceError: Buffer is not defined
在浏览器环境中使用viem/wagmi时,需要Node.js polyfills:
安装buffer polyfill
bash
npm install buffer
在入口文件中添加(在其他导入之前)
typescript
// src/main.tsx或src/index.tsx
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;

// 然后导入其他依赖
import React from 'react';
import { WagmiProvider } from 'wagmi';
// ...
Vite配置 (
vite.config.ts
):
typescript
export default defineConfig({
  define: {
    global: 'globalThis',
  },
  optimizeDeps: {
    include: ['buffer'],
  },
  resolve: {
    alias: {
      buffer: 'buffer',
    },
  },
});
如果不进行此配置,你会看到错误:
ReferenceError: Buffer is not defined

CORS Proxy Configuration

CORS代理配置

The Trading API does not support browser CORS preflight requests —
OPTIONS
requests return
415 Unsupported Media Type
. Direct
fetch()
calls from a browser will always fail. You must proxy API requests through your own server or dev server.
Vite dev proxy (merge into the same
vite.config.ts
used for the Buffer polyfill above):
typescript
export default defineConfig({
  server: {
    proxy: {
      '/api/uniswap': {
        target: 'https://trade-api.gateway.uniswap.org/v1',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api\/uniswap/, ''),
      },
    },
  },
});
Then use
/api/uniswap/quote
instead of the full URL in your frontend code.
Vercel production proxy (
vercel.json
):
json
{
  "rewrites": [
    {
      "source": "/api/uniswap/:path*",
      "destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
    }
  ]
}
Cloudflare Pages (
public/_redirects
):
text
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200
Next.js (
next.config.js
):
javascript
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/uniswap/:path*',
        destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
      },
    ];
  },
};
Without a proxy, you'll see:
415 Unsupported Media Type
on preflight or CORS errors in the browser console.
Trading API不支持浏览器CORS预检请求——
OPTIONS
请求会返回
415 Unsupported Media Type
。直接在浏览器中调用
fetch()
会始终失败。你必须通过自有服务器或开发服务器代理API请求。
Vite开发环境代理(与上述Buffer polyfill配置合并到同一个
vite.config.ts
中):
typescript
export default defineConfig({
  server: {
    proxy: {
      '/api/uniswap': {
        target: 'https://trade-api.gateway.uniswap.org/v1',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api\/uniswap/, ''),
      },
    },
  },
});
然后在前端代码中使用
/api/uniswap/quote
替代完整URL。
Vercel生产环境代理 (
vercel.json
):
json
{
  "rewrites": [
    {
      "source": "/api/uniswap/:path*",
      "destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
    }
  ]
}
Cloudflare Pages配置 (
public/_redirects
):
text
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200
Next.js配置 (
next.config.js
):
javascript
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/uniswap/:path*',
        destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
      },
    ];
  },
};
如果没有代理,你会在浏览器控制台看到:
415 Unsupported Media Type
(预检请求)或CORS错误。

6. Quote Freshness

6. 报价时效性

  • Quotes expire quickly (typically 30 seconds)
  • Always re-fetch if the user takes time to review
  • Use the
    deadline
    parameter to prevent stale execution
  • If
    /swap
    returns empty
    data
    , the quote likely expired

  • 报价过期速度快(通常为30秒)
  • 如果用户需要时间确认,务必重新获取报价
  • 使用
    deadline
    参数防止执行过期报价
  • 如果
    /swap
    返回空的
    data
    字段,说明报价可能已过期

Universal Router Reference

Universal Router参考

The Universal Router is a unified interface for swapping across Uniswap v2, v3, and v4.
Universal Router是一个统一的接口,支持在Uniswap v2、v3和v4之间进行兑换。

Core Function

核心函数

solidity
function execute(
    bytes calldata commands,
    bytes[] calldata inputs,
    uint256 deadline
) external payable;
solidity
function execute(
    bytes calldata commands,
    bytes[] calldata inputs,
    uint256 deadline
) external payable;

Command Encoding

命令编码

Each command is a single byte:
BitsNamePurpose
0flagAllow revert (1 = continue on fail)
1-2reservedUse 0
3-7commandOperation identifier
每个命令为单个字节:
位段名称用途
0flag是否允许回滚(1表示失败后继续执行)
1-2reserved保留位,使用0
3-7command操作标识符

Swap Commands

兑换命令

CodeCommandDescription
0x00V3_SWAP_EXACT_INv3 swap with exact input
0x01V3_SWAP_EXACT_OUTv3 swap with exact output
0x08V2_SWAP_EXACT_INv2 swap with exact input
0x09V2_SWAP_EXACT_OUTv2 swap with exact output
0x10V4_SWAPv4 swap
代码命令名称描述
0x00V3_SWAP_EXACT_INv3固定输入金额兑换
0x01V3_SWAP_EXACT_OUTv3固定输出金额兑换
0x08V2_SWAP_EXACT_INv2固定输入金额兑换
0x09V2_SWAP_EXACT_OUTv2固定输出金额兑换
0x10V4_SWAPv4兑换

Token Operations

代币操作命令

CodeCommandDescription
0x04SWEEPClear router token balance
0x05TRANSFERSend specific amount
0x0bWRAP_ETHETH to WETH
0x0cUNWRAP_WETHWETH to ETH
代码命令名称描述
0x04SWEEP清空路由中的代币余额
0x05TRANSFER发送指定金额的代币
0x0bWRAP_ETHETH转WETH
0x0cUNWRAP_WETHWETH转ETH

Permit2 Commands

Permit2命令

CodeCommandDescription
0x02PERMIT2_TRANSFER_FROMSingle token transfer
0x03PERMIT2_PERMIT_BATCHBatch approval
0x0aPERMIT2_PERMITSingle approval
代码命令名称描述
0x02PERMIT2_TRANSFER_FROM单一代币转账
0x03PERMIT2_PERMIT_BATCH批量授权
0x0aPERMIT2_PERMIT单次授权

SDK Usage

SDK使用示例

typescript
import { SwapRouter, UniswapTrade } from '@uniswap/universal-router-sdk'
import { TradeType } from '@uniswap/sdk-core'

// Build trade using v3-sdk or router-sdk
const trade = new RouterTrade({
  v3Routes: [...],
  tradeType: TradeType.EXACT_INPUT
})

// Get calldata for Universal Router
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1200 // 20 min
})

// Send transaction
const tx = await wallet.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value
})

typescript
import { SwapRouter, UniswapTrade } from '@uniswap/universal-router-sdk'
import { TradeType } from '@uniswap/sdk-core'

// 使用v3-sdk或router-sdk构建交易
const trade = new RouterTrade({
  v3Routes: [...],
  tradeType: TradeType.EXACT_INPUT
})

// 获取Universal Router调用数据
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1200 // 20分钟
})

// 发送交易
const tx = await wallet.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value
})

Permit2 Integration

Permit2集成

Permit2 enables signature-based token approvals instead of on-chain approve() calls.
Permit2支持基于签名的代币授权,替代链上
approve()
调用。

Approval Target: Permit2 vs Legacy (Direct to Router)

授权目标:Permit2 vs 传统方式(直接授权给路由)

There are two approval paths. Choose based on your integration type:
ApproachApprove ToPer-Swap AuthBest For
Permit2 (recommended)Permit2 contractEIP-712 signatureFrontends with user interaction
Legacy (direct approve)Universal RouterNone (pre-approved)Backend services, smart accounts
Permit2 flow (frontend with user signing):
  1. User approves token to Permit2 contract (one-time)
  2. Each swap: user signs an EIP-712 permit message
  3. Universal Router uses the signature to transfer tokens via Permit2
Legacy flow (backend services, ERC-4337 smart accounts):
  1. Approve token directly to the Universal Router address (one-time)
  2. Each swap: no additional authorization needed
  3. Simpler for automated systems that cannot sign EIP-712 messages
Use the Trading API's
/check_approval
endpoint — it returns the correct approval target based on the routing type.
有两种授权路径,根据你的集成类型选择:
方式授权目标地址每次兑换的认证方式最佳适用场景
Permit2(推荐)Permit2合约EIP-712签名有用户交互的前端应用
传统方式(直接授权)Universal Router地址无(预先授权完成)后端服务、智能账户
Permit2流程(有用户签名的前端应用):
  1. 用户一次性授权代币给Permit2合约
  2. 每次兑换:用户签署EIP-712许可消息
  3. Universal Router使用签名通过Permit2转移代币
传统流程(后端服务、ERC-4337智能账户):
  1. 直接将代币授权给Universal Router地址(一次性操作)
  2. 每次兑换:无需额外授权
  3. 更适合无法签署EIP-712消息的自动化系统
使用Trading API的
/check_approval
接口——它会根据路由类型返回正确的授权目标。

How It Works

工作原理

  1. User approves Permit2 contract once (infinite approval)
  2. For each swap, user signs a message authorizing the transfer
  3. Universal Router uses signature to transfer tokens via Permit2
  1. 用户一次性授权Permit2合约(无限授权)
  2. 每次兑换:用户签署授权转移的消息
  3. Universal Router使用签名通过Permit2转移代币

Two Modes

两种模式

ModeDescription
SignatureTransferOne-time signature, no on-chain state
AllowanceTransferTime-limited allowance with on-chain state
模式描述
SignatureTransfer单次签名,无链上状态变更
AllowanceTransfer限时授权,会产生链上状态变更

Integration Pattern

集成模式

typescript
import { getContract, maxUint256, type Address } from 'viem';

const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' as const;

// Check if Permit2 approval exists
const allowance = await publicClient.readContract({
  address: PERMIT2_ADDRESS,
  abi: permit2Abi,
  functionName: 'allowance',
  args: [userAddress, tokenAddress, spenderAddress],
});

// If not approved, user must approve Permit2 first
if (allowance.amount < requiredAmount) {
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'approve',
    args: [PERMIT2_ADDRESS, maxUint256],
  });
  await publicClient.waitForTransactionReceipt({ hash });
}

// Then sign permit for the swap
const permitSignature = await signPermit(...);

typescript
import { getContract, maxUint256, type Address } from 'viem';

const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' as const;

// 检查Permit2授权是否存在
const allowance = await publicClient.readContract({
  address: PERMIT2_ADDRESS,
  abi: permit2Abi,
  functionName: 'allowance',
  args: [userAddress, tokenAddress, spenderAddress],
});

// 如果未授权,用户必须先授权Permit2
if (allowance.amount < requiredAmount) {
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'approve',
    args: [PERMIT2_ADDRESS, maxUint256],
  });
  await publicClient.waitForTransactionReceipt({ hash });
}

// 然后为兑换签署许可
const permitSignature = await signPermit(...);

UniswapX Auction Types

UniswapX拍卖类型

UniswapX routes swaps through off-chain fillers who compete to execute orders at better prices than on-chain AMMs. The auction mechanism varies by chain.
UniswapX通过链下填充者完成兑换,填充者会竞争以比链上AMM更优的价格执行订单。不同链的拍卖机制有所差异。

Exclusive Dutch Auction (Ethereum)

专属荷兰式拍卖(Ethereum)

  • Starts with an RFQ (Request for Quote) phase where permissioned quoters compete
  • Winning quoter receives exclusive filling rights for a set period
  • If the exclusive filler doesn't execute, falls back to an open Dutch auction where the price decays each block
  • Best for large swaps where MEV protection matters most
Trading API routing type:
DUTCH_V2
or
DUTCH_V3
  • 先进行RFQ(报价请求)阶段,由授权报价方竞争
  • 获胜的报价方获得专属填充权,持续一定时间
  • 如果专属填充者未执行订单,会回退到公开荷兰式拍卖,价格随区块高度下降
  • 最适合对MEV防护要求高的大额兑换
Trading API路由类型
DUTCH_V2
DUTCH_V3

Open Dutch Auction (Arbitrum)

公开荷兰式拍卖(Arbitrum)

  • Direct open auction without an RFQ phase
  • Fillers compete on-chain through a descending price mechanism
  • Leverages Arbitrum's fast 0.25-second block times for rapid price discovery
  • The Unimind algorithm sets auction parameters based on historical pair performance
Trading API routing type:
DUTCH_V2
  • 直接进行公开拍卖,无RFQ阶段
  • 填充者通过链上降价机制竞争
  • 利用Arbitrum的0.25秒快速出块时间实现快速价格发现
  • Unimind算法根据历史交易对表现设置拍卖参数
Trading API路由类型
DUTCH_V2

Priority Gas Auction (Base, Unichain)

优先Gas拍卖(Base、Unichain)

  • Fillers bid by submitting transactions with varying priority fees at a target block
  • Highest priority fee wins the right to fill the order
  • Exploits OP Stack's priority ordering mechanism
  • Effective on chains where block builders respect priority ordering
Trading API routing type:
PRIORITY
  • 填充者通过在目标区块提交不同优先费用的交易来竞标
  • 优先费用最高的填充者获得订单填充权
  • 利用OP Stack的优先排序机制
  • 在区块构建者尊重优先排序的链上效果最佳
Trading API路由类型
PRIORITY

Key Properties (All Auction Types)

所有拍卖类型的核心特性

  • Gasless for users — fillers pay gas fees, incorporated into final pricing
  • No cost on failure — if a swap doesn't fill, the user pays nothing
  • MEV protection — auction mechanics prevent frontrunning and sandwich attacks
  • UniswapX V2 is currently supported on Ethereum (1), Arbitrum (42161), Base (8453), and Unichain (130)
For more detail, see the UniswapX Auction Types documentation.

  • 用户无需支付Gas — 填充者支付Gas费用,成本已包含在最终价格中
  • 失败无成本 — 如果兑换未完成,用户无需支付任何费用
  • MEV防护 — 拍卖机制防止抢跑和三明治攻击
  • UniswapX V2目前支持在Ethereum(1)、Arbitrum(42161)、Base(8453)和Unichain(130)上使用
更多详情请查看UniswapX拍卖类型文档

Direct Universal Router Integration (SDK)

直接集成Universal Router(SDK方式)

For direct Universal Router integration without the Trading API, use the SDK's high-level API.
如果不使用Trading API,直接集成Universal Router,请使用SDK的高级API。

Installation

安装命令

bash
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viem
bash
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viem

High-Level Approach (Recommended)

高级方式(推荐)

Use
RouterTrade
+
SwapRouter.swapCallParameters()
for automatic command building:
typescript
import { SwapRouter } from '@uniswap/universal-router-sdk';
import { Trade as RouterTrade } from '@uniswap/router-sdk';
import { TradeType, Percent } from '@uniswap/sdk-core';
import { Route as V3Route, Pool } from '@uniswap/v3-sdk';

// 1. Fetch pool data (required to construct routes)
// Using viem to read on-chain pool state:
const slot0 = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'slot0',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [
        { name: 'sqrtPriceX96', type: 'uint160' },
        { name: 'tick', type: 'int24' },
        { name: 'observationIndex', type: 'uint16' },
        { name: 'observationCardinality', type: 'uint16' },
        { name: 'observationCardinalityNext', type: 'uint16' },
        { name: 'feeProtocol', type: 'uint8' },
        { name: 'unlocked', type: 'bool' },
      ],
    },
  ],
  functionName: 'slot0',
});
const liquidity = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'liquidity',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [{ type: 'uint128' }],
    },
  ],
  functionName: 'liquidity',
});

const pool = new Pool(tokenIn, tokenOut, fee, slot0[0].toString(), liquidity.toString(), slot0[1]);

// 2. Build route and trade
const route = new V3Route([pool], tokenIn, tokenOut);
const trade = RouterTrade.createUncheckedTrade({
  route,
  inputAmount: amountIn,
  outputAmount: expectedOut,
  tradeType: TradeType.EXACT_INPUT,
});

// 3. Get calldata
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1800,
});

// 4. Execute with viem
const hash = await walletClient.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value: BigInt(value),
});
使用
RouterTrade
+
SwapRouter.swapCallParameters()
自动构建命令:
typescript
import { SwapRouter } from '@uniswap/universal-router-sdk';
import { Trade as RouterTrade } from '@uniswap/router-sdk';
import { TradeType, Percent } from '@uniswap/sdk-core';
import { Route as V3Route, Pool } from '@uniswap/v3-sdk';

// 1. 获取资金池数据(构建路由所需)
// 使用viem读取链上资金池状态:
const slot0 = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'slot0',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [
        { name: 'sqrtPriceX96', type: 'uint160' },
        { name: 'tick', type: 'int24' },
        { name: 'observationIndex', type: 'uint16' },
        { name: 'observationCardinality', type: 'uint16' },
        { name: 'observationCardinalityNext', type: 'uint16' },
        { name: 'feeProtocol', type: 'uint8' },
        { name: 'unlocked', type: 'bool' },
      ],
    },
  ],
  functionName: 'slot0',
});
const liquidity = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'liquidity',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [{ type: 'uint128' }],
    },
  ],
  functionName: 'liquidity',
});

const pool = new Pool(tokenIn, tokenOut, fee, slot0[0].toString(), liquidity.toString(), slot0[1]);

// 2. 构建路由和交易
const route = new V3Route([pool], tokenIn, tokenOut);
const trade = RouterTrade.createUncheckedTrade({
  route,
  inputAmount: amountIn,
  outputAmount: expectedOut,
  tradeType: TradeType.EXACT_INPUT,
});

// 3. 获取调用数据
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1800,
});

// 4. 使用viem执行交易
const hash = await walletClient.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value: BigInt(value),
});

Low-Level Approach (Manual Commands)

低级方式(手动构建命令)

For custom flows (fee collection, complex routing), use
RoutePlanner
directly:
typescript
import { RoutePlanner, CommandType, ROUTER_AS_RECIPIENT } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath } from '@uniswap/v3-sdk';

// Special addresses
const MSG_SENDER = '0x0000000000000000000000000000000000000001';
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002';
对于自定义流程(如费用收取、复杂路由),直接使用
RoutePlanner
typescript
import { RoutePlanner, CommandType, ROUTER_AS_RECIPIENT } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath } from '@uniswap/v3-sdk';

// 特殊地址
const MSG_SENDER = '0x0000000000000000000000000000000000000001';
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002';

Example: V3 Swap with Manual Commands

示例:手动构建V3兑换命令

typescript
import { RoutePlanner, CommandType } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath, Route } from '@uniswap/v3-sdk';

async function swapV3Manual(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();

  // Encode V3 path from route
  const path = encodeRouteToPath(route, false); // false = exactInput

  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER, // recipient
    amountIn, // amountIn
    amountOutMin, // amountOutMin
    path, // encoded path
    true, // payerIsUser
  ]);

  return executeRoute(planner);
}
typescript
import { RoutePlanner, CommandType } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath, Route } from '@uniswap/v3-sdk';

async function swapV3Manual(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();

  // 从路由编码V3路径
  const path = encodeRouteToPath(route, false); // false = 固定输入金额

  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER, // 接收者
    amountIn, // 输入金额
    amountOutMin, // 最小输出金额
    path, // 编码后的路径
    true, // 支付者为用户
  ]);

  return executeRoute(planner);
}

Example: ETH to Token (Wrap + Swap)

示例:ETH转代币(Wrap + 兑换)

typescript
async function swapEthToToken(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);

  // 1. Wrap ETH to WETH (keep in router)
  planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, amountIn]);

  // 2. Swap WETH → Token (payerIsUser = false since using router's WETH)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER,
    amountIn,
    amountOutMin,
    path,
    false,
  ]);

  return executeRoute(planner, { value: amountIn });
}
typescript
async function swapEthToToken(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);

  // 1. 将ETH包装为WETH(保留在路由中)
  planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, amountIn]);

  // 2. WETH → 代币兑换(支付者不是用户,因为使用路由中的WETH)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER,
    amountIn,
    amountOutMin,
    path,
    false,
  ]);

  return executeRoute(planner, { value: amountIn });
}

Example: Token to ETH (Swap + Unwrap)

示例:代币转ETH(兑换 + Unwrap)

typescript
async function swapTokenToEth(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);

  // 1. Swap Token → WETH (output to router)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    ADDRESS_THIS,
    amountIn,
    amountOutMin,
    path,
    true,
  ]);

  // 2. Unwrap WETH to ETH
  planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOutMin]);

  return executeRoute(planner);
}
typescript
async function swapTokenToEth(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);

  // 1. 代币 → WETH兑换(输出到路由)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    ADDRESS_THIS,
    amountIn,
    amountOutMin,
    path,
    true,
  ]);

  // 2. 将WETH解包为ETH
  planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOutMin]);

  return executeRoute(planner);
}

Example: Fee Collection with PAY_PORTION

示例:收取费用的兑换(使用PAY_PORTION)

typescript
async function swapWithFee(route: Route, amountIn: bigint, feeRecipient: Address, feeBips: number) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);
  const outputToken = route.output.wrapped.address;

  // Swap to router (ADDRESS_THIS)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ADDRESS_THIS, amountIn, 0n, path, true]);

  // Pay fee portion (e.g., 30 bips = 0.3%)
  planner.addCommand(CommandType.PAY_PORTION, [outputToken, feeRecipient, feeBips]);

  // Sweep remainder to user
  planner.addCommand(CommandType.SWEEP, [outputToken, MSG_SENDER, 0n]);

  return executeRoute(planner);
}
typescript
async function swapWithFee(route: Route, amountIn: bigint, feeRecipient: Address, feeBips: number) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);
  const outputToken = route.output.wrapped.address;

  // 兑换到路由(ADDRESS_THIS)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ADDRESS_THIS, amountIn, 0n, path, true]);

  // 支付费用部分(例如30个基点 = 0.3%)
  planner.addCommand(CommandType.PAY_PORTION, [outputToken, feeRecipient, feeBips]);

  // 将剩余金额转给用户
  planner.addCommand(CommandType.SWEEP, [outputToken, MSG_SENDER, 0n]);

  return executeRoute(planner);
}

Execute Route Helper

路由执行辅助函数

typescript
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk';

const ROUTER_ABI = [
  {
    name: 'execute',
    type: 'function',
    stateMutability: 'payable',
    inputs: [
      { name: 'commands', type: 'bytes' },
      { name: 'inputs', type: 'bytes[]' },
      { name: 'deadline', type: 'uint256' },
    ],
    outputs: [],
  },
] as const;

async function executeRoute(planner: RoutePlanner, options?: { value?: bigint }) {
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
  const routerAddress = UNIVERSAL_ROUTER_ADDRESS('2.0', 1); // version, chainId

  const { request } = await publicClient.simulateContract({
    address: routerAddress,
    abi: ROUTER_ABI,
    functionName: 'execute',
    args: [planner.commands, planner.inputs, deadline],
    account,
    value: options?.value ?? 0n,
  });

  return walletClient.writeContract(request);
}
typescript
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk';

const ROUTER_ABI = [
  {
    name: 'execute',
    type: 'function',
    stateMutability: 'payable',
    inputs: [
      { name: 'commands', type: 'bytes' },
      { name: 'inputs', type: 'bytes[]' },
      { name: 'deadline', type: 'uint256' },
    ],
    outputs: [],
  },
] as const;

async function executeRoute(planner: RoutePlanner, options?: { value?: bigint }) {
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
  const routerAddress = UNIVERSAL_ROUTER_ADDRESS('2.0', 1); // 版本号,链ID

  const { request } = await publicClient.simulateContract({
    address: routerAddress,
    abi: ROUTER_ABI,
    functionName: 'execute',
    args: [planner.commands, planner.inputs, deadline],
    account,
    value: options?.value ?? 0n,
  });

  return walletClient.writeContract(request);
}

Command Cheat Sheet

命令速查表

CommandParameters
V3_SWAP_EXACT_IN(recipient, amountIn, amountOutMin, path, payerIsUser)
V3_SWAP_EXACT_OUT(recipient, amountOut, amountInMax, path, payerIsUser)
V2_SWAP_EXACT_IN(recipient, amountIn, amountOutMin, path[], payerIsUser)
V2_SWAP_EXACT_OUT(recipient, amountOut, amountInMax, path[], payerIsUser)
WRAP_ETH(recipient, amount)
UNWRAP_WETH(recipient, amountMin)
SWEEP(token, recipient, amountMin)
TRANSFER(token, recipient, amount)
PAY_PORTION(token, recipient, bips)
命令名称参数列表
V3_SWAP_EXACT_IN(recipient, amountIn, amountOutMin, path, payerIsUser)
V3_SWAP_EXACT_OUT(recipient, amountOut, amountInMax, path, payerIsUser)
V2_SWAP_EXACT_IN(recipient, amountIn, amountOutMin, path[], payerIsUser)
V2_SWAP_EXACT_OUT(recipient, amountOut, amountInMax, path[], payerIsUser)
WRAP_ETH(recipient, amount)
UNWRAP_WETH(recipient, amountMin)
SWEEP(token, recipient, amountMin)
TRANSFER(token, recipient, amount)
PAY_PORTION(token, recipient, bips)

Fee Tiers

手续费层级

TierValuePercentage
LOWEST1000.01%
LOW5000.05%
MEDIUM30000.30%
HIGH100001.00%

层级数值百分比
LOWEST1000.01%
LOW5000.05%
MEDIUM30000.30%
HIGH100001.00%

Common Integration Patterns

常见集成模式

Frontend Swap Hook (React)

前端兑换Hook(React)

Note: Ensure you've set up the Buffer polyfill and CORS proxy (see Critical Implementation Notes). For wagmi v2
useWalletClient()
pitfalls, see wagmi v2 Integration Pitfalls below.
typescript
import { isAddress, isHex } from 'viem';
import { useWalletClient } from 'wagmi';

// In browser apps, use your CORS proxy path instead (see CORS Proxy Configuration)
// e.g., const API_URL = '/api/uniswap';
const API_URL = 'https://trade-api.gateway.uniswap.org/v1';

function useSwap() {
  const { data: walletClient } = useWalletClient();
  const [quoteResponse, setQuoteResponse] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const getQuote = async (params) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(`${API_URL}/quote`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': API_KEY,
          'x-universal-router-version': '2.0',
        },
        body: JSON.stringify(params),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.detail || 'Quote failed');
      setQuoteResponse(data); // Store the FULL response, not just data.quote
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const executeSwap = async (permit2Signature?: string) => {
    if (!quoteResponse) throw new Error('No quote available');

    // CRITICAL: Strip null fields and spread quote response into body
    const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

    const swapRequest: Record<string, unknown> = {
      ...cleanQuote,
    };

    // CRITICAL: Only include permitData if we have BOTH signature and permitData
    // The API requires both fields to be present or both to be absent
    if (permit2Signature && permitData && typeof permitData === 'object') {
      swapRequest.signature = permit2Signature;
      swapRequest.permitData = permitData;
    }

    const swapResponse = await fetch(`${API_URL}/swap`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY,
        'x-universal-router-version': '2.0',
      },
      body: JSON.stringify(swapRequest),
    });
    const data = await swapResponse.json();
    if (!swapResponse.ok) throw new Error(data.detail || 'Swap failed');

    // CRITICAL: Validate response before broadcasting
    if (!data.swap?.data || data.swap.data === '' || data.swap.data === '0x') {
      throw new Error('Empty swap data - quote may have expired. Please refresh.');
    }

    // Send transaction via wallet (walletClient from useWalletClient())
    if (!walletClient) throw new Error('Wallet not connected');
    const tx = await walletClient.sendTransaction(data.swap);
    return tx;
  };

  return { quote: quoteResponse?.quote, loading, error, getQuote, executeSwap };
}
注意:确保已完成Buffer polyfill和CORS代理配置(查看关键实现注意事项)。关于wagmi v2
useWalletClient()
的陷阱,查看下方wagmi v2集成陷阱
typescript
import { isAddress, isHex } from 'viem';
import { useWalletClient } from 'wagmi';

// 在浏览器应用中,使用你的CORS代理路径(查看CORS代理配置)
// 例如:const API_URL = '/api/uniswap';
const API_URL = 'https://trade-api.gateway.uniswap.org/v1';

function useSwap() {
  const { data: walletClient } = useWalletClient();
  const [quoteResponse, setQuoteResponse] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const getQuote = async (params) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(`${API_URL}/quote`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': API_KEY,
          'x-universal-router-version': '2.0',
        },
        body: JSON.stringify(params),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.detail || '获取报价失败');
      setQuoteResponse(data); // 存储完整响应,而不仅仅是data.quote
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const executeSwap = async (permit2Signature?: string) => {
    if (!quoteResponse) throw new Error('暂无可用报价');

    // 重要:移除null字段并将报价响应展开到请求体中
    const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

    const swapRequest: Record<string, unknown> = {
      ...cleanQuote,
    };

    // 重要:仅当同时拥有signature和permitData时才包含这两个字段
    // API要求这两个字段要么同时存在,要么同时不存在
    if (permit2Signature && permitData && typeof permitData === 'object') {
      swapRequest.signature = permit2Signature;
      swapRequest.permitData = permitData;
    }

    const swapResponse = await fetch(`${API_URL}/swap`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY,
        'x-universal-router-version': '2.0',
      },
      body: JSON.stringify(swapRequest),
    });
    const data = await swapResponse.json();
    if (!swapResponse.ok) throw new Error(data.detail || '兑换失败');

    // 重要:广播前验证响应
    if (!data.swap?.data || data.swap.data === '' || data.swap.data === '0x') {
      throw new Error('兑换数据为空 - 报价可能已过期,请刷新。');
    }

    // 通过钱包发送交易(walletClient来自useWalletClient())
    if (!walletClient) throw new Error('钱包未连接');
    const tx = await walletClient.sendTransaction(data.swap);
    return tx;
  };

  return { quote: quoteResponse?.quote, loading, error, getQuote, executeSwap };
}

wagmi v2 Integration Pitfalls

wagmi v2集成陷阱

The
useWalletClient()
hook from wagmi v2 can return
undefined
even when the wallet is connected — it resolves asynchronously. This causes "wallet not connected" errors at swap time. Additionally, the returned client needs a
chain
for
sendTransaction()
to work.
Recommended pattern — use
@wagmi/core
action functions at swap time instead of hooks:
typescript
import { getWalletClient, getPublicClient, switchChain } from '@wagmi/core';
import type { Config } from 'wagmi';

async function executeSwapTransaction(
  config: Config,
  chainId: number,
  swapTx: { to: string; data: string; value: string }
) {
  // 1. Ensure the wallet is on the correct chain
  await switchChain(config, { chainId });

  // 2. Get wallet client with explicit chainId — avoids undefined and missing chain
  const walletClient = await getWalletClient(config, { chainId });

  // 3. Execute the swap
  const hash = await walletClient.sendTransaction({
    to: swapTx.to as `0x${string}`,
    data: swapTx.data as `0x${string}`,
    value: BigInt(swapTx.value || '0'),
  });

  // 4. Wait for confirmation
  const publicClient = getPublicClient(config, { chainId });
  if (!publicClient) throw new Error(`No public client configured for chainId ${chainId}`);
  return publicClient.waitForTransactionReceipt({ hash });
}
Why this matters:
  • useWalletClient()
    hook returns
    { data: undefined }
    during async resolution, even after
    useAccount()
    shows connected
  • getWalletClient(config, { chainId })
    is a promise that resolves only when the client is ready, and includes the chain
  • switchChain()
    prevents "chain mismatch" errors when the wallet is on a different network than the swap
wagmi v2的
useWalletClient()
钩子即使在钱包已连接的情况下也可能返回
undefined
——它是异步解析的。这会导致兑换时出现“钱包未连接”的错误。此外,返回的客户端需要
chain
参数才能正常使用
sendTransaction()
推荐模式 — 在兑换时使用
@wagmi/core
的action函数替代钩子:
typescript
import { getWalletClient, getPublicClient, switchChain } from '@wagmi/core';
import type { Config } from 'wagmi';

async function executeSwapTransaction(
  config: Config,
  chainId: number,
  swapTx: { to: string; data: string; value: string }
) {
  // 1. 确保钱包在正确的链上
  await switchChain(config, { chainId });

  // 2. 获取带明确chainId的钱包客户端——避免undefined和缺失chain的问题
  const walletClient = await getWalletClient(config, { chainId });

  // 3. 执行兑换
  const hash = await walletClient.sendTransaction({
    to: swapTx.to as `0x${string}`,
    data: swapTx.data as `0x${string}`,
    value: BigInt(swapTx.value || '0'),
  });

  // 4. 等待确认
  const publicClient = getPublicClient(config, { chainId });
  if (!publicClient) throw new Error(`未为链ID ${chainId}配置公共客户端`);
  return publicClient.waitForTransactionReceipt({ hash });
}
为什么这很重要
  • useWalletClient()
    钩子在异步解析期间返回
    { data: undefined }
    ,即使
    useAccount()
    显示已连接
  • getWalletClient(config, { chainId })
    是一个Promise,仅在客户端准备好后才会解析,并且包含chain信息
  • switchChain()
    可以防止钱包网络与兑换网络不匹配的错误

Backend Swap Script (Node.js)

后端兑换脚本(Node.js)

typescript
import { createWalletClient, createPublicClient, http, isAddress, isHex, type Address } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

const API_URL = 'https://trade-api.gateway.uniswap.org/v1';
const API_KEY = process.env.UNISWAP_API_KEY!;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });

// Helper to strip null fields from quote response
function prepareSwapRequest(quoteResponse: Record<string, unknown>, signature?: string): object {
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

  const request: Record<string, unknown> = { ...cleanQuote };

  // CRITICAL: Only include permitData if we have BOTH signature and permitData
  // The API requires both fields to be present or both to be absent
  if (signature && permitData && typeof permitData === 'object') {
    request.signature = signature;
    request.permitData = permitData;
  }

  return request;
}

// Validate swap response before broadcasting
function validateSwap(swap: { data?: string; to?: string; from?: string }): void {
  if (!swap?.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.data is empty - quote may have expired');
  }
  if (!isHex(swap.data)) {
    throw new Error('swap.data is not valid hex');
  }
  if (!swap.to || !isAddress(swap.to) || !swap.from || !isAddress(swap.from)) {
    throw new Error('Invalid address in swap response');
  }
}

async function executeSwap(tokenIn: Address, tokenOut: Address, amount: string, chainId: number) {
  const ETH_ADDRESS = '0x0000000000000000000000000000000000000000';

  // 1. Check approval (for ERC20 tokens, not native ETH)
  if (tokenIn !== ETH_ADDRESS) {
    const approvalRes = await fetch(`${API_URL}/check_approval`, {
      method: 'POST',
      headers: {
        'x-api-key': API_KEY,
        'Content-Type': 'application/json',
        'x-universal-router-version': '2.0',
      },
      body: JSON.stringify({
        walletAddress: account.address,
        token: tokenIn,
        amount,
        chainId,
      }),
    });
    const approvalData = await approvalRes.json();

    if (approvalData.approval) {
      const hash = await walletClient.sendTransaction({
        to: approvalData.approval.to,
        data: approvalData.approval.data,
        value: BigInt(approvalData.approval.value || '0'),
      });
      await publicClient.waitForTransactionReceipt({ hash });
    }
  }

  // 2. Get quote
  const quoteRes = await fetch(`${API_URL}/quote`, {
    method: 'POST',
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
      'x-universal-router-version': '2.0',
    },
    body: JSON.stringify({
      swapper: account.address,
      tokenIn,
      tokenOut,
      tokenInChainId: String(chainId),
      tokenOutChainId: String(chainId),
      amount,
      type: 'EXACT_INPUT',
      slippageTolerance: 0.5,
    }),
  });
  const quoteResponse = await quoteRes.json(); // Store FULL response

  if (!quoteRes.ok) {
    throw new Error(quoteResponse.detail || 'Quote failed');
  }

  // 3. Execute swap - CRITICAL: spread quote response, strip null fields
  const swapRequest = prepareSwapRequest(quoteResponse);

  const swapRes = await fetch(`${API_URL}/swap`, {
    method: 'POST',
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
      'x-universal-router-version': '2.0',
    },
    body: JSON.stringify(swapRequest),
  });
  const swapData = await swapRes.json();

  if (!swapRes.ok) {
    throw new Error(swapData.detail || 'Swap request failed');
  }

  // 4. Validate before broadcasting
  validateSwap(swapData.swap);

  const hash = await walletClient.sendTransaction({
    to: swapData.swap.to,
    data: swapData.swap.data,
    value: BigInt(swapData.swap.value || '0'),
  });
  return publicClient.waitForTransactionReceipt({ hash });
}
typescript
import { createWalletClient, createPublicClient, http, isAddress, isHex, type Address } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

const API_URL = 'https://trade-api.gateway.uniswap.org/v1';
const API_KEY = process.env.UNISWAP_API_KEY!;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });

// 移除报价响应中null字段的辅助函数
function prepareSwapRequest(quoteResponse: Record<string, unknown>, signature?: string): object {
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

  const request: Record<string, unknown> = { ...cleanQuote };

  // 重要:仅当同时拥有signature和permitData时才包含这两个字段
  // API要求这两个字段要么同时存在,要么同时不存在
  if (signature && permitData && typeof permitData === 'object') {
    request.signature = signature;
    request.permitData = permitData;
  }

  return request;
}

// 广播前验证兑换响应
function validateSwap(swap: { data?: string; to?: string; from?: string }): void {
  if (!swap?.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.data为空 - 报价可能已过期');
  }
  if (!isHex(swap.data)) {
    throw new Error('swap.data不是有效的十六进制字符串');
  }
  if (!swap.to || !isAddress(swap.to) || !swap.from || !isAddress(swap.from)) {
    throw new Error('兑换响应中的地址无效');
  }
}

async function executeSwap(tokenIn: Address, tokenOut: Address, amount: string, chainId: number) {
  const ETH_ADDRESS = '0x0000000000000000000000000000000000000000';

  // 1. 检查授权(仅针对ERC20代币,不包括原生ETH)
  if (tokenIn !== ETH_ADDRESS) {
    const approvalRes = await fetch(`${API_URL}/check_approval`, {
      method: 'POST',
      headers: {
        'x-api-key': API_KEY,
        'Content-Type': 'application/json',
        'x-universal-router-version': '2.0',
      },
      body: JSON.stringify({
        walletAddress: account.address,
        token: tokenIn,
        amount,
        chainId,
      }),
    });
    const approvalData = await approvalRes.json();

    if (approvalData.approval) {
      const hash = await walletClient.sendTransaction({
        to: approvalData.approval.to,
        data: approvalData.approval.data,
        value: BigInt(approvalData.approval.value || '0'),
      });
      await publicClient.waitForTransactionReceipt({ hash });
    }
  }

  // 2. 获取报价
  const quoteRes = await fetch(`${API_URL}/quote`, {
    method: 'POST',
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
      'x-universal-router-version': '2.0',
    },
    body: JSON.stringify({
      swapper: account.address,
      tokenIn,
      tokenOut,
      tokenInChainId: String(chainId),
      tokenOutChainId: String(chainId),
      amount,
      type: 'EXACT_INPUT',
      slippageTolerance: 0.5,
    }),
  });
  const quoteResponse = await quoteRes.json(); // 存储完整响应

  if (!quoteRes.ok) {
    throw new Error(quoteResponse.detail || '获取报价失败');
  }

  // 3. 执行兑换 - 重要:展开报价响应,移除null字段
  const swapRequest = prepareSwapRequest(quoteResponse);

  const swapRes = await fetch(`${API_URL}/swap`, {
    method: 'POST',
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
      'x-universal-router-version': '2.0',
    },
    body: JSON.stringify(swapRequest),
  });
  const swapData = await swapRes.json();

  if (!swapRes.ok) {
    throw new Error(swapData.detail || '兑换请求失败');
  }

  // 4. 广播前验证
  validateSwap(swapData.swap);

  const hash = await walletClient.sendTransaction({
    to: swapData.swap.to,
    data: swapData.swap.data,
    value: BigInt(swapData.swap.value || '0'),
  });
  return publicClient.waitForTransactionReceipt({ hash });
}

Smart Contract Integration (Solidity)

智能合约集成(Solidity)

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IUniversalRouter {
    function execute(
        bytes calldata commands,
        bytes[] calldata inputs,
        uint256 deadline
    ) external payable;
}

interface IERC20 {
    function approve(address spender, uint256 amount) external returns (bool);
}

contract SwapIntegration {
    IUniversalRouter public immutable router;
    address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

    constructor(address _router) {
        router = IUniversalRouter(_router);
    }

    function swap(
        bytes calldata commands,
        bytes[] calldata inputs,
        uint256 deadline
    ) external payable {
        router.execute{value: msg.value}(commands, inputs, deadline);
    }

    // Approve token for Permit2 (one-time setup)
    function approveToken(address token) external {
        IERC20(token).approve(PERMIT2, type(uint256).max);
    }
}

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IUniversalRouter {
    function execute(
        bytes calldata commands,
        bytes[] calldata inputs,
        uint256 deadline
    ) external payable;
}

interface IERC20 {
    function approve(address spender, uint256 amount) external returns (bool);
}

contract SwapIntegration {
    IUniversalRouter public immutable router;
    address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

    constructor(address _router) {
        router = IUniversalRouter(_router);
    }

    function swap(
        bytes calldata commands,
        bytes[] calldata inputs,
        uint256 deadline
    ) external payable {
        router.execute{value: msg.value}(commands, inputs, deadline);
    }

    // 授权代币给Permit2(一次性设置)
    function approveToken(address token) external {
        IERC20(token).approve(PERMIT2, type(uint256).max);
    }
}

Advanced Patterns

高级模式

Smart Account Integration (ERC-4337)

智能账户集成(ERC-4337)

Execute Trading API swaps through ERC-4337 smart accounts with delegation. The pattern:
  1. Get swap calldata from Trading API (standard 3-step flow)
  2. Wrap the calldata in a delegation redemption execution
  3. Submit via bundler as a UserOperation
typescript
// After getting swap calldata from Trading API:
const { to, data, value } = swapResponse.swap;

// Wrap in delegation execution
const execution = {
  target: to, // Universal Router
  callData: data,
  value: BigInt(value),
};

// Submit via bundler
const userOpHash = await bundlerClient.sendUserOperation({
  account: delegateSmartAccount,
  calls: [
    {
      to: delegationManagerAddress,
      data: encodeFunctionData({
        abi: delegationManagerAbi,
        functionName: 'redeemDelegations',
        args: [[[signedDelegation]], [0], [[execution]]],
      }),
      value: execution.value,
    },
  ],
});
Key considerations:
  • Use legacy approvals (direct to Universal Router) instead of Permit2 for smart accounts — see Approval Target
  • Add 20-30% gas buffer for bundler gas estimation
  • Handle bundler-specific error codes separately from standard transaction errors
See Advanced Patterns Reference for the complete implementation with types and error handling.
通过ERC-4337智能账户执行Trading API兑换,使用委托模式。流程如下:
  1. 从Trading API获取兑换调用数据(标准三步流程)
  2. 将调用数据包装在委托赎回执行中
  3. 通过bundler作为UserOperation提交
typescript
// 从Trading API获取兑换调用数据后:
const { to, data, value } = swapResponse.swap;

// 包装为委托执行
const execution = {
  target: to, // Universal Router
  callData: data,
  value: BigInt(value),
};

// 通过bundler提交
const userOpHash = await bundlerClient.sendUserOperation({
  account: delegateSmartAccount,
  calls: [
    {
      to: delegationManagerAddress,
      data: encodeFunctionData({
        abi: delegationManagerAbi,
        functionName: 'redeemDelegations',
        args: [[[signedDelegation]], [0], [[execution]]],
      }),
      value: execution.value,
    },
  ],
});
关键注意事项:
  • 对于智能账户,使用传统授权方式(直接授权给Universal Router)替代Permit2——查看授权目标
  • 为bundler的Gas估算添加20-30%的缓冲
  • 单独处理bundler特定的错误码,与标准交易错误区分开
查看高级模式参考获取完整实现,包括类型定义和错误处理。

WETH Handling on L2s

L2链上的WETH处理

On L2 chains (Base, Optimism, Arbitrum), swaps outputting ETH may deliver WETH instead of native ETH. Always check and unwrap after swaps:
typescript
import { parseAbi, type Address } from 'viem';

const WETH_ABI = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'function withdraw(uint256)',
]);

const WETH_ADDRESSES: Record<number, Address> = {
  1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  10: '0x4200000000000000000000000000000000000006',
  8453: '0x4200000000000000000000000000000000000006',
  42161: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
};

// After swap completes on an L2:
const wethAddress = WETH_ADDRESSES[chainId];
if (wethAddress) {
  const wethBalance = await publicClient.readContract({
    address: wethAddress,
    abi: WETH_ABI,
    functionName: 'balanceOf',
    args: [accountAddress],
  });

  if (wethBalance > 0n) {
    const hash = await walletClient.writeContract({
      address: wethAddress,
      abi: WETH_ABI,
      functionName: 'withdraw',
      args: [wethBalance],
    });
    await publicClient.waitForTransactionReceipt({ hash });
  }
}
See Advanced Patterns Reference for chain-specific WETH addresses and integration details.
在L2链(Base、Optimism、Arbitrum)上,输出为ETH的兑换可能会返回WETH而非原生ETH。兑换后务必检查并解包:
typescript
import { parseAbi, type Address } from 'viem';

const WETH_ABI = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'function withdraw(uint256)',
]);

const WETH_ADDRESSES: Record<number, Address> = {
  1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  10: '0x4200000000000000000000000000000000000006',
  8453: '0x4200000000000000000000000000000000000006',
  42161: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
};

// 在L2链上完成兑换后:
const wethAddress = WETH_ADDRESSES[chainId];
if (wethAddress) {
  const wethBalance = await publicClient.readContract({
    address: wethAddress,
    abi: WETH_ABI,
    functionName: 'balanceOf',
    args: [accountAddress],
  });

  if (wethBalance > 0n) {
    const hash = await walletClient.writeContract({
      address: wethAddress,
      abi: WETH_ABI,
      functionName: 'withdraw',
      args: [wethBalance],
    });
    await publicClient.waitForTransactionReceipt({ hash });
  }
}
查看高级模式参考获取各链的WETH地址和集成详情。

Rate Limiting

速率限制

The Trading API enforces rate limits (~10 requests/second per endpoint). For batch operations:
  • Add 100-200ms delays between sequential API calls
  • Implement exponential backoff with jitter on 429 responses
  • Cache approval results — approvals rarely change between calls
typescript
// Exponential backoff for 429 responses
async function fetchWithRetry(url: string, init: RequestInit, maxRetries = 5): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, init);
    if (response.status !== 429 && response.status < 500) return response;
    if (attempt === maxRetries) throw new Error(`Failed after ${maxRetries} retries`);

    const delay = Math.min(200 * Math.pow(2, attempt) + Math.random() * 100, 10000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
  throw new Error('Unreachable');
}
See Advanced Patterns Reference for batch operation patterns and full retry implementation.

Trading API实施速率限制(每个端点约10次请求/秒)。对于批量操作:
  • 在连续API调用之间添加100-200毫秒的延迟
  • 收到429响应时实现带抖动的指数退避重试
  • 缓存授权结果 — 授权结果在多次调用之间很少变化
typescript
// 针对429响应的指数退避重试
async function fetchWithRetry(url: string, init: RequestInit, maxRetries = 5): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, init);
    if (response.status !== 429 && response.status < 500) return response;
    if (attempt === maxRetries) throw new Error(`经过${maxRetries}次重试后仍失败`);

    const delay = Math.min(200 * Math.pow(2, attempt) + Math.random() * 100, 10000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
  throw new Error('无法到达此处');
}
查看高级模式参考获取批量操作模式和完整重试实现。

Key Contract Addresses

核心合约地址

Universal Router (v4)

Universal Router(v4)

Addresses are per-chain. The legacy v1 address
0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD
is deprecated.
ChainIDAddress
Ethereum1
0x66a9893cc07d91d95644aedd05d03f95e1dba8af
Unichain130
0xef740bf23acae26f6492b10de645d6b98dc8eaf3
Optimism10
0x851116d9223fabed8e56c0e6b8ad0c31d98b3507
Base8453
0x6ff5693b99212da76ad316178a184ab56d299b43
Arbitrum42161
0xa51afafe0263b40edaef0df8781ea9aa03e381a3
Polygon137
0x1095692a6237d83c6a72f3f5efedb9a670c49223
Blast81457
0xeabbcb3e8e415306207ef514f660a3f820025be3
BNB56
0x1906c1d672b88cd1b9ac7593301ca990f94eae07
Zora7777777
0x3315ef7ca28db74abadc6c44570efdf06b04b020
World Chain480
0x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743
Avalanche43114
0x94b75331ae8d42c1b61065089b7d48fe14aa73b7
Celo42220
0xcb695bc5d3aa22cad1e6df07801b061a05a0233a
Soneium1868
0x4cded7edf52c8aa5259a54ec6a3ce7c6d2a455df
Ink57073
0x112908dac86e20e7241b0927479ea3bf935d1fa0
Monad143
0x0d97dc33264bfc1c226207428a79b26757fb9dc3
For testnet addresses, see Uniswap v4 Deployments.
地址按链区分。旧版v1地址
0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD
已废弃。
区块链名称链ID地址
Ethereum1
0x66a9893cc07d91d95644aedd05d03f95e1dba8af
Unichain130
0xef740bf23acae26f6492b10de645d6b98dc8eaf3
Optimism10
0x851116d9223fabed8e56c0e6b8ad0c31d98b3507
Base8453
0x6ff5693b99212da76ad316178a184ab56d299b43
Arbitrum42161
0xa51afafe0263b40edaef0df8781ea9aa03e381a3
Polygon137
0x1095692a6237d83c6a72f3f5efedb9a670c49223
Blast81457
0xeabbcb3e8e415306207ef514f660a3f820025be3
BNB56
0x1906c1d672b88cd1b9ac7593301ca990f94eae07
Zora7777777
0x3315ef7ca28db74abadc6c44570efdf06b04b020
World Chain480
0x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743
Avalanche43114
0x94b75331ae8d42c1b61065089b7d48fe14aa73b7
Celo42220
0xcb695bc5d3aa22cad1e6df07801b061a05a0233a
Soneium1868
0x4cded7edf52c8aa5259a54ec6a3ce7c6d2a455df
Ink57073
0x112908dac86e20e7241b0927479ea3bf935d1fa0
Monad143
0x0d97dc33264bfc1c226207428a79b26757fb9dc3
测试网地址请查看Uniswap v4部署文档

Permit2

Permit2

ChainAddress
All chains
0x000000000022D473030F116dDEE9F6B43aC78BA3

区块链名称地址
所有链
0x000000000022D473030F116dDEE9F6B43aC78BA3

Troubleshooting

故障排除

Common Issues

常见问题

IssueSolution
"Insufficient allowance"Call /check_approval first and submit approval tx
"Quote expired"Increase deadline or re-fetch quote
"Slippage exceeded"Increase slippageTolerance or retry
"Insufficient liquidity"Try smaller amount or different route
"Buffer is not defined"Add Buffer polyfill (see Critical Implementation Notes)
On-chain revert with empty dataValidate
swap.data
is non-empty hex before broadcasting
"permitData must be of type object"Strip
permitData: null
from request - omit field entirely
"quote does not match any of the allowed types"Don't wrap quote in
{quote: ...}
- spread it into request body
Received WETH instead of ETH on L2Check and unwrap WETH after swap (see WETH Handling on L2s)
429 Too Many RequestsImplement exponential backoff and add delays between batch requests (see Rate Limiting)
415 on OPTIONS preflight / CORS errorSet up a CORS proxy (see CORS Proxy Configuration in Browser Environment Setup)
walletClient is undefined when wallet is connectedUse
getWalletClient()
from
@wagmi/core
instead of the
useWalletClient()
hook (see wagmi v2 Integration Pitfalls)
"Please provide a chain with the chain argument"Pass
chainId
to
getWalletClient(config, { chainId })
Chain mismatch error on swapCall
switchChain()
before
getWalletClient()
(see wagmi v2 Integration Pitfalls)
问题描述解决方案
"授权额度不足"先调用/check_approval并提交授权交易
"报价已过期"延长deadline或重新获取报价
"滑点超出限制"提高滑点容忍度或重试
"流动性不足"尝试更小的金额或不同的路由
"Buffer is not defined"添加Buffer polyfill(查看关键实现注意事项)
链上回滚且数据为空广播前验证
swap.data
为非空十六进制字符串
"permitData must be of type object"从请求中移除
permitData: null
字段 - 完全省略该字段
"quote does not match any of the allowed types"不要将报价包裹在
{quote: ...}
中 - 直接展开到请求体中
在L2链上收到WETH而非ETH兑换后检查并解包WETH(查看L2链上的WETH处理
429 Too Many Requests实现指数退避重试,并在批量请求之间添加延迟(查看速率限制)
OPTIONS预检请求返回415 / CORS错误设置CORS代理(查看浏览器环境设置中的CORS代理配置
钱包已连接但walletClient为undefined使用
@wagmi/core
getWalletClient()
替代
useWalletClient()
钩子(查看wagmi v2集成陷阱)
"Please provide a chain with the chain argument"
getWalletClient(config, { chainId })
传入
chainId
参数
兑换时出现链不匹配错误
getWalletClient()
前调用
switchChain()
(查看wagmi v2集成陷阱)

API Validation Errors (400)

API验证错误(400)

Error MessageCauseFix
"permitData" must be of type object
Sending
permitData: null
Omit the field entirely when null
"quote" does not match any of the allowed types
Wrapping quote in
{quote: quoteResponse}
Spread quote response:
{...quoteResponse}
signature and permitData must both be present
Including only one Permit2 fieldInclude both or neither
错误信息原因修复方案
"permitData" must be of type object
发送了
permitData: null
当为null时完全省略该字段
"quote" does not match any of the allowed types
将报价包裹在
{quote: quoteResponse}
展开报价响应:
{...quoteResponse}
signature and permitData must both be present
仅包含了一个Permit2字段同时包含两个字段或都不包含

API Error Codes

API错误码

CodeMeaning
400Invalid request parameters (see validation errors above)
401Invalid or missing API key
404No route found for pair
429Rate limit exceeded
500API error - implement exponential backoff retry
错误码含义
400请求参数无效(查看上方验证错误)
401API密钥无效或缺失
404未找到交易对的路由
429超出速率限制
500API内部错误 - 实现指数退避重试

Pre-Broadcast Checklist

广播前检查清单

Before sending a swap transaction to the blockchain:
  1. Verify
    swap.data
    is non-empty hex (not
    ''
    , not
    '0x'
    )
  2. Verify addresses -
    swap.to
    and
    swap.from
    are valid
  3. Check quote freshness - Re-fetch if older than 30 seconds
  4. Validate gas - Apply 10-20% buffer to estimates
  5. Confirm balance - User has sufficient token balance

在将兑换交易发送到区块链前:
  1. 验证
    swap.data
    为非空十六进制字符串(不是
    ''
    ,不是
    '0x'
  2. 验证地址 -
    swap.to
    swap.from
    为有效地址
  3. 检查报价时效性 - 如果超过30秒,重新获取报价
  4. 验证Gas - 为估算值添加10-20%的缓冲
  5. 确认余额 - 用户有足够的代币余额

Additional Resources

额外资源