@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/src/aggregate.ts CHANGED
@@ -78,6 +78,12 @@ const AGGREGATE3_RESULT_SELECTOR = id('Aggregate3Result((bool,bytes)[])').slice(
78
78
  */
79
79
  const unusableMulticalls = new Map<string, number>()
80
80
 
81
+ /**
82
+ * 不支持 deployless 的链(合约创建式 eth_call 返回空数据 / 无法解码,如 Oasis Emerald、Aurora),记录到什么时候为止
83
+ * 直接逐条 eth_call,不再先试一次注定失败的 deployless;到期后重新尝试
84
+ */
85
+ const deploylessUnsupported = new Map<number, number>()
86
+
81
87
  /** 判定不可用后多久重新尝试合约(节点偶发返回空数据时不至于整个会话都不用 Multicall3) */
82
88
  const UNUSABLE_TTL = 10 * 60 * 1000
83
89
  /** 单节点时连续返回空数据 / 无法解码的次数(多节点时由多个节点一致的结果确认,见 aggregateRawChunk) */
@@ -483,9 +489,109 @@ async function callDeployless(
483
489
  overrides: CallOverrides,
484
490
  atLeast?: bigint,
485
491
  ): Promise<RawResult[]> {
486
- const chunks = splitByInitcodeSize(requests)
487
- const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides, atLeast)))
488
- return results.flat()
492
+ const until = deploylessUnsupported.get(ctx.chainId)
493
+ if (until !== undefined) {
494
+ if (Date.now() < until) {
495
+ return callPlain(ctx, requests, overrides, atLeast)
496
+ }
497
+ deploylessUnsupported.delete(ctx.chainId)
498
+ }
499
+ try {
500
+ const chunks = splitByInitcodeSize(requests)
501
+ const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides, atLeast)))
502
+ invalidStrikes.delete(deploylessKey(ctx.chainId))
503
+ return results.flat()
504
+ } catch (err) {
505
+ const verdict = deploylessVerdict(err)
506
+ if (verdict === null) {
507
+ throw err
508
+ }
509
+ // 空数据 / 无法解码:这条链(或这个节点)不支持合约创建式 eth_call。这一次改成逐条 eth_call;
510
+ // 多个节点一致、或单节点连续 2 次才记住(节点偶发返回一次空数据不算)
511
+ // 只看最新状态:历史区块查询返回空数据可能只是节点裁剪了状态,不能据此判定这条链不支持
512
+ if (isLatest(overrides.blockTag)) {
513
+ const key = deploylessKey(ctx.chainId)
514
+ const strikes = (invalidStrikes.get(key) ?? 0) + 1
515
+ if (verdict === 'confirmed' || strikes >= 2) {
516
+ deploylessUnsupported.set(ctx.chainId, Date.now() + UNUSABLE_TTL)
517
+ invalidStrikes.delete(key)
518
+ } else {
519
+ invalidStrikes.set(key, strikes)
520
+ }
521
+ }
522
+ return callPlain(ctx, requests, overrides, atLeast)
523
+ }
524
+ }
525
+
526
+ const deploylessKey = (chainId: number) => `deployless:${chainId}`
527
+
528
+ /** deployless 的失败是不是“不支持”:InvalidResultError;经 FallbackRpc 时是所有节点都这样(2 个以上为 'confirmed') */
529
+ function deploylessVerdict(err: unknown): 'confirmed' | 'maybe' | null {
530
+ if (err instanceof InvalidResultError) {
531
+ return 'maybe'
532
+ }
533
+ if (err instanceof AllNodesFailedError && err.errors.length > 0 && err.errors.every((e) => e.error instanceof InvalidResultError)) {
534
+ return err.errors.length >= 2 ? 'confirmed' : 'maybe'
535
+ }
536
+ return null
537
+ }
538
+
539
+ /** 逐条 eth_call 的并发上限(公共节点常对 batch 大小和并发限频) */
540
+ const PLAIN_CONCURRENCY = 50
541
+
542
+ /**
543
+ * 最后的兜底:每条请求单独 eth_call(主币余额用 eth_getBalance),不需要链上有合约、也不要求节点支持合约创建。
544
+ * 同时最多 PLAIN_CONCURRENCY 条在途,JsonRpcProvider / FallbackRpc 会把同一时刻的请求合到 HTTP batch 里。
545
+ * 区块号查询:blockTag 是数字(或 earliest)时就是它,最新状态时用 getBlockNumber 并把所有读取固定在这个区块上
546
+ * (minBlock 时先校验再读,保证各条一致);finalized / safe 这类标签没法换算成区块号,带区块号查询时报错
547
+ */
548
+ async function callPlain(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides, atLeast?: bigint): Promise<RawResult[]> {
549
+ const { provider } = ctx
550
+ let blockTag = overrides.blockTag
551
+ let block: bigint | null = null
552
+ if (atLeast !== undefined || requests.some((req) => req.blockNumber)) {
553
+ if (isLatest(blockTag)) {
554
+ if (!provider.getBlockNumber) {
555
+ throw new Error('This chain does not support deployless calls and the provider has no getBlockNumber(): block number queries (minBlock / withBlock) are unavailable')
556
+ }
557
+ block = BigInt(await provider.getBlockNumber())
558
+ if (atLeast !== undefined) {
559
+ assertBlock(block, atLeast)
560
+ }
561
+ blockTag = Number(block)
562
+ } else if (blockTag === 'earliest') {
563
+ block = 0n
564
+ } else if (typeof blockTag === 'number' || typeof blockTag === 'bigint' || (typeof blockTag === 'string' && /^0x[0-9a-f]+$/i.test(blockTag))) {
565
+ block = BigInt(blockTag)
566
+ } else {
567
+ throw new Error(`Block number queries are not supported with blockTag '${String(blockTag)}' on a chain without deployless calls; pass a block number`)
568
+ }
569
+ }
570
+ const results: RawResult[] = new Array(requests.length)
571
+ let next = 0
572
+ const worker = async () => {
573
+ for (let i = next++; i < requests.length; i = next++) {
574
+ const req = requests[i] as CallRequest
575
+ if (req.blockNumber && block !== null) {
576
+ results[i] = { success: true, returnData: encodeUint256(block) }
577
+ } else if (req.ethBalanceOf !== undefined) {
578
+ results[i] = { success: true, returnData: encodeUint256(await provider.getBalance(req.ethBalanceOf, blockTag)) }
579
+ } else {
580
+ try {
581
+ results[i] = { success: true, returnData: await provider.call({ to: req.target, data: req.callData, blockTag, from: overrides.from }) }
582
+ } catch (err) {
583
+ // 合约自己 revert 才算这一条失败(allowFailure 为 false 时和 Multicall3 一样整批失败);限频、超时等节点错误照常抛出
584
+ if (req.allowFailure && isExecutionError(err)) {
585
+ results[i] = { success: false, returnData: (err as { data?: string | null }).data ?? '0x' }
586
+ } else {
587
+ throw err
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ await Promise.all(Array.from({ length: Math.min(PLAIN_CONCURRENCY, requests.length) }, worker))
594
+ return results
489
595
  }
490
596
 
491
597
  async function callDeploylessChunk(
@@ -605,4 +711,5 @@ function cacheKey(chainId: number, address: string): string {
605
711
  export function resetMulticallCache(): void {
606
712
  unusableMulticalls.clear()
607
713
  invalidStrikes.clear()
714
+ deploylessUnsupported.clear()
608
715
  }
@@ -0,0 +1,462 @@
1
+ /**
2
+ * 授权管理(子路径 `@w3lib/evm-batch-call/approvals`):批量查一个地址给出的授权——ERC20 额度、Permit2 额度(含过期时间)、
3
+ * NFT 的 setApprovalForAll;按 Approval 事件增量发现授权过的代币;批量撤销(approve 0、Permit2 lockdown、setApprovalForAll false)
4
+ */
5
+ import { Interface } from 'ethers'
6
+
7
+ import { formatUnits } from './erc20.js'
8
+ import { LP_PROTOCOLS } from './lp.js'
9
+ import type { Provider } from './provider.js'
10
+ import { runLogScan, type LogScanProgress } from './scan.js'
11
+ import { isTronChain, toEvmAddress, toTronAddress } from './tron.js'
12
+ import type { BatchCallOptions } from './write/batch.js'
13
+ import type { WriteResult } from './write/send.js'
14
+ import type { SignerInput } from './write/signer.js'
15
+
16
+ /** Uniswap 的 Permit2(各 EVM 链上地址相同;Tron 上没有) */
17
+ export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
18
+
19
+ /** 已知的被授权方(显示名称用;也是 approvals 默认检查的范围) */
20
+ export interface KnownSpender {
21
+ address: string
22
+ name: string
23
+ /** 'erc20':代币授权的对象(Router、聚合器);'nft':NFT 市场的 operator */
24
+ kind: 'erc20' | 'nft'
25
+ }
26
+
27
+ const erc20 = (address: string, name: string): KnownSpender => ({ address, name, kind: 'erc20' })
28
+ const nft = (address: string, name: string): KnownSpender => ({ address, name, kind: 'nft' })
29
+
30
+ /** 多条链上地址相同的合约(CREATE2 部署) */
31
+ const EVERYWHERE: readonly KnownSpender[] = [
32
+ erc20(PERMIT2_ADDRESS, 'Uniswap Permit2'),
33
+ erc20('0x111111125421cA6dc452d289314280a0f8842A65', '1inch Router v6'),
34
+ erc20('0x1111111254EEB25477B68fb85Ed929f73A960582', '1inch Router v5'),
35
+ erc20('0x6A000F20005980200259B80c5102003040001068', 'ParaSwap Augustus v6.2'),
36
+ erc20('0x0000000000001fF3684f28c67538d4D072C22734', '0x AllowanceHolder'),
37
+ nft('0x1E0049783F008A0085193E00003D00cd54003c71', 'OpenSea Seaport Conduit'),
38
+ ]
39
+
40
+ /** 各链的 Router(不含 LP_PROTOCOLS 里已有的 V2 Router、仓位合约,它们自动加入)。2026-10 在链上核对过有合约 */
41
+ const BY_CHAIN: Readonly<Record<number, readonly KnownSpender[]>> = {
42
+ 1: [
43
+ erc20('0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', 'Uniswap SwapRouter02'),
44
+ erc20('0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af', 'Uniswap Universal Router'),
45
+ erc20('0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', 'Uniswap Universal Router (old)'),
46
+ erc20('0x13f4EA83D0bd40E75C8222255bc855a974568Dd4', 'PancakeSwap Smart Router'),
47
+ erc20('0xDef1C0ded9bec7F1a1670819833240f027b25EfF', '0x Exchange Proxy'),
48
+ erc20('0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', 'ParaSwap Augustus v5'),
49
+ nft('0x00000000000111AbE46ff893f3B2fdF1F759a8A8', 'Blur Execution Delegate'),
50
+ ],
51
+ 56: [
52
+ erc20('0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2', 'Uniswap SwapRouter02'),
53
+ erc20('0x1906c1d672b88cD1B9aC7593301cA990F94Eae07', 'Uniswap Universal Router'),
54
+ erc20('0x13f4EA83D0bd40E75C8222255bc855a974568Dd4', 'PancakeSwap Smart Router'),
55
+ erc20('0xd9C500DfF816a1Da21A48A732d3498Bf09dc9AEB', 'PancakeSwap Universal Router'),
56
+ erc20('0xDef1C0ded9bec7F1a1670819833240f027b25EfF', '0x Exchange Proxy'),
57
+ erc20('0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', 'ParaSwap Augustus v5'),
58
+ ],
59
+ 8453: [
60
+ erc20('0x2626664c2603336E57B271c5C0b26F421741e481', 'Uniswap SwapRouter02'),
61
+ erc20('0x6fF5693b99212Da76ad316178A184AB56D299b43', 'Uniswap Universal Router'),
62
+ erc20('0x678Aa4bF4E210cf2166753e054d5b7c31cc7fa86', 'PancakeSwap Smart Router'),
63
+ ],
64
+ 42161: [
65
+ erc20('0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', 'Uniswap SwapRouter02'),
66
+ erc20('0xA51afAFe0263b40EdaEf0Df8781eA9aa03E381a3', 'Uniswap Universal Router'),
67
+ erc20('0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', 'ParaSwap Augustus v5'),
68
+ ],
69
+ 10: [
70
+ erc20('0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', 'Uniswap SwapRouter02'),
71
+ erc20('0x851116D9223fabED8E56C0E6b8Ad0c31d98B3507', 'Uniswap Universal Router'),
72
+ erc20('0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', 'ParaSwap Augustus v5'),
73
+ ],
74
+ 137: [
75
+ erc20('0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', 'Uniswap SwapRouter02'),
76
+ erc20('0x1095692A6237d83C6a72F3F5eFEdb9A670C49223', 'Uniswap Universal Router'),
77
+ erc20('0xDef1C0ded9bec7F1a1670819833240f027b25EfF', '0x Exchange Proxy'),
78
+ erc20('0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', 'ParaSwap Augustus v5'),
79
+ ],
80
+ 43114: [
81
+ erc20('0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE', 'Uniswap SwapRouter02'),
82
+ erc20('0x94b75331AE8d42C1b61065089B7d48FE14aA73b7', 'Uniswap Universal Router'),
83
+ erc20('0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', 'ParaSwap Augustus v5'),
84
+ ],
85
+ }
86
+
87
+ /**
88
+ * 这条链上已知的被授权方:内置协议表的 V2 Router、V3 / V4 仓位合约,各链的 Router / 聚合器,NFT 市场。
89
+ * Tron 上只有协议表里的 SunSwap 合约(没有 Permit2)
90
+ */
91
+ export function knownSpenders(chainId: number): KnownSpender[] {
92
+ const tron = isTronChain(chainId)
93
+ const fromLp = LP_PROTOCOLS.filter((p) => p.chainId === chainId).flatMap((p) => [
94
+ ...(p.router ? [erc20(p.router, `${p.name} Router`)] : []),
95
+ // V3 / V4 仓位合约:代币授权(添加流动性)
96
+ ...(p.positionManager ? [erc20(p.positionManager, `${p.name} Positions`)] : []),
97
+ ])
98
+ const list = [...fromLp, ...(tron ? [] : EVERYWHERE), ...(BY_CHAIN[chainId] ?? [])]
99
+ const seen = new Set<string>()
100
+ return list.filter((s) => {
101
+ const k = `${s.kind}:${keyOf(chainId, s.address)}`
102
+ if (seen.has(k)) return false
103
+ seen.add(k)
104
+ return true
105
+ })
106
+ }
107
+
108
+ /** 地址的比较键:EVM 小写 0x;Tron 是 T 开头的 base58 */
109
+ const keyOf = (chainId: number, address: string) => (isTronChain(chainId) ? toTronAddress(address) : address.toLowerCase())
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // 读取
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export interface ApprovalsQuery {
116
+ /** 要检查的 ERC20 代币(如持有的代币,或 scanApprovals 发现的) */
117
+ tokens?: readonly string[]
118
+ /** 被授权方:地址或 KnownSpender,默认 knownSpenders(chainId) 里 kind 为 erc20 的 */
119
+ spenders?: ReadonlyArray<string | KnownSpender>
120
+ /** 直接给出要检查的 代币 × 被授权方(和 tokens × spenders 合并) */
121
+ pairs?: ReadonlyArray<{ token: string; spender: string }>
122
+ /** 也检查 Permit2 的额度(代币 × 被授权方,Tron 上没有),默认 true */
123
+ permit2?: boolean
124
+ /** 要检查 setApprovalForAll 的 NFT 合约(ERC721 / ERC1155) */
125
+ nfts?: readonly string[]
126
+ /** NFT 的 operator,默认 knownSpenders 里 kind 为 nft 的 */
127
+ operators?: ReadonlyArray<string | KnownSpender>
128
+ /** 直接给出要检查的 NFT 合约 × operator */
129
+ nftPairs?: ReadonlyArray<{ collection: string; operator: string }>
130
+ /** 也返回额度为 0 / 已过期 / 未授权的项,默认 false(只返回有效的授权) */
131
+ includeZero?: boolean
132
+ }
133
+
134
+ interface ApprovalBase {
135
+ /** 被授权方(spender / operator)和它的名称(不在已知列表里时为 null) */
136
+ spender: string
137
+ spenderName: string | null
138
+ }
139
+
140
+ export interface Erc20Approval extends ApprovalBase {
141
+ type: 'erc20'
142
+ token: string
143
+ symbol: string | null
144
+ decimals: number | null
145
+ /** 额度(最小单位) */
146
+ amount: string
147
+ /** 按精度换算的额度;精度读不到时为 null */
148
+ formatted: string | null
149
+ /** 额度 ≥ 2^96(无限授权,包括 MaxUint256 和截断为 uint96 的代币) */
150
+ unlimited: boolean
151
+ }
152
+
153
+ export interface Permit2Approval extends ApprovalBase {
154
+ type: 'permit2'
155
+ token: string
156
+ symbol: string | null
157
+ decimals: number | null
158
+ amount: string
159
+ formatted: string | null
160
+ unlimited: boolean
161
+ /** 过期时间(unix 秒);过期后额度不能再用 */
162
+ expiration: number
163
+ expired: boolean
164
+ nonce: number
165
+ }
166
+
167
+ export interface NftApproval extends ApprovalBase {
168
+ type: 'nft'
169
+ collection: string
170
+ /** setApprovalForAll 的状态(includeZero 时也返回 false 的项) */
171
+ approved: boolean
172
+ }
173
+
174
+ export type Approval = Erc20Approval | Permit2Approval | NftApproval
175
+
176
+ const ERC20_ABI = [
177
+ 'function allowance(address owner, address spender) view returns (uint256)',
178
+ 'function decimals() view returns (uint8)',
179
+ 'function symbol() view returns (string)',
180
+ ]
181
+ const PERMIT2_ABI = ['function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)']
182
+ const NFT_ABI = ['function isApprovedForAll(address owner, address operator) view returns (bool)']
183
+ const UNLIMITED = 2n ** 96n
184
+
185
+ /**
186
+ * 查一个地址给出的授权(全部读取合成一次 multicall)。默认检查已知的 Router / 聚合器 / NFT 市场,
187
+ * 代币要自己给(如持有的代币,或用 scanApprovals 按事件发现)。
188
+ *
189
+ * ```ts
190
+ * await approvals(provider, user, { tokens: [USDT, USDC] }) // 对已知的被授权方
191
+ * await approvals(provider, user, { pairs: [{ token: USDT, spender: someContract }] })
192
+ * await approvals(provider, user, { nfts: [BAYC] }) // NFT 市场的 setApprovalForAll
193
+ * ```
194
+ */
195
+ export async function approvals(provider: Provider, owner: string, query: ApprovalsQuery): Promise<Approval[]> {
196
+ const chainId = await provider.getChainId()
197
+ const tron = isTronChain(chainId)
198
+ const key = (a: string) => keyOf(chainId, a)
199
+ const known = knownSpenders(chainId)
200
+ const names = new Map(known.map((s) => [key(s.address), s.name]))
201
+ const addressOf = (s: string | KnownSpender) => {
202
+ if (typeof s !== 'string') names.set(key(s.address), s.name)
203
+ return typeof s === 'string' ? s : s.address
204
+ }
205
+ const spenders = (query.spenders ?? known.filter((s) => s.kind === 'erc20')).map(addressOf)
206
+ const operators = (query.operators ?? known.filter((s) => s.kind === 'nft')).map(addressOf)
207
+
208
+ const pairs = dedupe([...(query.tokens ?? []).flatMap((token) => spenders.map((spender) => ({ token, spender }))), ...(query.pairs ?? [])], (p) => `${key(p.token)}:${key(p.spender)}`)
209
+ const nftPairs = dedupe(
210
+ [...(query.nfts ?? []).flatMap((collection) => operators.map((operator) => ({ collection, operator }))), ...(query.nftPairs ?? [])],
211
+ (p) => `${key(p.collection)}:${key(p.operator)}`,
212
+ )
213
+ // Permit2 的额度:被授权方是 Permit2 自己的没有意义(ERC20 授权给 Permit2 才有意义),不查
214
+ const permitPairs = (query.permit2 ?? true) && !tron ? pairs.filter((p) => key(p.spender) !== key(PERMIT2_ADDRESS)) : []
215
+ const tokens = dedupe(
216
+ pairs.map((p) => p.token),
217
+ key,
218
+ )
219
+
220
+ const calls = [
221
+ ...pairs.map((p) => provider.contract(p.token, ERC20_ABI).allowance(owner, p.spender)),
222
+ ...permitPairs.map((p) => provider.contract(PERMIT2_ADDRESS, PERMIT2_ABI).allowance(owner, p.token, p.spender)),
223
+ ...tokens.flatMap((t) => [provider.contract(t, ERC20_ABI).decimals(), provider.contract(t, ERC20_ABI).symbol()]),
224
+ ...nftPairs.map((p) => provider.contract(p.collection, NFT_ABI).isApprovedForAll(owner, p.operator)),
225
+ ]
226
+ const results = calls.length ? await provider.tryAll(calls) : []
227
+ let i = 0
228
+ const allowances = results.slice(i, (i += pairs.length)) as Array<bigint | null>
229
+ const permits = new Map(permitPairs.map((p, j) => [p, results[i + j] as { 0: bigint; 1: bigint; 2: bigint } | null]))
230
+ i += permitPairs.length
231
+ const meta = new Map<string, { decimals: number | null; symbol: string | null }>()
232
+ for (const t of tokens) {
233
+ const [decimals, symbol] = results.slice(i, (i += 2)) as [bigint | null, string | null]
234
+ meta.set(key(t), { decimals: decimals === null ? null : Number(decimals), symbol: typeof symbol === 'string' ? symbol : null })
235
+ }
236
+ const nftStates = results.slice(i) as Array<boolean | null>
237
+
238
+ const now = Math.floor(Date.now() / 1000)
239
+ const out: Approval[] = []
240
+ pairs.forEach((p, j) => {
241
+ const m = meta.get(key(p.token)) ?? { decimals: null, symbol: null }
242
+ const base = { spender: p.spender, spenderName: names.get(key(p.spender)) ?? null, token: p.token, symbol: m.symbol, decimals: m.decimals }
243
+ const amount = allowances[j]
244
+ if (amount !== null && amount !== undefined && (amount > 0n || query.includeZero)) {
245
+ out.push({ type: 'erc20', ...base, amount: amount.toString(), formatted: m.decimals === null ? null : formatUnits(amount, m.decimals), unlimited: amount >= UNLIMITED })
246
+ }
247
+ const permit = permits.get(p)
248
+ if (permit) {
249
+ const [value, expiration, nonce] = [permit[0], Number(permit[1]), Number(permit[2])]
250
+ // Permit2 在 block.timestamp > expiration 时才拒绝
251
+ const expired = expiration < now
252
+ if ((value > 0n && !expired) || query.includeZero) {
253
+ out.push({
254
+ type: 'permit2',
255
+ ...base,
256
+ amount: value.toString(),
257
+ formatted: m.decimals === null ? null : formatUnits(value, m.decimals),
258
+ unlimited: value >= UNLIMITED,
259
+ expiration,
260
+ expired,
261
+ nonce,
262
+ })
263
+ }
264
+ }
265
+ })
266
+ nftPairs.forEach((p, j) => {
267
+ const approved = nftStates[j]
268
+ if (approved === true || (approved === false && query.includeZero)) {
269
+ out.push({ type: 'nft', collection: p.collection, spender: p.operator, spenderName: names.get(key(p.operator)) ?? null, approved })
270
+ }
271
+ })
272
+ return out
273
+ }
274
+
275
+ function dedupe<T>(list: readonly T[], keyFn: (x: T) => string): T[] {
276
+ const seen = new Map<string, T>()
277
+ for (const x of list) if (!seen.has(keyFn(x))) seen.set(keyFn(x), x)
278
+ return [...seen.values()]
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // 按事件发现
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /** Approval(address indexed owner, address indexed spender, uint256):ERC20(3 个 topic)和 ERC721 单个 NFT(4 个 topic)相同 */
286
+ const APPROVAL_TOPIC = '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'
287
+ /** ApprovalForAll(address indexed owner, address indexed operator, bool) */
288
+ const APPROVAL_FOR_ALL_TOPIC = '0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31'
289
+ /** Permit2 的 Approval(address indexed owner, address indexed token, address indexed spender, uint160, uint48):直接调用 approve */
290
+ const PERMIT2_APPROVAL_TOPIC = '0xda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b'
291
+ /** Permit2 的 Permit(address indexed owner, address indexed token, address indexed spender, uint160, uint48, uint48):签名授权(Universal Router 等大多用它) */
292
+ const PERMIT2_PERMIT_TOPIC = '0xc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec'
293
+
294
+ export interface ApprovalScanOptions {
295
+ /** 从哪个区块开始扫(不传时从 当前区块 − lookbackBlocks 开始);有保存的进度时从进度继续 */
296
+ fromBlock?: number
297
+ /** 不传 fromBlock 时往回扫多少个区块,默认 0(只发现开始扫描之后的授权) */
298
+ lookbackBlocks?: number
299
+ /** 上次的进度(scanApprovals 返回的 progress),传了就接着扫 */
300
+ progress?: ApprovalScanProgress
301
+ /** 单次 getLogs 的最大区块范围,默认 1000;节点有上限时自动缩小 */
302
+ blockRange?: number
303
+ /** 这次最多发多少个 getLogs(三种事件合计),默认 30 */
304
+ maxRequests?: number
305
+ /** 这次最多花多少毫秒,默认 10000 */
306
+ timeBudget?: number
307
+ signal?: AbortSignal
308
+ }
309
+
310
+ /** 扫描进度:可以 JSON 序列化后保存,下次传回 progress 接着扫 */
311
+ export interface ApprovalScanProgress {
312
+ erc20: LogScanProgress | null
313
+ nft: LogScanProgress | null
314
+ permit2: LogScanProgress | null
315
+ }
316
+
317
+ export interface ApprovalScanResult {
318
+ /** 发现的 代币 × 被授权方(ERC20 的 Approval 事件和 Permit2 的 Approval 事件) */
319
+ pairs: Array<{ token: string; spender: string }>
320
+ /** 发现的 NFT 合约 × operator(ApprovalForAll 事件) */
321
+ nftPairs: Array<{ collection: string; operator: string }>
322
+ /** 已扫到的区块(三种事件里最慢的那个) */
323
+ cursor: number
324
+ progress: ApprovalScanProgress
325
+ }
326
+
327
+ /**
328
+ * 按事件发现地址授权过的代币和 NFT 合约(只发现,不判断是否还有效:把结果的 pairs / nftPairs 交给 approvals 查当前状态)。
329
+ * 需要节点支持 eth_getLogs;免费公共节点的范围限制很严(如 BSC 每次 25 个区块),从很早的区块扫全部历史不现实,
330
+ * 适合从某个时间点开始增量扫描(保存 progress,下次接着扫)。Tron 不支持
331
+ *
332
+ * ```ts
333
+ * let { pairs, nftPairs, progress } = await scanApprovals(provider, user, { lookbackBlocks: 50_000 })
334
+ * const list = await approvals(provider, user, { pairs, nftPairs, spenders: [] })
335
+ * // 之后:await scanApprovals(provider, user, { progress })
336
+ * ```
337
+ */
338
+ export async function scanApprovals(provider: Provider, owner: string, options: ApprovalScanOptions = {}): Promise<ApprovalScanResult> {
339
+ const chainId = await provider.getChainId()
340
+ if (isTronChain(chainId)) throw new Error('scanApprovals is not supported on Tron (no eth_getLogs)')
341
+ const ownerTopic = `0x${toEvmAddress(owner).slice(2).toLowerCase().padStart(64, '0')}`
342
+ const addr = (topic: string | undefined) => (topic ? `0x${topic.slice(26)}` : null)
343
+ const lookback = Math.max(0, Math.floor(options.lookbackBlocks ?? 0))
344
+ const maxRequests = Math.max(3, Math.floor(options.maxRequests ?? 30))
345
+ const timeBudget = options.timeBudget ?? 10_000
346
+ const base = {
347
+ provider,
348
+ initialCursor: (target: number) => (options.fromBlock !== undefined ? options.fromBlock - 1 : target - lookback),
349
+ maxRange: Math.max(1, Math.floor(options.blockRange ?? 1000)),
350
+ maxRequests: Math.ceil(maxRequests / 3),
351
+ timeBudget,
352
+ confirmations: 0,
353
+ ...(options.signal ? { signal: options.signal } : {}),
354
+ }
355
+ const [erc20Progress, nftProgress, permit2Progress] = await Promise.all([
356
+ runLogScan({
357
+ ...base,
358
+ saved: options.progress?.erc20 ?? undefined,
359
+ filter: { topics: [APPROVAL_TOPIC, ownerTopic] },
360
+ // ERC721 的单个 NFT 授权也是这个事件(4 个 topic),不是代币额度,跳过
361
+ collect: (log) => (log.topics.length === 3 ? `${log.address.toLowerCase()}:${addr(log.topics[2])}` : null),
362
+ }),
363
+ runLogScan({
364
+ ...base,
365
+ saved: options.progress?.nft ?? undefined,
366
+ filter: { topics: [APPROVAL_FOR_ALL_TOPIC, ownerTopic] },
367
+ collect: (log) => `${log.address.toLowerCase()}:${addr(log.topics[2])}`,
368
+ }),
369
+ runLogScan({
370
+ ...base,
371
+ saved: options.progress?.permit2 ?? undefined,
372
+ // 两种事件的 topic0 不同:按 owner 过滤 Permit2 的全部事件,只收这两种(Lockdown 等其他事件的 owner 也在 topic1)
373
+ filter: { address: PERMIT2_ADDRESS, topics: [null, ownerTopic] },
374
+ collect: (log) => (log.topics[0] === PERMIT2_APPROVAL_TOPIC || log.topics[0] === PERMIT2_PERMIT_TOPIC ? `${addr(log.topics[2])}:${addr(log.topics[3])}` : null),
375
+ }),
376
+ ])
377
+ const split = (item: string) => item.split(':') as [string, string]
378
+ const pairs = dedupe(
379
+ [...(erc20Progress?.items ?? []), ...(permit2Progress?.items ?? [])].map((item) => {
380
+ const [token, spender] = split(item)
381
+ return { token, spender }
382
+ }),
383
+ (p) => `${p.token}:${p.spender}`,
384
+ )
385
+ const nftPairs = (nftProgress?.items ?? []).map((item) => {
386
+ const [collection, operator] = split(item)
387
+ return { collection, operator }
388
+ })
389
+ const cursors = [erc20Progress, nftProgress, permit2Progress].map((p) => p?.cursor ?? -1)
390
+ return { pairs, nftPairs, cursor: Math.min(...cursors), progress: { erc20: erc20Progress, nft: nftProgress, permit2: permit2Progress } }
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // 撤销
395
+ // ---------------------------------------------------------------------------
396
+
397
+ export interface RevokeTx {
398
+ /** approve:ERC20 额度改为 0;lockdown:Permit2 的额度一次清零(一笔交易包含多个);setApprovalForAll:NFT 取消授权 */
399
+ kind: 'approve' | 'lockdown' | 'setApprovalForAll'
400
+ to: string
401
+ data: string
402
+ value: '0'
403
+ /** 这笔交易撤销的授权 */
404
+ approvals: Approval[]
405
+ }
406
+
407
+ const erc20Interface = new Interface(['function approve(address spender, uint256 amount) returns (bool)'])
408
+ const permit2Interface = new Interface(['function lockdown((address token, address spender)[] approvals)'])
409
+ const nftInterface = new Interface(['function setApprovalForAll(address operator, bool approved)'])
410
+
411
+ /**
412
+ * 撤销授权要发的交易(不签名):ERC20 每个一笔 approve(spender, 0);Permit2 的全部合成一笔 lockdown;NFT 每个一笔 setApprovalForAll(false)。
413
+ * Tron 上 to 是 T 地址
414
+ */
415
+ export function buildRevokes(list: readonly Approval[], chainId?: number): RevokeTx[] {
416
+ const tron = chainId !== undefined && isTronChain(chainId)
417
+ const to = (a: string) => (tron ? toTronAddress(a) : a)
418
+ const hex = (a: string) => toEvmAddress(a)
419
+ const txs: RevokeTx[] = []
420
+ const permits = list.filter((a): a is Permit2Approval => a.type === 'permit2')
421
+ for (const a of list) {
422
+ if (a.type === 'erc20') txs.push({ kind: 'approve', to: to(a.token), data: erc20Interface.encodeFunctionData('approve', [hex(a.spender), 0n]), value: '0', approvals: [a] })
423
+ if (a.type === 'nft') txs.push({ kind: 'setApprovalForAll', to: to(a.collection), data: nftInterface.encodeFunctionData('setApprovalForAll', [hex(a.spender), false]), value: '0', approvals: [a] })
424
+ }
425
+ if (permits.length) {
426
+ txs.push({
427
+ kind: 'lockdown',
428
+ to: PERMIT2_ADDRESS,
429
+ data: permit2Interface.encodeFunctionData('lockdown', [permits.map((p) => ({ token: hex(p.token), spender: hex(p.spender) }))]),
430
+ value: '0',
431
+ approvals: permits,
432
+ })
433
+ }
434
+ return txs
435
+ }
436
+
437
+ export interface RevokeResult extends WriteResult {
438
+ kind: RevokeTx['kind']
439
+ approvals: Approval[]
440
+ }
441
+
442
+ /**
443
+ * 撤销授权:构建(同 buildRevokes)后用 from 签名发送,钱包和选项同 batchCall(mode: '7702' 时同一钱包的撤销合成一笔交易)。
444
+ * 默认一笔失败不影响后面的(continueOnError: true)
445
+ *
446
+ * ```ts
447
+ * const list = await approvals(provider, user, { tokens })
448
+ * await revokeApprovals(provider, key, list.filter((a) => a.unlimited))
449
+ * ```
450
+ */
451
+ export async function revokeApprovals(provider: Provider, from: SignerInput, list: readonly Approval[], options: BatchCallOptions = {}): Promise<RevokeResult[]> {
452
+ const txs = buildRevokes(list, await provider.getChainId())
453
+ if (!txs.length) return []
454
+ // 发送的代码按需加载:只查授权时打包不会带上
455
+ const { batchCall } = await import('./write/batch.js')
456
+ const results = await batchCall(
457
+ provider,
458
+ txs.map((tx) => ({ from, to: tx.to, data: tx.data })),
459
+ { continueOnError: true, ...options },
460
+ )
461
+ return results.map((r, i) => ({ ...r, kind: (txs[i] as RevokeTx).kind, approvals: (txs[i] as RevokeTx).approvals }))
462
+ }
package/src/chains.ts CHANGED
@@ -24,7 +24,10 @@ export const NATIVE_CURRENCIES: Readonly<Record<number, NativeCurrency>> = {
24
24
  324: { name: 'Ether', symbol: 'ETH', decimals: 18 }, // zkSync Mainnet
25
25
  988: { name: 'USDT0', symbol: 'USDT0', decimals: 18 }, // Stable Mainnet
26
26
  999: { name: 'HYPE', symbol: 'HYPE', decimals: 18 }, // HyperEVM
27
+ 1030: { name: 'CFX', symbol: 'CFX', decimals: 18 }, // Conflux eSpace
27
28
  1116: { name: 'Core Blockchain Native Token', symbol: 'CORE', decimals: 18 }, // Core Blockchain Mainnet
29
+ 1672: { name: 'PharosCoin', symbol: 'PROS', decimals: 18 }, // Pharos Mainnet
30
+ 1776: { name: 'Injective', symbol: 'INJ', decimals: 18 }, // Injective
28
31
  2818: { name: 'Ether', symbol: 'ETH', decimals: 18 }, // Morph
29
32
  4663: { name: 'Ether', symbol: 'ETH', decimals: 18 }, // Robinhood Chain
30
33
  5000: { name: 'Mantle', symbol: 'MNT', decimals: 18 }, // Mantle
@@ -38,6 +41,7 @@ export const NATIVE_CURRENCIES: Readonly<Record<number, NativeCurrency>> = {
38
41
  80094: { name: 'BERA Token', symbol: 'BERA', decimals: 18 }, // Berachain
39
42
  81457: { name: 'Ether', symbol: 'ETH', decimals: 18 }, // Blast
40
43
  534352: { name: 'Ether', symbol: 'ETH', decimals: 18 }, // Scroll
44
+ 201022: { name: 'FON', symbol: 'FON', decimals: 18 }, // FSC(ave_web 链配置的主币)
41
45
  6666665: { name: 'SAFE(AnWang)', symbol: 'SAFE', decimals: 18 }, // Safe(AnWang) Mainnet
42
46
  20260131: { name: 'MetaAssets', symbol: 'MA', decimals: 18 }, // Meta Assets Chain
43
47
  728126428: { name: 'TRON', symbol: 'TRX', decimals: 6 }, // Tron