AquaにおけるResolverの役割
1inch Network全体では、Aquaの流動性を約定させるスワップは1inch Resolver、すなわち1inchの検証プロセスを完了したマーケットメイカーとアービトラージトレーダーによってルーティングされます。
Aqua自体はディスカバリーもマッチングも行いません。スワップ呼び出しごとに1つのポジションを1つのカウンターパーティに対して価格付けし実行するだけです。どのポジションをどの規模で約定させるかはResolverの仕事です。1inchルーティングがネットワークにスワップを渡すほか、自前のシステムからコントラクトを直接呼び出すこともできます。
基本の仕組みが初めてなら、まずはAquaの仕組みをご覧ください。
始める前に
- 1inch Businessポータルでの Resolver検証。チェーンごとのアクセストークンが付与されます
- スワップトランザクションを送るオペレーターEOA上のアクセストークン
- 対象チェーンのノードアクセス、およびフィルに使う入力トークンとガス代
- 正規のコントラクトアドレス。対応する全チェーンで同一で、下に記載しています
- Aqua APIの検出経路に使う、Business portalで発行するAPIキー
コントラクトアドレス
AquaとSwapVMルーターは決定論的デプロイで、対応する全チェーンで同一アドレスです。この2つのコントラクトとだけやり取りしてください。それ以外はAquaではありません。
- Aqua(レジストリ)
0x1111113ccf1426a8e30e2bff5e005d929bf6a90a- SwapVMルーター
0x111111338c5091e8440b67b168bae16a668ac0de
フィルの流れ
- 1
検証を受ける
BusinessポータルでResolverオンボーディングを完了し、約定させる各チェーンのアクセストークンを受け取ります。
- 2
ポジションを見つける
Aqua APIからオープンなポジションを取得するか、ShippedとDockedイベントからライブセットを再構築するか、ネットワークリゾルバーとして1inchルーティングからスワップを受け取ります。
- 3
送信直前に見積もる
デコードした注文でルーターのquote関数をスタティックコールします。見積もりはライブのウォレット裏付けを読むため、他のフィルが着地すると動きます。
- 4
境界を設定する
新しい見積もりから最小アウトプットを導き、短いデッドラインを設定します。カーブが動いた場合は、悪いレートで約定する代わりにリバートします。
- 5
スワップして検証する
アクセストークンを保有するEOAからスワップを送信し、成功したレシートを必須とし、実行された数量をSwappedイベントから読み取ります。
クイックスタート:1つのポジションを見積もって約定させる
以下のサンプルはAqua APIでオープンなポジションを見つけ、クオートを取り、保護された約定を送信します。Node 22とTypeScript SDKで動きます。コントラクト、イベント、SDK、APIはポジションをstrategyと呼ぶため、コードも同じ呼び方をします。
pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viemimport assert from 'node:assert'
import { HexString, Order } from '@1inch/swap-vm-sdk'
// Every currently open position from every LP, newest first. Auth is the
// same API key the other 1inch APIs use - issued in the Business portal.
const response = await fetch('https://api.1inch.com/aqua/v1.0/strategies/opened?chainIds=1&limit=50', {
headers: { Authorization: `Bearer ${process.env.ONEINCH_API_KEY}` },
})
assert(response.ok, `Aqua API responded ${response.status}`)
const { items, nextCursor } = await response.json()
// Pick by your own criteria. Per-token balance and allowance in each item are
// useful pre-filters, but they are indexed with a delay - only a fresh
// on-chain quote() is authoritative.
const strategy = items[0]
const order = Order.decode(new HexString(strategy.strategyBytes))
console.log('candidate:', strategy.strategyHash, 'tokens:', strategy.tokens.length, 'more:', nextCursor !== null)import assert from 'node:assert'
import { ABI, Address, HexString, Order, SwappedEvent, SwapVMContract, TakerTraits } from '@1inch/swap-vm-sdk'
import { createPublicClient, createWalletClient, decodeFunctionResult, erc20Abi, http, isHex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
// The SwapVM router is a deterministic deployment - the same address on every
// supported chain (see the contract addresses above). The strategy bytes come
// from the Aqua API or a Shipped event. The operator key belongs to the EOA
// holding the resolver credential - it must be the transaction origin.
const router = new Address('0x111111338c5091e8440b67b168bae16a668ac0de')
const strategyBytes = process.env.STRATEGY_BYTES
const operatorPrivateKey = process.env.OPERATOR_PRIVATE_KEY
assert(isHex(strategyBytes) && isHex(operatorPrivateKey))
const order = Order.decode(new HexString(strategyBytes))
const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48')
const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
const amountIn = 3_000n * 10n ** 6n
const operator = privateKeyToAccount(operatorPrivateKey)
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const wallet = createWalletClient({ chain: mainnet, transport: http(), account: operator })
// 1. Preview the swap with a static quote() call, right before you submit.
const quoteTx = SwapVMContract.buildQuoteTx(router, {
order,
tokenIn: USDC,
tokenOut: WETH,
amount: amountIn,
takerTraits: TakerTraits.new({ exactIn: true }),
})
const quoted = await publicClient.call({ to: quoteTx.to, data: quoteTx.data })
assert(quoted.data, 'quote call returned no data')
const [, quotedOut] = decodeFunctionResult({ abi: ABI.SWAP_VM_ABI, functionName: 'quote', data: quoted.data })
console.log('quoted output:', quotedOut)
// 2. Approve the router to move the input token (once per token and chain).
const approveHash = await wallet.writeContract({
address: USDC.toString(),
abi: erc20Abi,
functionName: 'approve',
args: [router.toString(), amountIn],
})
await publicClient.waitForTransactionReceipt({ hash: approveHash })
// 3. Swap with a minimum-output floor and a short deadline. Balances and
// curves move between quote and fill - never submit unprotected traits.
const swapTx = SwapVMContract.buildSwapTx(router, {
order,
tokenIn: USDC,
tokenOut: WETH,
amount: amountIn,
takerTraits: TakerTraits.new({
exactIn: true,
threshold: (quotedOut * 995n) / 1_000n,
deadline: BigInt(Math.floor(Date.now() / 1_000) + 60),
}),
})
const fillHash = await wallet.sendTransaction(swapTx)
const receipt = await publicClient.waitForTransactionReceipt({ hash: fillHash })
assert(receipt.status === 'success', 'fill reverted')
// 4. Verify the executed amounts from the router's Swapped event.
const swappedLog = receipt.logs.find((log) => log.topics[0] === SwappedEvent.TOPIC.toString())
assert(swappedLog, 'no Swapped event in the receipt')
const swapped = SwappedEvent.fromLog(swappedLog)
console.log('filled:', swapped.amountIn, '->', swapped.amountOut)まずフォークでループを試す
ここまでの内容はすべて、ローカルフォークで安全に予行できます。Foundryのanvilはあるブロックでチェーンを複製するため、本物のコントラクトと本物のライブなポジションを、何も賭けずに扱えます。鍵なし送信を有効にすれば、アクセストークンを既に持つ任意のEOAとして振る舞い、最初の実トランザクションの前に、発見、クオート、約定、検証のループ全体を歩けます。
# Real Aqua bytecode and live positions at the forked block, zero real spend
anvil --fork-url $YOUR_RPC_URL --auto-impersonate
# Point the quickstart at http://127.0.0.1:8545 and send from any EOA that
# already holds the per-chain access token - no key needed on a fork約定がオンチェーンでどう実行されるか
約定はSwapVMルーターへの1つのトランザクションです。ルーターはポジションの命令プログラムを実行します。最初にアクセスチェック、次に価格カーブ、最後にトークンの移動です。同じトランザクションの中で、入力トークンはあなたのEOAからLPのウォレットへ、出力トークンはあなたへ戻り、Aquaレジストリはポジションのバランスを更新してPushedとPulledを発行します。どの時点でも何もプールには置かれません。
すべてが全か無かです。実行時にいずれかのチェックが失敗すると、アクセストークンの欠如、threshold未達、deadline超過、ウォレット裏付け不足のどれでも、トランザクション全体がrevertし、トークンは一切動きません。revertした約定のコストはガス代だけです。
ルーターはquoteとswapを公開し、どちらも同じ価格計算パスを通ります。どちらもview関数ではないため、quoteはeth_callで静的に呼び出してください。ライブな状態を読み、渡したスワップを正確に価格付けし、何も変更しません。そのプレビューは約束ではありません。実行を拘束するのはswapに添えるtraitsだけです。
TakerTraitsは実行の境界をトランザクションに載せます。強制するのはチェーンであってSDKではありません。よく使うもの:
- exactIn
- どちら側を固定するか。厳密入力では送る量を固定し受け取りを制限します。厳密出力では受け取りを固定し送る量を制限します。
- threshold
- 境界そのもの。厳密入力では約定が必ず届けるべき最小出力。厳密出力では支払いの上限。ゼロのthresholdはチェックを無効にするため、必ず新しいクオートから導いてください。
- deadline
- unix秒での期限。それ以降にマイニングされたトランザクションはrevertするため、mempoolに滞留した約定が古いカーブで実行されることはありません。
- customReceiver
- 送信元EOA以外のアドレス、たとえばトレジャリーに出力を届けます。アクセスチェックは引き続きトランザクションの起点に対して実行されます。
- shouldUnwrap
- 出力がラップされたネイティブトークンの場合、ラップ形態ではなくネイティブトークンで受け取ります。
結果を自分の計算で信用しないでください。成功したレシートを要求し、ルーターのSwappedイベントをデコードします。orderHash、maker、taker、tokenIn、tokenOut、amountIn、amountOutです。これが実行された数量であり、帳簿に記録すべき値です。
対処すべき失敗パターン
- クローズ済みポジション
- LPはいつでもポジションを閉じられ、閉じた後のバランス読み取りはrevertします。APIのopened一覧にないポジションは閉鎖済みですが、ポーリングの合間はオンチェーンのrevertが真実の情報源です。約定のたびに再確認してください。
- ウォレット裏付け不足
- ポジションは共有ウォレット残高からクオートします。競合する約定が先に残高を使い切ると、クオートが正しく見えても転送はrevertします。APIのbalanceとallowanceは事前絞り込みに役立ちますが、遅延してインデックスされます。信頼できるのは新しいオンチェーンのクオートだけです。
- 価格の変動
- フィルの着地と時間経過で見積もりは古くなります。最小アウトプットとデッドラインが、悪いフィルをクリーンなリバートに変えます。
- アクセストークン未保有
- トランザクションオリジンにクレデンシャルがない場合、dApp作成のポジションは価格ロジックの前にスワップをリバートします。
- 非正規のアドレス
- 上の2つの正規アドレスだけがAquaです。それ以外に送った約定はライブなポジションに届きません。チェーンごとに検証するものはなく、デプロイはどこでも同一です。
本番での運用
オペレーターEOAは単なるウォレットではなく資格情報として扱ってください。チェーンごとのアクセストークンを保持しているので、その鍵は専用のサイナーに隔離し、アドレスには運転在庫とガスだけを置き、他の用途に使い回さないでください。
ガス見積もりを最後の関門にしてください。ウォレットライブラリはブロードキャスト前にガスを見積もり、失敗する見積もりは何も送信される前に早期に表面化したrevertです。それはスキップした約定として扱い、闇雲に再試行せず次の候補へ進んでください。
3つのシグナルを見てください。発見データが古くなるかサイズが裏付けに近すぎると上がるrevert率。各約定の背後にあるクオートの鮮度で、数秒に保つべきものです。そしてSwappedの実行数量とクオートの差で、ドリフトを損失になる前に捉えます。
本番環境でのポジション発見
ライブセットを得る方法は2つあります。最速で始められるのはAqua APIです。イベントからの再構築はトラストレスで、外部依存もありません。どちらも同じクオートと約定のフローにつながります。
Aqua APIは全LPの現在オープンなポジションを一覧します。api.1inch.com/aquaのGET /v1.0/strategies/openedで、カーソルページネーション(1ページ最大500件)とチェーン・appフィルターに対応します。各アイテムにはOrder.decodeに渡すstrategyBytesに加え、候補の事前絞り込みに使えるトークンごとのbalanceとallowanceが含まれます。リクエストには他の1inch APIと同じAPIキーが必要で、Business portalで発行されます。インデックスはチェーンよりわずかに遅れるため、約定前には必ずオンチェーンで新しいクオートを取り直してください。
プロトコルの中核コントラクトであるAquaレジストリは、ポジションのオープン時にShipped、クローズ時にDockedを発行します。ライブ集合はShippedからDockedを引いたもので、各チェーンのデプロイブロック以降のログから再構築します。イベントのフィールドはtopicsではなくログのdataにあるため、コントラクトアドレスとイベントシグネチャでフィルタし、クライアント側でデコードして照合してください。
イベントの形は小さいものです。Shippedはmaker、app、strategyHash、そして完全なストラテジーのバイト列を運びます。Dockedはmaker、app、strategyHashです。SDKは両方に型付きヘルパーを備え、ShippedEventとDockedEventにfromLogデコーダーとTOPIC定数が付くため、ABIを手書きすることはありません。
import { DockedEvent, ShippedEvent } from '@1inch/aqua-sdk'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const AQUA = '0x1111113ccf1426a8e30e2bff5e005d929bf6a90a'
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
// Shipped and Docked carry every field in the log data, none in topics, so
// pull the registry's logs and match on the event signature client-side.
const logs = await publicClient.getLogs({
address: AQUA,
fromBlock: 23_000_000n, // the per-chain deployment block, or your last checkpoint
toBlock: 'latest',
})
const live = new Map<string, ShippedEvent>()
for (const log of logs) {
if (log.topics[0] === ShippedEvent.TOPIC.toString()) {
const shipped = ShippedEvent.fromLog(log)
live.set(shipped.strategyHash.toString(), shipped)
} else if (log.topics[0] === DockedEvent.TOPIC.toString()) {
live.delete(DockedEvent.fromLog(log).strategyHash.toString())
}
}
// live now maps strategyHash -> Shipped payload: maker, app and the strategy
// bytes you decode with Order.decode before quoting.
console.log('open positions:', live.size)残りはオンチェーンの読み取りで揃います。レジストリのrawBalancesとsafeBalancesビューはポジションの裏付けを報告し、ルーターのSwappedイベントは実行されたスワップを運びます。クオートはキャッシュせず、新鮮に保ってください。
約定サイズは、クオートではなく裏付けから決めます。レジストリのビューでポジションの裏付けを読み、サイズをその十分内側に抑え、送信の直前にそのサイズちょうどの新しいクオートを取ります。APIのbalanceとallowanceは多数の候補への安価な一次フィルターで、1件の真実はビューとクオートです。
リゾルバーFAQ
いいえ。アクセスチェックはトランザクションの起点がアクセストークンを保持するEOAであることを要求するため、コントラクトウォレットやバンドラーは通過できません。約定は検証済みのオペレーターEOAから送ってください。
いいえ。quote関数はライブな状態の静的プレビューで、何も動かしません。実行を拘束するのはswapに添えるTakerTraitsだけで、thresholdとdeadlineがオンチェーンで強制されます。
ガス代だけです。どのチェックが失敗してもトランザクション全体がrevertするため、どちらの側でもトークンは動きません。
1inch Business portalでの検証はチェーンごとのアクセストークンを付与します。約定するすべてのチェーンで、オペレーターEOAにそのトークンが必要です。
Aquaの流動性を約定させる準備はできましたか?
1inch Businessポータルから検証を開始し、チームに連絡してください。