mcpField
V
v4-sdk-integration

v4-sdk-integration

github.com/uniswap开发236734 次安装2 天前更新
代码仓库

关于 v4-sdk-integration

面向应用层开发者的 Uniswap v4 SDK 集成技能,适用于构建交易和流动性功能。内容涵盖报价模拟、池状态读取、Permit2 授权,以及通过 Universal Router 和 PositionManager 管理头寸。

主要功能

  1. 01单跳与多跳兑换
  2. 02精确输入与精确输出报价
  3. 03通过 StateView 读取池状态
  4. 04Permit2 授权流程
  5. 05流动性头寸生命周期管理

适用场景

  1. 01构建 v4 代币兑换界面
  2. 02显示模拟交易报价
  3. 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 决策表

方面v3v4
交换执行直接使用 SwapRouter必须使用 Universal Router(V4Planner)
池架构每个池对应一个合约单例 PoolManager
池状态读取直接读取池合约StateView 合约
原生 ETH包装为 WETH原生支持(Ether.onChain(chainId))
头寸 NFTNonfungiblePositionManagerPositionManager + multicall
手续费收取显式调用 collect()修改头寸时自动收取
头寸发现链上枚举链下事件索引
代币授权直接调用 approve必须使用 Permit2
合约地址所有链上相同每条链各不相同——请从部署信息中核实

核心合约(按链)

在 https://docs.uniswap.org/contracts/v4/deployments 查询地址——不同链上的地址各不相同。

合约用途
PoolManager单例池状态
Universal Router交换执行入口
Quoter链下报价模拟(eth_call,绝不能作为交易发送)
StateView读取池状态(getSlot0、getLiquidity)
PositionManagerLP 头寸生命周期
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() 计算池标识符——不要手动构造。

链接


相关技能

  • swap-integration — Trading API 和以 v3 为核心的兑换集成(不是直接使用 v4 SDK)
  • uniswap-hooks — Solidity hook 合约生成(不是应用层 SDK)

评论

支持 Markdown