面向 Resolver

针对 Aqua 流动性成交兑换:Resolver 角色需要什么、一次成交在链上如何进行,以及如何用 SwapVM SDK 报价并执行。

10 分钟阅读更新于 2026年7月

Resolver 在 Aqua 中做什么

在整个 1inch Network 中,成交 Aqua 流动性的兑换由 1inch Resolver 路由:这些做市商和套利交易者均已完成 1inch 验证流程。

Aqua 本身既不做发现也不做撮合。每次兑换调用只会为一个头寸与一个交易对手定价并执行。找到该成交哪个头寸、成交多少,是 Resolver 的工作:1inch 路由把兑换交给网络,你自己的系统也可以直接调用合约。

不熟悉底层机制?请从这里开始:Aqua 的运作方式。

开始之前

  • 通过 1inch Business 门户完成 Resolver 验证,验证将按链授予访问代币
  • 发送兑换交易的运营 EOA 上持有访问代币
  • 目标链上的节点访问,以及成交所需的输入代币和燃料费
  • 规范合约地址,在所有受支持的链上完全一致,见下方列表
  • 用于 Aqua API 发现路径的 API 密钥,在 Business portal 签发

合约地址

Aqua 与 SwapVM 路由器为确定性部署,在所有受支持的链上地址相同。请只与这两个合约交互。除此之外都不是 Aqua。

Aqua(注册表)
0x1111113ccf1426a8e30e2bff5e005d929bf6a90a
SwapVM 路由器
0x111111338c5091e8440b67b168bae16a668ac0de

一次成交如何进行

  1. 1

    完成验证

    通过 Business 门户完成 Resolver 入驻,并为你要成交的每条链领取访问代币。

  2. 2

    发现头寸

    从 Aqua API 拉取未平仓位,或从 Shipped 与 Docked 事件重建实时集合,或作为网络 Resolver 从 1inch 路由接收兑换。

  3. 3

    提交前即时报价

    用解码后的订单以 static call 方式调用路由器的 quote 函数。报价读取实时的钱包支撑,会随其他成交落地而变化。

  4. 4

    设定边界

    从最新报价推导最小输出,并设置较短的截止时间。曲线一旦移动,交易会回滚,而不是按更差的汇率成交。

  5. 5

    兑换并核验

    从持有访问代币的 EOA 发送兑换,要求收据成功,并从 Swapped 事件中读取实际成交数量。

快速上手:报价并成交一个头寸

下面的示例通过 Aqua API 发现一个未平仓位,获取报价并提交受保护的成交。示例运行在 Node 22 和 TypeScript SDK 上。合约、事件、SDK 和 API 都把仓位称为 strategy,代码亦然。

安装 SDKbash
pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem
通过 Aqua API 发现未平仓位typescript
import 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)
报价、成交并核验typescript
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,在第一笔真实交易之前走完整个流程:发现、报价、成交、验证。

在主网分叉上试运行bash
# 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 路由器的一笔交易。路由器运行该仓位的指令程序:先是访问检查,然后是定价曲线,最后是代币转移。在同一笔交易中,输入代币从你的 EOA 转入 LP 的钱包,输出代币回到你手中,同时 Aqua 注册表更新该仓位的余额并发出 Pushed 与 Pulled。任何时刻都没有资产停留在池子里。

一切都是全有或全无。若执行时任一检查失败,缺少访问代币、threshold 未达标、deadline 已过或钱包支撑不足,整笔交易都会 revert,没有任何代币移动。被 revert 的成交只消耗 gas。

路由器提供 quote 和 swap,两者走同一条定价路径。二者都不是 view 函数,因此请通过 eth_call 静态调用 quote:它读取实时状态,为你传入的兑换精确定价,且不改变任何东西。该预览不是承诺。只有随 swap 发送的 traits 才约束执行。

TakerTraits 把你的执行边界带进交易。强制执行它们的是链,不是 SDK。最常用的有:

exactIn
固定哪一侧。精确输入时,你设定发送数量并给接收数量设下限。精确输出时,你固定接收数量并给发送数量设上限。
threshold
边界本身。精确输入时是成交必须交付的最小输出。精确输出时是你支出的上限。threshold 为零会关闭检查,所以务必从新报价推导它。
deadline
以 unix 秒计的过期时间。之后才被打包的交易会 revert,因此卡在内存池中的成交无法按过时曲线执行。
customReceiver
把输出交付到发送 EOA 之外的地址,例如金库地址。访问检查仍针对交易源执行。
shouldUnwrap
当输出是包装的原生代币时,收到原生代币而非包装形式。

不要凭自己的计算相信结果。要求收据成功,然后解码路由器的 Swapped 事件:orderHash、maker、taker、tokenIn、tokenOut、amountIn 和 amountOut。这些才是实际执行的数量,也是你的账目应当记录的数字。

需要处理的失败情形

已关闭的头寸
LP 可随时平仓,仓位消失后余额读取会 revert。API 的 opened 列表中不存在的仓位即已平仓,但在两次轮询之间,链上 revert 仍是事实来源。每次成交前请再次核查。
钱包支撑不足
仓位基于共享的钱包余额报价。竞争成交可能抢先耗尽余额,即使报价看起来没问题,你的转账也会 revert。API 返回的 balance 和 allowance 字段有助于预筛,但索引存在延迟。只有新的链上报价才是权威的。
价格移动
随着成交落地和时间流逝,报价会过期。最小输出下限和截止时间能把糟糕的成交变成干净的回滚。
缺少访问代币
若交易来源没有该凭证,dApp 创建的头寸会在定价逻辑运行之前就回滚兑换。
非规范地址
只有上方两个规范地址才是 Aqua。发往其他地址的成交都触及不到实时仓位。无需按链核验,各处部署完全一致。

生产环境运维

把操作员 EOA 当作凭证,而不只是钱包。它持有按链的访问代币,因此把它的私钥隔离在专用签名器中,地址上只保留周转库存和 gas,绝不挪作他用。

让 gas 估算成为最后一道闸。钱包库在广播前会估算 gas,估算失败就是 revert 提前显形,尚未发送任何东西。把它当作一次跳过的成交,转向下一个候选,而不是盲目重试。

盯住三个信号。你的 revert 率,当发现数据变旧或规模贴近支撑时会上升。每次成交背后报价的新鲜度,应保持在秒级。以及 Swapped 的实际执行数量与报价的对比,在偏差造成损失前把它抓住。

在生产环境中发现头寸

获取实时仓位集有两条路径。Aqua API 是最快的起点。从事件重建则是免信任的,且不引入外部依赖。两者都汇入同一套报价与成交流程。

Aqua API 列出所有 LP 当前打开的仓位:在 api.1inch.com/aqua 上调用 GET /v1.0/strategies/opened,支持游标分页(每页最多 500 条)以及链和 app 过滤。每条记录包含传给 Order.decode 的 strategyBytes,以及按代币的 balance 和 allowance,便于预筛候选。请求需要与其他 1inch API 相同的 API 密钥,在 Business portal 签发。索引略滞后于链上状态,成交前请务必重新获取链上报价。

Aqua 注册表是协议的核心合约,在头寸开启时发出 Shipped,关闭时发出 Docked。在线集合等于 Shipped 减去 Docked,从每条链的部署区块开始由日志重建。事件字段位于日志 data 而非 topics 中:先按合约地址和事件签名过滤,再在客户端解码并匹配。

事件结构很小。Shipped 携带 maker、app、strategyHash 和完整的策略字节。Docked 携带 maker、app 和 strategyHash。SDK 为两者提供类型化助手 ShippedEvent 与 DockedEvent,带 fromLog 解码器和 TOPIC 常量,你永远不用手写 ABI。

从事件重建实时集合typescript
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 字段是对大量候选的低成本初筛,对单个仓位而言,视图和报价才是事实。

解析者常见问题

不可以。访问检查要求交易源是持有访问代币的 EOA,因此合约钱包和打包器无法通过。请从已验证的操作员 EOA 发送成交。

没有。quote 函数是实时状态的静态预览,不移动任何东西。只有随 swap 发送的 TakerTraits 才约束执行:threshold 和 deadline 在链上强制执行。

只有 gas。任何检查失败都会让整笔交易 revert,两边都没有代币移动。

通过 1inch Business portal 的验证会授予按链的访问代币。你成交的每条链上,操作员 EOA 都需要持有该代币。

Aqua API 参考 用于仓位发现与统计的端点、参数和响应格式,见 Business portal。 Aqua SDK 与 SwapVM SDK 面向成交流程的类型化构建器、解码器与事件解析器,位于 1inch/sdks 仓库。 Resolver 入驻 1inch Business 门户上的验证步骤与 Resolver 文档。 Aqua 白皮书 共享流动性层的完整设计。

准备好成交 Aqua 流动性了吗?

通过 1inch Business 门户开始验证并联系团队。

打开 Business 门户