后端与接口
在应用层用 Uniswap v4 SDK 构建兑换…
适用于直接基于 Uniswap v4 SDK(而非 Trading API 或 v3 路由)开发 swap、quote、池状态和流动性头寸管理 UX 的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在应用层用 Uniswap v4 SDK特定
解决思路
- 安装依赖:`npm i @uniswap/v4-sdk @uniswap/sdk-core @uniswap/universal-router-sdk`。
- v4 swap 必须使用 `Universal Router`,并通过 `V4Planner` 构造;不能像 v3 那样直接调用 `SwapRouter`。
- v4 池架构是单例 `PoolManager`,不是每个池一个合约。
- v4 池状态读取使用 `StateView` 合约,而不是直接读池合约。
- v4 原生 ETH 使用 `Ether.onChain(chainId)`,无需先包装成 WETH。
- v4 头寸 NFT 使用 `PositionManager` + `multicall`,而不是 v3 的 `NonfungiblePositionManager`。
- v4 费用收取在头寸修改时自动发生,区别于 v3 的显式 `collect()`。
- v4 头寸发现依赖链下事件索引,而不是链上枚举。
- v4 token approvals 必须使用 `Permit2`,而不是直接 approve 给 router。
- v4 合约地址每条链不同,必须从 deployments 页面验证。
- 核心合约用途:`PoolManager` 负责单例池状态。
- 核心合约用途:`Universal Router` 是兑换执行入口。
- 核心合约用途:`Quoter` 用于链下报价模拟,必须用 `eth_call`,绝不能作为交易发送。
- 核心合约用途:`StateView` 用于池状态读取,包括 `getSlot0`、`getLiquidity`。
- 核心合约用途:`PositionManager` 负责 LP 头寸生命周期。
- 核心合约用途:`Permit2` 是 token 批准层,所有链地址相同:`0x000000000022D473030F116dDEE9F6B43aC78BA3`。
- 查合约地址:<https://docs.uniswap.org/contracts/v4/deployments>;不要假设跨链地址相同。
- 所有 swap 都走 `V4Planner -> RoutePlanner -> Universal Router execute()`。
- 单跳 exact input:创建 `new V4Planner()`。
- 单跳 exact input:调用 `v4Planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [swapConfig]);`。
- 单跳 exact input:调用 `v4Planner.addAction(Actions.SETTLE_ALL, [inputCurrency, amountIn]);`。
- 单跳 exact input:调用 `v4Planner.addAction(Actions.TAKE_ALL, [outputCurrency, amountOutMinimum]);`。
- 单跳 exact input:创建 `new RoutePlanner()`。
- 单跳 exact input:调用 `routePlanner.addCommand(CommandType.V4_SWAP, [v4Planner.actions, v4Planner.params]);`。
- 单跳 exact input:设置 `deadline = Math.floor(Date.now() / 1000) + 3600;`。
- 单跳 exact input:用 viem 调用 `walletClient.writeContract({ address: UNIVERSAL_ROUTER_ADDRESS, abi: universalRouterAbi, functionName: 'execute', args: [routePlanner.commands, [v4Planner.finalize()], deadline], ...txOptions })`。
- 单跳 exact input:调用 `await universalRouter.execute(routePlanner.commands, [v4Planner.finalize()], deadline, txOptions);`;原文注明 `universalRouter.execute()` 是 viem 调用模式的伪代码。
- 多跳 exact input:创建 `new V4Planner()`。
- 多跳 exact input:构造路径 `const path = encodeMultihopExactInPath([poolKeyAB, poolKeyBC], tokenA);`。
- 多跳 exact input:调用 `v4Planner.addAction(Actions.SWAP_EXACT_IN, [{ path, amountIn, amountOutMinimum }]);`。
- 多跳 exact input:`SETTLE_ALL` 使用第一个池的 input currency;`TAKE_ALL` 使用最后一个池的 output currency。
- 多跳 exact input:调用 `v4Planner.addAction(Actions.SETTLE_ALL, [tokenA, amountIn]);`。
- 多跳 exact input:调用 `v4Planner.addAction(Actions.TAKE_ALL, [tokenC, amountOutMinimum]);`。
- 多跳 exact input:创建 `new RoutePlanner()`。
- 多跳 exact input:调用 `routePlanner.addCommand(CommandType.V4_SWAP, [v4Planner.actions, v4Planner.params]);`。
- 多跳 exact input:设置 `deadline = Math.floor(Date.now() / 1000) + 3600;`。
- 多跳 exact input:用 viem 调用 `walletClient.writeContract({ address: UNIVERSAL_ROUTER_ADDRESS, abi: universalRouterAbi, functionName: 'execute', args: [routePlanner.commands, [v4Planner.finalize()], deadline], ...txOptions })`。
- 多跳 exact input:调用 `await universalRouter.execute(routePlanner.commands, [v4Planner.finalize()], deadline, txOptions);`;原文注明这是 viem 调用模式的伪代码。
- 构造 `SwapConfig`:`const swapConfig = { poolKey: { currency0, currency1, fee, tickSpacing, hooks }, zeroForOne, amountIn, amountOutMinimum, hookData: '0x00' };`。
- Quoter 不是 `view` 函数;v4 Quoter 依赖会被 revert 的状态更改调用来返回数据,发送真实交易会浪费 gas 且无法把值返回给代码。
- 报价规则:通过 `eth_call` 模拟报价,绝不能作为交易发送。
- 按库选择报价模拟方式:viem 用 `simulateContract`,ethers v6 用 `.staticCall`,ethers v5 用 `callStatic`。
- `quoteExactInputSingle` 返回两个值 `(uint256 amountOut, uint256 gasEstimate)`,必须解构;只有 `amountOut` 传给 `amountOutMinimum`,传整个 tuple 是 bug。
- viem 报价示例:`publicClient.simulateContract({ address: QUOTER_ADDRESS, abi: quoterAbi, functionName: 'quoteExactInputSingle', args: [{ poolKey, zeroForOne, exactAmount: amountIn, hookData: '0x00' }] })`,并解构 `result: [amountOut, gasEstimate]`;不要用 `readContract`,因为报价方法是非 payable 不是 view。
- ethers v6 报价示例:`quoterContract.quoteExactInputSingle.staticCall({ poolKey, zeroForOne, exactAmount: amountIn, hookData: '0x00' })`,解构 `[amountOut, gasEstimate]`。
- ethers v5 报价示例:`quoterContract.callStatic.quoteExactInputSingle({ poolKey, zeroForOne, exactAmount: amountIn, hookData: '0x00' })`,解构 `[amountOut, gasEstimate]`;v6 已移除 `callStatic`。
- 可用报价方法:`quoteExactInputSingle`,单跳、精确输入金额。
- 可用报价方法:`quoteExactInput`,多跳、精确输入金额。
- 可用报价方法:`quoteExactOutputSingle`,单跳、精确输出金额。
- 可用报价方法:`quoteExactOutput`,多跳、精确输出金额。
- 读取池状态:`import { Pool } from '@uniswap/v4-sdk';`。
- 计算 poolId:`const poolId = Pool.getPoolId(currency0, currency1, fee, tickSpacing, hooks);`,不要手动构造。
- 并行读取池状态:`const [slot0, liquidity] = await Promise.all([stateViewContract.getSlot0(poolId), stateViewContract.getLiquidity(poolId)]);`。
- `slot0` 包含 `{ sqrtPriceX96, tick, protocolFee, lpFee }`。
- ERC20 swap 需要两步批准:token -> Permit2,然后 Permit2 -> Universal Router。
- 批准步骤 1:`await erc20Contract.approve(PERMIT2_ADDRESS, MaxUint256);`。
- 批准步骤 2:`await permit2Contract.approve(tokenAddress, UNIVERSAL_ROUTER_ADDRESS, MAX_UINT160, deadline);`。
- 原生 ETH swap 跳过这两个批准,改为在交易参数中传 `value`。
- 所有头寸操作使用 `PositionManager.multicall()`。
- 添加流动性:`V4PositionManager.addCallParameters(position, options)`。
- 移除流动性:`V4PositionManager.removeCallParameters(position, options)`。
- 收取费用:`V4PositionManager.collectCallParameters(options)`。
- 创建头寸:`V4PositionManager.createCallParameters(position, options)`。
- 添加流动性示例:`const { calldata, value } = V4PositionManager.addCallParameters(position, { slippageTolerance: new Percent(50, 10_000), deadline: deadline.toString(), tokenId: tokenId.toString(), useNative: token0.isNative ? Ether.onChain(chainId) : undefined, batchPermit, hookData: '0x', });`。
- 提交头寸 multicall:`walletClient.writeContract({ address: POSITION_MANAGER_ADDRESS, functionName: 'multicall', args: [[calldata]], value: BigInt(value), });`。
- 严格规则:绝不为 swap 直接调用 `PoolManager`;始终通过 `Universal Router`。
- 严格规则:绝不要假设合约地址跨链相同;从 deployments 页面查询。
- 严格规则:绝不将 Quoter 作为交易发送;始终通过 `eth_call` 模拟:viem `simulateContract`,ethers v6 `.staticCall`,ethers v5 `callStatic`。
- 严格规则:绝不要为 ERC20 swap 跳过 Permit2;直接 `approve` 给 Universal Router 不会工作。
- 严格规则:始终为 swap 和 LP 操作设置 deadline。
- 严格规则:在 v4 池上下文中处理原生 ETH 始终用 `Ether.onChain(chainId)`,不要用 WETH。
- 严格规则:始终用 `Pool.getPoolId()` 计算池标识符,不要手动构造。
- 参考 SDK overview:<https://docs.uniswap.org/sdk/v4/overview>。
- 参考 Swap guides:<https://docs.uniswap.org/sdk/v4/guides/swaps/quoting>。
- 参考 Liquidity guide:<https://docs.uniswap.org/sdk/v4/guides/liquidity/add-remove-liquidity>。
- 参考 Pool data:<https://docs.uniswap.org/sdk/v4/guides/advanced/pool-data>。
- 参考 Deployments:<https://docs.uniswap.org/contracts/v4/deployments>。
- 参考 npm:<https://www.npmjs.com/package/@uniswap/v4-sdk>。
适用边界 · 注意事项
- 需要编写 Solidity hook 合约时,应使用 `uniswap-hooks` skill,而不是本 app-layer SDK 方案。
- 使用 Trading API 或 v3 为中心的兑换集成时,应使用 `swap-integration` skill。
- 不打算依赖 Uniswap v4 合约、`@uniswap/v4-sdk`、`@uniswap/universal-router-sdk` 等特定包和部署地址时,不适用。