swap-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwap 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-viemQuick Decision Guide
快速决策指南
| Building... | Use This Method |
|---|---|
| Frontend with React/Next.js | Trading API |
| Backend script or bot | Trading API |
| Smart contract integration | Universal Router direct calls |
| Need full control over routing | Universal Router SDK |
| 开发场景 | 推荐使用方法 |
|---|---|
| React/Next.js前端应用 | Trading API |
| 后端脚本或机器人 | Trading API |
| 智能合约集成 | 直接调用Universal Router |
| 需要完全控制兑换路由逻辑 | Universal Router SDK |
Routing Types Quick Reference
路由类型速查
| Type | Description | Chains |
|---|---|---|
| CLASSIC | Standard AMM swap through Uniswap pools | All supported chains |
| DUTCH_V2 | UniswapX Dutch auction V2 | Ethereum, Arbitrum, Base, Unichain |
| PRIORITY | MEV-protected priority order | Base, Unichain |
| WRAP | ETH to WETH conversion | All |
| UNWRAP | WETH to ETH conversion | All |
See Routing Types for the complete list including DUTCH_V3, DUTCH_LIMIT, LIMIT_ORDER, BRIDGE, and QUICKROUTE.
| 类型 | 描述 | 支持链 |
|---|---|---|
| CLASSIC | 通过Uniswap资金池的标准AMM兑换 | 所有支持的链 |
| DUTCH_V2 | UniswapX荷兰式拍卖V2版本 | Ethereum、Arbitrum、Base、Unichain |
| PRIORITY | 受MEV保护的优先订单 | Base、Unichain |
| WRAP | ETH转WETH兑换 | 所有支持的链 |
| UNWRAP | WETH转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/v1Authentication: header required
x-api-key: <your-api-key>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 header in all API requests.
x-api-keyRequired Headers — Include these in ALL Trading API requests:
text
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.03-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 submitSee 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-sdkKey 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 on Universal Router with encoded commands.
execute()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_approvalRequest:
json
{
"walletAddress": "0x...",
"token": "0x...",
"amount": "1000000000",
"chainId": 1
}Response:
json
{
"approval": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1
}
}If is , token is already approved.
approvalnullbash
POST /check_approval请求示例:
json
{
"walletAddress": "0x...",
"token": "0x...",
"amount": "1000000000",
"chainId": 1
}响应示例:
json
{
"approval": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1
}
}如果为,表示代币已完成授权。
approvalnullStep 2: Get Quote
步骤2:获取报价
bash
POST /quoteRequest:
json
{
"swapper": "0x...",
"tokenIn": "0x...",
"tokenOut": "0x...",
"tokenInChainId": "1",
"tokenOutChainId": "1",
"amount": "1000000000000000000",
"type": "EXACT_INPUT",
"slippageTolerance": 0.5,
"routingPreference": "BEST_PRICE"
}Note:andtokenInChainIdmust be strings (e.g.,tokenOutChainId), not numbers."1"
Key Parameters:
| Parameter | Description |
|---|---|
| |
| 0-100 percentage |
| Optional: |
| |
| |
| |
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(a string with the USD value) for gas cost display. Do not manually convertgasFeeUSD(wei) using a hardcoded ETH price — this leads to wildly inaccurate estimates (e.g., ~$87 instead of ~$0.01).gasFee
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"
关键参数说明:
| 参数名称 | 描述 |
|---|---|
| |
| 滑点容忍度,取值范围0-100(百分比) |
| 可选参数: |
| |
| 设置为 |
| |
响应示例:
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": {}
}显示提示:使用(美元价值的字符串)展示Gas成本。请勿手动将gasFeeUSD(wei单位)通过硬编码的ETH价格转换——这会导致估算结果严重不准确(例如实际约0.01美元,估算却显示约87美元)。gasFee
Step 3: Execute Swap
步骤3:执行兑换
bash
POST /swapRequest - 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 . The API expects the quote response fields spread into the request body.
{quote: quoteResponse}Permit2 Rules:
- and
signaturemust BOTH be present, or BOTH be absentpermitData - Never set - omit the field entirely
permitData: null - The quote response often includes - strip this before sending
permitData: null
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;
}重要提示:请勿将报价包裹在中。API期望将报价响应的字段直接展开到请求体中。
{quote: quoteResponse}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
支持的区块链
| ID | Chain | ID | Chain |
|---|---|---|---|
| 1 | Ethereum | 8453 | Base |
| 10 | Optimism | 42161 | Arbitrum |
| 56 | BNB | 42220 | Celo |
| 130 | Unichain | 43114 | Avalanche |
| 137 | Polygon | 81457 | Blast |
| 196 | X Layer | 7777777 | Zora |
| 324 | zkSync | 480 | World Chain |
| 1868 | Soneium | 143 | Monad |
| 链ID | 区块链名称 | 链ID | 区块链名称 |
|---|---|---|---|
| 1 | Ethereum | 8453 | Base |
| 10 | Optimism | 42161 | Arbitrum |
| 56 | BNB | 42220 | Celo |
| 130 | Unichain | 43114 | Avalanche |
| 137 | Polygon | 81457 | Blast |
| 196 | X Layer | 7777777 | Zora |
| 324 | zkSync | 480 | World Chain |
| 1868 | Soneium | 143 | Monad |
Routing Types
路由类型
| Type | Description |
|---|---|
| CLASSIC | Standard AMM swap through Uniswap pools |
| DUTCH_V2 | UniswapX Dutch auction V2 |
| DUTCH_V3 | UniswapX Dutch auction V3 |
| PRIORITY | MEV-protected priority order (Base, Unichain) |
| DUTCH_LIMIT | UniswapX Dutch limit order |
| LIMIT_ORDER | Limit order |
| WRAP | ETH to WETH conversion |
| UNWRAP | WETH to ETH conversion |
| BRIDGE | Cross-chain bridge |
| QUICKROUTE | Fast 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_V2 | UniswapX荷兰式拍卖V2版本 |
| DUTCH_V3 | UniswapX荷兰式拍卖V3版本 |
| PRIORITY | 受MEV保护的优先订单(Base、Unichain) |
| DUTCH_LIMIT | UniswapX限价荷兰式拍卖 |
| LIMIT_ORDER | 限价订单 |
| WRAP | ETH转WETH兑换 |
| UNWRAP | WETH转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 endpoint expects the quote response spread into the request body, not wrapped in a field.
/swapquotetypescript
// 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
};/swapquotetypescript
// 错误写法 - 会导致"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 . Always strip null fields before sending:
permitData: nulltypescript
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会拒绝的请求。发送前务必移除所有null字段:
permitData: nulltypescript
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 | | |
|---|---|---|
| Standard swap (no Permit2) | Omit | Omit |
| Permit2 swap | Required | Required |
| Invalid | Present | Missing |
| Invalid | Missing | Present |
| Invalid (API error) | Any | |
当使用Permit2实现无Gas授权时:
| 场景 | | |
|---|---|---|
| 标准兑换(不使用Permit2) | 省略 | 省略 |
| 使用Permit2的兑换 | 必填 | 必填 |
| 无效场景 | 存在 | 缺失 |
| 无效场景 | 缺失 | 存在 |
| 无效场景(API错误) | 任意值 | |
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 bufferAdd 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.tstypescript
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.tstypescript
export default defineConfig({
define: {
global: 'globalThis',
},
optimizeDeps: {
include: ['buffer'],
},
resolve: {
alias: {
buffer: 'buffer',
},
},
});如果不进行此配置,你会看到错误:
ReferenceError: Buffer is not definedCORS Proxy Configuration
CORS代理配置
The Trading API does not support browser CORS preflight requests — requests return . Direct calls from a browser will always fail. You must proxy API requests through your own server or dev server.
OPTIONS415 Unsupported Media Typefetch()Vite dev proxy (merge into the same used for the Buffer polyfill above):
vite.config.tstypescript
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 instead of the full URL in your frontend code.
/api/uniswap/quoteVercel production proxy ():
vercel.jsonjson
{
"rewrites": [
{
"source": "/api/uniswap/:path*",
"destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
}
]
}Cloudflare Pages ():
public/_redirectstext
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200Next.js ():
next.config.jsjavascript
module.exports = {
async rewrites() {
return [
{
source: '/api/uniswap/:path*',
destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
},
];
},
};Without a proxy, you'll see: on preflight or CORS errors in the browser console.
415 Unsupported Media TypeTrading API不支持浏览器CORS预检请求——请求会返回。直接在浏览器中调用会始终失败。你必须通过自有服务器或开发服务器代理API请求。
OPTIONS415 Unsupported Media Typefetch()Vite开发环境代理(与上述Buffer polyfill配置合并到同一个中):
vite.config.tstypescript
export default defineConfig({
server: {
proxy: {
'/api/uniswap': {
target: 'https://trade-api.gateway.uniswap.org/v1',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/uniswap/, ''),
},
},
},
});然后在前端代码中使用替代完整URL。
/api/uniswap/quoteVercel生产环境代理 ():
vercel.jsonjson
{
"rewrites": [
{
"source": "/api/uniswap/:path*",
"destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
}
]
}Cloudflare Pages配置 ():
public/_redirectstext
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200Next.js配置 ():
next.config.jsjavascript
module.exports = {
async rewrites() {
return [
{
source: '/api/uniswap/:path*',
destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
},
];
},
};如果没有代理,你会在浏览器控制台看到:(预检请求)或CORS错误。
415 Unsupported Media Type6. Quote Freshness
6. 报价时效性
- Quotes expire quickly (typically 30 seconds)
- Always re-fetch if the user takes time to review
- Use the parameter to prevent stale execution
deadline - If returns empty
/swap, the quote likely expireddata
- 报价过期速度快(通常为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:
| Bits | Name | Purpose |
|---|---|---|
| 0 | flag | Allow revert (1 = continue on fail) |
| 1-2 | reserved | Use 0 |
| 3-7 | command | Operation identifier |
每个命令为单个字节:
| 位段 | 名称 | 用途 |
|---|---|---|
| 0 | flag | 是否允许回滚(1表示失败后继续执行) |
| 1-2 | reserved | 保留位,使用0 |
| 3-7 | command | 操作标识符 |
Swap Commands
兑换命令
| Code | Command | Description |
|---|---|---|
| 0x00 | V3_SWAP_EXACT_IN | v3 swap with exact input |
| 0x01 | V3_SWAP_EXACT_OUT | v3 swap with exact output |
| 0x08 | V2_SWAP_EXACT_IN | v2 swap with exact input |
| 0x09 | V2_SWAP_EXACT_OUT | v2 swap with exact output |
| 0x10 | V4_SWAP | v4 swap |
| 代码 | 命令名称 | 描述 |
|---|---|---|
| 0x00 | V3_SWAP_EXACT_IN | v3固定输入金额兑换 |
| 0x01 | V3_SWAP_EXACT_OUT | v3固定输出金额兑换 |
| 0x08 | V2_SWAP_EXACT_IN | v2固定输入金额兑换 |
| 0x09 | V2_SWAP_EXACT_OUT | v2固定输出金额兑换 |
| 0x10 | V4_SWAP | v4兑换 |
Token Operations
代币操作命令
| Code | Command | Description |
|---|---|---|
| 0x04 | SWEEP | Clear router token balance |
| 0x05 | TRANSFER | Send specific amount |
| 0x0b | WRAP_ETH | ETH to WETH |
| 0x0c | UNWRAP_WETH | WETH to ETH |
| 代码 | 命令名称 | 描述 |
|---|---|---|
| 0x04 | SWEEP | 清空路由中的代币余额 |
| 0x05 | TRANSFER | 发送指定金额的代币 |
| 0x0b | WRAP_ETH | ETH转WETH |
| 0x0c | UNWRAP_WETH | WETH转ETH |
Permit2 Commands
Permit2命令
| Code | Command | Description |
|---|---|---|
| 0x02 | PERMIT2_TRANSFER_FROM | Single token transfer |
| 0x03 | PERMIT2_PERMIT_BATCH | Batch approval |
| 0x0a | PERMIT2_PERMIT | Single approval |
| 代码 | 命令名称 | 描述 |
|---|---|---|
| 0x02 | PERMIT2_TRANSFER_FROM | 单一代币转账 |
| 0x03 | PERMIT2_PERMIT_BATCH | 批量授权 |
| 0x0a | PERMIT2_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:
| Approach | Approve To | Per-Swap Auth | Best For |
|---|---|---|---|
| Permit2 (recommended) | Permit2 contract | EIP-712 signature | Frontends with user interaction |
| Legacy (direct approve) | Universal Router | None (pre-approved) | Backend services, smart accounts |
Permit2 flow (frontend with user signing):
- User approves token to Permit2 contract (one-time)
- Each swap: user signs an EIP-712 permit message
- Universal Router uses the signature to transfer tokens via Permit2
Legacy flow (backend services, ERC-4337 smart accounts):
- Approve token directly to the Universal Router address (one-time)
- Each swap: no additional authorization needed
- Simpler for automated systems that cannot sign EIP-712 messages
Use the Trading API's endpoint — it returns the correct approval target based on the routing type.
/check_approval有两种授权路径,根据你的集成类型选择:
| 方式 | 授权目标地址 | 每次兑换的认证方式 | 最佳适用场景 |
|---|---|---|---|
| Permit2(推荐) | Permit2合约 | EIP-712签名 | 有用户交互的前端应用 |
| 传统方式(直接授权) | Universal Router地址 | 无(预先授权完成) | 后端服务、智能账户 |
Permit2流程(有用户签名的前端应用):
- 用户一次性授权代币给Permit2合约
- 每次兑换:用户签署EIP-712许可消息
- Universal Router使用签名通过Permit2转移代币
传统流程(后端服务、ERC-4337智能账户):
- 直接将代币授权给Universal Router地址(一次性操作)
- 每次兑换:无需额外授权
- 更适合无法签署EIP-712消息的自动化系统
使用Trading API的接口——它会根据路由类型返回正确的授权目标。
/check_approvalHow It Works
工作原理
- User approves Permit2 contract once (infinite approval)
- For each swap, user signs a message authorizing the transfer
- Universal Router uses signature to transfer tokens via Permit2
- 用户一次性授权Permit2合约(无限授权)
- 每次兑换:用户签署授权转移的消息
- Universal Router使用签名通过Permit2转移代币
Two Modes
两种模式
| Mode | Description |
|---|---|
| SignatureTransfer | One-time signature, no on-chain state |
| AllowanceTransfer | Time-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: or
DUTCH_V2DUTCH_V3- 先进行RFQ(报价请求)阶段,由授权报价方竞争
- 获胜的报价方获得专属填充权,持续一定时间
- 如果专属填充者未执行订单,会回退到公开荷兰式拍卖,价格随区块高度下降
- 最适合对MEV防护要求高的大额兑换
Trading API路由类型:或
DUTCH_V2DUTCH_V3Open 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_V2Priority 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路由类型:
PRIORITYKey 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 viembash
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viemHigh-Level Approach (Recommended)
高级方式(推荐)
Use + for automatic command building:
RouterTradeSwapRouter.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. 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),
});使用 + 自动构建命令:
RouterTradeSwapRouter.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 directly:
RoutePlannertypescript
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';对于自定义流程(如费用收取、复杂路由),直接使用:
RoutePlannertypescript
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
命令速查表
| Command | Parameters |
|---|---|
| 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
手续费层级
| Tier | Value | Percentage |
|---|---|---|
| LOWEST | 100 | 0.01% |
| LOW | 500 | 0.05% |
| MEDIUM | 3000 | 0.30% |
| HIGH | 10000 | 1.00% |
| 层级 | 数值 | 百分比 |
|---|---|---|
| LOWEST | 100 | 0.01% |
| LOW | 500 | 0.05% |
| MEDIUM | 3000 | 0.30% |
| HIGH | 10000 | 1.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 pitfalls, see wagmi v2 Integration Pitfalls below.
useWalletClient()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 的陷阱,查看下方wagmi v2集成陷阱。
useWalletClient()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 hook from wagmi v2 can return even when the wallet is connected — it resolves asynchronously. This causes "wallet not connected" errors at swap time. Additionally, the returned client needs a for to work.
useWalletClient()undefinedchainsendTransaction()Recommended pattern — use action functions at swap time instead of hooks:
@wagmi/coretypescript
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:
- hook returns
useWalletClient()during async resolution, even after{ data: undefined }shows connecteduseAccount() - is a promise that resolves only when the client is ready, and includes the chain
getWalletClient(config, { chainId }) - prevents "chain mismatch" errors when the wallet is on a different network than the swap
switchChain()
wagmi v2的钩子即使在钱包已连接的情况下也可能返回——它是异步解析的。这会导致兑换时出现“钱包未连接”的错误。此外,返回的客户端需要参数才能正常使用。
useWalletClient()undefinedchainsendTransaction()推荐模式 — 在兑换时使用的action函数替代钩子:
@wagmi/coretypescript
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() - 是一个Promise,仅在客户端准备好后才会解析,并且包含chain信息
getWalletClient(config, { chainId }) - 可以防止钱包网络与兑换网络不匹配的错误
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:
- Get swap calldata from Trading API (standard 3-step flow)
- Wrap the calldata in a delegation redemption execution
- 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兑换,使用委托模式。流程如下:
- 从Trading API获取兑换调用数据(标准三步流程)
- 将调用数据包装在委托赎回执行中
- 通过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 is deprecated.
0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD| Chain | ID | Address |
|---|---|---|
| Ethereum | 1 | |
| Unichain | 130 | |
| Optimism | 10 | |
| Base | 8453 | |
| Arbitrum | 42161 | |
| Polygon | 137 | |
| Blast | 81457 | |
| BNB | 56 | |
| Zora | 7777777 | |
| World Chain | 480 | |
| Avalanche | 43114 | |
| Celo | 42220 | |
| Soneium | 1868 | |
| Ink | 57073 | |
| Monad | 143 | |
For testnet addresses, see Uniswap v4 Deployments.
地址按链区分。旧版v1地址已废弃。
0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD| 区块链名称 | 链ID | 地址 |
|---|---|---|
| Ethereum | 1 | |
| Unichain | 130 | |
| Optimism | 10 | |
| Base | 8453 | |
| Arbitrum | 42161 | |
| Polygon | 137 | |
| Blast | 81457 | |
| BNB | 56 | |
| Zora | 7777777 | |
| World Chain | 480 | |
| Avalanche | 43114 | |
| Celo | 42220 | |
| Soneium | 1868 | |
| Ink | 57073 | |
| Monad | 143 | |
测试网地址请查看Uniswap v4部署文档。
Permit2
Permit2
| Chain | Address |
|---|---|
| All chains | |
| 区块链名称 | 地址 |
|---|---|
| 所有链 | |
Troubleshooting
故障排除
Common Issues
常见问题
| Issue | Solution |
|---|---|
| "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 data | Validate |
| "permitData must be of type object" | Strip |
| "quote does not match any of the allowed types" | Don't wrap quote in |
| Received WETH instead of ETH on L2 | Check and unwrap WETH after swap (see WETH Handling on L2s) |
| 429 Too Many Requests | Implement exponential backoff and add delays between batch requests (see Rate Limiting) |
| 415 on OPTIONS preflight / CORS error | Set up a CORS proxy (see CORS Proxy Configuration in Browser Environment Setup) |
| walletClient is undefined when wallet is connected | Use |
| "Please provide a chain with the chain argument" | Pass |
| Chain mismatch error on swap | Call |
| 问题描述 | 解决方案 |
|---|---|
| "授权额度不足" | 先调用/check_approval并提交授权交易 |
| "报价已过期" | 延长deadline或重新获取报价 |
| "滑点超出限制" | 提高滑点容忍度或重试 |
| "流动性不足" | 尝试更小的金额或不同的路由 |
| "Buffer is not defined" | 添加Buffer polyfill(查看关键实现注意事项) |
| 链上回滚且数据为空 | 广播前验证 |
| "permitData must be of type object" | 从请求中移除 |
| "quote does not match any of the allowed types" | 不要将报价包裹在 |
| 在L2链上收到WETH而非ETH | 兑换后检查并解包WETH(查看L2链上的WETH处理) |
| 429 Too Many Requests | 实现指数退避重试,并在批量请求之间添加延迟(查看速率限制) |
| OPTIONS预检请求返回415 / CORS错误 | 设置CORS代理(查看浏览器环境设置中的CORS代理配置) |
| 钱包已连接但walletClient为undefined | 使用 |
| "Please provide a chain with the chain argument" | 向 |
| 兑换时出现链不匹配错误 | 在 |
API Validation Errors (400)
API验证错误(400)
| Error Message | Cause | Fix |
|---|---|---|
| Sending | Omit the field entirely when null |
| Wrapping quote in | Spread quote response: |
| Including only one Permit2 field | Include both or neither |
| 错误信息 | 原因 | 修复方案 |
|---|---|---|
| 发送了 | 当为null时完全省略该字段 |
| 将报价包裹在 | 展开报价响应: |
| 仅包含了一个Permit2字段 | 同时包含两个字段或都不包含 |
API Error Codes
API错误码
| Code | Meaning |
|---|---|
| 400 | Invalid request parameters (see validation errors above) |
| 401 | Invalid or missing API key |
| 404 | No route found for pair |
| 429 | Rate limit exceeded |
| 500 | API error - implement exponential backoff retry |
| 错误码 | 含义 |
|---|---|
| 400 | 请求参数无效(查看上方验证错误) |
| 401 | API密钥无效或缺失 |
| 404 | 未找到交易对的路由 |
| 429 | 超出速率限制 |
| 500 | API内部错误 - 实现指数退避重试 |
Pre-Broadcast Checklist
广播前检查清单
Before sending a swap transaction to the blockchain:
- Verify is non-empty hex (not
swap.data, not'')'0x' - Verify addresses - and
swap.toare validswap.from - Check quote freshness - Re-fetch if older than 30 seconds
- Validate gas - Apply 10-20% buffer to estimates
- Confirm balance - User has sufficient token balance
在将兑换交易发送到区块链前:
- 验证为非空十六进制字符串(不是
swap.data,不是'')'0x' - 验证地址 - 和
swap.to为有效地址swap.from - 检查报价时效性 - 如果超过30秒,重新获取报价
- 验证Gas - 为估算值添加10-20%的缓冲
- 确认余额 - 用户有足够的代币余额