@w3lib/evm-batch-call 0.1.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/LICENSE +24 -0
- package/README.md +257 -0
- package/contracts/DeploylessMulticall3.sol +66 -0
- package/dist/aggregate.d.ts +27 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +198 -0
- package/dist/aggregate.js.map +1 -0
- package/dist/batcher.d.ts +21 -0
- package/dist/batcher.d.ts.map +1 -0
- package/dist/batcher.js +100 -0
- package/dist/batcher.js.map +1 -0
- package/dist/call.d.ts +53 -0
- package/dist/call.d.ts.map +1 -0
- package/dist/call.js +82 -0
- package/dist/call.js.map +1 -0
- package/dist/contract.d.ts +45 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +107 -0
- package/dist/contract.js.map +1 -0
- package/dist/deployless.d.ts +3 -0
- package/dist/deployless.d.ts.map +1 -0
- package/dist/deployless.js +4 -0
- package/dist/deployless.js.map +1 -0
- package/dist/erc20.d.ts +27 -0
- package/dist/erc20.d.ts.map +1 -0
- package/dist/erc20.js +17 -0
- package/dist/erc20.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -0
- package/dist/fallback.d.ts +23 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +86 -0
- package/dist/fallback.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/multicall.d.ts +13 -0
- package/dist/multicall.d.ts.map +1 -0
- package/dist/multicall.js +51 -0
- package/dist/multicall.js.map +1 -0
- package/dist/provider.d.ts +137 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +269 -0
- package/dist/provider.js.map +1 -0
- package/dist/rpcNodes.d.ts +5 -0
- package/dist/rpcNodes.d.ts.map +1 -0
- package/dist/rpcNodes.js +193 -0
- package/dist/rpcNodes.js.map +1 -0
- package/dist/source.d.ts +23 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +56 -0
- package/dist/source.js.map +1 -0
- package/dist/tron.d.ts +62 -0
- package/dist/tron.d.ts.map +1 -0
- package/dist/tron.js +223 -0
- package/dist/tron.js.map +1 -0
- package/package.json +63 -0
- package/src/aggregate.ts +254 -0
- package/src/batcher.ts +126 -0
- package/src/call.ts +144 -0
- package/src/contract.ts +145 -0
- package/src/deployless.ts +5 -0
- package/src/erc20.ts +43 -0
- package/src/errors.ts +66 -0
- package/src/fallback.ts +105 -0
- package/src/index.ts +31 -0
- package/src/multicall.ts +60 -0
- package/src/provider.ts +352 -0
- package/src/rpcNodes.ts +194 -0
- package/src/source.ts +84 -0
- package/src/tron.ts +281 -0
package/src/erc20.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BoundCall } from './call.js'
|
|
2
|
+
import type { BoundMethod, Contract } from './contract.js'
|
|
3
|
+
|
|
4
|
+
export const ERC20_ABI = [
|
|
5
|
+
'function name() view returns (string)',
|
|
6
|
+
'function symbol() view returns (string)',
|
|
7
|
+
'function decimals() view returns (uint8)',
|
|
8
|
+
'function totalSupply() view returns (uint256)',
|
|
9
|
+
'function balanceOf(address owner) view returns (uint256)',
|
|
10
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
11
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
12
|
+
'function transfer(address to, uint256 amount) returns (bool)',
|
|
13
|
+
'function transferFrom(address from, address to, uint256 amount) returns (bool)',
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
type Method<Args extends unknown[], T> = ((...params: Args) => BoundCall<T>) & Pick<BoundMethod<T>, 'staticCall'>
|
|
17
|
+
|
|
18
|
+
/** `provider.erc20(address)` 返回的绑定合约(带结果类型) */
|
|
19
|
+
export interface Erc20Contract extends Contract {
|
|
20
|
+
name: Method<[], string>
|
|
21
|
+
symbol: Method<[], string>
|
|
22
|
+
decimals: Method<[], bigint>
|
|
23
|
+
totalSupply: Method<[], bigint>
|
|
24
|
+
balanceOf: Method<[owner: string], bigint>
|
|
25
|
+
allowance: Method<[owner: string, spender: string], bigint>
|
|
26
|
+
approve: Method<[spender: string, amount: bigint | string | number], boolean>
|
|
27
|
+
transfer: Method<[to: string, amount: bigint | string | number], boolean>
|
|
28
|
+
transferFrom: Method<[from: string, to: string, amount: bigint | string | number], boolean>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TokenInfo {
|
|
32
|
+
address: string
|
|
33
|
+
/** 读取失败(如 symbol 是 bytes32 的老代币)时为 null */
|
|
34
|
+
symbol: string | null
|
|
35
|
+
name: string | null
|
|
36
|
+
decimals: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 常见的主币占位地址:多数聚合器用 0xeeee…eeee,部分协议用零地址 */
|
|
40
|
+
export const DEFAULT_NATIVE_TOKENS: readonly string[] = [
|
|
41
|
+
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
|
42
|
+
'0x0000000000000000000000000000000000000000',
|
|
43
|
+
]
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Interface, isError } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import type { Call } from './call.js'
|
|
4
|
+
|
|
5
|
+
const errorInterface = new Interface(['error Error(string)', 'error Panic(uint256)'])
|
|
6
|
+
|
|
7
|
+
/** 自动合并模式下,单条调用失败时 reject 的错误 */
|
|
8
|
+
export class CallFailedError extends Error {
|
|
9
|
+
readonly call: Call
|
|
10
|
+
/** revert 原始数据 */
|
|
11
|
+
readonly returnData: string
|
|
12
|
+
/** 能解析出来的 revert 原因(Error(string) / Panic(uint256)) */
|
|
13
|
+
readonly reason: string | null
|
|
14
|
+
|
|
15
|
+
constructor(call: Call, returnData: string, cause?: unknown) {
|
|
16
|
+
const reason = decodeRevertReason(returnData)
|
|
17
|
+
super(
|
|
18
|
+
`${call.name} call to ${call.contract.address} failed${reason ? `: ${reason}` : ''}`,
|
|
19
|
+
cause === undefined ? undefined : { cause },
|
|
20
|
+
)
|
|
21
|
+
this.name = 'CallFailedError'
|
|
22
|
+
this.call = call
|
|
23
|
+
this.returnData = returnData
|
|
24
|
+
this.reason = reason
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 最后一项是 TronProvider 对交易校验失败(CONTRACT_VALIDATE_ERROR 等)抛出的错误
|
|
29
|
+
const EXECUTION_ERROR_RE =
|
|
30
|
+
/revert|max (init)?code size|out of gas|invalid opcode|stack (underflow|overflow)|invalid jump|^CONTRACT_[A-Z_]+_ERROR:/i
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* eth_call 的失败是否是 EVM 执行层面的确定性结果(revert、代码大小超限等),而不是节点/网络问题。
|
|
34
|
+
*
|
|
35
|
+
* ethers 会把 eth_call 返回的**任何** JSON-RPC 错误都包装成 CALL_EXCEPTION("missing revert data"),
|
|
36
|
+
* 包括限流(-32005)、header not found、非归档节点缺状态等,所以不能只看错误码:
|
|
37
|
+
* 有 revert 数据,或节点原始错误信息是执行错误,才算确定性失败;其余换节点重试可能会成功。
|
|
38
|
+
*/
|
|
39
|
+
export function isExecutionError(err: unknown): boolean {
|
|
40
|
+
if (!isError(err, 'CALL_EXCEPTION')) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
if (err.data && err.data !== '0x') {
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
const info = err.info as { error?: { message?: unknown } } | undefined
|
|
47
|
+
const raw = info?.error?.message
|
|
48
|
+
// 没有节点原始信息时(如 TronProvider 自己抛的)看 shortMessage;ethers 的 "missing revert data" 不算
|
|
49
|
+
const message = typeof raw === 'string' ? raw : err.shortMessage === 'missing revert data' ? '' : err.shortMessage
|
|
50
|
+
return EXECUTION_ERROR_RE.test(message ?? '')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decodeRevertReason(data: string): string | null {
|
|
54
|
+
if (!data || data === '0x') {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = errorInterface.parseError(data)
|
|
59
|
+
if (!parsed) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
return parsed.name === 'Panic' ? `Panic(${parsed.args[0]})` : String(parsed.args[0])
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/fallback.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isError, type TransactionRequest } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import type { EthersLikeProvider } from './aggregate.js'
|
|
4
|
+
import { isExecutionError } from './errors.js'
|
|
5
|
+
|
|
6
|
+
export interface FallbackOptions {
|
|
7
|
+
/** 单个节点单次请求超时(毫秒),超时视为该节点故障并换下一个。默认 10000;<= 0 表示不限制 */
|
|
8
|
+
timeout?: number
|
|
9
|
+
/** 节点出错后跳过它的时长(毫秒),期间优先用其他节点。默认 30000 */
|
|
10
|
+
cooldown?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 多节点故障切换:按顺序使用节点,出错或超时换下一个;出错的节点在 cooldown 内排到最后。
|
|
15
|
+
*
|
|
16
|
+
* 与 ethers FallbackProvider 相比:
|
|
17
|
+
* - 没有首次请求前对所有节点的 getBlockNumber 同步,一个卡住的节点不会拖住第一次查询
|
|
18
|
+
* - 合约 revert 等确定性结果直接返回,不会拿去别的节点重试
|
|
19
|
+
*/
|
|
20
|
+
export class FallbackRpc implements EthersLikeProvider {
|
|
21
|
+
readonly #nodes: EthersLikeProvider[]
|
|
22
|
+
readonly #timeout: number
|
|
23
|
+
readonly #cooldown: number
|
|
24
|
+
readonly #failedAt: number[]
|
|
25
|
+
|
|
26
|
+
constructor(nodes: EthersLikeProvider[], options: FallbackOptions = {}) {
|
|
27
|
+
if (!nodes.length) {
|
|
28
|
+
throw new Error('FallbackRpc requires at least one node')
|
|
29
|
+
}
|
|
30
|
+
this.#nodes = nodes
|
|
31
|
+
this.#timeout = options.timeout ?? 10_000
|
|
32
|
+
this.#cooldown = options.cooldown ?? 30_000
|
|
33
|
+
this.#failedAt = nodes.map(() => 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get nodes(): readonly EthersLikeProvider[] {
|
|
37
|
+
return this.#nodes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
call(tx: TransactionRequest): Promise<string> {
|
|
41
|
+
return this.#run((node) => node.call(tx))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getBalance(address: Parameters<EthersLikeProvider['getBalance']>[0], blockTag?: Parameters<EthersLikeProvider['getBalance']>[1]): Promise<bigint> {
|
|
45
|
+
return this.#run((node) => node.getBalance(address, blockTag))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async #run<T>(fn: (node: EthersLikeProvider) => Promise<T>): Promise<T> {
|
|
49
|
+
let lastError: unknown
|
|
50
|
+
for (const index of this.#order()) {
|
|
51
|
+
try {
|
|
52
|
+
const result = await withTimeout(fn(this.#nodes[index] as EthersLikeProvider), this.#timeout)
|
|
53
|
+
this.#failedAt[index] = 0
|
|
54
|
+
return result
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (isDeterministic(err)) {
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
this.#failedAt[index] = Date.now()
|
|
60
|
+
lastError = err
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw lastError
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 健康的节点按原顺序在前,冷却中的按出错时间先后排在后面(全挂时也都会再试一次) */
|
|
67
|
+
#order(): number[] {
|
|
68
|
+
const now = Date.now()
|
|
69
|
+
const healthy: number[] = []
|
|
70
|
+
const cooling: number[] = []
|
|
71
|
+
this.#failedAt.forEach((at, i) => (at && now - at < this.#cooldown ? cooling : healthy).push(i))
|
|
72
|
+
cooling.sort((a, b) => (this.#failedAt[a] as number) - (this.#failedAt[b] as number))
|
|
73
|
+
return [...healthy, ...cooling]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 换节点也不会变的错误:合约执行结果、参数错误、用户拒绝等 */
|
|
78
|
+
function isDeterministic(err: unknown): boolean {
|
|
79
|
+
return (
|
|
80
|
+
isExecutionError(err) ||
|
|
81
|
+
isError(err, 'INVALID_ARGUMENT') ||
|
|
82
|
+
isError(err, 'ACTION_REJECTED') ||
|
|
83
|
+
isError(err, 'NUMERIC_FAULT')
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class TimeoutError extends Error {
|
|
88
|
+
constructor(ms: number) {
|
|
89
|
+
super(`RPC request timed out after ${ms}ms`)
|
|
90
|
+
this.name = 'TimeoutError'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
95
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
96
|
+
return promise
|
|
97
|
+
}
|
|
98
|
+
let timer: ReturnType<typeof setTimeout>
|
|
99
|
+
return Promise.race([
|
|
100
|
+
promise,
|
|
101
|
+
new Promise<never>((_, reject) => {
|
|
102
|
+
timer = setTimeout(() => reject(new TimeoutError(ms)), ms)
|
|
103
|
+
}),
|
|
104
|
+
]).finally(() => clearTimeout(timer))
|
|
105
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { Contract, bindCall, type BoundMethod, type ContractAbi, type ContractRunner } from './contract.js'
|
|
2
|
+
export { DEFAULT_NATIVE_TOKENS, ERC20_ABI, type Erc20Contract, type TokenInfo } from './erc20.js'
|
|
3
|
+
export {
|
|
4
|
+
Provider,
|
|
5
|
+
type ProviderConfig,
|
|
6
|
+
type CallInput,
|
|
7
|
+
type CallResults,
|
|
8
|
+
type ProviderSource,
|
|
9
|
+
type TryCallResults,
|
|
10
|
+
type StaticCallItem,
|
|
11
|
+
type StaticCallOverrides,
|
|
12
|
+
type StaticCallResult,
|
|
13
|
+
} from './provider.js'
|
|
14
|
+
export {
|
|
15
|
+
TRON_CHAIN_ID,
|
|
16
|
+
TronProvider,
|
|
17
|
+
isTronAddress,
|
|
18
|
+
toEvmAddress,
|
|
19
|
+
toTronAddress,
|
|
20
|
+
type TronProviderOptions,
|
|
21
|
+
type TronRequest,
|
|
22
|
+
type TronWebLike,
|
|
23
|
+
} from './tron.js'
|
|
24
|
+
export { CallFailedError, decodeRevertReason, isExecutionError } from './errors.js'
|
|
25
|
+
export { FallbackRpc, type FallbackOptions } from './fallback.js'
|
|
26
|
+
export { getDefaultRpcUrls, isTronChain, type SourceOptions } from './source.js'
|
|
27
|
+
export { DEFAULT_RPC_URLS, DEFAULT_TRON_HOSTS } from './rpcNodes.js'
|
|
28
|
+
export { MULTICALL3_ADDRESS, getMulticall3, type Multicall } from './multicall.js'
|
|
29
|
+
export type { BlockTag, CallOverrides, EthersLikeProvider } from './aggregate.js'
|
|
30
|
+
export type { BatchOptions } from './batcher.js'
|
|
31
|
+
export type { BoundCall, Call, FailableCall, Params, RawResult } from './call.js'
|
package/src/multicall.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multicall3 部署地址表,来自 ethcall (MIT, https://github.com/Destiner/ethcall),另补充了 Tron。
|
|
3
|
+
* 表里没有的链(或地址在该链上无效)会自动走 deployless(见 contracts/DeploylessMulticall3.sol),
|
|
4
|
+
* 不需要链上部署任何合约。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface Multicall {
|
|
8
|
+
address: string
|
|
9
|
+
/** 合约部署所在区块;查询更早的区块时改走 deployless */
|
|
10
|
+
block: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11'
|
|
14
|
+
|
|
15
|
+
const CANONICAL = MULTICALL3_ADDRESS
|
|
16
|
+
|
|
17
|
+
const MULTICALL3: Record<number, Multicall> = {
|
|
18
|
+
1: { address: CANONICAL, block: 14353601 },
|
|
19
|
+
3: { address: CANONICAL, block: 12063863 },
|
|
20
|
+
4: { address: CANONICAL, block: 10299530 },
|
|
21
|
+
5: { address: CANONICAL, block: 6507670 },
|
|
22
|
+
10: { address: CANONICAL, block: 4286263 },
|
|
23
|
+
14: { address: CANONICAL, block: 3002461 },
|
|
24
|
+
16: { address: CANONICAL, block: 276388 },
|
|
25
|
+
19: { address: CANONICAL, block: 13382504 },
|
|
26
|
+
42: { address: CANONICAL, block: 30285908 },
|
|
27
|
+
56: { address: CANONICAL, block: 15921452 },
|
|
28
|
+
69: { address: CANONICAL, block: 1418387 },
|
|
29
|
+
97: { address: CANONICAL, block: 17422483 },
|
|
30
|
+
100: { address: CANONICAL, block: 21022491 },
|
|
31
|
+
114: { address: CANONICAL, block: 508735 },
|
|
32
|
+
137: { address: CANONICAL, block: 25770160 },
|
|
33
|
+
196: { address: CANONICAL, block: 47416 },
|
|
34
|
+
250: { address: CANONICAL, block: 33001987 },
|
|
35
|
+
252: { address: CANONICAL, block: 0 },
|
|
36
|
+
324: { address: '0x413fEb613604D46586c22801949A5b88b224c260', block: 9531414 },
|
|
37
|
+
420: { address: CANONICAL, block: 49461 },
|
|
38
|
+
1284: { address: CANONICAL, block: 609002 },
|
|
39
|
+
1285: { address: CANONICAL, block: 1597904 },
|
|
40
|
+
1287: { address: CANONICAL, block: 1850686 },
|
|
41
|
+
2222: { address: '0x1578f6d2D3168acF41b506AA666A521994F6BAB6', block: 1176602 },
|
|
42
|
+
4002: { address: CANONICAL, block: 8328688 },
|
|
43
|
+
8453: { address: CANONICAL, block: 5022 },
|
|
44
|
+
42161: { address: CANONICAL, block: 7654707 },
|
|
45
|
+
42220: { address: CANONICAL, block: 13112599 },
|
|
46
|
+
43113: { address: CANONICAL, block: 7096959 },
|
|
47
|
+
43114: { address: CANONICAL, block: 11907934 },
|
|
48
|
+
80001: { address: CANONICAL, block: 25444704 },
|
|
49
|
+
421611: { address: CANONICAL, block: 10228837 },
|
|
50
|
+
5000: { address: '0xd77b59d4cb13bea71f3cc093e401720867355d6e', block: 18877383 },
|
|
51
|
+
1666600000: { address: CANONICAL, block: 24185753 },
|
|
52
|
+
73927: { address: '0x138A85647768815078DF1dD85C6121e611381A0b', block: 14080843 },
|
|
53
|
+
// Tron 主网 TEazPvZwDjDtFeJupyo7QunvnrnUjPH8ED(链上合约名 Multicall3,2026-10 实测)
|
|
54
|
+
728126428: { address: '0x32a4f47a74a6810bd0bf861cabab99656a75de9e', block: 0 },
|
|
55
|
+
81457: { address: '0x23928c6f823e0d78eadc9d2f69ace652ff3cdcb1', block: 368182 },
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getMulticall3(chainId: number): Multicall | null {
|
|
59
|
+
return MULTICALL3[chainId] ?? null
|
|
60
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { type BigNumberish } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import { aggregate, type AggregateContext, type BlockTag, type CallOverrides, type EthersLikeProvider } from './aggregate.js'
|
|
4
|
+
import { Batcher, type BatchOptions } from './batcher.js'
|
|
5
|
+
import { decodeCall, encodeCall, type BoundCall, type Call, type CallRequest, type FailableCall, type RawResult } from './call.js'
|
|
6
|
+
import { Contract, bindCall, type ContractAbi, type ContractRunner } from './contract.js'
|
|
7
|
+
import { DEFAULT_NATIVE_TOKENS, ERC20_ABI, type Erc20Contract, type TokenInfo } from './erc20.js'
|
|
8
|
+
import { CallFailedError, isExecutionError } from './errors.js'
|
|
9
|
+
import { MULTICALL3_ADDRESS, getMulticall3, type Multicall } from './multicall.js'
|
|
10
|
+
import { resolveSource, type ProviderSource, type SourceOptions } from './source.js'
|
|
11
|
+
|
|
12
|
+
export type { ProviderSource }
|
|
13
|
+
|
|
14
|
+
export interface StaticCallOverrides extends CallOverrides {
|
|
15
|
+
value?: BigNumberish
|
|
16
|
+
gasLimit?: BigNumberish
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StaticCallItem {
|
|
20
|
+
call: Call
|
|
21
|
+
overrides?: StaticCallOverrides
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type StaticCallResult<T> = { success: true; data: T } | { success: false; error: Error }
|
|
25
|
+
|
|
26
|
+
/** all / tryAll 的输入:Call 数组,或 { 名字: Call } 对象 */
|
|
27
|
+
export type CallInput = readonly Call[] | Readonly<Record<string, Call>>
|
|
28
|
+
|
|
29
|
+
type ResultOf<C> = C extends Call<infer R> ? R : any
|
|
30
|
+
/** 与输入同构的结果:数组 → 数组(元组保留各项类型),对象 → 同名字段 */
|
|
31
|
+
export type CallResults<C> = { -readonly [K in keyof C]: ResultOf<C[K]> }
|
|
32
|
+
export type TryCallResults<C> = { -readonly [K in keyof C]: ResultOf<C[K]> | null }
|
|
33
|
+
|
|
34
|
+
export interface ProviderConfig extends SourceOptions {
|
|
35
|
+
/** 自定义 Multicall3 地址;不填则查内置地址表,表里没有就走 deployless */
|
|
36
|
+
multicall?: Partial<Multicall>
|
|
37
|
+
/** 强制走 deployless(不依赖链上 multicall 合约) */
|
|
38
|
+
deployless?: boolean
|
|
39
|
+
/** 单次 eth_call 最多打包的调用数,超出自动拆分并发请求。默认 500 */
|
|
40
|
+
chunkSize?: number
|
|
41
|
+
/** `provider.call()` 自动合并的参数 */
|
|
42
|
+
batch?: BatchOptions
|
|
43
|
+
/** `balances()` 里视为主币的地址,默认 0xeeee…eeee 和零地址 */
|
|
44
|
+
nativeTokens?: readonly string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ETH_BALANCE_INPUTS = [{ name: 'addr', type: 'address' }] as const
|
|
48
|
+
const ETH_BALANCE_OUTPUTS = [{ name: 'balance', type: 'uint256' }] as const
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Multicall Provider,API 与 ethcall 的 Provider 保持一致:
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const multi = new Provider(chainId, ethersProvider)
|
|
55
|
+
* const [symbol, decimals] = await multi.all([erc20.symbol(), erc20.decimals()])
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class Provider implements ContractRunner {
|
|
59
|
+
readonly #ctx: AggregateContext
|
|
60
|
+
readonly #batcher: Batcher
|
|
61
|
+
readonly #nativeTokens: Set<string>
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param chainId 链 ID(接受数字字符串)
|
|
65
|
+
* @param provider 节点:RPC URL、ethers Provider、钱包(window.ethereum / tronWeb)或它们的数组(主节点 + 备用节点)。
|
|
66
|
+
* 不传则使用内置的公共节点表
|
|
67
|
+
* @param config 可选配置
|
|
68
|
+
*/
|
|
69
|
+
constructor(chainId: number | string, provider?: ProviderSource | readonly ProviderSource[], config: ProviderConfig = {}) {
|
|
70
|
+
const id = Number(chainId)
|
|
71
|
+
this.#ctx = {
|
|
72
|
+
provider: resolveSource(id, provider, config),
|
|
73
|
+
chainId: id,
|
|
74
|
+
multicall: config.deployless ? null : resolveMulticall(id, config.multicall),
|
|
75
|
+
chunkSize: config.chunkSize ?? 500,
|
|
76
|
+
}
|
|
77
|
+
this.#batcher = new Batcher((requests, overrides) => this.#aggregate(requests, overrides), config.batch)
|
|
78
|
+
this.#nativeTokens = new Set((config.nativeTokens ?? DEFAULT_NATIVE_TOKENS).map((t) => t.toLowerCase()))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 绑定到本 Provider 的合约:方法返回的 Call 可以直接 await(自动合并成 multicall),
|
|
83
|
+
* 也可以传给 all / tryAll;`method.staticCall(...)` 预执行。
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const pair = multi.contract(pairAddress, PAIR_ABI)
|
|
87
|
+
* const [reserves, token0] = await Promise.all([pair.getReserves(), pair.token0()]) // 一次请求
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
contract(address: string, abi: ContractAbi): Contract {
|
|
91
|
+
return new Contract(address, abi, this)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 绑定的 ERC20 合约(内置 ABI,结果带类型) */
|
|
95
|
+
erc20(address: string): Erc20Contract {
|
|
96
|
+
return new Contract(address, ERC20_ABI as unknown as string[], this) as Erc20Contract
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 批量查余额,主币和代币混在一起、一次请求。主币地址见 config.nativeTokens。
|
|
101
|
+
* 查询失败(非合约地址、非法地址等)的位置为 0n;需要区分失败请用 tryAll。
|
|
102
|
+
*/
|
|
103
|
+
async balances(owner: string, tokens: readonly string[], overrides?: CallOverrides): Promise<bigint[]> {
|
|
104
|
+
const calls = tokens.map((token) =>
|
|
105
|
+
this.#nativeTokens.has(token.toLowerCase()) ? this.getEthBalance(owner) : this.erc20(token).balanceOf(owner),
|
|
106
|
+
)
|
|
107
|
+
const results = await this.tryAll<bigint>(calls, overrides)
|
|
108
|
+
return results.map((balance) => balance ?? 0n)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 批量查代币信息(symbol / name / decimals),一次请求。
|
|
113
|
+
* decimals 读不到(非代币合约、主币占位地址等)的位置为 null。
|
|
114
|
+
*/
|
|
115
|
+
async tokenInfo(tokens: readonly string[], overrides?: CallOverrides): Promise<(TokenInfo | null)[]> {
|
|
116
|
+
// 主币占位地址不是合约,不发请求
|
|
117
|
+
const targets = tokens.filter((token) => !this.#nativeTokens.has(token.toLowerCase()))
|
|
118
|
+
const calls = targets.flatMap((token) => {
|
|
119
|
+
const erc20 = this.erc20(token)
|
|
120
|
+
return [erc20.symbol(), erc20.name(), erc20.decimals()]
|
|
121
|
+
})
|
|
122
|
+
const results = calls.length ? await this.tryAll(calls, overrides) : []
|
|
123
|
+
const infoByToken = new Map<string, TokenInfo | null>()
|
|
124
|
+
targets.forEach((address, i) => {
|
|
125
|
+
const decimals = results[i * 3 + 2] as bigint | null
|
|
126
|
+
infoByToken.set(
|
|
127
|
+
address,
|
|
128
|
+
decimals === null
|
|
129
|
+
? null
|
|
130
|
+
: {
|
|
131
|
+
address,
|
|
132
|
+
symbol: (results[i * 3] as string | null) ?? null,
|
|
133
|
+
name: (results[i * 3 + 1] as string | null) ?? null,
|
|
134
|
+
decimals: Number(decimals),
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
return tokens.map((token) => infoByToken.get(token) ?? null)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 主币余额查询,可以和合约调用放在同一批里(同一次 eth_call):
|
|
143
|
+
* 有 Multicall3 时调合约的 getEthBalance;deployless 时由 deployless 合约直接用 BALANCE 读取。
|
|
144
|
+
*/
|
|
145
|
+
getEthBalance(address: string): BoundCall<bigint> {
|
|
146
|
+
const call: Call<bigint> = {
|
|
147
|
+
contract: { address: this.#ctx.multicall?.address ?? MULTICALL3_ADDRESS },
|
|
148
|
+
name: 'getEthBalance',
|
|
149
|
+
inputs: ETH_BALANCE_INPUTS,
|
|
150
|
+
outputs: ETH_BALANCE_OUTPUTS,
|
|
151
|
+
params: [address],
|
|
152
|
+
kind: 'ethBalance',
|
|
153
|
+
}
|
|
154
|
+
return bindCall(call, this)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 合并成一次调用,任意一条失败则整体抛错。
|
|
159
|
+
* 可以传数组(结果顺序一致),也可以传 `{ 名字: Call }` 对象(结果是同名字段)。
|
|
160
|
+
*/
|
|
161
|
+
all<const C extends CallInput>(calls: C, overrides?: CallOverrides): Promise<CallResults<C>>
|
|
162
|
+
all<T = any>(calls: readonly Call[], overrides?: CallOverrides): Promise<T[]>
|
|
163
|
+
async all(calls: CallInput, overrides?: CallOverrides): Promise<unknown> {
|
|
164
|
+
const [list, pack] = unpack(calls)
|
|
165
|
+
const results = await this.#aggregate(list.map((call) => encodeCall(call, false)), overrides)
|
|
166
|
+
return pack(list.map((call, i) => decodeCall(call, (results[i] as RawResult).returnData)))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 合并成一次调用,失败(revert、解码失败、参数编码失败如非法地址)的位置返回 null。
|
|
171
|
+
* 与 all 一样支持数组或对象。
|
|
172
|
+
*/
|
|
173
|
+
tryAll<const C extends CallInput>(calls: C, overrides?: CallOverrides): Promise<TryCallResults<C>>
|
|
174
|
+
tryAll<T = any>(calls: readonly Call[], overrides?: CallOverrides): Promise<(T | null)[]>
|
|
175
|
+
async tryAll(calls: CallInput, overrides?: CallOverrides): Promise<unknown> {
|
|
176
|
+
const [list, pack] = unpack(calls)
|
|
177
|
+
return pack(await this.tryEach(list, list.map(() => true), overrides))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 逐条指定是否允许失败:允许失败的位置返回 null,不允许失败的一旦失败整体抛错。
|
|
182
|
+
*/
|
|
183
|
+
async tryEach<T = any>(calls: readonly Call[], canFail: readonly boolean[], overrides?: CallOverrides): Promise<(T | null)[]> {
|
|
184
|
+
// 允许失败的条目编码出错(如后端给的脏地址)只让该条为 null,不拖垮整批
|
|
185
|
+
const requests: Array<CallRequest | null> = calls.map((call, i) => {
|
|
186
|
+
const callCanFail = canFail[i]
|
|
187
|
+
if (callCanFail === undefined) {
|
|
188
|
+
throw new Error('Unable to access the canFail value')
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return encodeCall(call, callCanFail)
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (callCanFail) {
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
throw err
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
const valid = requests.filter((req): req is CallRequest => req !== null)
|
|
200
|
+
const results = await this.#aggregate(valid, overrides)
|
|
201
|
+
let j = 0
|
|
202
|
+
const aligned = requests.map((req) => (req ? (results[j++] as RawResult) : { success: false, returnData: '0x' }))
|
|
203
|
+
return decodeTry<T>(calls, aligned)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 单条调用,自动与同一收集窗口内的其他 `call()` 合并为一次 multicall。
|
|
208
|
+
* 失败时 reject CallFailedError。适合各组件各自查数据、又希望只发一个请求的场景。
|
|
209
|
+
*/
|
|
210
|
+
call<T = any>(call: Call, overrides?: CallOverrides): Promise<T> {
|
|
211
|
+
const own = call.overrides
|
|
212
|
+
return this.#batcher.load<T>(call, merge({ blockTag: own?.blockTag, from: own?.from }, overrides))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 预执行(模拟交易):直接对目标合约发 eth_call,可带 from / value / gasLimit,
|
|
217
|
+
* 用于 swap 报价、检查交易是否会 revert 等。失败时抛 CallFailedError(带解析后的 revert 原因)。
|
|
218
|
+
*
|
|
219
|
+
* 不走 multicall:multicall 里子调用的 msg.sender 是 multicall 合约本身、也无法按条带 value,
|
|
220
|
+
* 与真实交易不一致。
|
|
221
|
+
*
|
|
222
|
+
* overrides 优先级:参数 overrides > Call 上的 overrides(`contract.swap(params, { value })`)。
|
|
223
|
+
*/
|
|
224
|
+
async staticCall<T = any>(call: Call, overrides: StaticCallOverrides = {}): Promise<T> {
|
|
225
|
+
const result = await this.#staticCall<T>(call, overrides)
|
|
226
|
+
if (!result.success) {
|
|
227
|
+
throw result.error
|
|
228
|
+
}
|
|
229
|
+
return result.data
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 批量预执行:每条都是独立的 eth_call,同时发出(JsonRpcProvider 会合进一个 JSON-RPC batch 请求;
|
|
234
|
+
* TronProvider 按 concurrency 限流)。每条结果单独返回成功/失败,不会互相影响。
|
|
235
|
+
*
|
|
236
|
+
* @param items Call,或 `{ call, overrides }` 以便每条带不同的 value / from
|
|
237
|
+
* @param overrides 所有条目共享的 overrides,优先级最低
|
|
238
|
+
*/
|
|
239
|
+
async staticCallAll<T = any>(
|
|
240
|
+
items: ReadonlyArray<Call | StaticCallItem>,
|
|
241
|
+
overrides: StaticCallOverrides = {},
|
|
242
|
+
): Promise<StaticCallResult<T>[]> {
|
|
243
|
+
return Promise.all(
|
|
244
|
+
items.map((item) =>
|
|
245
|
+
'call' in item && 'contract' in item.call
|
|
246
|
+
? this.#staticCall<T>(item.call, item.overrides, overrides)
|
|
247
|
+
: this.#staticCall<T>(item as Call, undefined, overrides),
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 优先级:explicit > Call 自带的 overrides > shared;值为 undefined 的字段不覆盖 */
|
|
253
|
+
async #staticCall<T>(
|
|
254
|
+
call: Call,
|
|
255
|
+
explicit?: StaticCallOverrides,
|
|
256
|
+
shared?: StaticCallOverrides,
|
|
257
|
+
): Promise<StaticCallResult<T>> {
|
|
258
|
+
const merged: StaticCallOverrides = merge(merge({ ...shared }, call.overrides), explicit)
|
|
259
|
+
if (call.kind === 'ethBalance') {
|
|
260
|
+
try {
|
|
261
|
+
return { success: true, data: (await this.#ctx.provider.getBalance(call.params[0], merged.blockTag)) as T }
|
|
262
|
+
} catch (err) {
|
|
263
|
+
return { success: false, error: err instanceof Error ? err : new Error(String(err)) }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let returnData: string
|
|
267
|
+
try {
|
|
268
|
+
const request = encodeCall(call, false)
|
|
269
|
+
returnData = await this.#ctx.provider.call({
|
|
270
|
+
to: request.target,
|
|
271
|
+
data: request.callData,
|
|
272
|
+
from: merged.from,
|
|
273
|
+
value: merged.value,
|
|
274
|
+
gasLimit: merged.gasLimit,
|
|
275
|
+
blockTag: merged.blockTag,
|
|
276
|
+
})
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// 只有执行层面的失败才是 CallFailedError;限流、超时等节点错误原样返回
|
|
279
|
+
if (isExecutionError(err)) {
|
|
280
|
+
return { success: false, error: new CallFailedError(call, (err as { data?: string | null }).data ?? '0x', err) }
|
|
281
|
+
}
|
|
282
|
+
return { success: false, error: err instanceof Error ? err : new Error(String(err)) }
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
return { success: true, data: decodeCall<T>(call, returnData) }
|
|
286
|
+
} catch (err) {
|
|
287
|
+
// 目标地址没有合约时返回 0x,解码失败
|
|
288
|
+
return { success: false, error: new CallFailedError(call, returnData, err) }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 底层连接(多节点时是 FallbackRpc) */
|
|
293
|
+
get rpc(): EthersLikeProvider {
|
|
294
|
+
return this.#ctx.provider
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 当前使用的 multicall 合约(null 表示 deployless) */
|
|
298
|
+
get multicall(): Multicall | null {
|
|
299
|
+
return this.#ctx.multicall
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#aggregate(requests: CallRequest[], overrides?: CallOverrides): Promise<RawResult[]> {
|
|
303
|
+
return aggregate(this.#ctx, requests, overrides)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function resolveMulticall(chainId: number, custom?: Partial<Multicall>): Multicall | null {
|
|
308
|
+
if (custom?.address) {
|
|
309
|
+
return { address: custom.address, block: custom.block ?? 0 }
|
|
310
|
+
}
|
|
311
|
+
return Number.isFinite(chainId) ? getMulticall3(chainId) : null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 浅合并,source 里值为 undefined 的字段不覆盖 target */
|
|
315
|
+
function merge<T extends object>(target: T, source?: object): T {
|
|
316
|
+
const out = { ...target } as Record<string, unknown>
|
|
317
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
318
|
+
if (value !== undefined) {
|
|
319
|
+
out[key] = value
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return out as T
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 把数组 / 对象输入展开成 Call 列表,并返回把结果还原成同样形状的函数 */
|
|
326
|
+
function unpack(calls: CallInput): [readonly Call[], (results: unknown[]) => unknown] {
|
|
327
|
+
if (Array.isArray(calls)) {
|
|
328
|
+
return [calls, (results) => results]
|
|
329
|
+
}
|
|
330
|
+
const keys = Object.keys(calls)
|
|
331
|
+
const record = calls as Readonly<Record<string, Call>>
|
|
332
|
+
return [keys.map((key) => record[key] as Call), (results) => Object.fromEntries(keys.map((key, i) => [key, results[i]]))]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function decodeTry<T>(calls: ReadonlyArray<Call | FailableCall>, results: RawResult[]): (T | null)[] {
|
|
336
|
+
return calls.map((call, i) => {
|
|
337
|
+
const result = results[i] as RawResult
|
|
338
|
+
if (!result.success) {
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
return decodeCall<T>(call, result.returnData)
|
|
343
|
+
} catch {
|
|
344
|
+
// 解码失败:多半是目标地址没有合约
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export default Provider
|
|
351
|
+
|
|
352
|
+
export type { BlockTag, CallOverrides }
|