@w3lib/evm-batch-call 0.2.0 → 0.4.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 (108) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +184 -30
  3. package/contracts/DeploylessMulticall3.sol +5 -0
  4. package/dist/aggregate.d.ts +25 -3
  5. package/dist/aggregate.d.ts.map +1 -1
  6. package/dist/aggregate.js +143 -5
  7. package/dist/aggregate.js.map +1 -1
  8. package/dist/batcher.d.ts +6 -1
  9. package/dist/batcher.d.ts.map +1 -1
  10. package/dist/batcher.js +59 -9
  11. package/dist/batcher.js.map +1 -1
  12. package/dist/call.d.ts +27 -2
  13. package/dist/call.d.ts.map +1 -1
  14. package/dist/call.js +22 -0
  15. package/dist/call.js.map +1 -1
  16. package/dist/chains.d.ts +7 -0
  17. package/dist/chains.d.ts.map +1 -1
  18. package/dist/chains.js +11 -0
  19. package/dist/chains.js.map +1 -1
  20. package/dist/deployless.d.ts +1 -1
  21. package/dist/deployless.d.ts.map +1 -1
  22. package/dist/deployless.js +1 -1
  23. package/dist/deployless.js.map +1 -1
  24. package/dist/erc20.d.ts +70 -4
  25. package/dist/erc20.d.ts.map +1 -1
  26. package/dist/erc20.js +121 -5
  27. package/dist/erc20.js.map +1 -1
  28. package/dist/events.d.ts +25 -0
  29. package/dist/events.d.ts.map +1 -0
  30. package/dist/events.js +25 -0
  31. package/dist/events.js.map +1 -0
  32. package/dist/fallback.d.ts +54 -3
  33. package/dist/fallback.d.ts.map +1 -1
  34. package/dist/fallback.js +273 -21
  35. package/dist/fallback.js.map +1 -1
  36. package/dist/index.d.ts +10 -8
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/multicall.d.ts.map +1 -1
  41. package/dist/multicall.js +33 -2
  42. package/dist/multicall.js.map +1 -1
  43. package/dist/nft.d.ts +5 -0
  44. package/dist/nft.d.ts.map +1 -1
  45. package/dist/nft.js +5 -0
  46. package/dist/nft.js.map +1 -1
  47. package/dist/owner.d.ts +145 -0
  48. package/dist/owner.d.ts.map +1 -0
  49. package/dist/owner.js +510 -0
  50. package/dist/owner.js.map +1 -0
  51. package/dist/provider.d.ts +63 -18
  52. package/dist/provider.d.ts.map +1 -1
  53. package/dist/provider.js +166 -100
  54. package/dist/provider.js.map +1 -1
  55. package/dist/scan.d.ts +59 -0
  56. package/dist/scan.d.ts.map +1 -0
  57. package/dist/scan.js +245 -0
  58. package/dist/scan.js.map +1 -0
  59. package/dist/shortcuts.d.ts +34 -35
  60. package/dist/shortcuts.d.ts.map +1 -1
  61. package/dist/shortcuts.js +41 -73
  62. package/dist/shortcuts.js.map +1 -1
  63. package/dist/source.d.ts +6 -2
  64. package/dist/source.d.ts.map +1 -1
  65. package/dist/source.js +49 -6
  66. package/dist/source.js.map +1 -1
  67. package/dist/subpaths/nft.d.ts +46 -0
  68. package/dist/subpaths/nft.d.ts.map +1 -0
  69. package/dist/subpaths/nft.js +55 -0
  70. package/dist/subpaths/nft.js.map +1 -0
  71. package/dist/subpaths/owner.d.ts +33 -0
  72. package/dist/subpaths/owner.d.ts.map +1 -0
  73. package/dist/subpaths/owner.js +34 -0
  74. package/dist/subpaths/owner.js.map +1 -0
  75. package/dist/tron.d.ts +2 -0
  76. package/dist/tron.d.ts.map +1 -1
  77. package/dist/tron.js +5 -0
  78. package/dist/tron.js.map +1 -1
  79. package/dist/util.d.ts +5 -0
  80. package/dist/util.d.ts.map +1 -1
  81. package/dist/util.js +23 -0
  82. package/dist/util.js.map +1 -1
  83. package/dist/watch.d.ts +25 -0
  84. package/dist/watch.d.ts.map +1 -0
  85. package/dist/watch.js +105 -0
  86. package/dist/watch.js.map +1 -0
  87. package/package.json +19 -10
  88. package/src/aggregate.ts +177 -9
  89. package/src/batcher.ts +69 -9
  90. package/src/call.ts +42 -2
  91. package/src/chains.ts +15 -0
  92. package/src/deployless.ts +1 -1
  93. package/src/erc20.ts +171 -8
  94. package/src/events.ts +42 -0
  95. package/src/fallback.ts +306 -22
  96. package/src/index.ts +20 -27
  97. package/src/multicall.ts +34 -2
  98. package/src/nft.ts +5 -0
  99. package/src/owner.ts +660 -0
  100. package/src/provider.ts +255 -112
  101. package/src/scan.ts +293 -0
  102. package/src/shortcuts.ts +76 -88
  103. package/src/source.ts +51 -8
  104. package/src/subpaths/nft.ts +107 -0
  105. package/src/subpaths/owner.ts +69 -0
  106. package/src/tron.ts +7 -0
  107. package/src/util.ts +27 -0
  108. package/src/watch.ts +149 -0
package/src/provider.ts CHANGED
@@ -1,17 +1,31 @@
1
1
  import { type BigNumberish } from 'ethers'
2
2
 
3
- import { aggregate, type AggregateContext, type BlockTag, type CallOverrides, type EthersLikeProvider } from './aggregate.js'
3
+ import { aggregate, type AggregateContext, type BlockTag, type CallOverrides, type EthersLikeProvider, type NativeBalanceMode } from './aggregate.js'
4
4
  import { Batcher, type BatchOptions } from './batcher.js'
5
- import { asStringOrBytes32, decodeCall, encodeCall, type BoundCall, type Call, type CallRequest, type FailableCall, type RawResult } from './call.js'
5
+ import {
6
+ asStringOrBytes32,
7
+ decodeCall,
8
+ encodeCall,
9
+ settleResult,
10
+ type BoundCall,
11
+ type Call,
12
+ type CallRequest,
13
+ type FailableCall,
14
+ type FailureReason,
15
+ type RawResult,
16
+ type Settled,
17
+ } from './call.js'
6
18
  import { Contract, bindCall, type ContractAbi, type ContractRunner } from './contract.js'
7
19
  import {
8
20
  DEFAULT_NATIVE_TOKENS,
9
21
  ERC20_ABI,
10
- formatAmount,
22
+ formatUnits,
11
23
  getCachedTokenMeta,
12
24
  setCachedTokenMeta,
13
25
  type BalanceToken,
14
26
  type Erc20Contract,
27
+ type RawTokenAllowance,
28
+ type RawTokenBalance,
15
29
  type TokenBalance,
16
30
  type TokenAllowance,
17
31
  MAX_UINT256,
@@ -23,26 +37,10 @@ import {
23
37
  } from './erc20.js'
24
38
  import { CallFailedError, isExecutionError } from './errors.js'
25
39
  import { MULTICALL3_ADDRESS, getMulticall3, type Multicall } from './multicall.js'
26
- import { getNativeCurrency } from './chains.js'
27
- import {
28
- erc1155Balances,
29
- nftBalances,
30
- nftCollections,
31
- nftOwners,
32
- nftTokenUris,
33
- type DefaultNftCollectionField,
34
- type Erc1155Balance,
35
- type NftBalance,
36
- type NftCollection,
37
- type NftCollectionField,
38
- type NftCollectionsOptions,
39
- type NftItem,
40
- type NftOwner,
41
- type NftTokenUri,
42
- type NftTokenUriOptions,
43
- } from './nft.js'
40
+ import { getNativeBalanceMode, getNativeCurrency } from './chains.js'
44
41
  import { detectChainId } from './detect.js'
45
42
  import { isTronChain, resolveSource, type ProviderSource, type SourceOptions } from './source.js'
43
+ import { withSignal } from './util.js'
46
44
 
47
45
  export type { ProviderSource }
48
46
 
@@ -62,9 +60,11 @@ export type StaticCallResult<T> = { success: true; data: T } | { success: false;
62
60
  export type CallInput = readonly Call[] | Readonly<Record<string, Call>>
63
61
 
64
62
  type ResultOf<C> = C extends Call<infer R> ? R : any
63
+ /** C 是 any(如 `all<any>(calls)` 显式写了 any,会落到第一个重载上)时按 ethcall 的写法返回 any[] */
64
+ type IsAny<T> = 0 extends 1 & T ? true : false
65
65
  /** 与输入同构的结果:数组 → 数组(元组保留各项类型),对象 → 同名字段 */
66
- export type CallResults<C> = { -readonly [K in keyof C]: ResultOf<C[K]> }
67
- export type TryCallResults<C> = { -readonly [K in keyof C]: ResultOf<C[K]> | null }
66
+ export type CallResults<C> = IsAny<C> extends true ? any[] : { -readonly [K in keyof C]: ResultOf<C[K]> }
67
+ export type TryCallResults<C> = IsAny<C> extends true ? any[] : { -readonly [K in keyof C]: ResultOf<C[K]> | null }
68
68
 
69
69
  export interface ProviderConfig extends SourceOptions {
70
70
  /** 自定义 Multicall3 地址;不填则查内置地址表,表里没有就走 deployless */
@@ -83,16 +83,48 @@ export interface ProviderConfig extends SourceOptions {
83
83
  nativeSymbol?: string
84
84
  /** 主币名称(tokens 用),默认按内置链信息表,表里没有时为 null */
85
85
  nativeName?: string
86
+ /**
87
+ * 主币余额的读取方式,默认按内置表 NATIVE_BALANCE_MODES,表里没有时为 'contract':
88
+ * - 'contract':和其他调用放在同一次 eth_call 里(Multicall3.getEthBalance / deployless 里的 BALANCE)
89
+ * - 'rpc':单独用 eth_getBalance。有的链在合约里读主币余额恒为 0、不报错,需要用这个
90
+ * - `{ erc20: '0x…' }`:主币其实是一个 ERC20 合约,改调它的 balanceOf
91
+ */
92
+ nativeBalance?: NativeBalanceMode
86
93
  }
87
94
 
88
- export interface TokensOptions<F extends TokenField = DefaultTokenField> extends CallOverrides {
95
+ /** balances / allowances / tokens 共用:结果带读取时的区块号 */
96
+ export interface BlockOptions {
97
+ /**
98
+ * 每项结果带 blockNumber(读取时的区块号,与数据在同一次 eth_call 里读出)。传了 minBlock 时自动带上。
99
+ * 大批量拆成多次 eth_call 时为其中包含区块号查询的那一次
100
+ */
101
+ withBlock?: boolean
102
+ }
103
+
104
+ export interface TokensOptions<F extends TokenField = DefaultTokenField> extends CallOverrides, BlockOptions {
89
105
  /** 要返回的字段,默认 ['name', 'symbol', 'decimals'] */
90
106
  fields?: readonly F[]
91
107
  }
92
108
 
93
- export interface BalancesOptions extends CallOverrides {
109
+ export interface BalancesOptions extends CallOverrides, BlockOptions {
94
110
  /** 同时返回 symbol(代币查一次后缓存;主币取 nativeSymbol / 内置链信息表)。默认 false */
95
111
  symbol?: boolean
112
+ /**
113
+ * 是否查 decimals 并返回 decimals / formatted。默认 true。
114
+ * false:只查余额(没有 decimals 的合约如 ERC721 也能拿到余额,冷启动时子调用数减半),结果里不带 decimals / formatted
115
+ */
116
+ decimals?: boolean
117
+ }
118
+
119
+ /** multiBalances / getMultiBalances 的单项查询 */
120
+ export interface BalancesQuery {
121
+ owner: string
122
+ tokens: readonly BalanceToken[]
123
+ }
124
+
125
+ export interface AllowancesOptions extends CallOverrides, BlockOptions {
126
+ /** 是否查 decimals 并返回 decimals / formatted。默认 true;false 时只查额度,结果里不带 decimals / formatted */
127
+ decimals?: boolean
96
128
  }
97
129
 
98
130
  /** 构造参数第一个是 chainId 还是节点:数字、数字字符串、0x 十六进制视为 chainId,其余(URL、对象、数组)视为节点 */
@@ -106,6 +138,7 @@ function isChainIdArg(value: unknown): value is number | bigint | string {
106
138
 
107
139
  const ETH_BALANCE_INPUTS = [{ name: 'addr', type: 'address' }] as const
108
140
  const ETH_BALANCE_OUTPUTS = [{ name: 'balance', type: 'uint256' }] as const
141
+ const BLOCK_NUMBER_OUTPUTS = [{ name: 'blockNumber', type: 'uint256' }] as const
109
142
 
110
143
  /**
111
144
  * Multicall Provider,API 与 ethcall 的 Provider 保持一致:
@@ -212,6 +245,7 @@ export class Provider implements ContractRunner {
212
245
  chainId,
213
246
  multicall: config.deployless ? null : resolveMulticall(chainId, config.multicall),
214
247
  chunkSize: config.chunkSize ?? 500,
248
+ nativeBalance: config.nativeBalance ?? getNativeBalanceMode(chainId),
215
249
  }
216
250
  }
217
251
 
@@ -265,8 +299,14 @@ export class Provider implements ContractRunner {
265
299
  * - decimals、symbol 查到一次后缓存,之后只查 balanceOf;也可以直接传 `{ address, decimals }`
266
300
  * - 单个代币失败(非合约地址、非法地址等)不影响其他代币,该项 success 为 false
267
301
  */
268
- async balances(owner: string, tokens: readonly BalanceToken[], options: BalancesOptions = {}): Promise<TokenBalance[]> {
269
- const { symbol: withSymbol = false, ...overrides } = options
302
+ balances(owner: string, tokens: readonly BalanceToken[], options: BalancesOptions & { decimals: false }): Promise<RawTokenBalance[]>
303
+ balances(owner: string, tokens: readonly BalanceToken[], options?: BalancesOptions): Promise<TokenBalance[]>
304
+ balances(owner: string, tokens: readonly BalanceToken[], options: BalancesOptions = {}): Promise<Array<TokenBalance | RawTokenBalance>> {
305
+ return withSignal(options.signal, () => this.#balances(owner, tokens, options))
306
+ }
307
+
308
+ async #balances(owner: string, tokens: readonly BalanceToken[], options: BalancesOptions): Promise<Array<TokenBalance | RawTokenBalance>> {
309
+ const { symbol: withSymbol = false, decimals: withDecimals = true, ...overrides } = options
270
310
  const { chainId } = await this.#ensureReady()
271
311
  const nativeCurrency = this.#nativeCurrency(chainId)
272
312
  const items = tokens.map((token) => {
@@ -278,41 +318,65 @@ export class Provider implements ContractRunner {
278
318
  const calls: Call[] = []
279
319
  const plan = items.map(({ address, native, decimals, symbol }) => {
280
320
  const balanceIndex = calls.push(native ? this.getEthBalance(owner) : this.erc20(address).balanceOf(owner)) - 1
281
- const decimalsIndex = decimals === undefined ? calls.push(this.erc20(address).decimals()) - 1 : -1
321
+ const decimalsIndex = withDecimals && decimals === undefined ? calls.push(this.erc20(address).decimals()) - 1 : -1
282
322
  const symbolIndex = withSymbol && !native && symbol === undefined ? calls.push(this.#symbolCall(address)) - 1 : -1
283
323
  return { balanceIndex, decimalsIndex, symbolIndex }
284
324
  })
285
- const results = await this.tryAll(calls, overrides)
325
+ const blockIndex = this.#pushBlockNumber(calls, options)
326
+ const results = await this.#settleAll(calls, overrides)
327
+ const block = blockField(results, blockIndex)
286
328
 
287
329
  return items.map((item, i) => {
288
330
  const { balanceIndex, decimalsIndex, symbolIndex } = plan[i] as { balanceIndex: number; decimalsIndex: number; symbolIndex: number }
289
- const balance = results[balanceIndex] as bigint | null
290
- const fetchedDecimals = decimalsIndex === -1 ? null : (results[decimalsIndex] as bigint | null)
291
- const fetchedSymbol = symbolIndex === -1 ? null : (results[symbolIndex] as string | null)
331
+ const balance = results[balanceIndex] as Settled<bigint>
332
+ const fetchedDecimals = valueOf<bigint>(results[decimalsIndex])
333
+ const fetchedSymbol = valueOf<string>(results[symbolIndex])
292
334
  if (!item.native && (fetchedDecimals !== null || fetchedSymbol !== null)) {
293
335
  setCachedTokenMeta(chainId, item.address, {
294
336
  ...(fetchedDecimals !== null ? { decimals: Number(fetchedDecimals) } : {}),
295
337
  ...(fetchedSymbol !== null ? { symbol: fetchedSymbol } : {}),
296
338
  })
297
339
  }
340
+ const extra = { ...(withSymbol ? { symbol: item.symbol ?? fetchedSymbol ?? null } : {}), ...block }
341
+ if (!withDecimals) {
342
+ return balance.ok
343
+ ? { token: item.address, native: item.native, balance: balance.value.toString(), ...extra, success: true }
344
+ : { token: item.address, native: item.native, balance: '0', ...extra, success: false, error: balance.reason, errorField: 'balance' as const }
345
+ }
298
346
  const decimals = item.decimals ?? (fetchedDecimals === null ? null : Number(fetchedDecimals))
299
- const extra = withSymbol ? { symbol: item.symbol ?? fetchedSymbol ?? null } : {}
300
- if (balance === null || decimals === null) {
301
- return { token: item.address, native: item.native, balance: '0', decimals: decimals ?? 0, formatted: '0', ...extra, success: false }
347
+ if (!balance.ok || decimals === null) {
348
+ const failure = !balance.ok
349
+ ? { error: balance.reason, errorField: 'balance' as const }
350
+ : { error: reasonOf(results[decimalsIndex]), errorField: 'decimals' as const }
351
+ return { token: item.address, native: item.native, balance: '0', decimals: decimals ?? 0, formatted: '0', ...extra, success: false, ...failure }
302
352
  }
303
353
  // 结果全是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify
304
354
  return {
305
355
  token: item.address,
306
356
  native: item.native,
307
- balance: balance.toString(),
357
+ balance: balance.value.toString(),
308
358
  decimals,
309
- formatted: formatAmount(balance, decimals),
359
+ formatted: formatUnits(balance.value, decimals),
310
360
  ...extra,
311
361
  success: true,
312
362
  }
313
363
  })
314
364
  }
315
365
 
366
+ /**
367
+ * 多个钱包一次查:同一条链上所有钱包的余额合成一次 multicall(与 balances 选项相同),结果与 queries 一一对应。
368
+ *
369
+ * ```ts
370
+ * const [a, b] = await multi.multiBalances([{ owner: walletA, tokens: [NATIVE_TOKEN, USDT] }, { owner: walletB, tokens: [USDT] }])
371
+ * ```
372
+ */
373
+ multiBalances(queries: readonly BalancesQuery[], options: BalancesOptions & { decimals: false }): Promise<RawTokenBalance[][]>
374
+ multiBalances(queries: readonly BalancesQuery[], options?: BalancesOptions): Promise<TokenBalance[][]>
375
+ multiBalances(queries: readonly BalancesQuery[], options: BalancesOptions = {}): Promise<Array<Array<TokenBalance | RawTokenBalance>>> {
376
+ // 各钱包的子调用在同一 tick 进入合并队列,合成一次 eth_call(相同的 decimals / 区块号查询只发一次)
377
+ return withSignal(options.signal, () => Promise.all(queries.map((query) => this.#balances(query.owner, query.tokens, options))))
378
+ }
379
+
316
380
  /**
317
381
  * 批量查 ERC20 授权额度(发交易前判断是否需要 approve),一次请求。
318
382
  *
@@ -324,72 +388,88 @@ export class Provider implements ContractRunner {
324
388
  * - 主币不需要授权:native 为 true,额度视为 MaxUint256、unlimited 为 true,不发请求
325
389
  * - decimals 与 balances / tokens 共用缓存;也可以直接传 `{ address, decimals }`
326
390
  */
327
- async allowances(
391
+ allowances(owner: string, spender: string, tokens: readonly BalanceToken[], options: AllowancesOptions & { decimals: false }): Promise<RawTokenAllowance[]>
392
+ allowances(owner: string, spender: string, tokens: readonly BalanceToken[], options?: AllowancesOptions): Promise<TokenAllowance[]>
393
+ allowances(
328
394
  owner: string,
329
395
  spender: string,
330
396
  tokens: readonly BalanceToken[],
331
- overrides?: CallOverrides,
332
- ): Promise<TokenAllowance[]> {
397
+ options: AllowancesOptions = {},
398
+ ): Promise<Array<TokenAllowance | RawTokenAllowance>> {
399
+ return withSignal(options.signal, () => this.#allowances(owner, spender, tokens, options))
400
+ }
401
+
402
+ async #allowances(owner: string, spender: string, tokens: readonly BalanceToken[], options: AllowancesOptions): Promise<Array<TokenAllowance | RawTokenAllowance>> {
403
+ const { decimals: withDecimals = true, ...overrides } = options
333
404
  const { chainId } = await this.#ensureReady()
334
405
  const nativeDecimals = this.#nativeCurrency(chainId).decimals
335
406
  const calls: Call[] = []
336
407
  const plan = tokens.map((token) => {
337
408
  const { address, native, decimals } = this.#resolveToken(chainId, token, nativeDecimals)
338
409
  const allowanceIndex = native ? -1 : calls.push(this.erc20(address).allowance(owner, spender)) - 1
339
- const decimalsIndex = native || decimals !== undefined ? -1 : calls.push(this.erc20(address).decimals()) - 1
410
+ const decimalsIndex = native || !withDecimals || decimals !== undefined ? -1 : calls.push(this.erc20(address).decimals()) - 1
340
411
  return { address, native, decimals, allowanceIndex, decimalsIndex }
341
412
  })
342
- const results = calls.length ? await this.tryAll(calls, overrides) : []
413
+ const blockIndex = this.#pushBlockNumber(calls, options)
414
+ const results = await this.#settleAll(calls, overrides)
415
+ const block = blockField(results, blockIndex)
343
416
 
344
417
  return plan.map(({ address, native, decimals: known, allowanceIndex, decimalsIndex }) => {
345
- const allowance = native ? MAX_UINT256 : ((results[allowanceIndex] as bigint | null) ?? null)
346
- const fetched = decimalsIndex === -1 ? null : ((results[decimalsIndex] as bigint | null) ?? null)
418
+ const settled: Settled<bigint> = native ? { ok: true, value: MAX_UINT256 } : (results[allowanceIndex] as Settled<bigint>)
419
+ const fetched = valueOf<bigint>(results[decimalsIndex])
347
420
  if (fetched !== null) {
348
421
  setCachedTokenMeta(chainId, address, { decimals: Number(fetched) })
349
422
  }
423
+ if (!withDecimals) {
424
+ return settled.ok
425
+ ? { token: address, spender, native, allowance: settled.value.toString(), unlimited: settled.value >= UNLIMITED_ALLOWANCE_THRESHOLD, ...block, success: true }
426
+ : { token: address, spender, native, allowance: '0', unlimited: false, ...block, success: false, error: settled.reason, errorField: 'allowance' as const }
427
+ }
350
428
  const decimals = known ?? (fetched === null ? null : Number(fetched))
351
- if (allowance === null || decimals === null) {
352
- return { token: address, spender, native, allowance: '0', decimals: decimals ?? 0, formatted: '0', unlimited: false, success: false }
429
+ if (!settled.ok || decimals === null) {
430
+ const failure = !settled.ok
431
+ ? { error: settled.reason, errorField: 'allowance' as const }
432
+ : { error: reasonOf(results[decimalsIndex]), errorField: 'decimals' as const }
433
+ return { token: address, spender, native, allowance: '0', decimals: decimals ?? 0, formatted: '0', unlimited: false, ...block, success: false, ...failure }
353
434
  }
354
435
  return {
355
436
  token: address,
356
437
  spender,
357
438
  native,
358
- allowance: allowance.toString(),
439
+ allowance: settled.value.toString(),
359
440
  decimals,
360
- formatted: formatAmount(allowance, decimals),
361
- unlimited: allowance >= UNLIMITED_ALLOWANCE_THRESHOLD,
441
+ formatted: formatUnits(settled.value, decimals),
442
+ unlimited: settled.value >= UNLIMITED_ALLOWANCE_THRESHOLD,
443
+ ...block,
362
444
  success: true,
363
445
  }
364
446
  })
365
447
  }
366
448
 
367
- /** 批量查 NFT 集合信息(标准 / name / symbol / totalSupply),字段可选 */
368
- nftCollections<const F extends NftCollectionField = DefaultNftCollectionField>(
369
- collections: readonly string[],
370
- options?: NftCollectionsOptions<F>,
371
- ): Promise<NftCollection<F>[]> {
372
- return nftCollections<F>(this, collections, options)
373
- }
374
-
375
- /** 批量查 ERC721 持有数量(balanceOf) */
376
- nftBalances(owner: string, collections: readonly string[], overrides?: CallOverrides): Promise<NftBalance[]> {
377
- return nftBalances(this, owner, collections, overrides)
378
- }
379
-
380
- /** 批量查 ERC721 持有人(ownerOf),可混合多个集合 */
381
- nftOwners(items: readonly NftItem[], overrides?: CallOverrides): Promise<NftOwner[]> {
382
- return nftOwners(this, items, overrides)
383
- }
384
-
385
- /** 批量查 NFT 元数据地址:兼容 ERC721 tokenURI 与 ERC1155 uri({id} 按规范替换),可转换 ipfs:// */
386
- nftTokenUris(items: readonly NftItem[], options?: NftTokenUriOptions): Promise<NftTokenUri[]> {
387
- return nftTokenUris(this, items, options)
449
+ /**
450
+ * balances / tokens / allowances 的子调用进自动合并队列:与同一时刻其他组件发起的查询合并成一次 multicall。
451
+ * 逐条返回值或失败原因;节点错误照常抛出
452
+ */
453
+ #settleAll(calls: readonly Call[], overrides: CallOverrides = {}): Promise<Settled[]> {
454
+ return this.#batcher.loadSettled(calls, { blockTag: overrides.blockTag, from: overrides.from, minBlock: overrides.minBlock, signal: overrides.signal })
388
455
  }
389
456
 
390
- /** 批量查 ERC1155 余额 */
391
- erc1155Balances(owner: string, items: readonly NftItem[], overrides?: CallOverrides): Promise<Erc1155Balance[]> {
392
- return erc1155Balances(this, owner, items, overrides)
457
+ /** withBlock / minBlock 时在同一批里加一条区块号查询,返回它的下标(不需要时为 -1) */
458
+ #pushBlockNumber(calls: Call[], options: BlockOptions & CallOverrides): number {
459
+ if (!options.withBlock && options.minBlock === undefined) {
460
+ return -1
461
+ }
462
+ return (
463
+ calls.push({
464
+ // 地址只用于展示:执行时换成实际的 multicall 地址,deployless 时由合约内部处理
465
+ contract: { address: MULTICALL3_ADDRESS },
466
+ name: 'getBlockNumber',
467
+ inputs: [],
468
+ outputs: BLOCK_NUMBER_OUTPUTS,
469
+ params: [],
470
+ kind: 'blockNumber',
471
+ }) - 1
472
+ )
393
473
  }
394
474
 
395
475
  /** symbol() 调用:string 解码失败时按 bytes32 解析(MKR 等老代币) */
@@ -420,10 +500,11 @@ export class Provider implements ContractRunner {
420
500
  * - symbol / name 兼容返回 bytes32 的老代币(MKR 等)
421
501
  * - 读取失败的字段为 null;请求的字段都读到时 success 为 true
422
502
  */
423
- async tokens<const F extends TokenField = DefaultTokenField>(
424
- tokens: readonly string[],
425
- options: TokensOptions<F> = {},
426
- ): Promise<TokenDetails<F>[]> {
503
+ tokens<const F extends TokenField = DefaultTokenField>(tokens: readonly string[], options: TokensOptions<F> = {}): Promise<TokenDetails<F>[]> {
504
+ return withSignal(options.signal, () => this.#tokens<F>(tokens, options))
505
+ }
506
+
507
+ async #tokens<F extends TokenField>(tokens: readonly string[], options: TokensOptions<F>): Promise<TokenDetails<F>[]> {
427
508
  const { fields = DEFAULT_TOKEN_FIELDS as unknown as readonly F[], ...overrides } = options
428
509
  const wanted = new Set<TokenField>(fields)
429
510
  const needDecimals = wanted.has('decimals') || wanted.has('totalSupply')
@@ -454,8 +535,10 @@ export class Provider implements ContractRunner {
454
535
  }
455
536
  return { address, native, meta, index }
456
537
  })
457
- const results = calls.length ? await this.tryAll(calls, overrides) : []
458
- const read = <T>(i: number | undefined): T | null => (i === undefined ? null : ((results[i] as T | null) ?? null))
538
+ const blockIndex = this.#pushBlockNumber(calls, options)
539
+ const results = await this.#settleAll(calls, overrides)
540
+ const block = blockField(results, blockIndex)
541
+ const read = <T>(i: number | undefined): T | null => (i === undefined ? null : valueOf<T>(results[i]))
459
542
 
460
543
  return plan.map(({ address, native, meta, index }) => {
461
544
  const name = meta.name ?? read<string>(index.name)
@@ -478,18 +561,25 @@ export class Provider implements ContractRunner {
478
561
  totalSupply: totalSupply === null ? null : totalSupply.toString(),
479
562
  }
480
563
  const out: Record<string, unknown> = { address, native }
481
- let success = true
564
+ let failedField: TokenField | null = null
482
565
  for (const field of fields) {
483
566
  out[field] = values[field]
484
567
  // 主币没有 totalSupply,不算失败
485
568
  if (values[field] === null && !(native && field === 'totalSupply')) {
486
- success = false
569
+ failedField ??= field
487
570
  }
488
571
  }
489
572
  if (wanted.has('totalSupply')) {
490
- out.totalSupplyFormatted = totalSupply === null || decimals === null ? null : formatAmount(totalSupply, decimals)
573
+ out.totalSupplyFormatted = totalSupply === null || decimals === null ? null : formatUnits(totalSupply, decimals)
574
+ }
575
+ Object.assign(out, block)
576
+ out.success = failedField === null
577
+ if (failedField !== null) {
578
+ // 没发请求的字段(主币信息不在内置表、也没有配置)不是调用失败
579
+ const callIndex = index[failedField]
580
+ out.error = callIndex === undefined ? 'not-configured' : reasonOf(results[callIndex])
581
+ out.errorField = failedField
491
582
  }
492
- out.success = success
493
583
  return out as TokenDetails<F>
494
584
  })
495
585
  }
@@ -517,10 +607,12 @@ export class Provider implements ContractRunner {
517
607
  */
518
608
  all<const C extends CallInput>(calls: C, overrides?: CallOverrides): Promise<CallResults<C>>
519
609
  all<T = any>(calls: readonly Call[], overrides?: CallOverrides): Promise<T[]>
520
- async all(calls: CallInput, overrides?: CallOverrides): Promise<unknown> {
521
- const [list, pack] = unpack(calls)
522
- const results = await this.#aggregate(list.map((call) => encodeCall(call, false)), overrides)
523
- return pack(list.map((call, i) => decodeCall(call, (results[i] as RawResult).returnData)))
610
+ all(calls: CallInput, overrides?: CallOverrides): Promise<unknown> {
611
+ return withSignal(overrides?.signal, async () => {
612
+ const [list, pack] = unpack(calls)
613
+ const results = await this.#aggregate(list.map((call) => encodeCall(call, false)), overrides)
614
+ return pack(list.map((call, i) => decodeCall(call, (results[i] as RawResult).returnData)))
615
+ })
524
616
  }
525
617
 
526
618
  /**
@@ -537,7 +629,11 @@ export class Provider implements ContractRunner {
537
629
  /**
538
630
  * 逐条指定是否允许失败:允许失败的位置返回 null,不允许失败的一旦失败整体抛错。
539
631
  */
540
- async tryEach<T = any>(calls: readonly Call[], canFail: readonly boolean[], overrides?: CallOverrides): Promise<(T | null)[]> {
632
+ tryEach<T = any>(calls: readonly Call[], canFail: readonly boolean[], overrides?: CallOverrides): Promise<(T | null)[]> {
633
+ return withSignal(overrides?.signal, () => this.#tryEach<T>(calls, canFail, overrides))
634
+ }
635
+
636
+ async #tryEach<T>(calls: readonly Call[], canFail: readonly boolean[], overrides?: CallOverrides): Promise<(T | null)[]> {
541
637
  // 允许失败的条目编码出错(如后端给的脏地址)只让该条为 null,不拖垮整批
542
638
  const requests: Array<CallRequest | null> = calls.map((call, i) => {
543
639
  const callCanFail = canFail[i]
@@ -566,7 +662,12 @@ export class Provider implements ContractRunner {
566
662
  */
567
663
  call<T = any>(call: Call, overrides?: CallOverrides): Promise<T> {
568
664
  const own = call.overrides
569
- return this.#batcher.load<T>(call, merge({ blockTag: own?.blockTag, from: own?.from }, overrides))
665
+ return withSignal(overrides?.signal, () =>
666
+ this.#batcher.load<T>(
667
+ call,
668
+ merge({ blockTag: own?.blockTag, from: own?.from }, { blockTag: overrides?.blockTag, from: overrides?.from, minBlock: overrides?.minBlock, signal: overrides?.signal }),
669
+ ),
670
+ )
570
671
  }
571
672
 
572
673
  /**
@@ -578,12 +679,14 @@ export class Provider implements ContractRunner {
578
679
  *
579
680
  * overrides 优先级:参数 overrides > Call 上的 overrides(`contract.swap(params, { value })`)。
580
681
  */
581
- async staticCall<T = any>(call: Call, overrides: StaticCallOverrides = {}): Promise<T> {
582
- const result = await this.#staticCall<T>(call, overrides)
583
- if (!result.success) {
584
- throw result.error
585
- }
586
- return result.data
682
+ staticCall<T = any>(call: Call, overrides: StaticCallOverrides = {}): Promise<T> {
683
+ return withSignal(overrides.signal, async () => {
684
+ const result = await this.#staticCall<T>(call, overrides)
685
+ if (!result.success) {
686
+ throw result.error
687
+ }
688
+ return result.data
689
+ })
587
690
  }
588
691
 
589
692
  /**
@@ -593,10 +696,11 @@ export class Provider implements ContractRunner {
593
696
  * @param items Call,或 `{ call, overrides }` 以便每条带不同的 value / from
594
697
  * @param overrides 所有条目共享的 overrides,优先级最低
595
698
  */
596
- async staticCallAll<T = any>(
597
- items: ReadonlyArray<Call | StaticCallItem>,
598
- overrides: StaticCallOverrides = {},
599
- ): Promise<StaticCallResult<T>[]> {
699
+ staticCallAll<T = any>(items: ReadonlyArray<Call | StaticCallItem>, overrides: StaticCallOverrides = {}): Promise<StaticCallResult<T>[]> {
700
+ return withSignal(overrides.signal, () => this.#staticCallAll<T>(items, overrides))
701
+ }
702
+
703
+ #staticCallAll<T>(items: ReadonlyArray<Call | StaticCallItem>, overrides: StaticCallOverrides): Promise<StaticCallResult<T>[]> {
600
704
  return Promise.all(
601
705
  items.map((item) =>
602
706
  'call' in item && 'contract' in item.call
@@ -615,7 +719,10 @@ export class Provider implements ContractRunner {
615
719
  const merged: StaticCallOverrides = merge(merge({ ...shared }, call.overrides), explicit)
616
720
  if (call.kind === 'ethBalance') {
617
721
  try {
618
- const { provider } = await this.#ensureReady()
722
+ const { provider, nativeBalance } = await this.#ensureReady()
723
+ if (typeof nativeBalance === 'object') {
724
+ return await this.#staticCall<T>(this.erc20(nativeBalance.erc20).balanceOf(call.params[0]), explicit, shared)
725
+ }
619
726
  return { success: true, data: (await provider.getBalance(call.params[0], merged.blockTag)) as T }
620
727
  } catch (err) {
621
728
  return { success: false, error: err instanceof Error ? err : new Error(String(err)) }
@@ -648,6 +755,34 @@ export class Provider implements ContractRunner {
648
755
  }
649
756
  }
650
757
 
758
+ /** 当前区块号(底层节点需支持 getBlockNumber) */
759
+ async getBlockNumber(): Promise<number> {
760
+ const { provider } = await this.#ensureReady()
761
+ if (!provider.getBlockNumber) {
762
+ throw new Error('getBlockNumber is not supported by this provider')
763
+ }
764
+ return provider.getBlockNumber()
765
+ }
766
+
767
+ /** eth_getLogs(底层节点需支持 getLogs)。多节点时依次尝试,全部失败抛 GetLogsError */
768
+ async getLogs(filter: Parameters<NonNullable<EthersLikeProvider['getLogs']>>[0]): Promise<Awaited<ReturnType<NonNullable<EthersLikeProvider['getLogs']>>>> {
769
+ const { provider } = await this.#ensureReady()
770
+ if (!provider.getLogs) {
771
+ throw new Error('getLogs is not supported by this provider')
772
+ }
773
+ return provider.getLogs(filter)
774
+ }
775
+
776
+ /** 是否是按主币处理的地址(config.nativeTokens,默认 0xeeee…eeee 和零地址) */
777
+ isNativeToken(address: string): boolean {
778
+ return this.#nativeTokens.has(address.toLowerCase())
779
+ }
780
+
781
+ /** 按主币处理的地址(小写),顺序同 config.nativeTokens */
782
+ get nativeTokens(): readonly string[] {
783
+ return [...this.#nativeTokens]
784
+ }
785
+
651
786
  /** 底层连接(多节点时是 FallbackRpc)。只传节点构造时,需在 ready() 之后读取 */
652
787
  get rpc(): EthersLikeProvider {
653
788
  if (!this.#ctx) {
@@ -695,18 +830,26 @@ function unpack(calls: CallInput): [readonly Call[], (results: unknown[]) => unk
695
830
  }
696
831
 
697
832
  function decodeTry<T>(calls: ReadonlyArray<Call | FailableCall>, results: RawResult[]): (T | null)[] {
698
- return calls.map((call, i) => {
699
- const result = results[i] as RawResult
700
- if (!result.success) {
701
- return null
702
- }
703
- try {
704
- return decodeCall<T>(call, result.returnData)
705
- } catch {
706
- // 解码失败:多半是目标地址没有合约
707
- return null
708
- }
709
- })
833
+ return calls.map((call, i) => valueOf<T>(settleResult<T>(call, results[i] as RawResult)))
834
+ }
835
+
836
+ /** withBlock 时每项结果附带的 { blockNumber }(区块号查询失败时为 null);不需要时为空对象 */
837
+ function blockField(results: readonly Settled[], index: number): { blockNumber?: number | null } {
838
+ if (index === -1) {
839
+ return {}
840
+ }
841
+ const value = valueOf<bigint>(results[index])
842
+ return { blockNumber: value === null ? null : Number(value) }
843
+ }
844
+
845
+ /** 成功的值;失败或没有这一项(下标为 -1)时为 null */
846
+ function valueOf<T>(settled: Settled | undefined): T | null {
847
+ return settled?.ok ? (settled.value as T) : null
848
+ }
849
+
850
+ /** 失败原因;没有对应的调用时(不应发生)按 decode-failed */
851
+ function reasonOf(settled: Settled | undefined): FailureReason {
852
+ return settled && !settled.ok ? settled.reason : 'decode-failed'
710
853
  }
711
854
 
712
855
  export default Provider