Agent Skills by ALSEL
OpenAIソフトウェア開発⭐ リポ 1品質スコア 68/100

swap-integration

Uniswapのスワップ機能をアプリケーションに統合できます。ユーザーが「スワップを統合したい」「Uniswapを使いたい」「取引API」「スワップ機能を追加したい」「スワップフロントエンドを構築したい」「スワップスクリプトを作成したい」「スマートコントラクトのスワップ統合」「Universal Routerを使いたい」「Trading API」などと言及した場合、またはUniswap経由でトークンをスワップすることについて述べた場合に使用します。

description の原文を見る

Integrate Uniswap swaps into applications. Use when user says "integrate swaps", "uniswap", "trading api", "add swap functionality", "build a swap frontend", "create a swap script", "smart contract swap integration", "use Universal Router", "Trading API", or mentions swapping tokens via Uniswap.

SKILL.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(推奨)

最適用途:フロントエンド、バックエンド、スクリプト。ルーティング最適化を自動的に処理します。

ベースURLhttps://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リクエストにこれらを含めてください:

Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0

3ステップフロー

1. POST /check_approval  -> トークンが承認されているか確認
2. POST /quote           -> ルーティング付きの実行可能なクォートを取得
3. POST /swap            -> 署名・送信するトランザクションを取得

完全なドキュメントについては、以下の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リファレンスセクションを参照してください。


Trading APIリファレンス

ステップ1:トークン承認を確認

POST /check_approval

リクエスト

{
  "walletAddress": "0x...",
  "token": "0x...",
  "amount": "1000000000",
  "chainId": 1
}

レスポンス

{
  "approval": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1
  }
}

approvalnullの場合、トークンは既に承認されています。

ステップ2:クォートを取得

POST /quote

リクエスト

{
  "swapper": "0x...",
  "tokenIn": "0x...",
  "tokenOut": "0x...",
  "tokenInChainId": "1",
  "tokenOutChainId": "1",
  "amount": "1000000000000000000",
  "type": "EXACT_INPUT",
  "slippageTolerance": 0.5,
  "routingPreference": "BEST_PRICE"
}

注意tokenInChainIdtokenOutChainIdは、数字ではなく文字列(例:"1")である必要があります。

キーパラメータ

パラメータ説明
typeEXACT_INPUTまたはEXACT_OUTPUT
slippageTolerance0~100のパーセンテージ
protocolsオプション:["V2", "V3", "V4"]
routingPreferenceBEST_PRICEFASTESTCLASSIC
autoSlippageスリップページを自動計算するにはtrueslippageToleranceをオーバーライド)
urgencynormalまたはfast — UniswapXオークションタイミングに影響

レスポンス

{
  "routing": "CLASSIC",
  "quote": {
    "input": { "token": "0x...", "amount": "1000000000000000000" },
    "output": { "token": "0x...", "amount": "999000000" },
    "slippage": 0.5,
    "route": [],
    "gasFee": "5000000000000000",
    "gasFeeUSD": "0.01",
    "gasUseEstimate": "150000"
  },
  "permitData": {}
}

表示のヒント:ガスコスト表示にはgasFeeUSD(USD値の文字列)を使用してください。gasFee(wei)をハードコードされたETH価格を使って手動で変換しないでください — これにより非常に不正確な推定値が生成されます(例えば、$0.01ではなく$87)。

ステップ3:スワップを実行

POST /swap

リクエスト - クォートレスポンスをリクエストボディに直接スプレッドします:

// 正解:クォートレスポンスをスプレッド、nullフィールドを削除
const quoteResponse = await fetchQuote(params);

// null permitData/permitTransactionを削除(APIはnull値を拒否)
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

const swapRequest = {
  ...cleanQuote,
  // permitDataが有効なオブジェクト(nullではない)場合のみ含める
  ...(permitData && { permitData }),
};

// Permit2署名を使用する場合は、署名とpermitDataの両方を含める
if (permit2Signature && permitData) {
  swapRequest.signature = permit2Signature;
  swapRequest.permitData = permitData;
}

重要:クォートを{quote: quoteResponse}にラップしないでください。APIはクォートレスポンスフィールドがリクエストボディにスプレッドされることを期待します。

Permit2ルール

  • signaturepermitDataは両方存在するか、両方存在しないかのいずれかである必要があります
  • permitData: nullを設定しないでください - フィールド全体を省略してください
  • クォートレスポンスは多くの場合permitData: nullを含みます - 送信する前にこれをストリップしてください

レスポンス(署名・送信準備完了のトランザクション):

{
  "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が空です - クォートの有効期限が切れている可能性があります');
  }
  if (!isAddress(response.swap.to) || !isAddress(response.swap.from)) {
    throw new Error('スワップレスポンスに無効なアドレスがあります');
  }
}

サポートされているチェーン

IDチェーンIDチェーン
1Ethereum8453Base
10Optimism42161Arbitrum
56BNB42220Celo
130Unichain43114Avalanche
137Polygon81457Blast
196X Layer7777777Zora
324zkSync480World Chain
1868Soneium143Monad

ルーティングタイプ

タイプ説明
CLASSICUniswapプールを通じた標準AMMスワップ
DUTCH_V2UniswapX Dutch auction V2
DUTCH_V3UniswapX Dutch auction V3
PRIORITYMEV保護優先度注文(Base、Unichain)
DUTCH_LIMITUniswapX Dutch限定注文
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エンドポイントは、クォートレスポンスがquoteフィールドにラップされるのではなく、リクエストボディにスプレッドされることを期待しています。

// 間違い - 「quote does not match any of the allowed types」を引き起こす
const badRequest = {
  quote: quoteResponse, // ラップしないでください!
  signature: '0x...',
};

// 正解 - クォートレスポンスをスプレッド
const goodRequest = {
  ...quoteResponse,
  signature: '0x...', // Permit2を使用する場合のみ
};

2. Nullフィールド処理

APIはpermitData: nullを拒否します。送信前に常にnullフィールドをストリップしてください:

function prepareSwapRequest(quoteResponse: QuoteResponse, signature?: string): object {
  // APIが拒否するnull値をストリップ
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

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

  // permitDataが有効なオブジェクトで、署名がある場合のみ含める
  if (signature && permitData && typeof permitData === 'object') {
    request.signature = signature;
    request.permitData = permitData;
  }

  return request;
}

3. Permit2フィールドルール

Permit2をガスレス承認に使用する場合:

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

4. 放送前検証

常にブロックチェーンに送信する前にスワップレスポンスを検証してください:

import { isAddress, isHex } from 'viem';

function validateSwapBeforeBroadcast(swap: SwapTransaction): void {
  // 1. dataは空でないhexである必要があります
  if (!swap.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.dataが空です - これはオンチェーンで失敗します。クォートを再取得してください。');
  }

  if (!isHex(swap.data)) {
    throw new Error('swap.dataは有効なhexではありません');
  }

  // 2. アドレスは有効である必要があります
  if (!isAddress(swap.to)) {
    throw new Error('swap.toは有効なアドレスではありません');
  }

  if (!isAddress(swap.from)) {
    throw new Error('swap.fromは有効なアドレスではありません');
  }

  // 3. valueは存在する必要があります(非ETHスワップの場合は「0」が可能)
  if (swap.value === undefined || swap.value === null) {
    throw new Error('swap.valueが欠落しています');
  }
}

5. ブラウザ環境セットアップ

ブラウザ環境でviem/wagmiを使用する場合、Node.jsポリフィルが必要です:

bufferポリフィルをインストール

npm install buffer

エントリファイルに追加(他のインポートの前)

// src/main.tsx or src/index.tsx
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;

// その後、他のインポート
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 Pagespublic/_redirects):

/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200

Next.jsnext.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を返す場合、クォートの有効期限が切れている可能性があります

Universal Router リファレンス

Universal Routerは、Uniswap v2、v3、v4全体でスワップするための統一インターフェースです。

コア機能

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

コマンドエンコーディング

各コマンドは単一バイトです:

ビット名前目的
0flagリバートを許可(1 = 失敗時に続行)
1-2reserved0を使用
3-7commandオペレーション識別子

スワップコマンド

コードコマンド説明
0x00V3_SWAP_EXACT_INv3 exact inputスワップ
0x01V3_SWAP_EXACT_OUTv3 exact outputスワップ
0x08V2_SWAP_EXACT_INv2 exact inputスワップ
0x09V2_SWAP_EXACT_OUTv2 exact outputスワップ
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'

// v3-sdkまたはrouter-sdkを使用してトレードを構築
const trade = new RouterTrade({
  v3Routes: [...],
  tradeType: TradeType.EXACT_INPUT
})

// Universal Routerのコールデータを取得
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1200 // 20分
})

// トランザクションを送信
const tx = await wallet.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value
})

Permit2統合

Permit2は、オンチェーン承認呼び出しの代わりに署名ベースのトークン承認を有効にします。

承認対象:Permit2対レガシー(ルーターへの直接承認)

2つの承認パスがあります。統合タイプに基づいて選択してください:

アプローチ承認対象スワップごと認証最適用途
Permit2(推奨)Permit2コントラクトEIP-712署名ユーザー相互作用を伴うフロントエンド
レガシー(直接承認)Universal Routerなし(事前承認)バックエンドサービス、スマートアカウント

Permit2フロー(ユーザー署名を伴うフロントエンド):

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

レガシーフロー(バックエンドサービス、ERC-4337スマートアカウント):

  1. トークンをUniversal Routerアドレスに直接承認します(1回のみ)
  2. スワップごと:追加の認証は必要ありません
  3. EIP-712メッセージに署名できない自動化されたシステムの方が簡単です

Trading APIの/check_approvalエンドポイントを使用してください — ルーティングタイプに基づいて正しい承認対象を返します。

仕組み

  1. ユーザーが一度Permit2コントラクトを承認します(無制限承認)
  2. スワップごと、ユーザーが転送を認可するメッセージに署名します
  3. Universal Routerが署名を使用してPermit2経由でトークンを転送します

2つのモード

モード説明
SignatureTransfer1回の署名、オンチェーン状態なし
AllowanceTransfer時間限定の手当でオンチェーン状態あり

統合パターン

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

const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' as const;

// Permit2承認が存在するかどうかを確認
const allowance = await publicClient.readContract({
  address: PERMIT2_ADDRESS,
  abi: permit2Abi,
  functionName: 'allowance',
  args: [userAddress, tokenAddress, spenderAddress],
});

// 承認されていない場合、ユーザーは最初にPermit2を承認する必要があります
if (allowance.amount < requiredAmount) {
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'approve',
    args: [PERMIT2_ADDRESS, maxUint256],
  });
  await publicClient.waitForTransactionReceipt({ hash });
}

// その後、スワップのpermitに署名
const permitSignature = await signPermit(...);

UniswapXオークションタイプ

UniswapXはスワップをオフチェーンフィラーを通じてルーティングします。フィラーは、オンチェーンAMMよりも良い価格で実行するために競合します。オークションメカニズムはチェーンごとに異なります。

排他的Dutch Auction(Ethereum)

  • RFQ(Request for Quote)フェーズで開始。認可されたクォーター者が競合します
  • 勝者は、設定期間排他的充填権を受け取ります
  • 排他的フィラーが実行しない場合、フォールバックして、ブロックごとに価格が低下するオープンDutch auctionを行います
  • 大規模なスワップでMEV保護が最も重要な場合に最適です

Trading APIルーティングタイプDUTCH_V2またはDUTCH_V3

オープンDutch Auction(Arbitrum)

  • RFQフェーズなしの直接オープンオークション
  • フィラーは降価メカニズムを通じてオンチェーンで競合します
  • Arbitrumの高速0.25秒ブロック時間を活用して、迅速な価格発見を行います
  • Uniアルゴリズムは履歴ペアパフォーマンスに基づいてオークションパラメータを設定します

Trading APIルーティングタイプDUTCH_V2

優先度ガスオークション(Base、Unichain)

  • フィラーが異なる優先度料金でターゲットブロックにトランザクションを送信することで入札します
  • 最高優先度料金がオーダーを充填する権利を獲得します
  • OP Stack の優先度順序メカニズムを利用します
  • ブロックビルダーが優先度順序を尊重するチェーンで効果的です

Trading APIルーティングタイプPRIORITY

キープロパティ(すべてのオークションタイプ)

  • ユーザーのためのガスレス — フィラーはガス代を支払い、最終価格に組み込まれます
  • 失敗時のコストなし — スワップが充填されない場合、ユーザーは何も支払いません
  • MEV保護 — オークションメカニズムはフロントランニングとサンドイッチ攻撃を防止します
  • UniswapX V2は現在Ethereum(1)、Arbitrum(42161)、Base(8453)、Unichain(130)でサポートされています

詳細については、UniswapXオークションタイプドキュメントを参照してください。


ダイレクト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. プールデータを取得(ルート構築に必須)
// viemを使用してオンチェーンプール状態を読み取ります:
const slot0 = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'slot0',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [
        { name: 'sqrtPriceX96', type: 'uint160' },
        { name: 'tick', type: 'int24' },
        { name: 'observationIndex', type: 'uint16' },
        { name: 'observationCardinality', type: 'uint16' },
        { name: 'observationCardinalityNext', type: 'uint16' },
        { name: 'feeProtocol', type: 'uint8' },
        { name: 'unlocked', type: 'bool' },
      ],
    },
  ],
  functionName: 'slot0',
});
const liquidity = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'liquidity',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [{ type: 'uint128' }],
    },
  ],
  functionName: 'liquidity',
});

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

// 2. ルートとトレードを構築
const route = new V3Route([pool], tokenIn, tokenOut);
const trade = RouterTrade.createUncheckedTrade({
  route,
  inputAmount: amountIn,
  outputAmount: expectedOut,
  tradeType: TradeType.EXACT_INPUT,
});

// 3. コールデータを取得
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1800,
});

// 4. viemで実行
const hash = await walletClient.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value: BigInt(value),
});

低レベルアプローチ(手動コマンド)

カスタムフロー(手数料収集、複雑なルーティング)の場合、RoutePlannerを直接使用してください:

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

// 特別なアドレス
const MSG_SENDER = '0x0000000000000000000000000000000000000001';
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002';

例:手動コマンドを使用した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();

  // ルートからV3パスをエンコード
  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からトークンへ(Wrap + Swap)

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

  // 1. ETHをWETHにラップ(ルーター内に保持)
  planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, amountIn]);

  // 2. WETH → トークンをスワップ(payerIsUser = falseルーターのWETHを使用)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER,
    amountIn,
    amountOutMin,
    path,
    false,
  ]);

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

例:トークンからETHへ(Swap + Unwrap)

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

  // 1. トークン → WETH をスワップ(出力をルーターへ)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    ADDRESS_THIS,
    amountIn,
    amountOutMin,
    path,
    true,
  ]);

  // 2. WETHをETHにアンラップ
  planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOutMin]);

  return executeRoute(planner);
}

例: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;

  // ルーターにスワップ(ADDRESS_THIS)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ADDRESS_THIS, amountIn, 0n, path, true]);

  // 手数料部分を支払う(例:30 bips = 0.3%)
  planner.addCommand(CommandType.PAY_PORTION, [outputToken, feeRecipient, feeBips]);

  // 残りをユーザーにスイープ
  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ポリフィルとコース プロキシを設定していることを確認してください(重要な実装に関する注意を参照)。wagmi v2 useWalletClient()の落とし穴については、wagmi v2統合の落とし穴を参照してください。

import { isAddress, isHex } from 'viem';
import { useWalletClient } from 'wagmi';

// ブラウザアプリでは、CORSプロキシパスを代わりに使用してください(CORSプロキシ設定を参照)
// 例:const API_URL = '/api/uniswap';
const API_URL = 'https://trade-api.gateway.uniswap.org/v1';

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

  const getQuote = async (params) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(`${API_URL}/quote`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': API_KEY,
          'x-universal-router-version': '2.0',
        },
        body: JSON.stringify(params),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.detail || 'Quote failed');
      setQuoteResponse(data); // 完全なレスポンスを保存、data.quoteだけではない
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

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

    // 重要:nullフィールドをストリップしてクォートレスポンスをボディに展開
    const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

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

    // 重要:署名とpermitDataの両方がある場合のみpermitDataを含める
    // APIは両方のフィールドが存在するか両方存在しないかのいずれかを要求
    if (permit2Signature && permitData && typeof permitData === 'object') {
      swapRequest.signature = permit2Signature;
      swapRequest.permitData = permitData;
    }

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

    // 重要:放送前に応答を検証
    if (!data.swap?.data || data.swap.data === '' || data.swap.data === '0x') {
      throw new Error('Empty swap data - quote may have expired. Please refresh.');
    }

    // useWalletClient()からのwalletClientを使用してウォレット経由でトランザクションを送信
    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()が機能するためにチェーンが必要です。

推奨パターン — スワップ時に代わりに@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. ウォレットが正しいチェーン上にあることを確認
  await switchChain(config, { chainId });

  // 2. 明示的なchainIdを使用してウォレットクライアントを取得 — undefinedとチェーン欠落を回避
  const walletClient = await getWalletClient(config, { chainId });

  // 3. スワップを実行
  const hash = await walletClient.sendTransaction({
    to: swapTx.to as `0x${string}`,
    data: swapTx.data as `0x${string}`,
    value: BigInt(swapTx.value || '0'),
  });

  // 4. 確認を待つ
  const publicClient = getPublicClient(config, { chainId });
  if (!publicClient) throw new Error(`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!;

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

// クォートレスポンスからnullフィールドをストリップするヘルパー
function prepareSwapRequest(quoteResponse: Record<string, unknown>, signature?: string): object {
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;

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

  // 重要:署名とpermitDataの両方がある場合のみpermitDataを含める
  // APIは両方のフィールドが存在するか両方存在しないかのいずれかを要求
  if (signature && permitData && typeof permitData === 'object') {
    request.signature = signature;
    request.permitData = permitData;
  }

  return request;
}

// 放送前にスワップレスポンスを検証
function validateSwap(swap: { data?: string; to?: string; from?: string }): void {
  if (!swap?.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.dataが空です - クォートの有効期限が切れている可能性があります');
  }
  if (!isHex(swap.data)) {
    throw new Error('swap.dataは有効なhexではありません');
  }
  if (!swap.to || !isAddress(swap.to) || !swap.from || !isAddress(swap.from)) {
    throw new Error('スワップレスポンスに無効なアドレスがあります');
  }
}

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

  // 1. 承認を確認(ネイティブETHではなくERC20トークン用)
  if (tokenIn !== ETH_ADDRESS) {
    const approvalRes = await fetch(`${API_URL}/check_approval`, {
      method: 'POST',
      headers: {
        'x-api-key': API_KEY,
        'Content-Type': 'application/json',
        'x-universal-router-version': '2.0',
      },
      body: JSON.stringify({
        walletAddress: account.address,
        token: tokenIn,
        amount,
        chainId,
      }),
    });
    const approvalData = await approvalRes.json();

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

  // 2. クォートを取得
  const quoteRes = await fetch(`${API_URL}/quote`, {
    method: 'POST',
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
      'x-universal-router-version': '2.0',
    },
    body: JSON.stringify({
      swapper: account.address,
      tokenIn,
      tokenOut,
      tokenInChainId: String(chainId),
      tokenOutChainId: String(chainId),
      amount,
      type: 'EXACT_INPUT',
      slippageTolerance: 0.5,
    }),
  });
  const quoteResponse = await quoteRes.json(); // 完全なレスポンスを保存

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

  // 3. スワップを実行 - 重要:クォートレスポンスをスプレッド、nullフィールドをストリップ
  const swapRequest = prepareSwapRequest(quoteResponse);

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

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

  // 4. 放送前に検証
  validateSwap(swapData.swap);

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

スマートコントラクト統合(Solidity)

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

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

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

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

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

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

    // トークンをPermit2用に承認(1回のセットアップ)
    function approveToken(address token) external {
        IERC20(token).approve(PERMIT2, type(uint256).max);
    }
}

高度なパターン

スマートアカウント統合(ERC-4337)

委任を使用してERC-4337スマートアカウント経由でTrading APIスワップを実行します。パターン:

  1. Trading APIからスワップコールデータを取得(標準的な3段階フロー)
  2. コールデータを委任引き換え実行にラップ
  3. バンドラーとしてUserOperationを送信
// Trading APIからスワップコールデータを取得した後:
const { to, data, value } = swapResponse.swap;

// 委任実行にラップ
const execution = {
  target: to, // Universal Router
  callData: data,
  value: BigInt(value),
};

// バンドラー経由で送信
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',
};

// 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レスポンスで指数バックオフとジッターを実装してください
  • 承認結果をキャッシュ — 承認はコール間でめったに変わりません
// 429レスポンスの指数バックオフ
async function fetchWithRetry(url: string, init: RequestInit, maxRetries = 5): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, init);
    if (response.status !== 429 && response.status < 500) return response;
    if (attempt === maxRetries) throw new Error(`Failed after ${maxRetries} retries`);

    const delay = Math.min(200 * Math.pow(2, attempt) + Math.random() * 100, 10000);
    await new

ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
Mental-Wealth-Academy
リポジトリ
Mental-Wealth-Academy/platform
ライセンス
Apache-2.0
最終更新
2026/5/11

Source: https://github.com/Mental-Wealth-Academy/platform / ライセンス: Apache-2.0

関連スキル

汎用ソフトウェア開発⭐ リポ 39,967

doubt-driven-development

重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 1,175

apprun-skills

TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。

by yysun
OpenAIソフトウェア開発⭐ リポ 797

desloppify

コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。

by Git-on-my-level
汎用ソフトウェア開発⭐ リポ 39,967

debugging-and-error-recovery

テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

test-driven-development

テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

incremental-implementation

変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。

by addyosmani
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: Mental-Wealth-Academy · Mental-Wealth-Academy/platform · ライセンス: Apache-2.0