@w3lib/evm-batch-call 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +242 -2
  3. package/dist/fallback.d.ts +5 -0
  4. package/dist/fallback.d.ts.map +1 -1
  5. package/dist/fallback.js +7 -0
  6. package/dist/fallback.js.map +1 -1
  7. package/dist/liquidity/common.d.ts +72 -0
  8. package/dist/liquidity/common.d.ts.map +1 -0
  9. package/dist/liquidity/common.js +271 -0
  10. package/dist/liquidity/common.js.map +1 -0
  11. package/dist/liquidity/concentrated.d.ts +19 -0
  12. package/dist/liquidity/concentrated.d.ts.map +1 -0
  13. package/dist/liquidity/concentrated.js +106 -0
  14. package/dist/liquidity/concentrated.js.map +1 -0
  15. package/dist/liquidity/index.d.ts +42 -0
  16. package/dist/liquidity/index.d.ts.map +1 -0
  17. package/dist/liquidity/index.js +143 -0
  18. package/dist/liquidity/index.js.map +1 -0
  19. package/dist/liquidity/types.d.ts +182 -0
  20. package/dist/liquidity/types.d.ts.map +1 -0
  21. package/dist/liquidity/types.js +2 -0
  22. package/dist/liquidity/types.js.map +1 -0
  23. package/dist/liquidity/v2.d.ts +5 -0
  24. package/dist/liquidity/v2.d.ts.map +1 -0
  25. package/dist/liquidity/v2.js +342 -0
  26. package/dist/liquidity/v2.js.map +1 -0
  27. package/dist/liquidity/v3.d.ts +6 -0
  28. package/dist/liquidity/v3.d.ts.map +1 -0
  29. package/dist/liquidity/v3.js +310 -0
  30. package/dist/liquidity/v3.js.map +1 -0
  31. package/dist/liquidity/v4.d.ts +5 -0
  32. package/dist/liquidity/v4.d.ts.map +1 -0
  33. package/dist/liquidity/v4.js +279 -0
  34. package/dist/liquidity/v4.js.map +1 -0
  35. package/dist/lp.d.ts +156 -0
  36. package/dist/lp.d.ts.map +1 -0
  37. package/dist/lp.js +510 -0
  38. package/dist/lp.js.map +1 -0
  39. package/dist/lpMath.d.ts +80 -0
  40. package/dist/lpMath.d.ts.map +1 -0
  41. package/dist/lpMath.js +312 -0
  42. package/dist/lpMath.js.map +1 -0
  43. package/dist/lpV4.d.ts +108 -0
  44. package/dist/lpV4.d.ts.map +1 -0
  45. package/dist/lpV4.js +283 -0
  46. package/dist/lpV4.js.map +1 -0
  47. package/dist/owner.d.ts +1 -1
  48. package/dist/owner.d.ts.map +1 -1
  49. package/dist/owner.js +18 -5
  50. package/dist/owner.js.map +1 -1
  51. package/dist/scan.d.ts +54 -0
  52. package/dist/scan.d.ts.map +1 -1
  53. package/dist/scan.js +124 -92
  54. package/dist/scan.js.map +1 -1
  55. package/dist/source.d.ts +2 -0
  56. package/dist/source.d.ts.map +1 -1
  57. package/dist/source.js +4 -0
  58. package/dist/source.js.map +1 -1
  59. package/dist/subpaths/liquidity.d.ts +24 -0
  60. package/dist/subpaths/liquidity.d.ts.map +1 -0
  61. package/dist/subpaths/liquidity.js +21 -0
  62. package/dist/subpaths/liquidity.js.map +1 -0
  63. package/dist/subpaths/lp.d.ts +33 -0
  64. package/dist/subpaths/lp.d.ts.map +1 -0
  65. package/dist/subpaths/lp.js +47 -0
  66. package/dist/subpaths/lp.js.map +1 -0
  67. package/dist/subpaths/write.d.ts +20 -0
  68. package/dist/subpaths/write.d.ts.map +1 -0
  69. package/dist/subpaths/write.js +18 -0
  70. package/dist/subpaths/write.js.map +1 -0
  71. package/dist/tron.d.ts +2 -0
  72. package/dist/tron.d.ts.map +1 -1
  73. package/dist/tron.js +4 -0
  74. package/dist/tron.js.map +1 -1
  75. package/dist/write/batch.d.ts +88 -0
  76. package/dist/write/batch.d.ts.map +1 -0
  77. package/dist/write/batch.js +231 -0
  78. package/dist/write/batch.js.map +1 -0
  79. package/dist/write/collect.d.ts +53 -0
  80. package/dist/write/collect.d.ts.map +1 -0
  81. package/dist/write/collect.js +255 -0
  82. package/dist/write/collect.js.map +1 -0
  83. package/dist/write/rpc.d.ts +68 -0
  84. package/dist/write/rpc.d.ts.map +1 -0
  85. package/dist/write/rpc.js +236 -0
  86. package/dist/write/rpc.js.map +1 -0
  87. package/dist/write/send.d.ts +124 -0
  88. package/dist/write/send.d.ts.map +1 -0
  89. package/dist/write/send.js +373 -0
  90. package/dist/write/send.js.map +1 -0
  91. package/dist/write/signer.d.ts +58 -0
  92. package/dist/write/signer.d.ts.map +1 -0
  93. package/dist/write/signer.js +41 -0
  94. package/dist/write/signer.js.map +1 -0
  95. package/dist/write/transfer.d.ts +83 -0
  96. package/dist/write/transfer.d.ts.map +1 -0
  97. package/dist/write/transfer.js +259 -0
  98. package/dist/write/transfer.js.map +1 -0
  99. package/dist/write/tron.d.ts +60 -0
  100. package/dist/write/tron.d.ts.map +1 -0
  101. package/dist/write/tron.js +960 -0
  102. package/dist/write/tron.js.map +1 -0
  103. package/package.json +26 -5
  104. package/src/fallback.ts +8 -0
  105. package/src/liquidity/common.ts +314 -0
  106. package/src/liquidity/concentrated.ts +121 -0
  107. package/src/liquidity/index.ts +171 -0
  108. package/src/liquidity/types.ts +194 -0
  109. package/src/liquidity/v2.ts +371 -0
  110. package/src/liquidity/v3.ts +382 -0
  111. package/src/liquidity/v4.ts +348 -0
  112. package/src/lp.ts +739 -0
  113. package/src/lpMath.ts +336 -0
  114. package/src/lpV4.ts +406 -0
  115. package/src/owner.ts +19 -6
  116. package/src/scan.ts +155 -86
  117. package/src/source.ts +5 -0
  118. package/src/subpaths/liquidity.ts +52 -0
  119. package/src/subpaths/lp.ts +90 -0
  120. package/src/subpaths/write.ts +26 -0
  121. package/src/tron.ts +5 -0
  122. package/src/write/batch.ts +302 -0
  123. package/src/write/collect.ts +312 -0
  124. package/src/write/rpc.ts +277 -0
  125. package/src/write/send.ts +490 -0
  126. package/src/write/signer.ts +90 -0
  127. package/src/write/transfer.ts +344 -0
  128. package/src/write/tron.ts +1105 -0
package/src/lp.ts ADDED
@@ -0,0 +1,739 @@
1
+ import type { CallOverrides } from './aggregate.js'
2
+ import type { Call } from './call.js'
3
+ import { formatUnits } from './erc20.js'
4
+ import { getAmountsForLiquidity, getFeeGrowthInside, getFeesEarned, getSqrtRatioAtTick, sqrtPriceToPrice, tickToPrice } from './lpMath.js'
5
+ import { v4Positions, type LpV4Options } from './lpV4.js'
6
+ import { defillamaPrices, type PriceSource, type TokenSource } from './owner.js'
7
+ import type { Provider } from './provider.js'
8
+ import { TRON_CHAIN_ID, isTronAddress, isTronChain, toEvmAddress, toTronAddress } from './tron.js'
9
+ import { withSignal } from './util.js'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // 协议表
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export type LpVersion = 'v2' | 'v3' | 'v4'
16
+
17
+ /** 一个 DEX 在一条链上的部署 */
18
+ export interface LpProtocol {
19
+ /** 协议 id,如 'uniswap-v3'、'pancakeswap-v2' */
20
+ id: string
21
+ /** 显示名,如 'Uniswap V3' */
22
+ name: string
23
+ version: LpVersion
24
+ chainId: number
25
+ /** V2:工厂合约(getPair、识别 LP 代币属于哪个协议);V3:池子的工厂合约(getPool) */
26
+ factory?: string
27
+ /** V2:Router 合约(添加 / 移除流动性用,见子路径 `/liquidity`) */
28
+ router?: string
29
+ /**
30
+ * V2:协议手续费开启(factory.feeTo 不为零)时,每次添加 / 移除前给 feeTo 增发的 LP 占 √k 增长的比例:
31
+ * Uniswap 及大多数分叉是 '1/6',PancakeSwap 是 '8/25'。不填时按 '8/25'(增发得多,算出的移除数量偏少,最小值更宽松)
32
+ */
33
+ protocolFee?: '1/6' | '8/25'
34
+ /** V3 / V4:仓位 NFT 合约(NonfungiblePositionManager / PositionManager) */
35
+ positionManager?: string
36
+ /** V4:StateView(读池子和仓位的状态) */
37
+ stateView?: string
38
+ /** V4:仓位合约的部署区块(深度扫描 Transfer 事件的默认起点;不知道时为 undefined) */
39
+ deployBlock?: number
40
+ }
41
+
42
+ const v2 = (id: string, name: string, chainId: number, factory: string, router: string, protocolFee: '1/6' | '8/25'): LpProtocol => ({
43
+ id,
44
+ name,
45
+ version: 'v2',
46
+ chainId,
47
+ factory,
48
+ router,
49
+ protocolFee,
50
+ })
51
+ const v3 = (id: string, name: string, chainId: number, positionManager: string, factory: string): LpProtocol => ({ id, name, version: 'v3', chainId, positionManager, factory })
52
+ const v4 = (chainId: number, positionManager: string, stateView: string, deployBlock?: number): LpProtocol => ({
53
+ id: 'uniswap-v4',
54
+ name: 'Uniswap V4',
55
+ version: 'v4',
56
+ chainId,
57
+ positionManager,
58
+ stateView,
59
+ ...(deployBlock !== undefined ? { deployBlock } : {}),
60
+ })
61
+
62
+ const UNI_V3_NPM = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88'
63
+ const UNI_V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984'
64
+ const CAKE_V3_NPM = '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364'
65
+ const CAKE_V3_FACTORY = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865'
66
+ const SUSHI_V2_FACTORY = '0xc35DADB65012eC5796536bD9864eD8773aBc74C4'
67
+ const SUSHI_V2_ROUTER = '0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506'
68
+ const UNI_V2_ROUTER_L2 = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24'
69
+ const CAKE_V2_ROUTER_L2 = '0x8cFe327CEc66d1C090Dd72bd0FF11d690C33a2Eb'
70
+
71
+ /**
72
+ * 内置的 DEX 部署(2026-10 在链上逐个核对:V3 的 name / factory、V2 的 allPairsLength、V2 Router 的 factory / WETH)。
73
+ * 不在表里的 DEX / 链可以通过 `protocols` 选项传入自己的 LpProtocol。
74
+ */
75
+ export const LP_PROTOCOLS: readonly LpProtocol[] = [
76
+ // Ethereum
77
+ v2('uniswap-v2', 'Uniswap V2', 1, '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', '1/6'),
78
+ v2('sushiswap-v2', 'SushiSwap', 1, '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac', '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F', '1/6'),
79
+ v2('pancakeswap-v2', 'PancakeSwap V2', 1, '0x1097053Fd2ea711dad45caCcc45EfF7548fCB362', '0xEfF92A263d31888d860bD50809A8D171709b7b1c', '8/25'),
80
+ v3('uniswap-v3', 'Uniswap V3', 1, UNI_V3_NPM, UNI_V3_FACTORY),
81
+ v3('pancakeswap-v3', 'PancakeSwap V3', 1, CAKE_V3_NPM, CAKE_V3_FACTORY),
82
+ // BSC
83
+ v2('pancakeswap-v2', 'PancakeSwap V2', 56, '0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73', '0x10ED43C718714eb63d5aA57B78B54704E256024E', '8/25'),
84
+ v2('uniswap-v2', 'Uniswap V2', 56, '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6', UNI_V2_ROUTER_L2, '1/6'),
85
+ v2('sushiswap-v2', 'SushiSwap', 56, SUSHI_V2_FACTORY, SUSHI_V2_ROUTER, '1/6'),
86
+ v3('pancakeswap-v3', 'PancakeSwap V3', 56, CAKE_V3_NPM, CAKE_V3_FACTORY),
87
+ v3('uniswap-v3', 'Uniswap V3', 56, '0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613', '0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7'),
88
+ // Base
89
+ v2('uniswap-v2', 'Uniswap V2', 8453, '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6', UNI_V2_ROUTER_L2, '1/6'),
90
+ v2('pancakeswap-v2', 'PancakeSwap V2', 8453, '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E', CAKE_V2_ROUTER_L2, '8/25'),
91
+ v3('uniswap-v3', 'Uniswap V3', 8453, '0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1', '0x33128a8fC17869897dcE68Ed026d694621f6FDfD'),
92
+ v3('pancakeswap-v3', 'PancakeSwap V3', 8453, CAKE_V3_NPM, CAKE_V3_FACTORY),
93
+ // Arbitrum
94
+ v2('uniswap-v2', 'Uniswap V2', 42161, '0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9', UNI_V2_ROUTER_L2, '1/6'),
95
+ v2('sushiswap-v2', 'SushiSwap', 42161, SUSHI_V2_FACTORY, SUSHI_V2_ROUTER, '1/6'),
96
+ v2('pancakeswap-v2', 'PancakeSwap V2', 42161, '0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E', CAKE_V2_ROUTER_L2, '8/25'),
97
+ v3('uniswap-v3', 'Uniswap V3', 42161, UNI_V3_NPM, UNI_V3_FACTORY),
98
+ v3('pancakeswap-v3', 'PancakeSwap V3', 42161, CAKE_V3_NPM, CAKE_V3_FACTORY),
99
+ // Optimism
100
+ v2('uniswap-v2', 'Uniswap V2', 10, '0x0c3c1c532F1e39EdF36BE9Fe0bE1410313E074Bf', '0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2', '1/6'),
101
+ v3('uniswap-v3', 'Uniswap V3', 10, UNI_V3_NPM, UNI_V3_FACTORY),
102
+ // Polygon
103
+ v2('uniswap-v2', 'Uniswap V2', 137, '0x9e5A52f57b3038F1B8EeE45F28b3C1967e22799C', '0xedf6066a2b290C185783862C7F4776A2C8077AD1', '1/6'),
104
+ v2('sushiswap-v2', 'SushiSwap', 137, SUSHI_V2_FACTORY, SUSHI_V2_ROUTER, '1/6'),
105
+ v3('uniswap-v3', 'Uniswap V3', 137, UNI_V3_NPM, UNI_V3_FACTORY),
106
+ // Avalanche
107
+ v3('uniswap-v3', 'Uniswap V3', 43114, '0x655C406EBFa14EE2006250925e54ec43AD184f8B', '0x740b1c1de25031C31FF4fC9A62f554A55cdC1baD'),
108
+ // Uniswap V4(PositionManager / StateView 在链上核对过:name、poolManager 一致)
109
+ v4(1, '0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e', '0x7ffe42c4a5deea5b0fec41c94c136cf115597227', 21689089),
110
+ v4(56, '0x7a4a5c919ae2541aed11041a1aeee68f1287f95b', '0xd13dd3d6e93f276fafc9db9e6bb47c1180aee0c4', 45970613),
111
+ v4(8453, '0x7c5f5a4bbd8fd63184577525326123b519429bdc', '0xa3c0c9b65bad0b08107aa264b0f3db444b867a71', 25350993),
112
+ v4(42161, '0xd88f38f930b7952f2db2432cb002e7abbf3dd869', '0x76fd297e2d437cd7f76d50f01afe6160f86e9990', 297842893),
113
+ v4(10, '0x3c3ea4b57a46241e54610e5f022e5c45859a1017', '0xc18a3169788f4f75a170290584eca6395c75ecdb', 130947681),
114
+ v4(137, '0x1ec2ebf4f37e7363fdfe3551602425af0b3ceef9', '0x5ea1bd7974c8a611cbab0bdcafcb1d9cc9b3ba5a', 66980394),
115
+ // Tron:SunSwap(协议手续费与 Uniswap 相同,1/6,用主网数据核对过;V2 工厂的 allPairsLength、V3 仓位合约的 name / factory 在链上核对过)
116
+ v2('sunswap-v2', 'SunSwap V2', TRON_CHAIN_ID.mainnet, 'TKWJdrQkqHisa1X8HUdHEfREvTzw4pMAaY', 'TNJVzGqKBWkJxJB5XYSqGAwUTV15U24pPq', '1/6'),
117
+ v3('sunswap-v3', 'SunSwap V3', TRON_CHAIN_ID.mainnet, 'TLSWrv7eC1AZCXkRjpqMZUmvgd99cj7pPF', 'TThJt8zaJzJMhCEScH7zWKnp5buVZqys9x'),
118
+ ]
119
+
120
+ /**
121
+ * 各链常见的计价币:查 V2 LP 时,用“传入的代币 × 计价币”(以及计价币之间)在工厂合约里查池子。
122
+ * 主流池子几乎都有一边是这些币
123
+ */
124
+ export const LP_QUOTE_TOKENS: Readonly<Record<number, readonly string[]>> = {
125
+ 1: [
126
+ '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH
127
+ '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
128
+ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
129
+ '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
130
+ ],
131
+ 56: [
132
+ '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', // WBNB
133
+ '0x55d398326f99059fF775485246999027B3197955', // USDT
134
+ '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', // USDC
135
+ '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', // BUSD
136
+ ],
137
+ 8453: [
138
+ '0x4200000000000000000000000000000000000006', // WETH
139
+ '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
140
+ ],
141
+ 42161: [
142
+ '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH
143
+ '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC
144
+ '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', // USDT
145
+ ],
146
+ 10: [
147
+ '0x4200000000000000000000000000000000000006', // WETH
148
+ '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC
149
+ '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', // USDT
150
+ ],
151
+ 137: [
152
+ '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', // WPOL
153
+ '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC
154
+ '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', // USDT
155
+ ],
156
+ 43114: [
157
+ '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', // WAVAX
158
+ '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', // USDC
159
+ ],
160
+ [TRON_CHAIN_ID.mainnet]: [
161
+ 'TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR', // WTRX
162
+ 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', // USDT
163
+ 'TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn', // USDD
164
+ 'TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S', // SUN
165
+ ],
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // 结果类型
170
+ // ---------------------------------------------------------------------------
171
+
172
+ export interface LpToken {
173
+ address: string
174
+ /** 是否是主币(V4 的池子可以直接用主币,地址为零地址) */
175
+ native: boolean
176
+ symbol: string | null
177
+ /** 读不到精度时为 null(这时 formatted 字段也为 null) */
178
+ decimals: number | null
179
+ }
180
+
181
+ interface LpPositionBase {
182
+ chainId: number
183
+ /** 协议 id,如 'uniswap-v3';工厂不在协议表里的 V2 LP 为 'v2:<工厂地址>'(Tron 上是 T 地址;读不到工厂时为 'v2:unknown') */
184
+ protocol: string
185
+ /** 协议显示名,如 'Uniswap V3';不认识的 V2 工厂为 'Unknown V2' */
186
+ protocolName: string
187
+ /** 池子地址(V4 为 poolId) */
188
+ pool: string
189
+ token0: LpToken
190
+ token1: LpToken
191
+ /** 这个仓位对应的 token0 / token1 数量(最小单位的十进制字符串;V3 / V4 是现在全部移除能拿到的数量,不含手续费) */
192
+ amount0: string
193
+ amount1: string
194
+ /** 按精度换算后的数量;读不到精度时为 null */
195
+ amount0Formatted: string | null
196
+ amount1Formatted: string | null
197
+ /** 开启 prices 时:这个仓位的美元价值(数量不为 0 的代币没有价格时为 null);没开启时不返回 */
198
+ valueUsd?: number | null
199
+ }
200
+
201
+ /** V2(Uniswap V2 及其分叉):持有池子的 LP 代币,手续费已经计入份额,没有单独的未领取手续费 */
202
+ export interface LpV2Position extends LpPositionBase {
203
+ version: 'v2'
204
+ /** 持有的 LP 代币数量(最小单位) */
205
+ balance: string
206
+ /** LP 代币总量 */
207
+ totalSupply: string
208
+ /** 占池子的份额(0~1) */
209
+ share: number
210
+ }
211
+
212
+ /** V3 / V4(集中流动性):一个仓位 NFT */
213
+ export interface LpConcentratedPosition extends LpPositionBase {
214
+ version: 'v3' | 'v4'
215
+ /** 仓位 NFT 的 tokenId(十进制字符串) */
216
+ tokenId: string
217
+ /** 仓位 NFT 合约 */
218
+ positionManager: string
219
+ /** 手续费档位(百万分之一:500 = 0.05%);V4 动态手续费池子为当前的 lpFee */
220
+ fee: number
221
+ tickLower: number
222
+ tickUpper: number
223
+ /** 池子当前的 tick */
224
+ tick: number
225
+ /** 当前价格是否在仓位的区间内(在区间内才赚手续费) */
226
+ inRange: boolean
227
+ /** 仓位的流动性 L */
228
+ liquidity: string
229
+ /** 价格都是 token1 / token0(按精度换算,只用于展示);读不到精度时为 null */
230
+ price: number | null
231
+ priceLower: number | null
232
+ priceUpper: number | null
233
+ /** 未领取的手续费(最小单位) */
234
+ fees0: string
235
+ fees1: string
236
+ fees0Formatted: string | null
237
+ fees1Formatted: string | null
238
+ /** 开启 prices 时:未领取手续费的美元价值;没开启时不返回 */
239
+ feesUsd?: number | null
240
+ /** V4:池子的 hooks 合约(没有时为零地址) */
241
+ hooks?: string
242
+ }
243
+
244
+ export type LpPosition = LpV2Position | LpConcentratedPosition
245
+
246
+ export interface LpPositionsOptions extends CallOverrides {
247
+ /**
248
+ * 查哪些协议:协议 id(如 ['uniswap-v3', 'pancakeswap-v2']),或自己的 LpProtocol(内置表里没有的 DEX / 链)。
249
+ * 默认这条链上内置的全部协议
250
+ */
251
+ protocols?: ReadonlyArray<string | LpProtocol>
252
+ /** 只查这些类型,默认全部 */
253
+ versions?: readonly LpVersion[]
254
+ /** V2:直接指定要查的池子(LP 代币)地址,不限协议 */
255
+ pools?: readonly string[]
256
+ /** V2:用这些代币 × 计价币在工厂合约里找池子(如用户持有的代币,可以来自资产列表) */
257
+ tokens?: readonly string[]
258
+ /** V2:计价币,默认 LP_QUOTE_TOKENS 里这条链的 */
259
+ quoteTokens?: readonly string[]
260
+ /** V2:从代币来源发现 LP 代币(如 alchemy({ apiKey }) 能列出地址持有的全部 ERC20,包括 LP 代币) */
261
+ source?: TokenSource
262
+ /**
263
+ * V4:怎么找仓位。V4 的仓位合约不能按地址列出 NFT:传已知的 tokenIds,或开启 scan 扫描 Transfer 事件。
264
+ * 两个都没有时不查 V4
265
+ */
266
+ v4?: LpV4Options
267
+ /** V3 / V4:也返回已经撤出全部流动性、手续费也领完了的仓位(NFT 还在),默认不返回 */
268
+ includeClosed?: boolean
269
+ /** V3 / V4:每个协议最多核对多少个仓位 NFT(超出时取最近的:V3 按持有顺序取最后的,V4 取 tokenId 最大的),默认 500 */
270
+ maxPositions?: number
271
+ /** 美元价值(可选):true 用 DefiLlama,也可以传自己的 PriceSource。默认不查 */
272
+ prices?: boolean | PriceSource
273
+ /**
274
+ * 价格源出错时调用(服务不可用、被拦截等):这时照样返回仓位,只是 valueUsd / feesUsd 为 null,
275
+ * 用它区分“价格服务出错”和“这个代币没有价格”。回调本身出错会被忽略
276
+ */
277
+ onPriceError?: (error: unknown) => void
278
+ /** 自定义 fetch(价格、代币来源用) */
279
+ fetch?: typeof fetch
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // ABI
284
+ // ---------------------------------------------------------------------------
285
+
286
+ const V2_FACTORY_ABI = ['function getPair(address tokenA, address tokenB) view returns (address)']
287
+ const V2_PAIR_ABI = [
288
+ 'function balanceOf(address owner) view returns (uint256)',
289
+ 'function totalSupply() view returns (uint256)',
290
+ 'function token0() view returns (address)',
291
+ 'function token1() view returns (address)',
292
+ 'function factory() view returns (address)',
293
+ 'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
294
+ ]
295
+ const V3_NPM_ABI = [
296
+ 'function balanceOf(address owner) view returns (uint256)',
297
+ 'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)',
298
+ 'function positions(uint256 tokenId) view returns (uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)',
299
+ ]
300
+ const V3_FACTORY_ABI = ['function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)']
301
+ // 只声明前几个返回值:各分叉后面的字段类型不同(如 PancakeSwap 的 feeProtocol 是 uint32),多出来的数据解码时忽略
302
+ const V3_POOL_ABI = [
303
+ 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick)',
304
+ 'function feeGrowthGlobal0X128() view returns (uint256)',
305
+ 'function feeGrowthGlobal1X128() view returns (uint256)',
306
+ 'function ticks(int24 tick) view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128)',
307
+ ]
308
+
309
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
310
+ const U128_MASK = (1n << 128n) - 1n
311
+ /** 地址统一成小写的 0x 格式再比较(Tron 的 T 地址先转换;合约调用返回的地址都是 0x 格式) */
312
+ const lower = (address: string) => (isTronAddress(address) ? toEvmAddress(address) : address).toLowerCase()
313
+
314
+ /** 选项里传给合约调用的部分(区块、取消等) */
315
+ function callOverrides(options: CallOverrides): CallOverrides {
316
+ const { blockTag, from, minBlock, signal } = options
317
+ return { ...(blockTag !== undefined ? { blockTag } : {}), ...(from !== undefined ? { from } : {}), ...(minBlock !== undefined ? { minBlock } : {}), ...(signal ? { signal } : {}) }
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // 查询
322
+ // ---------------------------------------------------------------------------
323
+
324
+ /** 选出要查的协议 */
325
+ export function resolveLpProtocols(chainId: number, options: Pick<LpPositionsOptions, 'protocols' | 'versions'> = {}): LpProtocol[] {
326
+ const builtin = LP_PROTOCOLS.filter((p) => p.chainId === chainId)
327
+ const selected = options.protocols
328
+ ? options.protocols.flatMap((p) => {
329
+ if (typeof p !== 'string') {
330
+ return p.chainId === chainId ? [p] : []
331
+ }
332
+ const found = builtin.filter((b) => b.id === p)
333
+ if (!found.length) {
334
+ throw new Error(`Unknown LP protocol ${p} on chain ${chainId}; pass an LpProtocol object for protocols that are not built in`)
335
+ }
336
+ return found
337
+ })
338
+ : builtin
339
+ return options.versions ? selected.filter((p) => options.versions?.includes(p.version)) : selected
340
+ }
341
+
342
+ /** 解出 V2 / V3 的原始仓位(还没有代币信息) */
343
+ interface RawV2 {
344
+ version: 'v2'
345
+ protocol: LpProtocol | null
346
+ factory: string
347
+ pool: string
348
+ token0: string
349
+ token1: string
350
+ balance: bigint
351
+ totalSupply: bigint
352
+ amount0: bigint
353
+ amount1: bigint
354
+ }
355
+
356
+ interface RawConcentrated {
357
+ version: 'v3' | 'v4'
358
+ protocol: LpProtocol
359
+ pool: string
360
+ tokenId: bigint
361
+ token0: string
362
+ token1: string
363
+ fee: number
364
+ tickLower: number
365
+ tickUpper: number
366
+ tick: number
367
+ sqrtPriceX96: bigint
368
+ liquidity: bigint
369
+ amount0: bigint
370
+ amount1: bigint
371
+ fees0: bigint
372
+ fees1: bigint
373
+ hooks?: string
374
+ }
375
+
376
+ type RawPosition = RawV2 | RawConcentrated
377
+
378
+ async function v2Positions(provider: Provider, owner: string, protocols: LpProtocol[], allV2: LpProtocol[], options: LpPositionsOptions, chainId: number): Promise<RawV2[]> {
379
+ const { pools = [], tokens = [], quoteTokens = LP_QUOTE_TOKENS[chainId] ?? [], source, fetch: fetchOption } = options
380
+ const overrides = callOverrides(options)
381
+ const candidates = new Set(pools.map(lower))
382
+
383
+ // 代币来源(如 alchemy)列出的代币里可能有 LP 代币:都当作候选,下面用 token0 / token1 / factory 识别
384
+ if (source) {
385
+ const fetchFn = fetchOption ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
386
+ const discovered = (await source.discover({ chainId, owner, fetch: fetchFn, provider })) ?? []
387
+ for (const token of discovered) {
388
+ candidates.add(lower(token.address))
389
+ }
390
+ }
391
+
392
+ // 传入的代币 × 计价币(以及计价币之间)在每个 V2 工厂里查池子
393
+ const quotes = [...new Set(quoteTokens.map(lower))]
394
+ const bases = [...new Set([...tokens.map(lower), ...quotes])]
395
+ const pairKeys = new Set<string>()
396
+ const getPair: Call[] = []
397
+ for (const protocol of protocols) {
398
+ const factory = provider.contract(protocol.factory as string, V2_FACTORY_ABI)
399
+ for (const base of bases) {
400
+ for (const quote of quotes) {
401
+ const key = `${lower(protocol.factory as string)}:${[base, quote].sort().join(':')}`
402
+ if (base === quote || pairKeys.has(key)) {
403
+ continue
404
+ }
405
+ pairKeys.add(key)
406
+ getPair.push(factory.getPair(base, quote))
407
+ }
408
+ }
409
+ }
410
+ for (const pair of await provider.tryAll<string>(getPair, overrides)) {
411
+ if (pair && pair !== ZERO_ADDRESS) {
412
+ candidates.add(lower(pair))
413
+ }
414
+ }
415
+ if (!candidates.size) {
416
+ return []
417
+ }
418
+
419
+ // 持有的 LP 代币
420
+ const list = [...candidates]
421
+ const balances = await provider.tryAll<bigint>(
422
+ list.map((pair) => provider.contract(pair, V2_PAIR_ABI).balanceOf(owner)),
423
+ overrides,
424
+ )
425
+ const held = list.filter((_, i) => (balances[i] ?? 0n) > 0n)
426
+ if (!held.length) {
427
+ return []
428
+ }
429
+
430
+ // 池子详情:token0 / token1 / 储备 / 总量 / 工厂(识别协议;读不到的不是 V2 池子)
431
+ const details = await provider.tryAll(
432
+ held.flatMap((pair) => {
433
+ const c = provider.contract(pair, V2_PAIR_ABI)
434
+ return [c.token0(), c.token1(), c.getReserves(), c.totalSupply(), c.factory()]
435
+ }),
436
+ overrides,
437
+ )
438
+ const selectedFactories = new Set(protocols.map((p) => lower(p.factory as string)))
439
+ return held.flatMap((pool, i): RawV2[] => {
440
+ const [token0, token1, reserves, totalSupply, factory] = details.slice(i * 5, i * 5 + 5) as [string | null, string | null, { 0: bigint; 1: bigint } | null, bigint | null, string | null]
441
+ if (!token0 || !token1 || !reserves || !totalSupply) {
442
+ return []
443
+ }
444
+ const factoryAddress = factory ? lower(factory) : ''
445
+ const protocol = allV2.find((p) => lower(p.factory as string) === factoryAddress) ?? null
446
+ // 显式传入的池子不限协议;查出来 / 来源发现的只保留选中的协议(来源里的普通代币没有 token0,上面已经去掉)
447
+ if (!pools.some((p) => lower(p) === pool) && protocol && !selectedFactories.has(factoryAddress)) {
448
+ return []
449
+ }
450
+ const balance = balances[list.indexOf(pool)] as bigint
451
+ return [
452
+ {
453
+ version: 'v2',
454
+ protocol,
455
+ factory: factoryAddress,
456
+ pool,
457
+ token0,
458
+ token1,
459
+ balance,
460
+ totalSupply,
461
+ amount0: totalSupply === 0n ? 0n : (reserves[0] * balance) / totalSupply,
462
+ amount1: totalSupply === 0n ? 0n : (reserves[1] * balance) / totalSupply,
463
+ },
464
+ ]
465
+ })
466
+ }
467
+
468
+ async function v3Positions(provider: Provider, owner: string, protocols: LpProtocol[], options: LpPositionsOptions): Promise<RawConcentrated[]> {
469
+ const { includeClosed = false } = options
470
+ const maxPositions = Math.floor(options.maxPositions ?? 500)
471
+ const overrides = callOverrides(options)
472
+ if (!protocols.length || !(maxPositions > 0)) {
473
+ return []
474
+ }
475
+
476
+ // 每个协议持有几个仓位 NFT → 逐个取 tokenId
477
+ const counts = await provider.tryAll<bigint>(
478
+ protocols.map((p) => provider.contract(p.positionManager as string, V3_NPM_ABI).balanceOf(owner)),
479
+ overrides,
480
+ )
481
+ const indexCalls: Call[] = []
482
+ const indexOwners: LpProtocol[] = []
483
+ protocols.forEach((protocol, i) => {
484
+ // 仓位很多的地址(做市机器人等)只看最近的 maxPositions 个:新铸造的 NFT 排在最后
485
+ const count = Number(counts[i] ?? 0n)
486
+ const npm = provider.contract(protocol.positionManager as string, V3_NPM_ABI)
487
+ for (let index = Math.max(0, count - maxPositions); index < count; index++) {
488
+ indexCalls.push(npm.tokenOfOwnerByIndex(owner, index))
489
+ indexOwners.push(protocol)
490
+ }
491
+ })
492
+ if (!indexCalls.length) {
493
+ return []
494
+ }
495
+ const tokenIds = await provider.tryAll<bigint>(indexCalls, overrides)
496
+ const ids = tokenIds.flatMap((id, i) => (id === null ? [] : [{ protocol: indexOwners[i] as LpProtocol, tokenId: id }]))
497
+
498
+ // 仓位详情
499
+ type PositionData = { token0: string; token1: string; fee: bigint; tickLower: bigint; tickUpper: bigint; liquidity: bigint; feeGrowthInside0LastX128: bigint; feeGrowthInside1LastX128: bigint; tokensOwed0: bigint; tokensOwed1: bigint }
500
+ const positions = await provider.tryAll<PositionData>(
501
+ ids.map(({ protocol, tokenId }) => provider.contract(protocol.positionManager as string, V3_NPM_ABI).positions(tokenId)),
502
+ overrides,
503
+ )
504
+ const open = ids
505
+ .map((item, i) => ({ ...item, data: positions[i] ?? null }))
506
+ .filter((x): x is typeof x & { data: PositionData } => x.data !== null)
507
+ .filter((x) => includeClosed || x.data.liquidity > 0n || x.data.tokensOwed0 > 0n || x.data.tokensOwed1 > 0n)
508
+ if (!open.length) {
509
+ return []
510
+ }
511
+
512
+ // 池子地址(同一个协议、同一对代币、同一档手续费只查一次)
513
+ const poolKey = (x: (typeof open)[number]) => `${x.protocol.positionManager}:${lower(x.data.token0)}:${lower(x.data.token1)}:${x.data.fee}`
514
+ const poolKeys = [...new Set(open.map(poolKey))]
515
+ const poolSamples = poolKeys.map((key) => open.find((x) => poolKey(x) === key) as (typeof open)[number])
516
+ const poolAddresses = await provider.tryAll<string>(
517
+ poolSamples.map((x) => provider.contract(x.protocol.factory as string, V3_FACTORY_ABI).getPool(x.data.token0, x.data.token1, x.data.fee)),
518
+ overrides,
519
+ )
520
+ const poolOf = new Map(poolKeys.map((key, i) => [key, poolAddresses[i] && poolAddresses[i] !== ZERO_ADDRESS ? (poolAddresses[i] as string) : null]))
521
+
522
+ // 池子状态(每个池子一次)+ 每个仓位两端 tick 的手续费增长
523
+ const pools = [...new Set([...poolOf.values()].filter((p): p is string => p !== null).map(lower))]
524
+ const poolCalls = pools.flatMap((pool) => {
525
+ const c = provider.contract(pool, V3_POOL_ABI)
526
+ return [c.slot0(), c.feeGrowthGlobal0X128(), c.feeGrowthGlobal1X128()]
527
+ })
528
+ const tickCalls = open.flatMap((x) => {
529
+ const pool = poolOf.get(poolKey(x))
530
+ if (!pool) {
531
+ return []
532
+ }
533
+ const c = provider.contract(pool, V3_POOL_ABI)
534
+ return [c.ticks(x.data.tickLower), c.ticks(x.data.tickUpper)]
535
+ })
536
+ const state = await provider.tryAll([...poolCalls, ...tickCalls], overrides)
537
+ const poolState = new Map(
538
+ pools.map((pool, i) => {
539
+ const [slot0, global0, global1] = state.slice(i * 3, i * 3 + 3) as [{ 0: bigint; 1: bigint } | null, bigint | null, bigint | null]
540
+ return [pool, slot0 && global0 !== null && global1 !== null ? { sqrtPriceX96: slot0[0], tick: Number(slot0[1]), global0, global1 } : null]
541
+ }),
542
+ )
543
+
544
+ let t = poolCalls.length
545
+ return open.flatMap((x): RawConcentrated[] => {
546
+ const pool = poolOf.get(poolKey(x))
547
+ if (!pool) {
548
+ return []
549
+ }
550
+ const lowerTick = state[t++] as { 2: bigint; 3: bigint } | null
551
+ const upperTick = state[t++] as { 2: bigint; 3: bigint } | null
552
+ const s = poolState.get(lower(pool))
553
+ if (!s || !lowerTick || !upperTick) {
554
+ return []
555
+ }
556
+ const tickLower = Number(x.data.tickLower)
557
+ const tickUpper = Number(x.data.tickUpper)
558
+ const [amount0, amount1] = getAmountsForLiquidity(s.sqrtPriceX96, getSqrtRatioAtTick(tickLower), getSqrtRatioAtTick(tickUpper), x.data.liquidity)
559
+ const inside0 = getFeeGrowthInside(s.tick, tickLower, tickUpper, s.global0, lowerTick[2], upperTick[2])
560
+ const inside1 = getFeeGrowthInside(s.tick, tickLower, tickUpper, s.global1, lowerTick[3], upperTick[3])
561
+ return [
562
+ {
563
+ version: 'v3',
564
+ protocol: x.protocol,
565
+ pool,
566
+ tokenId: x.tokenId,
567
+ token0: x.data.token0,
568
+ token1: x.data.token1,
569
+ fee: Number(x.data.fee),
570
+ tickLower,
571
+ tickUpper,
572
+ tick: s.tick,
573
+ sqrtPriceX96: s.sqrtPriceX96,
574
+ liquidity: x.data.liquidity,
575
+ amount0,
576
+ amount1,
577
+ // 与仓位合约一致:tokensOwed += uint128(新增),按 uint128 截断 / 回绕(正常仓位不会碰到)
578
+ fees0: (x.data.tokensOwed0 + (getFeesEarned(x.data.liquidity, inside0, x.data.feeGrowthInside0LastX128) & U128_MASK)) & U128_MASK,
579
+ fees1: (x.data.tokensOwed1 + (getFeesEarned(x.data.liquidity, inside1, x.data.feeGrowthInside1LastX128) & U128_MASK)) & U128_MASK,
580
+ },
581
+ ]
582
+ })
583
+ }
584
+
585
+ function reportPriceError(onPriceError: ((error: unknown) => void) | undefined, err: unknown): void {
586
+ try {
587
+ onPriceError?.(err)
588
+ } catch {
589
+ // 回调出错不影响查询结果
590
+ }
591
+ }
592
+
593
+ /** 补上代币信息(symbol / 精度)、换算数量、可选的美元价值 */
594
+ async function finalize(provider: Provider, chainId: number, raw: RawPosition[], options: LpPositionsOptions): Promise<LpPosition[]> {
595
+ const { prices, fetch: fetchOption } = options
596
+ const overrides = callOverrides(options)
597
+ const tron = isTronChain(chainId)
598
+ /** 输出的地址:Tron 上转成 T 地址(与 tronWeb 等一致) */
599
+ const out = (address: string) => (tron && /^0x[0-9a-fA-F]{40}$/.test(address) ? toTronAddress(address) : address)
600
+ /** 价格源的键:EVM 小写 0x 地址;Tron 用 T 地址(区分大小写) */
601
+ const priceKey = (address: string) => (tron ? toTronAddress(address) : lower(address))
602
+ const addresses = [...new Set(raw.flatMap((r) => [lower(r.token0), lower(r.token1)]))]
603
+ // V4 的主币池子 currency 是零地址:Provider 的 nativeTokens 不含零地址时(如配置成只认 0xeeee…),按它的第一个主币地址查主币信息
604
+ const nativeAlias = provider.isNativeToken(ZERO_ADDRESS) ? undefined : provider.nativeTokens[0]
605
+ const query = addresses.map((a) => (a === ZERO_ADDRESS && nativeAlias !== undefined ? nativeAlias : a))
606
+ const details = addresses.length ? await provider.tokens(query, { ...overrides, fields: ['symbol', 'decimals'] }) : []
607
+ const info = new Map(
608
+ addresses.map((address, i): [string, LpToken] => {
609
+ const d = details[i]
610
+ return [address, { address: out(address), native: d?.native ?? false, symbol: d?.symbol ?? null, decimals: d?.decimals ?? null }]
611
+ }),
612
+ )
613
+ const token = (address: string): LpToken => {
614
+ const t = info.get(lower(address)) as LpToken
615
+ return { ...t, address: out(address) }
616
+ }
617
+ const fmt = (value: bigint, t: LpToken) => (t.decimals === null ? null : formatUnits(value, t.decimals))
618
+
619
+ // 美元价值(可选)
620
+ const priceSource = prices === true ? defillamaPrices() : prices || null
621
+ let priceMap = new Map<string, number>()
622
+ if (priceSource && raw.length) {
623
+ const fetchFn = fetchOption ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
624
+ const nativeToken = [...info.values()].find((t) => t.native)
625
+ // 价格源出错(服务不可用、被拦截等)时照样返回仓位,只是不带价格(valueUsd / feesUsd 为 null)
626
+ try {
627
+ priceMap = await priceSource.prices({
628
+ chainId,
629
+ tokens: addresses.filter((a) => !info.get(a)?.native).map((a) => (tron ? toTronAddress(a) : a)),
630
+ nativeSymbol: nativeToken ? nativeToken.symbol : null,
631
+ fetch: fetchFn,
632
+ })
633
+ } catch (err) {
634
+ // 同步抛错和 reject 都在这里接住
635
+ reportPriceError(options.onPriceError, err)
636
+ }
637
+ }
638
+ const usd = (t: LpToken, formatted: string | null) => {
639
+ const price = priceMap.get(t.native ? 'native' : priceKey(t.address))
640
+ return formatted === null || price === undefined ? null : Number(formatted) * price
641
+ }
642
+ const sum = (a: number | null, b: number | null, amountA: bigint, amountB: bigint) => {
643
+ // 数量为 0 的一边没有价格也不影响
644
+ if ((a === null && amountA > 0n) || (b === null && amountB > 0n)) {
645
+ return null
646
+ }
647
+ return (a ?? 0) + (b ?? 0)
648
+ }
649
+
650
+ const list = raw.map((r): LpPosition => {
651
+ const token0 = token(r.token0)
652
+ const token1 = token(r.token1)
653
+ const amount0Formatted = fmt(r.amount0, token0)
654
+ const amount1Formatted = fmt(r.amount1, token1)
655
+ const base = {
656
+ chainId,
657
+ // 不认识的工厂:'v2:<工厂地址>'(Tron 上是 T 地址;读不到工厂时为 'v2:unknown')
658
+ protocol: r.protocol ? r.protocol.id : `v2:${(r as RawV2).factory ? out((r as RawV2).factory as string) : 'unknown'}`,
659
+ protocolName: r.protocol ? r.protocol.name : 'Unknown V2',
660
+ pool: r.version === 'v4' ? r.pool : out(r.pool),
661
+ token0,
662
+ token1,
663
+ amount0: r.amount0.toString(),
664
+ amount1: r.amount1.toString(),
665
+ amount0Formatted,
666
+ amount1Formatted,
667
+ ...(priceSource ? { valueUsd: sum(usd(token0, amount0Formatted), usd(token1, amount1Formatted), r.amount0, r.amount1) } : {}),
668
+ }
669
+ if (r.version === 'v2') {
670
+ return {
671
+ ...base,
672
+ version: 'v2',
673
+ balance: r.balance.toString(),
674
+ totalSupply: r.totalSupply.toString(),
675
+ share: r.totalSupply === 0n ? 0 : Number((r.balance * 10n ** 18n) / r.totalSupply) / 1e18,
676
+ }
677
+ }
678
+ const fees0Formatted = fmt(r.fees0, token0)
679
+ const fees1Formatted = fmt(r.fees1, token1)
680
+ const decimalsKnown = token0.decimals !== null && token1.decimals !== null
681
+ return {
682
+ ...base,
683
+ version: r.version,
684
+ tokenId: r.tokenId.toString(),
685
+ positionManager: out(r.protocol.positionManager as string),
686
+ fee: r.fee,
687
+ tickLower: r.tickLower,
688
+ tickUpper: r.tickUpper,
689
+ tick: r.tick,
690
+ inRange: r.tick >= r.tickLower && r.tick < r.tickUpper,
691
+ liquidity: r.liquidity.toString(),
692
+ price: decimalsKnown ? sqrtPriceToPrice(r.sqrtPriceX96, token0.decimals as number, token1.decimals as number) : null,
693
+ priceLower: decimalsKnown ? tickToPrice(r.tickLower, token0.decimals as number, token1.decimals as number) : null,
694
+ priceUpper: decimalsKnown ? tickToPrice(r.tickUpper, token0.decimals as number, token1.decimals as number) : null,
695
+ fees0: r.fees0.toString(),
696
+ fees1: r.fees1.toString(),
697
+ fees0Formatted,
698
+ fees1Formatted,
699
+ ...(priceSource ? { feesUsd: sum(usd(token0, fees0Formatted), usd(token1, fees1Formatted), r.fees0, r.fees1) } : {}),
700
+ ...(r.hooks !== undefined ? { hooks: r.hooks } : {}),
701
+ }
702
+ })
703
+ // 有价格时按价值从高到低(仓位价值 + 未领取手续费),没有价格的排在后面
704
+ if (priceSource) {
705
+ const total = (p: LpPosition) => (p.valueUsd === null || p.valueUsd === undefined ? -1 : p.valueUsd + ('feesUsd' in p ? (p.feesUsd ?? 0) : 0))
706
+ list.sort((a, b) => total(b) - total(a))
707
+ }
708
+ return list
709
+ }
710
+
711
+ /**
712
+ * 查一个地址的 LP 仓位:V2(持有的 LP 代币)、V3 / V4(仓位 NFT)。所有合约读取都用 multicall 批量完成。
713
+ *
714
+ * ```ts
715
+ * await lpPositions(multi, user) // 这条链上内置的全部协议
716
+ * await lpPositions(multi, user, { tokens: heldTokens, prices: true }) // V2 用持有的代币找池子,带美元价值
717
+ * ```
718
+ */
719
+ export function lpPositions(provider: Provider, owner: string, options: LpPositionsOptions = {}): Promise<LpPosition[]> {
720
+ // 取消时立即 reject(扫描 / 价格等进行中的请求不中断,结果丢弃)
721
+ return withSignal(options.signal, () => queryLpPositions(provider, owner, options))
722
+ }
723
+
724
+ async function queryLpPositions(provider: Provider, owner: string, options: LpPositionsOptions): Promise<LpPosition[]> {
725
+ const chainId = await provider.getChainId()
726
+ const protocols = resolveLpProtocols(chainId, options)
727
+ const allV2 = [...LP_PROTOCOLS.filter((p) => p.chainId === chainId && p.version === 'v2'), ...protocols.filter((p) => p.version === 'v2')]
728
+ const v2Selected = protocols.filter((p) => p.version === 'v2')
729
+ const v3Selected = protocols.filter((p) => p.version === 'v3')
730
+ const v4Selected = protocols.filter((p) => p.version === 'v4')
731
+ const wantV2 = !options.versions || options.versions.includes('v2')
732
+ const wantV4 = v4Selected.length > 0 && !!(options.v4?.tokenIds?.length || options.v4?.scan)
733
+ const [v2List, v3List, v4List] = await Promise.all([
734
+ wantV2 && (v2Selected.length || options.pools?.length || options.source) ? v2Positions(provider, owner, v2Selected, allV2, options, chainId) : Promise.resolve([]),
735
+ v3Positions(provider, owner, v3Selected, options),
736
+ wantV4 ? v4Positions(provider, owner, v4Selected, options.v4 ?? {}, options.includeClosed ?? false, options.maxPositions ?? 500, callOverrides(options)) : Promise.resolve([]),
737
+ ])
738
+ return finalize(provider, chainId, [...v2List, ...v3List, ...v4List], options)
739
+ }