@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,314 @@
1
+ import { Interface, MaxUint256, ZeroAddress, getAddress, getBigInt, parseUnits, type BigNumberish } from 'ethers'
2
+
3
+ import { formatUnits } from '../erc20.js'
4
+ import { LP_PROTOCOLS, type LpPosition, type LpProtocol, type LpToken, type LpVersion } from '../lp.js'
5
+ import { sqrtPriceToPrice, tickToPrice } from '../lpMath.js'
6
+ import type { Provider } from '../provider.js'
7
+ import { isTronAddress, isTronChain, toEvmAddress, toTronAddress } from '../tron.js'
8
+ import type { LiquidityOptions, LiquidityPlan, LiquidityTx, PositionRef } from './types.js'
9
+
10
+ export const ZERO_ADDRESS = ZeroAddress
11
+ export { MAX_UINT128 } from '../lpMath.js'
12
+ const MAX_UINT160 = 2n ** 160n - 1n
13
+ /** Uniswap 的 Permit2(各 EVM 链上地址相同),V4 的仓位合约通过它收取代币 */
14
+ export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
15
+ /** Permit2 授权的有效期:30 天(与 Uniswap 网页一致) */
16
+ const PERMIT2_EXPIRATION = 30 * 24 * 3600
17
+
18
+ export const erc20Interface = new Interface([
19
+ 'function allowance(address owner, address spender) view returns (uint256)',
20
+ 'function approve(address spender, uint256 amount) returns (bool)',
21
+ ])
22
+ const permit2Interface = new Interface([
23
+ 'function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)',
24
+ 'function approve(address token, address spender, uint160 amount, uint48 expiration)',
25
+ ])
26
+
27
+ /** 一次构建用到的公共信息 */
28
+ export interface Ctx {
29
+ provider: Provider
30
+ chainId: number
31
+ tron: boolean
32
+ /** 0x 格式(ABI 编码用) */
33
+ owner: string
34
+ recipient: string
35
+ slippage: number
36
+ /** 截止时间(秒级时间戳) */
37
+ deadline: number
38
+ options: LiquidityOptions
39
+ }
40
+
41
+ /** 地址统一成 0x 的校验和格式(Tron 的 T 地址先转换) */
42
+ export function evm(address: string): string {
43
+ return getAddress(isTronAddress(address) ? toEvmAddress(address) : address)
44
+ }
45
+
46
+ export const same = (a: string, b: string) => evm(a) === evm(b)
47
+
48
+ /** 输出的地址:Tron 上转成 T 地址 */
49
+ export function out(ctx: Pick<Ctx, 'tron'>, address: string): string {
50
+ return ctx.tron ? toTronAddress(address) : address
51
+ }
52
+
53
+ export async function createCtx(provider: Provider, owner: string, options: LiquidityOptions): Promise<Ctx> {
54
+ const chainId = await provider.getChainId()
55
+ const slippage = options.slippage ?? 0.005
56
+ if (!(slippage >= 0 && slippage < 1)) {
57
+ throw new RangeError(`slippage must be in [0, 1): ${slippage}`)
58
+ }
59
+ const seconds = options.deadline ?? 1200
60
+ if (!(seconds > 0)) {
61
+ throw new RangeError(`deadline must be greater than 0: ${seconds}`)
62
+ }
63
+ return {
64
+ provider,
65
+ chainId,
66
+ tron: isTronChain(chainId),
67
+ owner: evm(owner),
68
+ recipient: evm(options.recipient ?? owner),
69
+ slippage,
70
+ deadline: Math.floor(Date.now() / 1000) + Math.floor(seconds),
71
+ options,
72
+ }
73
+ }
74
+
75
+ /** 是否是主币地址(NATIVE_TOKEN / 零地址,或 Provider 配置的主币地址) */
76
+ export function isNative(ctx: Ctx, address: string): boolean {
77
+ return evm(address) === ZERO_ADDRESS || ctx.provider.isNativeToken(address) || ctx.provider.isNativeToken(evm(address))
78
+ }
79
+
80
+ /** 代币信息(symbol / 精度);零地址是 V4 的主币 */
81
+ export async function tokenInfo(ctx: Ctx, addresses: readonly string[]): Promise<LpToken[]> {
82
+ const nativeAlias = ctx.provider.isNativeToken(ZERO_ADDRESS) ? undefined : ctx.provider.nativeTokens[0]
83
+ const query = addresses.map((a) => (a === ZERO_ADDRESS && nativeAlias !== undefined ? nativeAlias : a))
84
+ const details = await ctx.provider.tokens(query, { fields: ['symbol', 'decimals'] })
85
+ return addresses.map((address, i) => {
86
+ const d = details[i]
87
+ return { address: out(ctx, address), native: d?.native ?? false, symbol: d?.symbol ?? null, decimals: d?.decimals ?? null }
88
+ })
89
+ }
90
+
91
+ export function fmt(value: bigint, token: LpToken): string | null {
92
+ return token.decimals === null ? null : formatUnits(value, token.decimals)
93
+ }
94
+
95
+ /** 解析输入的数量:amount(最小单位)和 formatted 二选一,都没给时返回 undefined */
96
+ export function parseAmount(raw: BigNumberish | undefined, formatted: string | undefined, token: LpToken, name: string): bigint | undefined {
97
+ if (raw !== undefined && formatted !== undefined) {
98
+ throw new Error(`Pass either ${name} or ${name}Formatted, not both`)
99
+ }
100
+ let value: bigint
101
+ if (formatted !== undefined) {
102
+ if (token.decimals === null) {
103
+ throw new Error(`Cannot read the decimals of ${token.address}; pass ${name} in raw units instead of ${name}Formatted`)
104
+ }
105
+ value = parseUnits(formatted, token.decimals)
106
+ } else if (raw !== undefined) {
107
+ value = getBigInt(raw)
108
+ } else {
109
+ return undefined
110
+ }
111
+ if (value < 0n) {
112
+ throw new RangeError(`${name} must not be negative`)
113
+ }
114
+ return value
115
+ }
116
+
117
+ /** 按滑点向下取:value × (1 − slippage) */
118
+ export function minus(value: bigint, slippage: number): bigint {
119
+ return (value * (1_000_000n - BigInt(Math.round(slippage * 1e6)))) / 1_000_000n
120
+ }
121
+
122
+ /** 移除的数量:percent(百分数)或 liquidity,二选一,默认全部 */
123
+ export function removeAmount(total: bigint, percent: number | undefined, liquidity: BigNumberish | undefined): bigint {
124
+ if (percent !== undefined && liquidity !== undefined) {
125
+ throw new Error('Pass either percent or liquidity, not both')
126
+ }
127
+ if (liquidity !== undefined) {
128
+ const value = getBigInt(liquidity)
129
+ if (value <= 0n || value > total) {
130
+ throw new RangeError(`liquidity must be in (0, ${total}]: ${value}`)
131
+ }
132
+ return value
133
+ }
134
+ const p = percent ?? 100
135
+ if (!(p > 0 && p <= 100)) {
136
+ throw new RangeError(`percent must be in (0, 100]: ${p}`)
137
+ }
138
+ const amount = p === 100 ? total : (total * BigInt(Math.round(p * 10_000))) / 1_000_000n
139
+ if (amount === 0n) {
140
+ throw new RangeError(`${p}% of ${total} rounds to 0`)
141
+ }
142
+ return amount
143
+ }
144
+
145
+ /** 选出协议:自定义的优先,再找内置表 */
146
+ function candidates(ctx: Ctx): LpProtocol[] {
147
+ return [...(ctx.options.protocols ?? []).filter((p) => p.chainId === ctx.chainId), ...LP_PROTOCOLS.filter((p) => p.chainId === ctx.chainId)]
148
+ }
149
+
150
+ export function protocolById(ctx: Ctx, protocol: string | LpProtocol): LpProtocol {
151
+ if (typeof protocol !== 'string') {
152
+ if (protocol.chainId !== ctx.chainId) {
153
+ throw new Error(`Protocol ${protocol.id} is on chain ${protocol.chainId}, not ${ctx.chainId}`)
154
+ }
155
+ return protocol
156
+ }
157
+ const found = candidates(ctx).find((p) => p.id === protocol)
158
+ if (!found) {
159
+ throw new Error(`Unknown LP protocol ${protocol} on chain ${ctx.chainId}; pass an LpProtocol object (or options.protocols) for protocols that are not built in`)
160
+ }
161
+ return found
162
+ }
163
+
164
+ /** V3 / V4 仓位所属的协议:按 positionManager 找,没有时按协议 id */
165
+ export function concentratedProtocol(ctx: Ctx, ref: PositionRef | LpPosition): LpProtocol {
166
+ const pm = 'positionManager' in ref && ref.positionManager ? evm(ref.positionManager) : null
167
+ if (pm) {
168
+ const fromRef = ref.protocol && typeof ref.protocol !== 'string' ? [ref.protocol] : []
169
+ const found = [...fromRef, ...candidates(ctx)].find((p) => (p.version === 'v3' || p.version === 'v4') && p.positionManager && same(p.positionManager, pm))
170
+ if (!found) {
171
+ throw new Error(`Unknown position manager ${pm} on chain ${ctx.chainId}; pass its LpProtocol in options.protocols`)
172
+ }
173
+ return found
174
+ }
175
+ if (!ref.protocol) {
176
+ throw new Error('position needs positionManager or protocol')
177
+ }
178
+ const found = protocolById(ctx, ref.protocol)
179
+ if (found.version === 'v2') {
180
+ throw new Error(`${found.id} is a V2 protocol; V2 positions are identified by pool`)
181
+ }
182
+ return found
183
+ }
184
+
185
+ /** 仓位是哪个版本:LpPosition 自带;PositionRef 有 tokenId 的看协议,否则是 V2 */
186
+ export function positionVersion(ctx: Ctx, ref: PositionRef | LpPosition): LpVersion {
187
+ if ('version' in ref && (ref.version === 'v2' || ref.version === 'v3' || ref.version === 'v4')) {
188
+ return ref.version
189
+ }
190
+ if (!('tokenId' in ref) || ref.tokenId === undefined) {
191
+ if (!ref.pool) {
192
+ throw new Error('position needs pool (V2) or tokenId (V3 / V4)')
193
+ }
194
+ // 传了 V3 / V4 的协议却没有 tokenId:说清楚,不要当成 V2 的池子去读
195
+ if (ref.protocol && (typeof ref.protocol === 'string' ? protocolById(ctx, ref.protocol) : ref.protocol).version !== 'v2') {
196
+ throw new Error('position.tokenId is required for V3 / V4 positions')
197
+ }
198
+ return 'v2'
199
+ }
200
+ return concentratedProtocol(ctx, ref).version
201
+ }
202
+
203
+ export function tokenIdOf(ref: PositionRef | LpPosition): bigint {
204
+ if (!('tokenId' in ref) || ref.tokenId === undefined) {
205
+ throw new Error('position.tokenId is required')
206
+ }
207
+ if (typeof ref.tokenId === 'number' && !Number.isSafeInteger(ref.tokenId)) {
208
+ throw new RangeError(`tokenId ${ref.tokenId} is not a safe integer; pass it as a string or bigint`)
209
+ }
210
+ return getBigInt(ref.tokenId)
211
+ }
212
+
213
+ /**
214
+ * 要求先把额度归零才能改成另一个非零值的代币(chainId:小写地址)。只对它们先 approve 0:
215
+ * 有的代币(如 Ethereum 上的 BNB)反过来不允许 approve 0
216
+ */
217
+ const RESET_FIRST = new Set([
218
+ '1:0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
219
+ '1:0xdd974d5c2e2928dea5f71b9825b8b646686bd200', // KNC(旧版)
220
+ ])
221
+
222
+ /** ERC20 授权:额度不够时 approve(USDT 等已有非零额度时先 approve 0)。amount 为 0 的不需要 */
223
+ export async function approvals(ctx: Ctx, items: ReadonlyArray<{ token: string; spender: string; amount: bigint }>): Promise<LiquidityTx[]> {
224
+ const needed = items.filter((i) => i.amount > 0n)
225
+ if (!needed.length) {
226
+ return []
227
+ }
228
+ const current = await ctx.provider.all(needed.map((i) => ctx.provider.contract(i.token, erc20Interface).allowance(ctx.owner, i.spender)))
229
+ return needed.flatMap((item, k): LiquidityTx[] => {
230
+ const allowance = (current as bigint[])[k] as bigint
231
+ if (allowance >= item.amount) {
232
+ return []
233
+ }
234
+ const amount = ctx.options.approve === 'max' ? MaxUint256 : item.amount
235
+ const tx = (value: bigint): LiquidityTx => ({
236
+ kind: 'approve',
237
+ to: out(ctx, item.token),
238
+ data: erc20Interface.encodeFunctionData('approve', [item.spender, value]),
239
+ value: '0',
240
+ token: out(ctx, item.token),
241
+ spender: out(ctx, item.spender),
242
+ amount: value.toString(),
243
+ })
244
+ return allowance > 0n && RESET_FIRST.has(`${ctx.chainId}:${item.token.toLowerCase()}`) ? [tx(0n), tx(amount)] : [tx(amount)]
245
+ })
246
+ }
247
+
248
+ /** V4:代币先授权给 Permit2,再在 Permit2 里授权给仓位合约(额度不够或已过期时) */
249
+ export async function permit2Approvals(ctx: Ctx, spender: string, items: ReadonlyArray<{ token: string; amount: bigint }>): Promise<LiquidityTx[]> {
250
+ const needed = items.filter((i) => i.amount > 0n)
251
+ if (!needed.length) {
252
+ return []
253
+ }
254
+ const erc20 = await approvals(
255
+ ctx,
256
+ needed.map((i) => ({ token: i.token, spender: PERMIT2_ADDRESS, amount: i.amount })),
257
+ )
258
+ const permits = (await ctx.provider.all(needed.map((i) => ctx.provider.contract(PERMIT2_ADDRESS, permit2Interface).allowance(ctx.owner, i.token, spender)))) as Array<{ 0: bigint; 1: bigint }>
259
+ const now = Math.floor(Date.now() / 1000)
260
+ const permit2 = needed.flatMap((item, k): LiquidityTx[] => {
261
+ const [amount, expiration] = [permits[k]?.[0] ?? 0n, Number(permits[k]?.[1] ?? 0n)]
262
+ // 额度够、并且在交易截止时间之后才过期:不用再授权
263
+ if (amount >= item.amount && expiration >= ctx.deadline) {
264
+ return []
265
+ }
266
+ // 过期重新授权时保留原来更大的额度(如之前授权过无限额度)
267
+ const value = ctx.options.approve === 'max' ? MAX_UINT160 : amount > item.amount ? amount : item.amount
268
+ return [
269
+ {
270
+ kind: 'permit2',
271
+ to: PERMIT2_ADDRESS,
272
+ data: permit2Interface.encodeFunctionData('approve', [item.token, spender, value, Math.max(now, ctx.deadline) + PERMIT2_EXPIRATION]),
273
+ value: '0',
274
+ token: item.token,
275
+ spender,
276
+ amount: value.toString(),
277
+ },
278
+ ]
279
+ })
280
+ return [...erc20, ...permit2]
281
+ }
282
+
283
+ /** 计划里的价格字段(token1 / token0,按精度换算) */
284
+ export function priceFields(token0: LpToken, token1: LpToken, sqrtPriceX96: bigint | null, tickLower: number, tickUpper: number) {
285
+ if (token0.decimals === null || token1.decimals === null) {
286
+ return { price: null, priceLower: null, priceUpper: null }
287
+ }
288
+ return {
289
+ price: sqrtPriceX96 ? sqrtPriceToPrice(sqrtPriceX96, token0.decimals, token1.decimals) : null,
290
+ priceLower: tickToPrice(tickLower, token0.decimals, token1.decimals),
291
+ priceUpper: tickToPrice(tickUpper, token0.decimals, token1.decimals),
292
+ }
293
+ }
294
+
295
+ type PlanInput = Omit<LiquidityPlan, 'chainId' | 'owner' | 'recipient' | 'deadline' | 'amount0Formatted' | 'amount1Formatted' | 'fees0Formatted' | 'fees1Formatted' | 'pool'> & {
296
+ pool: string | null
297
+ }
298
+
299
+ /** 补上公共字段、格式化数量 */
300
+ export function makePlan(ctx: Ctx, input: PlanInput): LiquidityPlan {
301
+ const { token0, token1 } = input
302
+ return {
303
+ ...input,
304
+ chainId: ctx.chainId,
305
+ pool: input.pool === null || input.version === 'v4' ? input.pool : out(ctx, input.pool),
306
+ owner: out(ctx, ctx.owner),
307
+ recipient: out(ctx, ctx.recipient),
308
+ amount0Formatted: fmt(BigInt(input.amount0), token0),
309
+ amount1Formatted: fmt(BigInt(input.amount1), token1),
310
+ fees0Formatted: fmt(BigInt(input.fees0), token0),
311
+ fees1Formatted: fmt(BigInt(input.fees1), token1),
312
+ deadline: ctx.deadline,
313
+ }
314
+ }
@@ -0,0 +1,121 @@
1
+ import type { LpToken } from '../lp.js'
2
+ import {
3
+ MAX_TICK,
4
+ MIN_TICK,
5
+ clampedTickAtSqrtRatio,
6
+ getLiquidityForAmount0,
7
+ getLiquidityForAmount1,
8
+ getLiquidityForAmounts,
9
+ getMintAmounts,
10
+ getSqrtRatioAtTick,
11
+ nearestUsableTick,
12
+ parseDecimal,
13
+ ratioToSqrtPriceX96,
14
+ sqrtPriceBounds,
15
+ toUint128,
16
+ } from '../lpMath.js'
17
+ import type { NewPositionParams } from './types.js'
18
+
19
+ /**
20
+ * 价格(tokenB / tokenA,按精度换算)→ 池子的 sqrtPriceX96(token1 / token0)。aIs0:tokenA 是不是 token0
21
+ */
22
+ export function sqrtPriceOf(price: string | number, aIs0: boolean, token0: LpToken, token1: LpToken, name: string): bigint {
23
+ if (token0.decimals === null || token1.decimals === null) {
24
+ throw new Error(`Cannot read token decimals to convert ${name}; pass ticks instead`)
25
+ }
26
+ const { num, den } = parseDecimal(price)
27
+ if (num === 0n) {
28
+ throw new RangeError(`${name} must be greater than 0`)
29
+ }
30
+ // tokenA 是 token1 时取倒数
31
+ return aIs0 ? ratioToSqrtPriceX96(num, den, token0.decimals, token1.decimals) : ratioToSqrtPriceX96(den, num, token0.decimals, token1.decimals)
32
+ }
33
+
34
+ /** 新建仓位的 tick 区间:fullRange / tickLower + tickUpper / priceLower + priceUpper 三选一 */
35
+ export function resolveRange(params: NewPositionParams, aIs0: boolean, token0: LpToken, token1: LpToken, tickSpacing: number): [number, number] {
36
+ const byTicks = params.tickLower !== undefined || params.tickUpper !== undefined
37
+ const byPrices = params.priceLower !== undefined || params.priceUpper !== undefined
38
+ if (Number(!!params.fullRange) + Number(byTicks) + Number(byPrices) !== 1) {
39
+ throw new Error('Pass exactly one of fullRange, tickLower + tickUpper, or priceLower + priceUpper')
40
+ }
41
+ let lower: number
42
+ let upper: number
43
+ if (params.fullRange) {
44
+ lower = Math.ceil(MIN_TICK / tickSpacing) * tickSpacing
45
+ upper = Math.floor(MAX_TICK / tickSpacing) * tickSpacing
46
+ } else if (byTicks) {
47
+ if (params.tickLower === undefined || params.tickUpper === undefined) {
48
+ throw new Error('Pass both tickLower and tickUpper')
49
+ }
50
+ lower = params.tickLower
51
+ upper = params.tickUpper
52
+ for (const tick of [lower, upper]) {
53
+ if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK || tick % tickSpacing !== 0) {
54
+ throw new RangeError(`tick ${tick} must be an integer multiple of tickSpacing ${tickSpacing} within [${MIN_TICK}, ${MAX_TICK}]`)
55
+ }
56
+ }
57
+ } else {
58
+ if (params.priceLower === undefined || params.priceUpper === undefined) {
59
+ throw new Error('Pass both priceLower and priceUpper')
60
+ }
61
+ // tokenA 是 token1 时价格取倒数,区间两端也随之对调;下面按大小排序
62
+ const ticks = [params.priceLower, params.priceUpper].map((price, i) => {
63
+ return nearestUsableTick(clampedTickAtSqrtRatio(sqrtPriceOf(price, aIs0, token0, token1, i ? 'priceUpper' : 'priceLower')), tickSpacing)
64
+ }) as [number, number]
65
+ ;[lower, upper] = ticks[0] <= ticks[1] ? ticks : [ticks[1], ticks[0]]
66
+ }
67
+ if (lower >= upper) {
68
+ throw new RangeError(`The price range is too narrow: tickLower ${lower} must be below tickUpper ${upper} (tickSpacing ${tickSpacing})`)
69
+ }
70
+ return [lower, upper]
71
+ }
72
+
73
+ /**
74
+ * 按输入的数量算添加的流动性:两边都给时取较少的一边;只给一边时另一边按当前价格算。
75
+ * 价格在区间外时只需要一种代币,给错了另一种会报错
76
+ */
77
+ export function liquidityFor(sqrtPrice: bigint, tickLower: number, tickUpper: number, amount0: bigint | undefined, amount1: bigint | undefined, token0: LpToken, token1: LpToken): bigint {
78
+ const sqrtA = getSqrtRatioAtTick(tickLower)
79
+ const sqrtB = getSqrtRatioAtTick(tickUpper)
80
+ const name = (t: LpToken) => t.symbol ?? t.address
81
+ if (amount0 === undefined && amount1 === undefined) {
82
+ throw new Error('Pass the amount of at least one token')
83
+ }
84
+ // 价格在区间外时只需要一种代币:给的是另一种(或需要的那种是 0)时说明要给哪种
85
+ if (sqrtPrice >= sqrtB && !amount1) {
86
+ throw new Error(`The current price is above the range: the position takes only ${name(token1)} (token1); pass its amount instead`)
87
+ }
88
+ if (sqrtPrice <= sqrtA && !amount0) {
89
+ throw new Error(`The current price is below the range: the position takes only ${name(token0)} (token0); pass its amount instead`)
90
+ }
91
+ let liquidity: bigint
92
+ if (amount0 !== undefined && amount1 !== undefined) {
93
+ liquidity = getLiquidityForAmounts(sqrtPrice, sqrtA, sqrtB, amount0, amount1)
94
+ } else if (amount0 !== undefined) {
95
+ liquidity = getLiquidityForAmount0(sqrtPrice > sqrtA ? sqrtPrice : sqrtA, sqrtB, amount0)
96
+ } else {
97
+ liquidity = getLiquidityForAmount1(sqrtA, sqrtPrice < sqrtB ? sqrtPrice : sqrtB, amount1 as bigint)
98
+ }
99
+ if (liquidity === 0n) {
100
+ throw new Error('Amount too small: the liquidity rounds to 0')
101
+ }
102
+ return toUint128(liquidity)
103
+ }
104
+
105
+ /**
106
+ * V3:按传给仓位合约的数量(desired),价格在滑点范围内变动时至少会用掉多少(amountMin)。
107
+ * 合约按当时的价格重新算流动性:token0 在价格最高时用得最少,token1 在最低时;取三个价格下的最小值
108
+ */
109
+ export function v3MinAmounts(sqrtPrice: bigint, tickLower: number, tickUpper: number, desired0: bigint, desired1: bigint, slippage: number): [bigint, bigint] {
110
+ const sqrtA = getSqrtRatioAtTick(tickLower)
111
+ const sqrtB = getSqrtRatioAtTick(tickUpper)
112
+ const [lower, upper] = sqrtPriceBounds(sqrtPrice, slippage)
113
+ let min0: bigint | null = null
114
+ let min1: bigint | null = null
115
+ for (const s of [lower, sqrtPrice, upper]) {
116
+ const [a0, a1] = getMintAmounts(s, sqrtA, sqrtB, getLiquidityForAmounts(s, sqrtA, sqrtB, desired0, desired1))
117
+ min0 = min0 === null || a0 < min0 ? a0 : min0
118
+ min1 = min1 === null || a1 < min1 ? a1 : min1
119
+ }
120
+ return [min0 as bigint, min1 as bigint]
121
+ }
@@ -0,0 +1,171 @@
1
+ import { Interface } from 'ethers'
2
+
3
+ import type { Provider } from '../provider.js'
4
+ import { batchCall, tronModule } from '../write/batch.js'
5
+ import { WriteRpc } from '../write/rpc.js'
6
+ import { resolveSigner, type SignerInput } from '../write/signer.js'
7
+ import { ZERO_ADDRESS, createCtx, evm, positionVersion, protocolById } from './common.js'
8
+ import type {
9
+ AddLiquidityParams,
10
+ CollectFeesParams,
11
+ LiquidityOptions,
12
+ LiquidityPlan,
13
+ LiquidityResult,
14
+ LiquiditySendOptions,
15
+ LiquidityTx,
16
+ LiquidityTxResult,
17
+ RemoveLiquidityParams,
18
+ } from './types.js'
19
+ import { v2Add, v2Remove } from './v2.js'
20
+ import { v3Add, v3Remove } from './v3.js'
21
+ import { v4Add, v4Remove } from './v4.js'
22
+
23
+ /**
24
+ * 添加流动性的计划(不签名、不发送):新建仓位,或往已有仓位追加。返回预计的数量和要按顺序发送的交易(授权在前)。
25
+ * 只做读取,也可以当作报价:输入一边的数量,看另一边要多少、能拿到多少流动性
26
+ *
27
+ * ```ts
28
+ * // 新建 V3 仓位:价格区间按 tokenB / tokenA,给一边的数量
29
+ * const plan = await buildAddLiquidity(multi, owner, { protocol: 'pancakeswap-v3', tokenA: WBNB, tokenB: USDT, fee: 500, priceLower: 550, priceUpper: 650, amountAFormatted: '1' })
30
+ * plan.amount1Formatted // 需要的 USDT
31
+ * plan.transactions // [approve…, add],可以交给钱包逐笔发送
32
+ * // 往已有仓位追加(position 可以直接用 getLpPositions 的结果)
33
+ * await buildAddLiquidity(multi, owner, { position, amount0Formatted: '0.5' })
34
+ * ```
35
+ */
36
+ export async function buildAddLiquidity(provider: Provider, owner: string, params: AddLiquidityParams, options: LiquidityOptions = {}): Promise<LiquidityPlan> {
37
+ const ctx = await createCtx(provider, owner, options)
38
+ if ('position' in params) {
39
+ const version = positionVersion(ctx, params.position)
40
+ return version === 'v2' ? v2Add(ctx, params) : version === 'v3' ? v3Add(ctx, params) : v4Add(ctx, params)
41
+ }
42
+ const protocol = protocolById(ctx, params.protocol)
43
+ const resolved = { ...params, protocol }
44
+ return protocol.version === 'v2' ? v2Add(ctx, resolved) : protocol.version === 'v3' ? v3Add(ctx, resolved) : v4Add(ctx, resolved)
45
+ }
46
+
47
+ /**
48
+ * 移除流动性的计划(不签名、不发送):按比例或指定数量移除,V3 / V4 同时领取全部未领取的手续费。
49
+ *
50
+ * ```ts
51
+ * await buildRemoveLiquidity(multi, owner, { position, percent: 50 })
52
+ * await buildRemoveLiquidity(multi, owner, { position, burn: true }) // 全部移除并销毁 NFT
53
+ * ```
54
+ */
55
+ export async function buildRemoveLiquidity(provider: Provider, owner: string, params: RemoveLiquidityParams, options: LiquidityOptions = {}): Promise<LiquidityPlan> {
56
+ const ctx = await createCtx(provider, owner, options)
57
+ const version = positionVersion(ctx, params.position)
58
+ return version === 'v2' ? v2Remove(ctx, params) : version === 'v3' ? v3Remove(ctx, params) : v4Remove(ctx, params)
59
+ }
60
+
61
+ /** 只领取 V3 / V4 仓位的手续费(不动流动性)的计划 */
62
+ export async function buildCollectFees(provider: Provider, owner: string, params: CollectFeesParams, options: LiquidityOptions = {}): Promise<LiquidityPlan> {
63
+ const ctx = await createCtx(provider, owner, options)
64
+ const version = positionVersion(ctx, params.position)
65
+ if (version === 'v2') {
66
+ throw new Error('V2 has no separate fees: they are part of the LP tokens, remove liquidity to get them')
67
+ }
68
+ return version === 'v3' ? v3Remove(ctx, { position: params.position }, true) : v4Remove(ctx, { position: params.position }, true)
69
+ }
70
+
71
+ /**
72
+ * 添加流动性:构建计划(同 buildAddLiquidity)后用 from 签名发送,授权不够时先发授权交易。
73
+ * from 与 /write 的 batchCall 相同:私钥、ethers Signer、浏览器钱包(Tron 上也可以是 tronWeb / adapter)
74
+ *
75
+ * ```ts
76
+ * const { status, tokenId, plan } = await addLiquidity(multi, privateKey, { protocol: 'uniswap-v3', tokenA: NATIVE_TOKEN, tokenB: USDC, fee: 500, fullRange: true, amountAFormatted: '0.1' })
77
+ * ```
78
+ */
79
+ export async function addLiquidity(provider: Provider, from: SignerInput, params: AddLiquidityParams, options: LiquiditySendOptions = {}): Promise<LiquidityResult> {
80
+ const plan = await buildAddLiquidity(provider, await signerAddress(provider, from), params, options)
81
+ if (options.checkBalance ?? true) {
82
+ await checkBalance(provider, plan)
83
+ }
84
+ return execute(provider, from, plan, options)
85
+ }
86
+
87
+ /** 移除流动性:构建计划(同 buildRemoveLiquidity)后用 from 签名发送 */
88
+ export async function removeLiquidity(provider: Provider, from: SignerInput, params: RemoveLiquidityParams, options: LiquiditySendOptions = {}): Promise<LiquidityResult> {
89
+ const plan = await buildRemoveLiquidity(provider, await signerAddress(provider, from), params, options)
90
+ return execute(provider, from, plan, options)
91
+ }
92
+
93
+ /** 领取 V3 / V4 仓位的手续费 */
94
+ export async function collectFees(provider: Provider, from: SignerInput, params: CollectFeesParams, options: LiquiditySendOptions = {}): Promise<LiquidityResult> {
95
+ const plan = await buildCollectFees(provider, await signerAddress(provider, from), params, options)
96
+ return execute(provider, from, plan, options)
97
+ }
98
+
99
+ /** 签名器的地址(Tron 上为 T 地址),不发交易 */
100
+ async function signerAddress(provider: Provider, from: SignerInput): Promise<string> {
101
+ const tron = await tronModule(provider)
102
+ return tron ? tron.tronSignerAddress(from) : (await resolveSigner(from)).address
103
+ }
104
+
105
+ /** 添加前检查余额:代币按预计数量,主币按附带的 value(不含手续费) */
106
+ async function checkBalance(provider: Provider, plan: LiquidityPlan): Promise<void> {
107
+ const needs: Array<[string, bigint]> = []
108
+ // V4 追加时先自动领取手续费、与存入相抵:只需要差额
109
+ const offset = (amount: string, fees: string) => {
110
+ const need = BigInt(amount) - (plan.version === 'v4' && plan.tokenId !== null ? BigInt(fees) : 0n)
111
+ return need > 0n ? need : 0n
112
+ }
113
+ if (plan.native !== 'token0') needs.push([plan.token0.address, offset(plan.amount0, plan.fees0)])
114
+ if (plan.native !== 'token1') needs.push([plan.token1.address, offset(plan.amount1, plan.fees1)])
115
+ if (BigInt(plan.value) > 0n) needs.push([provider.nativeTokens[0] ?? ZERO_ADDRESS, BigInt(plan.value)])
116
+ const list = needs.filter(([, amount]) => amount > 0n)
117
+ if (!list.length) {
118
+ return
119
+ }
120
+ const balances = await provider.balances(plan.owner, list.map(([token]) => token))
121
+ list.forEach(([token, amount], i) => {
122
+ const b = balances[i]
123
+ // 读不到余额的不拦(预估 gas 时也会失败)
124
+ if (b?.success && BigInt(b.balance) < amount) {
125
+ throw new Error(`Insufficient balance of ${token}: need ${amount}, have ${b.balance}`)
126
+ }
127
+ })
128
+ }
129
+
130
+ const transferTopic = new Interface(['event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)']).getEvent('Transfer')?.topicHash as string
131
+
132
+ async function execute(provider: Provider, from: SignerInput, plan: LiquidityPlan, options: LiquiditySendOptions): Promise<LiquidityResult> {
133
+ const { transactions } = plan
134
+ const results = (
135
+ await batchCall(
136
+ provider,
137
+ transactions.map((tx) => ({ from, to: tx.to, data: tx.data, value: BigInt(tx.value) })),
138
+ {
139
+ ...options,
140
+ mode: 'tx',
141
+ // 授权失败时不发主交易
142
+ continueOnError: false,
143
+ onUpdate: options.onUpdate ? (r) => options.onUpdate?.({ ...r, kind: (transactions[r.index] as LiquidityTx).kind }) : undefined,
144
+ },
145
+ )
146
+ ).map((r, i): LiquidityTxResult => ({ ...r, kind: (transactions[i] as LiquidityTx).kind }))
147
+ const main = results[results.length - 1] as LiquidityTxResult
148
+ let tokenId: string | null = plan.tokenId
149
+ if (plan.action === 'add' && plan.tokenId === null && plan.version !== 'v2') {
150
+ tokenId = main.status === 'confirmed' && main.hash ? await mintedTokenId(provider, plan, main.hash) : null
151
+ }
152
+ return { plan, results, status: main.status, hash: main.hash, tokenId }
153
+ }
154
+
155
+ /** 新建仓位的 tokenId:主交易回执里仓位合约从零地址转出的 NFT(Tron 上不读,为 null) */
156
+ async function mintedTokenId(provider: Provider, plan: LiquidityPlan, hash: string): Promise<string | null> {
157
+ try {
158
+ if (await tronModule(provider)) {
159
+ return null
160
+ }
161
+ const receipt = await (await WriteRpc.from(provider)).receipt(hash)
162
+ const pm = evm((plan.transactions[plan.transactions.length - 1] as { to: string }).to)
163
+ const log = receipt?.logs.find(
164
+ (l) => l.topics.length === 4 && l.topics[0] === transferTopic && BigInt(l.topics[1] as string) === 0n && evm(l.address) === pm,
165
+ )
166
+ return log ? BigInt(log.topics[3] as string).toString() : null
167
+ } catch {
168
+ return null // 读回执失败不影响结果(交易已确认)
169
+ }
170
+ }
171
+