@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.
- package/CHANGELOG.md +29 -0
- package/README.md +242 -2
- package/dist/fallback.d.ts +5 -0
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +7 -0
- package/dist/fallback.js.map +1 -1
- package/dist/liquidity/common.d.ts +72 -0
- package/dist/liquidity/common.d.ts.map +1 -0
- package/dist/liquidity/common.js +271 -0
- package/dist/liquidity/common.js.map +1 -0
- package/dist/liquidity/concentrated.d.ts +19 -0
- package/dist/liquidity/concentrated.d.ts.map +1 -0
- package/dist/liquidity/concentrated.js +106 -0
- package/dist/liquidity/concentrated.js.map +1 -0
- package/dist/liquidity/index.d.ts +42 -0
- package/dist/liquidity/index.d.ts.map +1 -0
- package/dist/liquidity/index.js +143 -0
- package/dist/liquidity/index.js.map +1 -0
- package/dist/liquidity/types.d.ts +182 -0
- package/dist/liquidity/types.d.ts.map +1 -0
- package/dist/liquidity/types.js +2 -0
- package/dist/liquidity/types.js.map +1 -0
- package/dist/liquidity/v2.d.ts +5 -0
- package/dist/liquidity/v2.d.ts.map +1 -0
- package/dist/liquidity/v2.js +342 -0
- package/dist/liquidity/v2.js.map +1 -0
- package/dist/liquidity/v3.d.ts +6 -0
- package/dist/liquidity/v3.d.ts.map +1 -0
- package/dist/liquidity/v3.js +310 -0
- package/dist/liquidity/v3.js.map +1 -0
- package/dist/liquidity/v4.d.ts +5 -0
- package/dist/liquidity/v4.d.ts.map +1 -0
- package/dist/liquidity/v4.js +279 -0
- package/dist/liquidity/v4.js.map +1 -0
- package/dist/lp.d.ts +156 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +510 -0
- package/dist/lp.js.map +1 -0
- package/dist/lpMath.d.ts +80 -0
- package/dist/lpMath.d.ts.map +1 -0
- package/dist/lpMath.js +312 -0
- package/dist/lpMath.js.map +1 -0
- package/dist/lpV4.d.ts +108 -0
- package/dist/lpV4.d.ts.map +1 -0
- package/dist/lpV4.js +283 -0
- package/dist/lpV4.js.map +1 -0
- package/dist/owner.d.ts +1 -1
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +18 -5
- package/dist/owner.js.map +1 -1
- package/dist/scan.d.ts +54 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +124 -92
- package/dist/scan.js.map +1 -1
- package/dist/source.d.ts +2 -0
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +4 -0
- package/dist/source.js.map +1 -1
- package/dist/subpaths/liquidity.d.ts +24 -0
- package/dist/subpaths/liquidity.d.ts.map +1 -0
- package/dist/subpaths/liquidity.js +21 -0
- package/dist/subpaths/liquidity.js.map +1 -0
- package/dist/subpaths/lp.d.ts +33 -0
- package/dist/subpaths/lp.d.ts.map +1 -0
- package/dist/subpaths/lp.js +47 -0
- package/dist/subpaths/lp.js.map +1 -0
- package/dist/subpaths/write.d.ts +20 -0
- package/dist/subpaths/write.d.ts.map +1 -0
- package/dist/subpaths/write.js +18 -0
- package/dist/subpaths/write.js.map +1 -0
- package/dist/tron.d.ts +2 -0
- package/dist/tron.d.ts.map +1 -1
- package/dist/tron.js +4 -0
- package/dist/tron.js.map +1 -1
- package/dist/write/batch.d.ts +88 -0
- package/dist/write/batch.d.ts.map +1 -0
- package/dist/write/batch.js +231 -0
- package/dist/write/batch.js.map +1 -0
- package/dist/write/collect.d.ts +53 -0
- package/dist/write/collect.d.ts.map +1 -0
- package/dist/write/collect.js +255 -0
- package/dist/write/collect.js.map +1 -0
- package/dist/write/rpc.d.ts +68 -0
- package/dist/write/rpc.d.ts.map +1 -0
- package/dist/write/rpc.js +236 -0
- package/dist/write/rpc.js.map +1 -0
- package/dist/write/send.d.ts +124 -0
- package/dist/write/send.d.ts.map +1 -0
- package/dist/write/send.js +373 -0
- package/dist/write/send.js.map +1 -0
- package/dist/write/signer.d.ts +58 -0
- package/dist/write/signer.d.ts.map +1 -0
- package/dist/write/signer.js +41 -0
- package/dist/write/signer.js.map +1 -0
- package/dist/write/transfer.d.ts +83 -0
- package/dist/write/transfer.d.ts.map +1 -0
- package/dist/write/transfer.js +259 -0
- package/dist/write/transfer.js.map +1 -0
- package/dist/write/tron.d.ts +60 -0
- package/dist/write/tron.d.ts.map +1 -0
- package/dist/write/tron.js +960 -0
- package/dist/write/tron.js.map +1 -0
- package/package.json +26 -5
- package/src/fallback.ts +8 -0
- package/src/liquidity/common.ts +314 -0
- package/src/liquidity/concentrated.ts +121 -0
- package/src/liquidity/index.ts +171 -0
- package/src/liquidity/types.ts +194 -0
- package/src/liquidity/v2.ts +371 -0
- package/src/liquidity/v3.ts +382 -0
- package/src/liquidity/v4.ts +348 -0
- package/src/lp.ts +739 -0
- package/src/lpMath.ts +336 -0
- package/src/lpV4.ts +406 -0
- package/src/owner.ts +19 -6
- package/src/scan.ts +155 -86
- package/src/source.ts +5 -0
- package/src/subpaths/liquidity.ts +52 -0
- package/src/subpaths/lp.ts +90 -0
- package/src/subpaths/write.ts +26 -0
- package/src/tron.ts +5 -0
- package/src/write/batch.ts +302 -0
- package/src/write/collect.ts +312 -0
- package/src/write/rpc.ts +277 -0
- package/src/write/send.ts +490 -0
- package/src/write/signer.ts +90 -0
- package/src/write/transfer.ts +344 -0
- 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
|
+
}
|