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 |
ルーティングタイプクイックリファレンス
| タイプ | 説明 | チェーン |
|---|---|---|
| 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リクエストにこれらを含めてください:
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
}
}
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オークションタイミングに影響 |
レスポンス:
{
"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ルール:
signatureとpermitDataは両方存在するか、両方存在しないかのいずれかである必要があります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 | チェーン |
|---|---|---|---|
| 1 | Ethereum | 8453 | Base |
| 10 | Optimism | 42161 | Arbitrum |
| 56 | BNB | 42220 | Celo |
| 130 | Unichain | 43114 | Avalanche |
| 137 | Polygon | 81457 | Blast |
| 196 | X Layer | 7777777 | Zora |
| 324 | zkSync | 480 | World Chain |
| 1868 | Soneium | 143 | Monad |
ルーティングタイプ
| タイプ | 説明 |
|---|---|
| CLASSIC | Uniswapプールを通じた標準AMMスワップ |
| DUTCH_V2 | UniswapX Dutch auction V2 |
| DUTCH_V3 | UniswapX Dutch auction V3 |
| PRIORITY | MEV保護優先度注文(Base、Unichain) |
| DUTCH_LIMIT | UniswapX Dutch限定注文 |
| 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エンドポイントは、クォートレスポンスが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をガスレス承認に使用する場合:
| シナリオ | signature | permitData |
|---|---|---|
| 標準スワップ(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 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を返す場合、クォートの有効期限が切れている可能性があります
Universal Router リファレンス
Universal Routerは、Uniswap v2、v3、v4全体でスワップするための統一インターフェースです。
コア機能
function execute(
bytes calldata commands,
bytes[] calldata inputs,
uint256 deadline
) external payable;
コマンドエンコーディング
各コマンドは単一バイトです:
| ビット | 名前 | 目的 |
|---|---|---|
| 0 | flag | リバートを許可(1 = 失敗時に続行) |
| 1-2 | reserved | 0を使用 |
| 3-7 | command | オペレーション識別子 |
スワップコマンド
| コード | コマンド | 説明 |
|---|---|---|
| 0x00 | V3_SWAP_EXACT_IN | v3 exact inputスワップ |
| 0x01 | V3_SWAP_EXACT_OUT | v3 exact outputスワップ |
| 0x08 | V2_SWAP_EXACT_IN | v2 exact inputスワップ |
| 0x09 | V2_SWAP_EXACT_OUT | v2 exact outputスワップ |
| 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'
// 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フロー(ユーザー署名を伴うフロントエンド):
- ユーザーがトークンをPermit2コントラクトに承認します(1回のみ)
- スワップごと:ユーザーはEIP-712許可メッセージに署名します
- Universal Routerが署名を使用してPermit2経由でトークンを転送します
レガシーフロー(バックエンドサービス、ERC-4337スマートアカウント):
- トークンをUniversal Routerアドレスに直接承認します(1回のみ)
- スワップごと:追加の認証は必要ありません
- EIP-712メッセージに署名できない自動化されたシステムの方が簡単です
Trading APIの/check_approvalエンドポイントを使用してください — ルーティングタイプに基づいて正しい承認対象を返します。
仕組み
- ユーザーが一度Permit2コントラクトを承認します(無制限承認)
- スワップごと、ユーザーが転送を認可するメッセージに署名します
- Universal Routerが署名を使用してPermit2経由でトークンを転送します
2つのモード
| モード | 説明 |
|---|---|
| SignatureTransfer | 1回の署名、オンチェーン状態なし |
| 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) |
手数料層
| 層 | 値 | パーセンテージ |
|---|---|---|
| LOWEST | 100 | 0.01% |
| LOW | 500 | 0.05% |
| MEDIUM | 3000 | 0.30% |
| HIGH | 10000 | 1.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スワップを実行します。パターン:
- Trading APIからスワップコールデータを取得(標準的な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
- ライセンス
- Apache-2.0
- 最終更新
- 2026/5/11
Source: https://github.com/Mental-Wealth-Academy/platform / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。