swap-integration について
フロントエンド、バックエンド、スクリプト、スマートコントラクトに Uniswap のスワップを組み込むためのスキルです。Trading API、Universal Router SDK、ルーターの直接呼び出しに加え、ルーティングや入力検証、取引承認も扱います。
主な機能
- 01Trading API のスワップ手順
- 02Universal Router SDK の利用ガイド
- 03スマートコントラクトからの直接呼び出し
- 04ルーティングと見積もりの処理
- 05入力検証と取引承認
活用シーン
- 01React または Next.js の画面にスワップを追加
- 02バックエンドのスワップスクリプトやボットを構築
- 03取引生成を直接制御
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uniswap/ uniswap-ai --skill swap-integrationインストール後、Claude Code で/swap-integrationを入力して読み込まれたか確認します。
同梱ファイルreferences/advanced-patterns.md
詳細
スワップの統合
Uniswap のスワップをフロントエンド、バックエンド、スマートコントラクトに統合します。
前提条件
このスキルでは、viem の基本(クライアントのセットアップ、アカウント管理、コントラクトとのやり取り、トランザクションの署名)を理解していることを前提としています。viem/wagmi の包括的なガイダンスを利用するには、uniswap-viem プラグインをインストールしてください:claude plugin add @uniswap/uniswap-viem
クイック選択ガイド
| 開発するもの | 使用する方法 |
|---|---|
| React/Next.js を使ったフロントエンド | Trading API |
| バックエンドのスクリプトまたはボット | Trading API |
| スマートコントラクトとの統合 | Universal Router の直接呼び出し |
| ルーティングを完全に制御する必要がある場合 | Universal Router SDK |
ルーティングタイプのクイックリファレンス
| タイプ | 説明 | チェーン |
|---|---|---|
| CLASSIC | Uniswap プールを経由する標準的な AMM スワップ | サポート対象の全チェーン |
| DUTCH_V2 | UniswapX Dutch auction V2 | Ethereum、Arbitrum、Base、Unichain |
| PRIORITY | MEV 保護された優先注文 | Base、Unichain |
| WRAP | ETH から WETH への変換 | すべて |
| UNWRAP | WETH から ETH への変換 | すべて |
DUTCH_V3、DUTCH_LIMIT、LIMIT_ORDER、BRIDGE、QUICKROUTE を含む完全な一覧については、ルーティングタイプを参照してください。
統合方法
1. Trading API(推奨)
最適な用途:フロントエンド、バックエンド、スクリプト。ルーティングの最適化を自動的に処理します。
ベース URL:https://trade-api.gateway.uniswap.org/v1
認証:x-api-key: <your-api-key> ヘッダーが必要です
API キーの取得:Trading API の認証には API キーが必要です。Uniswap Developer Portal にアクセスして登録し、API キーを取得してください。通常、登録後すぐにキーを使用できます。すべての API リクエストに x-api-key ヘッダーとして含めてください。
エージェントのアトリビューション:すべての Trading API リクエストで X-Agent-Info ヘッダー(以下の例では x-agent-info と記載)も送信してください。このヘッダーは分析のためにトラフィックをこの連携に関連付けます。リクエストに影響することはありません。値は次の3つのフィールドのみを含む1 KB未満の簡潔な JSON オブジェクトです。
integration_name:常に"swap-integration"decision_origin:**必須の選択項目です。デフォルト値をそのまま使用しないでください。**連携に合致する値を1つ選んでください(有効な値は次のものだけです。それ以外は不正な形式として扱われます)。"human_mediated"— アクションの実行前に、人間が毎回確認または承認します"autonomous"— アクションごとの人間による承認なしでエージェントが動作します(スケジュールされたジョブやエージェント型のハーネスなど)
version:"1.6.0"— このスキルのmetadata.versionを示します
値の形式が不正な場合:リクエストは通常どおり成功しますが、アトリビューションは破棄されます。ゲートウェイは分析側で不正な形式として記録し、そのまま処理を続行します。確認できるレスポンスヘッダーなどのシグナルはないため、実行時に検出する方法はありません。正しい値をあらかじめ設定してください。
制約 — 以下のいずれかに違反すると、値全体が破棄されます。
- ヘッダー全体は1 KBまで、各フィールドは256文字までです。
- ASCII 文字のみ使用できます。アクセント付き文字、曲線引用符、絵文字は使用できません。ASCII 以外の文字は
\uエスケープする必要があります。 nullを送信するのではなく、フィールドを省略してください。3つすべてを含め、有効な値を設定する必要があります。- リクエストごとにヘッダーを1回送信してください。
x-agent-infoの行が重複すると解析に失敗します。 - 未知の追加キーは無視されます。害はありませんが、必須の3つのフィールドの代わりにはなりません。
必須ヘッダー — すべての Trading API リクエストに次のヘッダーを含めてください。
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0
x-agent-info: {"integration_name":"swap-integration","decision_origin":"<human_mediated|autonomous>","version":"1.6.0"}
**権限が必要なトークンにはルーターバージョンを引き上げてください。**上記のヘッダーブロックでは 2.0 を指定しています。権限が必要なプールを通じて取引されるトークンの見積もりまたはスワップには、2.2.0 以上が必要です。該当するトークンには x-universal-router-version: 2.2.0 を送信してください。該当するトークンの確認方法はステップ 0:権限の事前確認をご覧ください。
リクエストの流れ:
0. POST /permissions -> Permissioned pools only: may this wallet trade this token?
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
詳しいドキュメントについては、以下のTrading API リファレンスをご覧ください。
2. Universal Router SDK
最適な用途:トランザクション構築を直接制御する場合。
インストール:
npm install @uniswap/universal-router-sdk @uniswap/sdk-core @uniswap/v3-sdk
基本パターン:
import { SwapRouter } from '@uniswap/universal-router-sdk';
const { calldata, value } = SwapRouter.swapCallParameters(trade, options);
詳しいドキュメントについては、以下のUniversal Router リファレンスをご覧ください。
3. スマートコントラクトの統合
最適な用途:オンチェーン統合、DeFi のコンポーザビリティ。
インターフェース:エンコードしたコマンドを指定して Universal Router の execute() を呼び出します。
コマンドのエンコード方法については、以下のUniversal Router リファレンスをご覧ください。
入力の検証ルール
ユーザーから提供された値を生成コード、API 呼び出し、またはコマンドに挿入する前に、必ず以下を確認してください。
- Ethereum アドレス:
^0x[a-fA-F0-9]{40}$に一致する必要があります。一致しない場合は拒否してください - チェーン ID:公式のサポート対象チェーン一覧に含まれている必要があります
- トークン数量:0 以上の数値で、
^[0-9]+\.?[0-9]*$に一致する必要があります - API キー:生成コードにハードコードしてはいけません。必ず環境変数を使用してください
- シェルのメタ文字(
;、|、&、$、`、(、)、>、<、\、'、"、改行)を含む入力は拒否してください
必須: ガスを消費する、またはトークンを送金するトランザクション(
sendTransaction、writeContract、署名済みスワップの送信を含む)を実行する前に、必ず AskUserQuestion を使用してユーザーに確認してください。トランザクションの概要(トークン、数量、チェーン、推定ガス代)を表示し、ユーザーから明示的な承認を得てください。ユーザーの確認なしにトランザクションを自動実行してはいけません。
Trading API リファレンス
ステップ 0:権限の事前確認(許可制プールのみ)
一部のトークンは、許可制プールを通じてのみ取引できます。許可リストコントラクトが、そのトークンを保有および取引できるウォレットを決定します。スワップエンドポイントは、リストに含まれていないウォレットを拒否します。見積もりの前にこのエンドポイントを呼び出すことで、取引できないウォレットをスワップのリバート時ではなく、ここで検出できます。
POST /permissions
リクエスト:
{
"walletAddress": "0x...",
"tokens": ["0x..."],
"chainId": 1
}
tokens には最大 2 つのアドレスを指定できます。ここでの chainId は数値ですが、/quote では tokenInChainId と tokenOutChainId に文字列を指定します。
レスポンス:
{
"requestId": "e63f1e1e-b9e9-411a-bcc8-ff18ce4e77cf",
"results": [
{
"token": "0x...",
"isPermissioned": true,
"isAllowlisted": false,
"adapterTokenAddress": "0x...",
"kycUrl": "https://your-kyc-provider.example/verify",
"issuer": "Your Issuer Name"
}
]
}
トークンが許可制の場合、adapterTokenAddress が含まれます。ウォレットが許可リストに含まれていない場合は kycUrl が含まれ、ユーザーが通過する必要のある本人確認(KYC)にリンクします。issuer は、本人確認プロバイダーの表示名です。
3 つの結果:
| 結果 | 対応 |
|---|---|
isPermissioned: false | 特別な対応は不要です。通常どおり見積もりとスワップを行います |
isPermissioned: true、isAllowlisted: true | 通常どおり見積もりとスワップを行います |
isPermissioned: true、isAllowlisted: false | ユーザーが価格を確認できるよう見積もりを行い、その後送信をブロックして kycUrl の案内を表示します |
このエンドポイントでは、他のすべての Trading API 呼び出しと同様に x-api-key が必要です。 このエンドポイントの公開されているサンプルでは、ヘッダーが省略されています。ヘッダーがない場合、エンドポイントは 401 と {"errorCode":"Unauthorized","detail":"Unauthenticated api key or session"} を返すため、そのサンプルをコピーしたインテグレーションは権限チェックに到達する前に失敗します。
許可制トークンを含む見積もりとスワップには、Universal Router 2.2.0 以上が必要です。これらのトークンに対する /quote および /swap 呼び出しでは、x-universal-router-version: 2.2.0 を送信してください。通常の UniversalRouter デプロイメントは、別の許可制ではないルーターです。コンストラクターでパーミッションアダプターファクトリーを受け取るのは、#v2.2 デプロイメントのみです。
許可制トークンの発行は別の作業です。uniswap-permissioned-pools プラグインの permissioned-pools-issuer スキルを参照してください。
ステップ 1:トークンの承認を確認する
POST /check_approval
リクエスト:
{
"walletAddress": "0x...",
"token": "0x...",
"amount": "1000000000",
"chainId": 1
}
レスポンス:
{
"approval": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1
}
}
approval が null の場合、トークンはすでに承認されています。
ステップ 2:見積もりの取得
POST /quote
リクエスト:
{
"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 のオークションのタイミングに影響します |
レスポンス — ルーティングの種類によって形式が異なります。Ethereum メインネットでの BEST_PRICE ルーティングでは、通常 CLASSIC ではなく UniswapX(DUTCH_V2)が返されます。
CLASSIC のレスポンス:
{
"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": null
}
UniswapX(DUTCH_V2/V3/PRIORITY)のレスポンス — quote の形式が異なり、quote.output はありません:
{
"routing": "DUTCH_V2",
"quote": {
"orderInfo": {
"reactor": "0x...",
"swapper": "0x...",
"nonce": "...",
"deadline": 1772031054,
"cosigner": "0x...",
"input": {
"token": "0x...",
"startAmount": "1000000000000000000",
"endAmount": "1000000000000000000"
},
"outputs": [
{
"token": "0x...",
"startAmount": "999000000",
"endAmount": "994000000",
"recipient": "0x..."
}
],
"chainId": 1
},
"encodedOrder": "0x...",
"orderHash": "0x..."
},
"permitData": { "domain": {}, "types": {}, "values": {} }
}
UniswapX の出力量:最良ケースの約定量には
quote.orderInfo.outputs[0].startAmountを使用してください。endAmountは、オークションが最後まで進行した後の下限額です。UniswapX のレスポンスにはquote.output.amountはありません。これにアクセスすると、実行時エラーが発生します。表示のヒント:CLASSIC のルートでは、ガス代の表示に
gasFeeUSD(USD 額を示す文字列)を使用してください。gasFee(wei)をハードコードした ETH 価格で手動換算しないでください。見積もりが大幅に不正確になります(例:約 0.01 ドルではなく約 87 ドル)。UniswapX のルートでは、スワップ実行者にガス代がかかりません。
ルーティングの種類をまたいだコンパイル時の型安全性については、QuoteResponse の TypeScript 型を参照してください。
ステップ 3:スワップの実行
POST /swap
リクエスト - 見積もりレスポンスをそのままリクエスト本文に展開します:
// CORRECT: Spread the quote response, strip null fields
const quoteResponse = await fetchQuote(params);
// Always strip permitData/permitTransaction — handle them explicitly by routing type
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
const swapRequest: Record<string, unknown> = { ...cleanQuote };
const isUniswapX =
quoteResponse.routing === 'DUTCH_V2' ||
quoteResponse.routing === 'DUTCH_V3' ||
quoteResponse.routing === 'PRIORITY';
if (isUniswapX) {
// UniswapX: signature only — permitData must NOT go to /swap
if (permit2Signature) swapRequest.signature = permit2Signature;
} else {
// CLASSIC: both signature and permitData, or neither
if (permit2Signature && permitData && typeof permitData === 'object') {
swapRequest.signature = permit2Signature;
swapRequest.permitData = permitData;
}
}
重要:見積もりを {quote: quoteResponse} でラップしないでください。API は、見積もりレスポンスのフィールドをリクエスト本文に展開した形式を想定しています。
Permit2 のルール(CLASSIC ルート):
signatureとpermitDataは、両方とも含めるか、両方とも含めない必要があります。permitData: nullは絶対に設定せず、フィールド自体を省略してください。- 見積もりレスポンスには
permitData: nullが含まれていることがよくあります。送信前にこれを取り除いてください。
UniswapX のルート(DUTCH_V2/V3/PRIORITY):permitData は注文への署名にローカルで使用しますが、/swap の本文からは除外する必要があります。署名と送信のフローを参照してください。
レスポンス(署名可能なトランザクション):
{
"swap": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1,
"gasLimit": "250000"
}
}
レスポンスの検証 - ブロードキャストする前に必ず検証してください:
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');
}
}
対応チェーン
現在対応しているチェーンとその ID については、公式の対応チェーン一覧を参照してください。
ルーティングの種類
| 種類 | 説明 |
|---|---|
| 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 オークションの種類を参照してください。
重要な実装上の注意事項
実際の Trading API 統合で判明した、よくある落とし穴です。オンチェーンでのリバートや API エラーを避けるため、以下のルールに従ってください。
1. Swap リクエストボディの形式
/swap エンドポイントでは、見積もりレスポンスを quote フィールドでラップせず、リクエストボディに展開して含める必要があります。
// 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
};
2. null フィールドの扱い
API は permitData: null を拒否します。さらに、permitData の扱いはルーティングタイプによって異なります。詳しくは署名と送信のフローを参照してください。
function prepareSwapRequest(quoteResponse: QuoteResponse, signature?: string): object {
// Always strip permitData and permitTransaction from the spread — handle them explicitly
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
const request: Record<string, unknown> = { ...cleanQuote };
// UniswapX (DUTCH_V2, DUTCH_V3, PRIORITY): permitData is for LOCAL signing only.
// The /swap body must NOT include permitData — the order is encoded in
// quote.encodedOrder. Only the signature is needed.
const isUniswapX =
quoteResponse.routing === 'DUTCH_V2' ||
quoteResponse.routing === 'DUTCH_V3' ||
quoteResponse.routing === 'PRIORITY';
if (isUniswapX) {
if (signature) request.signature = signature;
} else {
// CLASSIC: both signature and permitData required together, or both omitted.
// The Universal Router contract needs permitData to verify the Permit2
// authorization on-chain.
if (signature && permitData && typeof permitData === 'object') {
request.signature = signature;
request.permitData = permitData;
}
}
return request;
}
3. Permit2 フィールドのルール
/swap リクエストボディの signature と permitData のルールは、ルーティングタイプによって異なります。
CLASSIC ルート:
| シナリオ | signature | permitData |
|---|---|---|
| 標準スワップ(Permit2 なし) | 省略 | 省略 |
| Permit2 スワップ | 必須 | 必須 |
| 無効 | あり | なし |
| 無効 | なし | あり |
| 無効(API エラー) | 任意 | null |
UniswapX ルート(DUTCH_V2/V3/PRIORITY):
| シナリオ | signature | permitData |
|---|---|---|
| UniswapX 注文 | 必須 | 省略(送信しない) |
| 無効 | 任意 | あり(スキーマで拒否されます) |
4. ブロードキャスト前の検証
ブロックチェーンに送信する前に、必ずスワップレスポンスを検証してください。
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');
}
}
5. ブラウザー環境のセットアップ
ブラウザー環境で viem/wagmi を使用する場合は、Node.js のポリフィルが必要です。
buffer ポリフィルをインストールします:
npm install buffer
エントリーファイルに追加します(他のインポートより前):
// 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 の設定(vite.config.ts):
export default defineConfig({
define: {
global: 'globalThis',
},
optimizeDeps: {
include: ['buffer'],
},
resolve: {
alias: {
buffer: 'buffer',
},
},
});
この設定がない場合、ReferenceError: Buffer is not defined が表示されます。
CORS プロキシの設定
Trading API はブラウザーの CORS プリフライトリクエストに対応していません。OPTIONS リクエストは 415 Unsupported Media Type を返します。ブラウザーからの直接の fetch() 呼び出しは常に失敗します。API リクエストは必ず独自のサーバーまたは開発サーバー経由でプロキシしてください。
Vite 開発プロキシ(上記の Buffer ポリフィルに使用したものと同じ vite.config.ts に統合します):
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/quote を使用してください。
Vercel 本番プロキシ(vercel.json):
{
"rewrites": [
{
"source": "/api/uniswap/:path*",
"destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
}
]
}
Cloudflare Pages(public/_redirects):
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200
Next.js(next.config.js):
module.exports = {
async rewrites() {
return [
{
source: '/api/uniswap/:path*',
destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
},
];
},
};
プロキシがない場合、プリフライト時に 415 Unsupported Media Type が表示されるか、ブラウザーのコンソールに CORS エラーが表示されます。
6. 見積もりの有効性
- 見積もりはすぐに期限切れになります(通常 30 秒)。
- ユーザーが確認に時間をかけた場合は、必ず再取得してください。
- 古い見積もりでの実行を防ぐため、
deadlineパラメーターを使用してください。 /swapが空のdataを返した場合、見積もりの期限が切れている可能性があります。
7. QuoteResponse の TypeScript 型
見積もりレスポンスの形状はルーティングタイプによって異なります。any へのキャストではなく、routing フィールドによる判別可能なユニオンを使用して、コンパイル時の安全性を確保してください。
type ClassicQuoteResponse = {
routing: 'CLASSIC' | 'WRAP' | 'UNWRAP';
quote: {
input: { token: string; amount: string };
output: { token: string; amount: string };
slippage: number;
route: unknown[];
gasFee: string;
gasFeeUSD: string;
gasUseEstimate: string;
};
permitData: Record<string, unknown> | null;
};
type DutchOrderOutput = {
token: string;
startAmount: string;
endAmount: string;
recipient: string;
};
type UniswapXQuoteResponse = {
routing: 'DUTCH_V2' | 'DUTCH_V3' | 'PRIORITY';
quote: {
orderInfo: {
outputs: DutchOrderOutput[];
input: { token: string; startAmount: string; endAmount: string };
deadline: number;
nonce: string;
};
encodedOrder: string;
orderHash: string;
};
// EIP-712 typed data — sign locally, do NOT send to /swap
permitData: Record<string, unknown> | null;
};
type QuoteResponse = ClassicQuoteResponse | UniswapXQuoteResponse;
// Type guard for routing-aware logic
function isUniswapXQuote(q: QuoteResponse): q is UniswapXQuoteResponse {
return q.routing === 'DUTCH_V2' || q.routing === 'DUTCH_V3' || q.routing === 'PRIORITY';
}
// Reading the output amount by routing type
function getOutputAmount(q: QuoteResponse): string {
if (isUniswapXQuote(q)) {
const firstOutput = q.quote.orderInfo.outputs[0];
if (!firstOutput) throw new Error('UniswapX quote has no outputs');
// startAmount = best-case fill; endAmount = floor after auction decay
return firstOutput.startAmount;
}
return q.quote.output.amount;
}
Universal Router リファレンス
Universal Router は、Uniswap v2、v3、v4 にまたがるスワップのための統一インターフェイスです。
コア関数
function execute(
bytes calldata commands,
bytes[] calldata inputs,
uint256 deadline
) external payable;
コマンドのエンコード
各コマンドは 1 バイトです。
| ビット | 名前 | 目的 |
|---|---|---|
| 0 | フラグ | リバートを許可(1 = 失敗しても続行) |
| 1-2 | 予約済み | 0 を使用 |
| 3-7 | コマンド | 操作識別子 |
スワップコマンド
| コード | コマンド | 説明 |
|---|---|---|
| 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 スワップ |
トークン操作
| コード | コマンド | 説明 |
|---|---|---|
| 0x04 | SWEEP | ルーターのトークン残高をクリアします |
| 0x05 | TRANSFER | 指定した数量を送信します |
| 0x0b | WRAP_ETH | ETH から WETH へ |
| 0x0c | UNWRAP_WETH | WETH から ETH へ |
Permit2 コマンド
| コード | コマンド | 説明 |
|---|---|---|
| 0x02 | PERMIT2_TRANSFER_FROM | 単一トークンの送金 |
| 0x03 | PERMIT2_PERMIT_BATCH | 一括承認 |
| 0x0a | PERMIT2_PERMIT | 単一承認 |
SDK の使用方法
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
})
Permit2 の統合
Permit2 を使うと、オンチェーンの approve() 呼び出しの代わりに、署名ベースでトークンを承認できます。
承認先:Permit2 と従来方式(ルーターへ直接)
承認方法は 2 種類あります。統合の種類に応じて選択してください。
| 方法 | 承認先 | スワップごとの認証 | 最適な用途 |
|---|---|---|---|
| Permit2(推奨) | Permit2 コントラクト | EIP-712 署名 | ユーザーが操作するフロントエンド |
| Legacy(直接承認) | Universal Router | なし(事前承認済み) | バックエンドサービス、スマートアカウント |
Permit2 のフロー(ユーザーが署名するフロントエンド):
- ユーザーが Permit2 コントラクトへのトークンの使用を承認します(初回のみ)
- スワップごとに、ユーザーが EIP-712 の許可メッセージに署名します
- Universal Router は署名を使い、Permit2 経由でトークンを送金します
従来方式のフロー(バックエンドサービス、ERC-4337 スマートアカウント):
- Universal Router のアドレスに対してトークンを直接承認します(初回のみ)
- スワップごとの追加認証は不要です
- EIP-712 メッセージに署名できない自動化システムに適しています
Trading API の /check_approval エンドポイントを使用してください。ルーティングタイプに応じて、適切な承認先が返されます。
仕組み
- ユーザーは Permit2 コントラクトを一度だけ承認します(無制限の承認)
- スワップごとに、ユーザーは送金を許可するメッセージに署名します
- Universal Router は署名を使い、Permit2 経由でトークンを送金します
2 つのモード
| モード | 説明 |
|---|---|
| SignatureTransfer | 一度限りの署名で、オンチェーンの状態は不要です |
| AllowanceTransfer | オンチェーンの状態を伴う期間限定の使用許可 |
統合パターン
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(...);
UniswapX のオークションタイプ
UniswapX は、オンチェーン AMM よりも有利な価格で注文を執行しようと競争する、オフチェーンのフィラーを通じてスワップをルーティングします。オークションの仕組みはチェーンによって異なります。
排他的ダッチオークション(Ethereum)
- RFQ(見積もり依頼)フェーズから始まり、許可された見積もり提供者が競争します
- 落札した見積もり提供者は、一定期間、独占的な約定権を得ます
- 独占的なフィラーが執行しない場合、各ブロックで価格が下がるオープンダッチオークションに移行します
- MEV 保護が特に重要となる大規模なスワップに最適です
Trading API のルーティングタイプ:DUTCH_V2 または DUTCH_V3
オープンダッチオークション(Arbitrum)
- RFQ フェーズを介さず、直接オープンオークションを実施します
- フィラーは、価格が下落する仕組みを通じてオンチェーンで競争します
- Arbitrum の高速な 0.25 秒のブロック時間を活用し、迅速に価格を発見します
- Unimind アルゴリズムが、過去のペアのパフォーマンスに基づいてオークションのパラメーターを設定します
Trading API のルーティングタイプ:DUTCH_V2
優先ガスオークション(Base、Unichain)
- フィラーは、対象ブロックで優先手数料を変えたトランザクションを送信して入札します
- 最も高い優先手数料を提示したフィラーが、注文を約定する権利を得ます
- OP Stack の優先順位付けの仕組みを利用します
- ブロックビルダーが優先順位を尊重するチェーンで有効です
Trading API のルーティングタイプ:PRIORITY
主要な特性(すべてのオークションタイプ)
- ユーザーはガス代不要 — フィラーがガス代を支払い、最終的な価格に反映されます
- 失敗しても費用は発生しません — スワップが成立しなかった場合、ユーザーは何も支払いません
- MEV 保護 — オークションの仕組みにより、フロントランニングやサンドイッチ攻撃を防ぎます
- UniswapX V2 は現在、Ethereum(1)、Arbitrum(42161)、Base(8453)、Unichain(130)でサポートされています
詳しくは、UniswapX Auction Types documentationをご覧ください。
UniswapX:署名と送信のフロー
見積もりレスポンスのpermitDataフィールドは、ルーティングタイプによって用途が異なります。この2つを混同すると、/swapでRequestValidationErrorが発生します。
CLASSIC フロー — permitDataをサーバーに送信します:
/quoteがpermitData(Permit2 の許可に関する EIP-712 型付きデータ)を返します- ユーザーがローカルで
permitDataに署名し、signatureを生成します /swapの本文にはsignatureとpermitDataの両方を含めます — Universal Router コントラクトが、オンチェーンで Permit2 の許可を再構築して検証するためにpermitDataを必要とします
UniswapX フロー(DUTCH_V2/V3/PRIORITY) — permitDataはローカルに保持します:
/quoteがpermitData(Dutch 注文に関する EIP-712 型付きデータ)を返します- ユーザーがローカルで
permitDataに署名し、signatureを生成します /swapの本文にはsignatureのみを含めます — 注文はすでにquote.encodedOrderに完全にエンコードされており、オフチェーンのフィラーシステムが直接読み取ります。permitDataを/swapに送信すると、スキーマ検証エラーが発生します。
| ルートタイプ | permitDataで署名しますか? | /swapにpermitDataを送信しますか? | /swapにsignatureを送信しますか? |
|---|---|---|---|
| CLASSIC | はい | はい(ルーターに必要) | はい(Permit2 を使用する場合) |
| DUTCH_V2/V3/PRIORITY | はい | いいえ(スキーマで拒否されます) | はい |
よくある間違い:API エラー
"quote" does not match any of the allowed typesは、多くの場合quoteフィールドを示していますが、実際の原因は UniswapX ルートでpermitDataが含まれていることです。送信前にpermitDataを削除してください — Null Field Handlingにあるルーティング対応のprepareSwapRequestをご覧ください。
Universal Router の直接統合(SDK)
Trading API を使わずに Universal Router を直接統合する場合は、SDK の高レベル API を使用します。
インストール
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viem
高レベルの方法(推奨)
RouterTradeとSwapRouter.swapCallParameters()を使用して、コマンドを自動的に構築します:
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),
});
低レベルの方法(手動コマンド)
カスタムフロー(手数料の徴収、複雑なルーティング)には、RoutePlannerを直接使用します:
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';
例:手動コマンドを使った V3 スワップ
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);
}
例:ETH からトークンへの交換(ラップ+スワップ)
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 });
}
例:トークンから ETH への交換(スワップ+アンラップ)
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);
}
例:PAY_PORTION を使った手数料の徴収
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);
}
ルート実行ヘルパー
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);
}
コマンド早見表
| コマンド | パラメーター |
|---|---|
| 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) |
手数料ティア
| ティア | 値 | 割合 |
|---|---|---|
| LOWEST | 100 | 0.01% |
| LOW | 500 | 0.05% |
| MEDIUM | 3000 | 0.30% |
| HIGH | 10000 | 1.00% |
一般的な統合パターン
フロントエンドのスワップフック(React)
注:Buffer ポリフィルと CORS プロキシがセットアップされていることを確認してください(「実装に関する重要な注意事項」を参照)。wagmi v2 の useWalletClient() に関する落とし穴については、以下のwagmi v2 統合に関する落とし穴を参照してください。
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';
// REQUIRED: define decision_origin yourself — 'human_mediated' (a human reviews/approves
// each action before it executes) or 'autonomous' (no per-action human approval).
// See Agent Attribution above. There is no default — you must choose.
declare const DECISION_ORIGIN: 'human_mediated' | 'autonomous';
const AGENT_INFO = JSON.stringify({
integration_name: 'swap-integration',
decision_origin: DECISION_ORIGIN,
version: '1.6.0',
});
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',
'x-agent-info': AGENT_INFO,
},
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');
// Strip null fields and spread quote response into body
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
const swapRequest: Record<string, unknown> = { ...cleanQuote };
// CRITICAL: permitData handling differs by routing type
const isUniswapX =
quoteResponse.routing === 'DUTCH_V2' ||
quoteResponse.routing === 'DUTCH_V3' ||
quoteResponse.routing === 'PRIORITY';
if (isUniswapX) {
// UniswapX: signature only — permitData must NOT be sent to /swap
// (permitData is used locally to sign the order, not submitted to the API)
if (permit2Signature) swapRequest.signature = permit2Signature;
} else {
// CLASSIC: both signature and permitData required together, or both omitted
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',
'x-agent-info': AGENT_INFO,
},
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 };
}
wagmi v2 統合に関する落とし穴
wagmi v2 の useWalletClient() フックは、ウォレットが接続されていても undefined を返すことがあります。これは非同期で解決されるためです。その結果、スワップ時に「ウォレットが接続されていません」というエラーが発生します。また、sendTransaction() を動作させるには、返されたクライアントに chain が必要です。
推奨パターン — フックの代わりに、スワップ時に @wagmi/core のアクション関数を使用します。
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 });
}
これが重要な理由:
useWalletClient()フックは、useAccount()で接続済みと表示されていても、非同期で解決される間は{ data: undefined }を返しますgetWalletClient(config, { chainId })は、クライアントの準備が整ったときにのみ解決されるプロミスで、チェーンも含まれますswitchChain()を使うと、ウォレットがスワップ先とは異なるネットワークに接続されている場合の「チェーン不一致」エラーを防げます
バックエンドのスワップスクリプト(Node.js)
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!;
// REQUIRED: define decision_origin yourself — 'human_mediated' (a human reviews/approves
// each action before it executes) or 'autonomous' (no per-action human approval).
// See Agent Attribution above. There is no default — you must choose.
declare const DECISION_ORIGIN: 'human_mediated' | 'autonomous';
const AGENT_INFO = JSON.stringify({
integration_name: 'swap-integration',
decision_origin: DECISION_ORIGIN,
version: '1.6.0',
});
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 prepare /swap request body — routing-aware permitData handling
function prepareSwapRequest(quoteResponse: Record<string, unknown>, signature?: string): object {
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
const request: Record<string, unknown> = { ...cleanQuote };
// UniswapX (DUTCH_V2, DUTCH_V3, PRIORITY): permitData is for LOCAL signing only.
// The /swap body must NOT include permitData — the order is already encoded
// in quote.encodedOrder. Only the signature is needed.
const isUniswapX =
quoteResponse.routing === 'DUTCH_V2' ||
quoteResponse.routing === 'DUTCH_V3' ||
quoteResponse.routing === 'PRIORITY';
if (isUniswapX) {
if (signature) request.signature = signature;
} else {
// CLASSIC: both signature and permitData required together, or both omitted
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',
'x-agent-info': AGENT_INFO,
},
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',
'x-agent-info': AGENT_INFO,
},
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',
'x-agent-info': AGENT_INFO,
},
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 });
}
スマートコントラクトの統合(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);
}
}
高度なパターン
スマートアカウントの統合(ERC-4337)
委任機能を使用して、ERC-4337 スマートアカウント経由で Trading API のスワップを実行します。パターンは次のとおりです。
- Trading API からスワップの calldata を取得します(標準的な 3 ステップのフロー)
- calldata を委任の引き換え実行でラップします
bundlerClient.sendUserOperation()経由で送信します — 必ず viem バンドラークライアントのsendUserOperationメソッドを使用してください(カスタムのラッパー関数は使用しないでください)
必須: 最終送信ステップでは、viem の permissionless/bundler クライアントから提供される
bundlerClient.sendUserOperation()を必ず使用してください。これを迂回するカスタムのsubmitUserOperationラッパーを作成しないでください。バンドラークライアントは UserOperation の構築、ガス見積もり、 送信を自動的に処理します。
// 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 — use sendUserOperation directly
const userOpHash = await bundlerClient.sendUserOperation({
account: delegateSmartAccount,
calls: [
{
to: delegationManagerAddress,
data: encodeFunctionData({
abi: delegationManagerAbi,
functionName: 'redeemDelegations',
args: [[[signedDelegation]], [0], [[execution]]],
}),
value: execution.value,
},
],
});
主な考慮事項:
- スマートアカウントでは Permit2 ではなく、レガシー承認(Universal Router への直接承認)を使用してください。承認先を参照してください
- バンドラーによるガス見積もりには、20~30% のガスバッファーを追加してください
- バンドラー固有のエラーコードは、標準的なトランザクションエラーとは別に処理してください
型とエラー処理を含む完全な実装については、高度なパターンのリファレンスを参照してください。
L2 での WETH の処理
L2 チェーン(Base、Optimism、Arbitrum)では、ETH を出力するスワップで、ネイティブ ETH の代わりに WETH が送られる場合があります。スワップ後は必ず確認してアンラップしてください。
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 });
}
}
チェーンごとの WETH アドレスと統合の詳細については、高度なパターンのリファレンスを参照してください。
レート制限
Trading API には、エンドポイントごとに約 10 リクエスト/秒のレート制限があります。一括操作では、次の点に注意してください。
- API 呼び出しを順次実行する際は、100~200ms の遅延を入れます
- 429 レスポンスでは、ジッターを加えた指数バックオフを実装します
- 承認結果は頻繁に変わらないため、キャッシュします
// 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');
}
一括操作のパターンと完全な再試行の実装については、高度なパターンのリファレンスを参照してください。
主要なコントラクトアドレス
Universal Router(v4)
アドレスはチェーンごとに異なります。旧 v1 アドレス 0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD は非推奨です。
| チェーン | ID | アドレス |
|---|---|---|
| Ethereum | 1 | 0x66a9893cc07d91d95644aedd05d03f95e1dba8af |
| Unichain | 130 | 0xef740bf23acae26f6492b10de645d6b98dc8eaf3 |
| Optimism | 10 | 0x851116d9223fabed8e56c0e6b8ad0c31d98b3507 |
| Base | 8453 | 0x6ff5693b99212da76ad316178a184ab56d299b43 |
| Arbitrum | 42161 | 0xa51afafe0263b40edaef0df8781ea9aa03e381a3 |
| Polygon | 137 | 0x1095692a6237d83c6a72f3f5efedb9a670c49223 |
| Blast | 81457 | 0xeabbcb3e8e415306207ef514f660a3f820025be3 |
| BNB | 56 | 0x1906c1d672b88cd1b9ac7593301ca990f94eae07 |
| Zora | 7777777 | 0x3315ef7ca28db74abadc6c44570efdf06b04b020 |
| World Chain | 480 | 0x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743 |
| Avalanche | 43114 | 0x94b75331ae8d42c1b61065089b7d48fe14aa73b7 |
| Celo | 42220 | 0xcb695bc5d3aa22cad1e6df07801b061a05a0233a |
| Soneium | 1868 | 0x4cded7edf52c8aa5259a54ec6a3ce7c6d2a455df |
| Ink | 57073 | 0x112908dac86e20e7241b0927479ea3bf935d1fa0 |
| Monad | 143 | 0x0d97dc33264bfc1c226207428a79b26757fb9dc3 |
テストネットのアドレスについては、Uniswap v4 のデプロイ情報をご覧ください。
Permit2
| チェーン | アドレス |
|---|---|
| 全チェーン | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
トラブルシューティング
よくある問題
| 問題 | 解決策 |
|---|---|
| "Insufficient allowance" | 最初に /check_approval を呼び出し、承認トランザクションを送信します |
| "Quote expired" | deadline を延長するか、見積もりを再取得します |
| "Slippage exceeded" | slippageTolerance を増やすか、再試行します |
| "Insufficient liquidity" | 数量を減らすか、別のルートを試します |
| "Buffer is not defined" | Buffer のポリフィルを追加します(Critical Implementation Notes を参照) |
| On-chain revert with empty data | ブロードキャストする前に、swap.data が空でない 16 進数文字列であることを確認します |
| "permitData must be of type object" | リクエストから permitData: null を削除します。フィールド自体を省略してください |
| "quote does not match any of the allowed types" | quote を {quote: ...} でラップせず、リクエスト本文に展開してください。また、次の点も確認してください:UniswapX のルートでは、/swap 本文から permitData を省略する必要があります(API Validation Errors を参照) |
| Received WETH instead of ETH on L2 | スワップ後に WETH を確認してラップ解除します(WETH Handling on L2s を参照) |
| 429 Too Many Requests | 指数バックオフを実装し、バッチリクエスト間に遅延を追加します(Rate Limiting を参照) |
| 415 on OPTIONS preflight / CORS error | CORS プロキシを設定します(Browser Environment Setup の CORS Proxy Configuration を参照) |
| walletClient is undefined when wallet is connected | useWalletClient() フックの代わりに、@wagmi/core の getWalletClient() を使用します(wagmi v2 Integration Pitfalls を参照) |
| "Please provide a chain with the chain argument" | getWalletClient(config, { chainId }) に chainId を渡します |
| Chain mismatch error on swap | getWalletClient() の前に switchChain() を呼び出します(wagmi v2 Integration Pitfalls を参照) |
API の検証エラー(400)
| エラーメッセージ | 原因 | 修正 |
|---|---|---|
"permitData" must be of type object | permitData: null を送信している | null の場合はフィールドを完全に省略します |
"quote" does not match any of the allowed types | quote を {quote: quoteResponse} でラップしている | quote response を展開します:{...quoteResponse} |
"quote" does not match any of the allowed types | UniswapX(DUTCH_V2/V3/PRIORITY)の /swap ボディに permitData を含めている | UniswapX のルートでは permitData を省略します。署名と送信を参照してください |
signature and permitData must both be present | Permit2 フィールドの一方のみを含めている(CLASSIC ルートのみ) | CLASSIC では両方を含めるか、どちらも省略します。UniswapX では permitData を省略します |
API エラーコード
| コード | 意味 |
|---|---|
| 400 | リクエストパラメーターが無効です(上記の検証エラーを参照) |
| 401 | API キーが無効または指定されていません(/permissions を含む) |
| 404 | ペアのルートが見つかりません |
| 429 | レート制限を超過しました |
| 500 | API エラーです。指数バックオフによる再試行を実装してください |
ブロードキャスト前のチェックリスト
スワップトランザクションをブロックチェーンに送信する前に:
swap.dataを確認する — 空でない 16 進数であること(''や'0x'ではないこと)- アドレスを確認する —
swap.toとswap.fromが有効であること - 見積もりの鮮度を確認する — 30 秒以上経過している場合は再取得します
- ガスを検証する — 見積もりに 10~20%のバッファーを適用します
- 残高を確認する — ユーザーが十分なトークン残高を保有していること