@w3lib/evm-batch-call 0.4.1 → 0.5.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 (92) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +166 -3
  3. package/dist/aggregate.d.ts.map +1 -1
  4. package/dist/aggregate.js +153 -40
  5. package/dist/aggregate.js.map +1 -1
  6. package/dist/fallback.d.ts +20 -0
  7. package/dist/fallback.d.ts.map +1 -1
  8. package/dist/fallback.js +36 -5
  9. package/dist/fallback.js.map +1 -1
  10. package/dist/lp.d.ts +149 -0
  11. package/dist/lp.d.ts.map +1 -0
  12. package/dist/lp.js +499 -0
  13. package/dist/lp.js.map +1 -0
  14. package/dist/lpMath.d.ts +25 -0
  15. package/dist/lpMath.d.ts.map +1 -0
  16. package/dist/lpMath.js +103 -0
  17. package/dist/lpMath.js.map +1 -0
  18. package/dist/lpV4.d.ts +108 -0
  19. package/dist/lpV4.d.ts.map +1 -0
  20. package/dist/lpV4.js +283 -0
  21. package/dist/lpV4.js.map +1 -0
  22. package/dist/owner.d.ts +1 -1
  23. package/dist/owner.d.ts.map +1 -1
  24. package/dist/owner.js +18 -5
  25. package/dist/owner.js.map +1 -1
  26. package/dist/scan.d.ts +54 -0
  27. package/dist/scan.d.ts.map +1 -1
  28. package/dist/scan.js +124 -92
  29. package/dist/scan.js.map +1 -1
  30. package/dist/source.d.ts +2 -0
  31. package/dist/source.d.ts.map +1 -1
  32. package/dist/source.js +4 -0
  33. package/dist/source.js.map +1 -1
  34. package/dist/subpaths/lp.d.ts +33 -0
  35. package/dist/subpaths/lp.d.ts.map +1 -0
  36. package/dist/subpaths/lp.js +47 -0
  37. package/dist/subpaths/lp.js.map +1 -0
  38. package/dist/subpaths/write.d.ts +20 -0
  39. package/dist/subpaths/write.d.ts.map +1 -0
  40. package/dist/subpaths/write.js +18 -0
  41. package/dist/subpaths/write.js.map +1 -0
  42. package/dist/tron.d.ts +2 -0
  43. package/dist/tron.d.ts.map +1 -1
  44. package/dist/tron.js +4 -0
  45. package/dist/tron.js.map +1 -1
  46. package/dist/write/batch.d.ts +88 -0
  47. package/dist/write/batch.d.ts.map +1 -0
  48. package/dist/write/batch.js +231 -0
  49. package/dist/write/batch.js.map +1 -0
  50. package/dist/write/collect.d.ts +53 -0
  51. package/dist/write/collect.d.ts.map +1 -0
  52. package/dist/write/collect.js +255 -0
  53. package/dist/write/collect.js.map +1 -0
  54. package/dist/write/rpc.d.ts +68 -0
  55. package/dist/write/rpc.d.ts.map +1 -0
  56. package/dist/write/rpc.js +236 -0
  57. package/dist/write/rpc.js.map +1 -0
  58. package/dist/write/send.d.ts +124 -0
  59. package/dist/write/send.d.ts.map +1 -0
  60. package/dist/write/send.js +373 -0
  61. package/dist/write/send.js.map +1 -0
  62. package/dist/write/signer.d.ts +58 -0
  63. package/dist/write/signer.d.ts.map +1 -0
  64. package/dist/write/signer.js +41 -0
  65. package/dist/write/signer.js.map +1 -0
  66. package/dist/write/transfer.d.ts +83 -0
  67. package/dist/write/transfer.d.ts.map +1 -0
  68. package/dist/write/transfer.js +259 -0
  69. package/dist/write/transfer.js.map +1 -0
  70. package/dist/write/tron.d.ts +58 -0
  71. package/dist/write/tron.d.ts.map +1 -0
  72. package/dist/write/tron.js +956 -0
  73. package/dist/write/tron.js.map +1 -0
  74. package/package.json +27 -14
  75. package/src/aggregate.ts +168 -36
  76. package/src/fallback.ts +55 -11
  77. package/src/lp.ts +721 -0
  78. package/src/lpMath.ts +113 -0
  79. package/src/lpV4.ts +406 -0
  80. package/src/owner.ts +19 -6
  81. package/src/scan.ts +155 -86
  82. package/src/source.ts +5 -0
  83. package/src/subpaths/lp.ts +71 -0
  84. package/src/subpaths/write.ts +26 -0
  85. package/src/tron.ts +5 -0
  86. package/src/write/batch.ts +302 -0
  87. package/src/write/collect.ts +312 -0
  88. package/src/write/rpc.ts +277 -0
  89. package/src/write/send.ts +490 -0
  90. package/src/write/signer.ts +90 -0
  91. package/src/write/transfer.ts +344 -0
  92. package/src/write/tron.ts +1100 -0
package/src/lpMath.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 集中流动性(Uniswap V3 / V4 及其分叉)的数学:tick ↔ sqrtPrice、按流动性算代币数量、未领取的手续费。
3
+ * 与 Uniswap 合约里的整数算法逐位一致(TickMath、LiquidityAmounts、Position 的手续费计算)。
4
+ */
5
+
6
+ export const MIN_TICK = -887272
7
+ export const MAX_TICK = 887272
8
+
9
+ const Q96 = 2n ** 96n
10
+ const Q128 = 2n ** 128n
11
+ const Q256 = 2n ** 256n
12
+ const MAX_UINT256 = Q256 - 1n
13
+
14
+ /** TickMath.getSqrtRatioAtTick 里的常数:sqrt(1.0001)^(-2^i) 的 Q128 定点数 */
15
+ const RATIOS: readonly bigint[] = [
16
+ 0xfff97272373d413259a46990580e213an,
17
+ 0xfff2e50f5f656932ef12357cf3c7fdccn,
18
+ 0xffe5caca7e10e4e61c3624eaa0941cd0n,
19
+ 0xffcb9843d60f6159c9db58835c926644n,
20
+ 0xff973b41fa98c081472e6896dfb254c0n,
21
+ 0xff2ea16466c96a3843ec78b326b52861n,
22
+ 0xfe5dee046a99a2a811c461f1969c3053n,
23
+ 0xfcbe86c7900a88aedcffc83b479aa3a4n,
24
+ 0xf987a7253ac413176f2b074cf7815e54n,
25
+ 0xf3392b0822b70005940c7a398e4b70f3n,
26
+ 0xe7159475a2c29b7443b29c7fa6e889d9n,
27
+ 0xd097f3bdfd2022b8845ad8f792aa5825n,
28
+ 0xa9f746462d870fdf8a65dc1f90e061e5n,
29
+ 0x70d869a156d2a1b890bb3df62baf32f7n,
30
+ 0x31be135f97d08fd981231505542fcfa6n,
31
+ 0x9aa508b5b7a84e1c677de54f3e99bc9n,
32
+ 0x5d6af8dedb81196699c329225ee604n,
33
+ 0x2216e584f5fa1ea926041bedfe98n,
34
+ 0x48a170391f7dc42444e8fa2n,
35
+ ]
36
+
37
+ /** TickMath.getSqrtRatioAtTick:tick → sqrt(1.0001^tick) 的 Q64.96 定点数 */
38
+ export function getSqrtRatioAtTick(tick: number): bigint {
39
+ if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK) {
40
+ throw new RangeError(`tick out of range: ${tick}`)
41
+ }
42
+ const absTick = Math.abs(tick)
43
+ let ratio = absTick & 0x1 ? 0xfffcb933bd6fad37aa2d162d1a594001n : 0x100000000000000000000000000000000n
44
+ for (let i = 0; i < RATIOS.length; i++) {
45
+ if (absTick & (0x2 << i)) {
46
+ ratio = (ratio * (RATIOS[i] as bigint)) >> 128n
47
+ }
48
+ }
49
+ if (tick > 0) {
50
+ ratio = MAX_UINT256 / ratio
51
+ }
52
+ // Q128.128 → Q64.96,向上取整(与合约一致)
53
+ return (ratio >> 32n) + (ratio % (1n << 32n) === 0n ? 0n : 1n)
54
+ }
55
+
56
+ /** SqrtPriceMath.getAmount0Delta(向下取整):区间 [a, b] 内 L 对应的 token0 数量 */
57
+ function amount0Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint): bigint {
58
+ return ((liquidity << 96n) * (sqrtB - sqrtA)) / sqrtB / sqrtA
59
+ }
60
+
61
+ /** SqrtPriceMath.getAmount1Delta(向下取整):区间 [a, b] 内 L 对应的 token1 数量 */
62
+ function amount1Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint): bigint {
63
+ return (liquidity * (sqrtB - sqrtA)) / Q96
64
+ }
65
+
66
+ /**
67
+ * LiquidityAmounts.getAmountsForLiquidity:当前价格 sqrtPrice 下,价格区间 [sqrtA, sqrtB] 内流动性 L 对应的两种代币数量
68
+ * (与移除全部流动性时拿到的数量一致,向下取整)
69
+ */
70
+ export function getAmountsForLiquidity(sqrtPrice: bigint, sqrtA: bigint, sqrtB: bigint, liquidity: bigint): [bigint, bigint] {
71
+ if (sqrtA > sqrtB) {
72
+ ;[sqrtA, sqrtB] = [sqrtB, sqrtA]
73
+ }
74
+ if (liquidity === 0n) {
75
+ return [0n, 0n]
76
+ }
77
+ if (sqrtPrice <= sqrtA) {
78
+ return [amount0Delta(sqrtA, sqrtB, liquidity), 0n]
79
+ }
80
+ if (sqrtPrice < sqrtB) {
81
+ return [amount0Delta(sqrtPrice, sqrtB, liquidity), amount1Delta(sqrtA, sqrtPrice, liquidity)]
82
+ }
83
+ return [0n, amount1Delta(sqrtA, sqrtB, liquidity)]
84
+ }
85
+
86
+ const wrap = (x: bigint) => ((x % Q256) + Q256) % Q256
87
+
88
+ /**
89
+ * 区间内的累计手续费增长(Tick.getFeeGrowthInside,按 uint256 回绕)。
90
+ * global:池子的 feeGrowthGlobalX128;lowerOutside / upperOutside:两端 tick 的 feeGrowthOutsideX128
91
+ */
92
+ export function getFeeGrowthInside(tick: number, tickLower: number, tickUpper: number, global: bigint, lowerOutside: bigint, upperOutside: bigint): bigint {
93
+ const below = tick >= tickLower ? lowerOutside : wrap(global - lowerOutside)
94
+ const above = tick < tickUpper ? upperOutside : wrap(global - upperOutside)
95
+ return wrap(global - below - above)
96
+ }
97
+
98
+ /** 自上次结算以来新产生的手续费:L × (inside − insideLast) / 2^128(差值按 uint256 回绕,与合约一致) */
99
+ export function getFeesEarned(liquidity: bigint, feeGrowthInside: bigint, feeGrowthInsideLast: bigint): bigint {
100
+ return (liquidity * wrap(feeGrowthInside - feeGrowthInsideLast)) / Q128
101
+ }
102
+
103
+ /** sqrtPriceX96 → token1 / token0 的价格(按精度换算后的人类可读数字,有浮点误差,只用于展示) */
104
+ export function sqrtPriceToPrice(sqrtPriceX96: bigint, decimals0: number, decimals1: number): number {
105
+ // 全程浮点:sqrtPrice 不超过 2^160,转 Number 只丢末尾的位(相对误差约 1e-16);先除 2^96 再平方,价格很小时也不会变成 0
106
+ const ratio = Number(sqrtPriceX96) / 2 ** 96
107
+ return ratio * ratio * 10 ** (decimals0 - decimals1)
108
+ }
109
+
110
+ /** tick → token1 / token0 的价格(按精度换算,只用于展示) */
111
+ export function tickToPrice(tick: number, decimals0: number, decimals1: number): number {
112
+ return 1.0001 ** tick * 10 ** (decimals0 - decimals1)
113
+ }
package/src/lpV4.ts ADDED
@@ -0,0 +1,406 @@
1
+ import { AbiCoder, keccak256 } from 'ethers'
2
+
3
+ import type { CallOverrides } from './aggregate.js'
4
+ import { getAmountsForLiquidity, getFeesEarned, getSqrtRatioAtTick } from './lpMath.js'
5
+ import type { Provider } from './provider.js'
6
+ import { runLogScan, type LogScanProgress } from './scan.js'
7
+
8
+ /** Uniswap V4 用到的 LP 协议字段(与 lp.ts 的 LpProtocol 一致,避免循环引用) */
9
+ interface V4Protocol {
10
+ id: string
11
+ name: string
12
+ version: 'v2' | 'v3' | 'v4'
13
+ chainId: number
14
+ positionManager?: string
15
+ stateView?: string
16
+ deployBlock?: number
17
+ }
18
+
19
+ /** V4 扫描进度的存储:默认存在内存里;传自定义存储可以跨页面刷新 / 重启接着扫 */
20
+ export interface LpScanStorage {
21
+ get(key: string): LogScanProgress | undefined | Promise<LogScanProgress | undefined>
22
+ set(key: string, state: LogScanProgress): void | Promise<void>
23
+ }
24
+
25
+ /** 内存存储:最多保存 1000 组进度,超出淘汰最早的 */
26
+ export function memoryLpScanStorage(limit = 1000): LpScanStorage {
27
+ const map = new Map<string, LogScanProgress>()
28
+ return {
29
+ get: (key) => map.get(key),
30
+ set: (key, state) => {
31
+ map.delete(key)
32
+ map.set(key, state)
33
+ if (map.size > limit) {
34
+ map.delete(map.keys().next().value as string)
35
+ }
36
+ },
37
+ }
38
+ }
39
+
40
+ /** 基于 getItem / setItem 的存储(如 localStorage):`v4: { scan: { storage: keyValueLpScanStorage(localStorage) } }` */
41
+ export function keyValueLpScanStorage(store: { getItem(key: string): string | null; setItem(key: string, value: string): void }, prefix = 'w3lib:lp-v4-scan:'): LpScanStorage {
42
+ return {
43
+ get: (key) => {
44
+ try {
45
+ const raw = store.getItem(prefix + key)
46
+ return raw ? (JSON.parse(raw) as LogScanProgress) : undefined // 格式在读取后校验
47
+ } catch {
48
+ return undefined
49
+ }
50
+ },
51
+ set: (key, state) => {
52
+ try {
53
+ store.setItem(prefix + key, JSON.stringify(state))
54
+ } catch {
55
+ // 存储满或不可用:只影响下次能否接着扫
56
+ }
57
+ },
58
+ }
59
+ }
60
+
61
+ export interface V4ScanOptions {
62
+ /** 进度存储,默认内存(进程 / 页面结束就丢失) */
63
+ storage?: LpScanStorage
64
+ /**
65
+ * 第一次扫描从哪个区块开始:数字;'deployment' 从仓位合约部署的区块开始(扫全部历史;
66
+ * 自定义协议没有 deployBlock 时等同于 'latest');'latest' 从现在开始(只能发现之后的新仓位)。默认 'deployment'
67
+ */
68
+ fromBlock?: number | 'deployment' | 'latest'
69
+ /** 单次 getLogs 的最大区块范围,默认 10000(只查仓位合约的事件,节点一般允许较大的范围);节点有上限时自动缩小 */
70
+ blockRange?: number
71
+ /** 每次调用最多发多少个 getLogs,默认 20 */
72
+ maxRequests?: number
73
+ /** 每次调用的扫描时间预算(毫秒),默认 5000 */
74
+ timeBudget?: number
75
+ /** 只扫到最新区块往前这么多个区块,默认 5。不要设为 0:刚出的区块可能被重组,扫到的仓位核对时“不存在”会被从进度里去掉 */
76
+ confirmations?: number
77
+ /**
78
+ * 每次扫描后的进度:cursor 是已扫到的区块,target 是这次的目标区块,done 表示已扫到最新。
79
+ * 没扫完之前 V4 的结果不完整(从 fromBlock 往后扫,最近铸造的仓位最后才扫到),可以据此提示用户或继续调用
80
+ */
81
+ onProgress?(progress: V4ScanProgress): void
82
+ }
83
+
84
+ export interface V4ScanProgress {
85
+ chainId: number
86
+ positionManager: string
87
+ startBlock: number
88
+ cursor: number
89
+ /**
90
+ * 这次的目标区块;没扫(暂停中、拿不到最新区块)时为 null。
91
+ * 触发暂停的那一次照常有 target,同时带 pausedUntil;第一次就拿不到最新区块时 startBlock / cursor 为 -1
92
+ */
93
+ target: number | null
94
+ done: boolean
95
+ /** 节点不开放 getLogs 时暂停到这个时间(毫秒时间戳),之前的调用不再扫描 */
96
+ pausedUntil?: number
97
+ }
98
+
99
+ export interface LpV4Options {
100
+ /** 已知的仓位 tokenId(如应用自己记下的):直接查,不用扫描 */
101
+ tokenIds?: ReadonlyArray<string | number | bigint>
102
+ /**
103
+ * 扫描仓位合约发给这个地址的 NFT(Transfer 事件)来发现仓位。V4 的仓位合约不能按地址列出 NFT,只能这样找。
104
+ * 需要节点支持 eth_getLogs;扫全部历史在公共节点上很慢(每次调用推进一段,进度可以存起来),建议用自己的节点。默认不扫描
105
+ */
106
+ scan?: boolean | V4ScanOptions
107
+ }
108
+
109
+ const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
110
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
111
+ /** PoolKey.fee 的最高位:动态手续费池子(实际手续费看 slot0 的 lpFee) */
112
+ const DYNAMIC_FEE_FLAG = 0x800000
113
+
114
+ const POSITION_MANAGER_ABI = [
115
+ 'function ownerOf(uint256 tokenId) view returns (address)',
116
+ 'function getPoolAndPositionInfo(uint256 tokenId) view returns ((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, uint256 info)',
117
+ 'function getPositionLiquidity(uint256 tokenId) view returns (uint128)',
118
+ ]
119
+ const STATE_VIEW_ABI = [
120
+ 'function getSlot0(bytes32 poolId) view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)',
121
+ 'function getFeeGrowthInside(bytes32 poolId, int24 tickLower, int24 tickUpper) view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128)',
122
+ 'function getPositionInfo(bytes32 poolId, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) view returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)',
123
+ ]
124
+
125
+ const defaultStorage = memoryLpScanStorage()
126
+
127
+ /** int24 的补码 → number */
128
+ function int24(value: bigint): number {
129
+ const n = Number(value & 0xffffffn)
130
+ return n >= 0x800000 ? n - 0x1000000 : n
131
+ }
132
+
133
+ /** PositionInfo(uint256):| 200 位 poolId | 24 位 tickUpper | 24 位 tickLower | 8 位 hasSubscriber | */
134
+ export function decodePositionInfo(info: bigint): { tickLower: number; tickUpper: number } {
135
+ return { tickLower: int24(info >> 8n), tickUpper: int24(info >> 32n) }
136
+ }
137
+
138
+ /** poolId = keccak256(abi.encode(PoolKey)) */
139
+ export function poolIdOf(key: { currency0: string; currency1: string; fee: number | bigint; tickSpacing: number | bigint; hooks: string }): string {
140
+ return keccak256(
141
+ AbiCoder.defaultAbiCoder().encode(['address', 'address', 'uint24', 'int24', 'address'], [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks]),
142
+ )
143
+ }
144
+
145
+ const isProgress = (raw: unknown): raw is LogScanProgress => {
146
+ const s = raw as Partial<LogScanProgress> | null
147
+ return (
148
+ !!s &&
149
+ typeof s === 'object' &&
150
+ Number.isSafeInteger(s.startBlock) &&
151
+ Number.isSafeInteger(s.cursor) &&
152
+ Number.isSafeInteger(s.blockRange) &&
153
+ (s.blockRange as number) >= 1 &&
154
+ Array.isArray(s.items) &&
155
+ s.items.every((t) => typeof t === 'string' && /^\d+$/.test(t))
156
+ )
157
+ }
158
+
159
+ /** 扫描仓位合约发给 owner 的 NFT,返回扫描到的 tokenId(之后再核对是否仍归 owner) */
160
+ async function scanTokenIds(
161
+ provider: Provider,
162
+ chainId: number,
163
+ protocol: V4Protocol,
164
+ owner: string,
165
+ options: V4ScanOptions,
166
+ signal: AbortSignal | undefined,
167
+ ): Promise<{ ids: string[]; cursor: number; prune(ids: readonly string[]): Promise<void> }> {
168
+ const storage = options.storage ?? defaultStorage
169
+ const key = `${chainId}:${(protocol.positionManager as string).toLowerCase()}:${owner.toLowerCase()}`
170
+ let saved: LogScanProgress | undefined
171
+ try {
172
+ const raw = await storage.get(key)
173
+ saved = isProgress(raw) ? raw : undefined // 格式不对(旧版本 / 被改坏)时丢弃,重新开始
174
+ } catch {
175
+ saved = undefined
176
+ }
177
+ const fromBlock = options.fromBlock ?? 'deployment'
178
+ const ownerTopic = `0x${owner.slice(2).toLowerCase().padStart(64, '0')}`
179
+ let reported = false
180
+ const report = (state: LogScanProgress, target: number | null) => {
181
+ reported = true
182
+ try {
183
+ options.onProgress?.({
184
+ chainId,
185
+ positionManager: protocol.positionManager as string,
186
+ startBlock: state.startBlock,
187
+ cursor: state.cursor,
188
+ target,
189
+ done: target !== null && state.cursor >= target,
190
+ ...(state.pausedUntil ? { pausedUntil: state.pausedUntil } : {}),
191
+ })
192
+ } catch {
193
+ // 回调出错不影响扫描和保存进度
194
+ }
195
+ }
196
+ const progress = await runLogScan({
197
+ provider,
198
+ saved,
199
+ initialCursor: (target) =>
200
+ typeof fromBlock === 'number' ? fromBlock - 1 : fromBlock === 'deployment' && protocol.deployBlock !== undefined ? protocol.deployBlock - 1 : target,
201
+ filter: { address: protocol.positionManager as string, topics: [TRANSFER_TOPIC, null, ownerTopic] },
202
+ // ERC721 的 Transfer:第 4 个 topic 是 tokenId
203
+ collect: (log) => (log.topics.length === 4 ? BigInt(log.topics[3] as string).toString() : null),
204
+ maxRange: Math.max(1, Math.floor(options.blockRange ?? 10_000)),
205
+ maxRequests: Math.max(1, options.maxRequests ?? 20),
206
+ timeBudget: options.timeBudget ?? 5000,
207
+ confirmations: Math.max(0, Math.floor(options.confirmations ?? 5)),
208
+ signal,
209
+ onDone: report,
210
+ })
211
+ if (!progress) {
212
+ // 第一次扫描就拿不到最新区块:也告诉调用方(还没开始,没有进度)
213
+ report({ startBlock: -1, cursor: -1, items: [], blockRange: 0 }, null)
214
+ return { ids: [], cursor: -1, prune: async () => {} }
215
+ }
216
+ if (!reported) {
217
+ report(progress, null) // 暂停中 / 拿不到最新区块:这次没扫,也告诉调用方
218
+ }
219
+ const save = async (state: LogScanProgress) => {
220
+ try {
221
+ await storage.set(key, state)
222
+ } catch {
223
+ // 存储不可用:只影响下次能否接着扫
224
+ }
225
+ }
226
+ if (progress !== saved) {
227
+ await save(progress)
228
+ }
229
+ return {
230
+ ids: progress.items,
231
+ cursor: progress.cursor,
232
+ // 已经转给别人 / 销毁的 tokenId 从进度里去掉,之后不用每次再核对(转回来时会有新的 Transfer 事件,重新扫到)
233
+ prune: async (ids) => {
234
+ const drop = new Set(ids)
235
+ if (progress.items.some((id) => drop.has(id))) {
236
+ await save({ ...progress, items: progress.items.filter((id) => !drop.has(id)) })
237
+ }
238
+ },
239
+ }
240
+ }
241
+
242
+ /** V4 的原始仓位(与 lp.ts 的 RawConcentrated 结构一致) */
243
+ export interface RawV4Position {
244
+ version: 'v4'
245
+ protocol: V4Protocol
246
+ pool: string
247
+ tokenId: bigint
248
+ token0: string
249
+ token1: string
250
+ fee: number
251
+ tickLower: number
252
+ tickUpper: number
253
+ tick: number
254
+ sqrtPriceX96: bigint
255
+ liquidity: bigint
256
+ amount0: bigint
257
+ amount1: bigint
258
+ fees0: bigint
259
+ fees1: bigint
260
+ hooks: string
261
+ }
262
+
263
+ /** 查 Uniswap V4 仓位:先找 tokenId(传入的 + 扫描到的),核对仍归 owner,再批量读池子和仓位状态 */
264
+ export async function v4Positions(
265
+ provider: Provider,
266
+ owner: string,
267
+ protocols: readonly V4Protocol[],
268
+ v4: LpV4Options,
269
+ includeClosed: boolean,
270
+ maxPositions: number,
271
+ overrides: CallOverrides,
272
+ ): Promise<RawV4Position[]> {
273
+ const chainId = await provider.getChainId()
274
+ const explicit = (v4.tokenIds ?? []).flatMap((id) => {
275
+ try {
276
+ return [BigInt(id).toString()]
277
+ } catch {
278
+ return [] // 非法 tokenId 忽略
279
+ }
280
+ })
281
+ const scanOptions = v4.scan === true ? {} : v4.scan || null
282
+ const limit = Math.floor(maxPositions)
283
+ if (!(limit > 0)) {
284
+ return []
285
+ }
286
+
287
+ // 每个协议的候选 tokenId:太多时只核对最大的 maxPositions 个(tokenId 递增,越大越新)
288
+ const candidates = await Promise.all(
289
+ protocols.map(async (protocol) => {
290
+ const scan = scanOptions ? await scanTokenIds(provider, chainId, protocol, owner, scanOptions, overrides.signal) : null
291
+ const ids = [...new Set([...explicit, ...(scan?.ids ?? [])])].map(BigInt).sort((a, b) => (a < b ? 1 : a > b ? -1 : 0))
292
+ return { protocol, ids: ids.slice(0, limit), scan }
293
+ }),
294
+ )
295
+ const items = candidates.flatMap(({ protocol, ids }) => ids.map((tokenId) => ({ protocol, tokenId })))
296
+ if (!items.length) {
297
+ return []
298
+ }
299
+
300
+ // 核对仍归 owner(扫描到的可能已经转走 / 销毁),同时读仓位信息。
301
+ // 读最新状态时才清理扫描进度,并要求节点不落后于扫描进度(否则可能把刚转进来的仓位当成别人的,之后再也扫不到)
302
+ const latest = overrides.blockTag === undefined || overrides.blockTag === 'latest'
303
+ const scannedUpTo = Math.max(-1, ...candidates.map((c) => (c.scan ? c.scan.cursor : -1)))
304
+ const calls = () =>
305
+ items.flatMap(({ protocol, tokenId }) => {
306
+ const pm = provider.contract(protocol.positionManager as string, POSITION_MANAGER_ABI)
307
+ return [pm.ownerOf(tokenId), pm.getPoolAndPositionInfo(tokenId), pm.getPositionLiquidity(tokenId)]
308
+ })
309
+ let pruneSafe = latest && scannedUpTo >= 0
310
+ let first: unknown[]
311
+ if (pruneSafe) {
312
+ try {
313
+ first = await provider.tryAll(calls(), { ...overrides, minBlock: Math.max(overrides.minBlock ?? 0, scannedUpTo) })
314
+ } catch (err) {
315
+ if (overrides.signal?.aborted) {
316
+ throw err
317
+ }
318
+ // 节点都落后于扫描进度(或其他错误):照常查一次,这次不清理进度,查询本身不因此失败
319
+ pruneSafe = false
320
+ first = await provider.tryAll(calls(), overrides)
321
+ }
322
+ } else {
323
+ first = await provider.tryAll(calls(), overrides)
324
+ }
325
+ type PoolKey = { currency0: string; currency1: string; fee: bigint; tickSpacing: bigint; hooks: string }
326
+ const transferred = new Map<V4Protocol, string[]>(protocols.map((p) => [p, []]))
327
+ const owned = items.flatMap((item, i) => {
328
+ const holder = first[i * 3] as string | null
329
+ const info = first[i * 3 + 1] as { 0: PoolKey; 1: bigint } | null
330
+ // 转给了别人;或已销毁(ownerOf revert 且仓位信息为 0)
331
+ if ((holder && holder.toLowerCase() !== owner.toLowerCase()) || (holder === null && info !== null && info[1] === 0n)) {
332
+ transferred.get(item.protocol)?.push(item.tokenId.toString())
333
+ }
334
+ const liquidity = first[i * 3 + 2] as bigint | null
335
+ if (!holder || holder.toLowerCase() !== owner.toLowerCase() || !info || liquidity === null) {
336
+ return []
337
+ }
338
+ if (!includeClosed && liquidity === 0n) {
339
+ return [] // V4 修改流动性时自动领取手续费:流动性为 0 就没有未领取的手续费
340
+ }
341
+ const key = info[0]
342
+ const { tickLower, tickUpper } = decodePositionInfo(info[1])
343
+ return [{ ...item, key, poolId: poolIdOf(key), tickLower, tickUpper, liquidity }]
344
+ })
345
+ if (pruneSafe) {
346
+ await Promise.all(candidates.map(({ protocol, scan }) => scan?.prune(transferred.get(protocol) ?? [])))
347
+ }
348
+ if (!owned.length) {
349
+ return []
350
+ }
351
+
352
+ // 池子状态(每个池子一次)+ 每个仓位的手续费增长
353
+ const poolIds = [...new Set(owned.map((x) => `${(x.protocol.stateView as string).toLowerCase()}:${x.poolId}`))]
354
+ const second = await provider.tryAll(
355
+ [
356
+ ...poolIds.map((k) => {
357
+ const [stateView, poolId] = k.split(':') as [string, string]
358
+ return provider.contract(stateView, STATE_VIEW_ABI).getSlot0(poolId)
359
+ }),
360
+ ...owned.flatMap((x) => {
361
+ const sv = provider.contract(x.protocol.stateView as string, STATE_VIEW_ABI)
362
+ const salt = `0x${x.tokenId.toString(16).padStart(64, '0')}`
363
+ return [sv.getFeeGrowthInside(x.poolId, x.tickLower, x.tickUpper), sv.getPositionInfo(x.poolId, x.protocol.positionManager as string, x.tickLower, x.tickUpper, salt)]
364
+ }),
365
+ ],
366
+ overrides,
367
+ )
368
+ const slot0 = new Map(poolIds.map((k, i) => [k, second[i] as { 0: bigint; 1: bigint; 3: bigint } | null]))
369
+
370
+ return owned.flatMap((x, i): RawV4Position[] => {
371
+ const s = slot0.get(`${(x.protocol.stateView as string).toLowerCase()}:${x.poolId}`)
372
+ const inside = second[poolIds.length + i * 2] as { 0: bigint; 1: bigint } | null
373
+ const last = second[poolIds.length + i * 2 + 1] as { 1: bigint; 2: bigint } | null
374
+ if (!s || !inside || !last) {
375
+ return []
376
+ }
377
+ const sqrtPriceX96 = s[0]
378
+ const tick = Number(s[1])
379
+ const [amount0, amount1] = getAmountsForLiquidity(sqrtPriceX96, getSqrtRatioAtTick(x.tickLower), getSqrtRatioAtTick(x.tickUpper), x.liquidity)
380
+ const fee = Number(x.key.fee)
381
+ return [
382
+ {
383
+ version: 'v4',
384
+ protocol: x.protocol,
385
+ pool: x.poolId,
386
+ tokenId: x.tokenId,
387
+ token0: x.key.currency0,
388
+ token1: x.key.currency1,
389
+ // 动态手续费池子:PoolKey 里只有标记,实际手续费是 slot0 的 lpFee
390
+ fee: fee & DYNAMIC_FEE_FLAG ? Number(s[3]) : fee,
391
+ tickLower: x.tickLower,
392
+ tickUpper: x.tickUpper,
393
+ tick,
394
+ sqrtPriceX96,
395
+ liquidity: x.liquidity,
396
+ amount0,
397
+ amount1,
398
+ fees0: getFeesEarned(x.liquidity, inside[0], last[1]),
399
+ fees1: getFeesEarned(x.liquidity, inside[1], last[2]),
400
+ hooks: x.key.hooks,
401
+ },
402
+ ]
403
+ })
404
+ }
405
+
406
+ export { ZERO_ADDRESS as V4_NATIVE_CURRENCY }
package/src/owner.ts CHANGED
@@ -2,7 +2,7 @@ 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
4
  import { transferScan, type TransferScanOptions } from './scan.js'
5
- import { isTronAddress } from './tron.js'
5
+ import { TRON_CHAIN_ID, isTronAddress, isTronChain, toTronAddress } from './tron.js'
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // 代币来源:负责“这个地址可能持有哪些代币”。余额和精度一律由本库用 multicall 在链上核对。
@@ -425,7 +425,7 @@ export interface PriceSourceContext {
425
425
 
426
426
  export interface PriceSource {
427
427
  readonly name: string
428
- /** 返回 去重键(0x 地址小写)→ 美元价格;主币用键 'native'。没有价格的不返回 */
428
+ /** 返回 去重键 → 美元价格:EVM 是小写 0x 地址;Tron 是 T 开头的 base58 地址(区分大小写);主币用键 'native'。没有价格的不返回 */
429
429
  prices(ctx: PriceSourceContext): Promise<Map<string, number>>
430
430
  }
431
431
 
@@ -454,6 +454,7 @@ export const DEFILLAMA_CHAINS: Readonly<Record<number, string>> = {
454
454
  80094: 'berachain',
455
455
  81457: 'blast',
456
456
  534352: 'scroll',
457
+ [TRON_CHAIN_ID.mainnet]: 'tron',
457
458
  }
458
459
 
459
460
  /** 主币价格:按主币 symbol 对应 CoinGecko 的币种 id(DefiLlama 用 coingecko:<id> 查) */
@@ -484,7 +485,17 @@ export function defillamaPrices(options: { minConfidence?: number } = {}): Price
484
485
  const result = new Map<string, number>()
485
486
  const slug = DEFILLAMA_CHAINS[chainId]
486
487
  const nativeId = nativeSymbol ? NATIVE_PRICE_IDS[nativeSymbol.toUpperCase()] : undefined
487
- const keys = [...(nativeId ? [`coingecko:${nativeId}`] : []), ...(slug ? tokens.map((t) => `${slug}:${t.toLowerCase()}`) : [])]
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]
488
499
  const batches: string[][] = []
489
500
  for (let i = 0; i < keys.length; i += 100) {
490
501
  batches.push(keys.slice(i, i + 100))
@@ -497,7 +508,7 @@ export function defillamaPrices(options: { minConfidence?: number } = {}): Price
497
508
  if (typeof coin.price !== 'number' || !Number.isFinite(coin.price) || (coin.confidence !== undefined && coin.confidence < minConfidence)) {
498
509
  continue
499
510
  }
500
- result.set(key.startsWith('coingecko:') ? 'native' : key.slice(key.indexOf(':') + 1).toLowerCase(), coin.price)
511
+ result.set(key.startsWith('coingecko:') ? 'native' : (resultKey.get(key.toLowerCase()) ?? key.slice(key.indexOf(':') + 1).toLowerCase()), coin.price)
501
512
  }
502
513
  }
503
514
  return result
@@ -603,8 +614,10 @@ export async function ownerTokens(provider: Provider, owner: string, options: Ow
603
614
  held.length ? provider.tokens(held.map((x) => x.meta.address), { ...overrides, fields: ['decimals', 'name', 'symbol'] }) : Promise.resolve([]),
604
615
  nativeAddress ? provider.tokens([nativeAddress], { fields: ['name', 'symbol'] }) : Promise.resolve([undefined]),
605
616
  ])
617
+ // 价格源的键:Tron 上统一用 T 地址(代币来源给的可能是 0x 格式)
618
+ const priceKey = (address: string) => (isTronChain(chainId) ? toTronAddress(address) : addressKey(address))
606
619
  const priceMap = priceSource
607
- ? await priceSource.prices({ chainId, tokens: held.map((x) => x.meta.address), nativeSymbol: nativeAddress ? (nativeInfo?.symbol ?? null) : null, fetch: fetchFn })
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 })
608
621
  : new Map<string, number>()
609
622
  const valueOf = (formatted: string, price: number | undefined) => (price === undefined ? null : Number(formatted) * price)
610
623
 
@@ -616,7 +629,7 @@ export async function ownerTokens(provider: Provider, owner: string, options: Ow
616
629
  }
617
630
  const decimals = chain.decimals
618
631
  const formatted = decimals === balance.decimals ? balance.formatted : formatUnits(BigInt(balance.balance), decimals)
619
- const price = priceMap.get(addressKey(meta.address))
632
+ const price = priceMap.get(priceKey(meta.address))
620
633
  return [
621
634
  {
622
635
  token: meta.address,