@w3lib/evm-batch-call 0.5.0 → 0.7.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 (160) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +240 -4
  3. package/contracts/TokenProbe.sol +103 -0
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js +114 -3
  6. package/dist/aggregate.js.map +1 -1
  7. package/dist/approvals.d.ts +169 -0
  8. package/dist/approvals.d.ts.map +1 -0
  9. package/dist/approvals.js +310 -0
  10. package/dist/approvals.js.map +1 -0
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +4 -0
  13. package/dist/chains.js.map +1 -1
  14. package/dist/fees.d.ts +97 -0
  15. package/dist/fees.d.ts.map +1 -0
  16. package/dist/fees.js +115 -0
  17. package/dist/fees.js.map +1 -0
  18. package/dist/http.d.ts +10 -0
  19. package/dist/http.d.ts.map +1 -0
  20. package/dist/http.js +30 -0
  21. package/dist/http.js.map +1 -0
  22. package/dist/liquidity/common.d.ts +72 -0
  23. package/dist/liquidity/common.d.ts.map +1 -0
  24. package/dist/liquidity/common.js +271 -0
  25. package/dist/liquidity/common.js.map +1 -0
  26. package/dist/liquidity/concentrated.d.ts +19 -0
  27. package/dist/liquidity/concentrated.d.ts.map +1 -0
  28. package/dist/liquidity/concentrated.js +106 -0
  29. package/dist/liquidity/concentrated.js.map +1 -0
  30. package/dist/liquidity/index.d.ts +42 -0
  31. package/dist/liquidity/index.d.ts.map +1 -0
  32. package/dist/liquidity/index.js +143 -0
  33. package/dist/liquidity/index.js.map +1 -0
  34. package/dist/liquidity/types.d.ts +182 -0
  35. package/dist/liquidity/types.d.ts.map +1 -0
  36. package/dist/liquidity/types.js +2 -0
  37. package/dist/liquidity/types.js.map +1 -0
  38. package/dist/liquidity/v2.d.ts +5 -0
  39. package/dist/liquidity/v2.d.ts.map +1 -0
  40. package/dist/liquidity/v2.js +342 -0
  41. package/dist/liquidity/v2.js.map +1 -0
  42. package/dist/liquidity/v3.d.ts +6 -0
  43. package/dist/liquidity/v3.d.ts.map +1 -0
  44. package/dist/liquidity/v3.js +310 -0
  45. package/dist/liquidity/v3.js.map +1 -0
  46. package/dist/liquidity/v4.d.ts +5 -0
  47. package/dist/liquidity/v4.d.ts.map +1 -0
  48. package/dist/liquidity/v4.js +279 -0
  49. package/dist/liquidity/v4.js.map +1 -0
  50. package/dist/lp.d.ts +10 -2
  51. package/dist/lp.d.ts.map +1 -1
  52. package/dist/lp.js +31 -19
  53. package/dist/lp.js.map +1 -1
  54. package/dist/lpMath.d.ts +55 -0
  55. package/dist/lpMath.d.ts.map +1 -1
  56. package/dist/lpMath.js +209 -0
  57. package/dist/lpMath.js.map +1 -1
  58. package/dist/multicall.d.ts.map +1 -1
  59. package/dist/multicall.js +3 -0
  60. package/dist/multicall.js.map +1 -1
  61. package/dist/names.d.ts +55 -0
  62. package/dist/names.d.ts.map +1 -0
  63. package/dist/names.js +93 -0
  64. package/dist/names.js.map +1 -0
  65. package/dist/owner.d.ts +4 -26
  66. package/dist/owner.d.ts.map +1 -1
  67. package/dist/owner.js +7 -112
  68. package/dist/owner.js.map +1 -1
  69. package/dist/portfolio.d.ts +54 -0
  70. package/dist/portfolio.d.ts.map +1 -0
  71. package/dist/portfolio.js +49 -0
  72. package/dist/portfolio.js.map +1 -0
  73. package/dist/price.d.ts +95 -0
  74. package/dist/price.d.ts.map +1 -0
  75. package/dist/price.js +329 -0
  76. package/dist/price.js.map +1 -0
  77. package/dist/risk.d.ts +42 -0
  78. package/dist/risk.d.ts.map +1 -0
  79. package/dist/risk.js +134 -0
  80. package/dist/risk.js.map +1 -0
  81. package/dist/rpcNodes.d.ts.map +1 -1
  82. package/dist/rpcNodes.js +18 -0
  83. package/dist/rpcNodes.js.map +1 -1
  84. package/dist/subpaths/approvals.d.ts +26 -0
  85. package/dist/subpaths/approvals.d.ts.map +1 -0
  86. package/dist/subpaths/approvals.js +27 -0
  87. package/dist/subpaths/approvals.js.map +1 -0
  88. package/dist/subpaths/fees.d.ts +23 -0
  89. package/dist/subpaths/fees.d.ts.map +1 -0
  90. package/dist/subpaths/fees.js +25 -0
  91. package/dist/subpaths/fees.js.map +1 -0
  92. package/dist/subpaths/liquidity.d.ts +24 -0
  93. package/dist/subpaths/liquidity.d.ts.map +1 -0
  94. package/dist/subpaths/liquidity.js +21 -0
  95. package/dist/subpaths/liquidity.js.map +1 -0
  96. package/dist/subpaths/lp.d.ts +1 -1
  97. package/dist/subpaths/lp.d.ts.map +1 -1
  98. package/dist/subpaths/lp.js +1 -1
  99. package/dist/subpaths/lp.js.map +1 -1
  100. package/dist/subpaths/names.d.ts +35 -0
  101. package/dist/subpaths/names.d.ts.map +1 -0
  102. package/dist/subpaths/names.js +39 -0
  103. package/dist/subpaths/names.js.map +1 -0
  104. package/dist/subpaths/price.d.ts +38 -0
  105. package/dist/subpaths/price.d.ts.map +1 -0
  106. package/dist/subpaths/price.js +40 -0
  107. package/dist/subpaths/price.js.map +1 -0
  108. package/dist/subpaths/risk.d.ts +19 -0
  109. package/dist/subpaths/risk.d.ts.map +1 -0
  110. package/dist/subpaths/risk.js +20 -0
  111. package/dist/subpaths/risk.js.map +1 -0
  112. package/dist/subpaths/swap.d.ts +19 -0
  113. package/dist/subpaths/swap.d.ts.map +1 -0
  114. package/dist/subpaths/swap.js +20 -0
  115. package/dist/subpaths/swap.js.map +1 -0
  116. package/dist/swap.d.ts +128 -0
  117. package/dist/swap.d.ts.map +1 -0
  118. package/dist/swap.js +291 -0
  119. package/dist/swap.js.map +1 -0
  120. package/dist/tokenProbe.d.ts +3 -0
  121. package/dist/tokenProbe.d.ts.map +1 -0
  122. package/dist/tokenProbe.js +4 -0
  123. package/dist/tokenProbe.js.map +1 -0
  124. package/dist/write/tron.d.ts +27 -0
  125. package/dist/write/tron.d.ts.map +1 -1
  126. package/dist/write/tron.js +30 -0
  127. package/dist/write/tron.js.map +1 -1
  128. package/package.json +32 -1
  129. package/src/aggregate.ts +110 -3
  130. package/src/approvals.ts +462 -0
  131. package/src/chains.ts +4 -0
  132. package/src/fees.ts +201 -0
  133. package/src/http.ts +34 -0
  134. package/src/liquidity/common.ts +314 -0
  135. package/src/liquidity/concentrated.ts +121 -0
  136. package/src/liquidity/index.ts +171 -0
  137. package/src/liquidity/types.ts +194 -0
  138. package/src/liquidity/v2.ts +371 -0
  139. package/src/liquidity/v3.ts +382 -0
  140. package/src/liquidity/v4.ts +348 -0
  141. package/src/lp.ts +39 -19
  142. package/src/lpMath.ts +223 -0
  143. package/src/multicall.ts +3 -0
  144. package/src/names.ts +141 -0
  145. package/src/owner.ts +7 -138
  146. package/src/portfolio.ts +91 -0
  147. package/src/price.ts +415 -0
  148. package/src/risk.ts +166 -0
  149. package/src/rpcNodes.ts +18 -0
  150. package/src/subpaths/approvals.ts +48 -0
  151. package/src/subpaths/fees.ts +43 -0
  152. package/src/subpaths/liquidity.ts +52 -0
  153. package/src/subpaths/lp.ts +20 -1
  154. package/src/subpaths/names.ts +43 -0
  155. package/src/subpaths/price.ts +60 -0
  156. package/src/subpaths/risk.ts +23 -0
  157. package/src/subpaths/swap.ts +36 -0
  158. package/src/swap.ts +413 -0
  159. package/src/tokenProbe.ts +5 -0
  160. package/src/write/tron.ts +48 -0
package/src/fees.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 手续费估算(子路径 `@w3lib/evm-batch-call/fees`):EIP-1559 的 baseFee 和三档优先费(按最近区块的 eth_feeHistory),
3
+ * 一笔交易的手续费(含 OP Stack / Scroll 的 L1 数据费);Tron 按能量 / 带宽和账户剩余的资源算要烧的 TRX
4
+ */
5
+ import type { BigNumberish } from 'ethers'
6
+
7
+ import { getNativeCurrency } from './chains.js'
8
+ import { formatUnits } from './erc20.js'
9
+ import type { Provider } from './provider.js'
10
+ import { isTronChain } from './tron.js'
11
+
12
+ export type FeeLevel = 'slow' | 'normal' | 'fast'
13
+
14
+ /** 一档费用(wei 的十进制字符串):EIP-1559 链是 maxFeePerGas / maxPriorityFeePerGas,legacy 链是 gasPrice */
15
+ export type FeeLevelEstimate = { maxFeePerGas: string; maxPriorityFeePerGas: string } | { gasPrice: string }
16
+
17
+ export interface FeeEstimate {
18
+ type: 'eip1559' | 'legacy'
19
+ /** 下一个区块的 baseFee(wei);legacy 链为 null */
20
+ baseFee: string | null
21
+ slow: FeeLevelEstimate
22
+ normal: FeeLevelEstimate
23
+ fast: FeeLevelEstimate
24
+ }
25
+
26
+ export interface EstimateFeesOptions {
27
+ /** 看最近多少个区块,默认 20 */
28
+ blocks?: number
29
+ /** 三档优先费取各区块交易优先费的百分位,默认 [10, 50, 90] */
30
+ percentiles?: readonly [number, number, number]
31
+ /** maxFeePerGas = baseFee × baseFeeMultiplier + 优先费,默认 2(baseFee 连涨几个区块也够) */
32
+ baseFeeMultiplier?: number
33
+ }
34
+
35
+ interface FeeHistory {
36
+ baseFeePerGas?: string[]
37
+ reward?: string[][]
38
+ }
39
+
40
+ /** 节点的 JSON-RPC send(ethers 的 JsonRpcProvider / BrowserProvider) */
41
+ type Sender = { send(method: string, params: unknown[]): Promise<unknown> }
42
+
43
+ async function send<T>(provider: Provider, method: string, params: unknown[]): Promise<T> {
44
+ const { runOnNodes } = await import('./write/rpc.js')
45
+ return runOnNodes(provider.rpc, method, async (node) => {
46
+ if (typeof (node as Partial<Sender>).send !== 'function') throw new Error(`${method} is not supported by this node; pass an RPC URL or an ethers Provider`)
47
+ return (await (node as unknown as Sender).send(method, params)) as T
48
+ })
49
+ }
50
+
51
+ const median = (values: bigint[]): bigint => {
52
+ if (!values.length) return 0n
53
+ const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
54
+ return sorted[Math.floor(sorted.length / 2)] as bigint
55
+ }
56
+
57
+ /**
58
+ * 当前的手续费建议(EVM 链):baseFee 取下一个区块的,三档优先费是最近区块里交易优先费的百分位的中位数;
59
+ * 节点不支持 eth_feeHistory 或链上没有 baseFee 时按 eth_gasPrice(三档为 1、1、1.2 倍)。Tron 用 estimateCost
60
+ *
61
+ * ```ts
62
+ * const fees = await estimateFees(provider)
63
+ * // { type: 'eip1559', baseFee: '1200000000', slow: { maxFeePerGas, maxPriorityFeePerGas }, normal: {…}, fast: {…} }
64
+ * ```
65
+ */
66
+ export async function estimateFees(provider: Provider, options: EstimateFeesOptions = {}): Promise<FeeEstimate> {
67
+ if (isTronChain(await provider.getChainId())) throw new Error('estimateFees is for EVM chains; use estimateCost on Tron')
68
+ const blocks = Math.max(1, Math.min(1024, Math.floor(options.blocks ?? 20)))
69
+ const percentiles = options.percentiles ?? [10, 50, 90]
70
+ if (!percentiles.every((p, i) => p >= 0 && p <= 100 && (i === 0 || p >= (percentiles[i - 1] as number)))) throw new RangeError('percentiles must be ascending numbers in [0, 100]')
71
+ const multiplier = options.baseFeeMultiplier ?? 2
72
+ let history: FeeHistory | null = null
73
+ try {
74
+ history = await send<FeeHistory>(provider, 'eth_feeHistory', [`0x${blocks.toString(16)}`, 'latest', [...percentiles]])
75
+ } catch (err) {
76
+ // 只有节点 / 链不支持 feeHistory 时才按 gasPrice;网络等其他错误照常抛出(不要在 EIP-1559 链上悄悄退回 legacy)
77
+ if (!/not (supported|available|found)|unsupported|does not exist|unknown method|-32601|method.*not/i.test(err instanceof Error ? err.message : String(err))) throw err
78
+ history = null
79
+ }
80
+ const baseFees = history?.baseFeePerGas ?? []
81
+ const next = baseFees.length ? BigInt(baseFees[baseFees.length - 1] as string) : 0n
82
+ if (!history || !baseFees.length || next === 0n) {
83
+ const gasPrice = BigInt(await send<string>(provider, 'eth_gasPrice', []))
84
+ const level = (n: bigint, d: bigint) => ({ gasPrice: ((gasPrice * n) / d).toString() })
85
+ return { type: 'legacy', baseFee: null, slow: level(1n, 1n), normal: level(1n, 1n), fast: level(6n, 5n) }
86
+ }
87
+ const rewards = history.reward ?? []
88
+ // 每档:各区块这个百分位的优先费取中位数(空区块的 0 不算)
89
+ const tip = (k: number) => median(rewards.map((r) => BigInt(r[k] ?? '0')).filter((v) => v > 0n))
90
+ const base = (next * BigInt(Math.round(multiplier * 1000))) / 1000n
91
+ const level = (priority: bigint) => ({ maxFeePerGas: (base + priority).toString(), maxPriorityFeePerGas: priority.toString() })
92
+ const [slow, normal, fast] = [tip(0), tip(1), tip(2)]
93
+ // 优先费不递减(区块很空时三档可能相同)
94
+ return { type: 'eip1559', baseFee: next.toString(), slow: level(slow), normal: level(normal > slow ? normal : slow), fast: level(fast > normal ? fast : normal > slow ? normal : slow) }
95
+ }
96
+
97
+ export interface CostCall {
98
+ /** 发送者(预估 gas、Tron 上按它剩余的资源算) */
99
+ from: string
100
+ to: string
101
+ data?: string
102
+ value?: BigNumberish
103
+ }
104
+
105
+ export interface EvmCostEstimate {
106
+ type: 'evm'
107
+ gasLimit: string
108
+ /** 这一档的每 gas 价格:EIP-1559 是 baseFee + 优先费(预计实际付的),legacy 是 gasPrice */
109
+ gasPrice: string
110
+ /** gasLimit × gasPrice(wei) */
111
+ fee: string
112
+ /** 最多付多少:gasLimit × maxFeePerGas(legacy 同 fee) */
113
+ maxFee: string
114
+ /** OP Stack / Scroll 的 L1 数据费(wei,含 25% 余量),其他链为 0 */
115
+ l1Fee: string
116
+ /** fee + l1Fee(wei)和按主币精度换算的值 */
117
+ total: string
118
+ totalFormatted: string
119
+ symbol: string | null
120
+ }
121
+
122
+ export interface TronCost {
123
+ type: 'tron'
124
+ energy: number
125
+ bandwidth: number
126
+ /** 剩余能量 / 带宽不够时要烧的 TRX(sun) */
127
+ energyBurn: string
128
+ bandwidthBurn: string
129
+ /** 转 TRX 给没激活的地址(激活费算在 bandwidthBurn 里) */
130
+ activates: boolean
131
+ energyPrice: string
132
+ bandwidthPrice: string
133
+ /** energyBurn + bandwidthBurn(sun)和按 TRX 换算的值 */
134
+ total: string
135
+ totalFormatted: string
136
+ symbol: 'TRX'
137
+ }
138
+
139
+ export type CostEstimate = EvmCostEstimate | TronCost
140
+
141
+ export interface EstimateCostOptions extends EstimateFeesOptions {
142
+ /** 用哪一档,默认 'normal' */
143
+ level?: FeeLevel
144
+ /** 已经查好的费用建议(多笔交易共用,不再查一次) */
145
+ fees?: FeeEstimate
146
+ }
147
+
148
+ /**
149
+ * 估算一笔交易的手续费:EVM 预估 gas × 这一档的价格(+ L1 数据费);Tron 模拟能量,按发送者剩余的资源算要烧的 TRX。
150
+ * 预估 gas 会执行一遍交易,revert 时抛出
151
+ *
152
+ * ```ts
153
+ * await estimateCost(provider, { from: user, to: USDT, data }) // { type: 'evm', gasLimit, fee, total, totalFormatted: '0.00042', symbol: 'BNB', … }
154
+ * await estimateCost(tronProvider, { from, to: USDT, data }) // { type: 'tron', energy: 64285, energyBurn: '6750000', totalFormatted: '7.0', … }
155
+ * ```
156
+ */
157
+ export async function estimateCost(provider: Provider, call: CostCall, options: EstimateCostOptions = {}): Promise<CostEstimate> {
158
+ const chainId = await provider.getChainId()
159
+ const value = BigInt(call.value ?? 0)
160
+ if (isTronChain(chainId)) {
161
+ const { tronEstimateCost } = await import('./write/tron.js')
162
+ const c = await tronEstimateCost(provider, { from: call.from, to: call.to, ...(call.data ? { data: call.data } : {}), value })
163
+ const total = c.energyBurn + c.bandwidthBurn
164
+ return {
165
+ type: 'tron',
166
+ energy: c.energy,
167
+ bandwidth: c.bandwidth,
168
+ energyBurn: c.energyBurn.toString(),
169
+ bandwidthBurn: c.bandwidthBurn.toString(),
170
+ activates: c.activates,
171
+ energyPrice: c.energyPrice.toString(),
172
+ bandwidthPrice: c.bandwidthPrice.toString(),
173
+ total: total.toString(),
174
+ totalFormatted: formatUnits(total, 6),
175
+ symbol: 'TRX',
176
+ }
177
+ }
178
+ const { WriteRpc, l1Fee } = await import('./write/rpc.js')
179
+ const rpc = await WriteRpc.from(provider)
180
+ const tx = { from: call.from, to: call.to, data: call.data ?? '0x', value }
181
+ const [gasLimit, fees] = await Promise.all([rpc.estimateGas(tx), options.fees ? Promise.resolve(options.fees) : estimateFees(provider, options)])
182
+ const level = fees[options.level ?? 'normal']
183
+ const perGas = 'gasPrice' in level ? BigInt(level.gasPrice) : BigInt(fees.baseFee ?? '0') + BigInt(level.maxPriorityFeePerGas)
184
+ const maxPerGas = 'gasPrice' in level ? perGas : BigInt(level.maxFeePerGas)
185
+ const feeFields = 'gasPrice' in level ? { type: 0, gasPrice: perGas } : { type: 2, maxFeePerGas: maxPerGas, maxPriorityFeePerGas: BigInt(level.maxPriorityFeePerGas) }
186
+ const l1 = await l1Fee(rpc, { ...tx, from: undefined, gasLimit, ...feeFields })
187
+ const fee = gasLimit * perGas
188
+ const total = fee + l1
189
+ const native = getNativeCurrency(chainId)
190
+ return {
191
+ type: 'evm',
192
+ gasLimit: gasLimit.toString(),
193
+ gasPrice: perGas.toString(),
194
+ fee: fee.toString(),
195
+ maxFee: (gasLimit * maxPerGas).toString(),
196
+ l1Fee: l1.toString(),
197
+ total: total.toString(),
198
+ totalFormatted: formatUnits(total, native?.decimals ?? 18),
199
+ symbol: native?.symbol ?? null,
200
+ }
201
+ }
package/src/http.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** HTTP 请求(代币列表、价格服务):超时、状态码检查 */
2
+
3
+ const REQUEST_TIMEOUT = 20_000
4
+
5
+ export class HttpStatusError extends Error {
6
+ constructor(
7
+ readonly status: number,
8
+ target: string,
9
+ ) {
10
+ super(`HTTP ${status} from ${target}`)
11
+ this.name = 'HttpStatusError'
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @param secret 请求里带的 Key:出错时错误信息只写域名,不写完整地址,避免 Key 出现在日志里
17
+ */
18
+ export function fetchJson<T>(fetchFn: typeof fetch, url: string, init?: RequestInit, secret?: string): Promise<T> {
19
+ return fetchFn(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT) }).then(async (res) => {
20
+ if (!res.ok) {
21
+ throw new HttpStatusError(res.status, secret ? safeHost(url) : url)
22
+ }
23
+ return (await res.json()) as T
24
+ })
25
+ }
26
+
27
+ function safeHost(url: string): string {
28
+ try {
29
+ return new URL(url).host
30
+ } catch {
31
+ return 'request'
32
+ }
33
+ }
34
+
@@ -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
+ }