@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/aggregate.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AbiCoder, Interface, concat, dataSlice, id, isError, type Provider as EthersProvider } from 'ethers'
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { encodeUint256, type CallRequest, type RawResult } from './call.js'
|
|
4
4
|
import { DEPLOYLESS_MULTICALL3_BYTECODE } from './deployless.js'
|
|
5
5
|
import { isExecutionError } from './errors.js'
|
|
6
6
|
import { MULTICALL3_ADDRESS, type Multicall } from './multicall.js'
|
|
7
|
+
import { isTronChain } from './tron.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 只用到 ethers Provider 的这几个方法:必需 call / getBalance;getLogs / getBlockNumber 可选(只有增量扫描 Transfer 事件时用到)。
|
|
@@ -16,13 +17,55 @@ export type BlockTag = number | bigint | string
|
|
|
16
17
|
export interface CallOverrides {
|
|
17
18
|
blockTag?: BlockTag
|
|
18
19
|
from?: string
|
|
20
|
+
/**
|
|
21
|
+
* 最低区块:节点落后于这个区块时(如交易刚确认、公共节点还没跟上)不用它的结果——
|
|
22
|
+
* 改为按这个区块重查(落后的节点会报错、自动换节点),所有节点都落后时稍等重试,最多约 10 秒。
|
|
23
|
+
* 只对最新状态的查询生效(指定了 blockTag 时忽略)
|
|
24
|
+
*/
|
|
25
|
+
minBlock?: number
|
|
26
|
+
/** 取消查询:已取消时不发请求;执行中取消时立即 reject(合并在一起的底层请求继续完成,结果丢弃) */
|
|
27
|
+
signal?: AbortSignal
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 主币余额的读取方式:
|
|
32
|
+
* - 'contract':和其他调用放在同一次 eth_call 里(Multicall3.getEthBalance / deployless 合约里的 BALANCE)
|
|
33
|
+
* - 'rpc':单独用 eth_getBalance(与 eth_call 同时发出)。用于合约里读主币余额不可靠的链(如 BALANCE 恒为 0)
|
|
34
|
+
* - { erc20 }:主币其实是一个 ERC20 合约,改调它的 balanceOf
|
|
35
|
+
*/
|
|
36
|
+
export type NativeBalanceMode = 'contract' | 'rpc' | { erc20: string }
|
|
37
|
+
|
|
38
|
+
const balanceOfInterface = new Interface(['function balanceOf(address owner) view returns (uint256)'])
|
|
39
|
+
|
|
21
40
|
export const multicall3Interface = new Interface([
|
|
22
41
|
'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)',
|
|
23
42
|
'function getEthBalance(address addr) view returns (uint256 balance)',
|
|
43
|
+
'function getBlockNumber() view returns (uint256 blockNumber)',
|
|
24
44
|
])
|
|
25
45
|
|
|
46
|
+
/** 区块号查询(minBlock 检查用):目标在执行时换成 multicall 地址 */
|
|
47
|
+
const BLOCK_NUMBER_REQUEST: CallRequest = {
|
|
48
|
+
target: MULTICALL3_ADDRESS,
|
|
49
|
+
allowFailure: true,
|
|
50
|
+
callData: multicall3Interface.encodeFunctionData('getBlockNumber'),
|
|
51
|
+
blockNumber: true,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Arbitrum 系的链(Arbitrum One / Nova、Orbit 链如 Robinhood Chain)在合约里 block.number 是 L1 区块号,
|
|
56
|
+
* L2 区块号要从 ArbSys 预编译合约(0x64)的 arbBlockNumber() 读。区块号查询时顺带读它:有返回值就用它,
|
|
57
|
+
* 其他链上 0x64 没有代码、返回空数据,忽略
|
|
58
|
+
*/
|
|
59
|
+
const ARB_BLOCK_NUMBER_REQUEST: CallRequest = {
|
|
60
|
+
target: '0x0000000000000000000000000000000000000064',
|
|
61
|
+
allowFailure: true,
|
|
62
|
+
callData: '0xa3b1b31d', // arbBlockNumber()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** minBlock:所有节点都落后时的重试间隔和总时长 */
|
|
66
|
+
const MIN_BLOCK_RETRY_INTERVAL = 1000
|
|
67
|
+
const MIN_BLOCK_DEADLINE = 10_000
|
|
68
|
+
|
|
26
69
|
const CALL3_TUPLE = 'tuple(address target, bool allowFailure, bytes callData)[]'
|
|
27
70
|
const RESULT_TUPLE = 'tuple(bool success, bytes returnData)[]'
|
|
28
71
|
/** error Aggregate3Result((bool,bytes)[]) */
|
|
@@ -40,6 +83,8 @@ export interface AggregateContext {
|
|
|
40
83
|
/** null 表示直接走 deployless */
|
|
41
84
|
multicall: Multicall | null
|
|
42
85
|
chunkSize: number
|
|
86
|
+
/** 主币余额的读取方式,默认 'contract' */
|
|
87
|
+
nativeBalance?: NativeBalanceMode
|
|
43
88
|
}
|
|
44
89
|
|
|
45
90
|
/**
|
|
@@ -55,19 +100,139 @@ export async function aggregate(
|
|
|
55
100
|
if (requests.length === 0) {
|
|
56
101
|
return []
|
|
57
102
|
}
|
|
103
|
+
const mode = ctx.nativeBalance ?? 'contract'
|
|
104
|
+
if (mode !== 'contract' && requests.some((req) => req.ethBalanceOf !== undefined)) {
|
|
105
|
+
return typeof mode === 'object' ? aggregate(ctx, requests.map((req) => toErc20Balance(req, mode.erc20)), overrides) : aggregateWithRpcBalances(ctx, requests, overrides)
|
|
106
|
+
}
|
|
58
107
|
const size = Math.max(1, ctx.chunkSize)
|
|
108
|
+
const run = overrides.minBlock !== undefined && isLatest(overrides.blockTag) ? aggregateChunkAtLeast : aggregateChunk
|
|
59
109
|
if (requests.length <= size) {
|
|
60
|
-
return
|
|
110
|
+
return run(ctx, requests, overrides)
|
|
61
111
|
}
|
|
62
112
|
const chunks: CallRequest[][] = []
|
|
63
113
|
for (let i = 0; i < requests.length; i += size) {
|
|
64
114
|
chunks.push(requests.slice(i, i + size))
|
|
65
115
|
}
|
|
66
|
-
const results = await Promise.all(chunks.map((chunk) =>
|
|
116
|
+
const results = await Promise.all(chunks.map((chunk) => run(ctx, chunk, overrides)))
|
|
67
117
|
return results.flat()
|
|
68
118
|
}
|
|
69
119
|
|
|
70
|
-
|
|
120
|
+
/**
|
|
121
|
+
* minBlock:先按最新状态查,同一次 eth_call 里顺带读区块号;节点落后时按 minBlock 指定区块重查——
|
|
122
|
+
* 落后的节点对未来区块报 “header not found” 等错误,FallbackRpc 换节点(不冷却);所有节点都落后时稍等重试。
|
|
123
|
+
* 重查得到的是 minBlock 那个区块的状态(已包含刚确认的交易)。Tron 只能查最新状态,落后时等节点跟上再查。
|
|
124
|
+
* overrides.signal 取消后不再重试
|
|
125
|
+
*/
|
|
126
|
+
async function aggregateChunkAtLeast(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
|
|
127
|
+
const minBlock = overrides.minBlock as number
|
|
128
|
+
// Tron 节点只能查最新状态(不支持按区块号查询)
|
|
129
|
+
const canPinBlock = !isTronChain(ctx.chainId)
|
|
130
|
+
return retryUntilCaughtUp(overrides, async () => {
|
|
131
|
+
const results = await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], overrides)
|
|
132
|
+
if ((blockOf(results[results.length - 1]) ?? -1n) >= BigInt(minBlock)) {
|
|
133
|
+
return results.slice(0, -1)
|
|
134
|
+
}
|
|
135
|
+
if (canPinBlock) {
|
|
136
|
+
return aggregateChunk(ctx, requests, { ...overrides, blockTag: minBlock })
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Node is behind minBlock ${minBlock}`)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 节点落后时的重试:确定性错误(合约执行结果)直接抛;其余错误每秒重试,最多约 10 秒;signal 取消后停止 */
|
|
143
|
+
async function retryUntilCaughtUp<T>(overrides: CallOverrides, run: () => Promise<T>): Promise<T> {
|
|
144
|
+
const deadline = Date.now() + MIN_BLOCK_DEADLINE
|
|
145
|
+
for (;;) {
|
|
146
|
+
overrides.signal?.throwIfAborted()
|
|
147
|
+
try {
|
|
148
|
+
return await run()
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (isExecutionError(err) || Date.now() + MIN_BLOCK_RETRY_INTERVAL > deadline) {
|
|
151
|
+
throw err
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
await sleep(MIN_BLOCK_RETRY_INTERVAL, overrides.signal)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
signal?.removeEventListener('abort', onAbort)
|
|
162
|
+
resolve()
|
|
163
|
+
}, ms)
|
|
164
|
+
const onAbort = () => {
|
|
165
|
+
clearTimeout(timer)
|
|
166
|
+
reject(signal?.reason)
|
|
167
|
+
}
|
|
168
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 区块号查询的结果;失败时为 null */
|
|
173
|
+
function blockOf(result: RawResult | undefined): bigint | null {
|
|
174
|
+
return result?.success && result.returnData !== '0x' ? BigInt(result.returnData) : null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 主币余额请求改成主币 ERC20 合约的 balanceOf(返回值同样是 uint256,解码方式不变) */
|
|
178
|
+
function toErc20Balance(req: CallRequest, erc20: string): CallRequest {
|
|
179
|
+
if (req.ethBalanceOf === undefined) {
|
|
180
|
+
return req
|
|
181
|
+
}
|
|
182
|
+
return { target: erc20, allowFailure: req.allowFailure, callData: balanceOfInterface.encodeFunctionData('balanceOf', [req.ethBalanceOf]) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 主币余额单独走 eth_getBalance,结果按原顺序合并;节点错误照常抛出。
|
|
187
|
+
* 需要区块号(withBlock / minBlock)时,先执行其余请求并读出实际区块号,再在这个区块上读主币余额——
|
|
188
|
+
* 主币和代币余额、blockNumber 一致,minBlock 的等待 / 换节点也由其余请求负责;否则两者同时发出
|
|
189
|
+
*/
|
|
190
|
+
async function aggregateWithRpcBalances(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
|
|
191
|
+
const others = requests.filter((req) => req.ethBalanceOf === undefined)
|
|
192
|
+
const owners = requests.filter((req) => req.ethBalanceOf !== undefined).map((req) => req.ethBalanceOf as string)
|
|
193
|
+
const contractCtx: AggregateContext = { ...ctx, nativeBalance: 'contract' }
|
|
194
|
+
let otherResults: RawResult[]
|
|
195
|
+
let balances: bigint[]
|
|
196
|
+
// Tron 节点只能查最新状态,不能按区块号读余额
|
|
197
|
+
if (isLatest(overrides.blockTag) && !isTronChain(ctx.chainId) && (overrides.minBlock !== undefined || others.some((req) => req.blockNumber))) {
|
|
198
|
+
const results = await aggregate(contractCtx, [...others, BLOCK_NUMBER_REQUEST], overrides)
|
|
199
|
+
const block = blockOf(results.pop())
|
|
200
|
+
otherResults = results
|
|
201
|
+
const blockTag = block === null ? overrides.blockTag : Number(block)
|
|
202
|
+
const read = () => Promise.all(owners.map((owner) => ctx.provider.getBalance(owner, blockTag)))
|
|
203
|
+
// 只有 minBlock 才等待重试(读的区块可能来自比当前节点更新的节点);只要 withBlock 时出错照常抛出
|
|
204
|
+
balances = await (overrides.minBlock !== undefined ? retryUntilCaughtUp(overrides, read) : read())
|
|
205
|
+
} else {
|
|
206
|
+
;[otherResults, balances] = await Promise.all([
|
|
207
|
+
aggregate(contractCtx, others, overrides),
|
|
208
|
+
Promise.all(owners.map((owner) => ctx.provider.getBalance(owner, overrides.blockTag))),
|
|
209
|
+
])
|
|
210
|
+
}
|
|
211
|
+
let i = 0
|
|
212
|
+
let j = 0
|
|
213
|
+
return requests.map((req) =>
|
|
214
|
+
req.ethBalanceOf === undefined ? (otherResults[i++] as RawResult) : { success: true, returnData: encodeUint256(balances[j++] as bigint) },
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 执行一批请求;其中有区块号查询时顺带读 Arbitrum 的 L2 区块号,并用它替换(见 ARB_BLOCK_NUMBER_REQUEST) */
|
|
219
|
+
async function aggregateChunk(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
|
|
220
|
+
if (!requests.some((req) => req.blockNumber)) {
|
|
221
|
+
return aggregateRawChunk(ctx, requests, overrides)
|
|
222
|
+
}
|
|
223
|
+
const results = await aggregateRawChunk(ctx, [...requests, ARB_BLOCK_NUMBER_REQUEST], overrides)
|
|
224
|
+
const arb = results.pop() as RawResult
|
|
225
|
+
if (arb.success && arb.returnData.length === 66) {
|
|
226
|
+
requests.forEach((req, i) => {
|
|
227
|
+
if (req.blockNumber) {
|
|
228
|
+
results[i] = arb
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
return results
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function aggregateRawChunk(
|
|
71
236
|
ctx: AggregateContext,
|
|
72
237
|
requests: CallRequest[],
|
|
73
238
|
overrides: CallOverrides,
|
|
@@ -100,7 +265,7 @@ async function callContract(
|
|
|
100
265
|
overrides: CallOverrides,
|
|
101
266
|
): Promise<RawResult[] | 'invalid' | 'reverted'> {
|
|
102
267
|
const calls = requests.map((req) => ({
|
|
103
|
-
target: req.ethBalanceOf ? multicall.address : req.target,
|
|
268
|
+
target: req.ethBalanceOf || req.blockNumber ? multicall.address : req.target,
|
|
104
269
|
allowFailure: req.allowFailure,
|
|
105
270
|
callData: req.callData,
|
|
106
271
|
}))
|
|
@@ -157,8 +322,8 @@ async function callDeploylessChunk(
|
|
|
157
322
|
const args = AbiCoder.defaultAbiCoder().encode(
|
|
158
323
|
[CALL3_TUPLE],
|
|
159
324
|
[
|
|
160
|
-
requests.map(({ target, allowFailure, callData, ethBalanceOf }) => ({
|
|
161
|
-
target: ethBalanceOf ? MULTICALL3_ADDRESS : target,
|
|
325
|
+
requests.map(({ target, allowFailure, callData, ethBalanceOf, blockNumber }) => ({
|
|
326
|
+
target: ethBalanceOf || blockNumber ? MULTICALL3_ADDRESS : target,
|
|
162
327
|
allowFailure,
|
|
163
328
|
callData,
|
|
164
329
|
})),
|
package/src/batcher.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeCall, encodeCall, type Call, type CallRequest, type RawResult } from './call.js'
|
|
1
|
+
import { decodeCall, encodeCall, encodeFailureReason, settleResult, type Call, type CallRequest, type RawResult, type Settled } from './call.js'
|
|
2
2
|
import type { CallOverrides } from './aggregate.js'
|
|
3
3
|
import { CallFailedError } from './errors.js'
|
|
4
4
|
|
|
@@ -13,6 +13,10 @@ type Runner = (requests: CallRequest[], overrides: CallOverrides) => Promise<Raw
|
|
|
13
13
|
|
|
14
14
|
interface Pending {
|
|
15
15
|
call: Call
|
|
16
|
+
/** 调用方的 signal(没有时这个调用方不会取消,整批请求不能中断) */
|
|
17
|
+
signal?: AbortSignal
|
|
18
|
+
/** true:失败时 resolve 失败原因(Settled),不 reject;节点错误照常 reject */
|
|
19
|
+
settle: boolean
|
|
16
20
|
resolve: (value: unknown) => void
|
|
17
21
|
reject: (reason: unknown) => void
|
|
18
22
|
}
|
|
@@ -23,6 +27,26 @@ interface Queue {
|
|
|
23
27
|
timer: ReturnType<typeof setTimeout> | null
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 整批请求的 signal:所有调用方都传了 signal、并且都取消后才取消(如 minBlock 的等待重试随之停止);
|
|
32
|
+
* 有调用方没传 signal 时返回 undefined(整批照常完成)
|
|
33
|
+
*/
|
|
34
|
+
function combinedSignal(entries: readonly Pending[]): AbortSignal | undefined {
|
|
35
|
+
const signals = [...new Set(entries.map((entry) => entry.signal))]
|
|
36
|
+
if (!signals.length || signals.some((signal) => signal === undefined)) {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
const controller = new AbortController()
|
|
40
|
+
const check = () => {
|
|
41
|
+
if (signals.every((signal) => signal?.aborted)) {
|
|
42
|
+
controller.abort(signals[0]?.reason)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
signals.forEach((signal) => signal?.addEventListener('abort', check, { once: true }))
|
|
46
|
+
check()
|
|
47
|
+
return controller.signal
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
/**
|
|
27
51
|
* DataLoader 式的自动合并:各处独立调用 `provider.call(x)`,
|
|
28
52
|
* 落在同一个收集窗口、且 blockTag/from 相同的调用会合成一次 aggregate3。
|
|
@@ -41,21 +65,49 @@ export class Batcher {
|
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
load<T>(call: Call, overrides: CallOverrides = {}): Promise<T> {
|
|
44
|
-
const key =
|
|
68
|
+
const [key, queue] = this.#queue(overrides)
|
|
69
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
70
|
+
queue.entries.push({ call, signal: overrides.signal, settle: false, resolve: resolve as (value: unknown) => void, reject })
|
|
71
|
+
})
|
|
72
|
+
this.#schedule(key, queue)
|
|
73
|
+
return promise
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 一组调用一起入队(不会被 maxSize 拆到不同批次),逐条返回成功的值或失败原因;
|
|
78
|
+
* 只有节点错误(整批失败)时 reject。balances / tokens / allowances 用它参与自动合并。
|
|
79
|
+
*/
|
|
80
|
+
loadSettled(calls: readonly Call[], overrides: CallOverrides = {}): Promise<Settled[]> {
|
|
81
|
+
if (!calls.length) {
|
|
82
|
+
return Promise.resolve([])
|
|
83
|
+
}
|
|
84
|
+
const [key, queue] = this.#queue(overrides)
|
|
85
|
+
const promises = calls.map(
|
|
86
|
+
(call) =>
|
|
87
|
+
new Promise<Settled>((resolve, reject) => {
|
|
88
|
+
queue.entries.push({ call, signal: overrides.signal, settle: true, resolve: resolve as (value: unknown) => void, reject })
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
this.#schedule(key, queue)
|
|
92
|
+
return Promise.all(promises)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#queue(overrides: CallOverrides): [string, Queue] {
|
|
96
|
+
const key = `${String(overrides.blockTag ?? 'latest')}|${overrides.from ?? ''}|${overrides.minBlock ?? ''}`
|
|
45
97
|
let queue = this.#queues.get(key)
|
|
46
98
|
if (!queue) {
|
|
47
99
|
queue = { overrides, entries: [], timer: null }
|
|
48
100
|
this.#queues.set(key, queue)
|
|
49
101
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
102
|
+
return [key, queue]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#schedule(key: string, queue: Queue): void {
|
|
53
106
|
if (queue.entries.length >= this.#maxSize) {
|
|
54
107
|
this.#flush(key)
|
|
55
108
|
} else if (!queue.timer) {
|
|
56
109
|
queue.timer = setTimeout(() => this.#flush(key), this.#wait)
|
|
57
110
|
}
|
|
58
|
-
return promise
|
|
59
111
|
}
|
|
60
112
|
|
|
61
113
|
#flush(key: string): void {
|
|
@@ -80,11 +132,15 @@ export class Batcher {
|
|
|
80
132
|
try {
|
|
81
133
|
request = encodeCall(entry.call, true)
|
|
82
134
|
} catch (err) {
|
|
83
|
-
entry.
|
|
135
|
+
if (entry.settle) {
|
|
136
|
+
entry.resolve({ ok: false, reason: encodeFailureReason(err) } satisfies Settled)
|
|
137
|
+
} else {
|
|
138
|
+
entry.reject(err)
|
|
139
|
+
}
|
|
84
140
|
requestIndex.push(-1)
|
|
85
141
|
continue
|
|
86
142
|
}
|
|
87
|
-
const dedupeKey = `${request.ethBalanceOf ? 'eth' : request.target.toLowerCase()}|${request.callData}`
|
|
143
|
+
const dedupeKey = `${request.ethBalanceOf ? 'eth' : request.blockNumber ? 'block' : request.target.toLowerCase()}|${request.callData}`
|
|
88
144
|
let index = indexByKey.get(dedupeKey)
|
|
89
145
|
if (index === undefined) {
|
|
90
146
|
index = requests.push(request) - 1
|
|
@@ -99,7 +155,7 @@ export class Batcher {
|
|
|
99
155
|
|
|
100
156
|
let results: RawResult[]
|
|
101
157
|
try {
|
|
102
|
-
results = await this.#run(requests, overrides)
|
|
158
|
+
results = await this.#run(requests, { ...overrides, signal: combinedSignal(entries) })
|
|
103
159
|
} catch (err) {
|
|
104
160
|
entries.forEach((entry, i) => requestIndex[i] !== -1 && entry.reject(err))
|
|
105
161
|
return
|
|
@@ -111,6 +167,10 @@ export class Batcher {
|
|
|
111
167
|
return
|
|
112
168
|
}
|
|
113
169
|
const result = results[index] as RawResult
|
|
170
|
+
if (entry.settle) {
|
|
171
|
+
entry.resolve(settleResult(entry.call, result))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
114
174
|
if (!result.success) {
|
|
115
175
|
entry.reject(new CallFailedError(entry.call, result.returnData))
|
|
116
176
|
return
|
package/src/call.ts
CHANGED
|
@@ -32,9 +32,10 @@ export interface Call<T = any> {
|
|
|
32
32
|
/**
|
|
33
33
|
* 内部标记:
|
|
34
34
|
* - ethBalance:原生币余额查询(见 Provider.getEthBalance)
|
|
35
|
+
* - blockNumber:当前区块号(Multicall3.getBlockNumber;deployless 合约里直接返回 block.number)
|
|
35
36
|
* - stringOrBytes32:string 返回值,解码失败时按 bytes32 解析(MKR 等老代币的 symbol / name)
|
|
36
37
|
*/
|
|
37
|
-
kind?: 'ethBalance' | 'stringOrBytes32'
|
|
38
|
+
kind?: 'ethBalance' | 'blockNumber' | 'stringOrBytes32'
|
|
38
39
|
/** 仅用于类型推断 */
|
|
39
40
|
readonly __result?: T
|
|
40
41
|
}
|
|
@@ -52,13 +53,51 @@ export interface RawResult {
|
|
|
52
53
|
returnData: string
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 单条调用的失败原因:
|
|
58
|
+
* - invalid-address:合约地址或参数里的地址非法
|
|
59
|
+
* - invalid-argument:其他参数编码失败(个数不对、数值超出范围等)
|
|
60
|
+
* - no-contract:调用成功但返回空数据,多半是目标地址上没有合约
|
|
61
|
+
* - reverted:执行 revert
|
|
62
|
+
* - decode-failed:有返回数据但无法按 ABI 解码(如合约不是预期的标准)
|
|
63
|
+
* - not-configured:没有发请求的信息缺失,如主币不在内置链信息表里、也没有配置 nativeName / nativeSymbol
|
|
64
|
+
*/
|
|
65
|
+
export type FailureReason = 'invalid-address' | 'invalid-argument' | 'no-contract' | 'reverted' | 'decode-failed' | 'not-configured'
|
|
66
|
+
|
|
67
|
+
/** 单条调用的结果:成功的值,或失败原因 */
|
|
68
|
+
export type Settled<T = any> = { ok: true; value: T } | { ok: false; reason: FailureReason }
|
|
69
|
+
|
|
70
|
+
/** 按 multicall 的原始结果判断成功 / 失败原因 */
|
|
71
|
+
export function settleResult<T>(call: Call, result: RawResult): Settled<T> {
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
return { ok: false, reason: 'reverted' }
|
|
74
|
+
}
|
|
75
|
+
// 没有返回值的函数成功时本来就返回空数据,照常解码;有返回值却拿到空数据,多半是地址上没有合约
|
|
76
|
+
if ((!result.returnData || result.returnData === '0x') && call.outputs.length > 0) {
|
|
77
|
+
return { ok: false, reason: 'no-contract' }
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return { ok: true, value: decodeCall<T>(call, result.returnData) }
|
|
81
|
+
} catch {
|
|
82
|
+
return { ok: false, reason: 'decode-failed' }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 参数编码失败的原因:地址非法,还是其他参数问题 */
|
|
87
|
+
export function encodeFailureReason(err: unknown): FailureReason {
|
|
88
|
+
const e = err as { code?: unknown; argument?: unknown; message?: unknown } | null
|
|
89
|
+
return /address/i.test(String(e?.argument ?? '')) || /address/i.test(String(e?.message ?? '')) ? 'invalid-address' : 'invalid-argument'
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
/** 发给 aggregate3 前的单条请求 */
|
|
56
93
|
export interface CallRequest {
|
|
57
94
|
target: string
|
|
58
95
|
allowFailure: boolean
|
|
59
96
|
callData: string
|
|
60
|
-
/**
|
|
97
|
+
/** 原生币余额查询:默认有 multicall 合约时走 getEthBalance、deployless 时用 BALANCE;其他读取方式见 NativeBalanceMode */
|
|
61
98
|
ethBalanceOf?: string
|
|
99
|
+
/** 区块号查询:目标换成实际的 multicall 地址(deployless 时由合约内部处理) */
|
|
100
|
+
blockNumber?: boolean
|
|
62
101
|
}
|
|
63
102
|
|
|
64
103
|
// inputs → name → outputs → Interface。同一份 ABI 生成的 Call 共享 inputs/outputs 数组(见 contract.ts 的 ABI 缓存),
|
|
@@ -100,6 +139,7 @@ export function encodeCall(call: Call, allowFailure: boolean): CallRequest {
|
|
|
100
139
|
allowFailure,
|
|
101
140
|
callData: iface.encodeFunctionData(fragment, params),
|
|
102
141
|
ethBalanceOf: call.kind === 'ethBalance' ? toEvmAddress(params[0] as string) : undefined,
|
|
142
|
+
blockNumber: call.kind === 'blockNumber' ? true : undefined,
|
|
103
143
|
}
|
|
104
144
|
}
|
|
105
145
|
|
package/src/chains.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { NativeBalanceMode } from './aggregate.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 内置链的主币信息(balances() / tokens() 里主币的 name / symbol / 精度)。
|
|
3
5
|
* 来源:https://chainid.network/chains.json 的 nativeCurrency;999 在该表里与 Wanchain 测试网撞号,
|
|
@@ -44,3 +46,16 @@ export const NATIVE_CURRENCIES: Readonly<Record<number, NativeCurrency>> = {
|
|
|
44
46
|
export function getNativeCurrency(chainId: number): NativeCurrency | undefined {
|
|
45
47
|
return NATIVE_CURRENCIES[chainId]
|
|
46
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 合约里读主币余额不可靠的链,以及应改用的读取方式(ProviderConfig.nativeBalance 的默认值)。
|
|
52
|
+
* 新链可用 `pnpm check:rpc` 比对“合约里读到的主币余额”和 eth_getBalance 来发现。
|
|
53
|
+
*/
|
|
54
|
+
export const NATIVE_BALANCE_MODES: Readonly<Record<number, NativeBalanceMode>> = {
|
|
55
|
+
// Anubis:Multicall3.getEthBalance 和 deployless 里的 BALANCE 对任何地址都返回 0,eth_getBalance 正常
|
|
56
|
+
6714: 'rpc',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getNativeBalanceMode(chainId: number): NativeBalanceMode {
|
|
60
|
+
return NATIVE_BALANCE_MODES[chainId] ?? 'contract'
|
|
61
|
+
}
|
package/src/deployless.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
/** constructor((address target, bool allowFailure, bytes callData)[] calls) — 以 aggregate3 的返回格式带回结果 */
|
|
4
4
|
export const DEPLOYLESS_MULTICALL3_BYTECODE =
|
|
5
|
-
'
|
|
5
|
+
'0x60806040526040516106c13803806106c18339810160408190526100229161041e565b80516000816001600160401b0381111561003e5761003e61038c565b60405190808252806020026020018201604052801561008457816020015b60408051808201909152600081526060602082015281526020019060019003908161005c5790505b50905060005b828110156103395760008482815181106100a6576100a66105b9565b602090810291909101015160408101518151919250906001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156100e8575080516024145b801561010c5750631348c07360e21b610100826105cf565b6001600160e01b031916145b1561017e5760248101516040805180820182526001815290516001600160a01b039290921691602080830191610149918531910190815260200190565b60405160208183030381529060405281525085858151811061016d5761016d6105b9565b60200260200101819052505061032f565b81516001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156101ac575080516004145b80156101d057506310b2ec5760e21b6101c4826105cf565b6001600160e01b031916145b15610230576040518060400160405280600115158152602001436040516020016101fc91815260200190565b604051602081830303815290604052815250848481518110610220576102206105b9565b602002602001018190525061032f565b60008083600001516001600160a01b03168360405161024f919061060d565b6000604051808303816000865af19150503d806000811461028c576040519150601f19603f3d011682016040523d82523d6000602084013e610291565b606091505b5091509150811580156102a657508360200151155b156102f85760405162461bcd60e51b815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064015b60405180910390fd5b6040518060400160405280831515815260200182815250868681518110610321576103216105b9565b602002602001018190525050505b505060010161008a565b5060008160405160200161034d9190610629565b6040516020818303038152906040529050616000815111156103845781604051633649f05160e01b81526004016102ef9190610629565b805160208201f35b634e487b7160e01b600052604160045260246000fd5b604051606081016001600160401b03811182821017156103c4576103c461038c565b60405290565b604051601f8201601f191681016001600160401b03811182821017156103f2576103f261038c565b604052919050565b60005b838110156104155781810151838201526020016103fd565b50506000910152565b60006020828403121561043057600080fd5b81516001600160401b0381111561044657600080fd5b8201601f8101841361045757600080fd5b80516001600160401b038111156104705761047061038c565b8060051b610480602082016103ca565b9182526020818401810192908101908784111561049c57600080fd5b6020850192505b838310156105ae5782516001600160401b038111156104c157600080fd5b85016060818a03601f190112156104d757600080fd5b6104df6103a2565b60208201516001600160a01b03811681146104f957600080fd5b81526040820151801515811461050e57600080fd5b602082015260608201516001600160401b0381111561052c57600080fd5b60208184010192505089601f83011261054457600080fd5b81516001600160401b0381111561055d5761055d61038c565b610570601f8201601f19166020016103ca565b8181528b602083860101111561058557600080fd5b6105968260208301602087016103fa565b604083015250835250602092830192909101906104a3565b979650505050505050565b634e487b7160e01b600052603260045260246000fd5b805160208201516001600160e01b0319811691906004821015610606576001600160e01b0319600483900360031b81901b82161692505b5050919050565b6000825161061f8184602087016103fa565b9190910192915050565b6000602082016020835280845180835260408501915060408160051b86010192506020860160005b828110156106b457603f198786030184528151805115158652602081015190506040602087015280518060408801526106918160608901602085016103fa565b601f01601f19169590950160600194506020938401939190910190600101610651565b5092969550505050505056fe'
|