@w3lib/evm-batch-call 0.6.0 → 0.7.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 (110) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +161 -2
  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/fees.d.ts +97 -0
  15. package/dist/fees.d.ts.map +1 -0
  16. package/dist/fees.js +115 -0
  17. package/dist/fees.js.map +1 -0
  18. package/dist/http.d.ts +10 -0
  19. package/dist/http.d.ts.map +1 -0
  20. package/dist/http.js +30 -0
  21. package/dist/http.js.map +1 -0
  22. package/dist/lp.d.ts +2 -1
  23. package/dist/lp.d.ts.map +1 -1
  24. package/dist/lp.js +2 -1
  25. package/dist/lp.js.map +1 -1
  26. package/dist/multicall.d.ts.map +1 -1
  27. package/dist/multicall.js +3 -0
  28. package/dist/multicall.js.map +1 -1
  29. package/dist/names.d.ts +55 -0
  30. package/dist/names.d.ts.map +1 -0
  31. package/dist/names.js +93 -0
  32. package/dist/names.js.map +1 -0
  33. package/dist/owner.d.ts +4 -26
  34. package/dist/owner.d.ts.map +1 -1
  35. package/dist/owner.js +7 -112
  36. package/dist/owner.js.map +1 -1
  37. package/dist/portfolio.d.ts +54 -0
  38. package/dist/portfolio.d.ts.map +1 -0
  39. package/dist/portfolio.js +49 -0
  40. package/dist/portfolio.js.map +1 -0
  41. package/dist/price.d.ts +95 -0
  42. package/dist/price.d.ts.map +1 -0
  43. package/dist/price.js +329 -0
  44. package/dist/price.js.map +1 -0
  45. package/dist/risk.d.ts +42 -0
  46. package/dist/risk.d.ts.map +1 -0
  47. package/dist/risk.js +134 -0
  48. package/dist/risk.js.map +1 -0
  49. package/dist/rpcNodes.d.ts.map +1 -1
  50. package/dist/rpcNodes.js +18 -0
  51. package/dist/rpcNodes.js.map +1 -1
  52. package/dist/subpaths/approvals.d.ts +26 -0
  53. package/dist/subpaths/approvals.d.ts.map +1 -0
  54. package/dist/subpaths/approvals.js +27 -0
  55. package/dist/subpaths/approvals.js.map +1 -0
  56. package/dist/subpaths/fees.d.ts +23 -0
  57. package/dist/subpaths/fees.d.ts.map +1 -0
  58. package/dist/subpaths/fees.js +25 -0
  59. package/dist/subpaths/fees.js.map +1 -0
  60. package/dist/subpaths/names.d.ts +35 -0
  61. package/dist/subpaths/names.d.ts.map +1 -0
  62. package/dist/subpaths/names.js +39 -0
  63. package/dist/subpaths/names.js.map +1 -0
  64. package/dist/subpaths/price.d.ts +38 -0
  65. package/dist/subpaths/price.d.ts.map +1 -0
  66. package/dist/subpaths/price.js +40 -0
  67. package/dist/subpaths/price.js.map +1 -0
  68. package/dist/subpaths/risk.d.ts +19 -0
  69. package/dist/subpaths/risk.d.ts.map +1 -0
  70. package/dist/subpaths/risk.js +20 -0
  71. package/dist/subpaths/risk.js.map +1 -0
  72. package/dist/subpaths/swap.d.ts +19 -0
  73. package/dist/subpaths/swap.d.ts.map +1 -0
  74. package/dist/subpaths/swap.js +20 -0
  75. package/dist/subpaths/swap.js.map +1 -0
  76. package/dist/swap.d.ts +128 -0
  77. package/dist/swap.d.ts.map +1 -0
  78. package/dist/swap.js +291 -0
  79. package/dist/swap.js.map +1 -0
  80. package/dist/tokenProbe.d.ts +3 -0
  81. package/dist/tokenProbe.d.ts.map +1 -0
  82. package/dist/tokenProbe.js +4 -0
  83. package/dist/tokenProbe.js.map +1 -0
  84. package/dist/write/tron.d.ts +25 -0
  85. package/dist/write/tron.d.ts.map +1 -1
  86. package/dist/write/tron.js +26 -0
  87. package/dist/write/tron.js.map +1 -1
  88. package/package.json +39 -16
  89. package/src/aggregate.ts +110 -3
  90. package/src/approvals.ts +462 -0
  91. package/src/chains.ts +4 -0
  92. package/src/fees.ts +201 -0
  93. package/src/http.ts +34 -0
  94. package/src/lp.ts +3 -1
  95. package/src/multicall.ts +3 -0
  96. package/src/names.ts +141 -0
  97. package/src/owner.ts +7 -138
  98. package/src/portfolio.ts +91 -0
  99. package/src/price.ts +415 -0
  100. package/src/risk.ts +166 -0
  101. package/src/rpcNodes.ts +18 -0
  102. package/src/subpaths/approvals.ts +48 -0
  103. package/src/subpaths/fees.ts +43 -0
  104. package/src/subpaths/names.ts +43 -0
  105. package/src/subpaths/price.ts +60 -0
  106. package/src/subpaths/risk.ts +23 -0
  107. package/src/subpaths/swap.ts +36 -0
  108. package/src/swap.ts +413 -0
  109. package/src/tokenProbe.ts +5 -0
  110. package/src/write/tron.ts +43 -0
@@ -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