@w3lib/evm-batch-call 0.1.0 → 0.3.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 (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +194 -12
  3. package/dist/aggregate.d.ts +5 -2
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js.map +1 -1
  6. package/dist/call.d.ts +8 -2
  7. package/dist/call.d.ts.map +1 -1
  8. package/dist/call.js +25 -1
  9. package/dist/call.js.map +1 -1
  10. package/dist/chains.d.ts +13 -0
  11. package/dist/chains.d.ts.map +1 -0
  12. package/dist/chains.js +35 -0
  13. package/dist/chains.js.map +1 -0
  14. package/dist/detect.d.ts +23 -0
  15. package/dist/detect.d.ts.map +1 -0
  16. package/dist/detect.js +178 -0
  17. package/dist/detect.js.map +1 -0
  18. package/dist/erc20.d.ts +83 -7
  19. package/dist/erc20.d.ts.map +1 -1
  20. package/dist/erc20.js +36 -5
  21. package/dist/erc20.js.map +1 -1
  22. package/dist/fallback.d.ts +25 -0
  23. package/dist/fallback.d.ts.map +1 -1
  24. package/dist/fallback.js +123 -21
  25. package/dist/fallback.js.map +1 -1
  26. package/dist/index.d.ts +10 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/nft.d.ts +71 -0
  31. package/dist/nft.d.ts.map +1 -0
  32. package/dist/nft.js +194 -0
  33. package/dist/nft.js.map +1 -0
  34. package/dist/owner.d.ts +136 -0
  35. package/dist/owner.d.ts.map +1 -0
  36. package/dist/owner.js +501 -0
  37. package/dist/owner.js.map +1 -0
  38. package/dist/provider.d.ts +112 -12
  39. package/dist/provider.d.ts.map +1 -1
  40. package/dist/provider.js +343 -48
  41. package/dist/provider.js.map +1 -1
  42. package/dist/scan.d.ts +59 -0
  43. package/dist/scan.d.ts.map +1 -0
  44. package/dist/scan.js +245 -0
  45. package/dist/scan.js.map +1 -0
  46. package/dist/shortcuts.d.ts +113 -0
  47. package/dist/shortcuts.d.ts.map +1 -0
  48. package/dist/shortcuts.js +193 -0
  49. package/dist/shortcuts.js.map +1 -0
  50. package/dist/source.d.ts +23 -1
  51. package/dist/source.d.ts.map +1 -1
  52. package/dist/source.js +110 -3
  53. package/dist/source.js.map +1 -1
  54. package/dist/tron.d.ts +6 -0
  55. package/dist/tron.d.ts.map +1 -1
  56. package/dist/tron.js +16 -1
  57. package/dist/tron.js.map +1 -1
  58. package/dist/util.d.ts +6 -0
  59. package/dist/util.d.ts.map +1 -0
  60. package/dist/util.js +20 -0
  61. package/dist/util.js.map +1 -0
  62. package/package.json +3 -2
  63. package/src/aggregate.ts +5 -2
  64. package/src/call.ts +32 -3
  65. package/src/chains.ts +46 -0
  66. package/src/detect.ts +193 -0
  67. package/src/erc20.ts +111 -9
  68. package/src/fallback.ts +127 -23
  69. package/src/index.ts +90 -4
  70. package/src/nft.ts +298 -0
  71. package/src/owner.ts +651 -0
  72. package/src/provider.ts +458 -54
  73. package/src/scan.ts +293 -0
  74. package/src/shortcuts.ts +278 -0
  75. package/src/source.ts +118 -3
  76. package/src/tron.ts +21 -2
  77. package/src/util.ts +20 -0
package/src/provider.ts CHANGED
@@ -2,12 +2,48 @@ import { type BigNumberish } from 'ethers'
2
2
 
3
3
  import { aggregate, type AggregateContext, type BlockTag, type CallOverrides, type EthersLikeProvider } from './aggregate.js'
4
4
  import { Batcher, type BatchOptions } from './batcher.js'
5
- import { decodeCall, encodeCall, type BoundCall, type Call, type CallRequest, type FailableCall, type RawResult } from './call.js'
5
+ import { asStringOrBytes32, decodeCall, encodeCall, type BoundCall, type Call, type CallRequest, type FailableCall, type RawResult } from './call.js'
6
6
  import { Contract, bindCall, type ContractAbi, type ContractRunner } from './contract.js'
7
- import { DEFAULT_NATIVE_TOKENS, ERC20_ABI, type Erc20Contract, type TokenInfo } from './erc20.js'
7
+ import {
8
+ DEFAULT_NATIVE_TOKENS,
9
+ ERC20_ABI,
10
+ formatAmount,
11
+ getCachedTokenMeta,
12
+ setCachedTokenMeta,
13
+ type BalanceToken,
14
+ type Erc20Contract,
15
+ type TokenBalance,
16
+ type TokenAllowance,
17
+ MAX_UINT256,
18
+ UNLIMITED_ALLOWANCE_THRESHOLD,
19
+ DEFAULT_TOKEN_FIELDS,
20
+ type DefaultTokenField,
21
+ type TokenDetails,
22
+ type TokenField,
23
+ } from './erc20.js'
8
24
  import { CallFailedError, isExecutionError } from './errors.js'
9
25
  import { MULTICALL3_ADDRESS, getMulticall3, type Multicall } from './multicall.js'
10
- import { resolveSource, type ProviderSource, type SourceOptions } from './source.js'
26
+ import { getNativeCurrency } from './chains.js'
27
+ import { ownerTokens, type OwnedToken, type OwnerTokensOptions } from './owner.js'
28
+ import {
29
+ erc1155Balances,
30
+ nftBalances,
31
+ nftCollections,
32
+ nftOwners,
33
+ nftTokenUris,
34
+ type DefaultNftCollectionField,
35
+ type Erc1155Balance,
36
+ type NftBalance,
37
+ type NftCollection,
38
+ type NftCollectionField,
39
+ type NftCollectionsOptions,
40
+ type NftItem,
41
+ type NftOwner,
42
+ type NftTokenUri,
43
+ type NftTokenUriOptions,
44
+ } from './nft.js'
45
+ import { detectChainId } from './detect.js'
46
+ import { isTronChain, resolveSource, type ProviderSource, type SourceOptions } from './source.js'
11
47
 
12
48
  export type { ProviderSource }
13
49
 
@@ -40,8 +76,33 @@ export interface ProviderConfig extends SourceOptions {
40
76
  chunkSize?: number
41
77
  /** `provider.call()` 自动合并的参数 */
42
78
  batch?: BatchOptions
43
- /** `balances()` 里视为主币的地址,默认 0xeeee…eeee 和零地址 */
79
+ /** `balances()` 里视为主币的地址,默认 0xeeee…eeee(NATIVE_TOKEN)和零地址 */
44
80
  nativeTokens?: readonly string[]
81
+ /** 主币精度,默认按内置链信息表(NATIVE_CURRENCIES),表里没有时 EVM 18、Tron 6 */
82
+ nativeDecimals?: number
83
+ /** 主币 symbol(balances 的 symbol 选项、tokens 用),默认按内置链信息表,表里没有时为 null */
84
+ nativeSymbol?: string
85
+ /** 主币名称(tokens 用),默认按内置链信息表,表里没有时为 null */
86
+ nativeName?: string
87
+ }
88
+
89
+ export interface TokensOptions<F extends TokenField = DefaultTokenField> extends CallOverrides {
90
+ /** 要返回的字段,默认 ['name', 'symbol', 'decimals'] */
91
+ fields?: readonly F[]
92
+ }
93
+
94
+ export interface BalancesOptions extends CallOverrides {
95
+ /** 同时返回 symbol(代币查一次后缓存;主币取 nativeSymbol / 内置链信息表)。默认 false */
96
+ symbol?: boolean
97
+ }
98
+
99
+ /** 构造参数第一个是 chainId 还是节点:数字、数字字符串、0x 十六进制视为 chainId,其余(URL、对象、数组)视为节点 */
100
+ function isChainIdArg(value: unknown): value is number | bigint | string {
101
+ return (
102
+ typeof value === 'number' ||
103
+ typeof value === 'bigint' ||
104
+ (typeof value === 'string' && /^\s*(\d+|0x[0-9a-fA-F]+)\s*$/.test(value))
105
+ )
45
106
  }
46
107
 
47
108
  const ETH_BALANCE_INPUTS = [{ name: 'addr', type: 'address' }] as const
@@ -56,26 +117,123 @@ const ETH_BALANCE_OUTPUTS = [{ name: 'balance', type: 'uint256' }] as const
56
117
  * ```
57
118
  */
58
119
  export class Provider implements ContractRunner {
59
- readonly #ctx: AggregateContext
120
+ /** 不传 chainId 时在识别完成前为 null */
121
+ #ctx: AggregateContext | null = null
122
+ /** 识别中的 Promise;识别失败后置空,下次使用时重试 */
123
+ #detecting: Promise<AggregateContext> | null = null
124
+ readonly #source: ProviderSource | readonly ProviderSource[] | undefined
60
125
  readonly #batcher: Batcher
61
126
  readonly #nativeTokens: Set<string>
127
+ readonly #config: ProviderConfig
62
128
 
129
+ /**
130
+ * 两种写法:
131
+ *
132
+ * ```ts
133
+ * new Provider(window.ethereum) // 只传节点:chainId 从节点识别
134
+ * new Provider([url1, url2], config)
135
+ * new Provider(56) // 只传 chainId:使用内置公共节点
136
+ * new Provider(56, rpc, config) // 都传(与 ethcall 相同),不需要识别
137
+ * ```
138
+ *
139
+ * @param provider 节点:RPC URL、ethers Provider、钱包(window.ethereum / tronWeb)或它们的数组(主节点 + 备用节点)
140
+ * @param config 可选配置
141
+ */
142
+ constructor(provider: ProviderSource | readonly ProviderSource[], config?: ProviderConfig)
63
143
  /**
64
144
  * @param chainId 链 ID(接受数字字符串)
65
- * @param provider 节点:RPC URL、ethers Provider、钱包(window.ethereum / tronWeb)或它们的数组(主节点 + 备用节点)。
66
- * 不传则使用内置的公共节点表
145
+ * @param provider 节点,不传则使用内置的公共节点表
67
146
  * @param config 可选配置
68
147
  */
69
- constructor(chainId: number | string, provider?: ProviderSource | readonly ProviderSource[], config: ProviderConfig = {}) {
70
- const id = Number(chainId)
71
- this.#ctx = {
72
- provider: resolveSource(id, provider, config),
73
- chainId: id,
74
- multicall: config.deployless ? null : resolveMulticall(id, config.multicall),
75
- chunkSize: config.chunkSize ?? 500,
76
- }
148
+ constructor(chainId: number | string, provider?: ProviderSource | readonly ProviderSource[], config?: ProviderConfig)
149
+ constructor(
150
+ first: number | string | ProviderSource | readonly ProviderSource[],
151
+ second?: ProviderSource | readonly ProviderSource[] | ProviderConfig,
152
+ third?: ProviderConfig,
153
+ ) {
154
+ const hasChainId = isChainIdArg(first)
155
+ const source = (hasChainId ? second : first) as ProviderSource | readonly ProviderSource[] | undefined
156
+ const config = ((hasChainId ? third : second) ?? {}) as ProviderConfig
157
+ this.#config = config
77
158
  this.#batcher = new Batcher((requests, overrides) => this.#aggregate(requests, overrides), config.batch)
78
159
  this.#nativeTokens = new Set((config.nativeTokens ?? DEFAULT_NATIVE_TOKENS).map((t) => t.toLowerCase()))
160
+
161
+ this.#source = source
162
+ if (hasChainId) {
163
+ this.#ctx = this.#createContext(Number(first), source)
164
+ } else {
165
+ if (source === undefined) {
166
+ throw new Error('Provider requires a chainId or a provider')
167
+ }
168
+ // 提前开始识别;失败时由使用方的调用拿到错误(这里避免未处理的 rejection)
169
+ this.#ensureReady().catch(() => {})
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 只传节点时等 chainId 识别完成后再返回实例,之后 rpc / multicall 等同步属性可以直接用。
175
+ *
176
+ * ```ts
177
+ * const multi = await Provider.create(window.ethereum)
178
+ * multi.rpc // 已就绪
179
+ * ```
180
+ */
181
+ static async create(
182
+ provider: ProviderSource | readonly ProviderSource[],
183
+ config?: ProviderConfig,
184
+ ): Promise<Provider> {
185
+ return new Provider(provider, config).ready()
186
+ }
187
+
188
+ /** 取初始化好的上下文;识别失败不会被永久记住,下次调用重新识别(如节点短暂限流) */
189
+ #ensureReady(): Promise<AggregateContext> {
190
+ if (this.#ctx) {
191
+ return Promise.resolve(this.#ctx)
192
+ }
193
+ if (!this.#detecting) {
194
+ const source = this.#source as ProviderSource | readonly ProviderSource[]
195
+ const detecting = detectChainId(source, this.#config.fallback?.timeout).then((id) => {
196
+ this.#ctx = this.#createContext(id, source)
197
+ return this.#ctx
198
+ })
199
+ this.#detecting = detecting
200
+ detecting.catch(() => {
201
+ if (this.#detecting === detecting) {
202
+ this.#detecting = null
203
+ }
204
+ })
205
+ }
206
+ return this.#detecting
207
+ }
208
+
209
+ #createContext(chainId: number, source: ProviderSource | readonly ProviderSource[] | undefined): AggregateContext {
210
+ const config = this.#config
211
+ return {
212
+ provider: resolveSource(chainId, source, config),
213
+ chainId,
214
+ multicall: config.deployless ? null : resolveMulticall(chainId, config.multicall),
215
+ chunkSize: config.chunkSize ?? 500,
216
+ }
217
+ }
218
+
219
+ /** 链 ID(不传 chainId 构造时,会等待从节点识别完成) */
220
+ async getChainId(): Promise<number> {
221
+ return (await this.#ensureReady()).chainId
222
+ }
223
+
224
+ /** 等待初始化完成(只传节点时会先识别 chainId;识别失败在这里抛出) */
225
+ async ready(): Promise<this> {
226
+ await this.#ensureReady()
227
+ return this
228
+ }
229
+
230
+ #nativeCurrency(chainId: number): { decimals: number; symbol: string | null; name: string | null } {
231
+ const known = getNativeCurrency(chainId)
232
+ return {
233
+ decimals: this.#config.nativeDecimals ?? known?.decimals ?? (isTronChain(chainId) ? 6 : 18),
234
+ symbol: this.#config.nativeSymbol ?? known?.symbol ?? null,
235
+ name: this.#config.nativeName ?? known?.name ?? null,
236
+ }
79
237
  }
80
238
 
81
239
  /**
@@ -97,45 +255,257 @@ export class Provider implements ContractRunner {
97
255
  }
98
256
 
99
257
  /**
100
- * 批量查余额,主币和代币混在一起、一次请求。主币地址见 config.nativeTokens。
101
- * 查询失败(非合约地址、非法地址等)的位置为 0n;需要区分失败请用 tryAll。
258
+ * 批量查余额(含按 decimals 换算后的数值),主币和代币混在一起、一次请求。
259
+ *
260
+ * ```ts
261
+ * const list = await multi.balances(user, [NATIVE_TOKEN, USDT, { address: USDC, decimals: 6 }], { symbol: true })
262
+ * list[1] // { token: USDT, native: false, balance: '1234500000000000000000', decimals: 18, formatted: '1234.5', symbol: 'USDT', success: true }
263
+ * ```
264
+ *
265
+ * - 主币地址见 config.nativeTokens(默认 0xeeee…eeee 和零地址);精度 / symbol 按内置链信息表,可用 nativeDecimals / nativeSymbol 覆盖
266
+ * - decimals、symbol 查到一次后缓存,之后只查 balanceOf;也可以直接传 `{ address, decimals }`
267
+ * - 单个代币失败(非合约地址、非法地址等)不影响其他代币,该项 success 为 false
102
268
  */
103
- async balances(owner: string, tokens: readonly string[], overrides?: CallOverrides): Promise<bigint[]> {
104
- const calls = tokens.map((token) =>
105
- this.#nativeTokens.has(token.toLowerCase()) ? this.getEthBalance(owner) : this.erc20(token).balanceOf(owner),
106
- )
107
- const results = await this.tryAll<bigint>(calls, overrides)
108
- return results.map((balance) => balance ?? 0n)
269
+ async balances(owner: string, tokens: readonly BalanceToken[], options: BalancesOptions = {}): Promise<TokenBalance[]> {
270
+ const { symbol: withSymbol = false, ...overrides } = options
271
+ const { chainId } = await this.#ensureReady()
272
+ const nativeCurrency = this.#nativeCurrency(chainId)
273
+ const items = tokens.map((token) => {
274
+ const resolved = this.#resolveToken(chainId, token, nativeCurrency.decimals)
275
+ const symbol = resolved.native ? nativeCurrency.symbol : getCachedTokenMeta(chainId, resolved.address).symbol
276
+ return { ...resolved, symbol }
277
+ })
278
+
279
+ const calls: Call[] = []
280
+ const plan = items.map(({ address, native, decimals, symbol }) => {
281
+ const balanceIndex = calls.push(native ? this.getEthBalance(owner) : this.erc20(address).balanceOf(owner)) - 1
282
+ const decimalsIndex = decimals === undefined ? calls.push(this.erc20(address).decimals()) - 1 : -1
283
+ const symbolIndex = withSymbol && !native && symbol === undefined ? calls.push(this.#symbolCall(address)) - 1 : -1
284
+ return { balanceIndex, decimalsIndex, symbolIndex }
285
+ })
286
+ const results = await this.tryAll(calls, overrides)
287
+
288
+ return items.map((item, i) => {
289
+ const { balanceIndex, decimalsIndex, symbolIndex } = plan[i] as { balanceIndex: number; decimalsIndex: number; symbolIndex: number }
290
+ const balance = results[balanceIndex] as bigint | null
291
+ const fetchedDecimals = decimalsIndex === -1 ? null : (results[decimalsIndex] as bigint | null)
292
+ const fetchedSymbol = symbolIndex === -1 ? null : (results[symbolIndex] as string | null)
293
+ if (!item.native && (fetchedDecimals !== null || fetchedSymbol !== null)) {
294
+ setCachedTokenMeta(chainId, item.address, {
295
+ ...(fetchedDecimals !== null ? { decimals: Number(fetchedDecimals) } : {}),
296
+ ...(fetchedSymbol !== null ? { symbol: fetchedSymbol } : {}),
297
+ })
298
+ }
299
+ const decimals = item.decimals ?? (fetchedDecimals === null ? null : Number(fetchedDecimals))
300
+ const extra = withSymbol ? { symbol: item.symbol ?? fetchedSymbol ?? null } : {}
301
+ if (balance === null || decimals === null) {
302
+ return { token: item.address, native: item.native, balance: '0', decimals: decimals ?? 0, formatted: '0', ...extra, success: false }
303
+ }
304
+ // 结果全是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify
305
+ return {
306
+ token: item.address,
307
+ native: item.native,
308
+ balance: balance.toString(),
309
+ decimals,
310
+ formatted: formatAmount(balance, decimals),
311
+ ...extra,
312
+ success: true,
313
+ }
314
+ })
109
315
  }
110
316
 
111
317
  /**
112
- * 批量查代币信息(symbol / name / decimals),一次请求。
113
- * decimals 读不到(非代币合约、主币占位地址等)的位置为 null。
318
+ * 批量查 ERC20 授权额度(发交易前判断是否需要 approve),一次请求。
319
+ *
320
+ * ```ts
321
+ * const [usdt] = await multi.allowances(user, router, [USDT])
322
+ * usdt // { token: USDT, spender: router, native: false, allowance: '…', decimals: 18, formatted: '100', unlimited: false, success: true }
323
+ * ```
324
+ *
325
+ * - 主币不需要授权:native 为 true,额度视为 MaxUint256、unlimited 为 true,不发请求
326
+ * - decimals 与 balances / tokens 共用缓存;也可以直接传 `{ address, decimals }`
114
327
  */
115
- async tokenInfo(tokens: readonly string[], overrides?: CallOverrides): Promise<(TokenInfo | null)[]> {
116
- // 主币占位地址不是合约,不发请求
117
- const targets = tokens.filter((token) => !this.#nativeTokens.has(token.toLowerCase()))
118
- const calls = targets.flatMap((token) => {
119
- const erc20 = this.erc20(token)
120
- return [erc20.symbol(), erc20.name(), erc20.decimals()]
328
+ async allowances(
329
+ owner: string,
330
+ spender: string,
331
+ tokens: readonly BalanceToken[],
332
+ overrides?: CallOverrides,
333
+ ): Promise<TokenAllowance[]> {
334
+ const { chainId } = await this.#ensureReady()
335
+ const nativeDecimals = this.#nativeCurrency(chainId).decimals
336
+ const calls: Call[] = []
337
+ const plan = tokens.map((token) => {
338
+ const { address, native, decimals } = this.#resolveToken(chainId, token, nativeDecimals)
339
+ const allowanceIndex = native ? -1 : calls.push(this.erc20(address).allowance(owner, spender)) - 1
340
+ const decimalsIndex = native || decimals !== undefined ? -1 : calls.push(this.erc20(address).decimals()) - 1
341
+ return { address, native, decimals, allowanceIndex, decimalsIndex }
121
342
  })
122
343
  const results = calls.length ? await this.tryAll(calls, overrides) : []
123
- const infoByToken = new Map<string, TokenInfo | null>()
124
- targets.forEach((address, i) => {
125
- const decimals = results[i * 3 + 2] as bigint | null
126
- infoByToken.set(
127
- address,
128
- decimals === null
129
- ? null
130
- : {
131
- address,
132
- symbol: (results[i * 3] as string | null) ?? null,
133
- name: (results[i * 3 + 1] as string | null) ?? null,
134
- decimals: Number(decimals),
135
- },
136
- )
344
+
345
+ return plan.map(({ address, native, decimals: known, allowanceIndex, decimalsIndex }) => {
346
+ const allowance = native ? MAX_UINT256 : ((results[allowanceIndex] as bigint | null) ?? null)
347
+ const fetched = decimalsIndex === -1 ? null : ((results[decimalsIndex] as bigint | null) ?? null)
348
+ if (fetched !== null) {
349
+ setCachedTokenMeta(chainId, address, { decimals: Number(fetched) })
350
+ }
351
+ const decimals = known ?? (fetched === null ? null : Number(fetched))
352
+ if (allowance === null || decimals === null) {
353
+ return { token: address, spender, native, allowance: '0', decimals: decimals ?? 0, formatted: '0', unlimited: false, success: false }
354
+ }
355
+ return {
356
+ token: address,
357
+ spender,
358
+ native,
359
+ allowance: allowance.toString(),
360
+ decimals,
361
+ formatted: formatAmount(allowance, decimals),
362
+ unlimited: allowance >= UNLIMITED_ALLOWANCE_THRESHOLD,
363
+ success: true,
364
+ }
365
+ })
366
+ }
367
+
368
+ /**
369
+ * 列出持有人拥有的代币(资产列表):从代币来源拿候选代币,再用 multicall 在链上核对余额。
370
+ * 默认来源是公开代币列表,**只能发现列表里的代币**,局限性见 README「资产列表」一节。
371
+ *
372
+ * @deprecated 不推荐使用:EVM 链上无法只靠节点可靠地列出地址持有的全部代币,这个方法只是尽力而为——
373
+ * 默认来源只能发现公开列表里的代币,依赖第三方免费服务(可能限流、改格式或停止服务),要查几千个代币的余额、耗时数秒,
374
+ * 价格也不一定可信;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束。
375
+ * 已知要查哪些代币时请用 `getBalances`;需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。局限性详见 README「资产列表」。
376
+ */
377
+ ownerTokens(owner: string, options?: OwnerTokensOptions): Promise<OwnedToken[]> {
378
+ return ownerTokens(this, owner, options)
379
+ }
380
+
381
+ /** 批量查 NFT 集合信息(标准 / name / symbol / totalSupply),字段可选 */
382
+ nftCollections<const F extends NftCollectionField = DefaultNftCollectionField>(
383
+ collections: readonly string[],
384
+ options?: NftCollectionsOptions<F>,
385
+ ): Promise<NftCollection<F>[]> {
386
+ return nftCollections<F>(this, collections, options)
387
+ }
388
+
389
+ /** 批量查 ERC721 持有数量(balanceOf) */
390
+ nftBalances(owner: string, collections: readonly string[], overrides?: CallOverrides): Promise<NftBalance[]> {
391
+ return nftBalances(this, owner, collections, overrides)
392
+ }
393
+
394
+ /** 批量查 ERC721 持有人(ownerOf),可混合多个集合 */
395
+ nftOwners(items: readonly NftItem[], overrides?: CallOverrides): Promise<NftOwner[]> {
396
+ return nftOwners(this, items, overrides)
397
+ }
398
+
399
+ /** 批量查 NFT 元数据地址:兼容 ERC721 tokenURI 与 ERC1155 uri({id} 按规范替换),可转换 ipfs:// */
400
+ nftTokenUris(items: readonly NftItem[], options?: NftTokenUriOptions): Promise<NftTokenUri[]> {
401
+ return nftTokenUris(this, items, options)
402
+ }
403
+
404
+ /** 批量查 ERC1155 余额 */
405
+ erc1155Balances(owner: string, items: readonly NftItem[], overrides?: CallOverrides): Promise<Erc1155Balance[]> {
406
+ return erc1155Balances(this, owner, items, overrides)
407
+ }
408
+
409
+ /** symbol() 调用:string 解码失败时按 bytes32 解析(MKR 等老代币) */
410
+ #symbolCall(address: string): Call<string> {
411
+ return asStringOrBytes32(this.erc20(address).symbol())
412
+ }
413
+
414
+ /** 解析 balances / allowances 的代币参数:是否主币、已知的 decimals(传入的 > 主币配置 / 缓存) */
415
+ #resolveToken(chainId: number, token: BalanceToken, nativeDecimals: number): { address: string; native: boolean; decimals: number | undefined } {
416
+ const address = typeof token === 'string' ? token : token.address
417
+ const native = this.#nativeTokens.has(address.toLowerCase())
418
+ const passed = typeof token === 'string' ? undefined : token.decimals
419
+ const decimals = passed ?? (native ? nativeDecimals : getCachedTokenMeta(chainId, address).decimals)
420
+ return { address, native, decimals }
421
+ }
422
+
423
+ /**
424
+ * 批量查 ERC20 代币详情,一次请求;可以选择返回哪些字段。
425
+ *
426
+ * ```ts
427
+ * await multi.tokens([USDT, NATIVE_TOKEN]) // name / symbol / decimals
428
+ * await multi.tokens([USDT], { fields: ['symbol', 'decimals', 'totalSupply'] })
429
+ * // [{ address: USDT, native: false, symbol: 'USDT', decimals: 18, totalSupply: '…', totalSupplyFormatted: '…', success: true }]
430
+ * ```
431
+ *
432
+ * - name / symbol / decimals 查到一次后缓存(与 balances 共用),totalSupply 每次都查
433
+ * - 主币占位地址不发请求,信息取内置链信息表(可用 nativeName / nativeSymbol / nativeDecimals 覆盖),totalSupply 为 null
434
+ * - symbol / name 兼容返回 bytes32 的老代币(MKR 等)
435
+ * - 读取失败的字段为 null;请求的字段都读到时 success 为 true
436
+ */
437
+ async tokens<const F extends TokenField = DefaultTokenField>(
438
+ tokens: readonly string[],
439
+ options: TokensOptions<F> = {},
440
+ ): Promise<TokenDetails<F>[]> {
441
+ const { fields = DEFAULT_TOKEN_FIELDS as unknown as readonly F[], ...overrides } = options
442
+ const wanted = new Set<TokenField>(fields)
443
+ const needDecimals = wanted.has('decimals') || wanted.has('totalSupply')
444
+ const { chainId } = await this.#ensureReady()
445
+ const nativeCurrency = this.#nativeCurrency(chainId)
446
+
447
+ const calls: Call[] = []
448
+ const plan = tokens.map((address) => {
449
+ const native = this.#nativeTokens.has(address.toLowerCase())
450
+ const meta = native
451
+ ? { name: nativeCurrency.name ?? undefined, symbol: nativeCurrency.symbol ?? undefined, decimals: nativeCurrency.decimals }
452
+ : getCachedTokenMeta(chainId, address)
453
+ const index: Partial<Record<TokenField, number>> = {}
454
+ if (!native) {
455
+ const erc20 = this.erc20(address)
456
+ if (wanted.has('name') && meta.name === undefined) {
457
+ index.name = calls.push(asStringOrBytes32(erc20.name())) - 1
458
+ }
459
+ if (wanted.has('symbol') && meta.symbol === undefined) {
460
+ index.symbol = calls.push(this.#symbolCall(address)) - 1
461
+ }
462
+ if (needDecimals && meta.decimals === undefined) {
463
+ index.decimals = calls.push(erc20.decimals()) - 1
464
+ }
465
+ if (wanted.has('totalSupply')) {
466
+ index.totalSupply = calls.push(erc20.totalSupply()) - 1
467
+ }
468
+ }
469
+ return { address, native, meta, index }
470
+ })
471
+ const results = calls.length ? await this.tryAll(calls, overrides) : []
472
+ const read = <T>(i: number | undefined): T | null => (i === undefined ? null : ((results[i] as T | null) ?? null))
473
+
474
+ return plan.map(({ address, native, meta, index }) => {
475
+ const name = meta.name ?? read<string>(index.name)
476
+ const symbol = meta.symbol ?? read<string>(index.symbol)
477
+ const fetchedDecimals = read<bigint>(index.decimals)
478
+ const decimals = meta.decimals ?? (fetchedDecimals === null ? null : Number(fetchedDecimals))
479
+ const totalSupply = read<bigint>(index.totalSupply)
480
+ if (!native) {
481
+ setCachedTokenMeta(chainId, address, {
482
+ ...(index.name !== undefined && name !== null ? { name } : {}),
483
+ ...(index.symbol !== undefined && symbol !== null ? { symbol } : {}),
484
+ ...(fetchedDecimals !== null ? { decimals: Number(fetchedDecimals) } : {}),
485
+ })
486
+ }
487
+
488
+ const values: Record<TokenField, unknown> = {
489
+ name,
490
+ symbol,
491
+ decimals,
492
+ totalSupply: totalSupply === null ? null : totalSupply.toString(),
493
+ }
494
+ const out: Record<string, unknown> = { address, native }
495
+ let success = true
496
+ for (const field of fields) {
497
+ out[field] = values[field]
498
+ // 主币没有 totalSupply,不算失败
499
+ if (values[field] === null && !(native && field === 'totalSupply')) {
500
+ success = false
501
+ }
502
+ }
503
+ if (wanted.has('totalSupply')) {
504
+ out.totalSupplyFormatted = totalSupply === null || decimals === null ? null : formatAmount(totalSupply, decimals)
505
+ }
506
+ out.success = success
507
+ return out as TokenDetails<F>
137
508
  })
138
- return tokens.map((token) => infoByToken.get(token) ?? null)
139
509
  }
140
510
 
141
511
  /**
@@ -144,7 +514,8 @@ export class Provider implements ContractRunner {
144
514
  */
145
515
  getEthBalance(address: string): BoundCall<bigint> {
146
516
  const call: Call<bigint> = {
147
- contract: { address: this.#ctx.multicall?.address ?? MULTICALL3_ADDRESS },
517
+ // 地址只用于展示:执行时合约模式会换成实际的 multicall 地址,deployless 时由合约内部处理
518
+ contract: { address: this.#ctx?.multicall?.address ?? MULTICALL3_ADDRESS },
148
519
  name: 'getEthBalance',
149
520
  inputs: ETH_BALANCE_INPUTS,
150
521
  outputs: ETH_BALANCE_OUTPUTS,
@@ -258,7 +629,8 @@ export class Provider implements ContractRunner {
258
629
  const merged: StaticCallOverrides = merge(merge({ ...shared }, call.overrides), explicit)
259
630
  if (call.kind === 'ethBalance') {
260
631
  try {
261
- return { success: true, data: (await this.#ctx.provider.getBalance(call.params[0], merged.blockTag)) as T }
632
+ const { provider } = await this.#ensureReady()
633
+ return { success: true, data: (await provider.getBalance(call.params[0], merged.blockTag)) as T }
262
634
  } catch (err) {
263
635
  return { success: false, error: err instanceof Error ? err : new Error(String(err)) }
264
636
  }
@@ -266,7 +638,8 @@ export class Provider implements ContractRunner {
266
638
  let returnData: string
267
639
  try {
268
640
  const request = encodeCall(call, false)
269
- returnData = await this.#ctx.provider.call({
641
+ const { provider } = await this.#ensureReady()
642
+ returnData = await provider.call({
270
643
  to: request.target,
271
644
  data: request.callData,
272
645
  from: merged.from,
@@ -289,18 +662,49 @@ export class Provider implements ContractRunner {
289
662
  }
290
663
  }
291
664
 
292
- /** 底层连接(多节点时是 FallbackRpc) */
665
+ /** 当前区块号(底层节点需支持 getBlockNumber) */
666
+ async getBlockNumber(): Promise<number> {
667
+ const { provider } = await this.#ensureReady()
668
+ if (!provider.getBlockNumber) {
669
+ throw new Error('getBlockNumber is not supported by this provider')
670
+ }
671
+ return provider.getBlockNumber()
672
+ }
673
+
674
+ /** eth_getLogs(底层节点需支持 getLogs)。多节点时依次尝试,全部失败抛 GetLogsError */
675
+ async getLogs(filter: Parameters<NonNullable<EthersLikeProvider['getLogs']>>[0]): Promise<Awaited<ReturnType<NonNullable<EthersLikeProvider['getLogs']>>>> {
676
+ const { provider } = await this.#ensureReady()
677
+ if (!provider.getLogs) {
678
+ throw new Error('getLogs is not supported by this provider')
679
+ }
680
+ return provider.getLogs(filter)
681
+ }
682
+
683
+ /** 是否是按主币处理的地址(config.nativeTokens,默认 0xeeee…eeee 和零地址) */
684
+ isNativeToken(address: string): boolean {
685
+ return this.#nativeTokens.has(address.toLowerCase())
686
+ }
687
+
688
+ /** 按主币处理的地址(小写),顺序同 config.nativeTokens */
689
+ get nativeTokens(): readonly string[] {
690
+ return [...this.#nativeTokens]
691
+ }
692
+
693
+ /** 底层连接(多节点时是 FallbackRpc)。只传节点构造时,需在 ready() 之后读取 */
293
694
  get rpc(): EthersLikeProvider {
695
+ if (!this.#ctx) {
696
+ throw new Error('Provider is still detecting chainId; await provider.ready() first')
697
+ }
294
698
  return this.#ctx.provider
295
699
  }
296
700
 
297
- /** 当前使用的 multicall 合约(null 表示 deployless) */
701
+ /** 当前使用的 multicall 合约(null 表示 deployless;只传节点构造时在 ready() 之前也为 null) */
298
702
  get multicall(): Multicall | null {
299
- return this.#ctx.multicall
703
+ return this.#ctx?.multicall ?? null
300
704
  }
301
705
 
302
- #aggregate(requests: CallRequest[], overrides?: CallOverrides): Promise<RawResult[]> {
303
- return aggregate(this.#ctx, requests, overrides)
706
+ async #aggregate(requests: CallRequest[], overrides?: CallOverrides): Promise<RawResult[]> {
707
+ return aggregate(await this.#ensureReady(), requests, overrides)
304
708
  }
305
709
  }
306
710