@w3lib/evm-batch-call 0.3.0 → 0.4.1

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 +59 -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 +222 -32
  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 +64 -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 +133 -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 +49 -4
  33. package/dist/fallback.d.ts.map +1 -1
  34. package/dist/fallback.js +239 -37
  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 +106 -0
  82. package/dist/watch.js.map +1 -0
  83. package/package.json +19 -10
  84. package/src/aggregate.ts +253 -34
  85. package/src/batcher.ts +73 -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 +184 -8
  90. package/src/events.ts +42 -0
  91. package/src/fallback.ts +287 -38
  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 +152 -0
package/src/shortcuts.ts CHANGED
@@ -1,18 +1,6 @@
1
1
  import type { CallOverrides } from './aggregate.js'
2
- import type { OwnedToken, OwnerTokensOptions } from './owner.js'
3
- import type { BalanceToken, DefaultTokenField, TokenAllowance, TokenBalance, TokenDetails, TokenField } from './erc20.js'
4
- import type {
5
- DefaultNftCollectionField,
6
- Erc1155Balance,
7
- NftBalance,
8
- NftCollection,
9
- NftCollectionField,
10
- NftCollectionsOptions,
11
- NftItem,
12
- NftOwner,
13
- NftTokenUri,
14
- } from './nft.js'
15
- import { Provider, type BalancesOptions, type ProviderConfig, type TokensOptions } from './provider.js'
2
+ import type { BalanceToken, DefaultTokenField, RawTokenAllowance, RawTokenBalance, TokenAllowance, TokenBalance, TokenDetails, TokenField } from './erc20.js'
3
+ import { Provider, type AllowancesOptions, type BalancesOptions, type BalancesQuery, type ProviderConfig, type TokensOptions } from './provider.js'
16
4
  import type { ProviderSource } from './source.js'
17
5
 
18
6
  /** getBalances / getTokens 共用的节点参数 */
@@ -25,17 +13,21 @@ export interface ShortcutProviderOptions extends ProviderConfig {
25
13
  * - ethers Provider
26
14
  * - 浏览器插件钱包:`window.ethereum`(EIP-1193)、`window.tronWeb`
27
15
  * - 混合数组,如 `[window.ethereum, 'https://…']`:钱包出错或不在这条链上时自动用后面的节点
16
+ * - 已创建的 Provider 实例:直接使用(chainId 和其他节点配置以实例为准,不能再传)
28
17
  */
29
- provider?: ProviderSource | readonly ProviderSource[]
18
+ provider?: ProviderSource | readonly ProviderSource[] | Provider
30
19
  }
31
20
 
32
21
  export interface GetBalancesOptions extends BalancesOptions, ShortcutProviderOptions {}
33
22
 
34
23
  export interface GetTokensOptions<F extends TokenField = DefaultTokenField> extends TokensOptions<F>, ShortcutProviderOptions {}
35
24
 
25
+ export interface GetAllowancesOptions extends AllowancesOptions, ShortcutProviderOptions {}
26
+
36
27
  // 复用 Provider:多次调用共享连接、multicall 地址判定和自动合并队列。
37
- // 按 “chainId + 配置内容” 分组,组内 URL 按字符串、钱包 / Provider 对象按引用(WeakMap,不阻止回收)。
38
- // 不缓存的情况:配置里有函数等无法比较内容的值;含对象的节点数组;没传 chainId 且节点是对象
28
+ // 按 “chainId + 配置内容” 分组,组内 URL 按字符串、钱包 / Provider 对象按引用(WeakMap,不阻止回收);
29
+ // 钱包 + URL 的混合数组按 “对象编号 + URL” 组合成字符串 key(对象编号用 WeakMap 分配,缓存条数有上限)。
30
+ // 不缓存的情况:配置里有函数等无法比较内容的值;没传 chainId 且节点里有对象
39
31
  // (钱包可能切链,每次按当前链新建;chainId 识别本身有缓存,切链时自动失效,不会每次都发请求)
40
32
  interface ProviderGroup {
41
33
  byString: Map<string, Provider>
@@ -67,7 +59,20 @@ function configKey(config: ProviderConfig): string | null {
67
59
  }
68
60
  }
69
61
 
70
- function sourceKey(chainId: number | undefined, source: ShortcutProviderOptions['provider']): string | object | null {
62
+ /** 对象来源(钱包、ethers Provider)的编号,用于混合数组的缓存 key;WeakMap 不阻止对象被回收 */
63
+ const objectIds = new WeakMap<object, number>()
64
+ let nextObjectId = 0
65
+
66
+ function objectId(value: object): number {
67
+ let id = objectIds.get(value)
68
+ if (id === undefined) {
69
+ id = ++nextObjectId
70
+ objectIds.set(value, id)
71
+ }
72
+ return id
73
+ }
74
+
75
+ function sourceKey(chainId: number | undefined, source: ProviderSource | readonly ProviderSource[] | undefined): string | object | null {
71
76
  if (source === undefined) {
72
77
  return 'default'
73
78
  }
@@ -76,12 +81,23 @@ function sourceKey(chainId: number | undefined, source: ShortcutProviderOptions[
76
81
  }
77
82
  if (Array.isArray(source)) {
78
83
  // URL 数组每次调用都是新数组,按内容缓存
79
- return source.every((item) => typeof item === 'string') ? `urls:${source.join('\n')}` : null
84
+ if (source.every((item) => typeof item === 'string')) {
85
+ return `urls:${source.join('\n')}`
86
+ }
87
+ // 钱包 + URL:对象按编号、URL 按内容
88
+ return chainId === undefined ? null : `mixed:${source.map((item) => (typeof item === 'string' ? `url:${item}` : `obj:${objectId(item as object)}`)).join('\n')}`
80
89
  }
81
90
  return chainId === undefined ? null : (source as object)
82
91
  }
83
92
 
84
- function getProvider(chainId: number | undefined, source: ShortcutProviderOptions['provider'], config: ProviderConfig): Provider {
93
+ function getProvider(chainId: number | undefined, input: ShortcutProviderOptions['provider'], config: ProviderConfig): Provider {
94
+ if (input instanceof Provider) {
95
+ if (chainId !== undefined || Object.keys(config).length) {
96
+ throw new Error('chainId and provider config must be set on the Provider instance itself, not passed alongside it')
97
+ }
98
+ return input
99
+ }
100
+ const source = input
85
101
  if (chainId === undefined && source === undefined) {
86
102
  throw new Error('getBalances / getTokens requires chainId or provider')
87
103
  }
@@ -113,12 +129,15 @@ function getProvider(chainId: number | undefined, source: ShortcutProviderOption
113
129
 
114
130
  export interface ShortcutOptions extends CallOverrides, ShortcutProviderOptions {}
115
131
 
116
- /** 拆出节点参数、调用参数和函数自己的参数(ownKeys),其余作为 Provider 配置;值为 undefined 的参数忽略 */
117
- function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
132
+ /**
133
+ * 拆出节点参数、调用参数和函数自己的参数(ownKeys),其余作为 Provider 配置;值为 undefined 的参数忽略。
134
+ * 子路径(/nft、/owner)的快捷函数也用它,与 getBalances 共享 Provider 缓存
135
+ */
136
+ export function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
118
137
  options: T,
119
138
  ownKeys: readonly K[] = [],
120
139
  ): { provider: Provider; overrides: CallOverrides; own: Pick<T, K> } {
121
- const { chainId, provider, blockTag, from, ...others } = options
140
+ const { chainId, provider, blockTag, from, signal, minBlock, ...others } = options
122
141
  const own: Record<string, unknown> = {}
123
142
  const config: Record<string, unknown> = {}
124
143
  for (const [key, value] of Object.entries(others)) {
@@ -129,7 +148,7 @@ function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
129
148
  return {
130
149
  // null / 空字符串(如状态里还没有 chainId)按“未传”处理,走自动识别,而不是变成 chainId 0
131
150
  provider: getProvider(chainId === undefined || chainId === null || chainId === '' ? undefined : Number(chainId), provider, config as ProviderConfig),
132
- overrides: { blockTag, from },
151
+ overrides: { blockTag, from, signal, minBlock },
133
152
  own: own as Pick<T, K>,
134
153
  }
135
154
  }
@@ -145,12 +164,29 @@ function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
145
164
  * await getBalances(user, tokens, { provider: 'https://bsc-dataseed.bnbchain.org' })
146
165
  * await getBalances(user, tokens, { chainId: 56, provider: [window.ethereum, 'https://…'] }) // 钱包优先,失败用公共节点
147
166
  * await getBalances(user, tokens, { chainId: 56, symbol: true }) // 同时返回 symbol
167
+ * await getBalances(user, tokens, { chainId: 56, decimals: false }) // 只查余额,不带 decimals / formatted
148
168
  * // [{ token: NATIVE_TOKEN, native: true, balance: '1500000000000000000', decimals: 18, formatted: '1.5', success: true }, ...]
149
169
  * ```
150
170
  */
151
- export function getBalances(owner: string, tokens: readonly BalanceToken[], options: GetBalancesOptions = {}): Promise<TokenBalance[]> {
152
- const { provider, overrides, own } = resolve(options, ['symbol'])
153
- return provider.balances(owner, tokens, { ...overrides, symbol: own.symbol })
171
+ export function getBalances(owner: string, tokens: readonly BalanceToken[], options: GetBalancesOptions & { decimals: false }): Promise<RawTokenBalance[]>
172
+ export function getBalances(owner: string, tokens: readonly BalanceToken[], options?: GetBalancesOptions): Promise<TokenBalance[]>
173
+ export function getBalances(owner: string, tokens: readonly BalanceToken[], options: GetBalancesOptions = {}): Promise<Array<TokenBalance | RawTokenBalance>> {
174
+ const { provider, overrides, own } = resolve(options, ['symbol', 'decimals', 'withBlock'])
175
+ return provider.balances(owner, tokens, { ...overrides, ...own })
176
+ }
177
+
178
+ /**
179
+ * 多个钱包一次查(同一条链上合成一次 multicall),结果与 queries 一一对应。节点参数与 getBalances 相同。
180
+ *
181
+ * ```ts
182
+ * const [a, b] = await getMultiBalances([{ owner: walletA, tokens: [NATIVE_TOKEN, USDT] }, { owner: walletB, tokens: [USDT] }], { chainId: 56 })
183
+ * ```
184
+ */
185
+ export function getMultiBalances(queries: readonly BalancesQuery[], options: GetBalancesOptions & { decimals: false }): Promise<RawTokenBalance[][]>
186
+ export function getMultiBalances(queries: readonly BalancesQuery[], options?: GetBalancesOptions): Promise<TokenBalance[][]>
187
+ export function getMultiBalances(queries: readonly BalancesQuery[], options: GetBalancesOptions = {}): Promise<Array<Array<TokenBalance | RawTokenBalance>>> {
188
+ const { provider, overrides, own } = resolve(options, ['symbol', 'decimals', 'withBlock'])
189
+ return provider.multiBalances(queries, { ...overrides, ...own })
154
190
  }
155
191
 
156
192
  /**
@@ -168,8 +204,8 @@ export function getTokens<const F extends TokenField = DefaultTokenField>(
168
204
  tokens: readonly string[],
169
205
  options: GetTokensOptions<F> = {},
170
206
  ): Promise<TokenDetails<F>[]> {
171
- const { provider, overrides, own } = resolve(options, ['fields'])
172
- return provider.tokens<F>(tokens, { ...overrides, fields: own.fields })
207
+ const { provider, overrides, own } = resolve(options, ['fields', 'withBlock'])
208
+ return provider.tokens<F>(tokens, { ...overrides, ...own })
173
209
  }
174
210
 
175
211
  /**
@@ -184,87 +220,17 @@ export function getAllowances(
184
220
  owner: string,
185
221
  spender: string,
186
222
  tokens: readonly BalanceToken[],
187
- options: ShortcutOptions = {},
188
- ): Promise<TokenAllowance[]> {
189
- const { provider, overrides } = resolve(options)
190
- return provider.allowances(owner, spender, tokens, overrides)
191
- }
192
-
193
- export interface GetNftCollectionsOptions<F extends NftCollectionField = DefaultNftCollectionField>
194
- extends NftCollectionsOptions<F>,
195
- ShortcutProviderOptions {}
196
-
197
- /**
198
- * 批量查 NFT 集合信息,字段可选(默认 standard / name / symbol)。
199
- *
200
- * ```ts
201
- * await getNftCollections([BAYC, MAYC], { chainId: 1, fields: ['standard', 'name', 'totalSupply'] })
202
- * // [{ address: BAYC, standard: 'ERC721', name: 'BoredApeYachtClub', totalSupply: '10000', success: true }, ...]
203
- * ```
204
- */
205
- export function getNftCollections<const F extends NftCollectionField = DefaultNftCollectionField>(
206
- collections: readonly string[],
207
- options: GetNftCollectionsOptions<F> = {},
208
- ): Promise<NftCollection<F>[]> {
209
- const { provider, overrides, own } = resolve(options, ['fields'])
210
- return provider.nftCollections<F>(collections, { ...overrides, fields: own.fields })
211
- }
212
-
213
- /** 批量查 ERC721 持有数量:`await getNftBalances(user, [BAYC, MAYC], { chainId: 1 })` */
214
- export function getNftBalances(owner: string, collections: readonly string[], options: ShortcutOptions = {}): Promise<NftBalance[]> {
215
- const { provider, overrides } = resolve(options)
216
- return provider.nftBalances(owner, collections, overrides)
217
- }
218
-
219
- /** 批量查 ERC721 持有人:`await getNftOwners([{ contract: BAYC, tokenId: 1 }], { chainId: 1 })` */
220
- export function getNftOwners(items: readonly NftItem[], options: ShortcutOptions = {}): Promise<NftOwner[]> {
221
- const { provider, overrides } = resolve(options)
222
- return provider.nftOwners(items, overrides)
223
- }
224
-
225
- export interface GetNftTokenUrisOptions extends ShortcutOptions {
226
- /** 把 ipfs://xxx 转成 `${ipfsGateway}xxx`,如 'https://ipfs.io/ipfs/' */
227
- ipfsGateway?: string
228
- }
229
-
230
- /**
231
- * 批量查 NFT 元数据地址(ERC721 tokenURI / ERC1155 uri 自动兼容)。
232
- *
233
- * ```ts
234
- * await getNftTokenUris([{ contract: BAYC, tokenId: 1 }], { chainId: 1, ipfsGateway: 'https://ipfs.io/ipfs/' })
235
- * // [{ contract: BAYC, tokenId: '1', uri: 'https://ipfs.io/ipfs/Qm…/1', success: true }]
236
- * ```
237
- */
238
- export function getNftTokenUris(items: readonly NftItem[], options: GetNftTokenUrisOptions = {}): Promise<NftTokenUri[]> {
239
- const { provider, overrides, own } = resolve(options, ['ipfsGateway'])
240
- return provider.nftTokenUris(items, { ...overrides, ipfsGateway: own.ipfsGateway })
241
- }
242
-
243
- /** 批量查 ERC1155 余额:`await getErc1155Balances(user, [{ contract, tokenId: 1 }], { chainId: 137 })` */
244
- export function getErc1155Balances(owner: string, items: readonly NftItem[], options: ShortcutOptions = {}): Promise<Erc1155Balance[]> {
245
- const { provider, overrides } = resolve(options)
246
- return provider.erc1155Balances(owner, items, overrides)
247
- }
248
-
249
- export interface GetOwnerTokensOptions extends OwnerTokensOptions, ShortcutProviderOptions {}
250
-
251
- /**
252
- * 列出持有人拥有的代币(资产列表)。默认用免费公开代币列表发现代币,余额用 multicall 在链上核对。
253
- *
254
- * ```ts
255
- * await getOwnerTokens(user, { chainId: 56 }) // 免费,只能发现公开列表里的代币
256
- * await getOwnerTokens(user, { chainId: 1, prices: true, minUsd: 1 }) // 带美元价值,过滤零头和垃圾币
257
- * await getOwnerTokens(user, { chainId: 56, source: alchemy({ apiKey }) }) // 用 Alchemy 查全部历史持仓
258
- * ```
259
- *
260
- * @deprecated 不推荐使用:EVM 链上无法只靠节点可靠地列出地址持有的全部代币,这个方法只是尽力而为——
261
- * 默认来源只能发现公开列表里的代币,依赖第三方免费服务(可能限流、改格式或停止服务),要查几千个代币的余额、耗时数秒,
262
- * 价格也不一定可信;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束。
263
- * 已知要查哪些代币时请用 `getBalances`;需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。局限性详见 README「资产列表」。
264
- */
265
- export function getOwnerTokens(owner: string, options: GetOwnerTokensOptions = {}): Promise<OwnedToken[]> {
266
- const { provider, overrides, own } = resolve(options, ['source', 'prices', 'minUsd', 'includeNative', 'fetch', 'scanTransfers'])
267
- return provider.ownerTokens(owner, { ...overrides, ...own })
223
+ options: GetAllowancesOptions & { decimals: false },
224
+ ): Promise<RawTokenAllowance[]>
225
+ export function getAllowances(owner: string, spender: string, tokens: readonly BalanceToken[], options?: GetAllowancesOptions): Promise<TokenAllowance[]>
226
+ export function getAllowances(
227
+ owner: string,
228
+ spender: string,
229
+ tokens: readonly BalanceToken[],
230
+ options: GetAllowancesOptions = {},
231
+ ): Promise<Array<TokenAllowance | RawTokenAllowance>> {
232
+ const { provider, overrides, own } = resolve(options, ['decimals', 'withBlock'])
233
+ return provider.allowances(owner, spender, tokens, { ...overrides, ...own })
268
234
  }
269
235
 
270
236
  /** 测试用:按参数取(或创建)Provider,用于验证缓存复用(不在包的公开导出里) */
package/src/source.ts CHANGED
@@ -2,9 +2,9 @@ import { BrowserProvider, FetchRequest, JsonRpcProvider, makeError, type Eip1193
2
2
 
3
3
  import type { EthersLikeProvider } from './aggregate.js'
4
4
  import { canDetectChainId, detectChainId, detectChainIdCached } from './detect.js'
5
- import { FallbackRpc, type FallbackOptions } from './fallback.js'
5
+ import { FallbackRpc, type FallbackNodeInfo, type FallbackOptions } from './fallback.js'
6
6
  import { DEFAULT_RPC_URLS, DEFAULT_TRON_HOSTS } from './rpcNodes.js'
7
- import { TRON_CHAIN_ID, TronProvider, type TronProviderOptions, type TronWebLike } from './tron.js'
7
+ import { TRON_CHAIN_ID, TronProvider, isTronChain, type TronProviderOptions, type TronWebLike } from './tron.js'
8
8
 
9
9
  /**
10
10
  * 可传给 Provider 的底层连接(也可以传数组,按顺序作为主节点 + 备用节点):
@@ -22,11 +22,7 @@ export interface SourceOptions {
22
22
  tron?: Omit<TronProviderOptions, 'fullHost' | 'request'>
23
23
  }
24
24
 
25
- const TRON_CHAIN_IDS = new Set<number>(Object.values(TRON_CHAIN_ID))
26
-
27
- export function isTronChain(chainId: number): boolean {
28
- return TRON_CHAIN_IDS.has(chainId)
29
- }
25
+ export { isTronChain }
30
26
 
31
27
  /** 链的默认节点:EVM 用内置公共节点表,Tron 主网用内置全节点列表 */
32
28
  export function getDefaultRpcUrls(chainId: number): readonly string[] {
@@ -57,7 +53,38 @@ export function resolveSource(
57
53
  // 内置公共节点本来就是按 chainId 选出来的,不需要再校验
58
54
  return source === undefined ? node : withChainCheck(chainId, item, node, timeout)
59
55
  })
60
- return multiple ? new FallbackRpc(nodes, options.fallback) : (nodes[0] as EthersLikeProvider)
56
+ const info: FallbackNodeInfo[] = list.map((item) => ({ label: nodeLabel(item), key: item as string | object, chainId: Number.isFinite(chainId) ? chainId : undefined }))
57
+ // 单节点也经过 FallbackRpc(统一触发 onRequest 事件、抛原始错误)。外层超时只对单个 EVM URL 节点加(FetchRequest 本来就有同样的超时);
58
+ // 钱包等对象节点、Tron URL(TronProvider 有自己的超时和 429 退避、限流队列,外层超时会把排队 / 退避中的请求截断)不传 timeout 时不加,与原来一致
59
+ const singleEvmUrl = typeof list[0] === 'string' && !isTronChain(chainId)
60
+ const fallback = multiple || singleEvmUrl || options.fallback?.timeout !== undefined ? options.fallback : { ...options.fallback, timeout: 0 }
61
+ return new FallbackRpc(nodes, fallback, info)
62
+ }
63
+
64
+ /** 节点标识(错误信息、onRequest 事件用):URL 只取 host,不含 path / query(常带 API Key) */
65
+ export function nodeLabel(source: ProviderSource): string {
66
+ if (typeof source === 'string') {
67
+ try {
68
+ return new URL(source).host || 'url'
69
+ } catch {
70
+ return 'url'
71
+ }
72
+ }
73
+ const value = source as Partial<EthersLikeProvider & Eip1193Provider & TronWebLike>
74
+ if (value instanceof JsonRpcProvider) {
75
+ try {
76
+ return new URL(value._getConnection().url).host || 'provider'
77
+ } catch {
78
+ return 'provider'
79
+ }
80
+ }
81
+ if (value.fullNode) {
82
+ return 'tronWeb'
83
+ }
84
+ if (typeof value.call !== 'function' && typeof value.request === 'function') {
85
+ return 'wallet'
86
+ }
87
+ return 'provider'
61
88
  }
62
89
 
63
90
  /** 识别失败后多久内不再重试识别(期间请求照常发出,不做校验) */
@@ -0,0 +1,107 @@
1
+ /**
2
+ * NFT 查询(子路径 `@w3lib/evm-batch-call/nft`):ERC721 / ERC1155 的集合信息、持有数量、持有人、元数据地址。
3
+ * 不放在主入口,不用 NFT 功能时打包不会带上这部分代码。
4
+ *
5
+ * ```ts
6
+ * import { Provider } from '@w3lib/evm-batch-call'
7
+ * import { getNftOwners, nftOwners } from '@w3lib/evm-batch-call/nft'
8
+ *
9
+ * await getNftOwners([{ contract: BAYC, tokenId: 1 }], { chainId: 1 }) // 节点参数与 getBalances 相同
10
+ * await nftOwners(multi, [{ contract: BAYC, tokenId: 1 }]) // 或传已创建的 Provider
11
+ * ```
12
+ */
13
+ import {
14
+ erc1155Balances,
15
+ nftBalances,
16
+ nftCollections,
17
+ nftOwners,
18
+ nftTokenUris,
19
+ type DefaultNftCollectionField,
20
+ type Erc1155Balance,
21
+ type NftBalance,
22
+ type NftCollection,
23
+ type NftCollectionField,
24
+ type NftItem,
25
+ type NftOwner,
26
+ type NftTokenUri,
27
+ type NftCollectionsOptions,
28
+ } from '../nft.js'
29
+ import { resolve, type ShortcutOptions, type ShortcutProviderOptions } from '../shortcuts.js'
30
+
31
+ export {
32
+ DEFAULT_NFT_COLLECTION_FIELDS,
33
+ ERC1155_ABI,
34
+ ERC721_ABI,
35
+ erc1155Balances,
36
+ nftBalances,
37
+ nftCollections,
38
+ nftOwners,
39
+ nftTokenUris,
40
+ type DefaultNftCollectionField,
41
+ type Erc1155Balance,
42
+ type NftBalance,
43
+ type NftCollection,
44
+ type NftCollectionField,
45
+ type NftCollectionsOptions,
46
+ type NftItem,
47
+ type NftOwner,
48
+ type NftStandard,
49
+ type NftTokenUri,
50
+ type NftTokenUriOptions,
51
+ } from '../nft.js'
52
+
53
+ export interface GetNftCollectionsOptions<F extends NftCollectionField = DefaultNftCollectionField>
54
+ extends NftCollectionsOptions<F>,
55
+ ShortcutProviderOptions {}
56
+
57
+ /**
58
+ * 批量查 NFT 集合信息,字段可选(默认 standard / name / symbol)。
59
+ *
60
+ * ```ts
61
+ * await getNftCollections([BAYC, MAYC], { chainId: 1, fields: ['standard', 'name', 'totalSupply'] })
62
+ * // [{ address: BAYC, standard: 'ERC721', name: 'BoredApeYachtClub', totalSupply: '10000', success: true }, ...]
63
+ * ```
64
+ */
65
+ export function getNftCollections<const F extends NftCollectionField = DefaultNftCollectionField>(
66
+ collections: readonly string[],
67
+ options: GetNftCollectionsOptions<F> = {},
68
+ ): Promise<NftCollection<F>[]> {
69
+ const { provider, overrides, own } = resolve(options, ['fields'])
70
+ return nftCollections<F>(provider, collections, { ...overrides, fields: own.fields })
71
+ }
72
+
73
+ /** 批量查 ERC721 持有数量:`await getNftBalances(user, [BAYC, MAYC], { chainId: 1 })` */
74
+ export function getNftBalances(owner: string, collections: readonly string[], options: ShortcutOptions = {}): Promise<NftBalance[]> {
75
+ const { provider, overrides } = resolve(options)
76
+ return nftBalances(provider, owner, collections, overrides)
77
+ }
78
+
79
+ /** 批量查 ERC721 持有人:`await getNftOwners([{ contract: BAYC, tokenId: 1 }], { chainId: 1 })` */
80
+ export function getNftOwners(items: readonly NftItem[], options: ShortcutOptions = {}): Promise<NftOwner[]> {
81
+ const { provider, overrides } = resolve(options)
82
+ return nftOwners(provider, items, overrides)
83
+ }
84
+
85
+ export interface GetNftTokenUrisOptions extends ShortcutOptions {
86
+ /** 把 ipfs://xxx 转成 `${ipfsGateway}xxx`,如 'https://ipfs.io/ipfs/' */
87
+ ipfsGateway?: string
88
+ }
89
+
90
+ /**
91
+ * 批量查 NFT 元数据地址(ERC721 tokenURI / ERC1155 uri 自动兼容)。
92
+ *
93
+ * ```ts
94
+ * await getNftTokenUris([{ contract: BAYC, tokenId: 1 }], { chainId: 1, ipfsGateway: 'https://ipfs.io/ipfs/' })
95
+ * // [{ contract: BAYC, tokenId: '1', uri: 'https://ipfs.io/ipfs/Qm…/1', success: true }]
96
+ * ```
97
+ */
98
+ export function getNftTokenUris(items: readonly NftItem[], options: GetNftTokenUrisOptions = {}): Promise<NftTokenUri[]> {
99
+ const { provider, overrides, own } = resolve(options, ['ipfsGateway'])
100
+ return nftTokenUris(provider, items, { ...overrides, ipfsGateway: own.ipfsGateway })
101
+ }
102
+
103
+ /** 批量查 ERC1155 余额:`await getErc1155Balances(user, [{ contract, tokenId: 1 }], { chainId: 137 })` */
104
+ export function getErc1155Balances(owner: string, items: readonly NftItem[], options: ShortcutOptions = {}): Promise<Erc1155Balance[]> {
105
+ const { provider, overrides } = resolve(options)
106
+ return erc1155Balances(provider, owner, items, overrides)
107
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 资产列表(子路径 `@w3lib/evm-batch-call/owner`):从代币来源发现候选代币,multicall 核对余额;含代币来源、价格源、
3
+ * Transfer 事件增量扫描。不放在主入口,不用时打包不会带上这部分代码。
4
+ *
5
+ * ```ts
6
+ * import { getOwnerTokens, ownerTokens, alchemy } from '@w3lib/evm-batch-call/owner'
7
+ *
8
+ * await getOwnerTokens(user, { chainId: 56 })
9
+ * await ownerTokens(multi, user, { source: alchemy({ apiKey }) }) // 或传已创建的 Provider
10
+ * ```
11
+ */
12
+ import { ownerTokens, type OwnedToken, type OwnerTokensOptions } from '../owner.js'
13
+ import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
14
+
15
+ export {
16
+ ALCHEMY_NETWORKS,
17
+ COINGECKO_PLATFORMS,
18
+ DEFILLAMA_CHAINS,
19
+ alchemy,
20
+ clearTokenListCache,
21
+ coingeckoTokenList,
22
+ combine,
23
+ defaultTokenSource,
24
+ defillamaPrices,
25
+ firstAvailable,
26
+ metamaskTokenList,
27
+ nodereal,
28
+ ownerTokens,
29
+ staticTokens,
30
+ tokenList,
31
+ type DiscoveredToken,
32
+ type IndexerSourceOptions,
33
+ type OwnedToken,
34
+ type OwnerTokensOptions,
35
+ type PriceSource,
36
+ type PriceSourceContext,
37
+ type TokenSource,
38
+ type TokenSourceContext,
39
+ } from '../owner.js'
40
+ export {
41
+ getTransferScanState,
42
+ keyValueScanStorage,
43
+ memoryScanStorage,
44
+ transferScan,
45
+ type TransferScanOptions,
46
+ type TransferScanState,
47
+ type TransferScanStorage,
48
+ } from '../scan.js'
49
+
50
+ export interface GetOwnerTokensOptions extends OwnerTokensOptions, ShortcutProviderOptions {}
51
+
52
+ /**
53
+ * 列出持有人拥有的代币(资产列表)。默认用免费公开代币列表发现代币,余额用 multicall 在链上核对。
54
+ *
55
+ * ```ts
56
+ * await getOwnerTokens(user, { chainId: 56 }) // 免费,只能发现公开列表里的代币
57
+ * await getOwnerTokens(user, { chainId: 1, prices: true, minUsd: 1 }) // 带美元价值,过滤零头和垃圾币
58
+ * await getOwnerTokens(user, { chainId: 56, source: alchemy({ apiKey }) }) // 用 Alchemy 查全部历史持仓
59
+ * ```
60
+ *
61
+ * @deprecated 不推荐使用:EVM 链上无法只靠节点可靠地列出地址持有的全部代币,这个方法只是尽力而为——
62
+ * 默认来源只能发现公开列表里的代币,依赖第三方免费服务(可能限流、改格式或停止服务),要查几千个代币的余额、耗时数秒,
63
+ * 价格也不一定可信;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束。
64
+ * 已知要查哪些代币时请用 `getBalances`;需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。局限性详见 README「资产列表」。
65
+ */
66
+ export function getOwnerTokens(owner: string, options: GetOwnerTokensOptions = {}): Promise<OwnedToken[]> {
67
+ const { provider, overrides, own } = resolve(options, ['source', 'prices', 'minUsd', 'includeNative', 'fetch', 'scanTransfers'])
68
+ return ownerTokens(provider, owner, { ...overrides, ...own })
69
+ }
package/src/tron.ts CHANGED
@@ -19,6 +19,13 @@ export const TRON_CHAIN_ID = {
19
19
  nile: 3448148188,
20
20
  } as const
21
21
 
22
+ const TRON_CHAIN_IDS = new Set<number>(Object.values(TRON_CHAIN_ID))
23
+
24
+ /** 是否是 Tron 链(主网 / 测试网):只能查最新状态,地址区分大小写 */
25
+ export function isTronChain(chainId: number): boolean {
26
+ return TRON_CHAIN_IDS.has(chainId)
27
+ }
28
+
22
29
  const TRON_ADDRESS_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/
23
30
 
24
31
  export function isTronAddress(value: unknown): boolean {
package/src/util.ts CHANGED
@@ -18,3 +18,30 @@ export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
18
18
  }),
19
19
  ]).finally(() => clearTimeout(timer))
20
20
  }
21
+
22
+ /**
23
+ * 支持 AbortSignal:已取消时直接 reject(不执行 work,不发请求);执行中取消时立即 reject(reason 为 signal.reason),
24
+ * 底层请求不中断(可能与其他调用合并在同一个请求里),结果丢弃
25
+ */
26
+ export function withSignal<T>(signal: AbortSignal | undefined, work: () => Promise<T>): Promise<T> {
27
+ if (!signal) {
28
+ return work()
29
+ }
30
+ if (signal.aborted) {
31
+ return Promise.reject(signal.reason)
32
+ }
33
+ return new Promise<T>((resolve, reject) => {
34
+ const onAbort = () => reject(signal.reason)
35
+ signal.addEventListener('abort', onAbort, { once: true })
36
+ work().then(
37
+ (value) => {
38
+ signal.removeEventListener('abort', onAbort)
39
+ resolve(value)
40
+ },
41
+ (err: unknown) => {
42
+ signal.removeEventListener('abort', onAbort)
43
+ reject(err)
44
+ },
45
+ )
46
+ })
47
+ }