@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.
Files changed (248) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +367 -3
  3. package/contracts/TokenProbe.sol +103 -0
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js +114 -3
  6. package/dist/aggregate.js.map +1 -1
  7. package/dist/approvals.d.ts +169 -0
  8. package/dist/approvals.d.ts.map +1 -0
  9. package/dist/approvals.js +310 -0
  10. package/dist/approvals.js.map +1 -0
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +4 -0
  13. package/dist/chains.js.map +1 -1
  14. package/dist/dividends.d.ts +135 -0
  15. package/dist/dividends.d.ts.map +1 -0
  16. package/dist/dividends.js +253 -0
  17. package/dist/dividends.js.map +1 -0
  18. package/dist/fees.d.ts +97 -0
  19. package/dist/fees.d.ts.map +1 -0
  20. package/dist/fees.js +115 -0
  21. package/dist/fees.js.map +1 -0
  22. package/dist/history.d.ts +154 -0
  23. package/dist/history.d.ts.map +1 -0
  24. package/dist/history.js +247 -0
  25. package/dist/history.js.map +1 -0
  26. package/dist/historySources.d.ts +32 -0
  27. package/dist/historySources.d.ts.map +1 -0
  28. package/dist/historySources.js +319 -0
  29. package/dist/historySources.js.map +1 -0
  30. package/dist/http.d.ts +10 -0
  31. package/dist/http.d.ts.map +1 -0
  32. package/dist/http.js +30 -0
  33. package/dist/http.js.map +1 -0
  34. package/dist/liquidity/v3.d.ts +2 -0
  35. package/dist/liquidity/v3.d.ts.map +1 -1
  36. package/dist/liquidity/v3.js +1 -1
  37. package/dist/liquidity/v3.js.map +1 -1
  38. package/dist/liquidity/v4.d.ts +21 -0
  39. package/dist/liquidity/v4.d.ts.map +1 -1
  40. package/dist/liquidity/v4.js +4 -3
  41. package/dist/liquidity/v4.js.map +1 -1
  42. package/dist/lp.d.ts +10 -1
  43. package/dist/lp.d.ts.map +1 -1
  44. package/dist/lp.js +19 -2
  45. package/dist/lp.js.map +1 -1
  46. package/dist/lpV4.d.ts +4 -0
  47. package/dist/lpV4.d.ts.map +1 -1
  48. package/dist/lpV4.js +27 -3
  49. package/dist/lpV4.js.map +1 -1
  50. package/dist/multicall.d.ts.map +1 -1
  51. package/dist/multicall.js +3 -0
  52. package/dist/multicall.js.map +1 -1
  53. package/dist/names.d.ts +55 -0
  54. package/dist/names.d.ts.map +1 -0
  55. package/dist/names.js +93 -0
  56. package/dist/names.js.map +1 -0
  57. package/dist/owner.d.ts +4 -26
  58. package/dist/owner.d.ts.map +1 -1
  59. package/dist/owner.js +7 -112
  60. package/dist/owner.js.map +1 -1
  61. package/dist/permit.d.ts +137 -0
  62. package/dist/permit.d.ts.map +1 -0
  63. package/dist/permit.js +355 -0
  64. package/dist/permit.js.map +1 -0
  65. package/dist/portfolio.d.ts +56 -0
  66. package/dist/portfolio.d.ts.map +1 -0
  67. package/dist/portfolio.js +54 -0
  68. package/dist/portfolio.js.map +1 -0
  69. package/dist/price.d.ts +95 -0
  70. package/dist/price.d.ts.map +1 -0
  71. package/dist/price.js +329 -0
  72. package/dist/price.js.map +1 -0
  73. package/dist/rewards/farms.d.ts +26 -0
  74. package/dist/rewards/farms.d.ts.map +1 -0
  75. package/dist/rewards/farms.js +210 -0
  76. package/dist/rewards/farms.js.map +1 -0
  77. package/dist/rewards/index.d.ts +42 -0
  78. package/dist/rewards/index.d.ts.map +1 -0
  79. package/dist/rewards/index.js +382 -0
  80. package/dist/rewards/index.js.map +1 -0
  81. package/dist/rewards/types.d.ts +170 -0
  82. package/dist/rewards/types.d.ts.map +1 -0
  83. package/dist/rewards/types.js +2 -0
  84. package/dist/rewards/types.js.map +1 -0
  85. package/dist/risk.d.ts +42 -0
  86. package/dist/risk.d.ts.map +1 -0
  87. package/dist/risk.js +134 -0
  88. package/dist/risk.js.map +1 -0
  89. package/dist/rpcNodes.d.ts.map +1 -1
  90. package/dist/rpcNodes.js +18 -0
  91. package/dist/rpcNodes.js.map +1 -1
  92. package/dist/simulate.d.ts +186 -0
  93. package/dist/simulate.d.ts.map +1 -0
  94. package/dist/simulate.js +559 -0
  95. package/dist/simulate.js.map +1 -0
  96. package/dist/simulateDecode.d.ts +33 -0
  97. package/dist/simulateDecode.d.ts.map +1 -0
  98. package/dist/simulateDecode.js +321 -0
  99. package/dist/simulateDecode.js.map +1 -0
  100. package/dist/smartAccount/account.d.ts +31 -0
  101. package/dist/smartAccount/account.d.ts.map +1 -0
  102. package/dist/smartAccount/account.js +33 -0
  103. package/dist/smartAccount/account.js.map +1 -0
  104. package/dist/smartAccount/clients.d.ts +77 -0
  105. package/dist/smartAccount/clients.d.ts.map +1 -0
  106. package/dist/smartAccount/clients.js +122 -0
  107. package/dist/smartAccount/clients.js.map +1 -0
  108. package/dist/smartAccount/eip5792.d.ts +77 -0
  109. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  110. package/dist/smartAccount/eip5792.js +152 -0
  111. package/dist/smartAccount/eip5792.js.map +1 -0
  112. package/dist/smartAccount/send.d.ts +40 -0
  113. package/dist/smartAccount/send.d.ts.map +1 -0
  114. package/dist/smartAccount/send.js +271 -0
  115. package/dist/smartAccount/send.js.map +1 -0
  116. package/dist/smartAccount/userOp.d.ts +91 -0
  117. package/dist/smartAccount/userOp.d.ts.map +1 -0
  118. package/dist/smartAccount/userOp.js +124 -0
  119. package/dist/smartAccount/userOp.js.map +1 -0
  120. package/dist/subpaths/approvals.d.ts +26 -0
  121. package/dist/subpaths/approvals.d.ts.map +1 -0
  122. package/dist/subpaths/approvals.js +27 -0
  123. package/dist/subpaths/approvals.js.map +1 -0
  124. package/dist/subpaths/dividends.d.ts +28 -0
  125. package/dist/subpaths/dividends.d.ts.map +1 -0
  126. package/dist/subpaths/dividends.js +29 -0
  127. package/dist/subpaths/dividends.js.map +1 -0
  128. package/dist/subpaths/fees.d.ts +23 -0
  129. package/dist/subpaths/fees.d.ts.map +1 -0
  130. package/dist/subpaths/fees.js +25 -0
  131. package/dist/subpaths/fees.js.map +1 -0
  132. package/dist/subpaths/history.d.ts +22 -0
  133. package/dist/subpaths/history.d.ts.map +1 -0
  134. package/dist/subpaths/history.js +23 -0
  135. package/dist/subpaths/history.js.map +1 -0
  136. package/dist/subpaths/names.d.ts +35 -0
  137. package/dist/subpaths/names.d.ts.map +1 -0
  138. package/dist/subpaths/names.js +39 -0
  139. package/dist/subpaths/names.js.map +1 -0
  140. package/dist/subpaths/permit.d.ts +14 -0
  141. package/dist/subpaths/permit.d.ts.map +1 -0
  142. package/dist/subpaths/permit.js +14 -0
  143. package/dist/subpaths/permit.js.map +1 -0
  144. package/dist/subpaths/price.d.ts +38 -0
  145. package/dist/subpaths/price.d.ts.map +1 -0
  146. package/dist/subpaths/price.js +40 -0
  147. package/dist/subpaths/price.js.map +1 -0
  148. package/dist/subpaths/rewards.d.ts +27 -0
  149. package/dist/subpaths/rewards.d.ts.map +1 -0
  150. package/dist/subpaths/rewards.js +25 -0
  151. package/dist/subpaths/rewards.js.map +1 -0
  152. package/dist/subpaths/risk.d.ts +19 -0
  153. package/dist/subpaths/risk.d.ts.map +1 -0
  154. package/dist/subpaths/risk.js +20 -0
  155. package/dist/subpaths/risk.js.map +1 -0
  156. package/dist/subpaths/simulate.d.ts +25 -0
  157. package/dist/subpaths/simulate.d.ts.map +1 -0
  158. package/dist/subpaths/simulate.js +28 -0
  159. package/dist/subpaths/simulate.js.map +1 -0
  160. package/dist/subpaths/smart-account.d.ts +24 -0
  161. package/dist/subpaths/smart-account.d.ts.map +1 -0
  162. package/dist/subpaths/smart-account.js +24 -0
  163. package/dist/subpaths/smart-account.js.map +1 -0
  164. package/dist/subpaths/swap.d.ts +19 -0
  165. package/dist/subpaths/swap.d.ts.map +1 -0
  166. package/dist/subpaths/swap.js +20 -0
  167. package/dist/subpaths/swap.js.map +1 -0
  168. package/dist/swap.d.ts +168 -0
  169. package/dist/swap.d.ts.map +1 -0
  170. package/dist/swap.js +387 -0
  171. package/dist/swap.js.map +1 -0
  172. package/dist/tokenProbe.d.ts +3 -0
  173. package/dist/tokenProbe.d.ts.map +1 -0
  174. package/dist/tokenProbe.js +4 -0
  175. package/dist/tokenProbe.js.map +1 -0
  176. package/dist/write/batch.d.ts +34 -2
  177. package/dist/write/batch.d.ts.map +1 -1
  178. package/dist/write/batch.js +22 -3
  179. package/dist/write/batch.js.map +1 -1
  180. package/dist/write/collect.d.ts +9 -0
  181. package/dist/write/collect.d.ts.map +1 -1
  182. package/dist/write/collect.js +41 -1
  183. package/dist/write/collect.js.map +1 -1
  184. package/dist/write/send.d.ts +4 -0
  185. package/dist/write/send.d.ts.map +1 -1
  186. package/dist/write/send.js.map +1 -1
  187. package/dist/write/signer.d.ts +5 -1
  188. package/dist/write/signer.d.ts.map +1 -1
  189. package/dist/write/signer.js.map +1 -1
  190. package/dist/write/transfer.d.ts +5 -3
  191. package/dist/write/transfer.d.ts.map +1 -1
  192. package/dist/write/transfer.js +12 -6
  193. package/dist/write/transfer.js.map +1 -1
  194. package/dist/write/tron.d.ts +25 -0
  195. package/dist/write/tron.d.ts.map +1 -1
  196. package/dist/write/tron.js +26 -0
  197. package/dist/write/tron.js.map +1 -1
  198. package/package.json +73 -16
  199. package/src/aggregate.ts +110 -3
  200. package/src/approvals.ts +462 -0
  201. package/src/chains.ts +4 -0
  202. package/src/dividends.ts +360 -0
  203. package/src/fees.ts +201 -0
  204. package/src/history.ts +367 -0
  205. package/src/historySources.ts +360 -0
  206. package/src/http.ts +34 -0
  207. package/src/liquidity/v3.ts +1 -1
  208. package/src/liquidity/v4.ts +4 -3
  209. package/src/lp.ts +21 -2
  210. package/src/lpV4.ts +32 -2
  211. package/src/multicall.ts +3 -0
  212. package/src/names.ts +141 -0
  213. package/src/owner.ts +7 -138
  214. package/src/permit.ts +457 -0
  215. package/src/portfolio.ts +98 -0
  216. package/src/price.ts +415 -0
  217. package/src/rewards/farms.ts +229 -0
  218. package/src/rewards/index.ts +431 -0
  219. package/src/rewards/types.ts +179 -0
  220. package/src/risk.ts +166 -0
  221. package/src/rpcNodes.ts +18 -0
  222. package/src/simulate.ts +727 -0
  223. package/src/simulateDecode.ts +352 -0
  224. package/src/smartAccount/account.ts +63 -0
  225. package/src/smartAccount/clients.ts +180 -0
  226. package/src/smartAccount/eip5792.ts +187 -0
  227. package/src/smartAccount/send.ts +341 -0
  228. package/src/smartAccount/userOp.ts +180 -0
  229. package/src/subpaths/approvals.ts +48 -0
  230. package/src/subpaths/dividends.ts +47 -0
  231. package/src/subpaths/fees.ts +43 -0
  232. package/src/subpaths/history.ts +41 -0
  233. package/src/subpaths/names.ts +43 -0
  234. package/src/subpaths/permit.ts +39 -0
  235. package/src/subpaths/price.ts +60 -0
  236. package/src/subpaths/rewards.ts +46 -0
  237. package/src/subpaths/risk.ts +23 -0
  238. package/src/subpaths/simulate.ts +51 -0
  239. package/src/subpaths/smart-account.ts +40 -0
  240. package/src/subpaths/swap.ts +36 -0
  241. package/src/swap.ts +534 -0
  242. package/src/tokenProbe.ts +5 -0
  243. package/src/write/batch.ts +44 -5
  244. package/src/write/collect.ts +60 -1
  245. package/src/write/send.ts +4 -0
  246. package/src/write/signer.ts +5 -1
  247. package/src/write/transfer.ts +17 -10
  248. package/src/write/tron.ts +43 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * 历史记录的数据来源(适配器)。每个适配器按 HistoryQuery 返回原始的交易和转账(RawPage),合并和分类由 history.ts 完成。
3
+ *
4
+ * - blockscout:各链的 Blockscout 实例(免 Key)的 Etherscan 兼容接口(`/api?module=account&action=txlist …`)。
5
+ * 没用 v2 REST(`/api/v2/addresses/…`):同样的数据,v2 每页带大量标签 / 元数据(50 条约 500KB),这个接口只有必要字段,
6
+ * 并且与 Etherscan 系完全相同,一套解析两处用
7
+ * - etherscan:Etherscan v2(一个 Key 覆盖 60+ 条链)及其他 Etherscan 兼容的接口
8
+ * - rpc:只用 `eth_getLogs`(ERC20 / 721 / 1155 的 Transfer 日志按地址所在的 topic 查)+ 交易 / 回执 / 区块。没有主币转账(顶层的 value 只有
9
+ * 带代币转账的交易才有)、没有内部交易;要按区块窗口往回扫,稀疏的地址一次调用可能扫不到记录(返回 nextCursor 继续)
10
+ * - trongrid:TronGrid 的 `/v1/accounts/{address}/transactions` 和 `/transactions/trc20`(内部的 TRX 转账查不到)
11
+ */
12
+ import { AbiCoder, id, zeroPadValue } from 'ethers'
13
+
14
+ import { HttpStatusError, fetchJson } from './http.js'
15
+ import type { HistoryAdapter, HistoryQuery, RawMove, RawPage, RawTx } from './history.js'
16
+ import { errorText } from './fallback.js'
17
+ import { isTronChain, toTronAddress } from './tron.js'
18
+
19
+ export type HistorySource = 'blockscout' | 'etherscan' | 'rpc' | 'trongrid' | HistoryAdapter
20
+
21
+ /**
22
+ * 各链的 Blockscout 实例。2026-10 验证过可用:1、10、100、534352、324、130、42220、34443、57073;
23
+ * 8453 / 42161 / 137 的官方实例前面有 Cloudflare 人机验证,脚本请求可能被 403(此时自动换下一个来源)
24
+ */
25
+ export const BLOCKSCOUT_APIS: Readonly<Record<number, string>> = {
26
+ 1: 'https://eth.blockscout.com/api',
27
+ 10: 'https://explorer.optimism.io/api',
28
+ 100: 'https://gnosisscan.io/api',
29
+ 130: 'https://unichain.blockscout.com/api',
30
+ 137: 'https://polygon.blockscout.com/api',
31
+ 324: 'https://zksync.blockscout.com/api',
32
+ 8453: 'https://base.blockscout.com/api',
33
+ 34443: 'https://explorer.mode.network/api',
34
+ 42161: 'https://arbitrum.blockscout.com/api',
35
+ 42220: 'https://celo.blockscout.com/api',
36
+ 57073: 'https://explorer.inkonchain.com/api',
37
+ 534352: 'https://scrollscan.com/api',
38
+ }
39
+
40
+ const num = (v: unknown) => Number(v ?? 0)
41
+ const big = (v: unknown) => {
42
+ try {
43
+ return BigInt(String(v ?? '0') || '0')
44
+ } catch {
45
+ return 0n
46
+ }
47
+ }
48
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
49
+
50
+ type Row = Record<string, string>
51
+
52
+ /** 各个列表(每个都按位置从新到旧、最多 limit 条)合成一页:列表满了说明后面还有,被截断的位置要留到下一页 */
53
+ function paged(txs: RawTx[], moves: RawMove[], lists: ReadonlyArray<{ count: number; lastPos: number }>, limit: number): RawPage {
54
+ const full = lists.filter((l) => l.count >= limit && l.count > 0)
55
+ if (!full.length) return { txs, moves, completeFrom: 0 }
56
+ const last = Math.max(...full.map((l) => l.lastPos))
57
+ return { txs, moves, completeFrom: last + 1, next: last }
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Etherscan 兼容接口(Blockscout / Etherscan v2)
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const ACTIONS = { txs: 'txlist', internal: 'txlistinternal', tokens: 'tokentx', nfts: 'tokennfttx', multi: 'token1155tx' } as const
65
+
66
+ async function explorerCall(q: HistoryQuery, base: string, action: string, secret?: string): Promise<Row[]> {
67
+ const url = new URL(base)
68
+ const params: Record<string, string> = { module: 'account', action, address: q.address, startblock: '0', page: '1', offset: String(q.limit), sort: 'desc' }
69
+ if (q.endPos !== undefined) params.endblock = String(q.endPos)
70
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
71
+ for (let attempt = 0; ; attempt++) {
72
+ let json: { status?: string; message?: string; result?: unknown }
73
+ try {
74
+ json = await fetchJson(q.fetch, url.toString(), undefined, secret)
75
+ } catch (err) {
76
+ // 公共实例限流(HTTP 429):等一下重试
77
+ if (err instanceof HttpStatusError && err.status === 429 && attempt < 3) {
78
+ await sleep(1200 * (attempt + 1))
79
+ continue
80
+ }
81
+ throw err
82
+ }
83
+ if (Array.isArray(json.result)) {
84
+ const rows = json.result as Row[]
85
+ // 个别实例的某些列表不认 endblock,返回的还是最新的记录:翻页会重复 / 漏记录,换下一个来源
86
+ if (q.endPos !== undefined && rows.some((r) => num(r.blockNumber) > (q.endPos as number))) throw new Error(`${new URL(base).host}: ${action} ignores endblock`)
87
+ return rows
88
+ }
89
+ const text = typeof json.result === 'string' ? json.result : (json.message ?? 'unexpected response')
90
+ if (/no (transactions|records|token)/i.test(`${json.message} ${text}`)) return []
91
+ if (/rate limit|too many/i.test(text) && attempt < 3) {
92
+ await sleep(1200 * (attempt + 1))
93
+ continue
94
+ }
95
+ throw new Error(`${new URL(base).host}: ${text}`)
96
+ }
97
+ }
98
+
99
+ /** Blockscout / Etherscan 系适配器:`url` 是接口地址(可以带 chainid 等固定参数) */
100
+ export function explorerSource(name: string, url: (chainId: number) => string | undefined, secret?: string): HistoryAdapter {
101
+ return {
102
+ name,
103
+ supports: (chainId) => url(chainId) !== undefined,
104
+ async fetch(q) {
105
+ const base = url(q.chainId)
106
+ if (!base) throw new Error(`${name}: chain ${q.chainId} is not supported`)
107
+ const call = (action: string) => explorerCall(q, base, action, secret)
108
+ // 依次请求(公共实例按请求频率限流,并发会触发 429)
109
+ const txRows = await call(ACTIONS.txs)
110
+ const internalRows = await call(ACTIONS.internal)
111
+ const tokenRows = await call(ACTIONS.tokens)
112
+ const nftRows = q.nft ? await call(ACTIONS.nfts) : []
113
+ const multiRows = q.nft ? await call(ACTIONS.multi) : []
114
+ const txs: RawTx[] = txRows.map((r) => ({
115
+ hash: r.hash as string,
116
+ pos: num(r.blockNumber),
117
+ block: num(r.blockNumber),
118
+ time: num(r.timeStamp),
119
+ from: r.from as string,
120
+ to: r.to || r.contractAddress || null,
121
+ value: big(r.value),
122
+ input: r.input || '0x',
123
+ ok: r.isError !== '1' && r.txreceipt_status !== '0',
124
+ fee: big(r.gasUsed) * big(r.gasPrice),
125
+ }))
126
+ const moves: RawMove[] = []
127
+ const base0 = (r: Row) => ({ hash: (r.hash ?? r.transactionHash) as string, pos: num(r.blockNumber), block: num(r.blockNumber), time: num(r.timeStamp), from: r.from as string, to: r.to as string })
128
+ for (const r of internalRows) if (r.isError !== '1' && big(r.value) > 0n) moves.push({ ...base0(r), asset: 'native', value: big(r.value) })
129
+ for (const r of tokenRows) moves.push({ ...base0(r), asset: r.contractAddress as string, value: big(r.value), symbol: r.tokenSymbol || null, decimals: r.tokenDecimal ? num(r.tokenDecimal) : null })
130
+ for (const r of nftRows) moves.push({ ...base0(r), asset: r.contractAddress as string, value: 1n, tokenId: r.tokenID as string, symbol: r.tokenSymbol || null })
131
+ for (const r of multiRows) moves.push({ ...base0(r), asset: r.contractAddress as string, value: big(r.tokenValue || '1'), tokenId: r.tokenID as string, symbol: r.tokenSymbol || null })
132
+ const lists = [txRows, internalRows, tokenRows, nftRows, multiRows].map((rows) => ({ count: rows.length, lastPos: rows.length ? Math.min(...rows.map((r) => num(r.blockNumber))) : 0 }))
133
+ return paged(txs, moves, lists, q.limit)
134
+ },
135
+ }
136
+ }
137
+
138
+ /** Blockscout:`urls` 覆盖 / 补充内置的实例表(chainId → 接口地址) */
139
+ export function blockscoutSource(urls: Readonly<Record<number, string>> = {}): HistoryAdapter {
140
+ return explorerSource('blockscout', (chainId) => urls[chainId] ?? BLOCKSCOUT_APIS[chainId])
141
+ }
142
+
143
+ /** Etherscan v2:一个 Key 覆盖多条链;`baseUrl` 可换成其他 Etherscan 兼容的地址 */
144
+ export function etherscanSource(options: { apiKey: string; baseUrl?: string }): HistoryAdapter {
145
+ if (!options.apiKey) throw new Error('etherscan source requires an apiKey')
146
+ return explorerSource('etherscan', (chainId) => `${options.baseUrl ?? 'https://api.etherscan.io/v2/api'}?chainid=${chainId}&apikey=${encodeURIComponent(options.apiKey)}`, options.apiKey)
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // 纯 RPC 日志
151
+ // ---------------------------------------------------------------------------
152
+
153
+ const TRANSFER = id('Transfer(address,address,uint256)')
154
+ const SINGLE = id('TransferSingle(address,address,address,uint256,uint256)')
155
+ const BATCH = id('TransferBatch(address,address,address,uint256[],uint256[])')
156
+ const coder = AbiCoder.defaultAbiCoder()
157
+ const topicAddr = (hex: string) => `0x${hex.slice(-40)}`.toLowerCase()
158
+
159
+ export interface RpcSourceOptions {
160
+ /** 每次 getLogs 的区块窗口,默认 2000(节点嫌大时自动对半缩小) */
161
+ window?: number
162
+ /** 一次最多扫多少个窗口,默认 10(凑够条数就提前停;扫不到记录时用 nextCursor 继续) */
163
+ maxWindows?: number
164
+ }
165
+
166
+ type Log = { address: string; topics: readonly string[]; data: string; blockNumber: number; transactionHash: string }
167
+ type LogNode = { getTransaction(hash: string): Promise<{ from: string; to: string | null; value: bigint; data: string } | null>; getTransactionReceipt(hash: string): Promise<{ status: number | null; gasUsed: bigint; gasPrice: bigint } | null>; getBlock(n: number): Promise<{ timestamp: number } | null> }
168
+
169
+ async function pool<T, R>(items: readonly T[], size: number, work: (item: T) => Promise<R>): Promise<R[]> {
170
+ const out: R[] = new Array(items.length)
171
+ let next = 0
172
+ await Promise.all(
173
+ Array.from({ length: Math.min(size, items.length) }, async () => {
174
+ while (next < items.length) {
175
+ const i = next++
176
+ out[i] = await work(items[i] as T)
177
+ }
178
+ }),
179
+ )
180
+ return out
181
+ }
182
+
183
+ export function rpcSource(options: RpcSourceOptions = {}): HistoryAdapter {
184
+ const window = Math.max(10, Math.floor(options.window ?? 2000))
185
+ const maxWindows = Math.max(1, Math.floor(options.maxWindows ?? 10))
186
+ return {
187
+ name: 'rpc',
188
+ supports: (chainId) => !isTronChain(chainId),
189
+ async fetch(q) {
190
+ const addr = zeroPadValue(q.address, 32)
191
+ const filters: Array<Array<string | string[] | null>> = [[TRANSFER, null, addr], [TRANSFER, addr], ...(q.nft ? [[[SINGLE, BATCH], null, addr], [[SINGLE, BATCH], null, null, addr]] : [])]
192
+ const logsIn = async (from: number, to: number, depth = 0): Promise<Log[]> => {
193
+ try {
194
+ const lists = await Promise.all(filters.map((topics) => q.provider.getLogs({ fromBlock: from, toBlock: to, topics } as never)))
195
+ // ERC-721 的 Transfer 和 ERC-20 同一个 topic(4 个 topic):不要 NFT 时丢掉
196
+ return lists.flat().filter((l) => q.nft || l.topics.length === 3) as unknown as Log[]
197
+ } catch (err) {
198
+ // 范围太大 / 结果太多:对半拆开重试(最多拆 4 层)
199
+ if (depth < 4 && to > from && /range|too (large|wide|big|many)|exceed|limit|more than|response size/i.test(errorText(err))) {
200
+ const mid = from + Math.floor((to - from) / 2)
201
+ return [...(await logsIn(from, mid, depth + 1)), ...(await logsIn(mid + 1, to, depth + 1))]
202
+ }
203
+ throw err
204
+ }
205
+ }
206
+ let to = q.endPos ?? (await q.provider.getBlockNumber())
207
+ let from = to
208
+ const logs: Log[] = []
209
+ for (let w = 0; w < maxWindows; w++) {
210
+ from = Math.max(0, to - window + 1)
211
+ logs.push(...(await logsIn(from, to)))
212
+ if (new Set(logs.map((l) => l.transactionHash)).size >= q.limit || from === 0) break
213
+ to = from - 1
214
+ }
215
+ const next = from > 0 ? from - 1 : undefined
216
+
217
+ // 只保留最新的 limit 笔交易;被丢掉的更老的交易所在的区块留给下一页
218
+ const byBlock = new Map<string, number>()
219
+ for (const l of logs) byBlock.set(l.transactionHash, Math.max(byBlock.get(l.transactionHash) ?? 0, l.blockNumber))
220
+ const hashes = [...byBlock.entries()].sort((a, b) => b[1] - a[1])
221
+ const kept = hashes.slice(0, q.limit)
222
+ const dropped = hashes.slice(q.limit)
223
+ const completeFrom = dropped.length ? (dropped[0] as [string, number])[1] + 1 : from
224
+ const keepSet = new Set(kept.map(([h]) => h))
225
+
226
+ const { runOnNodes } = await import('./write/rpc.js')
227
+ const node = <T>(method: string, fn: (n: LogNode) => Promise<T>) => runOnNodes(q.provider.rpc, method, (n) => fn(n as unknown as LogNode))
228
+ const blockTimes = new Map<number, number>()
229
+ const blocks = [...new Set(kept.map(([, b]) => b))]
230
+ await pool(blocks, 6, async (b) => {
231
+ blockTimes.set(b, (await node('getBlock', (n) => n.getBlock(b)))?.timestamp ?? 0)
232
+ })
233
+ const txs = (
234
+ await pool(kept, 6, async ([hash, block]): Promise<RawTx | null> => {
235
+ const [tx, receipt] = await Promise.all([node('getTransaction', (n) => n.getTransaction(hash)), node('getTransactionReceipt', (n) => n.getTransactionReceipt(hash))])
236
+ if (!tx) return null
237
+ return { hash, pos: block, block, time: blockTimes.get(block) ?? 0, from: tx.from, to: tx.to, value: tx.value, input: tx.data, ok: receipt?.status !== 0, ...(receipt ? { fee: receipt.gasUsed * receipt.gasPrice } : {}) }
238
+ })
239
+ ).filter((t): t is RawTx => t !== null)
240
+
241
+ const moves: RawMove[] = []
242
+ for (const l of logs) {
243
+ if (!keepSet.has(l.transactionHash)) continue
244
+ const base = { hash: l.transactionHash, pos: l.blockNumber, block: l.blockNumber, time: blockTimes.get(l.blockNumber) ?? 0, asset: l.address.toLowerCase() }
245
+ if (l.topics[0] === TRANSFER && l.topics.length === 3) moves.push({ ...base, from: topicAddr(l.topics[1] as string), to: topicAddr(l.topics[2] as string), value: big(l.data) })
246
+ else if (l.topics[0] === TRANSFER && l.topics.length === 4) moves.push({ ...base, from: topicAddr(l.topics[1] as string), to: topicAddr(l.topics[2] as string), value: 1n, tokenId: BigInt(l.topics[3] as string).toString() })
247
+ else if (l.topics[0] === SINGLE) {
248
+ const [tokenId, value] = coder.decode(['uint256', 'uint256'], l.data) as unknown as [bigint, bigint]
249
+ moves.push({ ...base, from: topicAddr(l.topics[2] as string), to: topicAddr(l.topics[3] as string), value, tokenId: tokenId.toString() })
250
+ } else if (l.topics[0] === BATCH) {
251
+ const [ids, values] = coder.decode(['uint256[]', 'uint256[]'], l.data) as unknown as [bigint[], bigint[]]
252
+ ids.forEach((tokenId, i) => moves.push({ ...base, from: topicAddr(l.topics[2] as string), to: topicAddr(l.topics[3] as string), value: values[i] as bigint, tokenId: tokenId.toString() }))
253
+ }
254
+ }
255
+ // 代币的 symbol / decimals(一次 multicall);NFT 合约没有 decimals,symbol 能读到就用
256
+ const assets = [...new Set(moves.map((m) => m.asset))]
257
+ if (assets.length) {
258
+ const details = await q.provider.tokens(assets, { fields: ['symbol', 'decimals'] }).catch(() => [])
259
+ const meta = new Map(assets.map((a, i) => [a, details[i]]))
260
+ for (const m of moves) {
261
+ const d = meta.get(m.asset)
262
+ m.symbol = d?.symbol ?? null
263
+ m.decimals = typeof d?.decimals === 'number' ? d.decimals : null
264
+ }
265
+ }
266
+ return { txs, moves, completeFrom: dropped.length ? completeFrom : from, ...(dropped.length ? { next: completeFrom - 1 } : next === undefined ? {} : { next }) }
267
+ },
268
+ }
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // TronGrid
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export interface TrongridSourceOptions {
276
+ /** 全节点地址,默认 https://api.trongrid.io */
277
+ host?: string
278
+ apiKey?: string
279
+ }
280
+
281
+ interface TronTx {
282
+ txID: string
283
+ blockNumber?: number
284
+ block_timestamp?: number
285
+ net_fee?: number
286
+ energy_fee?: number
287
+ ret?: Array<{ contractRet?: string; fee?: number }>
288
+ raw_data?: { contract?: Array<{ type?: string; parameter?: { value?: Record<string, unknown> } }> }
289
+ }
290
+
291
+ interface TronTrc20 {
292
+ transaction_id: string
293
+ block_timestamp: number
294
+ from: string
295
+ to: string
296
+ value: string
297
+ type?: string
298
+ token_info?: { symbol?: string; address?: string; decimals?: number }
299
+ }
300
+
301
+ export function trongridSource(options: TrongridSourceOptions = {}): HistoryAdapter {
302
+ const host = (options.host ?? 'https://api.trongrid.io').replace(/\/+$/, '')
303
+ return {
304
+ name: 'trongrid',
305
+ supports: isTronChain,
306
+ async fetch(q) {
307
+ const headers: Record<string, string> = options.apiKey ? { 'TRON-PRO-API-KEY': options.apiKey } : {}
308
+ const qs = `limit=${Math.min(200, q.limit)}&order_by=block_timestamp,desc&only_confirmed=true${q.endPos === undefined ? '' : `&max_timestamp=${q.endPos}`}`
309
+ const get = async <T>(path: string): Promise<T[]> => {
310
+ const json = await fetchJson<{ success?: boolean; data?: T[]; error?: string }>(q.fetch, `${host}/v1/accounts/${q.address}/${path}?${qs}`, { headers }, options.apiKey)
311
+ if (json.success === false || !Array.isArray(json.data)) throw new Error(`trongrid: ${json.error ?? 'unexpected response'}`)
312
+ return json.data
313
+ }
314
+ const [txRows, trc20Rows] = await Promise.all([get<TronTx>('transactions'), get<TronTrc20>('transactions/trc20')])
315
+ const txs: RawTx[] = txRows.map((r) => {
316
+ const c = r.raw_data?.contract?.[0]
317
+ const v = (c?.parameter?.value ?? {}) as Record<string, string | number | undefined>
318
+ const hexAddr = (a: unknown) => (typeof a === 'string' && a ? toTronAddress(a) : undefined)
319
+ const trigger = c?.type === 'TriggerSmartContract'
320
+ const ms = r.block_timestamp ?? 0
321
+ return {
322
+ hash: r.txID,
323
+ pos: ms,
324
+ ...(r.blockNumber === undefined ? {} : { block: r.blockNumber }),
325
+ time: Math.floor(ms / 1000),
326
+ from: hexAddr(v.owner_address) ?? q.address,
327
+ to: hexAddr(trigger ? v.contract_address : v.to_address) ?? null,
328
+ value: big(trigger ? v.call_value : v.amount),
329
+ input: trigger && typeof v.data === 'string' && v.data ? `0x${v.data}` : '0x',
330
+ ok: r.ret?.[0]?.contractRet === 'SUCCESS',
331
+ fee: big(r.ret?.[0]?.fee ?? (r.net_fee ?? 0) + (r.energy_fee ?? 0)),
332
+ }
333
+ })
334
+ const moves: RawMove[] = trc20Rows
335
+ .filter((r) => (r.type ?? 'Transfer') === 'Transfer')
336
+ .map((r) => ({ hash: r.transaction_id, pos: r.block_timestamp, time: Math.floor(r.block_timestamp / 1000), asset: r.token_info?.address ?? '', from: r.from, to: r.to, value: big(r.value), symbol: r.token_info?.symbol ?? null, decimals: r.token_info?.decimals ?? null }))
337
+ .filter((m) => m.asset)
338
+ const lists = [txRows.map((r) => r.block_timestamp ?? 0), trc20Rows.map((r) => r.block_timestamp)].map((ts) => ({ count: ts.length, lastPos: ts.length ? Math.min(...ts) : 0 }))
339
+ return paged(txs, moves, lists, Math.min(200, q.limit))
340
+ },
341
+ }
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /** 来源列表 → 适配器:名字按默认配置创建;没传时按链选默认(传了 apiKey 时 etherscan 排最前) */
347
+ export function historySourcesFor(chainId: number, source: HistorySource | readonly HistorySource[] | undefined, apiKey?: string): HistoryAdapter[] {
348
+ const tron = isTronChain(chainId)
349
+ const defaults: HistorySource[] = tron ? ['trongrid'] : [...(apiKey ? (['etherscan'] as const) : []), ...(BLOCKSCOUT_APIS[chainId] ? (['blockscout'] as const) : []), 'rpc']
350
+ const list = source === undefined ? defaults : Array.isArray(source) ? source : [source as HistorySource]
351
+ return (list as readonly HistorySource[])
352
+ .map((s): HistoryAdapter => {
353
+ if (typeof s !== 'string') return s
354
+ if (s === 'blockscout') return blockscoutSource()
355
+ if (s === 'etherscan') return etherscanSource({ apiKey: apiKey ?? '' })
356
+ if (s === 'rpc') return rpcSource()
357
+ return trongridSource({ ...(apiKey ? { apiKey } : {}) })
358
+ })
359
+ .filter((a) => a.supports?.(chainId) !== false)
360
+ }
package/src/http.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** HTTP 请求(代币列表、价格服务):超时、状态码检查 */
2
+
3
+ const REQUEST_TIMEOUT = 20_000
4
+
5
+ export class HttpStatusError extends Error {
6
+ constructor(
7
+ readonly status: number,
8
+ target: string,
9
+ ) {
10
+ super(`HTTP ${status} from ${target}`)
11
+ this.name = 'HttpStatusError'
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @param secret 请求里带的 Key:出错时错误信息只写域名,不写完整地址,避免 Key 出现在日志里
17
+ */
18
+ export function fetchJson<T>(fetchFn: typeof fetch, url: string, init?: RequestInit, secret?: string): Promise<T> {
19
+ return fetchFn(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT) }).then(async (res) => {
20
+ if (!res.ok) {
21
+ throw new HttpStatusError(res.status, secret ? safeHost(url) : url)
22
+ }
23
+ return (await res.json()) as T
24
+ })
25
+ }
26
+
27
+ function safeHost(url: string): string {
28
+ try {
29
+ return new URL(url).host
30
+ } catch {
31
+ return 'request'
32
+ }
33
+ }
34
+
@@ -21,7 +21,7 @@ import {
21
21
  } from './common.js'
22
22
  import type { IncreaseLiquidityParams, LiquidityPlan, LiquidityTx, NewPositionParams, PositionRef, RemoveLiquidityParams } from './types.js'
23
23
 
24
- const npmInterface = new Interface([
24
+ export const npmInterface = new Interface([
25
25
  'function WETH9() view returns (address)',
26
26
  'function factory() view returns (address)',
27
27
  'function ownerOf(uint256 tokenId) view returns (address)',
@@ -20,7 +20,7 @@ import {
20
20
  } from './common.js'
21
21
  import type { IncreaseLiquidityParams, LiquidityPlan, NewPositionParams, PositionRef, RemoveLiquidityParams } from './types.js'
22
22
 
23
- const pmInterface = new Interface([
23
+ export const pmInterface = new Interface([
24
24
  'function ownerOf(uint256 tokenId) view returns (address)',
25
25
  'function getApproved(uint256 tokenId) view returns (address)',
26
26
  'function isApprovedForAll(address owner, address operator) view returns (bool)',
@@ -37,12 +37,13 @@ const stateViewInterface = new Interface([
37
37
  ])
38
38
 
39
39
  /** v4-periphery 的 Actions */
40
- const Actions = {
40
+ export const Actions = {
41
41
  INCREASE_LIQUIDITY: 0x00,
42
42
  DECREASE_LIQUIDITY: 0x01,
43
43
  MINT_POSITION: 0x02,
44
44
  BURN_POSITION: 0x03,
45
45
  SETTLE_PAIR: 0x0d,
46
+ TAKE: 0x0e,
46
47
  TAKE_PAIR: 0x11,
47
48
  CLOSE_CURRENCY: 0x12,
48
49
  SWEEP: 0x14,
@@ -65,7 +66,7 @@ interface PoolKey {
65
66
  const keyTuple = (k: PoolKey) => [k.currency0, k.currency1, k.fee, k.tickSpacing, k.hooks]
66
67
 
67
68
  /** 依次执行的 action 列表 → modifyLiquidities 的 unlockData */
68
- class Planner {
69
+ export class Planner {
69
70
  readonly actions: number[] = []
70
71
  readonly params: string[] = []
71
72
 
package/src/lp.ts CHANGED
@@ -3,7 +3,8 @@ import type { Call } from './call.js'
3
3
  import { formatUnits } from './erc20.js'
4
4
  import { getAmountsForLiquidity, getFeeGrowthInside, getFeesEarned, getSqrtRatioAtTick, sqrtPriceToPrice, tickToPrice } from './lpMath.js'
5
5
  import { v4Positions, type LpV4Options } from './lpV4.js'
6
- import { defillamaPrices, type PriceSource, type TokenSource } from './owner.js'
6
+ import type { TokenSource } from './owner.js'
7
+ import { defillamaPrices, type PriceSource } from './price.js'
7
8
  import type { Provider } from './provider.js'
8
9
  import { TRON_CHAIN_ID, isTronAddress, isTronChain, toEvmAddress, toTronAddress } from './tron.js'
9
10
  import { withSignal } from './util.js'
@@ -466,7 +467,6 @@ async function v2Positions(provider: Provider, owner: string, protocols: LpProto
466
467
  }
467
468
 
468
469
  async function v3Positions(provider: Provider, owner: string, protocols: LpProtocol[], options: LpPositionsOptions): Promise<RawConcentrated[]> {
469
- const { includeClosed = false } = options
470
470
  const maxPositions = Math.floor(options.maxPositions ?? 500)
471
471
  const overrides = callOverrides(options)
472
472
  if (!protocols.length || !(maxPositions > 0)) {
@@ -494,7 +494,16 @@ async function v3Positions(provider: Provider, owner: string, protocols: LpProto
494
494
  }
495
495
  const tokenIds = await provider.tryAll<bigint>(indexCalls, overrides)
496
496
  const ids = tokenIds.flatMap((id, i) => (id === null ? [] : [{ protocol: indexOwners[i] as LpProtocol, tokenId: id }]))
497
+ return v3Details(provider, ids, options)
498
+ }
497
499
 
500
+ /** 已知 tokenId 的 V3 仓位:读仓位信息、池子状态、算数量和未领取的手续费(不看 NFT 现在在谁手里,如质押在挖矿合约里的仓位) */
501
+ async function v3Details(provider: Provider, ids: ReadonlyArray<{ protocol: LpProtocol; tokenId: bigint }>, options: LpPositionsOptions): Promise<RawConcentrated[]> {
502
+ const { includeClosed = false } = options
503
+ const overrides = callOverrides(options)
504
+ if (!ids.length) {
505
+ return []
506
+ }
498
507
  // 仓位详情
499
508
  type PositionData = { token0: string; token1: string; fee: bigint; tickLower: bigint; tickUpper: bigint; liquidity: bigint; feeGrowthInside0LastX128: bigint; feeGrowthInside1LastX128: bigint; tokensOwed0: bigint; tokensOwed1: bigint }
500
509
  const positions = await provider.tryAll<PositionData>(
@@ -629,6 +638,7 @@ async function finalize(provider: Provider, chainId: number, raw: RawPosition[],
629
638
  tokens: addresses.filter((a) => !info.get(a)?.native).map((a) => (tron ? toTronAddress(a) : a)),
630
639
  nativeSymbol: nativeToken ? nativeToken.symbol : null,
631
640
  fetch: fetchFn,
641
+ provider,
632
642
  })
633
643
  } catch (err) {
634
644
  // 同步抛错和 reject 都在这里接住
@@ -721,6 +731,15 @@ export function lpPositions(provider: Provider, owner: string, options: LpPositi
721
731
  return withSignal(options.signal, () => queryLpPositions(provider, owner, options))
722
732
  }
723
733
 
734
+ /**
735
+ * 按 tokenId 查 V3 仓位(不要求持有者是谁),返回与 lpPositions 相同的结果(含美元价值)。
736
+ * 给 `/rewards` 读质押在挖矿合约里的仓位用
737
+ */
738
+ export async function lpPositionsByIds(provider: Provider, ids: ReadonlyArray<{ protocol: LpProtocol; tokenId: bigint }>, options: LpPositionsOptions = {}): Promise<LpPosition[]> {
739
+ const chainId = await provider.getChainId()
740
+ return finalize(provider, chainId, await v3Details(provider, ids, options), options)
741
+ }
742
+
724
743
  async function queryLpPositions(provider: Provider, owner: string, options: LpPositionsOptions): Promise<LpPosition[]> {
725
744
  const chainId = await provider.getChainId()
726
745
  const protocols = resolveLpProtocols(chainId, options)
package/src/lpV4.ts CHANGED
@@ -94,6 +94,10 @@ export interface V4ScanProgress {
94
94
  done: boolean
95
95
  /** 节点不开放 getLogs 时暂停到这个时间(毫秒时间戳),之前的调用不再扫描 */
96
96
  pausedUntil?: number
97
+ /** 地址没有持有这个仓位合约的 NFT(balanceOf 为 0):没有扫描,done 为 true */
98
+ skipped?: true
99
+ /** 地址当前持有这个仓位合约的 NFT 个数(balanceOf;查不到时没有这个字段) */
100
+ held?: number
97
101
  }
98
102
 
99
103
  export interface LpV4Options {
@@ -113,6 +117,7 @@ const DYNAMIC_FEE_FLAG = 0x800000
113
117
 
114
118
  const POSITION_MANAGER_ABI = [
115
119
  'function ownerOf(uint256 tokenId) view returns (address)',
120
+ 'function balanceOf(address owner) view returns (uint256)',
116
121
  'function getPoolAndPositionInfo(uint256 tokenId) view returns ((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, uint256 info)',
117
122
  'function getPositionLiquidity(uint256 tokenId) view returns (uint128)',
118
123
  ]
@@ -164,6 +169,7 @@ async function scanTokenIds(
164
169
  owner: string,
165
170
  options: V4ScanOptions,
166
171
  signal: AbortSignal | undefined,
172
+ held: number | undefined,
167
173
  ): Promise<{ ids: string[]; cursor: number; prune(ids: readonly string[]): Promise<void> }> {
168
174
  const storage = options.storage ?? defaultStorage
169
175
  const key = `${chainId}:${(protocol.positionManager as string).toLowerCase()}:${owner.toLowerCase()}`
@@ -188,6 +194,7 @@ async function scanTokenIds(
188
194
  target,
189
195
  done: target !== null && state.cursor >= target,
190
196
  ...(state.pausedUntil ? { pausedUntil: state.pausedUntil } : {}),
197
+ ...(held === undefined ? {} : { held }),
191
198
  })
192
199
  } catch {
193
200
  // 回调出错不影响扫描和保存进度
@@ -284,10 +291,33 @@ export async function v4Positions(
284
291
  return []
285
292
  }
286
293
 
294
+ // 先查持有几个仓位 NFT:一个都没有就不用扫描(最常见的情况;扫描在公共节点上要很久)。查不到时照常扫描
295
+ let held: unknown[] = []
296
+ if (scanOptions) {
297
+ try {
298
+ held = await provider.tryAll(
299
+ protocols.map((p) => provider.contract(p.positionManager as string, POSITION_MANAGER_ABI).balanceOf(owner)),
300
+ overrides,
301
+ )
302
+ } catch (err) {
303
+ if (overrides.signal?.aborted) throw err
304
+ // 节点出错 / 限频:不影响查询,照常扫描
305
+ }
306
+ }
307
+ const skip = (protocol: V4Protocol) => {
308
+ try {
309
+ scanOptions?.onProgress?.({ chainId, positionManager: protocol.positionManager as string, startBlock: -1, cursor: -1, target: null, done: true, skipped: true, held: 0 })
310
+ } catch {
311
+ // 回调出错不影响查询
312
+ }
313
+ return null
314
+ }
315
+
287
316
  // 每个协议的候选 tokenId:太多时只核对最大的 maxPositions 个(tokenId 递增,越大越新)
288
317
  const candidates = await Promise.all(
289
- protocols.map(async (protocol) => {
290
- const scan = scanOptions ? await scanTokenIds(provider, chainId, protocol, owner, scanOptions, overrides.signal) : null
318
+ protocols.map(async (protocol, i) => {
319
+ const count = typeof held[i] === 'bigint' ? Number(held[i]) : undefined
320
+ const scan = !scanOptions ? null : count === 0 ? skip(protocol) : await scanTokenIds(provider, chainId, protocol, owner, scanOptions, overrides.signal, count)
291
321
  const ids = [...new Set([...explicit, ...(scan?.ids ?? [])])].map(BigInt).sort((a, b) => (a < b ? 1 : a > b ? -1 : 0))
292
322
  return { protocol, ids: ids.slice(0, limit), scan }
293
323
  }),
package/src/multicall.ts CHANGED
@@ -81,6 +81,9 @@ const MULTICALL3: Record<number, Multicall> = {
81
81
  80094: { address: CANONICAL, block: 0 }, // Berachain,创世预置
82
82
  534352: { address: CANONICAL, block: 14 }, // Scroll
83
83
  86233268: { address: CANONICAL, block: 10854614 }, // Winchain,部署区块未查到
84
+ 1030: { address: CANONICAL, block: 159584122 }, // Conflux eSpace,部署区块未查到
85
+ 1672: { address: CANONICAL, block: 20812125 }, // Pharos,部署区块未查到
86
+ 1776: { address: CANONICAL, block: 188237272 }, // Injective EVM,部署区块未查到
84
87
  // 以下两条链不支持 deployless(合约创建式 eth_call 返回空 / 格式不对),只能用 Multicall3;
85
88
  // 早于部署区块的历史查询会失败
86
89
  42262: { address: CANONICAL, block: 22380155 }, // Oasis Emerald,部署区块未查到