@w3lib/evm-batch-call 0.6.0 → 0.8.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 (248) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +367 -3
  3. package/contracts/TokenProbe.sol +103 -0
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js +114 -3
  6. package/dist/aggregate.js.map +1 -1
  7. package/dist/approvals.d.ts +169 -0
  8. package/dist/approvals.d.ts.map +1 -0
  9. package/dist/approvals.js +310 -0
  10. package/dist/approvals.js.map +1 -0
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +4 -0
  13. package/dist/chains.js.map +1 -1
  14. package/dist/dividends.d.ts +135 -0
  15. package/dist/dividends.d.ts.map +1 -0
  16. package/dist/dividends.js +253 -0
  17. package/dist/dividends.js.map +1 -0
  18. package/dist/fees.d.ts +97 -0
  19. package/dist/fees.d.ts.map +1 -0
  20. package/dist/fees.js +115 -0
  21. package/dist/fees.js.map +1 -0
  22. package/dist/history.d.ts +154 -0
  23. package/dist/history.d.ts.map +1 -0
  24. package/dist/history.js +247 -0
  25. package/dist/history.js.map +1 -0
  26. package/dist/historySources.d.ts +32 -0
  27. package/dist/historySources.d.ts.map +1 -0
  28. package/dist/historySources.js +319 -0
  29. package/dist/historySources.js.map +1 -0
  30. package/dist/http.d.ts +10 -0
  31. package/dist/http.d.ts.map +1 -0
  32. package/dist/http.js +30 -0
  33. package/dist/http.js.map +1 -0
  34. package/dist/liquidity/v3.d.ts +2 -0
  35. package/dist/liquidity/v3.d.ts.map +1 -1
  36. package/dist/liquidity/v3.js +1 -1
  37. package/dist/liquidity/v3.js.map +1 -1
  38. package/dist/liquidity/v4.d.ts +21 -0
  39. package/dist/liquidity/v4.d.ts.map +1 -1
  40. package/dist/liquidity/v4.js +4 -3
  41. package/dist/liquidity/v4.js.map +1 -1
  42. package/dist/lp.d.ts +10 -1
  43. package/dist/lp.d.ts.map +1 -1
  44. package/dist/lp.js +19 -2
  45. package/dist/lp.js.map +1 -1
  46. package/dist/lpV4.d.ts +4 -0
  47. package/dist/lpV4.d.ts.map +1 -1
  48. package/dist/lpV4.js +27 -3
  49. package/dist/lpV4.js.map +1 -1
  50. package/dist/multicall.d.ts.map +1 -1
  51. package/dist/multicall.js +3 -0
  52. package/dist/multicall.js.map +1 -1
  53. package/dist/names.d.ts +55 -0
  54. package/dist/names.d.ts.map +1 -0
  55. package/dist/names.js +93 -0
  56. package/dist/names.js.map +1 -0
  57. package/dist/owner.d.ts +4 -26
  58. package/dist/owner.d.ts.map +1 -1
  59. package/dist/owner.js +7 -112
  60. package/dist/owner.js.map +1 -1
  61. package/dist/permit.d.ts +137 -0
  62. package/dist/permit.d.ts.map +1 -0
  63. package/dist/permit.js +355 -0
  64. package/dist/permit.js.map +1 -0
  65. package/dist/portfolio.d.ts +56 -0
  66. package/dist/portfolio.d.ts.map +1 -0
  67. package/dist/portfolio.js +54 -0
  68. package/dist/portfolio.js.map +1 -0
  69. package/dist/price.d.ts +95 -0
  70. package/dist/price.d.ts.map +1 -0
  71. package/dist/price.js +329 -0
  72. package/dist/price.js.map +1 -0
  73. package/dist/rewards/farms.d.ts +26 -0
  74. package/dist/rewards/farms.d.ts.map +1 -0
  75. package/dist/rewards/farms.js +210 -0
  76. package/dist/rewards/farms.js.map +1 -0
  77. package/dist/rewards/index.d.ts +42 -0
  78. package/dist/rewards/index.d.ts.map +1 -0
  79. package/dist/rewards/index.js +382 -0
  80. package/dist/rewards/index.js.map +1 -0
  81. package/dist/rewards/types.d.ts +170 -0
  82. package/dist/rewards/types.d.ts.map +1 -0
  83. package/dist/rewards/types.js +2 -0
  84. package/dist/rewards/types.js.map +1 -0
  85. package/dist/risk.d.ts +42 -0
  86. package/dist/risk.d.ts.map +1 -0
  87. package/dist/risk.js +134 -0
  88. package/dist/risk.js.map +1 -0
  89. package/dist/rpcNodes.d.ts.map +1 -1
  90. package/dist/rpcNodes.js +18 -0
  91. package/dist/rpcNodes.js.map +1 -1
  92. package/dist/simulate.d.ts +186 -0
  93. package/dist/simulate.d.ts.map +1 -0
  94. package/dist/simulate.js +559 -0
  95. package/dist/simulate.js.map +1 -0
  96. package/dist/simulateDecode.d.ts +33 -0
  97. package/dist/simulateDecode.d.ts.map +1 -0
  98. package/dist/simulateDecode.js +321 -0
  99. package/dist/simulateDecode.js.map +1 -0
  100. package/dist/smartAccount/account.d.ts +31 -0
  101. package/dist/smartAccount/account.d.ts.map +1 -0
  102. package/dist/smartAccount/account.js +33 -0
  103. package/dist/smartAccount/account.js.map +1 -0
  104. package/dist/smartAccount/clients.d.ts +77 -0
  105. package/dist/smartAccount/clients.d.ts.map +1 -0
  106. package/dist/smartAccount/clients.js +122 -0
  107. package/dist/smartAccount/clients.js.map +1 -0
  108. package/dist/smartAccount/eip5792.d.ts +77 -0
  109. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  110. package/dist/smartAccount/eip5792.js +152 -0
  111. package/dist/smartAccount/eip5792.js.map +1 -0
  112. package/dist/smartAccount/send.d.ts +40 -0
  113. package/dist/smartAccount/send.d.ts.map +1 -0
  114. package/dist/smartAccount/send.js +271 -0
  115. package/dist/smartAccount/send.js.map +1 -0
  116. package/dist/smartAccount/userOp.d.ts +91 -0
  117. package/dist/smartAccount/userOp.d.ts.map +1 -0
  118. package/dist/smartAccount/userOp.js +124 -0
  119. package/dist/smartAccount/userOp.js.map +1 -0
  120. package/dist/subpaths/approvals.d.ts +26 -0
  121. package/dist/subpaths/approvals.d.ts.map +1 -0
  122. package/dist/subpaths/approvals.js +27 -0
  123. package/dist/subpaths/approvals.js.map +1 -0
  124. package/dist/subpaths/dividends.d.ts +28 -0
  125. package/dist/subpaths/dividends.d.ts.map +1 -0
  126. package/dist/subpaths/dividends.js +29 -0
  127. package/dist/subpaths/dividends.js.map +1 -0
  128. package/dist/subpaths/fees.d.ts +23 -0
  129. package/dist/subpaths/fees.d.ts.map +1 -0
  130. package/dist/subpaths/fees.js +25 -0
  131. package/dist/subpaths/fees.js.map +1 -0
  132. package/dist/subpaths/history.d.ts +22 -0
  133. package/dist/subpaths/history.d.ts.map +1 -0
  134. package/dist/subpaths/history.js +23 -0
  135. package/dist/subpaths/history.js.map +1 -0
  136. package/dist/subpaths/names.d.ts +35 -0
  137. package/dist/subpaths/names.d.ts.map +1 -0
  138. package/dist/subpaths/names.js +39 -0
  139. package/dist/subpaths/names.js.map +1 -0
  140. package/dist/subpaths/permit.d.ts +14 -0
  141. package/dist/subpaths/permit.d.ts.map +1 -0
  142. package/dist/subpaths/permit.js +14 -0
  143. package/dist/subpaths/permit.js.map +1 -0
  144. package/dist/subpaths/price.d.ts +38 -0
  145. package/dist/subpaths/price.d.ts.map +1 -0
  146. package/dist/subpaths/price.js +40 -0
  147. package/dist/subpaths/price.js.map +1 -0
  148. package/dist/subpaths/rewards.d.ts +27 -0
  149. package/dist/subpaths/rewards.d.ts.map +1 -0
  150. package/dist/subpaths/rewards.js +25 -0
  151. package/dist/subpaths/rewards.js.map +1 -0
  152. package/dist/subpaths/risk.d.ts +19 -0
  153. package/dist/subpaths/risk.d.ts.map +1 -0
  154. package/dist/subpaths/risk.js +20 -0
  155. package/dist/subpaths/risk.js.map +1 -0
  156. package/dist/subpaths/simulate.d.ts +25 -0
  157. package/dist/subpaths/simulate.d.ts.map +1 -0
  158. package/dist/subpaths/simulate.js +28 -0
  159. package/dist/subpaths/simulate.js.map +1 -0
  160. package/dist/subpaths/smart-account.d.ts +24 -0
  161. package/dist/subpaths/smart-account.d.ts.map +1 -0
  162. package/dist/subpaths/smart-account.js +24 -0
  163. package/dist/subpaths/smart-account.js.map +1 -0
  164. package/dist/subpaths/swap.d.ts +19 -0
  165. package/dist/subpaths/swap.d.ts.map +1 -0
  166. package/dist/subpaths/swap.js +20 -0
  167. package/dist/subpaths/swap.js.map +1 -0
  168. package/dist/swap.d.ts +168 -0
  169. package/dist/swap.d.ts.map +1 -0
  170. package/dist/swap.js +387 -0
  171. package/dist/swap.js.map +1 -0
  172. package/dist/tokenProbe.d.ts +3 -0
  173. package/dist/tokenProbe.d.ts.map +1 -0
  174. package/dist/tokenProbe.js +4 -0
  175. package/dist/tokenProbe.js.map +1 -0
  176. package/dist/write/batch.d.ts +34 -2
  177. package/dist/write/batch.d.ts.map +1 -1
  178. package/dist/write/batch.js +22 -3
  179. package/dist/write/batch.js.map +1 -1
  180. package/dist/write/collect.d.ts +9 -0
  181. package/dist/write/collect.d.ts.map +1 -1
  182. package/dist/write/collect.js +41 -1
  183. package/dist/write/collect.js.map +1 -1
  184. package/dist/write/send.d.ts +4 -0
  185. package/dist/write/send.d.ts.map +1 -1
  186. package/dist/write/send.js.map +1 -1
  187. package/dist/write/signer.d.ts +5 -1
  188. package/dist/write/signer.d.ts.map +1 -1
  189. package/dist/write/signer.js.map +1 -1
  190. package/dist/write/transfer.d.ts +5 -3
  191. package/dist/write/transfer.d.ts.map +1 -1
  192. package/dist/write/transfer.js +12 -6
  193. package/dist/write/transfer.js.map +1 -1
  194. package/dist/write/tron.d.ts +25 -0
  195. package/dist/write/tron.d.ts.map +1 -1
  196. package/dist/write/tron.js +26 -0
  197. package/dist/write/tron.js.map +1 -1
  198. package/package.json +73 -16
  199. package/src/aggregate.ts +110 -3
  200. package/src/approvals.ts +462 -0
  201. package/src/chains.ts +4 -0
  202. package/src/dividends.ts +360 -0
  203. package/src/fees.ts +201 -0
  204. package/src/history.ts +367 -0
  205. package/src/historySources.ts +360 -0
  206. package/src/http.ts +34 -0
  207. package/src/liquidity/v3.ts +1 -1
  208. package/src/liquidity/v4.ts +4 -3
  209. package/src/lp.ts +21 -2
  210. package/src/lpV4.ts +32 -2
  211. package/src/multicall.ts +3 -0
  212. package/src/names.ts +141 -0
  213. package/src/owner.ts +7 -138
  214. package/src/permit.ts +457 -0
  215. package/src/portfolio.ts +98 -0
  216. package/src/price.ts +415 -0
  217. package/src/rewards/farms.ts +229 -0
  218. package/src/rewards/index.ts +431 -0
  219. package/src/rewards/types.ts +179 -0
  220. package/src/risk.ts +166 -0
  221. package/src/rpcNodes.ts +18 -0
  222. package/src/simulate.ts +727 -0
  223. package/src/simulateDecode.ts +352 -0
  224. package/src/smartAccount/account.ts +63 -0
  225. package/src/smartAccount/clients.ts +180 -0
  226. package/src/smartAccount/eip5792.ts +187 -0
  227. package/src/smartAccount/send.ts +341 -0
  228. package/src/smartAccount/userOp.ts +180 -0
  229. package/src/subpaths/approvals.ts +48 -0
  230. package/src/subpaths/dividends.ts +47 -0
  231. package/src/subpaths/fees.ts +43 -0
  232. package/src/subpaths/history.ts +41 -0
  233. package/src/subpaths/names.ts +43 -0
  234. package/src/subpaths/permit.ts +39 -0
  235. package/src/subpaths/price.ts +60 -0
  236. package/src/subpaths/rewards.ts +46 -0
  237. package/src/subpaths/risk.ts +23 -0
  238. package/src/subpaths/simulate.ts +51 -0
  239. package/src/subpaths/smart-account.ts +40 -0
  240. package/src/subpaths/swap.ts +36 -0
  241. package/src/swap.ts +534 -0
  242. package/src/tokenProbe.ts +5 -0
  243. package/src/write/batch.ts +44 -5
  244. package/src/write/collect.ts +60 -1
  245. package/src/write/send.ts +4 -0
  246. package/src/write/signer.ts +5 -1
  247. package/src/write/transfer.ts +17 -10
  248. package/src/write/tron.ts +43 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### 新功能
6
+
7
+ - Flap 分红,子路径 `@w3lib/evm-batch-call/dividends`(只支持 BSC):`getDividends` / `dividends`(持有人在 Flap 税收代币上的余额、可领取 / 已领取数量、分红资产(WBNB 按 BNB 领取)和美元价值;`tokens` 或 `discover`(扫描收到过的 Transfer,只收 `7777` / `8888` 后缀的地址,卖光后累计的分红也能发现))、`buildClaimDividends`、`claimDividends`(每个代币一笔 `withdrawDividendsFor(owner)`,任何人可代领、款项直接到持有人;支持 `mode: '7702' | '4337' | '5792'` 原子一笔,`simulate` 默认 true,先模拟剔除会 revert 的项;发送前在链上核对 `dividendContract`;分红资产精度读不到时标 `decimalsUnknown`、不算美元价值)
8
+ - 合约来自 docs.flap.sh 和链上核实(Portal、TaxTokenHelper、每个代币的 Dividend 合约);Dividend 合约实现没有验证源码,函数靠文档 + 选择器 + 链上调用核对。已在 BSC 主网分叉上对真实持有人核实 Tax Token V3:分红资产为 ERC20 和 WBNB 两种,到账等于预览,与 `TaxTokenHelper.getDividendInfo` 一致;Tax Token V2 没有找到链上实例,识别不了的合约放进 `unsupported`,不猜函数
9
+ - 一键领取手续费和挖矿奖励,子路径 `@w3lib/evm-batch-call/rewards`:`getClaimable` / `claimable`(每个仓位能领什么:V3 / V4 手续费、PancakeSwap V3 MasterChef 的 CAKE 和质押仓位的手续费、Aerodrome / Velodrome Slipstream gauge 的奖励,数量 + 美元价值)、`buildClaimAll`(合并成尽量少的交易:V3 `multicall`、V4 一次 `modifyLiquidities`、MasterChef `multicall`,先用 `/simulate` 模拟、剔除会失败的项)、`claimAll`(签名发送,支持 `mode: '7702' | '4337' | '5792'` 原子一笔;每笔独立,`continueOnError` 默认 true)
10
+ - 质押在 MasterChef 里的仓位也能读到(NFT 在合约里):CAKE 用所有者身份预执行 `harvest` 得到真正会发的数量(`pendingCake` 偏少)
11
+ - 模拟只有合并后的那一笔失败(单项都通过)时对半拆开重新模拟;取消(`signal`)时抛出;`claimAll({ dryRun: true })` 返回 `status: 'simulated'`、`claimed: []`、预估通过的项在 `simulated`;`minUsd` 不跳过有币没价格的项;harvest 预执行分批发出,失败退回 `pendingCake` 的项标 `approximate: true`;gauge 列表缓存不再受第一个调用方的 signal / blockTag 影响
12
+ - `/lp` 新增内部函数 `lpPositionsByIds`(按 tokenId 读 V3 仓位,不看 NFT 现在在谁手里)
13
+ - 交易模拟,子路径 `@w3lib/evm-batch-call/simulate`:`getSimulation` / `simulate`(一笔)、`getBatchSimulation` / `simulateBatch`(一串依赖的交易,如 approve + swap,同一个区块里依次执行、状态延续),可以直接传 `buildSwap` / `buildRevokes` / 流动性构建的交易
14
+ - EVM 用 `eth_simulateV1`(`traceTransfers`),按日志算资产变化(ERC20 / 721 / 1155、WETH Deposit / Withdrawal、主币)和授权变化(Approval、ApprovalForAll、Permit2);不支持的节点退回 `eth_call` + `estimateGas`(只有成败 / gas / revert 原因);支持 `stateOverrides`、`watch`、`usd`(美元价值)、`risk`(收到的代币做风险检查,按需加载)
15
+ - calldata 解码(ERC20 / 721 / 1155、WETH、Uniswap V2 / V3 Router、V3 仓位合约、Universal Router、Permit2、Multicall3、Disperse,Router 按内置协议表标协议名,multicall 解出子调用;`decodeCalldata`)、revert 原因解码(Error / Panic / 常见自定义错误;`decodeRevert`)
16
+ - `warnings`:无限授权、被授权方不在 `knownSpenders()`、授权给普通地址、调用没有代码的地址、转出大部分余额、代币风险;`assertSimulation` 一行守卫
17
+ - Tron:`triggerconstantcontract`(能量、TRC20 日志),TRX 只有 `call_value`
18
+ - 历史记录,子路径 `@w3lib/evm-batch-call/history`:`getHistory` / `history` 返回统一的 `Activity`(类型 transfer / swap / approve / liquidity / mint / contract / other、方向、对方、描述、资产变化、手续费、协议 / 方法),`nextCursor` 分页(按区块,各来源通用),`types` / `before` / `after` 过滤
19
+ - 可替换、可故障切换的数据来源(`source: [a, b]`):Blockscout(免 Key,各链实例表,用其 Etherscan 兼容接口)、Etherscan v2(`apiKey`,一个 Key 覆盖多条链)、纯 RPC 日志(ERC20 / 721 / 1155 Transfer,没有主币转账)、Tron 的 TronGrid;也可以自己实现适配器(`blockscoutSource` / `etherscanSource` / `rpcSource` / `trongridSource` / `explorerSource`)
20
+ - 智能账户批量,子路径 `@w3lib/evm-batch-call/smart-account`,并接入 `/write` 的 `batchCall` / `batchTransfer`(`batchCollect`、`swap` 同样支持代付 / 原子模式)的新 `mode`:
21
+ - `'4337'`:ERC-4337(EntryPoint v0.7 / v0.8)UserOperation 的手写打包 / 哈希 / 签名;账户 `SimpleAccount`(工厂 initCode,部署前即可算出地址)和 `Simple7702Account`;bundler 客户端(`BundlerClient`)、ERC-7677 paymaster 客户端(`PaymasterClient`,也可传自定义函数);`selfBundle` 自己发 `EntryPoint.handleOps`,不需要 bundler
22
+ - `'7702'` + `sponsor`:代付 gas 的 7702,用户只签授权和 UserOperation,不需要主币
23
+ - `'5792'`:浏览器钱包 EIP-5792(`wallet_getCapabilities` / `wallet_sendCalls` / `wallet_getCallsStatus`),不支持原子批量时退回逐笔发送
24
+ - `batchCollect` 新增 `sponsor`:代付 gas 归集,子钱包不用补 gas,主币全额转走
25
+ - `WriteResult` 新增可选的 `userOpHash`、`atomic`
26
+ - 这些代码按需加载,不用时 `/write` 的体积不变
27
+ - 免 gas 授权,子路径 `@w3lib/evm-batch-call/permit`:EIP-2612 permit(含 DAI 风格、USDC v2)、Permit2 的 PermitSingle / PermitBatch / permitTransferFrom;`detectPermit`、`buildPermit*`、`signPermit`(签名前核对,签完本地验签)、`verifyPermitSignature`、calldata 编码;默认精确额度、30 分钟有效期
28
+ - `swap` / `buildSwap` 的 `approve` 增加 `'permit'` / `'permit2'` / `'auto'`(V3 路线用 `selfPermitIfNecessary` 一笔交易完成;V2 路线在原子批量里才用 permit;`'permit2'` 内置 Router 不支持,退回普通授权),`plan.approval` / `result.approval` 说明实际方式和退回原因;`swap` 新增 `mode: '7702' | '4337' | '5792'`,授权 / permit 和兑换放进同一笔原子批量
29
+ - `WriteSigner` 新增可选的 `signTypedData` / `signMessage`
30
+ - `portfolio` / `getPortfolio`(`/price`)支持 `signal`:余额、LP 的节点请求随之中止,查价格的步骤立即以 `signal.reason` 失败(之前 `getPortfolio` 的 `signal` 被丢掉,不起作用)
31
+ - `/lp` V4 扫描(`v4.scan`)先查仓位合约的 `balanceOf`:地址没有持有仓位 NFT 时不扫描(`V4ScanProgress.skipped`),公共节点上不再白扫几千万个区块
32
+
33
+ ## 0.7.0
34
+
35
+ ### 新功能
36
+
37
+ - 价格,子路径 `@w3lib/evm-batch-call/price`:
38
+ - `getTokenPrices` / `tokenPrices`:代币(含主币)的美元价格,依次使用多个价格源,结果带给出价格的源
39
+ - `onchainPrices()`:从内置协议表的 V2 / V3 池子算价格(对包装主币、稳定币,用最深的池子,流动性下限 `minLiquidityUsd`),也支持 Tron 的 SunSwap
40
+ - `defillamaPrices({ at })`:历史价格
41
+ - `combinePrices([...])`:组合多个价格源,可以传给 `getLpPositions` / `getOwnerTokens` 的 `prices`
42
+ - `getPortfolio` / `portfolio`:地址的资产总值(传入代币的余额 × 价格,可选加上 LP 仓位)
43
+ - 兑换,子路径 `@w3lib/evm-batch-call/swap`:`getSwapQuote` / `buildSwap`(V2 / V3 池子的直接和两跳路线,一次 multicall 报价,选输出最多的,按滑点算最少收到、价格影响)、`swap`(授权 + 兑换,主币自动包装 / 解包),Tron 上是 SunSwap V2
44
+ - `swap` 有授权时,授权确认后重新报价再发兑换(deadline、最少收到从发兑换那一刻算);`tax` 选项:V2 报价不含转账税,带税代币的最少收到要再乘 (1 − tax)
45
+ - 代币风险检查,子路径 `@w3lib/evm-batch-call/risk`:`getTokenRisks` / `tokenRisks`,在 `eth_call` 里用 deployless 的探测合约(`contracts/TokenProbe.sol`)模拟买入 / 转账 / 卖出,算出买入税、转账税、卖出税,识别貔貅盘;检查 EIP-1967 代理和 owner
46
+ - 手续费估算,子路径 `@w3lib/evm-batch-call/fees`:`getFeeEstimate` / `estimateFees`(按 `eth_feeHistory` 的三档 EIP-1559 费用,不支持时按 `eth_gasPrice`)、`getCostEstimate` / `estimateCost`(一笔交易的手续费,含 OP Stack / Scroll 的 L1 数据费;Tron 按能量、带宽和账户剩余资源算要烧的 TRX,含激活费)
47
+ - 域名解析,子路径 `@w3lib/evm-batch-call/names`:`getNameAddresses` / `resolveNames`(名字 → 地址)、`getAddressNames` / `lookupAddresses`(地址 → 主名字,正查核对),内置 ENS、SPACE ID(.bnb)、Basenames(.base.eth),名字按 ENSIP-15 规范化
48
+ - 授权管理,子路径 `@w3lib/evm-batch-call/approvals`:
49
+ - `getApprovals` / `approvals`:批量查 ERC20 额度、Permit2 额度(含过期时间)、NFT 的 setApprovalForAll;默认检查 `knownSpenders(chainId)`(内置协议的 Router / 仓位合约、Uniswap / PancakeSwap Router、1inch、0x、ParaSwap、Permit2、OpenSea / Blur)
50
+ - `scanApprovals`:按 Approval / ApprovalForAll / Permit2 事件增量发现授权过的代币和 NFT 合约,进度可以保存
51
+ - `buildRevokes` / `revokeApprovals`:批量撤销(approve 0、Permit2 lockdown、setApprovalForAll false),发送同 `batchCall`,支持 7702 合成一笔
52
+ - Tron 支持查询和撤销(没有 Permit2,不支持扫描)
53
+ - 价格源的 `PriceSourceContext` 新增 `provider`(链上价格源用);`defillamaPrices`、`PriceSource` 仍可以从 `/owner`、`/lp` 导入
54
+ - 批量查询的链:Multicall3 地址表和内置节点补充 Conflux eSpace(1030)、Injective EVM(1776)、Pharos(1672,主币 PROS),星空链(202308)只有节点(没有 Multicall3,走 deployless);FSC(201022)补主币 FON
55
+ - deployless 不可用(合约创建式 `eth_call` 返回空数据 / 无法解码)时,自动改成逐条 `eth_call`(主币余额用 `eth_getBalance`,区块号用 `getBlockNumber`);确认不支持后记住 10 分钟,不再先试一次注定失败的 deployless
56
+
3
57
  ## 0.6.0
4
58
 
5
59
  ### 新功能
package/README.md CHANGED
@@ -6,7 +6,7 @@
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)
9
+ **在线 demo**:<https://web3-lib.github.io/libs/evm/>(余额、代币信息、资产列表、LP 仓位、添加 / 移除流动性(报价,连接浏览器钱包后可以发送)、NFT、价格 / 资产总值、授权(批量查询 + 撤销)、域名(ENS / SPACE ID / Basenames 正反向解析)、手续费估算、兑换(报价,连接浏览器钱包后可以发送,可填转账税)、代币风险(买卖税 / 貔貅检查),8 条链含 Tron,显示每次查询实际发出的节点请求数)。本地运行:`pnpm --filter @w3lib/evm-batch-call demo`(http://localhost:5174)
10
10
 
11
11
  ## 快速上手
12
12
 
@@ -284,7 +284,7 @@ await getLpPositions(user, { chainId: 1, v4: { scan: { storage } } }) //
284
284
  | --- | --- | --- |
285
285
  | **V3**(仓位 NFT) | 能 | 仓位合约可以按地址列出 NFT:`balanceOf` → `tokenOfOwnerByIndex` → `positions`,再读池子的价格和手续费增长。仓位很多的地址只核对按持有顺序最后的 `maxPositions` 个(默认 500,大致是最近的) |
286
286
  | **V2**(LP 代币) | **尽量** | 链上没有“地址持有哪些代币”的索引。默认用计价币之间的组合(WETH/USDT、WBNB/USDT…)在各工厂里找池子;`tokens` 传入更多代币(如资产列表里的)一起组合;`pools` 直接传池子地址;`source: alchemy({ apiKey })` 用代币来源列出地址持有的全部 ERC20(包括 LP 代币,见[资产列表](#资产列表))。找到的 LP 代币按 `token0` / `token1` / `factory` 识别池子和协议,工厂不在协议表里的显示为 `Unknown V2` |
287
- | **V4**(仓位 NFT) | **需要配置** | V4 的仓位合约不能按地址列出 NFT。`v4.tokenIds` 传已知的仓位;`v4.scan` 扫描仓位合约发给该地址的 NFT(`Transfer` 事件),扫到的再核对是否仍归这个地址(已转走 / 销毁的从进度里去掉)。两个都不传时不查 V4;候选太多时只核对 tokenId 最大的 `maxPositions` 个 |
287
+ | **V4**(仓位 NFT) | **需要配置** | V4 的仓位合约不能按地址列出 NFT。`v4.tokenIds` 传已知的仓位;`v4.scan` 扫描仓位合约发给该地址的 NFT(`Transfer` 事件),扫到的再核对是否仍归这个地址(已转走 / 销毁的从进度里去掉)。扫描前先查 `balanceOf`:地址一个仓位 NFT 都没有时直接跳过扫描(`onProgress` 收到 `skipped: true`)。两个都不传时不查 V4;候选太多时只核对 tokenId 最大的 `maxPositions` 个 |
288
288
 
289
289
  **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` 只对还没有进度的地址生效(进度按链 + 仓位合约 + 地址保存)。
290
290
 
@@ -371,15 +371,335 @@ await collectFees(multi, privateKey, { position })
371
371
  打包:在已经用到 `Provider` 的基础上,只用 `build*` 约增加 40KB(minify,gzip 约 13KB),加上发送约 77KB(gzip 约 26KB,其中大部分是 `/write` 的发送代码),均不含 ethers。
372
372
 
373
373
 
374
+ ## 领取手续费和挖矿奖励
375
+
376
+ 子路径 `@w3lib/evm-batch-call/rewards`:一键领取 V3 / V4 仓位未领取的手续费,以及质押在农场里的挖矿奖励。
377
+
378
+ - `getClaimable(owner, { chainId })` / `claimable(provider, owner)`:只读,列出每个仓位能领什么(数量按精度换算,`prices: true` 时带美元价值,按价值排序),来源出错只记进 `errors`
379
+ - `buildClaimAll(provider, owner, options)`:合并成尽量少的交易,先模拟、剔除会失败的项,只读取、不签名
380
+ - `claimAll(provider, from, options)`:构建后签名发送,`from` 与 `/write` 的 `batchCall` 相同;`mode: '7702' | '4337' | '5792'` 合成一笔原子交易
381
+
382
+ ```ts
383
+ import { claimAll, getClaimable } from '@w3lib/evm-batch-call/rewards'
384
+
385
+ const c = await getClaimable(user, { chainId: 56, prices: true })
386
+ // c.items: [{ id: 'farm:pancakeswap-v3:7682136', kind: 'farm', staked: true, amounts: [{ token: CAKE, formatted: '0.046', usd: 0.03 }], usd: 0.03 },
387
+ // { id: 'fees:pancakeswap-v3:7682136', kind: 'fees', staked: true, amounts: [token0, token1], … }, …]
388
+ // c.totals / c.usd:按代币汇总 / 合计
389
+
390
+ const { status, claimed, failed, plan } = await claimAll(multi, privateKey, { minUsd: 1 }) // 价值低于 1 美元的项跳过
391
+ await claimAll(multi, privateKey, { mode: '7702' }) // 一笔交易领完(要求钱包能本地签名)
392
+ await claimAll(multi, privateKey, { items: c.items.filter((i) => i.kind === 'fees') }) // 只领挑出来的
393
+ ```
394
+
395
+ | 来源 | 链 | 读取 | 领取 |
396
+ | --- | --- | --- | --- |
397
+ | V3 手续费(Uniswap V3、PancakeSwap V3、SunSwap V3) | 内置协议表 | 与 `/lp` 相同(仓位 NFT + 池子的手续费增长) | 仓位合约 `multicall([collect…])`,一笔合并多个仓位 |
398
+ | V4 手续费(Uniswap V4) | 内置协议表 | `v4: { tokenIds }` 或 `v4: { scan: true }`(同 `/lp`,V4 不能按地址列出 NFT) | 一次 `modifyLiquidities`:每个仓位 `DECREASE_LIQUIDITY(0)`,每个币 `TAKE` 一次 |
399
+ | PancakeSwap V3 农场(CAKE + 质押仓位的手续费) | BSC、Ethereum、Base、Arbitrum(MasterChefV3) | `balanceOf` / `tokenOfOwnerByIndex` 列出质押的仓位,以所有者身份预执行 `harvest` 得到真正会发的 CAKE(`pendingCake` 比实际少,只作备用) | MasterChef `multicall([harvest, collectTo…])` |
400
+ | Aerodrome / Velodrome Slipstream gauge | Base、Optimism | 经 Voter 枚举 gauge(缓存 1 小时,也可以传 `gauges`),`stakedValues` + `earned` | 每个仓位一笔 `getReward(tokenId)`(合约只能领一个) |
401
+
402
+ - `protocols`:只查某些协议(LP 协议 id 或挖矿协议 id;`'pancakeswap-v3'` 同时含它的农场);`fees` / `rewards`:只查手续费或只查奖励;`farms`:自己的 `FarmProtocol`;`maxPositions`:每个协议最多核对多少个仓位(默认 500)
403
+ - `recipient`:领到的代币发给哪个地址(默认 owner);gauge 的奖励只能发给 owner,传了别的地址时这些项被跳过
404
+ - `simulate`(默认 true,用 `/simulate`,按需加载):某一笔失败时逐项再试,失败的项放进 `plan.skipped`(带原因),其余照常;每笔交易带 `simulation`(资产变化就是实际会收到的数量)。原子模式下一个仓位失败会让整笔回滚,所以先剔除
405
+ - `minUsd`:低于这个美元价值的项跳过(有任何一个币没有价格的项算价值未知,不跳过);`batchSize`:一笔最多合并多少个仓位,默认 20
406
+ - `claimAll` 的 `continueOnError` 默认 true(每笔独立,不像 `batchCall` 默认遇错停);结果的 `claimed` / `failed` 是已确认 / 未确认的项
407
+
408
+ 核对:BSC 主网分叉(anvil)上,以真实质押用户的身份核对手续费与 `MasterChef.collectTo` 的预执行结果逐位一致、CAKE 与 `harvest` 一致,`claimAll` 的实际到账等于预览;再用随机钱包持有从真实持有者转来的 PancakeSwap V3 / Uniswap V3 / Uniswap V4 仓位并质押一个到 MasterChef,分别用逐笔和 `'7702'`(一笔原子交易)领取,每个代币到账数量与预览逐位一致,转走一个仓位后它被模拟剔除、其余照常。Base 分叉上以真实用户的身份领取 Aerodrome gauge 奖励(到账与预览一致,只多出几秒的累计);Base 主网(公共节点)枚举 Voter 的全部 gauge 读到质押的仓位和 AERO(带美元价值),Tron 主网读 SunSwap V3 真实用户的手续费并用 `triggerconstantcontract` 预执行领取交易。
409
+
410
+ **局限**:手续费 / 奖励随时间增加,预览是查询那一刻的数量;V4 要给 `tokenIds` 或开启扫描;Uniswap V3 的 UniswapV3Staker 激励按 `IncentiveKey` 领取、不能按用户列出,没有支持;Slipstream 的未质押仓位用了不同的 NonfungiblePositionManager(`tickSpacing` 代替 `fee`),不在 `/lp` 表里,手续费不查(质押后手续费归投票人,质押人只有奖励);V3 手续费收到的是代币本身(包装币不自动换成主币)。
411
+
412
+ ## Flap 分红
413
+
414
+ 子路径 `@w3lib/evm-batch-call/dividends`:查并领取 [Flap](https://flap.sh)(BNB Chain 发币平台)税收代币的分红。只支持 BSC(chainId 56)。
415
+
416
+ ```ts
417
+ import { claimDividends, getDividends } from '@w3lib/evm-batch-call/dividends'
418
+
419
+ const { items, unsupported } = await getDividends(user, { chainId: 56, tokens: [flapToken], discover: true })
420
+ // items: [{ token, symbol, balance, claimable: '194002826213118', claimableFormatted, claimed, asset: 'native', assetSymbol: 'BNB', usd: 0.12, accruing, dividendContract, … }]
421
+ await claimDividends(multi, privateKey, items) // 每个代币一笔
422
+ await claimDividends(multi, privateKey, items, { mode: '7702' }) // 一笔原子交易,默认先模拟、剔除会失败的项
423
+ ```
424
+
425
+ - `tokens`:要查的 Flap 代币;`discover`:从持有人收到过的 `Transfer` 事件里发现(只收地址后缀 `7777` / `8888` 的代币,再逐个核对;卖光的代币上已累计的分红仍可领取,所以不能只看当前余额)。传对象可配置 `lookbackBlocks`(默认 5000)、`fromBlock`、`progress`(结果 `scan.progress`,下次接着扫)、`blockRange`、`maxRequests`、`timeBudget`。`eth_getLogs` 不带地址时,公共 BSC 节点只允许很小的区块范围(25 / 50 个区块)或直接拒绝(publicnode 要求带地址),所以 `discover` 在公共节点上只能扫最近几百个区块;要完整发现请用自己的节点,或把已知的代币传给 `tokens`
426
+ - `prices`(默认 true):分红资产的美元价值;`includeZero`:也返回没有可领取分红的代币。一次最多检查 200 个代币
427
+ - `claimDividends`:每个代币一笔 `withdrawDividendsFor(owner)` 发给它的 Dividend 合约,`from` 与 `batchCall` 相同,`mode: '7702' | '4337' | '5792'` 合成一笔原子交易;`simulate`(默认 true,用 `/simulate`,按需加载)先模拟每一笔,会 revert 的不发;发送前还会在链上核对每项的 `dividendContract` 是代币自己的 Dividend 合约,不一致的跳过(`status: 'skipped'`,`error` 是原因);`dryRun` 只预估 gas。`buildClaimDividends(items)` 只构建、不签名
428
+ - **任何人都可以替持有人领取,款项直接到持有人地址**,所以 `from` 可以是别的钱包(如代付 gas),一批里也可以混多个持有人的项
429
+
430
+ ### 合约和核实范围
431
+
432
+ 资料来源:docs.flap.sh(Tax Token V2 / V3、Inspect A Tax Token、已部署地址)和链上调用。BSC 主网:Portal `0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0`、TaxTokenHelper `0x53841c73217735F37BC1775538b03b23feFD8346`、SwapRegistry `0x644A8f560138418bAD4EdEFC7c17878a3c2fBEB6`。
433
+
434
+ - 每个税收代币有自己的 Dividend 合约(EIP-1167 克隆,实现 `0xd67447C29908Cb901374b658cfD3CB261a375905`,**Sourcify / BscScan 上没有验证源码**;函数名靠官方文档、实现字节码里的函数选择器和链上调用核对)。代币的 `dividendContract()` 给出它;读 `dividendToken()`、`weth()`、`minimumShareBalance()`、`withdrawableDividends(user)`、`withdrawnDividends(user)`;领取 `withdrawDividendsFor(user)`
435
+ - 分红资产就是 Dividend 合约里的 `dividendToken()`:可以是 WBNB(领取时解包成 **BNB**,结果里 `asset: 'native'`)、代币自己、或任意 ERC20(BTCB、DOGE、FXIon 等,按 Flap 的 SwapRegistry 换成)。持有人要持有至少 `minimumShareBalance`(实测 10000 枚)才继续累计新分红(`accruing`);已累计的不受影响,卖光后仍可领取
436
+ - Flap 的触发机器人也会调 `distributeDividend` 自动给大额持有人发放,所以 `claimable` 为 0 不代表没领过(看 `claimed`)
437
+ - 只有一种合约形态,所以只有一个适配器(`adapter: 'flap'`):Tax Token V3(`TOKEN_TAXED_V3`,主网上目前绝大多数)已在主网和分叉上核实。Tax Token V2(`dividendBps > 0` 时才部署 Dividend 合约,文档说明函数相同)**没有找到链上实例核实**,能读到 `dividendContract()` 且函数齐全才会识别,否则放进 `unsupported`(带原因);V1 没有分红。不认识的合约不会猜函数
438
+ - 核对:BSC 主网分叉(anvil,Prague)上用随机钱包代领真实持有人的分红:分红资产为 ERC20(FXIon)和 WBNB(解包成 BNB)两种,持有人到账数量等于预览的 `claimable`,与官方 `TaxTokenHelper.getDividendInfo` 的 `(claimed, pending)` 一致,领完后 `claimable` 为 0、`claimed` 增加;`mode: '7702'` 把两个持有人的领取合成一笔原子交易;`simulate: true` 剔除会 revert 的项。另有连主网的读取 / 发现测试(`LIVE=1`)
439
+
440
+ **局限**:只有 BSC(Flap 在 Base、X Layer 等链上也有部署,没有核实);`claimable` 是查询那一刻的数量,之后还会增加;`usd` 依赖价格服务,没有价格时为 null;代币地址不是 `7777` / `8888` 后缀的不会被 `discover` 发现(传 `tokens` 即可);官方文档中 `MAGIC_DIVIDEND_COMPUTED` 的 LP / 篮子代币(如 Stocks Vault)作为分红资产时,领取到的是篮子代币解包后的底层资产,`assetSymbol` / `usd` 反映的是篮子代币,不一定等于实际收到的资产。
441
+
442
+ ## 兑换
443
+
444
+ 子路径 `@w3lib/evm-batch-call/swap`:在内置协议表的 V2 / V3 池子里找最好的路线,报价并构建 / 发送兑换交易(Tron 上是 SunSwap V2)。
445
+
446
+ ```ts
447
+ import { NATIVE_TOKEN } from '@w3lib/evm-batch-call'
448
+ import { getSwapQuote, swap } from '@w3lib/evm-batch-call/swap'
449
+
450
+ const plan = await getSwapQuote(user, { tokenIn: NATIVE_TOKEN, tokenOut: USDT, amountInFormatted: '0.1' }, { chainId: 56 })
451
+ plan.amountOutFormatted // '72.6'
452
+ plan.route // { protocol: 'pancakeswap-v3', version: 'v3', path: [WBNB, USDT], fees: [100], … }
453
+ plan.transactions // 授权(卖出代币且额度不够时)+ 兑换
454
+
455
+ await swap(provider, key, { tokenIn: USDT, tokenOut: NATIVE_TOKEN, amountInFormatted: '100' }, { slippage: 0.01 })
456
+ ```
457
+
458
+ - **路线**:直接兑换,加上经过包装主币 / 稳定币(`LP_QUOTE_TOKENS`,可用 `intermediates` 改)的两跳;V2 的每个协议、V3 的每个手续费档位都算一条候选。V3 先查哪些池子存在、当前流动性(没有流动性的不算,两跳时每一跳只用流动性最大的 2 个档位),然后批量报价(V2 用 Router 的 `getAmountsOut`,一次报完;QuoterV2 很费 gas,每 3 个一组并发,避免超过节点的 gas 上限),选输出最多的。`routes` 里是全部候选的报价
459
+ - **最少收到**:`amountOutMin = 报价 × (1 − slippage)`(默认 0.5%),交易里由 Router 检查;`priceImpact` 是这笔的价格比极小数量的价格差多少
460
+ - **带转账税的代币**:V2 的报价(`getAmountsOut`)不含转账税,实际收到的更少,默认滑点下会 `INSUFFICIENT_OUTPUT_AMOUNT` 回滚。用 `tax`(如 `0.05`)把最少收到再乘 (1 − tax),税率可用 `/risk` 查;`plan.amountOut` 仍是不含税的报价
461
+ - **授权后重新报价**:`swap` 在有授权时先发授权,确认后重新报价再发兑换,deadline 和最少收到从发兑换那一刻算
462
+ - **交易**:V2 走 Router 的 `swapExact…SupportingFeeOnTransferTokens`(主币进出用 ETH 版本);V3 走 SwapRouter02 / PancakeSwap SmartRouter 的 `multicall(deadline, …)`,卖出主币时附带 value,买入主币时先收到 Router 再 `unwrapWETH9`。卖出代币时额度不够先授权给 Router(`approve: 'max'` 可设无限额度)
463
+ - **协议**:`swapProtocols(chainId)`——V2 是协议表里有 Router 的,V3 是 `V3_SWAP_CONTRACTS` 里有报价和兑换合约的(Uniswap V3:7 条链;PancakeSwap V3:Ethereum、BSC、Base)。V4 和聚合器暂不支持
464
+ - 报价是池子当前的状态,交易上链前价格可能变化;有转账税的代币报价会偏高(实际收到的少),可以先用 [代币风险检查](#代币风险检查) 看税率
465
+
466
+ 主网核对:8 条链(含 Tron)报价正常。在 Base、BSC 上固定一个区块报价(`blockTag`),在同一个区块上用 `eth_call` 执行构建出的兑换交易(卖出代币时用 state override 设置额度):最少收到 = 报价时成功、= 报价 + 1 时失败,说明收到的正好等于报价——覆盖主币 → 稳定币、稳定币 → 主币(授权 + 解包)、V3 两跳、V2 的三种函数;Tron 的 SunSwap V2 两个方向同样核对过。
467
+
468
+ 其他选项:`route` 指定走 `routes` 里的某一条(如让用户选另一条路线);`blockTag` 按某个区块的状态报价。
469
+
470
+ 打包:在已经用到 `Provider` 的基础上,`buildSwap` 约增加 16KB(minify,gzip 约 7KB,含 `/liquidity` 的授权代码和协议表),`swap` 另外按需加载 `/write` 的发送代码。
471
+
472
+ ## 代币风险检查
473
+
474
+ 子路径 `@w3lib/evm-batch-call/risk`:在 `eth_call` 里模拟“买入 → 转账 → 卖出”(deployless 的探测合约,不部署、不发交易),算出税率、判断能不能卖。
475
+
476
+ ```ts
477
+ import { getTokenRisks } from '@w3lib/evm-batch-call/risk'
478
+
479
+ const [risk] = await getTokenRisks([token], { chainId: 56 })
480
+ // { stage: 'ok', honeypot: false, buyTax: 0.05, sellTax: 0.08, transferTax: 0.02, owner: '0x…', implementation: null,
481
+ // flags: ['has-tax', 'owned'], protocol: 'pancakeswap-v2', pool: '0x…', buyGas: 125000, sellGas: 98000 }
482
+ ```
483
+
484
+ - 用代币 / 包装主币里包装主币一侧最深的 V2 池子,从包装主币合约发起 `eth_call`(它持有大量主币,`eth_call` 不需要签名),默认用储备的 1 / 1000(最多 1 个主币)买入
485
+ - **税率**:买入税 = 1 − 实际收到 / Router 报价;转账税:转一成给新地址,看对方收到多少;卖出税取两种算法中较大的:池子实际收到的代币(转账时扣的税,精确),以及换回的主币比报价少多少(有的代币卖出时顺便把收来的税也卖进同一个池子)
486
+ - 节点、网络、限频的错误会抛出,不当成代币的风险;只有执行失败才标为买不进
487
+ - **判断**:`stage` 是模拟到哪一步(`buy-failed` 买不进:可能还没开放交易或限制了合约;`sell-failed` 卖不出)。`honeypot`:卖不出、转不了,或卖出税 ≥ 90%。`flags` 汇总:`honeypot`、`cannot-buy`、`high-buy-tax` / `high-sell-tax` / `high-transfer-tax`(> 10%)、`has-tax`(> 0.5%)、`proxy`(EIP-1967 可升级代理,规则以后可以改)、`owned`(`owner()` 不是零地址)、`no-pool`
488
+ - 只能发现模拟时的行为:针对特定地址、时间、数量才生效的限制(黑名单、交易冷却、单笔上限)不一定能发现;Tron 不支持
489
+
490
+ 核对:在 BSC 主网分叉上部署测试代币(买 5% / 卖 8% / 转账 2% 税、卖出时把攒下的税也卖进池子、禁止卖出、卖出税 95%)和 PancakeSwap V2 池子,税率一致、貔貅盘都能识别;主网上 CAKE、USDT、PEPE 等没有税。
491
+
492
+ 打包:探测合约约 3KB 字节码;在已经用到 `Provider` 的基础上 `getTokenRisks` 约增加 19KB(minify,gzip 约 8KB)。
493
+
494
+ ## 交易模拟
495
+
496
+ 子路径 `@w3lib/evm-batch-call/simulate`:发送前先看一笔(或一串依赖的)交易会发生什么,可以直接传 `buildSwap` / `buildRevokes` / 流动性构建的交易。
497
+
498
+ ```ts
499
+ import { getSimulation, getBatchSimulation } from '@w3lib/evm-batch-call/simulate'
500
+
501
+ const sim = await getSimulation({ to: router, data, value }, { chainId: 1, from: user, usd: true })
502
+ // { success: true, gasUsed: '152345', fee: '…',
503
+ // balanceChanges: [{ address, asset: 'native', symbol: 'ETH', delta: '-1000…', formatted: '-1', usd: -2400 }, { asset: '0xA0b8…', symbol: 'USDC', formatted: '2395.1', usd: 2395 }],
504
+ // approvalChanges: [], decoded: { type: 'swap', protocol: 'Uniswap SwapRouter02', method: 'multicall', summary: 'Swap 1 ETH for 2395.1 USDC on Uniswap SwapRouter02' },
505
+ // warnings: [], transfers: […], mode: 'simulateV1' }
506
+
507
+ const plan = await getSwapQuote(user, { tokenIn: USDT, tokenOut: NATIVE_TOKEN, amountInFormatted: '1000' }, { chainId: 1 })
508
+ const flow = await getBatchSimulation(plan, { chainId: 1, from: user }) // approve + swap 在同一个区块里依次执行,状态延续
509
+ if (!flow.success) throw new Error(flow.results[flow.failedIndex!].error)
510
+ ```
511
+
512
+ - **EVM**:`eth_simulateV1`(实测 publicnode、drpc、mainnet.base.org、bsc-dataseed 都支持,anvil 也支持;`traceTransfers` 把主币转账变成日志)。资产变化和授权变化从日志里算:ERC20 / 721 / 1155 的 Transfer、WETH 的 Deposit / Withdrawal(WETH9 没有 Transfer 事件)、Approval、ApprovalForAll、Permit2 的 Approval / Permit / Lockdown。没有用 deployless 探测合约:公共节点上日志方案够用,也不需要字节码
513
+ - 不支持 `eth_simulateV1` 的节点自动退回 `eth_call` + `eth_estimateGas`(`mode: 'eth_call'`):只有成败、gas、revert 原因,资产变化按 calldata 估(主币 value、ERC20 `transfer`),多笔交易之间状态不延续(结果里有 `no-state` 提示)。`debug_traceCall` 没有用(公共节点基本都关了)。`gas` 默认 min(1000 万, 2500 万 / 笔数):公共节点限制一次模拟的总 gas
514
+ - 选项:`from`、`watch`(另外统计资产变化的地址)、`blockTag`、`stateOverrides`(如给地址加余额)、`usd`(给变化带美元价值,用 `/price`,按需加载)、`risk`(对收到的代币做 `/risk` 检查,按需加载)、`fee`
515
+ - **calldata 解码**(不查网络):ERC20 / 721 / 1155、WETH、Uniswap V2 / V3 Router、V3 仓位合约、Universal Router(命令列表)、Permit2、Multicall3、Disperse、智能账户的 `executeBatch`;Router 的地址按内置协议表标出协议名;multicall 解出子调用;认不出的选择器是 `type: 'unknown'` 带原始选择器。`decodeCalldata` / `decodeRevert` 也可以单独用
516
+ - **revert 原因**:`Error(string)`、`Panic`(带含义)、常见自定义错误(Universal Router、Permit2、OpenZeppelin;`ExecutionFailed` 里的错误一并解开),认不出的是 `custom error 0x选择器`
517
+ - **warnings**(`{ code, severity: 'info' | 'warn' | 'danger', message, address? }`):`unlimited-approval`、`unlimited-nft-approval`、`unknown-spender`(不在 `knownSpenders()` 里)、`approve-to-eoa`(被授权方没有合约代码)、`no-code`(带 calldata 调用没有代码的地址)、`large-value`(主币转出超过余额的 90%)、`token-risk`(`risk: true`)、`unknown-method`、`reverted`
518
+ - `assertSimulation(simulate(…))`:失败或有 danger 提示时抛错,一行守卫“模拟通过才发送”
519
+ - **Tron**:`triggerconstantcontract`(能量记在 `gasUsed`、日志里的 TRC20 Transfer / Approval),TRX 的变化只有 `call_value`(内部转账查不到),没有手续费估算(用 `/fees` 的 `getCostEstimate`),每笔按当前状态单独模拟
520
+
521
+ 核对:以太坊主网用公共节点模拟 USDT 转账、revert、无限授权、WETH 存入、`buildSwap` 的 approve + swap(`usd` 价值与报价一致);在主网分叉(anvil)上先模拟、再真的发出去,USDT 和 ETH 的变化与模拟逐位一致。
522
+
523
+ ## 历史记录
524
+
525
+ 子路径 `@w3lib/evm-batch-call/history`:一个地址的转账 / 兑换 / 授权 / 流动性 / 铸造记录,统一成 `Activity`。
526
+
527
+ ```ts
528
+ import { getHistory } from '@w3lib/evm-batch-call/history'
529
+
530
+ const page = await getHistory(user, { chainId: 1, limit: 20 }) // 默认 Blockscout,失败换 RPC 日志
531
+ // { items: [{ hash, time, block, status: 'success', type: 'swap', label: 'Swapped 1 ETH for 2395 USDC on Uniswap V2 Router',
532
+ // changes: [{ asset: 'native', symbol: 'ETH', delta: '-1000…', formatted: '-1' }, …], fee: '…', protocol, method }, …],
533
+ // nextCursor: 'p:20123456', source: 'blockscout' }
534
+ const more = await getHistory(user, { chainId: 1, cursor: page.nextCursor, types: ['swap'] })
535
+ await getHistory(user, { chainId: 56, source: ['etherscan', 'rpc'], apiKey }) // Etherscan v2:一个 Key 覆盖多条链
536
+ await getHistory(tronAddress, { chainId: 728126428 }) // TronGrid
537
+ ```
538
+
539
+ - **来源(适配器)**:`'blockscout'`(免 Key,内置各链实例表 `BLOCKSCOUT_APIS`,可用 `blockscoutSource({ [chainId]: url })` 补充)、`'etherscan'`(Etherscan v2,`apiKey`)、`'rpc'`(`eth_getLogs`)、`'trongrid'`;传数组按顺序故障切换(某个来源出错 / 被限流就换下一个,全部失败抛 `HistorySourceError`,带每个来源的错误);也可以传自己实现的 `{ name, fetch(query) → RawPage }`。默认:有 Blockscout 实例的链 `[blockscout, rpc]`,其他链 `[rpc]`,传了 `apiKey` 时 etherscan 排最前;Tron 是 trongrid
540
+ - **Blockscout 用的是它的 Etherscan 兼容接口**(`/api?module=account&action=txlist | txlistinternal | tokentx | tokennfttx | token1155tx`),没用 v2 REST:数据相同,但 v2 一页 50 条带大量标签约 500KB;这个接口只有必要字段,并且与 Etherscan 系完全相同。公共实例按请求数限流(一页 3~5 个请求,被限流时自动退避重试,仍失败就换下一个来源);`nft: false` 少 2 个请求
541
+ - **分类**:`approve`(calldata 是 approve / permit / setApprovalForAll…)、`liquidity`(加 / 减流动性、领取手续费)、`mint`(收到来自零地址的 NFT / 代币)、`swap`(calldata 是兑换,或同一笔交易里不同资产一进一出)、`transfer`(其余有资产变化的、或普通转账)、`contract`、`other`;`direction` / `counterparty` / `label` 一并给出;`fee` 只在这个地址是发送者时有(最小单位的主币,Tron 是 sun);失败的交易 `status: 'failed'`、没有资产变化
542
+ - **分页**:`nextCursor` 是不透明字符串(按区块号 / Tron 的毫秒时间戳),各来源通用,可以中途换来源。`limit` 是“至少返回多少条”:按区块整块取,可能略多;`types` / `before` / `after` 过滤后不够 `limit` 会继续翻页(`maxPages`,默认 5)
543
+ - **rpc 来源的局限**:只有 ERC20 / 721 / 1155 的 Transfer 日志(按 topic 里的地址查),**没有主币转账和内部交易**(顶层 value 只在带代币转账的交易里有);按区块窗口(`rpcSource({ window, maxWindows })`,默认 2000 块 × 10)往回扫,稀疏的地址一次调用可能一条都没有,拿 `nextCursor` 继续;节点需要支持无合约地址过滤的 `eth_getLogs`(publicnode 的以太坊节点不支持,drpc / 1rpc 可以)
544
+ - **Tron**:TronGrid 的 `/v1/accounts/{address}/transactions` + `/transactions/trc20`(没有 Key 限频较严,传 `apiKey`);内部的 TRX 转账查不到
545
+
546
+ 核对:以太坊主网上 Blockscout 取一个活跃地址的两页(时间递减、分页不重复),RPC 日志来源取代币转账、识别出 Universal Router 的兑换并分页,TronGrid 取 Binance 热钱包的 TRX / USDT 转账(Blockscout 公共实例限流很严,连续跑测试会被 429 一段时间);用假的 Etherscan 响应单测了分类、分页不丢不重和故障切换。
547
+
548
+ ## 手续费估算
549
+
550
+ 子路径 `@w3lib/evm-batch-call/fees`:当前的手续费建议,和一笔交易大概要付多少手续费。
551
+
552
+ ```ts
553
+ import { getCostEstimate, getFeeEstimate } from '@w3lib/evm-batch-call/fees'
554
+
555
+ await getFeeEstimate({ chainId: 1 })
556
+ // { type: 'eip1559', baseFee: '1200000000', slow: { maxFeePerGas, maxPriorityFeePerGas }, normal: {…}, fast: {…} }
557
+
558
+ await getCostEstimate({ from: user, to: USDT, data }, { chainId: 8453 })
559
+ // { type: 'evm', gasLimit: '45000', gasPrice, fee, maxFee, l1Fee: '…', total, totalFormatted: '0.0000123', symbol: 'ETH' }
560
+
561
+ await getCostEstimate({ from, to: USDT, data }, { chainId: 728126428 })
562
+ // { type: 'tron', energy: 64285, bandwidth: 345, energyBurn: '6428500', bandwidthBurn: '345000', activates: false, totalFormatted: '6.7735', symbol: 'TRX' }
563
+ ```
564
+
565
+ - **费用建议**:按最近 20 个区块的 `eth_feeHistory`,baseFee 取下一个区块的,三档优先费是各区块第 10 / 50 / 90 百分位的中位数(空区块不算),`maxFeePerGas = baseFee × 2 + 优先费`(`/write` 默认用节点的 `getFeeData`,优先费取 `eth_maxPriorityFeePerGas`,公式相同但优先费来源不同)。节点不支持 `eth_feeHistory` 或 baseFee 为 0 的链(如 BSC)按 `eth_gasPrice`(快档 1.2 倍);网络等其他错误照常抛出
566
+ - **一笔交易**:预估 gas(会执行一遍,revert 时抛出)× 这一档的价格;`fee` 按 baseFee + 优先费算(预计实际付的),`maxFee` 是最多付的。OP Stack / Scroll 链加上 L1 数据费(含 25% 余量)。多笔交易可以先查一次 `estimateFees` 再传 `fees` 共用
567
+ - **Tron**:模拟合约调用得到能量,按发送者剩余的质押能量、免费 / 质押带宽抵扣,不够的部分按链参数的价格烧 TRX;转 TRX 给没激活的地址时算上激活费(`activates: true`)。能量按模拟值,不加余量
568
+
569
+ 主网核对:Ethereum、Base(含 L1 数据费)、BSC 上的 ERC20 转账;Tron 上有质押资源和全新地址两种发送者的 USDT 转账,以及给没激活的地址转 TRX。
570
+
571
+ 打包:在已经用到 `Provider` 的基础上,`getFeeEstimate` 约增加 6KB(minify,gzip 约 2KB);`getCostEstimate` 另外按需加载 `/write` 的节点代码,Tron 的部分只在 Tron 上加载。
572
+
573
+ ## 域名解析
574
+
575
+ 子路径 `@w3lib/evm-batch-call/names`:名字 → 地址、地址 → 名字,用 Provider 这条链的域名服务。
576
+
577
+ | 链 | 服务 | 名字 |
578
+ | --- | --- | --- |
579
+ | Ethereum | ENS | `.eth` 以及导入的 DNS 域名 |
580
+ | BSC | SPACE ID | `.bnb` |
581
+ | Base | Basenames | `.base.eth` |
582
+
583
+ ```ts
584
+ import { getAddressNames, getNameAddresses } from '@w3lib/evm-batch-call/names'
585
+
586
+ await getNameAddresses(['vitalik.eth', 'nick.eth'], { chainId: 1 })
587
+ // [{ name: 'vitalik.eth', normalized: 'vitalik.eth', address: '0xd8dA…6045', service: 'ens' }, …]
588
+ await getAddressNames([user], { chainId: 56 })
589
+ // [{ address: user, name: 'alice.bnb', service: 'spaceid' }]
590
+ ```
591
+
592
+ - 名字按 ENSIP-15 规范化(大小写、全角等),结果的 `normalized` 是规范化后的名字;不合法的名字 `normalized` / `address` 为 null
593
+ - 正查读注册表的 resolver、再读 `addr`,两次 multicall;反查读主名字后再正查核对(名字要解析回同一个地址,没核对的反查记录谁都能设置),共四次 multicall
594
+ - 不支持通配解析(ENSIP-10)和链下解析(CCIP-Read),这类名字(如部分子域名)返回 null;Tron 上没有域名服务
595
+ - `services` 可以换成自己的 ENS 式注册表(`{ id, name, chainId, registry, suffixes, reverseSuffix }`)
596
+
597
+ 打包:在已经用到 `Provider` 的基础上约增加 2KB(minify,gzip 不到 1KB;名字规范化用 ethers 自带的 `ensNormalize`)。
598
+
599
+ ## 授权管理
600
+
601
+ 子路径 `@w3lib/evm-batch-call/approvals`:查一个地址给出的授权,批量撤销。
602
+
603
+ ```ts
604
+ import { approvals, getApprovals, revokeApprovals, scanApprovals } from '@w3lib/evm-batch-call/approvals'
605
+
606
+ // 持有的代币 × 已知的被授权方(内置协议的 Router / 仓位合约、Uniswap / PancakeSwap 的 Router、1inch、0x、ParaSwap、Permit2),一次 multicall
607
+ const list = await getApprovals(user, { chainId: 56, tokens: [USDT, CAKE] })
608
+ // [{ type: 'erc20', token: USDT, symbol: 'USDT', spender: '0x1111…', spenderName: '1inch Router v6', amount: '…', unlimited: true },
609
+ // { type: 'permit2', token: USDT, spender: '0x66a9…', spenderName: 'Uniswap Universal Router', amount: '…', expiration: 1767225600, expired: false, … }]
610
+
611
+ await revokeApprovals(provider, key, list.filter((a) => a.type !== 'nft' && a.unlimited))
612
+ ```
613
+
614
+ - **三种授权**:ERC20 的 `allowance`;Permit2 的额度(`amount` / `expiration` / `nonce`,过期的不返回);NFT 的 `isApprovedForAll`(传 `nfts`,默认检查 OpenSea、Blur 的 operator)。`includeZero: true` 时也返回额度为 0 / 已过期 / 未授权的项
615
+ - **检查范围**:`tokens` × `spenders`(默认 `knownSpenders(chainId)`),加上直接给出的 `pairs` / `nftPairs`。被授权方不在已知列表里时 `spenderName` 为 null
616
+ - **按事件发现**:`scanApprovals` 扫地址发出的 `Approval` / `ApprovalForAll` 事件和 Permit2 的 `Approval` / `Permit` 事件,返回授权过的 `pairs` / `nftPairs`(只发现,当前是否还有效交给 `approvals` 查),可以保存 `progress` 下次接着扫。需要节点支持不带合约地址的 `eth_getLogs`(publicnode 等节点要求带地址,不行;drpc、1rpc、flashbots 可以),公共节点的区块范围限制很严,适合从某个时间点开始增量扫描,不适合扫全部历史。Tron 不支持
617
+ - **撤销**:`buildRevokes` 只构建交易(ERC20 每个一笔 `approve(spender, 0)`,Permit2 的全部合成一笔 `lockdown`,NFT 每个一笔 `setApprovalForAll(false)`);`revokeApprovals` 构建后发送,钱包和选项同 `batchCall`(`mode: '7702'` 时同一钱包的撤销合成一笔交易),默认一笔失败不影响后面的
618
+ - **Tron**:SunSwap 的 Router / 仓位合约的额度和撤销(T 地址),没有 Permit2
619
+
620
+ 主网核对:Ethereum 上从最近区块的真实 `Approval` / `Permit` / `ApprovalForAll` 事件取授权,额度和合约一致,`scanApprovals` 能发现,撤销的三种交易都能预估通过;Tron 上读到 SunSwap 的真实授权,撤销能模拟。
621
+
622
+ 打包:在已经用到 `Provider` 的基础上,`getApprovals` 约增加 11KB(minify,gzip 约 5KB),`scanApprovals` 很小(约 4KB gzip,不含 Provider);发送的代码按需加载,只查授权时不会打包。
623
+
624
+ ## 免 gas 授权(permit)
625
+
626
+ 子路径 `@w3lib/evm-batch-call/permit`:用签名代替 `approve` 交易。EIP-2612 permit(含 DAI 风格)、Permit2 的 `PermitSingle` / `PermitBatch`(AllowanceTransfer)和 `permitTransferFrom`(SignatureTransfer)。构建 EIP-712 数据 → 用 ethers Signer 签名(私钥 Wallet、浏览器钱包的 `JsonRpcSigner` 都行)→ 本地验证 → 生成上链 calldata。
627
+
628
+ ```ts
629
+ import { buildPermit, signPermit, permitCalldata, verifyPermitSignature, buildPermit2Single, permit2Calldata } from '@w3lib/evm-batch-call/permit'
630
+
631
+ const permit = await buildPermit(provider, { token: USDC, owner, spender: router, value: 100_000_000n }) // 精确额度、30 分钟有效
632
+ const signed = await signPermit(signer, permit) // 签名前核对,签完本地验签
633
+ verifyPermitSignature(permit, signed.signature) // true
634
+ permitCalldata(signed) // token.permit(...) 的 calldata:谁都可以提交
635
+
636
+ const p2 = await buildPermit2Single(provider, { token: USDC, owner, spender: router, amount: 100_000_000n })
637
+ permit2Calldata(await signPermit(signer, p2)) // Permit2.permit(...)
638
+ ```
639
+
640
+ - **检测**:`detectPermit(provider, token)` 读 `nonces` / `DOMAIN_SEPARATOR` / `name` / `version` / `eip712Domain`(EIP-5267),按链 ID 和地址重新算 domain,**与代币的 `DOMAIN_SEPARATOR` 逐位一致**才算支持(USDC v2 的 `version` 是 `'2'`;没有 `version()` 时试 `'1'` / `'2'`);`PERMIT_TYPEHASH` 识别 DAI 风格。不一致时返回原因,不签
641
+ - **安全默认**:精确额度(`MaxUint256` / `MaxUint160` 要 `allowUnlimited: true`;DAI 风格的 permit 只能无限额度,也要显式允许);有效期默认 30 分钟,超过 24 小时要 `allowLongDeadline: true`;数量为 0 的空白 permit、零地址 spender 直接拒绝;`signPermit` 签名前核对签名者 = owner、`chainId`、`verifyingContract`(代币本身 / Permit2)、typed data 里的 spender / 数量 / 有效期与 `permit` 摘要一致(防被替换),钱包返回的签名不是 owner 的也报错
642
+ - **Permit2 的 `permitTransferFrom`**:nonce 默认随机(无序位图,不会冲突);只有签名里的 `spender` 能调用;`permitTransferFromCalldata` 的 `requestedAmount` 不能超过签名的数量
643
+ - 合约账户(ERC-1271)的签名无法本地验证(`verifyPermitSignature` 返回 false),也不能签 EIP-2612(链上要求 EOA 签名);撤销 Permit2 额度用 `/approvals` 的 `buildRevokes`(lockdown)
644
+
645
+ **在 `swap` 里用**(`approve: 'permit' | 'permit2' | 'auto'`,实际采用的方式在 `result.approval`):
646
+
647
+ | 路线 | 能否用签名代替授权 |
648
+ | --- | --- |
649
+ | V3(Uniswap SwapRouter02 / PancakeSwap SmartRouter) | 能:`selfPermitIfNecessary` 放进同一笔 `multicall`,一次签名、一笔交易;代币要支持 EIP-2612 |
650
+ | V2 Router | Router 没有 `selfPermit`:单独发 `permit()` 交易省不了什么,所以默认退回普通授权;用 `mode: '7702' / '4337' / '5792'` 把 permit 和兑换放进同一笔原子批量时才用 permit |
651
+ | Permit2 | 内置的 V2 / V3 Router 都不读 Permit2(直接 `transferFrom`),`'permit2'` 会退回普通的精确授权,`result.approval.reason` 写明;Universal Router 暂不支持 |
652
+ | DAI 风格 / 不支持 permit 的代币 / Tron / 4337 的合约账户 | 退回普通授权,`reason` 里有原因 |
653
+
654
+ ```ts
655
+ const r = await swap(provider, signer, { tokenIn: USDC, tokenOut: NATIVE_TOKEN, amountInFormatted: '20' }, { approve: 'auto' })
656
+ r.approval // { requested: 'auto', method: 'permit', via: 'selfPermit' } 或 { method: 'approve', reason: '…' }
657
+ ```
658
+
659
+ permit 的有效期与兑换的 `deadline` 相同,额度是精确的卖出数量;用户拒绝签名时整个 `swap` 中止,不会悄悄改成发授权交易。
660
+
661
+ 主网核对(Base 分叉,anvil Prague):USDC 的 permit 提交后额度正好是签的数量、重放失败;Permit2 的 PermitSingle 链上验签通过,`permitTransferFrom` 由指定 spender 转走、超额 / 重放 / 他人调用都失败;`swap` 的 `'permit'` 在 Uniswap V3、PancakeSwap V3 上一笔交易完成(额度用完归零),V2 路线 + 7702 代付的 permit + 兑换原子完成。
662
+
663
+ ## 价格
664
+
665
+ 子路径 `@w3lib/evm-batch-call/price`:代币的美元价格和地址的资产总值。
666
+
667
+ ```ts
668
+ import { NATIVE_TOKEN } from '@w3lib/evm-batch-call'
669
+ import { getPortfolio, getTokenPrices } from '@w3lib/evm-batch-call/price'
670
+
671
+ await getTokenPrices([NATIVE_TOKEN, USDT, newToken], { chainId: 56 })
672
+ // [{ token: '0xEeee…', native: true, price: 612.3, source: 'defillama' }, …, { token: newToken, price: 0.0012, source: 'onchain' }]
673
+
674
+ const p = await getPortfolio(user, { chainId: 56, tokens: [NATIVE_TOKEN, USDT, CAKE], lp: true })
675
+ p.totalUsd // 代币 + LP 仓位的合计;p.tokens 按价值排序,p.lp 是 LP 仓位
676
+ ```
677
+
678
+ - 价格源依次使用,前面没有价格的代币交给后面的(`sources`,默认 `[defillamaPrices(), onchainPrices()]`),结果的 `source` 是给出价格的源
679
+ - `defillamaPrices({ at })`:历史价格(unix 秒或 `Date`);`minConfidence` 过滤置信度低的价格(默认 0.9)
680
+ - `onchainPrices()`:在内置协议表的 V2 / V3 池子里找代币对包装主币、稳定币的池子,用计价币一侧最深的池子的价格;稳定币按 1 美元。V3 池子的深度取计价币余额和当前价格附近的流动性折算值中较小的(只看余额会被“远处放一笔、再推价格”的方式骗过),`minLiquidityUsd`(默认 10000)以下的池子不用。这只能挡住很小的池子:有人愿意自己出资建一个够深的池子时,价格仍然可以是他定的,所以新币的链上价格只适合展示。主网实测和 DefiLlama 相差在 0.5% 以内(Ethereum、BSC、Base、Tron)
681
+ - `combinePrices([…])` 把多个源组合成一个,可以传给 `getLpPositions` / `getOwnerTokens` 的 `prices`
682
+ - 资产总值只计入传入的代币(EVM 不能只靠节点列出地址持有的全部代币),`lp: true` 时加上 LP 仓位(参数同 `getLpPositions`)
683
+
684
+ 价格只用于展示和估值:链上价格是池子当前的价格,可能被单个区块内的交易影响,不要用它算交易的最小数量。
685
+
686
+ 打包:在已经用到 `Provider` 的基础上,`getTokenPrices` 约增加 12KB(minify,gzip 约 5KB),`getPortfolio`(含 LP 仓位)约 31KB(gzip 约 12KB),均不含 ethers。
687
+
374
688
  ## 功能
375
689
 
376
690
  - **常用查询**:余额、代币详情、授权额度、NFT(集合 / 持有 / 元数据地址 / ERC1155 余额,子路径 `/nft`),一个函数一次请求
691
+ - **兑换**(子路径 `/swap`):V2 / V3 池子的最佳路线(直接或两跳)、报价、构建和发送,主币自动包装 / 解包
692
+ - **代币风险检查**(子路径 `/risk`):模拟买入 / 转账 / 卖出,算出买卖税、识别貔貅盘,代理合约、owner
693
+ - **手续费估算**(子路径 `/fees`):EIP-1559 三档费用建议,一笔交易的手续费(含 L1 数据费),Tron 按能量 / 带宽算要烧的 TRX
694
+ - **域名解析**(子路径 `/names`):ENS、SPACE ID(.bnb)、Basenames(.base.eth),批量正查、反查(正查核对)
695
+ - **授权管理**(子路径 `/approvals`):ERC20 / Permit2 / NFT 授权的批量查询(默认检查常见 Router、聚合器、NFT 市场),按事件发现,批量撤销
696
+ - **价格**(子路径 `/price`):DefiLlama 当前 / 历史价格、链上 V2 / V3 池子价格,组合价格源,地址的资产总值(代币 + LP 仓位)
377
697
  - **资产列表**(不推荐使用,尽力而为,子路径 `/owner`):`getOwnerTokens`,免费公开代币列表或带 Key 的索引服务发现代币,multicall 核对余额,可选 DefiLlama 价格
378
698
  - **批量读取**:`all` / `tryAll` / `tryEach`,N 条读调用合成一次 `eth_call`;支持数组或对象输入
379
699
  - **自动合并**:绑定合约直接 `await`、`provider.call(x)`,以及 `balances` / `tokens` / `allowances`,同一收集窗口内的调用合并成一次请求并去重
380
700
  - **预执行**:`staticCall` / `staticCallAll` / `method.staticCall`,带 `from` / `value` 模拟交易,返回解析好的 revert 原因
381
701
  - **主币余额合并**:`getEthBalance` 与合约调用在同一次 `eth_call` 里——无论链上有没有 Multicall3
382
- - **任意链可用**:内置 Multicall3 地址表;表里没有、地址无效、或查询早于部署区块时,自动改用 deployless(链上无需部署任何合约)。少数链不支持 deployless(合约创建式 `eth_call`,如 Oasis Emerald、Aurora),这些链已在地址表里,只能查合约部署之后的状态;`pnpm check:rpc` 会检查内置节点表里每条链至少有一种批量方式可用
702
+ - **任意链可用**:内置 Multicall3 地址表;表里没有、地址无效、或查询早于部署区块时,自动改用 deployless(链上无需部署任何合约)。少数链不支持 deployless(合约创建式 `eth_call` 返回空数据,如 Oasis Emerald、Aurora):表里有 Multicall3 的链只能查合约部署之后的状态;deployless 失败时(空数据 / 无法解码)自动改成逐条 `eth_call`(主币余额用 `eth_getBalance`,合并在一个 HTTP batch 里,慢一些但任何链都能用),确认不支持后记住 10 分钟、不再先试一次 deployless;`pnpm check:rpc` 会检查内置节点表里每条链至少有一种批量方式可用
383
703
  - **备用节点**:传多个节点自动故障切换;不传节点使用内置公共节点表(34 条 EVM 链 + Tron)
384
704
  - **chainId 可选**:传了节点就能自动识别 chainId,结果缓存,钱包切链自动失效
385
705
  - **Tron**:走 Tron HTTP API,T 开头地址可以直接用
@@ -596,6 +916,50 @@ await window.tronLink.request({ method: 'tron_requestAccounts' }) // 先连接
596
916
  await batchTransfer(new Provider(window.tronWeb), window.tronWeb, recipients)
597
917
  ```
598
918
 
919
+ ## 智能账户批量(原子执行、代付 gas)
920
+
921
+ `batchCall` / `batchTransfer`(以及 `batchCollect`、`swap`)的 `mode` 增加三种,同一钱包的调用合成**一笔原子执行**的操作,gas 可以由别人付。底层积木在子路径 `@w3lib/evm-batch-call/smart-account`;这些模式的代码按需加载,不用时 `/write` 的体积不变。
922
+
923
+ | mode | 谁签什么 | 谁付 gas | 适用钱包 |
924
+ | --- | --- | --- | --- |
925
+ | `'7702'` + `sponsor` | 用户签 EIP-7702 授权(未委托时)+ UserOperation(EIP-712) | `sponsor` 钱包(用户不需要主币) | 能本地签名的钱包(私钥 / Wallet) |
926
+ | `'4337'` | owner 签 UserOperation(v0.7 用 personal_sign,v0.8 用 EIP-712,浏览器钱包也行) | 账户自己 / paymaster / 自己打包的钱包 | 任何能签名的钱包 |
927
+ | `'5792'` | 钱包弹一次确认 | 钱包决定 | 支持 EIP-5792 的浏览器钱包 |
928
+
929
+ ```ts
930
+ // 1) 代付 gas 的 7702:用户没有主币,sponsor 付 gas;一个 UserOperation 里的转账原子执行
931
+ await batchTransfer(multi, userKey, recipients, { token: USDT, mode: '7702', sponsor: sponsorKey })
932
+ await batchCollect(multi, [kid1, kid2], hot, { tokens: [USDT, NATIVE_TOKEN], sponsor: sponsorKey }) // 子钱包不用补 gas,主币全额转走
933
+
934
+ // 2) ERC-4337:owner 对应的 SimpleAccount(首次使用时部署,资产放在账户地址里)
935
+ import { getSimpleAccount } from '@w3lib/evm-batch-call/smart-account'
936
+ const { address } = await getSimpleAccount(multi, owner) // 部署前就能算出
937
+ await batchTransfer(multi, ownerKey, recipients, {
938
+ mode: '4337',
939
+ smartAccount: {
940
+ bundler: 'https://public.pimlico.io/v2/8453/rpc', // 或 BundlerClient
941
+ paymaster: new PaymasterClient(pmUrl, { sponsorshipPolicyId }), // ERC-7677;或 (op, ctx) => ({ paymaster, paymasterData, … })
942
+ // account: 'simple7702' 让 UserOperation 的 sender 就是钱包自己(EntryPoint v0.8);entryPoint: '0.7' | '0.8'
943
+ // selfBundle: key | true 不用 bundler:自己发 EntryPoint.handleOps 交易
944
+ },
945
+ })
946
+
947
+ // 3) 浏览器钱包 EIP-5792:支持原子批量时一次确认,不支持时退回逐笔发送(result.atomic 为 false)
948
+ const signer = await new BrowserProvider(window.ethereum).getSigner()
949
+ await batchCall(multi, calls.map((c) => ({ from: signer, ...c })), { mode: '5792' }) // atomicRequired: true 时不退回、直接失败
950
+ ```
951
+
952
+ - **EntryPoint v0.7 / v0.8**:UserOperation 的打包、哈希(v0.8 是 EIP-712,`userOpTypedData`;7702 账户按 EntryPoint 的规则把 initCode 换成委托合约地址)、签名都是手写的(只依赖 ethers)。账户:`'simple'`(eth-infinitism SimpleAccount,工厂地址在 `SIMPLE_ACCOUNT_FACTORY_V07/V08`)和 `'simple7702'`(`SIMPLE_7702_ACCOUNT`,只能用 v0.8)
953
+ - **代付 7702 的原理**:Simple7702Account 的 `executeBatch` 只允许账户自己或 EntryPoint 调用,sponsor 不能直接调;所以由 sponsor 发 `EntryPoint.handleOps` 交易(带用户的 7702 授权),UserOperation 的 `maxFeePerGas` 为 0,用户不付 gas,sponsor 也拿不到补偿。用户签的 UserOperation 固定了调用内容,sponsor 改不了;用户的 nonce 防重放。委托会保留,可用 `revokeDelegation` 撤销
954
+ - **bundler / paymaster**:`BundlerClient`(`eth_sendUserOperation` / `eth_estimateUserOperationGas` / `eth_getUserOperationReceipt` / `eth_supportedEntryPoints`,轮询 `waitForUserOperation`)、`PaymasterClient`(ERC-7677 的 `pm_getPaymasterStubData` / `pm_getPaymasterData`,URL 和 Key 由应用提供,报错信息不带 URL);`paymaster` 也可以是自定义函数
955
+ - **结果**:仍是 `WriteResult`,`from` 是账户(sender)地址,多了 `userOpHash` 和 `atomic`;`gasUsed` / `fee` 是 UserOperation 的 `actualGasUsed` / `actualGasCost`(账户或 paymaster 付的,代付时为 0),不含打包交易的其余部分;UserOperation 执行失败(打包交易成功)为 `reverted`,EntryPoint 拒绝(如 `AA24 signature error`)为 `failed`
956
+ - 自己打包时 gas 是本地估算(整批能执行时估整批,否则逐个调用相加 + 余量;可用 `smartAccount.gas` 覆盖);用 bundler 时由 bundler 估算
957
+ - 同一钱包的多批(超过 `batchSize`)按顺序发,上一批确认后再发下一批;`sponsor` / 自己打包用同一个钱包发时,不同子钱包也按顺序发(避免抢 nonce)
958
+
959
+ 主网核对(Base 分叉,anvil Prague):`getUserOpHash` 与 `EntryPoint.getUserOpHash` 逐位一致(v0.7、v0.8、含 7702 的 initCode 替换);未部署的 SimpleAccount(v0.7 / v0.8)首次部署 + 主币 / USDC 批量转账、一个调用 revert 时整批回滚;7702 代付(用户余额为 0)、已委托后再来一批不带授权、代付归集、签名错误被 EntryPoint 拒绝(`AA24`)。公共 bundler(Pimlico 免费端点):`eth_supportedEntryPoints`、`eth_estimateUserOperationGas` 只读调用通过。EIP-5792 用模拟钱包的单元测试覆盖(没有在真实钱包里试过)。
960
+
961
+ 局限:4337 的账户 `'simple'` 的资产在账户地址里(不是 owner 的地址),合约账户不能签 EIP-2612;7702 的授权需要本地签名,浏览器钱包只能用 `'4337'` 的 SimpleAccount 或 `'5792'`;bundler 路径中带 7702 授权(`eip7702Auth`)的调用只做了请求格式层面的核对,没有在真实 bundler 上提交;Tron 不支持这些模式。
962
+
599
963
  ## 主币余额与 deployless
600
964
 
601
965
  `getEthBalance`(以及 `balances` 里的主币)默认在两种模式下都和合约调用在同一次 `eth_call` 里:
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ interface IRouter {
5
+ function WETH() external view returns (address);
6
+ function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts);
7
+ function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external payable;
8
+ function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external;
9
+ }
10
+
11
+ interface IERC20 {
12
+ function balanceOf(address owner) external view returns (uint256);
13
+ function transfer(address to, uint256 amount) external returns (bool);
14
+ function approve(address spender, uint256 amount) external returns (bool);
15
+ }
16
+
17
+ /// @notice 代币风险探测:只在 eth_call 里以 deployless 方式执行(构造函数跑完把结果作为“代码”返回),不部署、不发交易。
18
+ /// 用附带的主币通过 V2 Router 买入,把一部分转给一个新地址,再把剩下的卖回主币,分别记录报价和实际数量,用来算买入税、转账税、卖出税,
19
+ /// 以及能不能卖(貔貅盘)。每一步失败都不影响返回结果,失败的步骤在 stage 里标出。
20
+ contract TokenProbe {
21
+ struct Result {
22
+ uint256 stage;
23
+ uint256 buyQuote;
24
+ uint256 bought;
25
+ uint256 transferSent;
26
+ uint256 transferReceived;
27
+ uint256 sellAmount;
28
+ uint256 sellQuote;
29
+ uint256 sold;
30
+ uint256 buyGas;
31
+ uint256 sellGas;
32
+ /// 卖出时池子实际收到的代币(扣掉卖出税之后的)
33
+ uint256 pairReceived;
34
+ }
35
+
36
+ /// stage:0 全部完成;1 买入失败;2 转账失败;3 授权失败;4 卖出失败
37
+ constructor(address router, address token, address pair) payable {
38
+ Result memory r;
39
+ address weth = IRouter(router).WETH();
40
+ address[] memory path = new address[](2);
41
+ path[0] = weth;
42
+ path[1] = token;
43
+ try IRouter(router).getAmountsOut(msg.value, path) returns (uint256[] memory amounts) {
44
+ r.buyQuote = amounts[1];
45
+ } catch {}
46
+
47
+ uint256 gasBefore = gasleft();
48
+ try IRouter(router).swapExactETHForTokensSupportingFeeOnTransferTokens{value: msg.value}(0, path, address(this), block.timestamp) {
49
+ r.buyGas = gasBefore - gasleft();
50
+ r.bought = IERC20(token).balanceOf(address(this));
51
+ } catch {
52
+ r.stage = 1;
53
+ }
54
+
55
+ // 几乎没买到(买入税接近 100%)时跳过转账这一步(转 0 个可能失败,阶段会被误标),直接试卖出
56
+ if (r.stage == 0 && r.bought >= 10) {
57
+ // 转一成给一个没用过的地址,看对方实际收到多少
58
+ address receiver = address(uint160(uint256(keccak256(abi.encode(address(this), token)))));
59
+ r.transferSent = r.bought / 10;
60
+ if (_call(token, abi.encodeWithSelector(IERC20.transfer.selector, receiver, r.transferSent))) {
61
+ r.transferReceived = IERC20(token).balanceOf(receiver);
62
+ } else {
63
+ r.stage = 2;
64
+ }
65
+ }
66
+
67
+ if (r.stage == 0) {
68
+ r.sellAmount = IERC20(token).balanceOf(address(this));
69
+ if (!_call(token, abi.encodeWithSelector(IERC20.approve.selector, router, type(uint256).max))) {
70
+ r.stage = 3;
71
+ }
72
+ }
73
+
74
+ if (r.stage == 0) {
75
+ path[0] = token;
76
+ path[1] = weth;
77
+ try IRouter(router).getAmountsOut(r.sellAmount, path) returns (uint256[] memory amounts) {
78
+ r.sellQuote = amounts[1];
79
+ } catch {}
80
+ uint256 balanceBefore = address(this).balance;
81
+ uint256 pairBefore = IERC20(token).balanceOf(pair);
82
+ gasBefore = gasleft();
83
+ try IRouter(router).swapExactTokensForETHSupportingFeeOnTransferTokens(r.sellAmount, 0, path, address(this), block.timestamp) {
84
+ r.sellGas = gasBefore - gasleft();
85
+ r.sold = address(this).balance - balanceBefore;
86
+ r.pairReceived = IERC20(token).balanceOf(pair) - pairBefore;
87
+ } catch {
88
+ r.stage = 4;
89
+ }
90
+ }
91
+
92
+ bytes memory result = abi.encode(r);
93
+ assembly {
94
+ return(add(result, 32), mload(result))
95
+ }
96
+ }
97
+
98
+ /// 调用 transfer / approve:成功且(没有返回值或返回 true)。USDT 等不返回 bool 的代币也算成功
99
+ function _call(address token, bytes memory data) private returns (bool) {
100
+ (bool ok, bytes memory ret) = token.call(data);
101
+ return ok && (ret.length == 0 || (ret.length >= 32 && abi.decode(ret, (bool))));
102
+ }
103
+ }