@w3lib/evm-batch-call 0.6.0 → 0.8.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 +54 -0
- package/README.md +367 -3
- package/contracts/TokenProbe.sol +103 -0
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +114 -3
- package/dist/aggregate.js.map +1 -1
- package/dist/approvals.d.ts +169 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +310 -0
- package/dist/approvals.js.map +1 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +4 -0
- package/dist/chains.js.map +1 -1
- package/dist/dividends.d.ts +135 -0
- package/dist/dividends.d.ts.map +1 -0
- package/dist/dividends.js +253 -0
- package/dist/dividends.js.map +1 -0
- package/dist/fees.d.ts +97 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +115 -0
- package/dist/fees.js.map +1 -0
- package/dist/history.d.ts +154 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +247 -0
- package/dist/history.js.map +1 -0
- package/dist/historySources.d.ts +32 -0
- package/dist/historySources.d.ts.map +1 -0
- package/dist/historySources.js +319 -0
- package/dist/historySources.js.map +1 -0
- package/dist/http.d.ts +10 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +30 -0
- package/dist/http.js.map +1 -0
- package/dist/liquidity/v3.d.ts +2 -0
- package/dist/liquidity/v3.d.ts.map +1 -1
- package/dist/liquidity/v3.js +1 -1
- package/dist/liquidity/v3.js.map +1 -1
- package/dist/liquidity/v4.d.ts +21 -0
- package/dist/liquidity/v4.d.ts.map +1 -1
- package/dist/liquidity/v4.js +4 -3
- package/dist/liquidity/v4.js.map +1 -1
- package/dist/lp.d.ts +10 -1
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +19 -2
- package/dist/lp.js.map +1 -1
- package/dist/lpV4.d.ts +4 -0
- package/dist/lpV4.d.ts.map +1 -1
- package/dist/lpV4.js +27 -3
- package/dist/lpV4.js.map +1 -1
- package/dist/multicall.d.ts.map +1 -1
- package/dist/multicall.js +3 -0
- package/dist/multicall.js.map +1 -1
- package/dist/names.d.ts +55 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +93 -0
- package/dist/names.js.map +1 -0
- package/dist/owner.d.ts +4 -26
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +7 -112
- package/dist/owner.js.map +1 -1
- package/dist/permit.d.ts +137 -0
- package/dist/permit.d.ts.map +1 -0
- package/dist/permit.js +355 -0
- package/dist/permit.js.map +1 -0
- package/dist/portfolio.d.ts +56 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +54 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +95 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +329 -0
- package/dist/price.js.map +1 -0
- package/dist/rewards/farms.d.ts +26 -0
- package/dist/rewards/farms.d.ts.map +1 -0
- package/dist/rewards/farms.js +210 -0
- package/dist/rewards/farms.js.map +1 -0
- package/dist/rewards/index.d.ts +42 -0
- package/dist/rewards/index.d.ts.map +1 -0
- package/dist/rewards/index.js +382 -0
- package/dist/rewards/index.js.map +1 -0
- package/dist/rewards/types.d.ts +170 -0
- package/dist/rewards/types.d.ts.map +1 -0
- package/dist/rewards/types.js +2 -0
- package/dist/rewards/types.js.map +1 -0
- package/dist/risk.d.ts +42 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +134 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpcNodes.d.ts.map +1 -1
- package/dist/rpcNodes.js +18 -0
- package/dist/rpcNodes.js.map +1 -1
- package/dist/simulate.d.ts +186 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +559 -0
- package/dist/simulate.js.map +1 -0
- package/dist/simulateDecode.d.ts +33 -0
- package/dist/simulateDecode.d.ts.map +1 -0
- package/dist/simulateDecode.js +321 -0
- package/dist/simulateDecode.js.map +1 -0
- package/dist/smartAccount/account.d.ts +31 -0
- package/dist/smartAccount/account.d.ts.map +1 -0
- package/dist/smartAccount/account.js +33 -0
- package/dist/smartAccount/account.js.map +1 -0
- package/dist/smartAccount/clients.d.ts +77 -0
- package/dist/smartAccount/clients.d.ts.map +1 -0
- package/dist/smartAccount/clients.js +122 -0
- package/dist/smartAccount/clients.js.map +1 -0
- package/dist/smartAccount/eip5792.d.ts +77 -0
- package/dist/smartAccount/eip5792.d.ts.map +1 -0
- package/dist/smartAccount/eip5792.js +152 -0
- package/dist/smartAccount/eip5792.js.map +1 -0
- package/dist/smartAccount/send.d.ts +40 -0
- package/dist/smartAccount/send.d.ts.map +1 -0
- package/dist/smartAccount/send.js +271 -0
- package/dist/smartAccount/send.js.map +1 -0
- package/dist/smartAccount/userOp.d.ts +91 -0
- package/dist/smartAccount/userOp.d.ts.map +1 -0
- package/dist/smartAccount/userOp.js +124 -0
- package/dist/smartAccount/userOp.js.map +1 -0
- package/dist/subpaths/approvals.d.ts +26 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +27 -0
- package/dist/subpaths/approvals.js.map +1 -0
- package/dist/subpaths/dividends.d.ts +28 -0
- package/dist/subpaths/dividends.d.ts.map +1 -0
- package/dist/subpaths/dividends.js +29 -0
- package/dist/subpaths/dividends.js.map +1 -0
- package/dist/subpaths/fees.d.ts +23 -0
- package/dist/subpaths/fees.d.ts.map +1 -0
- package/dist/subpaths/fees.js +25 -0
- package/dist/subpaths/fees.js.map +1 -0
- package/dist/subpaths/history.d.ts +22 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +23 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/names.d.ts +35 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +39 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/permit.d.ts +14 -0
- package/dist/subpaths/permit.d.ts.map +1 -0
- package/dist/subpaths/permit.js +14 -0
- package/dist/subpaths/permit.js.map +1 -0
- package/dist/subpaths/price.d.ts +38 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +40 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/rewards.d.ts +27 -0
- package/dist/subpaths/rewards.d.ts.map +1 -0
- package/dist/subpaths/rewards.js +25 -0
- package/dist/subpaths/rewards.js.map +1 -0
- package/dist/subpaths/risk.d.ts +19 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +20 -0
- package/dist/subpaths/risk.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +25 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +28 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/smart-account.d.ts +24 -0
- package/dist/subpaths/smart-account.d.ts.map +1 -0
- package/dist/subpaths/smart-account.js +24 -0
- package/dist/subpaths/smart-account.js.map +1 -0
- package/dist/subpaths/swap.d.ts +19 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +20 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/swap.d.ts +168 -0
- package/dist/swap.d.ts.map +1 -0
- package/dist/swap.js +387 -0
- package/dist/swap.js.map +1 -0
- package/dist/tokenProbe.d.ts +3 -0
- package/dist/tokenProbe.d.ts.map +1 -0
- package/dist/tokenProbe.js +4 -0
- package/dist/tokenProbe.js.map +1 -0
- package/dist/write/batch.d.ts +34 -2
- package/dist/write/batch.d.ts.map +1 -1
- package/dist/write/batch.js +22 -3
- package/dist/write/batch.js.map +1 -1
- package/dist/write/collect.d.ts +9 -0
- package/dist/write/collect.d.ts.map +1 -1
- package/dist/write/collect.js +41 -1
- package/dist/write/collect.js.map +1 -1
- package/dist/write/send.d.ts +4 -0
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js.map +1 -1
- package/dist/write/signer.d.ts +5 -1
- package/dist/write/signer.d.ts.map +1 -1
- package/dist/write/signer.js.map +1 -1
- package/dist/write/transfer.d.ts +5 -3
- package/dist/write/transfer.d.ts.map +1 -1
- package/dist/write/transfer.js +12 -6
- package/dist/write/transfer.js.map +1 -1
- package/dist/write/tron.d.ts +25 -0
- package/dist/write/tron.d.ts.map +1 -1
- package/dist/write/tron.js +26 -0
- package/dist/write/tron.js.map +1 -1
- package/package.json +73 -16
- package/src/aggregate.ts +110 -3
- package/src/approvals.ts +462 -0
- package/src/chains.ts +4 -0
- package/src/dividends.ts +360 -0
- package/src/fees.ts +201 -0
- package/src/history.ts +367 -0
- package/src/historySources.ts +360 -0
- package/src/http.ts +34 -0
- package/src/liquidity/v3.ts +1 -1
- package/src/liquidity/v4.ts +4 -3
- package/src/lp.ts +21 -2
- package/src/lpV4.ts +32 -2
- package/src/multicall.ts +3 -0
- package/src/names.ts +141 -0
- package/src/owner.ts +7 -138
- package/src/permit.ts +457 -0
- package/src/portfolio.ts +98 -0
- package/src/price.ts +415 -0
- package/src/rewards/farms.ts +229 -0
- package/src/rewards/index.ts +431 -0
- package/src/rewards/types.ts +179 -0
- package/src/risk.ts +166 -0
- package/src/rpcNodes.ts +18 -0
- package/src/simulate.ts +727 -0
- package/src/simulateDecode.ts +352 -0
- package/src/smartAccount/account.ts +63 -0
- package/src/smartAccount/clients.ts +180 -0
- package/src/smartAccount/eip5792.ts +187 -0
- package/src/smartAccount/send.ts +341 -0
- package/src/smartAccount/userOp.ts +180 -0
- package/src/subpaths/approvals.ts +48 -0
- package/src/subpaths/dividends.ts +47 -0
- package/src/subpaths/fees.ts +43 -0
- package/src/subpaths/history.ts +41 -0
- package/src/subpaths/names.ts +43 -0
- package/src/subpaths/permit.ts +39 -0
- package/src/subpaths/price.ts +60 -0
- package/src/subpaths/rewards.ts +46 -0
- package/src/subpaths/risk.ts +23 -0
- package/src/subpaths/simulate.ts +51 -0
- package/src/subpaths/smart-account.ts +40 -0
- package/src/subpaths/swap.ts +36 -0
- package/src/swap.ts +534 -0
- package/src/tokenProbe.ts +5 -0
- package/src/write/batch.ts +44 -5
- package/src/write/collect.ts +60 -1
- package/src/write/send.ts +4 -0
- package/src/write/signer.ts +5 -1
- package/src/write/transfer.ts +17 -10
- package/src/write/tron.ts +43 -0
package/src/price.ts
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 价格(子路径 `@w3lib/evm-batch-call/price`):DefiLlama(当前 / 历史)、链上池子价格(V2 / V3 池子,按流动性选)、
|
|
3
|
+
* 组合多个价格源、按地址算资产总值。价格只用于展示和估值,不要用来计算交易的最小数量
|
|
4
|
+
*/
|
|
5
|
+
import type { CallOverrides } from './aggregate.js'
|
|
6
|
+
import type { Call } from './call.js'
|
|
7
|
+
import { formatUnits } from './erc20.js'
|
|
8
|
+
import { fetchJson } from './http.js'
|
|
9
|
+
import { LP_PROTOCOLS, LP_QUOTE_TOKENS, type LpProtocol } from './lp.js'
|
|
10
|
+
import { sqrtPriceToPrice } from './lpMath.js'
|
|
11
|
+
import { getNativeCurrency } from './chains.js'
|
|
12
|
+
import type { Provider } from './provider.js'
|
|
13
|
+
import { TRON_CHAIN_ID, isTronChain, toTronAddress } from './tron.js'
|
|
14
|
+
|
|
15
|
+
export interface PriceSourceContext {
|
|
16
|
+
chainId: number
|
|
17
|
+
/** 代币地址(不含主币) */
|
|
18
|
+
tokens: readonly string[]
|
|
19
|
+
/** 主币 symbol(需要主币价格时);不需要时为 null */
|
|
20
|
+
nativeSymbol: string | null
|
|
21
|
+
fetch: typeof fetch
|
|
22
|
+
/** 查询用的 Provider(链上价格源 onchainPrices 用它读池子) */
|
|
23
|
+
provider?: Provider
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PriceSource {
|
|
27
|
+
readonly name: string
|
|
28
|
+
/** 返回 去重键 → 美元价格:EVM 是小写 0x 地址;Tron 是 T 开头的 base58 地址(区分大小写);主币用键 'native'。没有价格的不返回 */
|
|
29
|
+
prices(ctx: PriceSourceContext): Promise<Map<string, number>>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** DefiLlama 的链标识:chainId → slug(2026-10 实测可用) */
|
|
33
|
+
export const DEFILLAMA_CHAINS: Readonly<Record<number, string>> = {
|
|
34
|
+
1: 'ethereum',
|
|
35
|
+
10: 'optimism',
|
|
36
|
+
56: 'bsc',
|
|
37
|
+
130: 'unichain',
|
|
38
|
+
137: 'polygon',
|
|
39
|
+
143: 'monad',
|
|
40
|
+
196: 'xlayer',
|
|
41
|
+
204: 'op_bnb',
|
|
42
|
+
324: 'era',
|
|
43
|
+
999: 'hyperliquid',
|
|
44
|
+
1116: 'core',
|
|
45
|
+
2818: 'morph',
|
|
46
|
+
5000: 'mantle',
|
|
47
|
+
8453: 'base',
|
|
48
|
+
9745: 'plasma',
|
|
49
|
+
34443: 'mode',
|
|
50
|
+
42161: 'arbitrum',
|
|
51
|
+
43114: 'avax',
|
|
52
|
+
57073: 'ink',
|
|
53
|
+
59144: 'linea',
|
|
54
|
+
80094: 'berachain',
|
|
55
|
+
81457: 'blast',
|
|
56
|
+
534352: 'scroll',
|
|
57
|
+
[TRON_CHAIN_ID.mainnet]: 'tron',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 主币价格:按主币 symbol 对应 CoinGecko 的币种 id(DefiLlama 用 coingecko:<id> 查) */
|
|
61
|
+
const NATIVE_PRICE_IDS: Readonly<Record<string, string>> = {
|
|
62
|
+
ETH: 'ethereum',
|
|
63
|
+
BNB: 'binancecoin',
|
|
64
|
+
POL: 'polygon-ecosystem-token',
|
|
65
|
+
AVAX: 'avalanche-2',
|
|
66
|
+
MNT: 'mantle',
|
|
67
|
+
OKB: 'okb',
|
|
68
|
+
HYPE: 'hyperliquid',
|
|
69
|
+
BERA: 'berachain-bera',
|
|
70
|
+
CORE: 'coredaoorg',
|
|
71
|
+
MON: 'monad',
|
|
72
|
+
XPL: 'plasma',
|
|
73
|
+
TRX: 'tron',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DefillamaPriceOptions {
|
|
77
|
+
/** 置信度下限(0~1),DefiLlama 给出的置信度低于它视为没有价格,默认 0.9 */
|
|
78
|
+
minConfidence?: number
|
|
79
|
+
/** 历史价格:这个时间点(unix 秒或 Date)附近的价格;不传为当前价格 */
|
|
80
|
+
at?: number | Date
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* DefiLlama 价格(免费、不需要 Key)。
|
|
85
|
+
* 注意:流动性很差的代币价格可能虚高,展示总资产时建议配合 minUsd 或自己的价格源。
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* defillamaPrices() // 当前价格
|
|
89
|
+
* defillamaPrices({ at: new Date('2026-01-01') }) // 历史价格
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function defillamaPrices(options: DefillamaPriceOptions = {}): PriceSource {
|
|
93
|
+
const minConfidence = options.minConfidence ?? 0.9
|
|
94
|
+
const at = options.at instanceof Date ? Math.floor(options.at.getTime() / 1000) : options.at
|
|
95
|
+
const endpoint = at === undefined ? 'https://coins.llama.fi/prices/current' : `https://coins.llama.fi/prices/historical/${Math.floor(at)}`
|
|
96
|
+
return {
|
|
97
|
+
name: at === undefined ? 'defillama' : `defillama@${Math.floor(at)}`,
|
|
98
|
+
async prices({ chainId, tokens, nativeSymbol, fetch: fetchFn }) {
|
|
99
|
+
const result = new Map<string, number>()
|
|
100
|
+
const slug = DEFILLAMA_CHAINS[chainId]
|
|
101
|
+
const nativeId = nativeSymbol ? NATIVE_PRICE_IDS[nativeSymbol.toUpperCase()] : undefined
|
|
102
|
+
// 查询键 → 结果里用的去重键:EVM 地址小写;Tron 地址区分大小写,DefiLlama 要用原样的 base58(T…)地址
|
|
103
|
+
const resultKey = new Map<string, string>()
|
|
104
|
+
const tokenKeys = slug
|
|
105
|
+
? tokens.map((t) => {
|
|
106
|
+
const tron = slug === 'tron'
|
|
107
|
+
const query = `${slug}:${tron ? toTronAddress(t) : t.toLowerCase()}`
|
|
108
|
+
resultKey.set(query.toLowerCase(), tron ? toTronAddress(t) : t.toLowerCase())
|
|
109
|
+
return query
|
|
110
|
+
})
|
|
111
|
+
: []
|
|
112
|
+
const keys = [...(nativeId ? [`coingecko:${nativeId}`] : []), ...tokenKeys]
|
|
113
|
+
const batches: string[][] = []
|
|
114
|
+
for (let i = 0; i < keys.length; i += 100) {
|
|
115
|
+
batches.push(keys.slice(i, i + 100))
|
|
116
|
+
}
|
|
117
|
+
const responses = await Promise.all(
|
|
118
|
+
batches.map((batch) => fetchJson<{ coins?: Record<string, { price?: number; confidence?: number }> }>(fetchFn, `${endpoint}/${batch.join(',')}`)),
|
|
119
|
+
)
|
|
120
|
+
for (const res of responses) {
|
|
121
|
+
for (const [key, coin] of Object.entries(res.coins ?? {})) {
|
|
122
|
+
if (typeof coin.price !== 'number' || !Number.isFinite(coin.price) || (coin.confidence !== undefined && coin.confidence < minConfidence)) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
result.set(key.startsWith('coingecko:') ? 'native' : (resultKey.get(key.toLowerCase()) ?? key.slice(key.indexOf(':') + 1).toLowerCase()), coin.price)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// 组合
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 依次使用多个价格源:前面的源没给出价格的代币(和主币)交给下一个源。某个源出错时跳过它(错误交给 onError)。
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* combinePrices([defillamaPrices(), onchainPrices()])
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function combinePrices(sources: readonly PriceSource[], options: { onError?: (source: PriceSource, err: unknown) => void } = {}): PriceSource {
|
|
146
|
+
return {
|
|
147
|
+
name: sources.map((s) => s.name).join('+'),
|
|
148
|
+
async prices(ctx) {
|
|
149
|
+
const found = await runSources(sources, ctx, options.onError)
|
|
150
|
+
return new Map([...found].map(([k, v]) => [k, v.price]))
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 依次查询,记下每个价格来自哪个源 */
|
|
156
|
+
async function runSources(sources: readonly PriceSource[], ctx: PriceSourceContext, onError?: (source: PriceSource, err: unknown) => void): Promise<Map<string, { price: number; source: string }>> {
|
|
157
|
+
const result = new Map<string, { price: number; source: string }>()
|
|
158
|
+
let tokens = [...ctx.tokens]
|
|
159
|
+
let nativeSymbol = ctx.nativeSymbol
|
|
160
|
+
for (const source of sources) {
|
|
161
|
+
if (!tokens.length && !nativeSymbol) break
|
|
162
|
+
let found: Map<string, number>
|
|
163
|
+
try {
|
|
164
|
+
found = await source.prices({ ...ctx, tokens, nativeSymbol })
|
|
165
|
+
} catch (err) {
|
|
166
|
+
onError?.(source, err)
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
for (const [key, price] of found) if (!result.has(key) && Number.isFinite(price) && price > 0) result.set(key, { price, source: source.name })
|
|
170
|
+
tokens = tokens.filter((t) => !result.has(priceKeyOf(ctx.chainId, t)))
|
|
171
|
+
if (result.has('native')) nativeSymbol = null
|
|
172
|
+
}
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 价格结果里的键:EVM 小写 0x 地址;Tron 是 T 开头的 base58 地址;主币为 'native' */
|
|
177
|
+
export function priceKeyOf(chainId: number, token: string): string {
|
|
178
|
+
return isTronChain(chainId) ? toTronAddress(token) : token.toLowerCase()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// 链上价格
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/** 按 1 美元计的稳定币(链上价格的计价锚点之一;另一个是包装主币) */
|
|
186
|
+
export const STABLECOINS: Readonly<Record<number, readonly string[]>> = {
|
|
187
|
+
1: ['0xdAC17F958D2ee523a2206206994597C13D831ec7', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', '0x6B175474E89094C44Da98b954EedeAC495271d0F'],
|
|
188
|
+
56: ['0x55d398326f99059fF775485246999027B3197955', '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d'],
|
|
189
|
+
8453: ['0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'],
|
|
190
|
+
42161: ['0xaf88d065e77c8cC2239327C5EDb3A432268e5831', '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'],
|
|
191
|
+
10: ['0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58'],
|
|
192
|
+
137: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'],
|
|
193
|
+
43114: ['0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E'],
|
|
194
|
+
[TRON_CHAIN_ID.mainnet]: ['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface OnchainPriceOptions {
|
|
198
|
+
/** 池子里计价币一侧的价值(美元)下限,低于它的池子不用(防止小池子的价格被操纵),默认 10000 */
|
|
199
|
+
minLiquidityUsd?: number
|
|
200
|
+
/** 包装主币(计价锚点,主币的价格也按它),默认 LP_QUOTE_TOKENS 里这条链的第一个 */
|
|
201
|
+
wrappedNative?: string
|
|
202
|
+
/** 按 1 美元计的稳定币,默认 STABLECOINS 里这条链的 */
|
|
203
|
+
stablecoins?: readonly string[]
|
|
204
|
+
/** 用哪些协议的池子,默认 LP_PROTOCOLS 里这条链的 V2 和 V3 */
|
|
205
|
+
protocols?: readonly LpProtocol[]
|
|
206
|
+
/** V3 池子的手续费档位,默认 [100, 500, 2500, 3000, 10000] */
|
|
207
|
+
fees?: readonly number[]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const ERC20_ABI = ['function decimals() view returns (uint8)', 'function balanceOf(address owner) view returns (uint256)']
|
|
211
|
+
const V2_FACTORY_ABI = ['function getPair(address tokenA, address tokenB) view returns (address)']
|
|
212
|
+
const V2_PAIR_ABI = ['function token0() view returns (address)', 'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)']
|
|
213
|
+
const V3_FACTORY_ABI = ['function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)']
|
|
214
|
+
const V3_POOL_ABI = ['function token0() view returns (address)', 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick)', 'function liquidity() view returns (uint128)']
|
|
215
|
+
const ZERO = '0x0000000000000000000000000000000000000000'
|
|
216
|
+
|
|
217
|
+
interface PoolQuote {
|
|
218
|
+
/** 1 个代币值多少个计价币 */
|
|
219
|
+
price: number
|
|
220
|
+
/** 池子里计价币的数量(按精度换算) */
|
|
221
|
+
depth: number
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 链上价格:在 V2 / V3 池子里找代币对包装主币、稳定币的池子,用计价币一侧最深的池子的价格。
|
|
226
|
+
* 稳定币按 1 美元;包装主币(和主币)按它对稳定币最深的池子。读取全部用 multicall 合并。
|
|
227
|
+
* 适合 DefiLlama 没有收录的新币;价格是池子当前的价格,可能被单个区块内的交易影响,只用于展示
|
|
228
|
+
*/
|
|
229
|
+
export function onchainPrices(options: OnchainPriceOptions = {}): PriceSource {
|
|
230
|
+
return {
|
|
231
|
+
name: 'onchain',
|
|
232
|
+
async prices({ chainId, tokens, nativeSymbol, provider }) {
|
|
233
|
+
if (!provider) throw new Error('onchainPrices needs a Provider (ctx.provider)')
|
|
234
|
+
const key = (t: string) => priceKeyOf(chainId, t)
|
|
235
|
+
const wrapped = options.wrappedNative ?? LP_QUOTE_TOKENS[chainId]?.[0]
|
|
236
|
+
const stables = options.stablecoins ?? STABLECOINS[chainId] ?? []
|
|
237
|
+
const protocols = (options.protocols ?? LP_PROTOCOLS.filter((p) => p.chainId === chainId)).filter((p) => (p.version === 'v2' || p.version === 'v3') && p.factory)
|
|
238
|
+
const minLiquidity = options.minLiquidityUsd ?? 10_000
|
|
239
|
+
const result = new Map<string, number>()
|
|
240
|
+
const stableKeys = new Set(stables.map(key))
|
|
241
|
+
for (const t of tokens) if (stableKeys.has(key(t))) result.set(key(t), 1)
|
|
242
|
+
if (!stables.length || !protocols.length) return result
|
|
243
|
+
|
|
244
|
+
// 包装主币先对稳定币定价,其他代币再对包装主币和稳定币定价(两轮都用一次 multicall 找池子 + 一次读池子)
|
|
245
|
+
const anchorsUsd = new Map(stables.map((s) => [key(s), 1]))
|
|
246
|
+
const needWrapped = wrapped && (nativeSymbol || tokens.some((t) => key(t) === key(wrapped)) || tokens.some((t) => !stableKeys.has(key(t))))
|
|
247
|
+
if (wrapped && needWrapped) {
|
|
248
|
+
const [quote] = await bestQuotes(provider, [wrapped], stables, anchorsUsd, protocols, options.fees, minLiquidity)
|
|
249
|
+
if (quote) {
|
|
250
|
+
anchorsUsd.set(key(wrapped), quote)
|
|
251
|
+
if (tokens.some((t) => key(t) === key(wrapped))) result.set(key(wrapped), quote)
|
|
252
|
+
if (nativeSymbol) result.set('native', quote)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const rest = [...new Set(tokens.map(key))].filter((k) => !result.has(k)).map((k) => tokens.find((t) => key(t) === k) as string)
|
|
256
|
+
if (!rest.length) return result
|
|
257
|
+
const anchors = [...(wrapped && anchorsUsd.has(key(wrapped)) ? [wrapped] : []), ...stables]
|
|
258
|
+
const quotes = await bestQuotes(provider, rest, anchors, anchorsUsd, protocols, options.fees, minLiquidity)
|
|
259
|
+
rest.forEach((t, i) => {
|
|
260
|
+
const q = quotes[i]
|
|
261
|
+
if (q !== null && q !== undefined) result.set(key(t), q)
|
|
262
|
+
})
|
|
263
|
+
return result
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 每个代币在 anchors 上最深的池子给出的美元价格(池子深度不够时为 null) */
|
|
269
|
+
async function bestQuotes(
|
|
270
|
+
provider: Provider,
|
|
271
|
+
tokens: readonly string[],
|
|
272
|
+
anchors: readonly string[],
|
|
273
|
+
anchorsUsd: ReadonlyMap<string, number>,
|
|
274
|
+
protocols: readonly LpProtocol[],
|
|
275
|
+
fees: readonly number[] = [100, 500, 2500, 3000, 10000],
|
|
276
|
+
minLiquidityUsd: number,
|
|
277
|
+
overrides?: CallOverrides,
|
|
278
|
+
): Promise<Array<number | null>> {
|
|
279
|
+
const chainId = await provider.getChainId()
|
|
280
|
+
const key = (t: string) => priceKeyOf(chainId, t)
|
|
281
|
+
// 1. 找池子:代币 × 计价币 × (V3 的每个手续费档位 + V2)
|
|
282
|
+
const lookups: Array<{ token: number; anchor: string; v3: boolean }> = []
|
|
283
|
+
const calls: Call[] = []
|
|
284
|
+
tokens.forEach((token, i) => {
|
|
285
|
+
for (const anchor of anchors) {
|
|
286
|
+
if (key(anchor) === key(token)) continue
|
|
287
|
+
for (const p of protocols) {
|
|
288
|
+
const factory = p.factory as string
|
|
289
|
+
if (p.version === 'v2') {
|
|
290
|
+
lookups.push({ token: i, anchor, v3: false })
|
|
291
|
+
calls.push(provider.contract(factory, V2_FACTORY_ABI).getPair(token, anchor))
|
|
292
|
+
} else {
|
|
293
|
+
for (const fee of fees) {
|
|
294
|
+
lookups.push({ token: i, anchor, v3: true })
|
|
295
|
+
calls.push(provider.contract(factory, V3_FACTORY_ABI).getPool(token, anchor, fee))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
const addresses = await provider.tryAll<string>(calls, overrides)
|
|
302
|
+
const pools = lookups.flatMap((l, i) => {
|
|
303
|
+
const address = addresses[i]
|
|
304
|
+
return address && address.toLowerCase() !== ZERO ? [{ ...l, address }] : []
|
|
305
|
+
})
|
|
306
|
+
if (!pools.length) return tokens.map(() => null)
|
|
307
|
+
|
|
308
|
+
// 2. 读池子(token0 和价格 / 储备、计价币在池子里的余额)和精度
|
|
309
|
+
const decimalsOf = [...new Set([...tokens, ...anchors].map(key))]
|
|
310
|
+
const original = new Map([...tokens, ...anchors].map((t) => [key(t), t]))
|
|
311
|
+
const state = await provider.tryAll(
|
|
312
|
+
[
|
|
313
|
+
...decimalsOf.map((k) => provider.contract(original.get(k) as string, ERC20_ABI).decimals()),
|
|
314
|
+
...pools.flatMap((p) =>
|
|
315
|
+
p.v3
|
|
316
|
+
? [
|
|
317
|
+
provider.contract(p.address, V3_POOL_ABI).token0(),
|
|
318
|
+
provider.contract(p.address, V3_POOL_ABI).slot0(),
|
|
319
|
+
provider.contract(p.anchor, ERC20_ABI).balanceOf(p.address),
|
|
320
|
+
provider.contract(p.address, V3_POOL_ABI).liquidity(),
|
|
321
|
+
]
|
|
322
|
+
: [provider.contract(p.address, V2_PAIR_ABI).token0(), provider.contract(p.address, V2_PAIR_ABI).getReserves()],
|
|
323
|
+
),
|
|
324
|
+
],
|
|
325
|
+
overrides,
|
|
326
|
+
)
|
|
327
|
+
const decimals = new Map(decimalsOf.map((k, i) => [k, state[i] === null || state[i] === undefined ? null : Number(state[i])]))
|
|
328
|
+
const best: Array<{ usd: number; depthUsd: number } | null> = tokens.map(() => null)
|
|
329
|
+
let offset = decimalsOf.length
|
|
330
|
+
for (const p of pools) {
|
|
331
|
+
const size = p.v3 ? 4 : 2
|
|
332
|
+
const [token0, second, third, liquidity] = state.slice(offset, offset + size) as [string | null, { 0: bigint; 1: bigint } | null, bigint | null, bigint | null]
|
|
333
|
+
offset += size
|
|
334
|
+
const token = tokens[p.token] as string
|
|
335
|
+
const dt = decimals.get(key(token))
|
|
336
|
+
const da = decimals.get(key(p.anchor))
|
|
337
|
+
const anchorUsd = anchorsUsd.get(key(p.anchor))
|
|
338
|
+
if (!token0 || !second || dt === null || dt === undefined || da === null || da === undefined || anchorUsd === undefined) continue
|
|
339
|
+
const tokenIs0 = key(token0) === key(token)
|
|
340
|
+
const q = p.v3 ? v3Quote(second[0], third, liquidity ?? null, tokenIs0, dt, da) : v2Quote(second[0], second[1], tokenIs0, dt, da)
|
|
341
|
+
if (!q || !Number.isFinite(q.price) || q.price <= 0) continue
|
|
342
|
+
const depthUsd = q.depth * anchorUsd
|
|
343
|
+
const current = best[p.token]
|
|
344
|
+
if (depthUsd >= minLiquidityUsd && (!current || depthUsd > current.depthUsd)) best[p.token] = { usd: q.price * anchorUsd, depthUsd }
|
|
345
|
+
}
|
|
346
|
+
return best.map((b) => (b ? b.usd : null))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* V3 的深度:池子里计价币的余额和当前价格附近的流动性折算的计价币数量(虚拟储备:计价币是 token1 时 L·√P,是 token0 时 L / √P)取较小的。
|
|
351
|
+
* 只看余额的话,在远离当前价格的区间放一笔计价币、再把价格推过没有流动性的区间,就能用很少的成本让池子“看起来很深”
|
|
352
|
+
*/
|
|
353
|
+
function v3Quote(sqrtPriceX96: bigint, anchorBalance: bigint | null, liquidity: bigint | null, tokenIs0: boolean, dt: number, da: number): PoolQuote | null {
|
|
354
|
+
if (sqrtPriceX96 === 0n || anchorBalance === null || liquidity === null) return null
|
|
355
|
+
// price = token1 / token0(按精度换算)
|
|
356
|
+
const price1Per0 = tokenIs0 ? sqrtPriceToPrice(sqrtPriceX96, dt, da) : sqrtPriceToPrice(sqrtPriceX96, da, dt)
|
|
357
|
+
const sqrtP = Number(sqrtPriceX96) / 2 ** 96
|
|
358
|
+
const virtual = (tokenIs0 ? Number(liquidity) * sqrtP : Number(liquidity) / sqrtP) / 10 ** da
|
|
359
|
+
return { price: tokenIs0 ? price1Per0 : 1 / price1Per0, depth: Math.min(Number(formatUnits(anchorBalance, da)), virtual) }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function v2Quote(reserve0: bigint, reserve1: bigint, tokenIs0: boolean, dt: number, da: number): PoolQuote | null {
|
|
363
|
+
const [rt, ra] = tokenIs0 ? [reserve0, reserve1] : [reserve1, reserve0]
|
|
364
|
+
if (rt === 0n || ra === 0n) return null
|
|
365
|
+
const anchor = Number(formatUnits(ra, da))
|
|
366
|
+
return { price: anchor / Number(formatUnits(rt, dt)), depth: anchor }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// tokenPrices
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
export interface TokenPricesOptions {
|
|
374
|
+
/** 价格源,依次使用(前面没有价格的交给后面的),默认 [defillamaPrices(), onchainPrices()] */
|
|
375
|
+
sources?: readonly PriceSource[]
|
|
376
|
+
/** 某个价格源出错时调用(继续用下一个源) */
|
|
377
|
+
onError?: (source: PriceSource, err: unknown) => void
|
|
378
|
+
/** 自定义 fetch(代理等场景);默认全局 fetch */
|
|
379
|
+
fetch?: typeof fetch
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export interface TokenPrice {
|
|
383
|
+
/** 传入的代币地址(原样) */
|
|
384
|
+
token: string
|
|
385
|
+
native: boolean
|
|
386
|
+
/** 美元价格;所有价格源都没有时为 null */
|
|
387
|
+
price: number | null
|
|
388
|
+
/** 给出价格的价格源(如 'defillama'、'onchain') */
|
|
389
|
+
source: string | null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 查代币的美元价格(主币用 NATIVE_TOKEN 或零地址)。默认先查 DefiLlama,没有收录的再从链上池子算。
|
|
394
|
+
*
|
|
395
|
+
* ```ts
|
|
396
|
+
* await tokenPrices(provider, [NATIVE_TOKEN, USDT, newToken])
|
|
397
|
+
* // [{ token: '0xEeee…', native: true, price: 612.3, source: 'defillama' }, …, { token: newToken, price: 0.0012, source: 'onchain' }]
|
|
398
|
+
* await tokenPrices(provider, tokens, { sources: [defillamaPrices({ at: new Date('2026-01-01') })] }) // 历史价格
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
export async function tokenPrices(provider: Provider, tokens: readonly string[], options: TokenPricesOptions = {}): Promise<TokenPrice[]> {
|
|
402
|
+
const { sources = [defillamaPrices(), onchainPrices()], onError, fetch: fetchOption } = options
|
|
403
|
+
const chainId = await provider.getChainId()
|
|
404
|
+
const fetchFn = fetchOption ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
405
|
+
const native = tokens.map((t) => provider.isNativeToken(t))
|
|
406
|
+
const erc20 = [...new Set(tokens.filter((_, i) => !native[i]).map((t) => (isTronChain(chainId) ? toTronAddress(t) : t)))]
|
|
407
|
+
// 主币 symbol:内置链信息表,或 Provider 的 nativeSymbol 配置(不在表里的链)
|
|
408
|
+
const nativeIndex = native.indexOf(true)
|
|
409
|
+
const nativeSymbol = nativeIndex < 0 ? null : (getNativeCurrency(chainId)?.symbol ?? (await provider.tokens([tokens[nativeIndex] as string], { fields: ['symbol'] }))[0]?.symbol ?? null)
|
|
410
|
+
const found = await runSources(sources, { chainId, tokens: erc20, nativeSymbol, fetch: fetchFn, provider }, onError)
|
|
411
|
+
return tokens.map((token, i) => {
|
|
412
|
+
const hit = found.get(native[i] ? 'native' : priceKeyOf(chainId, token))
|
|
413
|
+
return { token, native: native[i] as boolean, price: hit?.price ?? null, source: hit?.source ?? null }
|
|
414
|
+
})
|
|
415
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Interface } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import type { CallOverrides } from '../aggregate.js'
|
|
4
|
+
import type { Call } from '../call.js'
|
|
5
|
+
import { evm, fmt, same, tokenInfo, type Ctx } from '../liquidity/common.js'
|
|
6
|
+
import { lpPositionsByIds, type LpConcentratedPosition, type LpProtocol } from '../lp.js'
|
|
7
|
+
import type { Provider, StaticCallItem, StaticCallResult } from '../provider.js'
|
|
8
|
+
import { withSignal } from '../util.js'
|
|
9
|
+
import { mapPool } from '../write/send.js'
|
|
10
|
+
import type { ClaimAmount, ClaimItem, FarmProtocol } from './types.js'
|
|
11
|
+
|
|
12
|
+
const masterChef = (chainId: number, address: string): FarmProtocol => ({
|
|
13
|
+
id: 'pancakeswap-v3',
|
|
14
|
+
name: 'PancakeSwap V3 Farm',
|
|
15
|
+
kind: 'masterchef-v3',
|
|
16
|
+
chainId,
|
|
17
|
+
positionManager: '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364',
|
|
18
|
+
masterChef: address,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 内置的挖矿合约(2026-10 在链上逐个核对:合约有代码,MasterChefV3 的 CAKE / nonfungiblePositionManager / poolLength、
|
|
23
|
+
* Voter 的 length / pools / gauges 能读,CLGauge 的 stakedValues / earned 能读)。
|
|
24
|
+
* Uniswap V3 的激励(UniswapV3Staker)按 IncentiveKey 领取,key 不能从链上按用户列出,不在表里
|
|
25
|
+
*/
|
|
26
|
+
export const FARM_PROTOCOLS: readonly FarmProtocol[] = [
|
|
27
|
+
masterChef(56, '0x556B9306565093C855AEA9AE92A594704c2Cd59e'),
|
|
28
|
+
masterChef(1, '0x556B9306565093C855AEA9AE92A594704c2Cd59e'),
|
|
29
|
+
masterChef(8453, '0xC6A2Db661D5a5690172d8eB0a7DEA2d3008665A3'),
|
|
30
|
+
masterChef(42161, '0x5e09ACf80C0296740eC5d6F643005a4ef8DaA694'),
|
|
31
|
+
{
|
|
32
|
+
id: 'aerodrome-slipstream',
|
|
33
|
+
name: 'Aerodrome Slipstream',
|
|
34
|
+
kind: 'gauge',
|
|
35
|
+
chainId: 8453,
|
|
36
|
+
positionManager: '0x827922686190790b37229fd06084350E74485b72',
|
|
37
|
+
voter: '0x16613524e02ad97eDfeF371bC883F2F5d6C480A5',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'velodrome-slipstream',
|
|
41
|
+
name: 'Velodrome Slipstream',
|
|
42
|
+
kind: 'gauge',
|
|
43
|
+
chainId: 10,
|
|
44
|
+
positionManager: '0x416b433906b1B72FA758e166e239c43d68dC6F29',
|
|
45
|
+
voter: '0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C',
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
export const masterChefInterface = new Interface([
|
|
50
|
+
'function CAKE() view returns (address)',
|
|
51
|
+
'function balanceOf(address user) view returns (uint256)',
|
|
52
|
+
'function tokenOfOwnerByIndex(address user, uint256 index) view returns (uint256)',
|
|
53
|
+
'function pendingCake(uint256 tokenId) view returns (uint256)',
|
|
54
|
+
'function harvest(uint256 tokenId, address to) returns (uint256 reward)',
|
|
55
|
+
'function collectTo((uint256 tokenId, address recipient, uint128 amount0Max, uint128 amount1Max) params, address to) returns (uint256 amount0, uint256 amount1)',
|
|
56
|
+
'function multicall(bytes[] data) payable returns (bytes[] results)',
|
|
57
|
+
])
|
|
58
|
+
export const gaugeInterface = new Interface(['function getReward(uint256 tokenId)'])
|
|
59
|
+
|
|
60
|
+
const voterAbi = ['function length() view returns (uint256)', 'function pools(uint256 index) view returns (address)', 'function gauges(address pool) view returns (address)']
|
|
61
|
+
const gaugeAbi = ['function stakedValues(address depositor) view returns (uint256[])', 'function earned(address account, uint256 tokenId) view returns (uint256)', 'function rewardToken() view returns (address)']
|
|
62
|
+
|
|
63
|
+
const CHUNK = 400
|
|
64
|
+
/** 每个 Voter 最多枚举多少个池子(Aerodrome 目前两千多个) */
|
|
65
|
+
const MAX_POOLS = 6000
|
|
66
|
+
|
|
67
|
+
/** harvest 预执行:每条一个 eth_call,分批并发(受限节点上一次几百个会被限流) */
|
|
68
|
+
const HARVEST_CHUNK = 25
|
|
69
|
+
|
|
70
|
+
const chunks = <T>(list: readonly T[]): T[][] => Array.from({ length: Math.ceil(list.length / CHUNK) }, (_, i) => list.slice(i * CHUNK, (i + 1) * CHUNK))
|
|
71
|
+
|
|
72
|
+
async function staticCallChunked(provider: Provider, items: readonly StaticCallItem[]): Promise<StaticCallResult<bigint>[]> {
|
|
73
|
+
const parts = Array.from({ length: Math.ceil(items.length / HARVEST_CHUNK) }, (_, i) => items.slice(i * HARVEST_CHUNK, (i + 1) * HARVEST_CHUNK))
|
|
74
|
+
return (await mapPool(parts, 2, (part) => provider.staticCallAll<bigint>(part))).flat()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 分批 tryAll:一次 multicall 不塞太多调用 */
|
|
78
|
+
async function tryAllChunked<T>(provider: Provider, calls: readonly Call[], overrides: CallOverrides): Promise<(T | null)[]> {
|
|
79
|
+
return (await Promise.all(chunks(calls).map((part) => provider.tryAll<T>(part, overrides)))).flat()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Voter 枚举出的 gauge 列表(几乎不变):按 Provider(即按链)+ Voter 缓存 1 小时,只缓存最新状态的结果。
|
|
83
|
+
// 共享的查询不带调用方的 signal / minBlock(否则第一个调用方取消会让其他等着的调用方一起失败),调用方各自按自己的 signal 取消等待
|
|
84
|
+
const gaugeCache = new WeakMap<Provider, Map<string, { at: number; gauges: Promise<string[]> }>>()
|
|
85
|
+
const GAUGE_TTL = 3_600_000
|
|
86
|
+
|
|
87
|
+
async function enumerateGauges(provider: Provider, voter: string, overrides: CallOverrides): Promise<string[]> {
|
|
88
|
+
const v = provider.contract(voter, voterAbi)
|
|
89
|
+
const length = Math.min(Number(await provider.call<bigint>(v.length(), overrides)), MAX_POOLS)
|
|
90
|
+
const pools = await tryAllChunked<string>(provider, Array.from({ length }, (_, i) => v.pools(i)), overrides)
|
|
91
|
+
const list = pools.filter((p): p is string => p !== null)
|
|
92
|
+
const found = await tryAllChunked<string>(provider, list.map((p) => v.gauges(p)), overrides)
|
|
93
|
+
return [...new Set(found.filter((g): g is string => g !== null && !/^0x0{40}$/.test(g)))]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discoverGauges(provider: Provider, voter: string, overrides: CallOverrides): Promise<string[]> {
|
|
97
|
+
if (overrides.blockTag !== undefined && overrides.blockTag !== 'latest') {
|
|
98
|
+
return enumerateGauges(provider, voter, overrides) // 历史区块:不缓存
|
|
99
|
+
}
|
|
100
|
+
const cache = gaugeCache.get(provider) ?? new Map()
|
|
101
|
+
gaugeCache.set(provider, cache)
|
|
102
|
+
const key = voter.toLowerCase()
|
|
103
|
+
const hit = cache.get(key)
|
|
104
|
+
let gauges: Promise<string[]>
|
|
105
|
+
if (hit && Date.now() - hit.at < GAUGE_TTL) {
|
|
106
|
+
gauges = hit.gauges
|
|
107
|
+
} else {
|
|
108
|
+
gauges = enumerateGauges(provider, voter, {})
|
|
109
|
+
const entry = { at: Date.now(), gauges }
|
|
110
|
+
cache.set(key, entry)
|
|
111
|
+
gauges.catch(() => {
|
|
112
|
+
if (cache.get(key) === entry) cache.delete(key)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return withSignal(overrides.signal, () => gauges)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type Source = { ctx: Ctx; owner: string; overrides: CallOverrides; maxPositions: number; lpProtocols: readonly LpProtocol[] }
|
|
119
|
+
|
|
120
|
+
const amountOf = (token: ClaimAmount['token'], value: bigint): ClaimAmount => ({ token, amount: value.toString(), formatted: fmt(value, token) })
|
|
121
|
+
|
|
122
|
+
/** MasterChefV3:用户质押的仓位。返回挖矿奖励(CAKE)和质押仓位未领取的手续费 */
|
|
123
|
+
export async function readMasterChef(source: Source, farm: FarmProtocol, wantFees: boolean, wantRewards: boolean): Promise<ClaimItem[]> {
|
|
124
|
+
const { ctx, owner, overrides, maxPositions } = source
|
|
125
|
+
const { provider } = ctx
|
|
126
|
+
const mcAddress = evm(farm.masterChef as string)
|
|
127
|
+
const mc = provider.contract(mcAddress, masterChefInterface)
|
|
128
|
+
const count = Number(await provider.call<bigint>(mc.balanceOf(owner), overrides))
|
|
129
|
+
if (!count) {
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
// 质押多的地址只看最近的 maxPositions 个(新质押的排在最后)
|
|
133
|
+
const indexes = Array.from({ length: Math.min(count, maxPositions) }, (_, i) => count - Math.min(count, maxPositions) + i)
|
|
134
|
+
const ids = (await tryAllChunked<bigint>(provider, indexes.map((i) => mc.tokenOfOwnerByIndex(owner, i)), overrides)).filter((id): id is bigint => id !== null)
|
|
135
|
+
const items: ClaimItem[] = []
|
|
136
|
+
if (wantRewards) {
|
|
137
|
+
// pendingCake 不含奖励池自上次更新以来新增的部分(比实际领到的少,差额随时间变大):
|
|
138
|
+
// 以所有者的身份预执行 harvest 拿到合约真正会发的数量,预执行不了(钱包节点不支持 from 等)时退回 pendingCake
|
|
139
|
+
const [pending, exact, cake] = await Promise.all([
|
|
140
|
+
tryAllChunked<bigint>(provider, ids.map((id) => mc.pendingCake(id)), overrides),
|
|
141
|
+
staticCallChunked(provider, ids.map((id) => ({ call: mc.harvest(id, owner), overrides: { ...overrides, from: owner } }))),
|
|
142
|
+
provider.call<string>(mc.CAKE(), overrides),
|
|
143
|
+
])
|
|
144
|
+
const [token] = await tokenInfo(ctx, [evm(cake)])
|
|
145
|
+
ids.forEach((id, i) => {
|
|
146
|
+
const run = exact[i]
|
|
147
|
+
// 预执行失败(限流 / 节点不支持 …)时退回 pendingCake,数量可能偏少,标 approximate
|
|
148
|
+
const value = run?.success ? BigInt(run.data) : (pending[i] ?? 0n)
|
|
149
|
+
if (value > 0n && token) {
|
|
150
|
+
items.push({
|
|
151
|
+
id: `farm:${farm.id}:${id}`,
|
|
152
|
+
kind: 'farm',
|
|
153
|
+
protocol: farm.id,
|
|
154
|
+
protocolName: farm.name,
|
|
155
|
+
tokenId: id.toString(),
|
|
156
|
+
pool: null,
|
|
157
|
+
staked: true,
|
|
158
|
+
target: mcAddress,
|
|
159
|
+
amounts: [amountOf(token, value)],
|
|
160
|
+
...(run?.success ? {} : { approximate: true }),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
const lp = source.lpProtocols.find((p) => p.version === 'v3' && p.chainId === ctx.chainId && p.positionManager && same(p.positionManager, farm.positionManager))
|
|
166
|
+
if (wantFees && lp && ids.length) {
|
|
167
|
+
const positions = (await lpPositionsByIds(provider, ids.map((tokenId) => ({ protocol: lp, tokenId })), { ...overrides })) as LpConcentratedPosition[]
|
|
168
|
+
for (const p of positions) {
|
|
169
|
+
if (BigInt(p.fees0) + BigInt(p.fees1) > 0n) {
|
|
170
|
+
items.push({
|
|
171
|
+
id: `fees:${lp.id}:${p.tokenId}`,
|
|
172
|
+
kind: 'fees',
|
|
173
|
+
protocol: lp.id,
|
|
174
|
+
protocolName: lp.name,
|
|
175
|
+
version: 'v3',
|
|
176
|
+
tokenId: p.tokenId,
|
|
177
|
+
pool: p.pool,
|
|
178
|
+
staked: true,
|
|
179
|
+
target: mcAddress,
|
|
180
|
+
amounts: [
|
|
181
|
+
{ token: p.token0, amount: p.fees0, formatted: p.fees0Formatted },
|
|
182
|
+
{ token: p.token1, amount: p.fees1, formatted: p.fees1Formatted },
|
|
183
|
+
],
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return items
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Slipstream gauge:用户质押的仓位和待领取的奖励 */
|
|
192
|
+
export async function readGauges(source: Source, farm: FarmProtocol, gauges: readonly string[] | undefined): Promise<ClaimItem[]> {
|
|
193
|
+
const { ctx, owner, overrides } = source
|
|
194
|
+
const { provider } = ctx
|
|
195
|
+
const list = gauges ? gauges.map(evm) : await discoverGauges(provider, evm(farm.voter as string), overrides)
|
|
196
|
+
const staked = await tryAllChunked<bigint[]>(provider, list.map((g) => provider.contract(g, gaugeAbi).stakedValues(owner)), overrides)
|
|
197
|
+
const pairs = list.flatMap((gauge, i) => (staked[i] ?? []).slice(0, source.maxPositions).map((tokenId) => ({ gauge, tokenId })))
|
|
198
|
+
if (!pairs.length) {
|
|
199
|
+
return []
|
|
200
|
+
}
|
|
201
|
+
const [earned, rewardTokens] = await Promise.all([
|
|
202
|
+
tryAllChunked<bigint>(provider, pairs.map((x) => provider.contract(x.gauge, gaugeAbi).earned(owner, x.tokenId)), overrides),
|
|
203
|
+
tryAllChunked<string>(provider, [...new Set(pairs.map((x) => x.gauge))].map((g) => provider.contract(g, gaugeAbi).rewardToken()), overrides),
|
|
204
|
+
])
|
|
205
|
+
const gaugeList = [...new Set(pairs.map((x) => x.gauge))]
|
|
206
|
+
const rewardOf = new Map(gaugeList.map((g, i) => [g, rewardTokens[i] ?? null]))
|
|
207
|
+
const tokens = await tokenInfo(ctx, [...new Set([...rewardOf.values()].filter((t): t is string => t !== null).map(evm))])
|
|
208
|
+
const infoOf = new Map(tokens.map((t) => [evm(t.address), t]))
|
|
209
|
+
return pairs.flatMap(({ gauge, tokenId }, i): ClaimItem[] => {
|
|
210
|
+
const value = earned[i] ?? 0n
|
|
211
|
+
const reward = rewardOf.get(gauge)
|
|
212
|
+
const token = reward ? infoOf.get(evm(reward)) : undefined
|
|
213
|
+
return value > 0n && token
|
|
214
|
+
? [
|
|
215
|
+
{
|
|
216
|
+
id: `gauge:${farm.id}:${tokenId}`,
|
|
217
|
+
kind: 'gauge',
|
|
218
|
+
protocol: farm.id,
|
|
219
|
+
protocolName: farm.name,
|
|
220
|
+
tokenId: tokenId.toString(),
|
|
221
|
+
pool: null,
|
|
222
|
+
staked: true,
|
|
223
|
+
target: gauge,
|
|
224
|
+
amounts: [amountOf(token, value)],
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
: []
|
|
228
|
+
})
|
|
229
|
+
}
|