@w3lib/evm-batch-call 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +79 -2
  3. package/dist/liquidity/common.d.ts +72 -0
  4. package/dist/liquidity/common.d.ts.map +1 -0
  5. package/dist/liquidity/common.js +271 -0
  6. package/dist/liquidity/common.js.map +1 -0
  7. package/dist/liquidity/concentrated.d.ts +19 -0
  8. package/dist/liquidity/concentrated.d.ts.map +1 -0
  9. package/dist/liquidity/concentrated.js +106 -0
  10. package/dist/liquidity/concentrated.js.map +1 -0
  11. package/dist/liquidity/index.d.ts +42 -0
  12. package/dist/liquidity/index.d.ts.map +1 -0
  13. package/dist/liquidity/index.js +143 -0
  14. package/dist/liquidity/index.js.map +1 -0
  15. package/dist/liquidity/types.d.ts +182 -0
  16. package/dist/liquidity/types.d.ts.map +1 -0
  17. package/dist/liquidity/types.js +2 -0
  18. package/dist/liquidity/types.js.map +1 -0
  19. package/dist/liquidity/v2.d.ts +5 -0
  20. package/dist/liquidity/v2.d.ts.map +1 -0
  21. package/dist/liquidity/v2.js +342 -0
  22. package/dist/liquidity/v2.js.map +1 -0
  23. package/dist/liquidity/v3.d.ts +6 -0
  24. package/dist/liquidity/v3.d.ts.map +1 -0
  25. package/dist/liquidity/v3.js +310 -0
  26. package/dist/liquidity/v3.js.map +1 -0
  27. package/dist/liquidity/v4.d.ts +5 -0
  28. package/dist/liquidity/v4.d.ts.map +1 -0
  29. package/dist/liquidity/v4.js +279 -0
  30. package/dist/liquidity/v4.js.map +1 -0
  31. package/dist/lp.d.ts +8 -1
  32. package/dist/lp.d.ts.map +1 -1
  33. package/dist/lp.js +29 -18
  34. package/dist/lp.js.map +1 -1
  35. package/dist/lpMath.d.ts +55 -0
  36. package/dist/lpMath.d.ts.map +1 -1
  37. package/dist/lpMath.js +209 -0
  38. package/dist/lpMath.js.map +1 -1
  39. package/dist/subpaths/liquidity.d.ts +24 -0
  40. package/dist/subpaths/liquidity.d.ts.map +1 -0
  41. package/dist/subpaths/liquidity.js +21 -0
  42. package/dist/subpaths/liquidity.js.map +1 -0
  43. package/dist/subpaths/lp.d.ts +1 -1
  44. package/dist/subpaths/lp.d.ts.map +1 -1
  45. package/dist/subpaths/lp.js +1 -1
  46. package/dist/subpaths/lp.js.map +1 -1
  47. package/dist/write/tron.d.ts +2 -0
  48. package/dist/write/tron.d.ts.map +1 -1
  49. package/dist/write/tron.js +4 -0
  50. package/dist/write/tron.js.map +1 -1
  51. package/package.json +20 -12
  52. package/src/liquidity/common.ts +314 -0
  53. package/src/liquidity/concentrated.ts +121 -0
  54. package/src/liquidity/index.ts +171 -0
  55. package/src/liquidity/types.ts +194 -0
  56. package/src/liquidity/v2.ts +371 -0
  57. package/src/liquidity/v3.ts +382 -0
  58. package/src/liquidity/v4.ts +348 -0
  59. package/src/lp.ts +36 -18
  60. package/src/lpMath.ts +223 -0
  61. package/src/subpaths/liquidity.ts +52 -0
  62. package/src/subpaths/lp.ts +20 -1
  63. package/src/write/tron.ts +5 -0
@@ -0,0 +1,194 @@
1
+ import type { BigNumberish } from 'ethers'
2
+
3
+ import type { LpPosition, LpProtocol, LpToken, LpVersion } from '../lp.js'
4
+ import type { SendOptions, TronSendOptions, WriteResult, WriteStatus } from '../write/send.js'
5
+
6
+ /**
7
+ * 已有的仓位:直接传 getLpPositions 查到的 LpPosition,或者只传能定位仓位的字段。
8
+ * - V2:pool(LP 代币地址)
9
+ * - V3 / V4:tokenId,加上 positionManager 或 protocol(协议 id / LpProtocol)
10
+ */
11
+ export interface PositionRef {
12
+ pool?: string
13
+ tokenId?: string | number | bigint
14
+ positionManager?: string
15
+ protocol?: string | LpProtocol
16
+ }
17
+
18
+ /** 往已有仓位追加流动性:两边数量给一边即可(另一边按当前价格算),也可以都给(按较少的一边) */
19
+ export interface IncreaseLiquidityParams {
20
+ position: PositionRef | LpPosition
21
+ /** token0 的数量:amount0(最小单位)和 amount0Formatted(按精度换算,如 '1.5')二选一 */
22
+ amount0?: BigNumberish
23
+ amount0Formatted?: string
24
+ amount1?: BigNumberish
25
+ amount1Formatted?: string
26
+ }
27
+
28
+ /**
29
+ * 新建仓位(V3 / V4:一个新的仓位 NFT;V2:往 tokenA / tokenB 的池子里加,池子不存在时创建)。
30
+ * 两个代币不分顺序,价格都是“1 个 tokenA 值多少个 tokenB”
31
+ */
32
+ export interface NewPositionParams {
33
+ /** 协议 id(如 'uniswap-v3')或自己的 LpProtocol */
34
+ protocol: string | LpProtocol
35
+ /** 代币地址;主币用 NATIVE_TOKEN 或零地址(V2 / V3 换成包装币、用主币支付;V4 是主币池子) */
36
+ tokenA: string
37
+ tokenB: string
38
+ /** V3 / V4:手续费档位(百万分之一:500 = 0.05%) */
39
+ fee?: number
40
+ /** V4:tick 间距,默认按手续费档位(100 → 1、500 → 10、3000 → 60、10000 → 200) */
41
+ tickSpacing?: number
42
+ /** V4:池子的 hooks 合约,默认零地址 */
43
+ hooks?: string
44
+ /** V3 / V4:价格区间(tokenB / tokenA,取最近的可用 tick);或直接给 tickLower / tickUpper;或 fullRange */
45
+ priceLower?: string | number
46
+ priceUpper?: string | number
47
+ /** 池子里的 tick(token1 / token0 方向),必须是 tickSpacing 的整数倍 */
48
+ tickLower?: number
49
+ tickUpper?: number
50
+ /** 全区间(最小到最大的可用 tick) */
51
+ fullRange?: boolean
52
+ /** tokenA / tokenB 的数量:amount(最小单位)和 formatted 二选一;两边给一边即可,V2 新池子两边都要给 */
53
+ amountA?: BigNumberish
54
+ amountAFormatted?: string
55
+ amountB?: BigNumberish
56
+ amountBFormatted?: string
57
+ /** 池子不存在(或还没初始化)时用这个价格创建(tokenB / tokenA)。不传时池子不存在会报错 */
58
+ initialPrice?: string | number
59
+ }
60
+
61
+ export type AddLiquidityParams = IncreaseLiquidityParams | NewPositionParams
62
+
63
+ export interface RemoveLiquidityParams {
64
+ position: PositionRef | LpPosition
65
+ /** 移除的比例(百分数,(0, 100]),默认 100 */
66
+ percent?: number
67
+ /** 或者直接给移除的流动性:V3 / V4 是 L,V2 是 LP 代币数量(最小单位) */
68
+ liquidity?: BigNumberish
69
+ /** V3 / V4:全部移除后销毁仓位 NFT,默认 false */
70
+ burn?: boolean
71
+ }
72
+
73
+ export interface CollectFeesParams {
74
+ /** V3 / V4 的仓位(V2 的手续费已经计入 LP 份额,没有单独领取) */
75
+ position: PositionRef | LpPosition
76
+ }
77
+
78
+ export interface LiquidityOptions {
79
+ /** 价格滑点(比例,0.005 = 0.5%),默认 0.005 */
80
+ slippage?: number
81
+ /** 交易的有效期(秒),默认 1200 */
82
+ deadline?: number
83
+ /** 收到 LP 代币 / 仓位 NFT / 移除的代币的地址,默认 owner */
84
+ recipient?: string
85
+ /**
86
+ * V2 / V3:包装币(WETH / WBNB …)那一边用主币支付 / 收取。
87
+ * 新建仓位时 tokenA / tokenB 传主币地址会自动开启;往已有仓位追加、移除、领取时默认 false(收到包装币)。V4 的主币池子不需要
88
+ */
89
+ native?: boolean
90
+ /** 授权额度:'exact'(默认,本次需要的数量)或 'max'(无限额度,之后不用再授权) */
91
+ approve?: 'exact' | 'max'
92
+ /** 内置表里没有的协议(自己的 LpProtocol),定位仓位时也会用到 */
93
+ protocols?: readonly LpProtocol[]
94
+ /** V4:传给 hooks 的 hookData,默认 '0x' */
95
+ hookData?: string
96
+ }
97
+
98
+ /** 一笔要发送的交易 */
99
+ export interface LiquidityTx {
100
+ /**
101
+ * - approve:代币授权给 Router / 仓位合约 / Permit2
102
+ * - permit2:Permit2 授权给 V4 的仓位合约
103
+ * - add / remove / collect:添加 / 移除流动性、领取手续费
104
+ */
105
+ kind: 'approve' | 'permit2' | 'add' | 'remove' | 'collect'
106
+ to: string
107
+ data: string
108
+ /** 附带的主币(最小单位) */
109
+ value: string
110
+ /** approve / permit2:授权的代币、被授权方、额度 */
111
+ token?: string
112
+ spender?: string
113
+ amount?: string
114
+ }
115
+
116
+ /**
117
+ * 添加 / 移除流动性的计划:预计的数量 + 要按顺序发送的交易(授权在前)。
118
+ * 只做读取、不签名,可以当作报价用(输入一边数量,看另一边要多少)
119
+ */
120
+ export interface LiquidityPlan {
121
+ action: 'add' | 'remove' | 'collect'
122
+ chainId: number
123
+ version: LpVersion
124
+ /** 协议 id / 显示名 */
125
+ protocol: string
126
+ protocolName: string
127
+ /** 池子地址(V4 为 poolId);V2 / V3 的池子还不存在时为 null */
128
+ pool: string | null
129
+ /** 这次会创建(初始化)池子 */
130
+ createPool: boolean
131
+ owner: string
132
+ recipient: string
133
+ token0: LpToken
134
+ token1: LpToken
135
+ /** 已有仓位的 tokenId;新建 V3 / V4 仓位、V2 为 null */
136
+ tokenId: string | null
137
+ /** V3 / V4:仓位的 tick 区间、池子当前的 tick 和价格(token1 / token0,按精度换算,只用于展示) */
138
+ tickLower: number | null
139
+ tickUpper: number | null
140
+ tick: number | null
141
+ price: number | null
142
+ priceLower: number | null
143
+ priceUpper: number | null
144
+ /** 添加 / 移除的流动性:V3 / V4 是 L;V2 是 LP 代币数量(添加时为预计) */
145
+ liquidity: string
146
+ /** 按当前价格预计存入 / 取出的数量(不含手续费) */
147
+ amount0: string
148
+ amount1: string
149
+ amount0Formatted: string | null
150
+ amount1Formatted: string | null
151
+ /** 添加:最多会用掉的数量(V4 按滑点上浮;V2 / V3 即传给合约的数量);移除:为 null */
152
+ amount0Max: string | null
153
+ amount1Max: string | null
154
+ /** 移除:至少会拿到的数量(价格在滑点范围外时交易失败);添加:为 null */
155
+ amount0Min: string | null
156
+ amount1Min: string | null
157
+ /**
158
+ * 一并领取的未领取手续费(V3 移除 / 领取;V4 修改流动性时自动领取)。V2 没有单独的手续费,为 '0'
159
+ */
160
+ fees0: string
161
+ fees1: string
162
+ fees0Formatted: string | null
163
+ fees1Formatted: string | null
164
+ /** 哪一边用主币支付 / 收取(V2 / V3 的包装币那一边开启 native 时;V4 主币池子的 token0);都不是时为 null */
165
+ native: 'token0' | 'token1' | null
166
+ /** 主交易附带的主币 */
167
+ value: string
168
+ /** 交易的截止时间(秒级时间戳) */
169
+ deadline: number
170
+ /** 按顺序发送;不需要授权时只有一笔 */
171
+ transactions: LiquidityTx[]
172
+ }
173
+
174
+ export interface LiquiditySendOptions extends LiquidityOptions, Omit<SendOptions, 'onUpdate'>, TronSendOptions {
175
+ /** 发送前检查余额够不够(添加时),默认 true */
176
+ checkBalance?: boolean
177
+ /** 每笔交易状态变化时回调(index 是在 plan.transactions 里的下标) */
178
+ onUpdate?: (result: LiquidityTxResult) => void
179
+ }
180
+
181
+ export interface LiquidityTxResult extends WriteResult {
182
+ kind: LiquidityTx['kind']
183
+ }
184
+
185
+ export interface LiquidityResult {
186
+ plan: LiquidityPlan
187
+ /** 与 plan.transactions 一一对应 */
188
+ results: LiquidityTxResult[]
189
+ /** 主交易(最后一笔)的状态和哈希 */
190
+ status: WriteStatus
191
+ hash: string | null
192
+ /** 新建的 V3 / V4 仓位的 tokenId(EVM 上主交易 confirmed 后从回执里读;其他情况为 null) */
193
+ tokenId: string | null
194
+ }
@@ -0,0 +1,371 @@
1
+ import { Interface } from 'ethers'
2
+
3
+ import { LP_PROTOCOLS, type LpPosition, type LpProtocol, type LpToken } from '../lp.js'
4
+ import { sqrtBigInt } from '../lpMath.js'
5
+ import { ZERO_ADDRESS, approvals, evm, isNative, makePlan, minus, out, parseAmount, protocolById, removeAmount, same, tokenInfo, type Ctx } from './common.js'
6
+ import type { IncreaseLiquidityParams, LiquidityPlan, LiquidityTx, NewPositionParams, PositionRef, RemoveLiquidityParams } from './types.js'
7
+
8
+ const routerInterface = new Interface([
9
+ 'function WETH() view returns (address)',
10
+ 'function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity)',
11
+ 'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity)',
12
+ 'function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256 amountA, uint256 amountB)',
13
+ 'function removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) returns (uint256 amountToken, uint256 amountETH)',
14
+ ])
15
+ const factoryInterface = new Interface(['function getPair(address tokenA, address tokenB) view returns (address)', 'function feeTo() view returns (address)'])
16
+ const pairInterface = new Interface([
17
+ 'function token0() view returns (address)',
18
+ 'function token1() view returns (address)',
19
+ 'function factory() view returns (address)',
20
+ 'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
21
+ 'function totalSupply() view returns (uint256)',
22
+ 'function balanceOf(address owner) view returns (uint256)',
23
+ 'function kLast() view returns (uint256)',
24
+ ])
25
+
26
+ /** UniswapV2Pair.MINIMUM_LIQUIDITY:第一次添加时锁定的 LP 代币 */
27
+ const MINIMUM_LIQUIDITY = 1000n
28
+
29
+ interface PairState {
30
+ protocol: LpProtocol
31
+ router: string
32
+ pair: string | null
33
+ token0: string
34
+ token1: string
35
+ reserve0: bigint
36
+ reserve1: bigint
37
+ /** LP 总量,已加上下一次添加 / 移除前会先给 feeTo 增发的协议手续费(_mintFee) */
38
+ totalSupply: bigint
39
+ balance: bigint
40
+ }
41
+
42
+ /**
43
+ * UniswapV2Pair._mintFee:协议手续费开启(factory.feeTo 不为零)时,每次添加 / 移除前先按 √k 的增长给 feeTo 增发 LP,
44
+ * 按份额算数量时要用增发后的总量。比例按 LpProtocol.protocolFee,没填时按 8/25(增发得多、算出的数量偏少,最小值更宽松,不会因此 revert)
45
+ */
46
+ function protocolFeeMint(protocol: LpProtocol, totalSupply: bigint, reserve0: bigint, reserve1: bigint, kLast: bigint): bigint {
47
+ if (kLast === 0n || totalSupply === 0n) {
48
+ return 0n
49
+ }
50
+ const rootK = sqrtBigInt(reserve0 * reserve1)
51
+ const rootKLast = sqrtBigInt(kLast)
52
+ if (rootK <= rootKLast) {
53
+ return 0n
54
+ }
55
+ return protocol.protocolFee === '1/6'
56
+ ? (totalSupply * (rootK - rootKLast)) / (rootK * 5n + rootKLast)
57
+ : (totalSupply * (rootK - rootKLast) * 8n) / (rootK * 17n + rootKLast * 8n)
58
+ }
59
+
60
+ function routerOf(protocol: LpProtocol): string {
61
+ if (!protocol.router) {
62
+ throw new Error(`Protocol ${protocol.id} has no router; set router on its LpProtocol`)
63
+ }
64
+ return evm(protocol.router)
65
+ }
66
+
67
+ /** 读池子:储备、总量、owner 持有的 LP;协议没给时按池子的 factory 找(Router)。已知协议时协议手续费的状态一并读 */
68
+ async function readPair(ctx: Ctx, pair: string, protocol?: LpProtocol): Promise<PairState> {
69
+ const c = ctx.provider.contract(pair, pairInterface)
70
+ const feeToCall = (factory: string) => ctx.provider.contract(factory, factoryInterface).feeTo()
71
+ // kLast / feeTo 有的分叉没有:一起用 tryAll 读,读不到按没开启协议手续费
72
+ const first = (await ctx.provider.tryAll([
73
+ c.token0(),
74
+ c.token1(),
75
+ c.factory(),
76
+ c.getReserves(),
77
+ c.totalSupply(),
78
+ c.balanceOf(ctx.owner),
79
+ c.kLast(),
80
+ ...(protocol?.factory ? [feeToCall(protocol.factory)] : []),
81
+ ])) as [string | null, string | null, string | null, { 0: bigint; 1: bigint } | null, bigint | null, bigint | null, bigint | null, (string | null)?]
82
+ const [token0, token1, factory, reserves, totalSupply, balance, kLast] = first
83
+ if (!token0 || !token1 || !factory || !reserves || totalSupply === null || balance === null) {
84
+ throw new Error(`${out(ctx, pair)} is not a V2 pool`)
85
+ }
86
+ const found =
87
+ protocol ??
88
+ [...(ctx.options.protocols ?? []), ...LP_PROTOCOLS].find((p) => p.chainId === ctx.chainId && p.version === 'v2' && p.factory && same(p.factory, factory))
89
+ if (!found) {
90
+ throw new Error(`Unknown V2 factory ${out(ctx, evm(factory))} of pool ${out(ctx, pair)}; pass its LpProtocol (with router) in options.protocols`)
91
+ }
92
+ if (protocol?.factory && !same(protocol.factory, factory)) {
93
+ throw new Error(`Pool ${out(ctx, pair)} does not belong to ${protocol.id}`)
94
+ }
95
+ const feeTo = protocol?.factory ? (first[7] ?? null) : ((await ctx.provider.tryAll([feeToCall(factory)])) as [string | null])[0]
96
+ const feeOn = feeTo !== null && evm(feeTo) !== ZERO_ADDRESS
97
+ const minted = feeOn ? protocolFeeMint(found, totalSupply, reserves[0], reserves[1], kLast ?? 0n) : 0n
98
+ return {
99
+ protocol: found,
100
+ router: routerOf(found),
101
+ pair,
102
+ token0: evm(token0),
103
+ token1: evm(token1),
104
+ reserve0: reserves[0],
105
+ reserve1: reserves[1],
106
+ totalSupply: totalSupply + minted,
107
+ balance,
108
+ }
109
+ }
110
+
111
+ /** 仓位上带的协议:LpProtocol 对象直接用;协议 id 按内置表 / options.protocols 找(getLpPositions 里不认识的工厂是 'v2:<工厂>',按工厂找) */
112
+ function refProtocol(ctx: Ctx, ref: PositionRef | LpPosition): LpProtocol | undefined {
113
+ if (!ref.protocol || (typeof ref.protocol === 'string' && ref.protocol.startsWith('v2:'))) {
114
+ return undefined
115
+ }
116
+ const protocol = protocolById(ctx, ref.protocol)
117
+ if (protocol.version !== 'v2') {
118
+ throw new Error(`${protocol.id} is not a V2 protocol`)
119
+ }
120
+ return protocol
121
+ }
122
+
123
+ const wethOf = async (ctx: Ctx, router: string) => evm((await ctx.provider.call(ctx.provider.contract(router, routerInterface).WETH())) as string)
124
+
125
+ /** UniswapV2Library.quote:按储备比例换算 */
126
+ const quote = (amountA: bigint, reserveA: bigint, reserveB: bigint) => (amountA * reserveB) / reserveA
127
+
128
+ export async function v2Add(ctx: Ctx, params: IncreaseLiquidityParams | NewPositionParams): Promise<LiquidityPlan> {
129
+ let state: PairState
130
+ /** 新建时 tokenA 是不是 token0;往已有仓位追加时为 null(数量直接按 token0 / token1) */
131
+ let aIs0: boolean | null = null
132
+ let native = ctx.options.native ?? false
133
+ /** Router 的包装币(用主币时才读) */
134
+ let weth: string | null = null
135
+
136
+ if ('position' in params) {
137
+ const ref = params.position as PositionRef | LpPosition
138
+ if (!ref.pool) {
139
+ throw new Error('V2 position needs pool')
140
+ }
141
+ state = await readPair(ctx, evm(ref.pool), refProtocol(ctx, ref))
142
+ } else {
143
+ const protocol = params.protocol as LpProtocol
144
+ const router = routerOf(protocol)
145
+ let a = evm(params.tokenA)
146
+ let b = evm(params.tokenB)
147
+ if (isNative(ctx, a) || isNative(ctx, b)) {
148
+ // 主币:换成 Router 的包装币,用主币支付
149
+ const wrapped = await wethOf(ctx, router)
150
+ a = isNative(ctx, a) ? wrapped : a
151
+ b = isNative(ctx, b) ? wrapped : b
152
+ weth = wrapped
153
+ native = true
154
+ }
155
+ if (a === b) {
156
+ throw new Error('tokenA and tokenB must be different')
157
+ }
158
+ if (!protocol.factory) {
159
+ throw new Error(`Protocol ${protocol.id} has no factory; set factory on its LpProtocol`)
160
+ }
161
+ const pair = evm((await ctx.provider.call(ctx.provider.contract(protocol.factory, factoryInterface).getPair(a, b))) as string)
162
+ if (pair === ZERO_ADDRESS) {
163
+ // 池子还不存在:Router 添加时会创建
164
+ const [token0, token1] = BigInt(a) < BigInt(b) ? [a, b] : [b, a]
165
+ state = { protocol, router, pair: null, token0, token1, reserve0: 0n, reserve1: 0n, totalSupply: 0n, balance: 0n }
166
+ } else {
167
+ state = await readPair(ctx, pair, protocol)
168
+ }
169
+ aIs0 = a === state.token0
170
+ }
171
+
172
+ const { router, token0, token1, reserve0, reserve1, totalSupply } = state
173
+ // 代币信息和(往已有仓位用主币追加时)包装币地址互不依赖,一起读
174
+ const [tokens, wrapped] = await Promise.all([tokenInfo(ctx, [token0, token1]), native && !weth ? wethOf(ctx, router) : Promise.resolve(weth)])
175
+ const [t0, t1] = tokens as [LpToken, LpToken]
176
+ weth = wrapped
177
+ let given0: bigint | undefined
178
+ let given1: bigint | undefined
179
+ if (aIs0 === null) {
180
+ const p = params as IncreaseLiquidityParams
181
+ given0 = parseAmount(p.amount0, p.amount0Formatted, t0, 'amount0')
182
+ given1 = parseAmount(p.amount1, p.amount1Formatted, t1, 'amount1')
183
+ } else {
184
+ const p = params as NewPositionParams
185
+ const amountA = parseAmount(p.amountA, p.amountAFormatted, aIs0 ? t0 : t1, 'amountA')
186
+ const amountB = parseAmount(p.amountB, p.amountBFormatted, aIs0 ? t1 : t0, 'amountB')
187
+ ;[given0, given1] = aIs0 ? [amountA, amountB] : [amountB, amountA]
188
+ }
189
+ if (given0 === undefined && given1 === undefined) {
190
+ throw new Error('Pass the amount of at least one token')
191
+ }
192
+
193
+ // 主币:其中一边必须是包装币
194
+ if (native) {
195
+ weth ??= await wethOf(ctx, router)
196
+ if (weth !== token0 && weth !== token1) {
197
+ throw new Error('native: true needs one side of the pool to be the wrapped native token')
198
+ }
199
+ }
200
+
201
+ // 传给 Router 的数量(desired):给了的按给的,另一边按储备比例
202
+ let desired0: bigint
203
+ let desired1: bigint
204
+ const empty = reserve0 === 0n && reserve1 === 0n
205
+ if (!empty && (reserve0 === 0n || reserve1 === 0n)) {
206
+ throw new Error(`Pool ${out(ctx, state.pair as string)} has one empty reserve; liquidity cannot be added at a meaningful price`)
207
+ }
208
+ if (empty) {
209
+ if (!given0 || !given1) {
210
+ throw new Error('The pool is empty: pass the amounts of both tokens (they set the initial price)')
211
+ }
212
+ ;[desired0, desired1] = [given0, given1]
213
+ } else {
214
+ desired0 = given0 ?? quote(given1 as bigint, reserve1, reserve0)
215
+ desired1 = given1 ?? quote(given0 as bigint, reserve0, reserve1)
216
+ }
217
+ // 实际用掉的数量:与 Router 的 _addLiquidity 一致(A 是 addLiquidity 的第一个代币;addLiquidityETH 的 A 是非包装币那一边)。
218
+ // 先按 A 算 B 的最优数量,超出 B 的 desired 时反过来按 B 算 A
219
+ let amount0 = desired0
220
+ let amount1 = desired1
221
+ if (!empty) {
222
+ const aIsToken0 = weth !== token0
223
+ const [dA, dB, rA, rB] = aIsToken0 ? [desired0, desired1, reserve0, reserve1] : [desired1, desired0, reserve1, reserve0]
224
+ const optimalB = quote(dA, rA, rB)
225
+ const [amountA, amountB] = optimalB <= dB ? [dA, optimalB] : [quote(dB, rB, rA), dB]
226
+ ;[amount0, amount1] = aIsToken0 ? [amountA, amountB] : [amountB, amountA]
227
+ }
228
+ if (amount0 === 0n || amount1 === 0n) {
229
+ throw new Error('Amount too small: one side rounds to 0')
230
+ }
231
+ const liquidity = totalSupply === 0n ? sqrtBigInt(amount0 * amount1) - MINIMUM_LIQUIDITY : minBig((amount0 * totalSupply) / reserve0, (amount1 * totalSupply) / reserve1)
232
+ if (liquidity <= 0n) {
233
+ throw new Error('Amount too small: no LP tokens would be minted')
234
+ }
235
+
236
+ // 最小值按实际会用掉的数量算(两边都给、比例与池子不同时,Router 会按较少的一边收取,那一边的最小值不能按给的数量算)
237
+ const min0 = empty ? desired0 : minus(amount0, ctx.slippage)
238
+ const min1 = empty ? desired1 : minus(amount1, ctx.slippage)
239
+ let main: LiquidityTx
240
+ let value = 0n
241
+ if (weth) {
242
+ const tokenIs0 = weth === token1
243
+ const token = tokenIs0 ? token0 : token1
244
+ // 附带的主币按 desired 给,没用完的 Router 会退回
245
+ value = tokenIs0 ? desired1 : desired0
246
+ main = {
247
+ kind: 'add',
248
+ to: out(ctx, router),
249
+ data: routerInterface.encodeFunctionData('addLiquidityETH', [
250
+ token,
251
+ tokenIs0 ? desired0 : desired1,
252
+ tokenIs0 ? min0 : min1,
253
+ tokenIs0 ? min1 : min0,
254
+ ctx.recipient,
255
+ ctx.deadline,
256
+ ]),
257
+ value: value.toString(),
258
+ }
259
+ } else {
260
+ main = {
261
+ kind: 'add',
262
+ to: out(ctx, router),
263
+ data: routerInterface.encodeFunctionData('addLiquidity', [token0, token1, desired0, desired1, min0, min1, ctx.recipient, ctx.deadline]),
264
+ value: '0',
265
+ }
266
+ }
267
+ const approve = await approvals(
268
+ ctx,
269
+ [
270
+ { token: token0, spender: router, amount: desired0 },
271
+ { token: token1, spender: router, amount: desired1 },
272
+ ].filter((i) => i.token !== weth),
273
+ )
274
+ return makePlan(ctx, {
275
+ action: 'add',
276
+ version: 'v2',
277
+ protocol: state.protocol.id,
278
+ protocolName: state.protocol.name,
279
+ pool: state.pair,
280
+ createPool: state.pair === null,
281
+ token0: t0,
282
+ token1: t1,
283
+ tokenId: null,
284
+ tickLower: null,
285
+ tickUpper: null,
286
+ tick: null,
287
+ price: null,
288
+ priceLower: null,
289
+ priceUpper: null,
290
+ liquidity: liquidity.toString(),
291
+ amount0: amount0.toString(),
292
+ amount1: amount1.toString(),
293
+ amount0Max: desired0.toString(),
294
+ amount1Max: desired1.toString(),
295
+ amount0Min: null,
296
+ amount1Min: null,
297
+ fees0: '0',
298
+ fees1: '0',
299
+ native: weth === null ? null : weth === token0 ? 'token0' : 'token1',
300
+ value: value.toString(),
301
+ transactions: [...approve, main],
302
+ })
303
+ }
304
+
305
+ export async function v2Remove(ctx: Ctx, params: RemoveLiquidityParams): Promise<LiquidityPlan> {
306
+ const ref = params.position
307
+ if (!ref.pool) {
308
+ throw new Error('V2 position needs pool')
309
+ }
310
+ const state = await readPair(ctx, evm(ref.pool), refProtocol(ctx, ref))
311
+ const { router, token0, token1, reserve0, reserve1, totalSupply, balance } = state
312
+ if (balance === 0n) {
313
+ throw new Error(`${out(ctx, ctx.owner)} holds no LP tokens of ${out(ctx, state.pair as string)}`)
314
+ }
315
+ const liquidity = removeAmount(balance, params.percent, params.liquidity)
316
+ const [t0, t1] = (await tokenInfo(ctx, [token0, token1])) as [LpToken, LpToken]
317
+ // 按份额取出(与 pair.burn 一致;总量已含先给 feeTo 增发的协议手续费)
318
+ const amount0 = (liquidity * reserve0) / totalSupply
319
+ const amount1 = (liquidity * reserve1) / totalSupply
320
+ const min0 = minus(amount0, ctx.slippage)
321
+ const min1 = minus(amount1, ctx.slippage)
322
+
323
+ let weth: string | null = null
324
+ if (ctx.options.native) {
325
+ weth = await wethOf(ctx, router)
326
+ if (weth !== token0 && weth !== token1) {
327
+ throw new Error('native: true needs one side of the pool to be the wrapped native token')
328
+ }
329
+ }
330
+ const tokenIs0 = weth === token1
331
+ const main: LiquidityTx = {
332
+ kind: 'remove',
333
+ to: out(ctx, router),
334
+ data: weth
335
+ ? routerInterface.encodeFunctionData('removeLiquidityETH', [tokenIs0 ? token0 : token1, liquidity, tokenIs0 ? min0 : min1, tokenIs0 ? min1 : min0, ctx.recipient, ctx.deadline])
336
+ : routerInterface.encodeFunctionData('removeLiquidity', [token0, token1, liquidity, min0, min1, ctx.recipient, ctx.deadline]),
337
+ value: '0',
338
+ }
339
+ const approve = await approvals(ctx, [{ token: state.pair as string, spender: router, amount: liquidity }])
340
+ return makePlan(ctx, {
341
+ action: 'remove',
342
+ version: 'v2',
343
+ protocol: state.protocol.id,
344
+ protocolName: state.protocol.name,
345
+ pool: state.pair,
346
+ createPool: false,
347
+ token0: t0,
348
+ token1: t1,
349
+ tokenId: null,
350
+ tickLower: null,
351
+ tickUpper: null,
352
+ tick: null,
353
+ price: null,
354
+ priceLower: null,
355
+ priceUpper: null,
356
+ liquidity: liquidity.toString(),
357
+ amount0: amount0.toString(),
358
+ amount1: amount1.toString(),
359
+ amount0Max: null,
360
+ amount1Max: null,
361
+ amount0Min: min0.toString(),
362
+ amount1Min: min1.toString(),
363
+ fees0: '0',
364
+ fees1: '0',
365
+ native: weth === null ? null : weth === token0 ? 'token0' : 'token1',
366
+ value: '0',
367
+ transactions: [...approve, main],
368
+ })
369
+ }
370
+
371
+ const minBig = (a: bigint, b: bigint) => (a < b ? a : b)