@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.
- package/CHANGELOG.md +45 -0
- package/README.md +98 -35
- package/contracts/DeploylessMulticall3.sol +5 -0
- package/dist/aggregate.d.ts +20 -1
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +143 -5
- package/dist/aggregate.js.map +1 -1
- package/dist/batcher.d.ts +6 -1
- package/dist/batcher.d.ts.map +1 -1
- package/dist/batcher.js +59 -9
- package/dist/batcher.js.map +1 -1
- package/dist/call.d.ts +27 -2
- package/dist/call.d.ts.map +1 -1
- package/dist/call.js +22 -0
- package/dist/call.js.map +1 -1
- package/dist/chains.d.ts +7 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -0
- package/dist/chains.js.map +1 -1
- package/dist/deployless.d.ts +1 -1
- package/dist/deployless.d.ts.map +1 -1
- package/dist/deployless.js +1 -1
- package/dist/deployless.js.map +1 -1
- package/dist/erc20.d.ts +70 -4
- package/dist/erc20.d.ts.map +1 -1
- package/dist/erc20.js +121 -5
- package/dist/erc20.js.map +1 -1
- package/dist/events.d.ts +25 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +25 -0
- package/dist/events.js.map +1 -0
- package/dist/fallback.d.ts +31 -3
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +165 -27
- package/dist/fallback.js.map +1 -1
- package/dist/index.d.ts +9 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/multicall.d.ts.map +1 -1
- package/dist/multicall.js +33 -2
- package/dist/multicall.js.map +1 -1
- package/dist/nft.d.ts +5 -0
- package/dist/nft.d.ts.map +1 -1
- package/dist/nft.js +5 -0
- package/dist/nft.js.map +1 -1
- package/dist/owner.d.ts +9 -0
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +11 -2
- package/dist/owner.js.map +1 -1
- package/dist/provider.d.ts +55 -29
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +140 -111
- package/dist/provider.js.map +1 -1
- package/dist/shortcuts.d.ts +34 -53
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +41 -91
- package/dist/shortcuts.js.map +1 -1
- package/dist/source.d.ts +4 -2
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +35 -6
- package/dist/source.js.map +1 -1
- package/dist/subpaths/nft.d.ts +46 -0
- package/dist/subpaths/nft.d.ts.map +1 -0
- package/dist/subpaths/nft.js +55 -0
- package/dist/subpaths/nft.js.map +1 -0
- package/dist/subpaths/owner.d.ts +33 -0
- package/dist/subpaths/owner.d.ts.map +1 -0
- package/dist/subpaths/owner.js +34 -0
- package/dist/subpaths/owner.js.map +1 -0
- package/dist/tron.d.ts +2 -0
- package/dist/tron.d.ts.map +1 -1
- package/dist/tron.js +5 -0
- package/dist/tron.js.map +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +23 -0
- package/dist/util.js.map +1 -1
- package/dist/watch.d.ts +25 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +105 -0
- package/dist/watch.js.map +1 -0
- package/package.json +19 -10
- package/src/aggregate.ts +172 -7
- package/src/batcher.ts +69 -9
- package/src/call.ts +42 -2
- package/src/chains.ts +15 -0
- package/src/deployless.ts +1 -1
- package/src/erc20.ts +171 -8
- package/src/events.ts +42 -0
- package/src/fallback.ts +193 -26
- package/src/index.ts +19 -61
- package/src/multicall.ts +34 -2
- package/src/nft.ts +5 -0
- package/src/owner.ts +11 -2
- package/src/provider.ts +225 -124
- package/src/shortcuts.ts +76 -110
- package/src/source.ts +35 -8
- package/src/subpaths/nft.ts +107 -0
- package/src/subpaths/owner.ts +69 -0
- package/src/tron.ts +7 -0
- package/src/util.ts +27 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
|
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
|
|
291
|
-
const fetchedDecimals =
|
|
292
|
-
const fetchedSymbol =
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
347
|
-
const fetched =
|
|
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 (
|
|
353
|
-
|
|
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:
|
|
439
|
+
allowance: settled.value.toString(),
|
|
360
440
|
decimals,
|
|
361
|
-
formatted:
|
|
362
|
-
unlimited:
|
|
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
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
* @deprecated 不推荐使用:EVM 链上无法只靠节点可靠地列出地址持有的全部代币,这个方法只是尽力而为——
|
|
373
|
-
* 默认来源只能发现公开列表里的代币,依赖第三方免费服务(可能限流、改格式或停止服务),要查几千个代币的余额、耗时数秒,
|
|
374
|
-
* 价格也不一定可信;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束。
|
|
375
|
-
* 已知要查哪些代币时请用 `getBalances`;需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。局限性详见 README「资产列表」。
|
|
450
|
+
* balances / tokens / allowances 的子调用进自动合并队列:与同一时刻其他组件发起的查询合并成一次 multicall。
|
|
451
|
+
* 逐条返回值或失败原因;节点错误照常抛出
|
|
376
452
|
*/
|
|
377
|
-
|
|
378
|
-
return
|
|
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
|
-
/**
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
472
|
-
const
|
|
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
|
|
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
|
-
|
|
569
|
+
failedField ??= field
|
|
501
570
|
}
|
|
502
571
|
}
|
|
503
572
|
if (wanted.has('totalSupply')) {
|
|
504
|
-
out.totalSupplyFormatted = totalSupply === null || decimals === null ? null :
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|