mcpField
S
swap-integration

swap-integration

github.com/uniswap開発2362.2k 回インストール2 日前に更新
リポジトリ

swap-integration について

フロントエンド、バックエンド、スクリプト、スマートコントラクトに Uniswap のスワップを組み込むためのスキルです。Trading API、Universal Router SDK、ルーターの直接呼び出しに加え、ルーティングや入力検証、取引承認も扱います。

主な機能

  1. 01Trading API のスワップ手順
  2. 02Universal Router SDK の利用ガイド
  3. 03スマートコントラクトからの直接呼び出し
  4. 04ルーティングと見積もりの処理
  5. 05入力検証と取引承認

活用シーン

  1. 01React または Next.js の画面にスワップを追加
  2. 02バックエンドのスワップスクリプトやボットを構築
  3. 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
ルーティングタイプのクイックリファレンス
タイプ説明チェーン
CLASSICUniswap プールを経由する標準的な AMM スワップサポート対象の全チェーン
DUTCH_V2UniswapX Dutch auction V2Ethereum、Arbitrum、Base、Unichain
PRIORITYMEV 保護された優先注文Base、Unichain
WRAPETH から WETH への変換すべて
UNWRAPWETH から 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")である必要があります。

主なパラメーター:

パラメーター説明
typeEXACT_INPUT または EXACT_OUTPUT
slippageTolerance0~100 の割合
protocols任意:["V2", "V3", "V4"]
routingPreferenceBEST_PRICE、FASTEST、CLASSIC
autoSlippageスリッページを自動計算する場合は true(slippageTolerance を上書きします)
urgencynormal または 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 については、公式の対応チェーン一覧を参照してください。

ルーティングの種類
種類説明
CLASSICUniswap プールを介した標準的な AMM スワップ
DUTCH_V2UniswapX ダッチオークション V2
DUTCH_V3UniswapX ダッチオークション V3
PRIORITYMEV 保護された優先注文(Base、Unichain)
DUTCH_LIMITUniswapX ダッチ指値注文
LIMIT_ORDER指値注文
WRAPETH から WETH への変換
UNWRAPWETH から 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 ルート:

シナリオsignaturepermitData
標準スワップ(Permit2 なし)省略省略
Permit2 スワップ必須必須
無効ありなし
無効なしあり
無効(API エラー)任意null

UniswapX ルート(DUTCH_V2/V3/PRIORITY):

シナリオsignaturepermitData
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コマンド操作識別子
スワップコマンド
コードコマンド説明
0x00V3_SWAP_EXACT_IN入力量を指定する v3 スワップ
0x01V3_SWAP_EXACT_OUT出力量を指定する v3 スワップ
0x08V2_SWAP_EXACT_IN入力量を指定する v2 スワップ
0x09V2_SWAP_EXACT_OUT出力量を指定する v2 スワップ
0x10V4_SWAPv4 スワップ
トークン操作
コードコマンド説明
0x04SWEEPルーターのトークン残高をクリアします
0x05TRANSFER指定した数量を送信します
0x0bWRAP_ETHETH から WETH へ
0x0cUNWRAP_WETHWETH から ETH へ
Permit2 コマンド
コードコマンド説明
0x02PERMIT2_TRANSFER_FROM単一トークンの送金
0x03PERMIT2_PERMIT_BATCH一括承認
0x0aPERMIT2_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 のフロー(ユーザーが署名するフロントエンド):

  1. ユーザーが Permit2 コントラクトへのトークンの使用を承認します(初回のみ)
  2. スワップごとに、ユーザーが EIP-712 の許可メッセージに署名します
  3. Universal Router は署名を使い、Permit2 経由でトークンを送金します

従来方式のフロー(バックエンドサービス、ERC-4337 スマートアカウント):

  1. Universal Router のアドレスに対してトークンを直接承認します(初回のみ)
  2. スワップごとの追加認証は不要です
  3. EIP-712 メッセージに署名できない自動化システムに適しています

Trading API の /check_approval エンドポイントを使用してください。ルーティングタイプに応じて、適切な承認先が返されます。

仕組み
  1. ユーザーは Permit2 コントラクトを一度だけ承認します(無制限の承認)
  2. スワップごとに、ユーザーは送金を許可するメッセージに署名します
  3. 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をサーバーに送信します:

  1. /quoteがpermitData(Permit2 の許可に関する EIP-712 型付きデータ)を返します
  2. ユーザーがローカルでpermitDataに署名し、signatureを生成します
  3. /swapの本文にはsignatureとpermitDataの両方を含めます — Universal Router コントラクトが、オンチェーンで Permit2 の許可を再構築して検証するためにpermitDataを必要とします

UniswapX フロー(DUTCH_V2/V3/PRIORITY) — permitDataはローカルに保持します:

  1. /quoteがpermitData(Dutch 注文に関する EIP-712 型付きデータ)を返します
  2. ユーザーがローカルでpermitDataに署名し、signatureを生成します
  3. /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)
手数料ティア
ティア値割合
LOWEST1000.01%
LOW5000.05%
MEDIUM30000.30%
HIGH100001.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 のスワップを実行します。パターンは次のとおりです。

  1. Trading API からスワップの calldata を取得します(標準的な 3 ステップのフロー)
  2. calldata を委任の引き換え実行でラップします
  3. 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アドレス
Ethereum10x66a9893cc07d91d95644aedd05d03f95e1dba8af
Unichain1300xef740bf23acae26f6492b10de645d6b98dc8eaf3
Optimism100x851116d9223fabed8e56c0e6b8ad0c31d98b3507
Base84530x6ff5693b99212da76ad316178a184ab56d299b43
Arbitrum421610xa51afafe0263b40edaef0df8781ea9aa03e381a3
Polygon1370x1095692a6237d83c6a72f3f5efedb9a670c49223
Blast814570xeabbcb3e8e415306207ef514f660a3f820025be3
BNB560x1906c1d672b88cd1b9ac7593301ca990f94eae07
Zora77777770x3315ef7ca28db74abadc6c44570efdf06b04b020
World Chain4800x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743
Avalanche431140x94b75331ae8d42c1b61065089b7d48fe14aa73b7
Celo422200xcb695bc5d3aa22cad1e6df07801b061a05a0233a
Soneium18680x4cded7edf52c8aa5259a54ec6a3ce7c6d2a455df
Ink570730x112908dac86e20e7241b0927479ea3bf935d1fa0
Monad1430x0d97dc33264bfc1c226207428a79b26757fb9dc3

テストネットのアドレスについては、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 errorCORS プロキシを設定します(Browser Environment Setup の CORS Proxy Configuration を参照)
walletClient is undefined when wallet is connecteduseWalletClient() フックの代わりに、@wagmi/core の getWalletClient() を使用します(wagmi v2 Integration Pitfalls を参照)
"Please provide a chain with the chain argument"getWalletClient(config, { chainId }) に chainId を渡します
Chain mismatch error on swapgetWalletClient() の前に switchChain() を呼び出します(wagmi v2 Integration Pitfalls を参照)
API の検証エラー(400)
エラーメッセージ原因修正
"permitData" must be of type objectpermitData: null を送信しているnull の場合はフィールドを完全に省略します
"quote" does not match any of the allowed typesquote を {quote: quoteResponse} でラップしているquote response を展開します:{...quoteResponse}
"quote" does not match any of the allowed typesUniswapX(DUTCH_V2/V3/PRIORITY)の /swap ボディに permitData を含めているUniswapX のルートでは permitData を省略します。署名と送信を参照してください
signature and permitData must both be presentPermit2 フィールドの一方のみを含めている(CLASSIC ルートのみ)CLASSIC では両方を含めるか、どちらも省略します。UniswapX では permitData を省略します
API エラーコード
コード意味
400リクエストパラメーターが無効です(上記の検証エラーを参照)
401API キーが無効または指定されていません(/permissions を含む)
404ペアのルートが見つかりません
429レート制限を超過しました
500API エラーです。指数バックオフによる再試行を実装してください
ブロードキャスト前のチェックリスト

スワップトランザクションをブロックチェーンに送信する前に:

  1. swap.data を確認する — 空でない 16 進数であること('' や '0x' ではないこと)
  2. アドレスを確認する — swap.to と swap.from が有効であること
  3. 見積もりの鮮度を確認する — 30 秒以上経過している場合は再取得します
  4. ガスを検証する — 見積もりに 10~20%のバッファーを適用します
  5. 残高を確認する — ユーザーが十分なトークン残高を保有していること

追加リソース

コメント