关于 v4-sdk-integration
面向应用层开发者的 Uniswap v4 SDK 集成技能,适用于构建交易和流动性功能。内容涵盖报价模拟、池状态读取、Permit2 授权,以及通过 Universal Router 和 PositionManager 管理头寸。
主要功能
- 01单跳与多跳兑换
- 02精确输入与精确输出报价
- 03通过 StateView 读取池状态
- 04Permit2 授权流程
- 05流动性头寸生命周期管理
适用场景
- 01构建 v4 代币兑换界面
- 02显示模拟交易报价
- 03添加、移除或收取流动性头寸费用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uniswap/ uniswap-ai --skill v4-sdk-integration安装后在 Claude Code 中输入/v4-sdk-integration确认已加载。
详细介绍
Uniswap v4 SDK 集成
用于交换、报价和流动性的应用层 SDK。对于 Solidity hook 合约,请使用
uniswap-hooks技能。对于 Trading API 或以 v3 为核心的交换,请使用swap-integration技能。
适用场景
- 代币交换 UI(单跳或多跳)
- 执行交易前显示报价/价格
- 流动性头寸管理(添加/移除/领取)
- 读取池状态(价格、tick、流动性)
软件包
npm i @uniswap/v4-sdk @uniswap/sdk-core @uniswap/universal-router-sdk
v4 与 v3 决策表
| 方面 | v3 | v4 |
|---|---|---|
| 交换执行 | 直接使用 SwapRouter | 必须使用 Universal Router(V4Planner) |
| 池架构 | 每个池对应一个合约 | 单例 PoolManager |
| 池状态读取 | 直接读取池合约 | StateView 合约 |
| 原生 ETH | 包装为 WETH | 原生支持(Ether.onChain(chainId)) |
| 头寸 NFT | NonfungiblePositionManager | PositionManager + multicall |
| 手续费收取 | 显式调用 collect() | 修改头寸时自动收取 |
| 头寸发现 | 链上枚举 | 链下事件索引 |
| 代币授权 | 直接调用 approve | 必须使用 Permit2 |
| 合约地址 | 所有链上相同 | 每条链各不相同——请从部署信息中核实 |
核心合约(按链)
在 https://docs.uniswap.org/contracts/v4/deployments 查询地址——不同链上的地址各不相同。
| 合约 | 用途 |
|---|---|
| PoolManager | 单例池状态 |
| Universal Router | 交换执行入口 |
| Quoter | 链下报价模拟(eth_call,绝不能作为交易发送) |
| StateView | 读取池状态(getSlot0、getLiquidity) |
| PositionManager | LP 头寸生命周期 |
| Permit2 | 代币授权层(所有链上相同:0x000000000022D473030F116dDEE9F6B43aC78BA3) |
交换模式(Universal Router)
所有交换均使用:V4Planner -> RoutePlanner -> Universal Router execute()。
单跳(精确输入):
import { Actions, V4Planner } from '@uniswap/v4-sdk';
import { CommandType, RoutePlanner } from '@uniswap/universal-router-sdk';
const v4Planner = new V4Planner();
v4Planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [swapConfig]);
v4Planner.addAction(Actions.SETTLE_ALL, [inputCurrency, amountIn]);
v4Planner.addAction(Actions.TAKE_ALL, [outputCurrency, amountOutMinimum]);
const routePlanner = new RoutePlanner();
routePlanner.addCommand(CommandType.V4_SWAP, [v4Planner.actions, v4Planner.params]);
const deadline = Math.floor(Date.now() / 1000) + 3600;
// Note: universalRouter.execute() is pseudocode for the viem call pattern.
// With viem, use: walletClient.writeContract({ address: UNIVERSAL_ROUTER_ADDRESS, abi: universalRouterAbi, functionName: 'execute', args: [routePlanner.commands, [v4Planner.finalize()], deadline], ...txOptions })
await universalRouter.execute(routePlanner.commands, [v4Planner.finalize()], deadline, txOptions);
多跳(精确输入):
import { Actions, V4Planner, encodeMultihopExactInPath } from '@uniswap/v4-sdk';
import { CommandType, RoutePlanner } from '@uniswap/universal-router-sdk';
const v4Planner = new V4Planner();
// Build multi-hop path: tokenA -> tokenB -> tokenC
const path = encodeMultihopExactInPath([poolKeyAB, poolKeyBC], tokenA);
v4Planner.addAction(Actions.SWAP_EXACT_IN, [{ path, amountIn, amountOutMinimum }]);
// SETTLE_ALL uses first pool's input currency; TAKE_ALL uses last pool's output currency
v4Planner.addAction(Actions.SETTLE_ALL, [tokenA, amountIn]);
v4Planner.addAction(Actions.TAKE_ALL, [tokenC, amountOutMinimum]);
const routePlanner = new RoutePlanner();
routePlanner.addCommand(CommandType.V4_SWAP, [v4Planner.actions, v4Planner.params]);
const deadline = Math.floor(Date.now() / 1000) + 3600;
// Note: universalRouter.execute() is pseudocode for the viem call pattern.
// With viem, use: walletClient.writeContract({ address: UNIVERSAL_ROUTER_ADDRESS, abi: universalRouterAbi, functionName: 'execute', args: [routePlanner.commands, [v4Planner.finalize()], deadline], ...txOptions })
await universalRouter.execute(routePlanner.commands, [v4Planner.finalize()], deadline, txOptions);
SwapConfig(SwapExactInSingle):
const swapConfig = {
poolKey: { currency0, currency1, fee, tickSpacing, hooks },
zeroForOne,
amountIn,
amountOutMinimum,
hookData: '0x00',
};
报价模式
Quoter 不是 view 函数。Uniswap 自己的指南解释了原因:v4 Quoter 合约
“依赖设计为会回滚的状态更改调用,以返回所需数据”
(https://developers.uniswap.org/docs/sdks/v4/guides/swapping/quoting)。如果将其作为真实交易发送,
既会浪费 Gas,也无法将返回值交给你的代码。
规则与所用库无关:通过 eth_call 模拟报价。绝不要将其作为交易发送。 不同库对这种模拟的
写法各不相同——请选择与你的技术栈相符的写法,而不是照搬指南中的示例。
quoteExactInputSingle 会返回两个值——(uint256 amountOut, uint256 gasEstimate)——因此
要将它们解构。只有 amountOut 会用于 amountOutMinimum;传入整个元组会导致错误。
// viem — the stack the rest of this skill assumes.
// simulateContract, not readContract: the quote method is nonpayable, not view.
const {
result: [amountOut, gasEstimate],
} = await publicClient.simulateContract({
address: QUOTER_ADDRESS,
abi: quoterAbi,
functionName: 'quoteExactInputSingle',
args: [{ poolKey, zeroForOne, exactAmount: amountIn, hookData: '0x00' }],
});
// ethers v6 — `.staticCall` hangs off the method itself
const [amountOut, gasEstimate] = await quoterContract.quoteExactInputSingle.staticCall({
poolKey,
zeroForOne,
exactAmount: amountIn,
hookData: '0x00',
});
// ethers v5 only — `callStatic` was removed in v6
const [amountOut, gasEstimate] = await quoterContract.callStatic.quoteExactInputSingle({
poolKey,
zeroForOne,
exactAmount: amountIn,
hookData: '0x00',
});
有四种可用方法:
quoteExactInputSingle— 单跳、精确输入数量quoteExactInput— 多跳、精确输入数量quoteExactOutputSingle— 单跳、精确输出数量quoteExactOutput— 多跳、精确输出数量
读取池状态(StateView)
import { Pool } from '@uniswap/v4-sdk';
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 授权流程(Permit2)
ERC20 代币兑换需要两次授权:代币 -> Permit2,然后 Permit2 -> Universal Router:
// Step 1: Approve Permit2 on the token contract
await erc20Contract.approve(PERMIT2_ADDRESS, MaxUint256);
// Step 2: Approve Universal Router on Permit2
await permit2Contract.approve(tokenAddress, UNIVERSAL_ROUTER_ADDRESS, MAX_UINT160, deadline);
原生 ETH 兑换无需这两次授权——在交易选项中传入 value 即可。
仓位管理(PositionManager)
所有操作都使用 PositionManager.multicall():
| 操作 | SDK 方法 |
|---|---|
| 添加流动性 | 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',
});
await walletClient.writeContract({
address: POSITION_MANAGER_ADDRESS,
functionName: 'multicall',
args: [[calldata]],
value: BigInt(value),
});
严格规则
- 切勿直接调用 PoolManager 进行兑换——务必通过 Universal Router 路由。
- 切勿假定不同链上的合约地址相同——请从部署页面查找。
- 切勿将 Quoter 作为交易发送(这会消耗大量 Gas,而且无法返回值)——务必通过
eth_call模拟:viem 使用simulateContract,ethers v6 使用.staticCall,ethers v5 使用callStatic。 - ERC20 代币兑换切勿跳过 Permit2——直接授权给 Universal Router 将不起作用。
- 兑换和 LP 操作务必设置截止时间。
- 在 v4 池上下文中处理原生 ETH 时,务必使用
Ether.onChain(chainId),而不是 WETH。 - 务必使用
Pool.getPoolId()计算池标识符——不要手动构造。
链接
- SDK 概览:https://docs.uniswap.org/sdk/v4/overview
- 兑换指南:https://docs.uniswap.org/sdk/v4/guides/swaps/quoting
- 流动性指南:https://docs.uniswap.org/sdk/v4/guides/liquidity/add-remove-liquidity
- 池数据:https://docs.uniswap.org/sdk/v4/guides/advanced/pool-data
- 部署信息:https://docs.uniswap.org/contracts/v4/deployments
- npm:https://www.npmjs.com/package/@uniswap/v4-sdk
相关技能
swap-integration— Trading API 和以 v3 为核心的兑换集成(不是直接使用 v4 SDK)uniswap-hooks— Solidity hook 合约生成(不是应用层 SDK)