@w3lib/evm-batch-call 0.3.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 (103) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +98 -35
  3. package/contracts/DeploylessMulticall3.sol +5 -0
  4. package/dist/aggregate.d.ts +20 -1
  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 +31 -3
  33. package/dist/fallback.d.ts.map +1 -1
  34. package/dist/fallback.js +165 -27
  35. package/dist/fallback.js.map +1 -1
  36. package/dist/index.d.ts +9 -10
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +6 -7
  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 +9 -0
  48. package/dist/owner.d.ts.map +1 -1
  49. package/dist/owner.js +11 -2
  50. package/dist/owner.js.map +1 -1
  51. package/dist/provider.d.ts +55 -29
  52. package/dist/provider.d.ts.map +1 -1
  53. package/dist/provider.js +140 -111
  54. package/dist/provider.js.map +1 -1
  55. package/dist/shortcuts.d.ts +34 -53
  56. package/dist/shortcuts.d.ts.map +1 -1
  57. package/dist/shortcuts.js +41 -91
  58. package/dist/shortcuts.js.map +1 -1
  59. package/dist/source.d.ts +4 -2
  60. package/dist/source.d.ts.map +1 -1
  61. package/dist/source.js +35 -6
  62. package/dist/source.js.map +1 -1
  63. package/dist/subpaths/nft.d.ts +46 -0
  64. package/dist/subpaths/nft.d.ts.map +1 -0
  65. package/dist/subpaths/nft.js +55 -0
  66. package/dist/subpaths/nft.js.map +1 -0
  67. package/dist/subpaths/owner.d.ts +33 -0
  68. package/dist/subpaths/owner.d.ts.map +1 -0
  69. package/dist/subpaths/owner.js +34 -0
  70. package/dist/subpaths/owner.js.map +1 -0
  71. package/dist/tron.d.ts +2 -0
  72. package/dist/tron.d.ts.map +1 -1
  73. package/dist/tron.js +5 -0
  74. package/dist/tron.js.map +1 -1
  75. package/dist/util.d.ts +5 -0
  76. package/dist/util.d.ts.map +1 -1
  77. package/dist/util.js +23 -0
  78. package/dist/util.js.map +1 -1
  79. package/dist/watch.d.ts +25 -0
  80. package/dist/watch.d.ts.map +1 -0
  81. package/dist/watch.js +105 -0
  82. package/dist/watch.js.map +1 -0
  83. package/package.json +19 -10
  84. package/src/aggregate.ts +172 -7
  85. package/src/batcher.ts +69 -9
  86. package/src/call.ts +42 -2
  87. package/src/chains.ts +15 -0
  88. package/src/deployless.ts +1 -1
  89. package/src/erc20.ts +171 -8
  90. package/src/events.ts +42 -0
  91. package/src/fallback.ts +193 -26
  92. package/src/index.ts +19 -61
  93. package/src/multicall.ts +34 -2
  94. package/src/nft.ts +5 -0
  95. package/src/owner.ts +11 -2
  96. package/src/provider.ts +225 -124
  97. package/src/shortcuts.ts +76 -110
  98. package/src/source.ts +35 -8
  99. package/src/subpaths/nft.ts +107 -0
  100. package/src/subpaths/owner.ts +69 -0
  101. package/src/tron.ts +7 -0
  102. package/src/util.ts +27 -0
  103. 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,27 +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 { 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'
40
+ import { getNativeBalanceMode, getNativeCurrency } from './chains.js'
45
41
  import { detectChainId } from './detect.js'
46
42
  import { isTronChain, resolveSource, type ProviderSource, type SourceOptions } from './source.js'
43
+ import { withSignal } from './util.js'
47
44
 
48
45
  export type { ProviderSource }
49
46
 
@@ -63,9 +60,11 @@ export type StaticCallResult<T> = { success: true; data: T } | { success: false;
63
60
  export type CallInput = readonly Call[] | Readonly<Record<string, Call>>
64
61
 
65
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
66
65
  /** 与输入同构的结果:数组 → 数组(元组保留各项类型),对象 → 同名字段 */
67
- export type CallResults<C> = { -readonly [K in keyof C]: ResultOf<C[K]> }
68
- 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 }
69
68
 
70
69
  export interface ProviderConfig extends SourceOptions {
71
70
  /** 自定义 Multicall3 地址;不填则查内置地址表,表里没有就走 deployless */
@@ -84,16 +83,48 @@ export interface ProviderConfig extends SourceOptions {
84
83
  nativeSymbol?: string
85
84
  /** 主币名称(tokens 用),默认按内置链信息表,表里没有时为 null */
86
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
93
+ }
94
+
95
+ /** balances / allowances / tokens 共用:结果带读取时的区块号 */
96
+ export interface BlockOptions {
97
+ /**
98
+ * 每项结果带 blockNumber(读取时的区块号,与数据在同一次 eth_call 里读出)。传了 minBlock 时自动带上。
99
+ * 大批量拆成多次 eth_call 时为其中包含区块号查询的那一次
100
+ */
101
+ withBlock?: boolean
87
102
  }
88
103
 
89
- export interface TokensOptions<F extends TokenField = DefaultTokenField> extends CallOverrides {
104
+ export interface TokensOptions<F extends TokenField = DefaultTokenField> extends CallOverrides, BlockOptions {
90
105
  /** 要返回的字段,默认 ['name', 'symbol', 'decimals'] */
91
106
  fields?: readonly F[]
92
107
  }
93
108
 
94
- export interface BalancesOptions extends CallOverrides {
109
+ export interface BalancesOptions extends CallOverrides, BlockOptions {
95
110
  /** 同时返回 symbol(代币查一次后缓存;主币取 nativeSymbol / 内置链信息表)。默认 false */
96
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
97
128
  }
98
129
 
99
130
  /** 构造参数第一个是 chainId 还是节点:数字、数字字符串、0x 十六进制视为 chainId,其余(URL、对象、数组)视为节点 */
@@ -107,6 +138,7 @@ function isChainIdArg(value: unknown): value is number | bigint | string {
107
138
 
108
139
  const ETH_BALANCE_INPUTS = [{ name: 'addr', type: 'address' }] as const
109
140
  const ETH_BALANCE_OUTPUTS = [{ name: 'balance', type: 'uint256' }] as const
141
+ const BLOCK_NUMBER_OUTPUTS = [{ name: 'blockNumber', type: 'uint256' }] as const
110
142
 
111
143
  /**
112
144
  * Multicall Provider,API 与 ethcall 的 Provider 保持一致:
@@ -213,6 +245,7 @@ export class Provider implements ContractRunner {
213
245
  chainId,
214
246
  multicall: config.deployless ? null : resolveMulticall(chainId, config.multicall),
215
247
  chunkSize: config.chunkSize ?? 500,
248
+ nativeBalance: config.nativeBalance ?? getNativeBalanceMode(chainId),
216
249
  }
217
250
  }
218
251
 
@@ -266,8 +299,14 @@ export class Provider implements ContractRunner {
266
299
  * - decimals、symbol 查到一次后缓存,之后只查 balanceOf;也可以直接传 `{ address, decimals }`
267
300
  * - 单个代币失败(非合约地址、非法地址等)不影响其他代币,该项 success 为 false
268
301
  */
269
- async balances(owner: string, tokens: readonly BalanceToken[], options: BalancesOptions = {}): Promise<TokenBalance[]> {
270
- 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
271
310
  const { chainId } = await this.#ensureReady()
272
311
  const nativeCurrency = this.#nativeCurrency(chainId)
273
312
  const items = tokens.map((token) => {
@@ -279,41 +318,65 @@ export class Provider implements ContractRunner {
279
318
  const calls: Call[] = []
280
319
  const plan = items.map(({ address, native, decimals, symbol }) => {
281
320
  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
321
+ const decimalsIndex = withDecimals && decimals === undefined ? calls.push(this.erc20(address).decimals()) - 1 : -1
283
322
  const symbolIndex = withSymbol && !native && symbol === undefined ? calls.push(this.#symbolCall(address)) - 1 : -1
284
323
  return { balanceIndex, decimalsIndex, symbolIndex }
285
324
  })
286
- 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)
287
328
 
288
329
  return items.map((item, i) => {
289
330
  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)
331
+ const balance = results[balanceIndex] as Settled<bigint>
332
+ const fetchedDecimals = valueOf<bigint>(results[decimalsIndex])
333
+ const fetchedSymbol = valueOf<string>(results[symbolIndex])
293
334
  if (!item.native && (fetchedDecimals !== null || fetchedSymbol !== null)) {
294
335
  setCachedTokenMeta(chainId, item.address, {
295
336
  ...(fetchedDecimals !== null ? { decimals: Number(fetchedDecimals) } : {}),
296
337
  ...(fetchedSymbol !== null ? { symbol: fetchedSymbol } : {}),
297
338
  })
298
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
+ }
299
346
  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 }
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 }
303
352
  }
304
353
  // 结果全是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify
305
354
  return {
306
355
  token: item.address,
307
356
  native: item.native,
308
- balance: balance.toString(),
357
+ balance: balance.value.toString(),
309
358
  decimals,
310
- formatted: formatAmount(balance, decimals),
359
+ formatted: formatUnits(balance.value, decimals),
311
360
  ...extra,
312
361
  success: true,
313
362
  }
314
363
  })
315
364
  }
316
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
+
317
380
  /**
318
381
  * 批量查 ERC20 授权额度(发交易前判断是否需要 approve),一次请求。
319
382
  *
@@ -325,85 +388,88 @@ export class Provider implements ContractRunner {
325
388
  * - 主币不需要授权:native 为 true,额度视为 MaxUint256、unlimited 为 true,不发请求
326
389
  * - decimals 与 balances / tokens 共用缓存;也可以直接传 `{ address, decimals }`
327
390
  */
328
- 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(
329
394
  owner: string,
330
395
  spender: string,
331
396
  tokens: readonly BalanceToken[],
332
- overrides?: CallOverrides,
333
- ): 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
334
404
  const { chainId } = await this.#ensureReady()
335
405
  const nativeDecimals = this.#nativeCurrency(chainId).decimals
336
406
  const calls: Call[] = []
337
407
  const plan = tokens.map((token) => {
338
408
  const { address, native, decimals } = this.#resolveToken(chainId, token, nativeDecimals)
339
409
  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
410
+ const decimalsIndex = native || !withDecimals || decimals !== undefined ? -1 : calls.push(this.erc20(address).decimals()) - 1
341
411
  return { address, native, decimals, allowanceIndex, decimalsIndex }
342
412
  })
343
- 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)
344
416
 
345
417
  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)
418
+ const settled: Settled<bigint> = native ? { ok: true, value: MAX_UINT256 } : (results[allowanceIndex] as Settled<bigint>)
419
+ const fetched = valueOf<bigint>(results[decimalsIndex])
348
420
  if (fetched !== null) {
349
421
  setCachedTokenMeta(chainId, address, { decimals: Number(fetched) })
350
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
+ }
351
428
  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 }
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 }
354
434
  }
355
435
  return {
356
436
  token: address,
357
437
  spender,
358
438
  native,
359
- allowance: allowance.toString(),
439
+ allowance: settled.value.toString(),
360
440
  decimals,
361
- formatted: formatAmount(allowance, decimals),
362
- unlimited: allowance >= UNLIMITED_ALLOWANCE_THRESHOLD,
441
+ formatted: formatUnits(settled.value, decimals),
442
+ unlimited: settled.value >= UNLIMITED_ALLOWANCE_THRESHOLD,
443
+ ...block,
363
444
  success: true,
364
445
  }
365
446
  })
366
447
  }
367
448
 
368
449
  /**
369
- * 列出持有人拥有的代币(资产列表):从代币来源拿候选代币,再用 multicall 在链上核对余额。
370
- * 默认来源是公开代币列表,**只能发现列表里的代币**,局限性见 README「资产列表」一节。
371
- *
372
- * @deprecated 不推荐使用:EVM 链上无法只靠节点可靠地列出地址持有的全部代币,这个方法只是尽力而为——
373
- * 默认来源只能发现公开列表里的代币,依赖第三方免费服务(可能限流、改格式或停止服务),要查几千个代币的余额、耗时数秒,
374
- * 价格也不一定可信;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束。
375
- * 已知要查哪些代币时请用 `getBalances`;需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。局限性详见 README「资产列表」。
450
+ * balances / tokens / allowances 的子调用进自动合并队列:与同一时刻其他组件发起的查询合并成一次 multicall。
451
+ * 逐条返回值或失败原因;节点错误照常抛出
376
452
  */
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)
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 })
392
455
  }
393
456
 
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)
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
+ )
407
473
  }
408
474
 
409
475
  /** symbol() 调用:string 解码失败时按 bytes32 解析(MKR 等老代币) */
@@ -434,10 +500,11 @@ export class Provider implements ContractRunner {
434
500
  * - symbol / name 兼容返回 bytes32 的老代币(MKR 等)
435
501
  * - 读取失败的字段为 null;请求的字段都读到时 success 为 true
436
502
  */
437
- async tokens<const F extends TokenField = DefaultTokenField>(
438
- tokens: readonly string[],
439
- options: TokensOptions<F> = {},
440
- ): 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>[]> {
441
508
  const { fields = DEFAULT_TOKEN_FIELDS as unknown as readonly F[], ...overrides } = options
442
509
  const wanted = new Set<TokenField>(fields)
443
510
  const needDecimals = wanted.has('decimals') || wanted.has('totalSupply')
@@ -468,8 +535,10 @@ export class Provider implements ContractRunner {
468
535
  }
469
536
  return { address, native, meta, index }
470
537
  })
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))
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]))
473
542
 
474
543
  return plan.map(({ address, native, meta, index }) => {
475
544
  const name = meta.name ?? read<string>(index.name)
@@ -492,18 +561,25 @@ export class Provider implements ContractRunner {
492
561
  totalSupply: totalSupply === null ? null : totalSupply.toString(),
493
562
  }
494
563
  const out: Record<string, unknown> = { address, native }
495
- let success = true
564
+ let failedField: TokenField | null = null
496
565
  for (const field of fields) {
497
566
  out[field] = values[field]
498
567
  // 主币没有 totalSupply,不算失败
499
568
  if (values[field] === null && !(native && field === 'totalSupply')) {
500
- success = false
569
+ failedField ??= field
501
570
  }
502
571
  }
503
572
  if (wanted.has('totalSupply')) {
504
- 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
505
582
  }
506
- out.success = success
507
583
  return out as TokenDetails<F>
508
584
  })
509
585
  }
@@ -531,10 +607,12 @@ export class Provider implements ContractRunner {
531
607
  */
532
608
  all<const C extends CallInput>(calls: C, overrides?: CallOverrides): Promise<CallResults<C>>
533
609
  all<T = any>(calls: readonly Call[], overrides?: CallOverrides): Promise<T[]>
534
- async all(calls: CallInput, overrides?: CallOverrides): Promise<unknown> {
535
- const [list, pack] = unpack(calls)
536
- const results = await this.#aggregate(list.map((call) => encodeCall(call, false)), overrides)
537
- 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
+ })
538
616
  }
539
617
 
540
618
  /**
@@ -551,7 +629,11 @@ export class Provider implements ContractRunner {
551
629
  /**
552
630
  * 逐条指定是否允许失败:允许失败的位置返回 null,不允许失败的一旦失败整体抛错。
553
631
  */
554
- 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)[]> {
555
637
  // 允许失败的条目编码出错(如后端给的脏地址)只让该条为 null,不拖垮整批
556
638
  const requests: Array<CallRequest | null> = calls.map((call, i) => {
557
639
  const callCanFail = canFail[i]
@@ -580,7 +662,12 @@ export class Provider implements ContractRunner {
580
662
  */
581
663
  call<T = any>(call: Call, overrides?: CallOverrides): Promise<T> {
582
664
  const own = call.overrides
583
- 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
+ )
584
671
  }
585
672
 
586
673
  /**
@@ -592,12 +679,14 @@ export class Provider implements ContractRunner {
592
679
  *
593
680
  * overrides 优先级:参数 overrides > Call 上的 overrides(`contract.swap(params, { value })`)。
594
681
  */
595
- async staticCall<T = any>(call: Call, overrides: StaticCallOverrides = {}): Promise<T> {
596
- const result = await this.#staticCall<T>(call, overrides)
597
- if (!result.success) {
598
- throw result.error
599
- }
600
- 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
+ })
601
690
  }
602
691
 
603
692
  /**
@@ -607,10 +696,11 @@ export class Provider implements ContractRunner {
607
696
  * @param items Call,或 `{ call, overrides }` 以便每条带不同的 value / from
608
697
  * @param overrides 所有条目共享的 overrides,优先级最低
609
698
  */
610
- async staticCallAll<T = any>(
611
- items: ReadonlyArray<Call | StaticCallItem>,
612
- overrides: StaticCallOverrides = {},
613
- ): 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>[]> {
614
704
  return Promise.all(
615
705
  items.map((item) =>
616
706
  'call' in item && 'contract' in item.call
@@ -629,7 +719,10 @@ export class Provider implements ContractRunner {
629
719
  const merged: StaticCallOverrides = merge(merge({ ...shared }, call.overrides), explicit)
630
720
  if (call.kind === 'ethBalance') {
631
721
  try {
632
- 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
+ }
633
726
  return { success: true, data: (await provider.getBalance(call.params[0], merged.blockTag)) as T }
634
727
  } catch (err) {
635
728
  return { success: false, error: err instanceof Error ? err : new Error(String(err)) }
@@ -737,18 +830,26 @@ function unpack(calls: CallInput): [readonly Call[], (results: unknown[]) => unk
737
830
  }
738
831
 
739
832
  function decodeTry<T>(calls: ReadonlyArray<Call | FailableCall>, results: RawResult[]): (T | null)[] {
740
- return calls.map((call, i) => {
741
- const result = results[i] as RawResult
742
- if (!result.success) {
743
- return null
744
- }
745
- try {
746
- return decodeCall<T>(call, result.returnData)
747
- } catch {
748
- // 解码失败:多半是目标地址没有合约
749
- return null
750
- }
751
- })
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'
752
853
  }
753
854
 
754
855
  export default Provider