@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/lpMath.ts CHANGED
@@ -111,3 +111,226 @@ export function sqrtPriceToPrice(sqrtPriceX96: bigint, decimals0: number, decima
111
111
  export function tickToPrice(tick: number, decimals0: number, decimals1: number): number {
112
112
  return 1.0001 ** tick * 10 ** (decimals0 - decimals1)
113
113
  }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // 添加 / 移除流动性用到的计算
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /** getSqrtRatioAtTick(MIN_TICK) / getSqrtRatioAtTick(MAX_TICK):池子价格的上下限(价格必须在 [MIN, MAX) 内) */
120
+ export const MIN_SQRT_RATIO = 4295128739n
121
+ export const MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342n
122
+ export const MAX_UINT128 = 2n ** 128n - 1n
123
+
124
+ const divUp = (a: bigint, b: bigint) => (a + b - 1n) / b
125
+
126
+ /** SqrtPriceMath.getAmount0Delta:区间 [a, b] 内 L 对应的 token0,roundUp 时向上取整(添加流动性时池子收取的数量) */
127
+ export function getAmount0Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint, roundUp: boolean): bigint {
128
+ if (sqrtA > sqrtB) {
129
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
130
+ }
131
+ const n1 = liquidity << 96n
132
+ const n2 = sqrtB - sqrtA
133
+ return roundUp ? divUp(divUp(n1 * n2, sqrtB), sqrtA) : (n1 * n2) / sqrtB / sqrtA
134
+ }
135
+
136
+ /** SqrtPriceMath.getAmount1Delta:区间 [a, b] 内 L 对应的 token1,roundUp 时向上取整 */
137
+ export function getAmount1Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint, roundUp: boolean): bigint {
138
+ if (sqrtA > sqrtB) {
139
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
140
+ }
141
+ return roundUp ? divUp(liquidity * (sqrtB - sqrtA), Q96) : (liquidity * (sqrtB - sqrtA)) / Q96
142
+ }
143
+
144
+ /** 添加流动性 L 时池子收取的两种代币数量(向上取整,与池子的 modifyPosition 一致) */
145
+ export function getMintAmounts(sqrtPrice: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint): [bigint, bigint] {
146
+ if (sqrtA > sqrtB) {
147
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
148
+ }
149
+ if (sqrtPrice <= sqrtA) {
150
+ return [getAmount0Delta(sqrtA, sqrtB, liquidity, true), 0n]
151
+ }
152
+ if (sqrtPrice < sqrtB) {
153
+ return [getAmount0Delta(sqrtPrice, sqrtB, liquidity, true), getAmount1Delta(sqrtA, sqrtPrice, liquidity, true)]
154
+ }
155
+ return [0n, getAmount1Delta(sqrtA, sqrtB, liquidity, true)]
156
+ }
157
+
158
+ /** LiquidityAmounts.getLiquidityForAmount0:区间 [a, b] 内 amount0 个 token0 对应的流动性(向下取整) */
159
+ export function getLiquidityForAmount0(sqrtA: bigint, sqrtB: bigint, amount0: bigint): bigint {
160
+ if (sqrtA > sqrtB) {
161
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
162
+ }
163
+ return (amount0 * ((sqrtA * sqrtB) / Q96)) / (sqrtB - sqrtA)
164
+ }
165
+
166
+ /** LiquidityAmounts.getLiquidityForAmount1:区间 [a, b] 内 amount1 个 token1 对应的流动性(向下取整) */
167
+ export function getLiquidityForAmount1(sqrtA: bigint, sqrtB: bigint, amount1: bigint): bigint {
168
+ if (sqrtA > sqrtB) {
169
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
170
+ }
171
+ return (amount1 * Q96) / (sqrtB - sqrtA)
172
+ }
173
+
174
+ /**
175
+ * LiquidityAmounts.getLiquidityForAmounts:最多用 amount0 / amount1 时能添加的流动性(取两边较小的,与 V3 仓位合约一致)。
176
+ * 价格在区间下方只用 token0,上方只用 token1
177
+ */
178
+ export function getLiquidityForAmounts(sqrtPrice: bigint, sqrtA: bigint, sqrtB: bigint, amount0: bigint, amount1: bigint): bigint {
179
+ if (sqrtA > sqrtB) {
180
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
181
+ }
182
+ if (sqrtPrice <= sqrtA) {
183
+ return getLiquidityForAmount0(sqrtA, sqrtB, amount0)
184
+ }
185
+ if (sqrtPrice < sqrtB) {
186
+ const l0 = getLiquidityForAmount0(sqrtPrice, sqrtB, amount0)
187
+ const l1 = getLiquidityForAmount1(sqrtA, sqrtPrice, amount1)
188
+ return l0 < l1 ? l0 : l1
189
+ }
190
+ return getLiquidityForAmount1(sqrtA, sqrtB, amount1)
191
+ }
192
+
193
+ /** 流动性是 uint128:超出时报错(合约里同样会 revert) */
194
+ export function toUint128(liquidity: bigint): bigint {
195
+ if (liquidity < 0n || liquidity > MAX_UINT128) {
196
+ throw new RangeError(`liquidity out of uint128 range: ${liquidity}`)
197
+ }
198
+ return liquidity
199
+ }
200
+
201
+ /** TickMath.getTickAtSqrtRatio:满足 getSqrtRatioAtTick(tick) <= sqrtPrice 的最大 tick(精确,二分查找) */
202
+ export function getTickAtSqrtRatio(sqrtPriceX96: bigint): number {
203
+ if (sqrtPriceX96 < MIN_SQRT_RATIO || sqrtPriceX96 >= MAX_SQRT_RATIO) {
204
+ throw new RangeError(`sqrtPriceX96 out of range: ${sqrtPriceX96}`)
205
+ }
206
+ let lo = MIN_TICK
207
+ let hi = MAX_TICK
208
+ while (lo < hi) {
209
+ const mid = Math.floor((lo + hi + 1) / 2)
210
+ if (getSqrtRatioAtTick(mid) <= sqrtPriceX96) {
211
+ lo = mid
212
+ } else {
213
+ hi = mid - 1
214
+ }
215
+ }
216
+ return lo
217
+ }
218
+
219
+ /** 整数平方根(向下取整) */
220
+ export function sqrtBigInt(value: bigint): bigint {
221
+ if (value < 0n) {
222
+ throw new RangeError('square root of a negative number')
223
+ }
224
+ if (value < 2n) {
225
+ return value
226
+ }
227
+ // 牛顿迭代:从不小于平方根的 2^ceil(位数 / 2) 开始单调递减,收敛到向下取整的平方根
228
+ let x = 1n << BigInt((value.toString(2).length + 1) >> 1)
229
+ for (;;) {
230
+ const y = (x + value / x) >> 1n
231
+ if (y >= x) {
232
+ break
233
+ }
234
+ x = y
235
+ }
236
+ return x
237
+ }
238
+
239
+ /** 十进制数('1.5'、'2e-7'、1.5)→ 分子 / 分母,精确,不经过浮点 */
240
+ export function parseDecimal(value: string | number): { num: bigint; den: bigint } {
241
+ const text = typeof value === 'number' ? (Number.isFinite(value) ? String(value) : '') : value.trim()
242
+ const m = /^\+?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text)
243
+ if (!m || (!m[1] && !m[2])) {
244
+ throw new Error(`Invalid number: ${String(value)}`)
245
+ }
246
+ const frac = m[2] ?? ''
247
+ const exp = Number(m[3] ?? 0) - frac.length
248
+ if (Math.abs(exp) > 400) {
249
+ throw new RangeError(`Number out of range: ${String(value)}`)
250
+ }
251
+ const digits = BigInt(`${m[1] ?? ''}${frac}` || '0')
252
+ return exp >= 0 ? { num: digits * 10n ** BigInt(exp), den: 1n } : { num: digits, den: 10n ** BigInt(-exp) }
253
+ }
254
+
255
+ /**
256
+ * 价格(token1 / token0,按精度换算后的人类可读数字,如 600 表示 1 个 token0 值 600 个 token1)→ sqrtPriceX96。
257
+ * 精确计算(字符串不经过浮点),结果向下取整;价格必须大于 0
258
+ */
259
+ export function priceToSqrtPriceX96(price: string | number, decimals0: number, decimals1: number): bigint {
260
+ const { num, den } = parseDecimal(price)
261
+ return ratioToSqrtPriceX96(num, den, decimals0, decimals1)
262
+ }
263
+
264
+ /** 价格(分数 num / den,token1 / token0,按精度换算)→ sqrtPriceX96,向下取整;价格必须大于 0 */
265
+ export function ratioToSqrtPriceX96(num: bigint, den: bigint, decimals0: number, decimals1: number): bigint {
266
+ if (num <= 0n || den <= 0n) {
267
+ throw new RangeError('price must be greater than 0')
268
+ }
269
+ // raw = price × 10^(d1 − d0);sqrtPriceX96 = sqrt(raw × 2^192)
270
+ const scale = decimals1 - decimals0
271
+ const n = num * (scale > 0 ? 10n ** BigInt(scale) : 1n) * 2n ** 192n
272
+ const d = den * (scale < 0 ? 10n ** BigInt(-scale) : 1n)
273
+ return sqrtBigInt(n / d)
274
+ }
275
+
276
+ /** sqrtPriceX96 → tick(getTickAtSqrtRatio),超出池子价格范围时取 MIN_TICK / MAX_TICK */
277
+ export function clampedTickAtSqrtRatio(sqrtPrice: bigint): number {
278
+ if (sqrtPrice < MIN_SQRT_RATIO) {
279
+ return MIN_TICK
280
+ }
281
+ if (sqrtPrice >= MAX_SQRT_RATIO) {
282
+ return MAX_TICK
283
+ }
284
+ return getTickAtSqrtRatio(sqrtPrice)
285
+ }
286
+
287
+ /** 价格(token1 / token0,按精度换算)→ tick:满足 tickToPrice(tick) <= price 的最大 tick,限制在 [MIN_TICK, MAX_TICK] 内 */
288
+ export function priceToTick(price: string | number, decimals0: number, decimals1: number): number {
289
+ return clampedTickAtSqrtRatio(priceToSqrtPriceX96(price, decimals0, decimals1))
290
+ }
291
+
292
+ /** 最近的可用 tick(tickSpacing 的整数倍),限制在 [MIN_TICK, MAX_TICK] 内 */
293
+ export function nearestUsableTick(tick: number, tickSpacing: number): number {
294
+ if (!Number.isInteger(tickSpacing) || tickSpacing <= 0) {
295
+ throw new RangeError(`invalid tickSpacing: ${tickSpacing}`)
296
+ }
297
+ const rounded = Math.round(tick / tickSpacing) * tickSpacing
298
+ if (rounded < MIN_TICK) {
299
+ return rounded + tickSpacing
300
+ }
301
+ if (rounded > MAX_TICK) {
302
+ return rounded - tickSpacing
303
+ }
304
+ return rounded
305
+ }
306
+
307
+ /**
308
+ * 价格按 slippage 上下浮动后的 sqrtPrice:[sqrtPrice × sqrt(1 − s), sqrtPrice × sqrt(1 + s)],限制在池子允许的价格范围内。
309
+ * slippage 是比例(0.005 = 0.5%),精确到百万分之一
310
+ */
311
+ export function sqrtPriceBounds(sqrtPriceX96: bigint, slippage: number): [bigint, bigint] {
312
+ if (!(slippage >= 0 && slippage < 1)) {
313
+ throw new RangeError(`slippage must be in [0, 1): ${slippage}`)
314
+ }
315
+ const ppm = BigInt(Math.round(slippage * 1e6))
316
+ const square = sqrtPriceX96 * sqrtPriceX96
317
+ const lower = sqrtBigInt((square * (1_000_000n - ppm)) / 1_000_000n)
318
+ const upper = sqrtBigInt((square * (1_000_000n + ppm)) / 1_000_000n)
319
+ const clamp = (x: bigint) => (x <= MIN_SQRT_RATIO ? MIN_SQRT_RATIO + 1n : x >= MAX_SQRT_RATIO ? MAX_SQRT_RATIO - 1n : x)
320
+ return [clamp(lower), clamp(upper)]
321
+ }
322
+
323
+ /**
324
+ * 价格在 slippage 范围内变动时,流动性 L 至少对应多少代币(移除流动性 / V3 添加时的最小数量,向下取整)。
325
+ * token0 随价格上涨而减少,取上限价格时的数量;token1 取下限价格时的
326
+ */
327
+ export function getMinAmounts(sqrtPriceX96: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint, slippage: number): [bigint, bigint] {
328
+ const [lower, upper] = sqrtPriceBounds(sqrtPriceX96, slippage)
329
+ return [getAmountsForLiquidity(upper, sqrtA, sqrtB, liquidity)[0], getAmountsForLiquidity(lower, sqrtA, sqrtB, liquidity)[1]]
330
+ }
331
+
332
+ /** 价格在 slippage 范围内变动时,添加流动性 L 最多需要多少代币(V4 的 amountMax,向上取整) */
333
+ export function getMaxAmounts(sqrtPriceX96: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint, slippage: number): [bigint, bigint] {
334
+ const [lower, upper] = sqrtPriceBounds(sqrtPriceX96, slippage)
335
+ return [getMintAmounts(lower, sqrtA, sqrtB, liquidity)[0], getMintAmounts(upper, sqrtA, sqrtB, liquidity)[1]]
336
+ }
package/src/multicall.ts CHANGED
@@ -81,6 +81,9 @@ const MULTICALL3: Record<number, Multicall> = {
81
81
  80094: { address: CANONICAL, block: 0 }, // Berachain,创世预置
82
82
  534352: { address: CANONICAL, block: 14 }, // Scroll
83
83
  86233268: { address: CANONICAL, block: 10854614 }, // Winchain,部署区块未查到
84
+ 1030: { address: CANONICAL, block: 159584122 }, // Conflux eSpace,部署区块未查到
85
+ 1672: { address: CANONICAL, block: 20812125 }, // Pharos,部署区块未查到
86
+ 1776: { address: CANONICAL, block: 188237272 }, // Injective EVM,部署区块未查到
84
87
  // 以下两条链不支持 deployless(合约创建式 eth_call 返回空 / 格式不对),只能用 Multicall3;
85
88
  // 早于部署区块的历史查询会失败
86
89
  42262: { address: CANONICAL, block: 22380155 }, // Oasis Emerald,部署区块未查到
package/src/names.ts ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * 域名解析(子路径 `@w3lib/evm-batch-call/names`):ENS(Ethereum)、SPACE ID(BSC 的 .bnb)、Basenames(Base 的 .base.eth),
3
+ * 名字 → 地址、地址 → 名字(反查后再正查核对)。都是 ENS 式的注册表合约,读取合成几次 multicall
4
+ */
5
+ import { ensNormalize, namehash } from 'ethers'
6
+
7
+ import type { Provider } from './provider.js'
8
+ import { isTronChain } from './tron.js'
9
+
10
+ export interface NameService {
11
+ id: string
12
+ name: string
13
+ chainId: number
14
+ /** ENS 式的注册表合约(resolver(node)) */
15
+ registry: string
16
+ /** 这个服务的名字后缀(如 ['bnb']);null 表示任意名字都交给它(ENS 还支持导入的 DNS 域名) */
17
+ suffixes: readonly string[] | null
18
+ /** 反查用的节点后缀:ENS 是 addr.reverse,L2 上按 ENSIP-19 是 <coinType 十六进制>.reverse */
19
+ reverseSuffix: string
20
+ }
21
+
22
+ /** 内置的域名服务(2026-10 在链上核对过注册表和正查 / 反查) */
23
+ export const NAME_SERVICES: readonly NameService[] = [
24
+ { id: 'ens', name: 'ENS', chainId: 1, registry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', suffixes: null, reverseSuffix: 'addr.reverse' },
25
+ { id: 'spaceid', name: 'SPACE ID', chainId: 56, registry: '0x08CEd32a7f3eeC915Ba84415e9C07a7286977956', suffixes: ['bnb'], reverseSuffix: 'addr.reverse' },
26
+ { id: 'basenames', name: 'Basenames', chainId: 8453, registry: '0xb94704422c2a1e396835a571837aa5ae53285a95', suffixes: ['base.eth'], reverseSuffix: '80002105.reverse' },
27
+ ]
28
+
29
+ export interface NamesOptions {
30
+ /** 用哪些域名服务,默认 NAME_SERVICES 里 Provider 这条链的 */
31
+ services?: readonly NameService[]
32
+ }
33
+
34
+ export interface ResolvedName {
35
+ /** 传入的名字(原样) */
36
+ name: string
37
+ /** 规范化后的名字(ENSIP-15);名字不合法时为 null */
38
+ normalized: string | null
39
+ /** 解析出的地址;没有注册、没有设置地址、名字不合法时为 null */
40
+ address: string | null
41
+ /** 解析这个名字的服务 id;没有服务认这个后缀时为 null */
42
+ service: string | null
43
+ }
44
+
45
+ export interface AddressName {
46
+ /** 传入的地址(原样) */
47
+ address: string
48
+ /** 反查到、并且正查回到同一个地址的名字;没有时为 null */
49
+ name: string | null
50
+ service: string | null
51
+ }
52
+
53
+ const REGISTRY_ABI = ['function resolver(bytes32 node) view returns (address)']
54
+ const RESOLVER_ABI = ['function addr(bytes32 node) view returns (address)', 'function name(bytes32 node) view returns (string)']
55
+ const ZERO = '0x0000000000000000000000000000000000000000'
56
+ const present = (a: string | null | undefined): a is string => !!a && a.toLowerCase() !== ZERO
57
+
58
+ function servicesOf(chainId: number, options: NamesOptions): readonly NameService[] {
59
+ return options.services ?? NAME_SERVICES.filter((s) => s.chainId === chainId)
60
+ }
61
+
62
+ /** 名字归哪个服务:后缀最长的匹配优先,其次是不限后缀的服务 */
63
+ function serviceFor(name: string, services: readonly NameService[]): NameService | null {
64
+ let best: { s: NameService; len: number } | null = null
65
+ for (const s of services) {
66
+ for (const suffix of s.suffixes ?? []) {
67
+ if ((name === suffix || name.endsWith(`.${suffix}`)) && (!best || suffix.length > best.len)) best = { s, len: suffix.length }
68
+ }
69
+ }
70
+ return best?.s ?? services.find((s) => s.suffixes === null) ?? null
71
+ }
72
+
73
+ function normalize(name: string): string | null {
74
+ try {
75
+ const n = ensNormalize(name.trim())
76
+ return n.includes('.') ? n : null
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 名字 → 地址(读注册表的 resolver,再读 resolver 的 addr,两次 multicall)。
84
+ * 不支持通配解析(ENSIP-10)和链下解析(CCIP-Read,如 cb.id 等子域名),这些名字返回 null
85
+ *
86
+ * ```ts
87
+ * await resolveNames(new Provider(1), ['vitalik.eth', 'nick.eth'])
88
+ * // [{ name: 'vitalik.eth', normalized: 'vitalik.eth', address: '0xd8dA…6045', service: 'ens' }, …]
89
+ * ```
90
+ */
91
+ export async function resolveNames(provider: Provider, names: readonly string[], options: NamesOptions = {}): Promise<ResolvedName[]> {
92
+ const chainId = await provider.getChainId()
93
+ const services = isTronChain(chainId) ? [] : servicesOf(chainId, options)
94
+ const items = names.map((name) => {
95
+ const normalized = normalize(name)
96
+ const service = normalized ? serviceFor(normalized, services) : null
97
+ return { name, normalized, service, node: normalized && service ? namehash(normalized) : null }
98
+ })
99
+ const todo = items.filter((x) => x.node && x.service)
100
+ const resolvers = todo.length ? await provider.tryAll<string>(todo.map((x) => provider.contract((x.service as NameService).registry, REGISTRY_ABI).resolver(x.node as string))) : []
101
+ const withResolver = todo.flatMap((x, i) => (present(resolvers[i]) ? [{ x, resolver: resolvers[i] as string }] : []))
102
+ const addresses = withResolver.length ? await provider.tryAll<string>(withResolver.map(({ x, resolver }) => provider.contract(resolver, RESOLVER_ABI).addr(x.node as string))) : []
103
+ const found = new Map(withResolver.map(({ x }, i) => [x, present(addresses[i]) ? (addresses[i] as string) : null]))
104
+ return items.map((x) => ({ name: x.name, normalized: x.normalized, address: found.get(x) ?? null, service: x.service?.id ?? null }))
105
+ }
106
+
107
+ /**
108
+ * 地址 → 名字:读反查记录(主名字),再正查核对名字确实指回这个地址(没有核对的反查记录可以随便设置,不能信)。
109
+ * 共四次 multicall;Tron 上没有域名服务,都返回 null
110
+ *
111
+ * ```ts
112
+ * await lookupAddresses(new Provider(56), [user]) // [{ address: user, name: 'alice.bnb', service: 'spaceid' }]
113
+ * ```
114
+ */
115
+ export async function lookupAddresses(provider: Provider, addresses: readonly string[], options: NamesOptions = {}): Promise<AddressName[]> {
116
+ const chainId = await provider.getChainId()
117
+ const services = isTronChain(chainId) ? [] : servicesOf(chainId, options)
118
+ const queries = addresses.flatMap((address, i) =>
119
+ /^0x[0-9a-fA-F]{40}$/.test(address) ? services.map((s) => ({ i, s, node: namehash(`${address.slice(2).toLowerCase()}.${s.reverseSuffix}`) })) : [],
120
+ )
121
+ const resolvers = queries.length ? await provider.tryAll<string>(queries.map((q) => provider.contract(q.s.registry, REGISTRY_ABI).resolver(q.node))) : []
122
+ const withResolver = queries.flatMap((q, j) => (present(resolvers[j]) ? [{ q, resolver: resolvers[j] as string }] : []))
123
+ const names = withResolver.length ? await provider.tryAll<string>(withResolver.map(({ q, resolver }) => provider.contract(resolver, RESOLVER_ABI).name(q.node))) : []
124
+ const claimed = withResolver.flatMap(({ q }, j) => (typeof names[j] === 'string' && names[j] ? [{ q, name: names[j] as string }] : []))
125
+ // 正查核对:名字要由同一个服务解析回这个地址
126
+ const forward = await resolveNames(
127
+ provider,
128
+ claimed.map((c) => c.name),
129
+ { services },
130
+ )
131
+ const result: AddressName[] = addresses.map((address) => ({ address, name: null, service: null }))
132
+ claimed.forEach((c, j) => {
133
+ const f = forward[j]
134
+ const out = result[c.q.i] as AddressName
135
+ if (out.name === null && f?.address && f.service === c.q.s.id && f.address.toLowerCase() === out.address.toLowerCase()) {
136
+ out.name = f.normalized
137
+ out.service = c.q.s.id
138
+ }
139
+ })
140
+ return result
141
+ }
package/src/owner.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { CallOverrides } from './aggregate.js'
2
2
  import { NATIVE_TOKEN, formatUnits, type BalanceToken } from './erc20.js'
3
3
  import type { Provider } from './provider.js'
4
+ import { HttpStatusError, fetchJson } from './http.js'
5
+ import { defillamaPrices, type PriceSource } from './price.js'
4
6
  import { transferScan, type TransferScanOptions } from './scan.js'
5
- import { TRON_CHAIN_ID, isTronAddress, isTronChain, toTronAddress } from './tron.js'
7
+ import { isTronAddress, isTronChain, toTronAddress } from './tron.js'
6
8
 
7
9
  // ---------------------------------------------------------------------------
8
10
  // 代币来源:负责“这个地址可能持有哪些代币”。余额和精度一律由本库用 multicall 在链上核对。
@@ -31,41 +33,10 @@ export interface TokenSource {
31
33
  discover(ctx: TokenSourceContext): Promise<DiscoveredToken[] | null>
32
34
  }
33
35
 
34
- const REQUEST_TIMEOUT = 20_000
35
36
  const LIST_TTL = 60 * 60 * 1000
36
37
  const MAX_CACHED_LISTS = 16
37
38
  const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
38
39
 
39
- export class HttpStatusError extends Error {
40
- constructor(
41
- readonly status: number,
42
- target: string,
43
- ) {
44
- super(`HTTP ${status} from ${target}`)
45
- this.name = 'HttpStatusError'
46
- }
47
- }
48
-
49
- /**
50
- * @param secret 请求里带的 Key:出错时错误信息只写域名,不写完整地址,避免 Key 出现在日志里
51
- */
52
- function fetchJson<T>(fetchFn: typeof fetch, url: string, init?: RequestInit, secret?: string): Promise<T> {
53
- return fetchFn(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT) }).then(async (res) => {
54
- if (!res.ok) {
55
- throw new HttpStatusError(res.status, secret ? safeHost(url) : url)
56
- }
57
- return (await res.json()) as T
58
- })
59
- }
60
-
61
- function safeHost(url: string): string {
62
- try {
63
- return new URL(url).host
64
- } catch {
65
- return 'request'
66
- }
67
- }
68
-
69
40
  // 公开代币列表:缓存“按链过滤、转换后”的结果(不缓存原始 JSON),1 小时;缓存 Promise,同时发起的请求共用,失败不缓存。
70
41
  // 公开列表与使用哪个 fetch 无关,所以缓存键只看列表和链
71
42
  const listCache = new Map<string, { at: number; promise: Promise<DiscoveredToken[] | null> }>()
@@ -410,111 +381,9 @@ function dedupe<T extends DiscoveredToken>(tokens: T[]): T[] {
410
381
  return [...seen.values()]
411
382
  }
412
383
 
413
- // ---------------------------------------------------------------------------
414
- // 价格来源
415
- // ---------------------------------------------------------------------------
416
-
417
- export interface PriceSourceContext {
418
- chainId: number
419
- /** 代币地址(不含主币) */
420
- tokens: readonly string[]
421
- /** 主币 symbol(需要主币价格时);不需要时为 null */
422
- nativeSymbol: string | null
423
- fetch: typeof fetch
424
- }
425
-
426
- export interface PriceSource {
427
- readonly name: string
428
- /** 返回 去重键 → 美元价格:EVM 是小写 0x 地址;Tron 是 T 开头的 base58 地址(区分大小写);主币用键 'native'。没有价格的不返回 */
429
- prices(ctx: PriceSourceContext): Promise<Map<string, number>>
430
- }
431
-
432
- /** DefiLlama 的链标识:chainId → slug(2026-10 实测可用) */
433
- export const DEFILLAMA_CHAINS: Readonly<Record<number, string>> = {
434
- 1: 'ethereum',
435
- 10: 'optimism',
436
- 56: 'bsc',
437
- 130: 'unichain',
438
- 137: 'polygon',
439
- 143: 'monad',
440
- 196: 'xlayer',
441
- 204: 'op_bnb',
442
- 324: 'era',
443
- 999: 'hyperliquid',
444
- 1116: 'core',
445
- 2818: 'morph',
446
- 5000: 'mantle',
447
- 8453: 'base',
448
- 9745: 'plasma',
449
- 34443: 'mode',
450
- 42161: 'arbitrum',
451
- 43114: 'avax',
452
- 57073: 'ink',
453
- 59144: 'linea',
454
- 80094: 'berachain',
455
- 81457: 'blast',
456
- 534352: 'scroll',
457
- [TRON_CHAIN_ID.mainnet]: 'tron',
458
- }
459
-
460
- /** 主币价格:按主币 symbol 对应 CoinGecko 的币种 id(DefiLlama 用 coingecko:<id> 查) */
461
- const NATIVE_PRICE_IDS: Readonly<Record<string, string>> = {
462
- ETH: 'ethereum',
463
- BNB: 'binancecoin',
464
- POL: 'polygon-ecosystem-token',
465
- AVAX: 'avalanche-2',
466
- MNT: 'mantle',
467
- OKB: 'okb',
468
- HYPE: 'hyperliquid',
469
- BERA: 'berachain-bera',
470
- CORE: 'coredaoorg',
471
- MON: 'monad',
472
- XPL: 'plasma',
473
- TRX: 'tron',
474
- }
475
-
476
- /**
477
- * DefiLlama 价格(免费、不需要 Key)。minConfidence:DefiLlama 给出的置信度下限(0~1),低于它视为没有价格,默认 0.9。
478
- * 注意:流动性很差的代币价格可能虚高,展示总资产时建议配合 minUsd 或自己的价格源。
479
- */
480
- export function defillamaPrices(options: { minConfidence?: number } = {}): PriceSource {
481
- const minConfidence = options.minConfidence ?? 0.9
482
- return {
483
- name: 'defillama',
484
- async prices({ chainId, tokens, nativeSymbol, fetch: fetchFn }) {
485
- const result = new Map<string, number>()
486
- const slug = DEFILLAMA_CHAINS[chainId]
487
- const nativeId = nativeSymbol ? NATIVE_PRICE_IDS[nativeSymbol.toUpperCase()] : undefined
488
- // 查询键 → 结果里用的去重键:EVM 地址小写;Tron 地址区分大小写,DefiLlama 要用原样的 base58(T…)地址
489
- const resultKey = new Map<string, string>()
490
- const tokenKeys = slug
491
- ? tokens.map((t) => {
492
- const tron = slug === 'tron'
493
- const query = `${slug}:${tron ? toTronAddress(t) : t.toLowerCase()}`
494
- resultKey.set(query.toLowerCase(), tron ? toTronAddress(t) : t.toLowerCase())
495
- return query
496
- })
497
- : []
498
- const keys = [...(nativeId ? [`coingecko:${nativeId}`] : []), ...tokenKeys]
499
- const batches: string[][] = []
500
- for (let i = 0; i < keys.length; i += 100) {
501
- batches.push(keys.slice(i, i + 100))
502
- }
503
- const responses = await Promise.all(
504
- batches.map((batch) => fetchJson<{ coins?: Record<string, { price?: number; confidence?: number }> }>(fetchFn, `https://coins.llama.fi/prices/current/${batch.join(',')}`)),
505
- )
506
- for (const res of responses) {
507
- for (const [key, coin] of Object.entries(res.coins ?? {})) {
508
- if (typeof coin.price !== 'number' || !Number.isFinite(coin.price) || (coin.confidence !== undefined && coin.confidence < minConfidence)) {
509
- continue
510
- }
511
- result.set(key.startsWith('coingecko:') ? 'native' : (resultKey.get(key.toLowerCase()) ?? key.slice(key.indexOf(':') + 1).toLowerCase()), coin.price)
512
- }
513
- }
514
- return result
515
- },
516
- }
517
- }
384
+ // 价格来源在 price.ts(子路径 /price);这里继续导出,旧的 import 不受影响
385
+ export { DEFILLAMA_CHAINS, defillamaPrices, type PriceSource, type PriceSourceContext } from './price.js'
386
+ export { HttpStatusError }
518
387
 
519
388
  // ---------------------------------------------------------------------------
520
389
  // ownerTokens
@@ -617,7 +486,7 @@ export async function ownerTokens(provider: Provider, owner: string, options: Ow
617
486
  // 价格源的键:Tron 上统一用 T 地址(代币来源给的可能是 0x 格式)
618
487
  const priceKey = (address: string) => (isTronChain(chainId) ? toTronAddress(address) : addressKey(address))
619
488
  const priceMap = priceSource
620
- ? await priceSource.prices({ chainId, tokens: held.map((x) => (isTronChain(chainId) ? toTronAddress(x.meta.address) : x.meta.address)), nativeSymbol: nativeAddress ? (nativeInfo?.symbol ?? null) : null, fetch: fetchFn })
489
+ ? await priceSource.prices({ chainId, tokens: held.map((x) => (isTronChain(chainId) ? toTronAddress(x.meta.address) : x.meta.address)), nativeSymbol: nativeAddress ? (nativeInfo?.symbol ?? null) : null, fetch: fetchFn, provider })
621
490
  : new Map<string, number>()
622
491
  const valueOf = (formatted: string, price: number | undefined) => (price === undefined ? null : Number(formatted) * price)
623
492
 
@@ -0,0 +1,91 @@
1
+ /**
2
+ * 资产总值(子路径 `@w3lib/evm-batch-call/price`):已知代币的余额 × 价格,可选加上 LP 仓位的价值
3
+ */
4
+ import { lpPositions, type LpPosition, type LpPositionsOptions } from './lp.js'
5
+ import { combinePrices, defillamaPrices, onchainPrices, tokenPrices, type PriceSource } from './price.js'
6
+ import type { Provider } from './provider.js'
7
+
8
+ export interface PortfolioOptions {
9
+ /** 要计入的代币(含主币:NATIVE_TOKEN),余额用 multicall 一次读出 */
10
+ tokens: readonly string[]
11
+ /** 价格源,依次使用,默认 [defillamaPrices(), onchainPrices()] */
12
+ sources?: readonly PriceSource[]
13
+ /** 同时计入 LP 仓位(V2 LP 代币、V3 仓位 NFT;参数同 lpPositions,价格源用上面的 sources),默认不计入 */
14
+ lp?: boolean | Omit<LpPositionsOptions, 'prices'>
15
+ /** 价值低于它的代币不列出(不影响合计);大于 0 时没有价格的代币也不列出(计入 unpriced),默认 0 */
16
+ minUsd?: number
17
+ /** 某个价格源出错时调用 */
18
+ onPriceError?: (err: unknown) => void
19
+ fetch?: typeof fetch
20
+ }
21
+
22
+ export interface PortfolioToken {
23
+ token: string
24
+ native: boolean
25
+ symbol: string | null
26
+ balance: string
27
+ decimals: number
28
+ formatted: string
29
+ price: number | null
30
+ /** 美元价值;没有价格时为 null */
31
+ value: number | null
32
+ /** 给出价格的价格源 */
33
+ priceSource: string | null
34
+ }
35
+
36
+ export interface Portfolio {
37
+ /** 合计(代币 + LP;没有价格的不计入) */
38
+ totalUsd: number
39
+ tokensUsd: number
40
+ lpUsd: number
41
+ /** 有余额的代币,按价值从高到低 */
42
+ tokens: PortfolioToken[]
43
+ /** LP 仓位(开启 lp 时) */
44
+ lp: LpPosition[]
45
+ /** 有余额但没有价格的代币数(LP 仓位里缺价格的也算) */
46
+ unpriced: number
47
+ }
48
+
49
+ /**
50
+ * 一个地址的资产总值:代币余额(只看传入的代币,EVM 不能只靠节点列出持仓)× 价格,可选加上 LP 仓位。
51
+ *
52
+ * ```ts
53
+ * const p = await portfolio(provider, user, { tokens: [NATIVE_TOKEN, USDT, USDC], lp: true })
54
+ * p.totalUsd // 1234.5
55
+ * ```
56
+ */
57
+ export async function portfolio(provider: Provider, owner: string, options: PortfolioOptions): Promise<Portfolio> {
58
+ const { tokens, sources = [defillamaPrices(), onchainPrices()], lp, minUsd = 0, onPriceError, fetch: fetchFn } = options
59
+ const onError = onPriceError ? (_: PriceSource, err: unknown) => onPriceError(err) : undefined
60
+ const [balances, positions] = await Promise.all([
61
+ tokens.length ? provider.balances(owner, tokens, { symbol: true }) : Promise.resolve([]),
62
+ lp ? lpPositions(provider, owner, { ...(lp === true ? {} : lp), prices: combinePrices(sources, { onError }), ...(fetchFn ? { fetch: fetchFn } : {}), ...(onPriceError ? { onPriceError } : {}) }) : Promise.resolve([]),
63
+ ])
64
+ const held = balances.filter((b) => b.success && BigInt(b.balance) > 0n)
65
+ const prices = held.length ? await tokenPrices(provider, held.map((b) => b.token), { sources, ...(onError ? { onError } : {}), ...(fetchFn ? { fetch: fetchFn } : {}) }) : []
66
+ const list: PortfolioToken[] = held.map((b, i) => {
67
+ const p = prices[i]
68
+ const price = p?.price ?? null
69
+ return {
70
+ token: b.token,
71
+ native: b.native,
72
+ symbol: b.symbol ?? null,
73
+ balance: b.balance,
74
+ decimals: b.decimals,
75
+ formatted: b.formatted,
76
+ price,
77
+ value: price === null ? null : Number(b.formatted) * price,
78
+ priceSource: p?.source ?? null,
79
+ }
80
+ })
81
+ const tokensUsd = list.reduce((sum, t) => sum + (t.value ?? 0), 0)
82
+ const lpUsd = positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
83
+ return {
84
+ totalUsd: tokensUsd + lpUsd,
85
+ tokensUsd,
86
+ lpUsd,
87
+ tokens: list.filter((t) => (t.value ?? 0) >= minUsd || (minUsd === 0 && t.value === null)).sort((a, b) => (b.value ?? -1) - (a.value ?? -1)),
88
+ lp: positions,
89
+ unpriced: list.filter((t) => t.price === null).length + positions.filter((p) => p.valueUsd === null).length,
90
+ }
91
+ }