@w3lib/evm-batch-call 0.4.1 → 0.5.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 +21 -0
- package/README.md +166 -3
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +153 -40
- package/dist/aggregate.js.map +1 -1
- package/dist/fallback.d.ts +20 -0
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +36 -5
- package/dist/fallback.js.map +1 -1
- package/dist/lp.d.ts +149 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +499 -0
- package/dist/lp.js.map +1 -0
- package/dist/lpMath.d.ts +25 -0
- package/dist/lpMath.d.ts.map +1 -0
- package/dist/lpMath.js +103 -0
- package/dist/lpMath.js.map +1 -0
- package/dist/lpV4.d.ts +108 -0
- package/dist/lpV4.d.ts.map +1 -0
- package/dist/lpV4.js +283 -0
- package/dist/lpV4.js.map +1 -0
- package/dist/owner.d.ts +1 -1
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +18 -5
- package/dist/owner.js.map +1 -1
- package/dist/scan.d.ts +54 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +124 -92
- package/dist/scan.js.map +1 -1
- package/dist/source.d.ts +2 -0
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +4 -0
- package/dist/source.js.map +1 -1
- package/dist/subpaths/lp.d.ts +33 -0
- package/dist/subpaths/lp.d.ts.map +1 -0
- package/dist/subpaths/lp.js +47 -0
- package/dist/subpaths/lp.js.map +1 -0
- package/dist/subpaths/write.d.ts +20 -0
- package/dist/subpaths/write.d.ts.map +1 -0
- package/dist/subpaths/write.js +18 -0
- package/dist/subpaths/write.js.map +1 -0
- package/dist/tron.d.ts +2 -0
- package/dist/tron.d.ts.map +1 -1
- package/dist/tron.js +4 -0
- package/dist/tron.js.map +1 -1
- package/dist/write/batch.d.ts +88 -0
- package/dist/write/batch.d.ts.map +1 -0
- package/dist/write/batch.js +231 -0
- package/dist/write/batch.js.map +1 -0
- package/dist/write/collect.d.ts +53 -0
- package/dist/write/collect.d.ts.map +1 -0
- package/dist/write/collect.js +255 -0
- package/dist/write/collect.js.map +1 -0
- package/dist/write/rpc.d.ts +68 -0
- package/dist/write/rpc.d.ts.map +1 -0
- package/dist/write/rpc.js +236 -0
- package/dist/write/rpc.js.map +1 -0
- package/dist/write/send.d.ts +124 -0
- package/dist/write/send.d.ts.map +1 -0
- package/dist/write/send.js +373 -0
- package/dist/write/send.js.map +1 -0
- package/dist/write/signer.d.ts +58 -0
- package/dist/write/signer.d.ts.map +1 -0
- package/dist/write/signer.js +41 -0
- package/dist/write/signer.js.map +1 -0
- package/dist/write/transfer.d.ts +83 -0
- package/dist/write/transfer.d.ts.map +1 -0
- package/dist/write/transfer.js +259 -0
- package/dist/write/transfer.js.map +1 -0
- package/dist/write/tron.d.ts +58 -0
- package/dist/write/tron.d.ts.map +1 -0
- package/dist/write/tron.js +956 -0
- package/dist/write/tron.js.map +1 -0
- package/package.json +27 -14
- package/src/aggregate.ts +168 -36
- package/src/fallback.ts +55 -11
- package/src/lp.ts +721 -0
- package/src/lpMath.ts +113 -0
- package/src/lpV4.ts +406 -0
- package/src/owner.ts +19 -6
- package/src/scan.ts +155 -86
- package/src/source.ts +5 -0
- package/src/subpaths/lp.ts +71 -0
- package/src/subpaths/write.ts +26 -0
- package/src/tron.ts +5 -0
- package/src/write/batch.ts +302 -0
- package/src/write/collect.ts +312 -0
- package/src/write/rpc.ts +277 -0
- package/src/write/send.ts +490 -0
- package/src/write/signer.ts +90 -0
- package/src/write/transfer.ts +344 -0
- package/src/write/tron.ts +1100 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### 新功能
|
|
6
|
+
|
|
7
|
+
- 批量写操作,子路径 `@w3lib/evm-batch-call/write`(只做查询时打包不会带上):
|
|
8
|
+
- `batchCall`:批量调用合约。`'tx'` 模式每个调用一笔交易,同一钱包按 nonce 顺序连续发出、不同钱包并发;`'7702'` 模式通过 EIP-7702 委托给 Simple7702Account,同一钱包的调用合成一笔交易。依赖前一笔结果的调用(先 approve 再调用)预估失败时自动等前一笔确认后重新预估
|
|
9
|
+
- `batchTransfer`:一对多转账(主币 / ERC20 可混合),默认走 Disperse 合约,链上没有合约、只有一个收款人、某一批预估 revert 时自动改为逐笔发送;为 Disperse 发出的 approve 交易在结果的 `approvals` 里
|
|
10
|
+
- `batchCollect`:多钱包归集,可选 `gasWallet` 自动补 gas(只补差额)、扫空剩余主币(扣除手续费,OP Stack / Scroll 预留 L1 数据费;代币没有全部归集成功的钱包不扫)
|
|
11
|
+
- `revokeDelegation`:撤销 EIP-7702 委托
|
|
12
|
+
- Tron:同样的三个函数传 Tron 的 Provider 即可(TRX / TRC20 转账、合约调用、归集含补 gas 和激活账户)。交易本地编码签名、`broadcasthex` 广播,合约调用先模拟,过期按链上时间判断后自动重发;支持浏览器钱包(tronWeb / tronwallet-adapter);这部分按需加载
|
|
13
|
+
- 钱包可以传私钥或 ethers Signer(含浏览器钱包的 JsonRpcSigner);结果可以直接 JSON 序列化,`onUpdate` 回调进度,支持 `dryRun` / `wait: false` / `signal`
|
|
14
|
+
- `FallbackRpc.request(method, fn)`:用任意方法请求节点,与 call / getBalance 相同的故障切换
|
|
15
|
+
- LP 仓位,子路径 `@w3lib/evm-batch-call/lp`:`getLpPositions` / `lpPositions` 查一个地址的 V2 LP 代币、V3 / V4 仓位 NFT,两种代币的数量、未领取手续费(与合约 `decreaseLiquidity` / `collect` 的结果逐位核对过)、可选美元价值。内置 Uniswap、PancakeSwap、SushiSwap(7 条 EVM 链)和 Tron 的 SunSwap;V4 支持传 tokenId 或扫描 Transfer 事件发现仓位
|
|
16
|
+
- DefiLlama 价格源支持 Tron(资产列表在 Tron 上也可以带价格)
|
|
17
|
+
|
|
18
|
+
## 0.4.2
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
|
|
22
|
+
- `minBlock`:节点落后但不报错(返回早于 `minBlock` 的区块)时当场换下一个节点;原来会一直在这个节点上重试,直到约 10 秒后报错,从不问已经跟上的节点。区块号校验移到故障切换里做(deployless 拆成多片时每片各自校验,不会混进落后节点的旧数据):跟上的节点直接给出最新状态,落后的节点之后排到后面;所有节点都落后时按区块号重查一次,然后等待最新状态跟上;落后不进入冷却,也不会被误判成 Multicall3 不可用
|
|
23
|
+
|
|
3
24
|
## 0.4.1
|
|
4
25
|
|
|
5
26
|
### 修复
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @w3lib/evm-batch-call
|
|
2
2
|
|
|
3
|
-
基于 Multicall3 的 EVM / Tron 批量合约读取与交易预执行库,依赖 ethers v6,API 兼容 [ethcall](https://github.com/Destiner/ethcall)
|
|
3
|
+
基于 Multicall3 的 EVM / Tron 批量合约读取与交易预执行库,依赖 ethers v6,API 兼容 [ethcall](https://github.com/Destiner/ethcall)。另有[批量写操作](#批量写操作)(批量转账、归集、批量调用合约)。
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @w3lib/evm-batch-call ethers
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
**在线 demo**:<https://web3-lib.github.io/libs/evm/>(余额、代币信息、资产列表、LP 仓位、NFT,8 条链含 Tron,显示每次查询实际发出的节点请求数)。本地运行:`pnpm --filter @w3lib/evm-batch-call demo`(http://localhost:5174)
|
|
10
|
+
|
|
9
11
|
## 快速上手
|
|
10
12
|
|
|
11
13
|
### 常用查询
|
|
@@ -50,8 +52,9 @@ await getErc1155Balances(user, [{ contract: ITEMS, tokenId: 7 }], { chainId: 137
|
|
|
50
52
|
| `getNftTokenUris(items, opts)` | `nftTokenUris(multi, items, opts)` | **`/nft`**。元数据地址:自动兼容 ERC721 `tokenURI` 与 ERC1155 `uri`(`{id}` 按规范替换),`ipfsGateway` 转换 `ipfs://`;首次查询顺带识别集合标准并缓存,之后只发对应的调用 |
|
|
51
53
|
| `getErc1155Balances(owner, items, opts)` | `erc1155Balances(multi, owner, items)` | **`/nft`**。ERC1155 余额 |
|
|
52
54
|
| `getOwnerTokens(owner, opts)` | `ownerTokens(multi, owner, opts)` | **子路径 `/owner`**。资产列表:地址持有的代币(**不推荐使用**,结果不保证完整,见[资产列表](#资产列表)) |
|
|
55
|
+
| `getLpPositions(owner, opts)` | `lpPositions(multi, owner, opts)` | **子路径 `/lp`**。LP 仓位:V2 的 LP 代币、V3 / V4 的仓位 NFT,代币数量、未领取手续费、可选美元价值,见 [LP 仓位](#lp-仓位) |
|
|
53
56
|
|
|
54
|
-
**子路径**:NFT 查询在 `@w3lib/evm-batch-call/nft`,资产列表(含代币来源、价格源、Transfer 增量扫描)在 `@w3lib/evm-batch-call/owner`。不用这些功能时打包不会带上它们的代码(拆出后约少 15KB:只用 `getBalances` 时 minify 后约
|
|
57
|
+
**子路径**:NFT 查询在 `@w3lib/evm-batch-call/nft`,资产列表(含代币来源、价格源、Transfer 增量扫描)在 `@w3lib/evm-batch-call/owner`,LP 仓位在 `@w3lib/evm-batch-call/lp`,批量写操作在 `@w3lib/evm-batch-call/write`。不用这些功能时打包不会带上它们的代码(拆出后约少 15KB:只用 `getBalances` 时 minify 后约 50KB、gzip 约 17KB,均不含 ethers)。
|
|
55
58
|
|
|
56
59
|
共同的规则:
|
|
57
60
|
|
|
@@ -83,7 +86,7 @@ await getBalances(user, tokens, { chainId: 56, withBlock: true }) // 每项带 b
|
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
- **`withBlock`**:区块号与数据在同一次 `eth_call` 里读出(Multicall3 的 `getBlockNumber`;deployless 合约里直接取 `block.number`),不多发请求。`balances` / `tokens` / `allowances` 及对应函数都支持
|
|
86
|
-
- **`minBlock
|
|
89
|
+
- **`minBlock`**:先按最新状态查并顺带读区块号,节点落后(结果的区块号早于 `minBlock`)时当场换下一个节点,跟上的节点直接给出最新状态;所有节点都落后时按 `minBlock` 指定区块重查——落后的节点对未来区块报错或返回较旧区块,同样换节点(落后不算故障,不进入冷却);仍然都落后时每秒重试,最多约 10 秒后抛错,传了 `signal` 时取消后立即停止。重查得到的是 `minBlock` 那个区块的状态(已包含刚确认的交易)。只对最新状态的查询生效(指定了 `blockTag` 时忽略)。Tron 节点只能查最新状态,落后时等节点跟上再查。`nativeBalance: 'rpc'` 时主币余额在批量查询实际读取的区块上用 `eth_getBalance` 读取,与代币余额、`blockNumber` 一致
|
|
87
90
|
- **`watchBalances`**:相同的节点参数 + 钱包 + 代币 + 选项共用一份轮询(多处订阅时取最小的 `interval`),新订阅者立即拿到最近一次的结果;查询失败调用 `onError`,轮询继续;上一次查询没完成时不会重叠发起;最后一个订阅者取消后停止
|
|
88
91
|
|
|
89
92
|
### 节点参数
|
|
@@ -237,6 +240,64 @@ await getOwnerTokens(user, { chainId: 1, scanTransfers: { lookbackBlocks: 20_000
|
|
|
237
240
|
7. 使用 Alchemy / NodeReal 时受它们的免费额度和限频约束,翻页次数默认最多 20 页(2000 个代币),可用 `maxPages` 调整
|
|
238
241
|
8. 开启 `scanTransfers` 时另有局限,见上面「增量扫描」
|
|
239
242
|
|
|
243
|
+
## LP 仓位
|
|
244
|
+
|
|
245
|
+
查一个地址在各 DEX 的流动性仓位(子路径 `@w3lib/evm-batch-call/lp`):V2 的 LP 代币、V3 / V4 的仓位 NFT,每个仓位的两种代币数量、未领取的手续费,可选美元价值。所有合约读取都用 multicall 批量完成(整个查询通常 4~6 次请求)。
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { getLpPositions } from '@w3lib/evm-batch-call/lp'
|
|
249
|
+
|
|
250
|
+
await getLpPositions(user, { chainId: 56 }) // 这条链上内置的全部协议
|
|
251
|
+
await getLpPositions(user, { chainId: 56, prices: true }) // 带美元价值(DefiLlama),按价值排序
|
|
252
|
+
await getLpPositions(user, { chainId: 56, tokens: heldTokens }) // V2:再用这些代币 × 计价币找池子
|
|
253
|
+
await getLpPositions(user, { chainId: 1, v4: { tokenIds: [12345n] } }) // V4:已知的仓位
|
|
254
|
+
await getLpPositions(user, { chainId: 1, v4: { scan: { storage } } }) // V4:扫描 Transfer 事件发现仓位
|
|
255
|
+
// [
|
|
256
|
+
// { version: 'v3', protocol: 'pancakeswap-v3', protocolName: 'PancakeSwap V3', pool: '0x…', tokenId: '123', positionManager: '0x…',
|
|
257
|
+
// token0: { address, symbol: 'WBNB', decimals: 18, native: false }, token1: { symbol: 'USDT', … },
|
|
258
|
+
// amount0: '1200000000000000000', amount0Formatted: '1.2', amount1Formatted: '700',
|
|
259
|
+
// fees0Formatted: '0.003', fees1Formatted: '1.8', fee: 500, tickLower, tickUpper, tick, inRange: true,
|
|
260
|
+
// liquidity, price: 583.2, priceLower: 500, priceUpper: 650, valueUsd: 1400, feesUsd: 3.6, chainId: 56 },
|
|
261
|
+
// { version: 'v2', protocol: 'pancakeswap-v2', pool: '0x…', balance, totalSupply, share: 0.0001, amount0Formatted, amount1Formatted, valueUsd, … },
|
|
262
|
+
// ]
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 内置的协议
|
|
266
|
+
|
|
267
|
+
| 链 | V2 | V3 | V4 |
|
|
268
|
+
| --- | --- | --- | --- |
|
|
269
|
+
| Ethereum | Uniswap、SushiSwap、PancakeSwap | Uniswap、PancakeSwap | Uniswap |
|
|
270
|
+
| BSC | PancakeSwap、Uniswap、SushiSwap | PancakeSwap、Uniswap | Uniswap |
|
|
271
|
+
| Base | Uniswap、PancakeSwap | Uniswap、PancakeSwap | Uniswap |
|
|
272
|
+
| Arbitrum | Uniswap、SushiSwap、PancakeSwap | Uniswap、PancakeSwap | Uniswap |
|
|
273
|
+
| Optimism | Uniswap | Uniswap | Uniswap |
|
|
274
|
+
| Polygon | Uniswap、SushiSwap | Uniswap | Uniswap |
|
|
275
|
+
| Avalanche | | Uniswap | |
|
|
276
|
+
| Tron | SunSwap | SunSwap | |
|
|
277
|
+
|
|
278
|
+
地址都在链上核对过(2026-10)。其他 DEX / 链传自己的协议:`protocols: [{ id: 'my-dex', name: 'My DEX', version: 'v3', chainId, positionManager, factory }]`(V2 只需要 `factory`;V3 需要 `positionManager` 和 `factory`,ABI 与 Uniswap V3 相同的分叉都可以;V4 需要 `positionManager` 和 `stateView`,`deployBlock` 可选)。`protocols: ['uniswap-v3']` 只查某些协议,`versions: ['v3']` 只查某些类型。
|
|
279
|
+
|
|
280
|
+
### 每种类型怎么查、能不能查全
|
|
281
|
+
|
|
282
|
+
| 类型 | 能否查全 | 做法 |
|
|
283
|
+
| --- | --- | --- |
|
|
284
|
+
| **V3**(仓位 NFT) | 能 | 仓位合约可以按地址列出 NFT:`balanceOf` → `tokenOfOwnerByIndex` → `positions`,再读池子的价格和手续费增长。仓位很多的地址只核对按持有顺序最后的 `maxPositions` 个(默认 500,大致是最近的) |
|
|
285
|
+
| **V2**(LP 代币) | **尽量** | 链上没有“地址持有哪些代币”的索引。默认用计价币之间的组合(WETH/USDT、WBNB/USDT…)在各工厂里找池子;`tokens` 传入更多代币(如资产列表里的)一起组合;`pools` 直接传池子地址;`source: alchemy({ apiKey })` 用代币来源列出地址持有的全部 ERC20(包括 LP 代币,见[资产列表](#资产列表))。找到的 LP 代币按 `token0` / `token1` / `factory` 识别池子和协议,工厂不在协议表里的显示为 `Unknown V2` |
|
|
286
|
+
| **V4**(仓位 NFT) | **需要配置** | V4 的仓位合约不能按地址列出 NFT。`v4.tokenIds` 传已知的仓位;`v4.scan` 扫描仓位合约发给该地址的 NFT(`Transfer` 事件),扫到的再核对是否仍归这个地址(已转走 / 销毁的从进度里去掉)。两个都不传时不查 V4;候选太多时只核对 tokenId 最大的 `maxPositions` 个 |
|
|
287
|
+
|
|
288
|
+
**V4 扫描**(`v4: { scan: true }` 或传选项):每次调用从上次的位置往后扫,进度默认在内存里,`storage: keyValueLpScanStorage(localStorage)` 可以跨页面刷新接着扫。`fromBlock`:`'deployment'`(默认,从仓位合约部署的区块开始扫全部历史;自定义协议没传 `deployBlock` 时等同于 `'latest'`)、`'latest'`(只发现之后的新仓位)或区块号。只查仓位合约的事件,单次范围默认 10000 个区块,节点有上限时自动缩小,限频 / 超时自动退避(与[增量扫描](#增量扫描可选)相同)。扫全部历史在公共节点上很慢(BSC 公共节点每次只能查几百个区块),每次调用只推进 `maxRequests` 个请求 / `timeBudget` 时间,建议用自己的节点或把进度存起来。**没扫到最新区块之前 V4 的结果不完整**(从旧往新扫,最近铸造的仓位最后才扫到):`onProgress: ({ cursor, target, done, pausedUntil }) => …` 可以拿到进度,没扫完时提示用户或再调用几次(节点不开放 getLogs 时会暂停一段时间:触发暂停的那次带 `pausedUntil`,暂停期间的调用 `target` 为 null;拿不到最新区块时 `target` 也为 null)。`confirmations`(默认 5)不要设为 0:刚出的区块可能被重组。已转走 / 销毁的仓位只在节点跟上扫描进度时才从进度里去掉,节点落后时照常返回结果、这次不清理。改 `fromBlock` 只对还没有进度的地址生效(进度按链 + 仓位合约 + 地址保存)。
|
|
289
|
+
|
|
290
|
+
### 数值
|
|
291
|
+
|
|
292
|
+
- **数量**(`amount0` / `amount1`):V2 按份额(`储备 × 持有 / 总量`);V3 / V4 是在当前价格下撤出全部流动性能拿到的数量(与合约的 `decreaseLiquidity` 结果逐位一致,不含手续费)
|
|
293
|
+
- **手续费**(`fees0` / `fees1`,V3 / V4):未领取的手续费,按池子的手续费增长计算,不用发交易。V3 与仓位合约记的账一致;仓位合约按每个仓位单独取整、池子按同一区间合并记账,实际能领到的偶尔会少几 wei。V2 的手续费已经计入份额,没有单独的未领取手续费
|
|
294
|
+
- **价格**(`price` / `priceLower` / `priceUpper`):token1 / token0,按精度换算的浮点数,只用于展示;`inRange` 表示当前价格在仓位区间内(在区间内才赚手续费)
|
|
295
|
+
- **已清空的仓位**:V3 / V4 撤出全部流动性、手续费也领完后 NFT 还在,默认不返回;`includeClosed: true` 时返回
|
|
296
|
+
- **美元价值(可选)**:默认不查;价格源出错(服务不可用、被拦截等)时照样返回仓位,只是 `valueUsd` / `feesUsd` 为 `null`;传 `onPriceError: (err) => …` 可以知道是价格服务出错(而不是代币没有价格)。`prices: true` 用 DefiLlama(与资产列表相同),也可以传自己的 `PriceSource`。`valueUsd` 是仓位里两种代币的价值,`feesUsd` 是未领取手续费的价值;数量不为 0 的代币没有价格时为 `null`;开启后结果按价值从高到低排序
|
|
297
|
+
- **Tron**:地址(池子、代币、仓位合约)都是 T 开头的格式;价格用 DefiLlama 的 Tron 数据
|
|
298
|
+
|
|
299
|
+
**局限**:PancakeSwap 等质押到农场(MasterChef)的仓位 NFT 不在用户名下,查不到;V2 只能尽量查(见上表);V4 需要传 `tokenIds` 或开启扫描。
|
|
300
|
+
|
|
240
301
|
## 功能
|
|
241
302
|
|
|
242
303
|
- **常用查询**:余额、代币详情、授权额度、NFT(集合 / 持有 / 元数据地址 / ERC1155 余额,子路径 `/nft`),一个函数一次请求
|
|
@@ -360,6 +421,108 @@ for (const r of results) {
|
|
|
360
421
|
|
|
361
422
|
预执行**不走 multicall**:multicall 里子调用的 `msg.sender` 是 multicall 合约,也无法逐条带 `value`,与真实交易不一致。每条都是对目标合约的独立 `eth_call`,同时发出(`JsonRpcProvider` 会合进一个 JSON-RPC batch 请求)。
|
|
362
423
|
|
|
424
|
+
## 批量写操作
|
|
425
|
+
|
|
426
|
+
子路径 `@w3lib/evm-batch-call/write`,只做查询时打包不会带上。三个函数都复用 `Provider`(节点故障切换、一次 multicall 读余额 / decimals)。在已经用到 `Provider` 的基础上,只引入 `batchCall` 约增加 12KB(minify,gzip 约 4KB),三个函数都引入约 21KB(gzip 约 8KB),均不含 ethers;私钥用 ethers 的 `BaseWallet` 签名,不会带上 `Wallet` 的 keystore 加解密代码(约 74KB):
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { Contract, ERC20_ABI, NATIVE_TOKEN, Provider } from '@w3lib/evm-batch-call'
|
|
430
|
+
import { batchCall, batchCollect, batchTransfer } from '@w3lib/evm-batch-call/write'
|
|
431
|
+
|
|
432
|
+
const multi = new Provider(56, [rpc1, rpc2])
|
|
433
|
+
|
|
434
|
+
// 一对多转账:默认走 Disperse 合约,每种代币一笔交易(代币先 approve 本次的精确总额)
|
|
435
|
+
await batchTransfer(multi, privateKey, [
|
|
436
|
+
{ to: a, formatted: '1.5' }, // 默认代币(token 选项,默认主币)
|
|
437
|
+
{ to: b, amount: 10n ** 18n }, // amount 是最小单位,formatted 按精度换算,二选一
|
|
438
|
+
{ to: c, token: USDT, formatted: '20' }, // 主币、代币可以混在一起
|
|
439
|
+
])
|
|
440
|
+
|
|
441
|
+
// 归集:多个钱包 → 一个地址;主币不够付手续费的子钱包先由 gasWallet 补 gas
|
|
442
|
+
const { results, topUps } = await batchCollect(multi, [key1, key2, key3], hotWallet, {
|
|
443
|
+
tokens: [USDT, NATIVE_TOKEN], // 包含主币:代币归集确认后,扣掉手续费扫空剩余主币
|
|
444
|
+
gasWallet: gasKey,
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
// 通用批量调用合约
|
|
448
|
+
const usdt = new Contract(USDT, ERC20_ABI)
|
|
449
|
+
await batchCall(multi, [
|
|
450
|
+
{ from: key1, call: usdt.approve(router, amount) },
|
|
451
|
+
{ from: key1, to: router, data: swapData }, // 依赖上一笔:预估失败时先等 approve 确认再预估
|
|
452
|
+
{ from: key2, to: someone, value: 10n ** 16n },
|
|
453
|
+
])
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**钱包**:私钥字符串,或 ethers Signer。能本地签名的(`Wallet` 等)自己管理 nonce,可以连续发出、等待时重发;浏览器钱包的 `JsonRpcSigner` 交给钱包签名并发送(每笔需要用户确认,不支持 `'7702'`)。
|
|
457
|
+
|
|
458
|
+
**发送方式**:
|
|
459
|
+
|
|
460
|
+
| mode | 说明 | 用于 |
|
|
461
|
+
| --- | --- | --- |
|
|
462
|
+
| `'tx'` | 每个调用一笔交易。同一钱包按 nonce 顺序连续发出(不等上一笔确认),不同钱包并发(`concurrency`,默认 8) | `batchCall` 默认;所有链 |
|
|
463
|
+
| `'disperse'` | [Disperse](https://disperse.app) 合约(`DISPERSE_ADDRESS`),每种代币每 `batchSize`(默认 200)个收款人一笔交易 | `batchTransfer` 默认 |
|
|
464
|
+
| `'7702'` | 钱包通过 EIP-7702 委托给 Simple7702Account(`SIMPLE_7702_ACCOUNT`,eth-infinitism),同一钱包的调用合成一笔发给自己的 `executeBatch` 交易(每 `batchSize`,默认 50 个一笔),一笔里的调用要么全成功要么全失败;`msg.sender` 仍是钱包本身 | `batchCall` / `batchTransfer` 可选 |
|
|
465
|
+
|
|
466
|
+
- **Disperse 自动退回逐笔发送**:链上没有 Disperse 合约、某种代币只有一个收款人、某一批预估就 revert(收款地址拒收主币、转账收税的代币、被拉黑的地址等)时,这部分改为逐笔发送,出问题的收款项单独标记失败。结果里的 `via` 是实际的发送方式。Disperse 只会从调用者自己的账户 `transferFrom`,approve 给它的额度别人无法动用
|
|
467
|
+
- **7702 会改变账户**:委托一直保留,直到撤销(`revokeDelegation(multi, key)`)。钱包已委托给其他合约时默认报错,传 `redelegate: true` 才会改。需要链支持 EIP-7702、委托合约在这条链上已部署(没部署时直接报错:委托给没有代码的地址时调用会“成功”但什么都不执行),钱包需要能本地签名 7702 授权(私钥 / ethers Wallet)。发授权交易时钱包不能有未确认的交易(否则 gas 预估不准、上链后执行失败)。第一笔带授权的交易确认后才发同一钱包的下一笔,没确认(超时)时后面的批次跳过
|
|
468
|
+
- **Disperse 不能用于通用调用**:它只能分发主币 / 代币。任意合约调用合成一笔交易只能用 `'7702'`(Multicall3 的 `msg.sender` 是 Multicall3 合约本身,approve 或转给它的代币任何人都能拿走)
|
|
469
|
+
|
|
470
|
+
**结果**:与输入一一对应,字段都是字符串 / 数字 / null,可以直接 `JSON.stringify`:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
{ index: 0, from, to, status: 'confirmed', hash: '0x…', nonce: 7, gasLimit: '55320', gasUsed: '46100', fee: '46100000000000', blockNumber: 123, error: null }
|
|
474
|
+
// batchTransfer 另有 token / native / amount / formatted / via / approvals(为走 Disperse 发出的 approve 交易);batchCollect 另有 token / native / amount / formatted
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| status | 含义 |
|
|
478
|
+
| --- | --- |
|
|
479
|
+
| `confirmed` | 已上链且执行成功 |
|
|
480
|
+
| `reverted` | 已上链但执行失败(手续费已扣) |
|
|
481
|
+
| `failed` | 没有发出去(预估 revert、余额不足、节点拒绝、用户在钱包里拒绝),不花手续费;`error` 是原因(能解析时带 revert 原因) |
|
|
482
|
+
| `sent` | 已广播,没等确认(`wait: false`,或等待中途取消)。广播结果不确定的交易也算 `sent`(超时 / 连不上节点,或前一个节点超时后另一个节点拒绝;浏览器钱包已广播、之后查询出错),可能已进交易池,不能当作失败重试,`error` 里有说明,等待期间会定期重发。节点明确拒绝的(给出了错误信息)是 `failed` |
|
|
483
|
+
| `pending` | 等到 `timeout` 还没确认,之后仍可能上链 |
|
|
484
|
+
| `skipped` | 没有执行:同一钱包前面的调用发送前失败(`batchCall` 默认 `continueOnError: false`;已发出、之后上链 revert 的拦不住后面已发出的交易)、已取消、同一笔 7702 交易里其他调用失败、余额为 0 等 |
|
|
485
|
+
| `simulated` | `dryRun: true` 时预估通过(不发交易) |
|
|
486
|
+
|
|
487
|
+
**依赖前一笔的调用**:预估 gas 失败且同一钱包还有未确认的交易时,先等它们确认再重新预估一次,所以“先 approve 再调用”可以直接写在一批里。`dryRun` 不会真的执行前一笔,这类调用预估会失败。
|
|
488
|
+
|
|
489
|
+
**归集流程**(`batchCollect`):一次 multicall 读出所有钱包的余额 → 预估每笔归集的手续费(gasLimit × maxFeePerGas,OP Stack / Scroll 加上 L1 数据费)→ 不够的钱包由 `gasWallet` 补上差额(`gasMode`,默认 `'disperse'` 一笔补齐,等确认)→ 重新预估、发代币归集(手续费价格沿用补 gas 时的那一个,补 gas 等确认期间节点报价上涨也不会超出补的数量)→ 包含主币时等代币归集确认,按最新余额(`minBlock` 不早于最后一笔交易的区块)扣掉手续费扫空。代币没有全部归集成功的钱包不扫主币(留着付重新归集的手续费);补的 gas 用剩的零头不够付扫尾手续费时,主币一项也为 `skipped`。余额为 0、低于 `minAmount`、钱包就是归集地址的项为 `skipped`。`dryRun: true` 返回计划(补多少 gas、每笔 gasLimit),不发交易。
|
|
490
|
+
|
|
491
|
+
Disperse / 7702 里同一笔交易的多个调用,`hash`、`fee`、`gasUsed` 都是整笔交易的值(汇总手续费时按 `hash` 去重)。
|
|
492
|
+
|
|
493
|
+
**nonce 被占用**:广播时节点说 nonce 已被用掉,先多查几次回执(节点可能落后):是这笔已上链就按已发出处理;确定没上链(钱包在别处发了交易)才换 nonce 重发;查询出错时不确定,按已发出跟踪,绝不重发。
|
|
494
|
+
|
|
495
|
+
**浏览器钱包**(ethers 的 `JsonRpcSigner`):先确认钱包当前所在的链与 Provider 一致,不一致时报错、不发送(否则会在另一条链上真的转账);交易里也带上 `chainId`,钱包会再校验一次。
|
|
496
|
+
|
|
497
|
+
**共同选项**(`SendOptions`):`wait`(默认 true,等上链确认)、`timeout`(默认 180 秒)、`pollInterval`(默认 2 秒,等待期间每 5 次重发一次原交易)、`concurrency`、`gasMultiplier`(gas 预估的放大倍数,默认 1.2)、`gasPrice` / `maxFeePerGas` / `maxPriorityFeePerGas`(不传按节点的 `getFeeData`)、`dryRun`、`signal`(取消:不再发新交易)、`onUpdate`(每个结果状态变化时回调,用于展示进度;回调抛错会被忽略,不影响发送)。
|
|
498
|
+
|
|
499
|
+
**Tron**:同样的 `batchCall` / `batchTransfer` / `batchCollect`,传 Tron 的 `Provider` 即可(地址可以用 T 开头的格式)。Tron 部分按需加载:打包工具做代码分割时(Vite / Rollup / webpack 默认如此),只用 EVM 不会带上它(约 17KB)。
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
const tron = new Provider(TRON_CHAIN_ID.mainnet, 'https://api.trongrid.io', { tron: { apiKey } })
|
|
503
|
+
await batchTransfer(tron, privateKey, [{ to: 'T…', formatted: '1.5' }, { to: 'T…', token: USDT, formatted: '20' }])
|
|
504
|
+
await batchCollect(tron, [key1, key2], hotWallet, { tokens: [USDT, NATIVE_TOKEN], gasWallet: gasKey })
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
- 交易在本地按 protobuf 编码(与 TronWeb 逐字节一致)、本地签名,通过 `/wallet/broadcasthex` 广播:不让节点代为构造交易,节点无法替换交易内容
|
|
508
|
+
- 每个调用一笔交易(Tron 上没有通用的 Disperse 合约,`mode` 不支持 `'7702'`,`gasMode` 和调用里的 `gasLimit` 不起作用);Tron 没有 nonce,交易互不阻塞
|
|
509
|
+
- 合约调用先用 `triggerconstantcontract` 模拟,revert 的不发送;`fee_limit` 默认按模拟的能量 × 能量单价 × 1.5(`feeLimit` 可以指定)
|
|
510
|
+
- **发送前检查 TRX 够不够**:Tron 上能量不够的合约调用照样上链、执行失败(OUT_OF_ENERGY)并烧掉 TRX,所以按每笔的金额 + 能量(× 1.2,扣掉质押的能量)+ 带宽 + 激活费累计,不够的那笔及之后在发送前就是 `failed`
|
|
511
|
+
- 给合约地址转 TRX 时用合约调用(空 calldata 带 `call_value`),主网禁止用普通转账给合约转 TRX
|
|
512
|
+
- 交易 60 秒过期(浏览器钱包 3 分钟)。过期判断按固化数据:同一个节点上固化区块的时间超过过期时间 + 60 秒、并且固化的交易信息里没有这笔,连续两次确认后才换新的参考区块重发,最多 `retries` 次(默认 2);查询出错时一律当作“不确定”,不会重发,避免重复执行
|
|
513
|
+
- 广播时只有交易本身的问题(签名、合约校验、带宽不够、交易太大)判为 `failed`;其他错误码(如 TAPOS_ERROR、节点忙)可能只是某个节点的问题,按 `sent` 继续跟踪
|
|
514
|
+
- 默认 `pollInterval` 3 秒,等待期间每 3 次重发一次原交易
|
|
515
|
+
- 带宽按 java-tron 的规则算:质押带宽或免费带宽有一项够这笔交易用时不收费,两者不能凑在一起用,都不够时整笔按字节烧 TRX
|
|
516
|
+
- 归集的手续费按上面的资源模型算;没激活的子钱包由补 gas 的转账激活(激活费约 1.1 TRX 由 `gasWallet` 付),补 gas 前先确认 `gasWallet` 付得起全部(含激活费),不够时什么都不发、直接报错;TRX 余额读不到的钱包不补 gas。扫空 TRX 时扣掉带宽费;归集地址是合约时还要预留能量,会剩一点零头
|
|
517
|
+
- 结果的 `hash` 是交易 ID(不带 0x),`gasLimit` 是模拟的能量,`gasUsed` 是实际消耗的能量,`fee` 是实际烧掉的 sun;`nonce` 为 null
|
|
518
|
+
- 钱包:私钥、ethers `Wallet`、自定义 `{ address, signDigest(digest) }`(KMS / 硬件钱包),或浏览器钱包:注入的 `window.tronWeb`(TronLink / OKX 等)、@tronweb3/tronwallet-adapter 的 adapter
|
|
519
|
+
- 浏览器钱包:每笔交易弹一次确认(Tron 钱包不支持一次签多笔),过期时间放宽到 3 分钟。钱包拿到的是 TronWeb 格式的交易 JSON(能通过 TronWeb 的 `txCheck`,和本地编码的 `raw_data_hex` 一致);签完后从签名恢复出地址核对,钱包切换了账户或改了交易时这笔失败。用户拒绝时 `error` 为 `'rejected by user'`
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
await window.tronLink.request({ method: 'tron_requestAccounts' }) // 先连接(TronLink)
|
|
523
|
+
await batchTransfer(new Provider(window.tronWeb), window.tronWeb, recipients)
|
|
524
|
+
```
|
|
525
|
+
|
|
363
526
|
## 主币余额与 deployless
|
|
364
527
|
|
|
365
528
|
`getEthBalance`(以及 `balances` 里的主币)默认在两种模式下都和合约调用在同一次 `eth_call` 里:
|
package/dist/aggregate.d.ts.map
CHANGED
|
@@ -1 +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,EAAiB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAA;AAI3E,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAGnE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,gBAAgB,CAAC,CAAC,CAAA;AAE1I,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;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,KAAK,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAItE,eAAO,MAAM,mBAAmB,WAI9B,CAAA;
|
|
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,EAAiB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAA;AAI3E,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAGnE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,gBAAgB,CAAC,CAAC,CAAA;AAE1I,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;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,KAAK,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAItE,eAAO,MAAM,mBAAmB,WAI9B,CAAA;AAyJF,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;IACjB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,iBAAiB,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,WAAW,EAAE,EACvB,SAAS,GAAE,aAAkB,GAC5B,OAAO,CAAC,SAAS,EAAE,CAAC,CAmBtB;AAgXD,uBAAuB;AACvB,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C"}
|
package/dist/aggregate.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AbiCoder, Interface, concat, dataSlice, id, isError } from 'ethers';
|
|
|
2
2
|
import { encodeUint256 } from './call.js';
|
|
3
3
|
import { DEPLOYLESS_MULTICALL3_BYTECODE } from './deployless.js';
|
|
4
4
|
import { isExecutionError } from './errors.js';
|
|
5
|
-
import { AllNodesFailedError, FallbackRpc, InvalidResultError } from './fallback.js';
|
|
5
|
+
import { AllNodesFailedError, FallbackRpc, InvalidResultError, NodeBehindError, isBehind } from './fallback.js';
|
|
6
6
|
import { MULTICALL3_ADDRESS } from './multicall.js';
|
|
7
7
|
import { isTronChain } from './tron.js';
|
|
8
8
|
const balanceOfInterface = new Interface(['function balanceOf(address owner) view returns (uint256)']);
|
|
@@ -48,22 +48,94 @@ const invalidStrikes = new Map();
|
|
|
48
48
|
* eth_call 并解码 aggregate3 的结果:通过 FallbackRpc 时空数据 / 无法解码的结果换下一个节点(解码只做一次,结果复用);
|
|
49
49
|
* 其他 Provider 不支持校验参数,取到结果后自己校验
|
|
50
50
|
*/
|
|
51
|
-
async function callAggregate3(provider, tx) {
|
|
51
|
+
async function callAggregate3(provider, tx, requests, atLeast) {
|
|
52
52
|
const decoded = new Map();
|
|
53
|
+
// deployless 结果超过 24KB 时通过 revert Aggregate3Result 带回:同样校验区块号,落后时换成 NodeBehindError(FallbackRpc 换节点)
|
|
54
|
+
const inspectError = (err) => {
|
|
55
|
+
const results = atLeast === undefined ? null : revertedResults(err);
|
|
56
|
+
if (results) {
|
|
57
|
+
assertAtLeast(requests, results, atLeast);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
53
60
|
const accept = (data) => {
|
|
54
61
|
const results = decodeAggregate3Data(data);
|
|
55
|
-
if (results) {
|
|
56
|
-
|
|
62
|
+
if (!results) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// minBlock:结果里的区块号早于要求时节点落后,抛 NodeBehindError 让 FallbackRpc 换下一个节点
|
|
66
|
+
if (atLeast !== undefined) {
|
|
67
|
+
assertAtLeast(requests, results, atLeast);
|
|
57
68
|
}
|
|
58
|
-
|
|
69
|
+
decoded.set(data, results);
|
|
70
|
+
return true;
|
|
59
71
|
};
|
|
60
|
-
|
|
72
|
+
let data;
|
|
73
|
+
if (provider instanceof FallbackRpc) {
|
|
74
|
+
data = await provider.call(tx, { accept, inspectError });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// 其他 Provider 不支持校验参数:取到结果后自己校验(同样包括区块号)
|
|
78
|
+
try {
|
|
79
|
+
data = await provider.call(tx);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
inspectError(err);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
if (!accept(data)) {
|
|
86
|
+
throw new InvalidResultError(data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
61
89
|
const results = decoded.get(data) ?? decodeAggregate3Data(data);
|
|
62
90
|
if (!results) {
|
|
63
91
|
throw new InvalidResultError(data);
|
|
64
92
|
}
|
|
65
93
|
return results;
|
|
66
94
|
}
|
|
95
|
+
/** 已解析过的 revert 结果(同一个错误在 inspectError 和 callDeploylessChunk 里各用一次,只解析一次) */
|
|
96
|
+
const revertedCache = new WeakMap();
|
|
97
|
+
/** deployless 通过 revert Aggregate3Result(...) 带回的结果;不是这种 revert、或数据无法解码时为 null(保留原来的错误) */
|
|
98
|
+
function revertedResults(err) {
|
|
99
|
+
const revertData = err?.data;
|
|
100
|
+
if (!isError(err, 'CALL_EXCEPTION') || !revertData?.startsWith(AGGREGATE3_RESULT_SELECTOR)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const cached = revertedCache.get(err);
|
|
104
|
+
if (cached !== undefined) {
|
|
105
|
+
return cached;
|
|
106
|
+
}
|
|
107
|
+
let results;
|
|
108
|
+
try {
|
|
109
|
+
results = decodeAggregate3(AbiCoder.defaultAbiCoder().decode([RESULT_TUPLE], dataSlice(revertData, 4))[0]);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
results = null;
|
|
113
|
+
}
|
|
114
|
+
revertedCache.set(err, results);
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
/** ArbSys.arbBlockNumber() 的结果(Arbitrum 系的 L2 区块号);其他链上 0x64 没有代码、返回空数据,为 null */
|
|
118
|
+
function l2BlockOf(result) {
|
|
119
|
+
return result?.success && result.returnData.length === 66 ? BigInt(result.returnData) : null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* minBlock 校验:这次 eth_call 里附带的区块号查询(BLOCK_NUMBER_REQUEST,Arbitrum 上用一起附带的 L2 区块号)早于要求时,
|
|
123
|
+
* 抛 NodeBehindError(FallbackRpc 换下一个节点)。每次 eth_call 都附带了这两条(deployless 切片时每片各自附带)
|
|
124
|
+
*/
|
|
125
|
+
function assertAtLeast(requests, results, atLeast) {
|
|
126
|
+
const blockIndex = requests.lastIndexOf(BLOCK_NUMBER_REQUEST);
|
|
127
|
+
if (blockIndex === -1) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const arbIndex = requests.lastIndexOf(ARB_BLOCK_NUMBER_REQUEST);
|
|
131
|
+
assertBlock((arbIndex === -1 ? null : l2BlockOf(results[arbIndex])) ?? blockOf(results[blockIndex]), atLeast);
|
|
132
|
+
}
|
|
133
|
+
/** 区块号不早于 minBlock,否则节点落后(读不到区块号也按落后处理) */
|
|
134
|
+
function assertBlock(block, atLeast) {
|
|
135
|
+
if (block === null || block < atLeast) {
|
|
136
|
+
throw new NodeBehindError(atLeast, block ?? -1n);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
67
139
|
/** aggregate3 返回数据的解码结果;空数据或无法解码时为 null */
|
|
68
140
|
function decodeAggregate3Data(data) {
|
|
69
141
|
if (!data || data === '0x') {
|
|
@@ -111,21 +183,51 @@ async function aggregateChunkAtLeast(ctx, requests, overrides) {
|
|
|
111
183
|
const minBlock = overrides.minBlock;
|
|
112
184
|
// Tron 节点只能查最新状态(不支持按区块号查询)
|
|
113
185
|
const canPinBlock = !isTronChain(ctx.chainId);
|
|
186
|
+
const atLeast = BigInt(minBlock);
|
|
187
|
+
let pinned = false;
|
|
114
188
|
return retryUntilCaughtUp(overrides, async () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
189
|
+
// 区块号校验在 FallbackRpc 里做:落后的节点(结果的区块号早于 minBlock)当场换下一个节点,跟上的节点直接给出最新状态
|
|
190
|
+
try {
|
|
191
|
+
return withoutBlock(await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], overrides, atLeast));
|
|
118
192
|
}
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return pinned.slice(0, -1);
|
|
193
|
+
catch (err) {
|
|
194
|
+
// 有节点落后(其余节点可能因限频、超时等失败)时,按区块号重查一次;之后只等最新状态跟上,避免每秒把所有节点问两遍
|
|
195
|
+
if (!canPinBlock || pinned || !isBehindFailure(err)) {
|
|
196
|
+
throw err;
|
|
124
197
|
}
|
|
125
198
|
}
|
|
126
|
-
|
|
199
|
+
// 按区块号重查:落后的节点对未来区块报错或返回较旧区块,同样校验区块号、换节点;仍然都落后时由 retryUntilCaughtUp 稍等重试。
|
|
200
|
+
// 确认节点都落后后不再重查(只等最新状态跟上);因限频、超时等失败时下次照常重查
|
|
201
|
+
try {
|
|
202
|
+
return withoutBlock(await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], { ...overrides, blockTag: minBlock }, atLeast));
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
// 所有节点都落后才不再重查;有节点只是限频、超时等失败时下次照常重查
|
|
206
|
+
if (isAllBehind(err)) {
|
|
207
|
+
pinned = true;
|
|
208
|
+
}
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
127
211
|
});
|
|
128
212
|
}
|
|
213
|
+
/** 去掉附带的区块号查询(区块号已在每次 eth_call 时由 assertAtLeast 校验过) */
|
|
214
|
+
function withoutBlock(results) {
|
|
215
|
+
return results.slice(0, -1);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 是否有节点落后:结果早于 minBlock(NodeBehindError),或对按区块号的查询报“区块不存在”(header not found 等);
|
|
219
|
+
* 多节点时其中有节点是这样(其余可能是限频、超时等)
|
|
220
|
+
*/
|
|
221
|
+
function isBehindFailure(err) {
|
|
222
|
+
return isBehindError(err) || (err instanceof AllNodesFailedError && err.errors.some((e) => isBehindError(e.error)));
|
|
223
|
+
}
|
|
224
|
+
/** 所有节点都落后(单节点时就是它落后) */
|
|
225
|
+
function isAllBehind(err) {
|
|
226
|
+
return isBehindError(err) || (err instanceof AllNodesFailedError && err.errors.every((e) => isBehindError(e.error)));
|
|
227
|
+
}
|
|
228
|
+
function isBehindError(err) {
|
|
229
|
+
return err instanceof NodeBehindError || isBehind(err);
|
|
230
|
+
}
|
|
129
231
|
/** 节点落后时的重试:确定性错误(合约执行结果)直接抛;其余错误每秒重试,最多约 10 秒;signal 取消后停止 */
|
|
130
232
|
async function retryUntilCaughtUp(overrides, run) {
|
|
131
233
|
const deadline = Date.now() + MIN_BLOCK_DEADLINE;
|
|
@@ -183,6 +285,8 @@ async function aggregateWithRpcBalances(ctx, requests, overrides) {
|
|
|
183
285
|
const block = blockOf(results.pop());
|
|
184
286
|
otherResults = results;
|
|
185
287
|
const blockTag = block === null ? overrides.blockTag : Number(block);
|
|
288
|
+
// 已知限制:eth_getBalance 不返回区块号,没法校验。读余额的节点如果忽略区块参数、又比给出 blockTag 的节点落后,
|
|
289
|
+
// 主币余额可能比代币余额早几个块(仍不早于 minBlock 的节点才会给出 blockTag 之后的区块)
|
|
186
290
|
const read = () => Promise.all(owners.map((owner) => ctx.provider.getBalance(owner, blockTag)));
|
|
187
291
|
// 只有 minBlock 才等待重试(读的区块可能来自比当前节点更新的节点);只要 withBlock 时出错照常抛出
|
|
188
292
|
balances = await (overrides.minBlock !== undefined ? retryUntilCaughtUp(overrides, read) : read());
|
|
@@ -199,13 +303,13 @@ async function aggregateWithRpcBalances(ctx, requests, overrides) {
|
|
|
199
303
|
return requests.map((req) => req.ethBalanceOf === undefined ? otherResults[i++] : { success: true, returnData: encodeUint256(balances[j++]) });
|
|
200
304
|
}
|
|
201
305
|
/** 执行一批请求;其中有区块号查询时顺带读 Arbitrum 的 L2 区块号,并用它替换(见 ARB_BLOCK_NUMBER_REQUEST) */
|
|
202
|
-
async function aggregateChunk(ctx, requests, overrides) {
|
|
306
|
+
async function aggregateChunk(ctx, requests, overrides, atLeast) {
|
|
203
307
|
if (!requests.some((req) => req.blockNumber)) {
|
|
204
|
-
return aggregateRawChunk(ctx, requests, overrides);
|
|
308
|
+
return aggregateRawChunk(ctx, requests, overrides, atLeast);
|
|
205
309
|
}
|
|
206
|
-
const results = await aggregateRawChunk(ctx, [...requests, ARB_BLOCK_NUMBER_REQUEST], overrides);
|
|
310
|
+
const results = await aggregateRawChunk(ctx, [...requests, ARB_BLOCK_NUMBER_REQUEST], overrides, atLeast);
|
|
207
311
|
const arb = results.pop();
|
|
208
|
-
if (arb
|
|
312
|
+
if (l2BlockOf(arb) !== null) {
|
|
209
313
|
requests.forEach((req, i) => {
|
|
210
314
|
if (req.blockNumber) {
|
|
211
315
|
results[i] = arb;
|
|
@@ -214,11 +318,11 @@ async function aggregateChunk(ctx, requests, overrides) {
|
|
|
214
318
|
}
|
|
215
319
|
return results;
|
|
216
320
|
}
|
|
217
|
-
async function aggregateRawChunk(ctx, requests, overrides) {
|
|
321
|
+
async function aggregateRawChunk(ctx, requests, overrides, atLeast) {
|
|
218
322
|
const multicall = ctx.multicall;
|
|
219
323
|
if (multicall && isUsable(ctx.chainId, multicall, overrides.blockTag)) {
|
|
220
324
|
const key = cacheKey(ctx.chainId, multicall.address);
|
|
221
|
-
const results = await callContract(ctx, multicall, requests, overrides);
|
|
325
|
+
const results = await callContract(ctx, multicall, requests, overrides, atLeast);
|
|
222
326
|
if (Array.isArray(results)) {
|
|
223
327
|
invalidStrikes.delete(key);
|
|
224
328
|
return results;
|
|
@@ -239,14 +343,14 @@ async function aggregateRawChunk(ctx, requests, overrides) {
|
|
|
239
343
|
}
|
|
240
344
|
}
|
|
241
345
|
}
|
|
242
|
-
return callDeployless(ctx, requests, overrides);
|
|
346
|
+
return callDeployless(ctx, requests, overrides, atLeast);
|
|
243
347
|
}
|
|
244
348
|
/**
|
|
245
349
|
* 'invalid':返回空数据(地址上没有代码)或无法解码——经 FallbackRpc 时会先换节点重试;
|
|
246
350
|
* 所有节点(2 个以上)都这样时为 'invalid-confirmed'。'reverted':执行 revert 且没有 revert 数据。
|
|
247
351
|
* 这几种情况都退回 deployless。
|
|
248
352
|
*/
|
|
249
|
-
async function callContract(ctx, multicall, requests, overrides) {
|
|
353
|
+
async function callContract(ctx, multicall, requests, overrides, atLeast) {
|
|
250
354
|
const calls = requests.map((req) => ({
|
|
251
355
|
target: req.ethBalanceOf || req.blockNumber ? multicall.address : req.target,
|
|
252
356
|
allowFailure: req.allowFailure,
|
|
@@ -254,17 +358,17 @@ async function callContract(ctx, multicall, requests, overrides) {
|
|
|
254
358
|
}));
|
|
255
359
|
try {
|
|
256
360
|
// 地址上没有代码时 eth_call 返回 0x;空数据 / 无法解码的结果换下一个节点(节点偶发出错时别的节点能给出正确结果)
|
|
257
|
-
return await callAggregate3(ctx.provider, {
|
|
258
|
-
to: multicall.address,
|
|
259
|
-
data: multicall3Interface.encodeFunctionData('aggregate3', [calls]),
|
|
260
|
-
blockTag: overrides.blockTag,
|
|
261
|
-
from: overrides.from,
|
|
262
|
-
});
|
|
361
|
+
return await callAggregate3(ctx.provider, { to: multicall.address, data: multicall3Interface.encodeFunctionData('aggregate3', [calls]), blockTag: overrides.blockTag, from: overrides.from }, requests, atLeast);
|
|
263
362
|
}
|
|
264
363
|
catch (err) {
|
|
265
364
|
if (err instanceof InvalidResultError) {
|
|
266
365
|
return 'invalid';
|
|
267
366
|
}
|
|
367
|
+
// 有节点只是落后(minBlock):不能据此判断合约不存在,按落后处理(由 minBlock 的等待重试负责),不记次数
|
|
368
|
+
// (只看 minBlock 校验得出的 NodeBehindError;“区块不存在”的报错和空数据混在一起时照常退回 deployless)
|
|
369
|
+
if (atLeast !== undefined && (err instanceof NodeBehindError || (err instanceof AllNodesFailedError && err.errors.some((e) => e.error instanceof NodeBehindError)))) {
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
268
372
|
// 有节点返回空数据、其余节点出错(超时、限频等):合约可能不存在,退回 deployless;
|
|
269
373
|
// 只有 2 个以上节点都返回空数据才算确认,记缓存
|
|
270
374
|
if (err instanceof AllNodesFailedError && err.errors.some((e) => e.error instanceof InvalidResultError)) {
|
|
@@ -287,14 +391,22 @@ async function callContract(ctx, multicall, requests, overrides) {
|
|
|
287
391
|
* 合约创建受 EIP-3860 限制,initcode(字节码 + 参数)不能超过 48KB,这里按编码后的大小切片;
|
|
288
392
|
* 返回超过 24KB 的情况由合约改用 revert 带回,见 contracts/DeploylessMulticall3.sol。
|
|
289
393
|
*/
|
|
290
|
-
async function callDeployless(ctx, requests, overrides) {
|
|
394
|
+
async function callDeployless(ctx, requests, overrides, atLeast) {
|
|
291
395
|
const chunks = splitByInitcodeSize(requests);
|
|
292
|
-
const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides)));
|
|
396
|
+
const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides, atLeast)));
|
|
293
397
|
return results.flat();
|
|
294
398
|
}
|
|
295
|
-
async function callDeploylessChunk(ctx, requests, overrides) {
|
|
399
|
+
async function callDeploylessChunk(ctx, requests, overrides, atLeast) {
|
|
400
|
+
// minBlock:每片都附带区块号查询并各自校验——只校验带区块号的那一片的话,其他片可能来自落后节点、混进旧数据
|
|
401
|
+
const sent = atLeast === undefined
|
|
402
|
+
? requests
|
|
403
|
+
: [
|
|
404
|
+
...requests,
|
|
405
|
+
...(requests.includes(BLOCK_NUMBER_REQUEST) ? [] : [BLOCK_NUMBER_REQUEST]),
|
|
406
|
+
...(requests.includes(ARB_BLOCK_NUMBER_REQUEST) ? [] : [ARB_BLOCK_NUMBER_REQUEST]),
|
|
407
|
+
];
|
|
296
408
|
const args = AbiCoder.defaultAbiCoder().encode([CALL3_TUPLE], [
|
|
297
|
-
|
|
409
|
+
sent.map(({ target, allowFailure, callData, ethBalanceOf, blockNumber }) => ({
|
|
298
410
|
target: ethBalanceOf || blockNumber ? MULTICALL3_ADDRESS : target,
|
|
299
411
|
allowFailure,
|
|
300
412
|
callData,
|
|
@@ -302,20 +414,21 @@ async function callDeploylessChunk(ctx, requests, overrides) {
|
|
|
302
414
|
]);
|
|
303
415
|
try {
|
|
304
416
|
// 返回空数据 / 无法解码(节点不支持合约创建式 eth_call,或偶发出错)时换下一个节点
|
|
305
|
-
|
|
417
|
+
const results = await callAggregate3(ctx.provider, { data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]), blockTag: overrides.blockTag, from: overrides.from }, sent, atLeast);
|
|
418
|
+
return results.slice(0, requests.length);
|
|
306
419
|
}
|
|
307
420
|
catch (err) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return
|
|
421
|
+
// 结果超过 24KB,合约通过 revert Aggregate3Result(...) 带回(区块号已在 callAggregate3 里校验)
|
|
422
|
+
const reverted = revertedResults(err);
|
|
423
|
+
if (reverted) {
|
|
424
|
+
return reverted.slice(0, requests.length);
|
|
312
425
|
}
|
|
313
426
|
// 有的钱包 / 中间层会丢掉 revert 数据,大结果拿不回来:对半拆开重试,小批量能正常 return
|
|
314
427
|
if (requests.length > 1 && isExecutionError(err) && !hasRevertData(err)) {
|
|
315
428
|
const mid = Math.ceil(requests.length / 2);
|
|
316
429
|
const [a, b] = await Promise.all([
|
|
317
|
-
callDeploylessChunk(ctx, requests.slice(0, mid), overrides),
|
|
318
|
-
callDeploylessChunk(ctx, requests.slice(mid), overrides),
|
|
430
|
+
callDeploylessChunk(ctx, requests.slice(0, mid), overrides, atLeast),
|
|
431
|
+
callDeploylessChunk(ctx, requests.slice(mid), overrides, atLeast),
|
|
319
432
|
]);
|
|
320
433
|
return [...a, ...b];
|
|
321
434
|
}
|