@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.
Files changed (74) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +257 -0
  3. package/contracts/DeploylessMulticall3.sol +66 -0
  4. package/dist/aggregate.d.ts +27 -0
  5. package/dist/aggregate.d.ts.map +1 -0
  6. package/dist/aggregate.js +198 -0
  7. package/dist/aggregate.js.map +1 -0
  8. package/dist/batcher.d.ts +21 -0
  9. package/dist/batcher.d.ts.map +1 -0
  10. package/dist/batcher.js +100 -0
  11. package/dist/batcher.js.map +1 -0
  12. package/dist/call.d.ts +53 -0
  13. package/dist/call.d.ts.map +1 -0
  14. package/dist/call.js +82 -0
  15. package/dist/call.js.map +1 -0
  16. package/dist/contract.d.ts +45 -0
  17. package/dist/contract.d.ts.map +1 -0
  18. package/dist/contract.js +107 -0
  19. package/dist/contract.js.map +1 -0
  20. package/dist/deployless.d.ts +3 -0
  21. package/dist/deployless.d.ts.map +1 -0
  22. package/dist/deployless.js +4 -0
  23. package/dist/deployless.js.map +1 -0
  24. package/dist/erc20.d.ts +27 -0
  25. package/dist/erc20.d.ts.map +1 -0
  26. package/dist/erc20.js +17 -0
  27. package/dist/erc20.js.map +1 -0
  28. package/dist/errors.d.ts +20 -0
  29. package/dist/errors.d.ts.map +1 -0
  30. package/dist/errors.js +56 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/fallback.d.ts +23 -0
  33. package/dist/fallback.d.ts.map +1 -0
  34. package/dist/fallback.js +86 -0
  35. package/dist/fallback.js.map +1 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +10 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/multicall.d.ts +13 -0
  41. package/dist/multicall.d.ts.map +1 -0
  42. package/dist/multicall.js +51 -0
  43. package/dist/multicall.js.map +1 -0
  44. package/dist/provider.d.ts +137 -0
  45. package/dist/provider.d.ts.map +1 -0
  46. package/dist/provider.js +269 -0
  47. package/dist/provider.js.map +1 -0
  48. package/dist/rpcNodes.d.ts +5 -0
  49. package/dist/rpcNodes.d.ts.map +1 -0
  50. package/dist/rpcNodes.js +193 -0
  51. package/dist/rpcNodes.js.map +1 -0
  52. package/dist/source.d.ts +23 -0
  53. package/dist/source.d.ts.map +1 -0
  54. package/dist/source.js +56 -0
  55. package/dist/source.js.map +1 -0
  56. package/dist/tron.d.ts +62 -0
  57. package/dist/tron.d.ts.map +1 -0
  58. package/dist/tron.js +223 -0
  59. package/dist/tron.js.map +1 -0
  60. package/package.json +63 -0
  61. package/src/aggregate.ts +254 -0
  62. package/src/batcher.ts +126 -0
  63. package/src/call.ts +144 -0
  64. package/src/contract.ts +145 -0
  65. package/src/deployless.ts +5 -0
  66. package/src/erc20.ts +43 -0
  67. package/src/errors.ts +66 -0
  68. package/src/fallback.ts +105 -0
  69. package/src/index.ts +31 -0
  70. package/src/multicall.ts +60 -0
  71. package/src/provider.ts +352 -0
  72. package/src/rpcNodes.ts +194 -0
  73. package/src/source.ts +84 -0
  74. package/src/tron.ts +281 -0
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 L
4
+
5
+ Portions (Multicall3 address registry) derived from
6
+ ethcall, Copyright (c) Timur Badretdinov, licensed under the MIT License.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # @w3lib/evm-batch-call
2
+
3
+ 基于 Multicall3 的 EVM / Tron 批量合约读取与交易预执行库,依赖 ethers v6,API 兼容 [ethcall](https://github.com/Destiner/ethcall)。
4
+
5
+ ```bash
6
+ pnpm add @w3lib/evm-batch-call ethers
7
+ ```
8
+
9
+ ## 快速上手
10
+
11
+ ```ts
12
+ import { Provider } from '@w3lib/evm-batch-call'
13
+
14
+ const multi = new Provider(56) // 不传节点:使用内置公共节点(多节点自动故障切换)
15
+
16
+ const usdt = multi.erc20('0x55d398326f99059fF775485246999027B3197955')
17
+
18
+ // 直接 await;同一 tick 内发起的调用自动合并成一次 multicall
19
+ const [symbol, balance, bnb] = await Promise.all([
20
+ usdt.symbol(),
21
+ usdt.balanceOf(user),
22
+ multi.getEthBalance(user), // 主币余额也在同一次请求里
23
+ ])
24
+
25
+ // 对象形式:结果是同名字段,类型自动推断
26
+ const { decimals, allowance } = await multi.all({
27
+ decimals: usdt.decimals(),
28
+ allowance: usdt.allowance(user, spender),
29
+ })
30
+
31
+ // 批量余额(主币 0xeeee…eeee / 零地址 自动识别)、批量代币信息
32
+ const balances = await multi.balances(user, [USDT, NATIVE_TOKEN, BUSD]) // bigint[],失败为 0n
33
+ const infos = await multi.tokenInfo([USDT, BUSD]) // { symbol, name, decimals } | null
34
+
35
+ // 任意合约
36
+ const pair = multi.contract(pairAddress, [
37
+ 'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
38
+ 'function token0() view returns (address)',
39
+ ])
40
+ const [reserves, token0] = await Promise.all([pair.getReserves(), pair.token0()])
41
+ reserves.reserve0 // 多返回值可按名字或下标取
42
+
43
+ // 预执行(模拟交易),写法同 ethers
44
+ const router = multi.contract(routerAddress, RouterABI)
45
+ const amountOut = await router.swap.staticCall(params, { value, from: user })
46
+ ```
47
+
48
+ ## 功能
49
+
50
+ - **批量读取**:`all` / `tryAll` / `tryEach`,N 条读调用合成一次 `eth_call`;支持数组或对象输入
51
+ - **自动合并**:绑定合约直接 `await`,或 `provider.call(x)`,同一收集窗口内的调用合并成一次请求并去重
52
+ - **预执行**:`staticCall` / `staticCallAll` / `method.staticCall`,带 `from` / `value` 模拟交易,返回解析好的 revert 原因
53
+ - **主币余额合并**:`getEthBalance` 与合约调用在同一次 `eth_call` 里——无论链上有没有 Multicall3
54
+ - **任意链可用**:内置 Multicall3 地址表;表里没有、地址无效、或查询早于部署区块时,自动改用 deployless(链上无需部署任何合约)
55
+ - **备用节点**:传多个节点自动故障切换;不传节点使用内置公共节点表(34 条 EVM 链 + Tron)
56
+ - **Tron**:走 Tron HTTP API,T 开头地址可以直接用
57
+ - **浏览器插件钱包**:直接传 `window.ethereum`(EIP-1193)或 `window.tronWeb`
58
+ - **大批量**:合约模式按 `chunkSize`(默认 500)分片;deployless 按 48KB initcode 上限自动分片,结果超过 24KB 也能拿回
59
+
60
+ ## 节点与备用节点
61
+
62
+ ```ts
63
+ new Provider(56) // 内置公共节点
64
+ new Provider(56, 'https://bsc-dataseed.bnbchain.org') // 单个 URL
65
+ new Provider(56, ['https://a.example', 'https://b.example']) // 主节点 + 备用节点
66
+ new Provider(56, [window.ethereum, 'https://bsc-dataseed.bnbchain.org']) // 钱包优先,失败用公共节点
67
+ new Provider(56, new JsonRpcProvider(url)) // 任意 ethers Provider
68
+ new Provider(56, DEFAULT_RPC_URLS[56], { fallback: { timeout: 8000, cooldown: 60_000 } })
69
+ ```
70
+
71
+ 多节点时按顺序使用:出错或超时(默认 10s)换下一个节点,出错的节点在 `cooldown`(默认 30s)内排到最后。与 ethers 的 `FallbackProvider` 相比,没有首次请求前对所有节点的同步,一个卡住的节点不会拖住第一次查询;合约 revert 等确定性结果不会拿去别的节点重试。
72
+
73
+ ethers 会把 `eth_call` 的**任何** JSON-RPC 错误(限流、`header not found` 等)都包装成 `CALL_EXCEPTION`,库内用 `isExecutionError(err)` 区分“合约执行结果”和“节点问题”,只有后者会触发切换。
74
+
75
+ 内置节点表 `DEFAULT_RPC_URLS`(按 chainId)/ `DEFAULT_TRON_HOSTS` 中的节点都能在浏览器里直连:放行跨域、支持 batch 请求、chainId 正确、不需要 API Key。公共节点随时可能下线或限流,生产环境建议传入自己的节点,内置表作为备用。
76
+
77
+ ## 批量读取
78
+
79
+ ```ts
80
+ import { Contract, Provider } from '@w3lib/evm-batch-call'
81
+
82
+ const usdt = new Contract(USDT, ERC20_ABI) // 未绑定的 Contract:方法返回 Call,与 ethcall 一致
83
+ const multi = new Provider(56)
84
+
85
+ const [symbol, decimals] = await multi.all([usdt.symbol(), usdt.decimals()]) // 任意一条失败整体抛错
86
+ const res = await multi.tryAll([usdt.symbol(), other.symbol()]) // 失败的位置为 null
87
+ const res2 = await multi.tryEach([usdt.symbol(), usdt.balanceOf(user)], [true, false])
88
+ await multi.all(calls, { blockTag: 40_000_000 }) // 指定区块
89
+ ```
90
+
91
+ - 单返回值的函数直接返回该值;多返回值返回 ethers `Result`(可按下标或名字取);整数统一是 `bigint`
92
+ - `tryAll` 中出现非法地址(如后端给的脏数据)只让该条为 `null`,不会拖垮整批
93
+ - `Contract` 暴露 ABI 里的**所有**函数(不只 view/pure),重载函数用完整签名访问:`c['getAmount(uint256,address)'](...)`
94
+
95
+ ## 自动合并
96
+
97
+ 绑定合约(`multi.contract` / `multi.erc20`)的方法返回值可以直接 `await`,等价于 `multi.call(call)`:
98
+
99
+ ```ts
100
+ const multi = new Provider(chainId, rpc, { batch: { wait: 10, maxSize: 500 } })
101
+
102
+ // 分散在不同组件里的调用,只要落在同一个收集窗口内就只发一次 eth_call
103
+ const a = multi.erc20(tokenA).balanceOf(user)
104
+ const b = multi.erc20(tokenB).balanceOf(user)
105
+ ```
106
+
107
+ - `wait`:收集窗口,默认 `0`(同一事件循环 tick);`maxSize`:达到即发出,默认 500
108
+ - 单条失败只 reject 自己(`CallFailedError`,`err.reason` 是 revert 原因)
109
+ - blockTag / from 不同的调用分开发
110
+ - 合并发生在**同一个 Provider 实例**内,应按链缓存 Provider,不要每次 `new`
111
+ - 绑定合约的 Call 只在第一次 `await` 时才发请求,传给 `all` / `tryAll` 不会额外发请求
112
+ - 末位参数可以带 overrides:`await pair.getReserves({ blockTag: 18_000_000 })`。传给 `all` / `tryAll` 时以批次的 overrides 为准(一批只能在同一个区块、同一个 from 下执行)
113
+
114
+ ## 预执行
115
+
116
+ ```ts
117
+ const router = multi.contract(routerAddress, RouterABI)
118
+
119
+ await router.swap.staticCall(params, { value, from: user }) // 写法同 ethers
120
+ await multi.staticCall(router.swap(params, { value }), { from: user })
121
+
122
+ // 批量:每条单独返回成功/失败
123
+ const results = await multi.staticCallAll(
124
+ [quoter.quoteExactInputSingle(paramsA), { call: router.swap(paramsB), overrides: { value: amountIn } }],
125
+ { from: user },
126
+ )
127
+ for (const r of results) {
128
+ if (r.success) console.log(r.data)
129
+ else console.log(r.error) // 合约 revert 时是 CallFailedError(r.error.reason)
130
+ }
131
+ ```
132
+
133
+ 预执行**不走 multicall**:multicall 里子调用的 `msg.sender` 是 multicall 合约,也无法逐条带 `value`,与真实交易不一致。每条都是对目标合约的独立 `eth_call`,同时发出(`JsonRpcProvider` 会合进一个 JSON-RPC batch 请求)。
134
+
135
+ ## 主币余额与 deployless
136
+
137
+ `getEthBalance` 在两种模式下都和合约调用在同一次 `eth_call` 里:
138
+
139
+ - 有 Multicall3:调合约自带的 `getEthBalance`
140
+ - deployless:使用本库的 [DeploylessMulticall3](contracts/DeploylessMulticall3.sol),构造函数里直接用 `BALANCE` 读取
141
+
142
+ deployless 还修正了 ethcall 自带字节码的两个上限问题(在 BSC 上实测,约 150 条 `balanceOf` 起就会失败):
143
+
144
+ | 限制 | 现象 | 处理 |
145
+ | --- | --- | --- |
146
+ | EIP-170:合约创建返回 ≤ 24KB | `max code size exceeded` | 结果过大时合约改为 `revert Aggregate3Result(...)` 带回(revert 数据没有大小限制) |
147
+ | EIP-3860:initcode ≤ 48KB | `max initcode size exceeded` | 按编码后的大小自动分片 |
148
+
149
+ 如果钱包或中间层丢掉了 revert 数据,会对半拆分重试,直到结果能正常返回。合约用 `pnpm build:contracts` 编译(solc 0.8.30,evmVersion=paris,不使用 PUSH0,兼容老链和 Tron TVM)。
150
+
151
+ ## 浏览器插件钱包
152
+
153
+ ```ts
154
+ const multi = new Provider(chainId, window.ethereum) // MetaMask / OKX / Rabby …
155
+ const tron = new Provider(TRON_CHAIN_ID.mainnet, window.tronWeb) // TronLink / OKX …
156
+ ```
157
+
158
+ - EVM 钱包内部用 ethers `BrowserProvider` 包装
159
+ - Tron 钱包请求走钱包配置的节点,预执行默认以当前连接地址作为 `from`,切换账号后自动跟随
160
+ - 请求发到的是**钱包当前所选的链**,要与传入的 `chainId` 一致;可以写成 `[window.ethereum, ...公共节点]`,钱包出错时自动用公共节点
161
+
162
+ ## Tron
163
+
164
+ ```ts
165
+ import { Provider, TRON_CHAIN_ID, TronProvider, toTronAddress } from '@w3lib/evm-batch-call'
166
+
167
+ const multi = new Provider(TRON_CHAIN_ID.mainnet) // 内置 4 个全节点,自动故障切换
168
+ // 或自定义:new Provider(TRON_CHAIN_ID.mainnet, new TronProvider({ apiKey, minInterval: 200 }))
169
+
170
+ const usdt = multi.erc20('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t')
171
+ const [symbol, balance, trx] = await Promise.all([
172
+ usdt.symbol(),
173
+ usdt.balanceOf('TNXoiAJ3dct8Fjg4M9fkLFh9S2v9TXc32G'), // 参数里的 T 地址自动转换
174
+ multi.getEthBalance('TNXoiAJ3dct8Fjg4M9fkLFh9S2v9TXc32G'), // TRX,单位 sun
175
+ ])
176
+ toTronAddress('0xa614f803b6fd780986a42c78ec9c7f77e6ded13c') // 返回值里的地址是 0x 格式,需要时转换
177
+ ```
178
+
179
+ - 主网使用链上的 Multicall3 `TEazPvZwDjDtFeJupyo7QunvnrnUjPH8ED`;Nile / Shasta 走 deployless
180
+ - 用 Tron 全节点 HTTP API 而不是 `/jsonrpc`:后者不支持 deployless、revert 不返回数据、不支持 `value`
181
+ - 只能查最新状态,传历史 `blockTag` 会报错
182
+ - 免费的 TronGrid 限频很严,`TronProvider` 支持 `concurrency`(默认 4)、`minInterval`、429 自动退避重试(`retries`,默认 3;多节点时为 0,直接切换节点)
183
+
184
+ ## 配置
185
+
186
+ ```ts
187
+ new Provider(chainId, nodes?, {
188
+ multicall: { address: '0x…', block: 0 }, // 自定义 Multicall3 地址(默认查内置表)
189
+ deployless: false, // true:强制走 deployless
190
+ chunkSize: 500, // 合约模式单次 eth_call 最多打包的调用数
191
+ batch: { wait: 0, maxSize: 500 }, // 自动合并参数
192
+ fallback: { timeout: 10_000, cooldown: 30_000 }, // 节点超时 / 出错节点冷却时间
193
+ tron: { apiKey, minInterval: 200 }, // 用 URL 创建 TronProvider 时的参数
194
+ nativeTokens: ['0xeeee…eeee'], // balances() 里视为主币的地址
195
+ })
196
+ ```
197
+
198
+ ## 从 ethcall 迁移
199
+
200
+ API 兼容,大多数情况下只需替换 import,现有调用(含 `tryAll<bigint>(calls)` 这种显式泛型写法)无需改动:
201
+
202
+ ```ts
203
+ - import { Contract, Provider } from 'ethcall'
204
+ + import { Contract, Provider } from '@w3lib/evm-batch-call'
205
+
206
+ const multi = new Provider(chainId, ethersProvider)
207
+ const token = new Contract(address, abi) // ABI 现在也可以直接用人类可读字符串
208
+ const [symbol, decimals] = await multi.all([token.symbol(), token.decimals()])
209
+ ```
210
+
211
+ 之后可以逐步简化:
212
+
213
+ ```ts
214
+ // 节点:直接传 URL 列表(或不传,用内置公共节点),不需要自己组装 FallbackProvider
215
+ const multi = new Provider(chainId, ['https://rpc-1.example', 'https://rpc-2.example'])
216
+
217
+ // 主币余额不用再单独 getBalance,和代币余额一次查
218
+ const balances = await multi.balances(account, tokens) // bigint[]
219
+
220
+ // 代币信息一次查
221
+ const infos = await multi.tokenInfo(tokens)
222
+
223
+ // Tron 也能批量查,不用逐个 triggerConstantContract
224
+ const tron = new Provider(TRON_CHAIN_ID.mainnet, window.tronWeb)
225
+
226
+ // 预执行:绑定合约后写法同 ethers
227
+ const router = multi.contract(routerAddress, RouterABI)
228
+ await router.swap.staticCall(params, { value })
229
+ ```
230
+
231
+ 与 ethcall 的行为差异:
232
+
233
+ | | ethcall 6 | evm-batch-call |
234
+ | --- | --- | --- |
235
+ | ABI | 仅 JSON(需要 `abiToJson`) | 人类可读字符串 / JSON / `Interface` |
236
+ | 合约实现 | `all`/`tryAll`/`tryEach` 分别用 Multicall1/2/3 | 全部用 Multicall3 `aggregate3` |
237
+ | 地址表有但链上地址无效 | 直接报错 | 自动退回 deployless 并记住 |
238
+ | deployless 大批量 | 约 150 条起失败 | 自动分片 + revert 带回 |
239
+ | 主币余额 | 仅有 multicall 合约时可用 | 任何模式都在同一批 |
240
+ | `tryAll` 中出现非法地址 | 整批抛错 | 只有该条为 `null` |
241
+ | 暴露的函数 | 仅 view / pure | 全部函数(便于模拟调用) |
242
+ | 节点 | 单个 ethers Provider | URL / 多节点故障切换 / 钱包 / 内置公共节点 |
243
+ | Tron、自动合并、预执行、绑定合约 | 无 | 有 |
244
+
245
+ ## 开发
246
+
247
+ ```bash
248
+ pnpm test # 单元测试(mock 节点,不需要网络)
249
+ pnpm test:live # 连真实节点:BSC / Ethereum / Tron 主网(BSC_RPC / ETH_RPC / TRON_HOST / TRON_API_KEY 可覆盖)
250
+ pnpm build # 编译到 dist
251
+ pnpm build:contracts # 修改 contracts/ 后重新生成 src/deployless.ts
252
+ pnpm check:rpc # 检查内置公共节点表的可用性(--chain=56 只查一条链)
253
+ ```
254
+
255
+ ## License
256
+
257
+ MIT。Multicall3 地址表来自 ethcall(MIT)。
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.30;
3
+
4
+ /**
5
+ * 不需要部署的 Multicall3:字节码 + 构造参数作为合约创建 eth_call 发出,构造函数里执行全部调用并带回结果。
6
+ *
7
+ * 与 ethcall 自带的 deployless 字节码相比:
8
+ * - 对 `MULTICALL3.getEthBalance(addr)` 的调用在构造函数里直接用 BALANCE 读取,
9
+ * 主币余额和合约调用在同一次 eth_call 里完成(链上没有 Multicall3 时也一样)
10
+ * - 结果超过 EIP-170 的 24KB 时,合约创建会因 "max code size exceeded" 失败,
11
+ * 此时改为通过 revert Aggregate3Result(...) 带回(revert 数据没有大小限制)
12
+ *
13
+ * 编译:pnpm build:contracts(evmVersion=paris,不使用 PUSH0,兼容老链与 Tron TVM)
14
+ */
15
+ contract DeploylessMulticall3 {
16
+ struct Call3 {
17
+ address target;
18
+ bool allowFailure;
19
+ bytes callData;
20
+ }
21
+
22
+ struct Result {
23
+ bool success;
24
+ bytes returnData;
25
+ }
26
+
27
+ /// 主币余额查询的约定目标地址(即 Multicall3 的标准地址)
28
+ address private constant MULTICALL3 = 0xcA11bde05977b3631167028862bE2a173976CA11;
29
+ /// getEthBalance(address)
30
+ bytes4 private constant GET_ETH_BALANCE = 0x4d2301cc;
31
+ /// EIP-170
32
+ uint256 private constant MAX_CODE_SIZE = 24576;
33
+
34
+ /// 结果过大时以此 revert 带回,客户端识别 selector 后按 aggregate3 的返回格式解码
35
+ error Aggregate3Result(Result[] returnData);
36
+
37
+ constructor(Call3[] memory calls) payable {
38
+ uint256 length = calls.length;
39
+ Result[] memory results = new Result[](length);
40
+ for (uint256 i; i < length; ++i) {
41
+ Call3 memory c = calls[i];
42
+ bytes memory data = c.callData;
43
+ if (c.target == MULTICALL3 && data.length == 36 && bytes4(data) == GET_ETH_BALANCE) {
44
+ address account;
45
+ assembly {
46
+ account := and(mload(add(data, 36)), 0xffffffffffffffffffffffffffffffffffffffff)
47
+ }
48
+ results[i] = Result(true, abi.encode(account.balance));
49
+ } else {
50
+ (bool success, bytes memory ret) = c.target.call(data);
51
+ if (!success && !c.allowFailure) {
52
+ revert("Multicall3: call failed");
53
+ }
54
+ results[i] = Result(success, ret);
55
+ }
56
+ }
57
+
58
+ bytes memory out = abi.encode(results);
59
+ if (out.length > MAX_CODE_SIZE) {
60
+ revert Aggregate3Result(results);
61
+ }
62
+ assembly {
63
+ return(add(out, 32), mload(out))
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,27 @@
1
+ import { Interface, type Provider as EthersProvider } from 'ethers';
2
+ import type { CallRequest, RawResult } from './call.js';
3
+ import { type Multicall } from './multicall.js';
4
+ /** 只用到 ethers Provider 的这两个方法,JsonRpcProvider / BrowserProvider 都满足 */
5
+ export type EthersLikeProvider = Pick<EthersProvider, 'call' | 'getBalance'>;
6
+ export type BlockTag = number | bigint | string;
7
+ export interface CallOverrides {
8
+ blockTag?: BlockTag;
9
+ from?: string;
10
+ }
11
+ export declare const multicall3Interface: Interface;
12
+ export interface AggregateContext {
13
+ provider: EthersLikeProvider;
14
+ chainId: number;
15
+ /** null 表示直接走 deployless */
16
+ multicall: Multicall | null;
17
+ chunkSize: number;
18
+ }
19
+ /**
20
+ * 执行一批请求,结果与 requests 一一对应。
21
+ * 有 multicall 合约就调合约的 aggregate3,否则(或合约不可用时)走 deployless。
22
+ * 超过 chunkSize 的请求会拆成多次 eth_call 并发执行(JsonRpcProvider 会把它们合到一个 HTTP batch 里)。
23
+ */
24
+ export declare function aggregate(ctx: AggregateContext, requests: CallRequest[], overrides?: CallOverrides): Promise<RawResult[]>;
25
+ /** 测试用:清掉“地址不可用”的缓存 */
26
+ export declare function resetMulticallCache(): void;
27
+ //# sourceMappingURL=aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAkC,KAAK,QAAQ,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE7G,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGvD,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAEnE,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,YAAY,CAAC,CAAA;AAE5E,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,mBAAmB,WAG9B,CAAA;AAaF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,4BAA4B;IAC5B,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,WAAW,EAAE,EACvB,SAAS,GAAE,aAAkB,GAC5B,OAAO,CAAC,SAAS,EAAE,CAAC,CActB;AA0LD,uBAAuB;AACvB,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
@@ -0,0 +1,198 @@
1
+ import { AbiCoder, Interface, concat, dataSlice, id, isError } from 'ethers';
2
+ import { DEPLOYLESS_MULTICALL3_BYTECODE } from './deployless.js';
3
+ import { isExecutionError } from './errors.js';
4
+ import { MULTICALL3_ADDRESS } from './multicall.js';
5
+ export const multicall3Interface = new Interface([
6
+ 'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)',
7
+ 'function getEthBalance(address addr) view returns (uint256 balance)',
8
+ ]);
9
+ const CALL3_TUPLE = 'tuple(address target, bool allowFailure, bytes callData)[]';
10
+ const RESULT_TUPLE = 'tuple(bool success, bytes returnData)[]';
11
+ /** error Aggregate3Result((bool,bytes)[]) */
12
+ const AGGREGATE3_RESULT_SELECTOR = id('Aggregate3Result((bool,bytes)[])').slice(0, 10);
13
+ /**
14
+ * 判定为无效的 multicall 地址(无代码 / 不是 Multicall3),按 chainId+address 记在模块级,
15
+ * 这样业务里每次 new Provider 也只会多试一次。
16
+ */
17
+ const unusableMulticalls = new Set();
18
+ /**
19
+ * 执行一批请求,结果与 requests 一一对应。
20
+ * 有 multicall 合约就调合约的 aggregate3,否则(或合约不可用时)走 deployless。
21
+ * 超过 chunkSize 的请求会拆成多次 eth_call 并发执行(JsonRpcProvider 会把它们合到一个 HTTP batch 里)。
22
+ */
23
+ export async function aggregate(ctx, requests, overrides = {}) {
24
+ if (requests.length === 0) {
25
+ return [];
26
+ }
27
+ const size = Math.max(1, ctx.chunkSize);
28
+ if (requests.length <= size) {
29
+ return aggregateChunk(ctx, requests, overrides);
30
+ }
31
+ const chunks = [];
32
+ for (let i = 0; i < requests.length; i += size) {
33
+ chunks.push(requests.slice(i, i + size));
34
+ }
35
+ const results = await Promise.all(chunks.map((chunk) => aggregateChunk(ctx, chunk, overrides)));
36
+ return results.flat();
37
+ }
38
+ async function aggregateChunk(ctx, requests, overrides) {
39
+ const multicall = ctx.multicall;
40
+ if (multicall && isUsable(ctx.chainId, multicall, overrides.blockTag)) {
41
+ const results = await callContract(ctx, multicall, requests, overrides);
42
+ if (Array.isArray(results)) {
43
+ return results;
44
+ }
45
+ // 只有“最新状态下地址上没有可用合约”才记缓存。
46
+ // - revert 无数据('reverted')有歧义:可能是 out of gas / 节点 gas 上限、子调用失败而钱包丢了 revert 数据,
47
+ // 这些情况地址本身是好的,只对这一次退回 deployless,不记缓存
48
+ // - 历史区块查到无代码可能只是那时还没部署,不能推及 latest
49
+ if (results === 'invalid' && isLatest(overrides.blockTag)) {
50
+ unusableMulticalls.add(cacheKey(ctx.chainId, multicall.address));
51
+ }
52
+ }
53
+ return callDeployless(ctx, requests, overrides);
54
+ }
55
+ /**
56
+ * 'invalid':地址上没有代码或返回无法解码;'reverted':执行 revert 且没有 revert 数据。
57
+ * 两种情况都退回 deployless。
58
+ */
59
+ async function callContract(ctx, multicall, requests, overrides) {
60
+ const calls = requests.map((req) => ({
61
+ target: req.ethBalanceOf ? multicall.address : req.target,
62
+ allowFailure: req.allowFailure,
63
+ callData: req.callData,
64
+ }));
65
+ let data;
66
+ try {
67
+ data = await ctx.provider.call({
68
+ to: multicall.address,
69
+ data: multicall3Interface.encodeFunctionData('aggregate3', [calls]),
70
+ blockTag: overrides.blockTag,
71
+ from: overrides.from,
72
+ });
73
+ }
74
+ catch (err) {
75
+ // aggregate3 自己的失败(allowFailure=false 的子调用失败)会带 "Multicall3: call failed" 的 revert 数据;
76
+ // 执行层面 revert 且没有数据,基本就是这个地址上不是 Multicall3。
77
+ // 限流、节点缺状态等错误 ethers 也会包装成 CALL_EXCEPTION,那些要原样抛出,不能把地址判成无效
78
+ if (isExecutionError(err) && !hasRevertData(err)) {
79
+ return 'reverted';
80
+ }
81
+ throw err;
82
+ }
83
+ // 地址上没有代码时 eth_call 返回 0x
84
+ if (!data || data === '0x') {
85
+ return 'invalid';
86
+ }
87
+ try {
88
+ return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0]);
89
+ }
90
+ catch {
91
+ return 'invalid';
92
+ }
93
+ }
94
+ /**
95
+ * deployless:字节码 + 构造参数作为合约创建 eth_call 发出。
96
+ * 主币余额(ethBalanceOf)也在合约里用 BALANCE 读取,和其他调用在同一次请求里。
97
+ *
98
+ * 合约创建受 EIP-3860 限制,initcode(字节码 + 参数)不能超过 48KB,这里按编码后的大小切片;
99
+ * 返回超过 24KB 的情况由合约改用 revert 带回,见 contracts/DeploylessMulticall3.sol。
100
+ */
101
+ async function callDeployless(ctx, requests, overrides) {
102
+ const chunks = splitByInitcodeSize(requests);
103
+ const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides)));
104
+ return results.flat();
105
+ }
106
+ async function callDeploylessChunk(ctx, requests, overrides) {
107
+ const args = AbiCoder.defaultAbiCoder().encode([CALL3_TUPLE], [
108
+ requests.map(({ target, allowFailure, callData, ethBalanceOf }) => ({
109
+ target: ethBalanceOf ? MULTICALL3_ADDRESS : target,
110
+ allowFailure,
111
+ callData,
112
+ })),
113
+ ]);
114
+ let data;
115
+ try {
116
+ data = await ctx.provider.call({
117
+ data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]),
118
+ blockTag: overrides.blockTag,
119
+ from: overrides.from,
120
+ });
121
+ }
122
+ catch (err) {
123
+ const revertData = err.data;
124
+ if (isError(err, 'CALL_EXCEPTION') && revertData?.startsWith(AGGREGATE3_RESULT_SELECTOR)) {
125
+ // 结果超过 24KB,合约通过 revert Aggregate3Result(...) 带回
126
+ return decodeAggregate3(AbiCoder.defaultAbiCoder().decode([RESULT_TUPLE], dataSlice(revertData, 4))[0]);
127
+ }
128
+ // 有的钱包 / 中间层会丢掉 revert 数据,大结果拿不回来:对半拆开重试,小批量能正常 return
129
+ if (requests.length > 1 && isExecutionError(err) && !hasRevertData(err)) {
130
+ const mid = Math.ceil(requests.length / 2);
131
+ const [a, b] = await Promise.all([
132
+ callDeploylessChunk(ctx, requests.slice(0, mid), overrides),
133
+ callDeploylessChunk(ctx, requests.slice(mid), overrides),
134
+ ]);
135
+ return [...a, ...b];
136
+ }
137
+ throw err;
138
+ }
139
+ return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0]);
140
+ }
141
+ /** EIP-3860 initcode 上限 49152 字节,留出余量 */
142
+ const MAX_INITCODE_SIZE = 48_000;
143
+ const BYTECODE_SIZE = (DEPLOYLESS_MULTICALL3_BYTECODE.length - 2) / 2;
144
+ function splitByInitcodeSize(requests) {
145
+ // 动态数组头 64 字节;每条 = 数组内偏移 32 + (target, allowFailure, bytes 偏移) 96 + bytes 长度 32 + 补齐后的数据
146
+ const budget = MAX_INITCODE_SIZE - BYTECODE_SIZE - 64;
147
+ const chunks = [];
148
+ let current = [];
149
+ let size = 0;
150
+ for (const req of requests) {
151
+ const dataLength = (req.callData.length - 2) / 2;
152
+ const itemSize = 160 + Math.ceil(dataLength / 32) * 32;
153
+ if (current.length && size + itemSize > budget) {
154
+ chunks.push(current);
155
+ current = [];
156
+ size = 0;
157
+ }
158
+ current.push(req);
159
+ size += itemSize;
160
+ }
161
+ if (current.length) {
162
+ chunks.push(current);
163
+ }
164
+ return chunks;
165
+ }
166
+ function hasRevertData(err) {
167
+ const data = err.data;
168
+ return Boolean(data && data !== '0x');
169
+ }
170
+ function decodeAggregate3(raw) {
171
+ return raw.map(([success, returnData]) => ({ success, returnData }));
172
+ }
173
+ function isUsable(chainId, multicall, blockTag) {
174
+ if (unusableMulticalls.has(cacheKey(chainId, multicall.address))) {
175
+ return false;
176
+ }
177
+ if (blockTag === 'earliest') {
178
+ return false;
179
+ }
180
+ if (typeof blockTag === 'number' || typeof blockTag === 'bigint') {
181
+ return BigInt(multicall.block) < BigInt(blockTag);
182
+ }
183
+ if (typeof blockTag === 'string' && /^0x[0-9a-f]+$/i.test(blockTag)) {
184
+ return BigInt(multicall.block) < BigInt(blockTag);
185
+ }
186
+ return true;
187
+ }
188
+ function isLatest(blockTag) {
189
+ return blockTag === undefined || blockTag === null || blockTag === 'latest' || blockTag === 'pending';
190
+ }
191
+ function cacheKey(chainId, address) {
192
+ return `${chainId}:${address.toLowerCase()}`;
193
+ }
194
+ /** 测试用:清掉“地址不可用”的缓存 */
195
+ export function resetMulticallCache() {
196
+ unusableMulticalls.clear();
197
+ }
198
+ //# sourceMappingURL=aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.js","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type Call, type CallRequest, type RawResult } from './call.js';
2
+ import type { CallOverrides } from './aggregate.js';
3
+ export interface BatchOptions {
4
+ /** 收集窗口(毫秒),窗口内发起的 call 合并成一次 multicall。默认 0:同一事件循环 tick 内的调用合并 */
5
+ wait?: number;
6
+ /** 队列达到这个数量立即发出,不再等窗口结束。默认 500 */
7
+ maxSize?: number;
8
+ }
9
+ type Runner = (requests: CallRequest[], overrides: CallOverrides) => Promise<RawResult[]>;
10
+ /**
11
+ * DataLoader 式的自动合并:各处独立调用 `provider.call(x)`,
12
+ * 落在同一个收集窗口、且 blockTag/from 相同的调用会合成一次 aggregate3。
13
+ * 相同的 (target, callData) 只请求一次。
14
+ */
15
+ export declare class Batcher {
16
+ #private;
17
+ constructor(run: Runner, options?: BatchOptions);
18
+ load<T>(call: Call, overrides?: CallOverrides): Promise<T>;
19
+ }
20
+ export {};
21
+ //# sourceMappingURL=batcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../src/batcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAA;AAC/F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnD,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,KAAK,MAAM,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;AAczF;;;;GAIG;AACH,qBAAa,OAAO;;gBAMN,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;IAMnD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,GAAE,aAAkB,GAAG,OAAO,CAAC,CAAC,CAAC;CAmF/D"}