@w3lib/evm-batch-call 0.7.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 (159) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +206 -1
  3. package/dist/dividends.d.ts +135 -0
  4. package/dist/dividends.d.ts.map +1 -0
  5. package/dist/dividends.js +253 -0
  6. package/dist/dividends.js.map +1 -0
  7. package/dist/history.d.ts +154 -0
  8. package/dist/history.d.ts.map +1 -0
  9. package/dist/history.js +247 -0
  10. package/dist/history.js.map +1 -0
  11. package/dist/historySources.d.ts +32 -0
  12. package/dist/historySources.d.ts.map +1 -0
  13. package/dist/historySources.js +319 -0
  14. package/dist/historySources.js.map +1 -0
  15. package/dist/liquidity/v3.d.ts +2 -0
  16. package/dist/liquidity/v3.d.ts.map +1 -1
  17. package/dist/liquidity/v3.js +1 -1
  18. package/dist/liquidity/v3.js.map +1 -1
  19. package/dist/liquidity/v4.d.ts +21 -0
  20. package/dist/liquidity/v4.d.ts.map +1 -1
  21. package/dist/liquidity/v4.js +4 -3
  22. package/dist/liquidity/v4.js.map +1 -1
  23. package/dist/lp.d.ts +8 -0
  24. package/dist/lp.d.ts.map +1 -1
  25. package/dist/lp.js +17 -1
  26. package/dist/lp.js.map +1 -1
  27. package/dist/lpV4.d.ts +4 -0
  28. package/dist/lpV4.d.ts.map +1 -1
  29. package/dist/lpV4.js +27 -3
  30. package/dist/lpV4.js.map +1 -1
  31. package/dist/permit.d.ts +137 -0
  32. package/dist/permit.d.ts.map +1 -0
  33. package/dist/permit.js +355 -0
  34. package/dist/permit.js.map +1 -0
  35. package/dist/portfolio.d.ts +2 -0
  36. package/dist/portfolio.d.ts.map +1 -1
  37. package/dist/portfolio.js +9 -4
  38. package/dist/portfolio.js.map +1 -1
  39. package/dist/rewards/farms.d.ts +26 -0
  40. package/dist/rewards/farms.d.ts.map +1 -0
  41. package/dist/rewards/farms.js +210 -0
  42. package/dist/rewards/farms.js.map +1 -0
  43. package/dist/rewards/index.d.ts +42 -0
  44. package/dist/rewards/index.d.ts.map +1 -0
  45. package/dist/rewards/index.js +382 -0
  46. package/dist/rewards/index.js.map +1 -0
  47. package/dist/rewards/types.d.ts +170 -0
  48. package/dist/rewards/types.d.ts.map +1 -0
  49. package/dist/rewards/types.js +2 -0
  50. package/dist/rewards/types.js.map +1 -0
  51. package/dist/simulate.d.ts +186 -0
  52. package/dist/simulate.d.ts.map +1 -0
  53. package/dist/simulate.js +559 -0
  54. package/dist/simulate.js.map +1 -0
  55. package/dist/simulateDecode.d.ts +33 -0
  56. package/dist/simulateDecode.d.ts.map +1 -0
  57. package/dist/simulateDecode.js +321 -0
  58. package/dist/simulateDecode.js.map +1 -0
  59. package/dist/smartAccount/account.d.ts +31 -0
  60. package/dist/smartAccount/account.d.ts.map +1 -0
  61. package/dist/smartAccount/account.js +33 -0
  62. package/dist/smartAccount/account.js.map +1 -0
  63. package/dist/smartAccount/clients.d.ts +77 -0
  64. package/dist/smartAccount/clients.d.ts.map +1 -0
  65. package/dist/smartAccount/clients.js +122 -0
  66. package/dist/smartAccount/clients.js.map +1 -0
  67. package/dist/smartAccount/eip5792.d.ts +77 -0
  68. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  69. package/dist/smartAccount/eip5792.js +152 -0
  70. package/dist/smartAccount/eip5792.js.map +1 -0
  71. package/dist/smartAccount/send.d.ts +40 -0
  72. package/dist/smartAccount/send.d.ts.map +1 -0
  73. package/dist/smartAccount/send.js +271 -0
  74. package/dist/smartAccount/send.js.map +1 -0
  75. package/dist/smartAccount/userOp.d.ts +91 -0
  76. package/dist/smartAccount/userOp.d.ts.map +1 -0
  77. package/dist/smartAccount/userOp.js +124 -0
  78. package/dist/smartAccount/userOp.js.map +1 -0
  79. package/dist/subpaths/dividends.d.ts +28 -0
  80. package/dist/subpaths/dividends.d.ts.map +1 -0
  81. package/dist/subpaths/dividends.js +29 -0
  82. package/dist/subpaths/dividends.js.map +1 -0
  83. package/dist/subpaths/history.d.ts +22 -0
  84. package/dist/subpaths/history.d.ts.map +1 -0
  85. package/dist/subpaths/history.js +23 -0
  86. package/dist/subpaths/history.js.map +1 -0
  87. package/dist/subpaths/permit.d.ts +14 -0
  88. package/dist/subpaths/permit.d.ts.map +1 -0
  89. package/dist/subpaths/permit.js +14 -0
  90. package/dist/subpaths/permit.js.map +1 -0
  91. package/dist/subpaths/price.js +2 -2
  92. package/dist/subpaths/price.js.map +1 -1
  93. package/dist/subpaths/rewards.d.ts +27 -0
  94. package/dist/subpaths/rewards.d.ts.map +1 -0
  95. package/dist/subpaths/rewards.js +25 -0
  96. package/dist/subpaths/rewards.js.map +1 -0
  97. package/dist/subpaths/simulate.d.ts +25 -0
  98. package/dist/subpaths/simulate.d.ts.map +1 -0
  99. package/dist/subpaths/simulate.js +28 -0
  100. package/dist/subpaths/simulate.js.map +1 -0
  101. package/dist/subpaths/smart-account.d.ts +24 -0
  102. package/dist/subpaths/smart-account.d.ts.map +1 -0
  103. package/dist/subpaths/smart-account.js +24 -0
  104. package/dist/subpaths/smart-account.js.map +1 -0
  105. package/dist/swap.d.ts +45 -5
  106. package/dist/swap.d.ts.map +1 -1
  107. package/dist/swap.js +108 -12
  108. package/dist/swap.js.map +1 -1
  109. package/dist/write/batch.d.ts +34 -2
  110. package/dist/write/batch.d.ts.map +1 -1
  111. package/dist/write/batch.js +22 -3
  112. package/dist/write/batch.js.map +1 -1
  113. package/dist/write/collect.d.ts +9 -0
  114. package/dist/write/collect.d.ts.map +1 -1
  115. package/dist/write/collect.js +41 -1
  116. package/dist/write/collect.js.map +1 -1
  117. package/dist/write/send.d.ts +4 -0
  118. package/dist/write/send.d.ts.map +1 -1
  119. package/dist/write/send.js.map +1 -1
  120. package/dist/write/signer.d.ts +5 -1
  121. package/dist/write/signer.d.ts.map +1 -1
  122. package/dist/write/signer.js.map +1 -1
  123. package/dist/write/transfer.d.ts +5 -3
  124. package/dist/write/transfer.d.ts.map +1 -1
  125. package/dist/write/transfer.js +12 -6
  126. package/dist/write/transfer.js.map +1 -1
  127. package/package.json +35 -1
  128. package/src/dividends.ts +360 -0
  129. package/src/history.ts +367 -0
  130. package/src/historySources.ts +360 -0
  131. package/src/liquidity/v3.ts +1 -1
  132. package/src/liquidity/v4.ts +4 -3
  133. package/src/lp.ts +18 -1
  134. package/src/lpV4.ts +32 -2
  135. package/src/permit.ts +457 -0
  136. package/src/portfolio.ts +11 -4
  137. package/src/rewards/farms.ts +229 -0
  138. package/src/rewards/index.ts +431 -0
  139. package/src/rewards/types.ts +179 -0
  140. package/src/simulate.ts +727 -0
  141. package/src/simulateDecode.ts +352 -0
  142. package/src/smartAccount/account.ts +63 -0
  143. package/src/smartAccount/clients.ts +180 -0
  144. package/src/smartAccount/eip5792.ts +187 -0
  145. package/src/smartAccount/send.ts +341 -0
  146. package/src/smartAccount/userOp.ts +180 -0
  147. package/src/subpaths/dividends.ts +47 -0
  148. package/src/subpaths/history.ts +41 -0
  149. package/src/subpaths/permit.ts +39 -0
  150. package/src/subpaths/price.ts +2 -2
  151. package/src/subpaths/rewards.ts +46 -0
  152. package/src/subpaths/simulate.ts +51 -0
  153. package/src/subpaths/smart-account.ts +40 -0
  154. package/src/swap.ts +139 -18
  155. package/src/write/batch.ts +44 -5
  156. package/src/write/collect.ts +60 -1
  157. package/src/write/send.ts +4 -0
  158. package/src/write/signer.ts +5 -1
  159. package/src/write/transfer.ts +17 -10
@@ -0,0 +1,352 @@
1
+ /**
2
+ * 模拟用的解码:calldata(ERC20 / 721 / 1155、WETH、Uniswap V2 / V3 Router、Universal Router、V3 仓位合约、Permit2、Multicall3、Disperse,
3
+ * 其中 Router / 仓位合约的地址按内置协议表标出协议名)和 revert 数据(Error / Panic、常见的自定义错误)。
4
+ * 不查网络(不用 4byte):认不出的选择器返回 unknown 和原始选择器。
5
+ */
6
+ import { Interface, dataSlice, type FunctionFragment, type Result } from 'ethers'
7
+
8
+ import { knownSpenders } from './approvals.js'
9
+ import { decodeRevertReason } from './errors.js'
10
+
11
+ export type DecodedType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'contract' | 'unknown'
12
+
13
+ export interface DecodedCall {
14
+ type: DecodedType
15
+ /** 协议 / 合约名:Router 地址在内置协议表里时用表里的名字,否则是 ABI 所属的类别(如 "Uniswap V2 Router") */
16
+ protocol?: string
17
+ /** 方法名(unknown 时没有) */
18
+ method?: string
19
+ /** 一句话描述(simulate 会在有余额变化时补上数量) */
20
+ summary: string
21
+ /** 参数(地址 / 数字都是字符串,可以直接 JSON 序列化);unknown 时是 { selector } */
22
+ args?: Record<string, unknown>
23
+ /** multicall / Universal Router 等里面包含的子调用 */
24
+ calls?: DecodedCall[]
25
+ }
26
+
27
+ interface Group {
28
+ name: string
29
+ abi: string[]
30
+ }
31
+
32
+ const TOKEN_ID = 'tuple(address token, uint160 amount, uint48 expiration, uint48 nonce)'
33
+
34
+ const GROUPS: Group[] = [
35
+ {
36
+ name: 'Token',
37
+ abi: [
38
+ 'function transfer(address to, uint256 amount)',
39
+ 'function transferFrom(address from, address to, uint256 amount)',
40
+ 'function approve(address spender, uint256 amount)',
41
+ 'function increaseAllowance(address spender, uint256 amount)',
42
+ 'function decreaseAllowance(address spender, uint256 amount)',
43
+ 'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',
44
+ 'function setApprovalForAll(address operator, bool approved)',
45
+ 'function safeTransferFrom(address from, address to, uint256 tokenId)',
46
+ 'function safeTransferFrom(address from, address to, uint256 tokenId, bytes data)',
47
+ 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)',
48
+ 'function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data)',
49
+ 'function deposit() payable',
50
+ 'function withdraw(uint256 amount)',
51
+ ],
52
+ },
53
+ {
54
+ name: 'Uniswap V2 Router',
55
+ abi: [
56
+ 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
57
+ 'function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] path, address to, uint256 deadline)',
58
+ 'function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable',
59
+ 'function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] path, address to, uint256 deadline)',
60
+ 'function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
61
+ 'function swapETHForExactTokens(uint256 amountOut, address[] path, address to, uint256 deadline) payable',
62
+ 'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
63
+ 'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable',
64
+ 'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
65
+ 'function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)',
66
+ 'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) payable',
67
+ 'function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)',
68
+ 'function removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline)',
69
+ 'function removeLiquidityETHSupportingFeeOnTransferTokens(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline)',
70
+ ],
71
+ },
72
+ {
73
+ name: 'Uniswap V3 Router',
74
+ abi: [
75
+ // SwapRouter(struct 里有 deadline)和 SwapRouter02(没有)
76
+ 'function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable',
77
+ 'function exactInput(tuple(bytes path, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum) params) payable',
78
+ 'function exactOutputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) payable',
79
+ 'function exactOutput(tuple(bytes path, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum) params) payable',
80
+ 'function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable',
81
+ 'function exactInput(tuple(bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum) params) payable',
82
+ 'function exactOutputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) payable',
83
+ 'function exactOutput(tuple(bytes path, address recipient, uint256 amountOut, uint256 amountInMaximum) params) payable',
84
+ 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to) payable',
85
+ 'function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] path, address to) payable',
86
+ 'function multicall(uint256 deadline, bytes[] data) payable',
87
+ 'function multicall(bytes32 previousBlockhash, bytes[] data) payable',
88
+ 'function multicall(bytes[] data) payable',
89
+ 'function unwrapWETH9(uint256 amountMinimum, address recipient) payable',
90
+ 'function unwrapWETH9(uint256 amountMinimum) payable',
91
+ 'function wrapETH(uint256 value) payable',
92
+ 'function refundETH() payable',
93
+ 'function sweepToken(address token, uint256 amountMinimum, address recipient) payable',
94
+ 'function sweepToken(address token, uint256 amountMinimum) payable',
95
+ ],
96
+ },
97
+ {
98
+ name: 'Uniswap V3 Positions',
99
+ abi: [
100
+ 'function mint(tuple(address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, address recipient, uint256 deadline) params) payable',
101
+ 'function increaseLiquidity(tuple(uint256 tokenId, uint256 amount0Desired, uint256 amount1Desired, uint256 amount0Min, uint256 amount1Min, uint256 deadline) params) payable',
102
+ 'function decreaseLiquidity(tuple(uint256 tokenId, uint128 liquidity, uint256 amount0Min, uint256 amount1Min, uint256 deadline) params) payable',
103
+ 'function collect(tuple(uint256 tokenId, address recipient, uint128 amount0Max, uint128 amount1Max) params) payable',
104
+ 'function burn(uint256 tokenId) payable',
105
+ 'function createAndInitializePoolIfNecessary(address token0, address token1, uint24 fee, uint160 sqrtPriceX96) payable',
106
+ 'function modifyLiquidities(bytes unlockData, uint256 deadline) payable',
107
+ ],
108
+ },
109
+ {
110
+ name: 'Universal Router',
111
+ abi: ['function execute(bytes commands, bytes[] inputs, uint256 deadline) payable', 'function execute(bytes commands, bytes[] inputs) payable'],
112
+ },
113
+ {
114
+ name: 'Permit2',
115
+ abi: [
116
+ 'function approve(address token, address spender, uint160 amount, uint48 expiration)',
117
+ `function permit(address owner, tuple(${TOKEN_ID} details, address spender, uint256 sigDeadline) permitSingle, bytes signature)`,
118
+ `function permit(address owner, tuple(${TOKEN_ID}[] details, address spender, uint256 sigDeadline) permitBatch, bytes signature)`,
119
+ 'function transferFrom(address from, address to, uint160 amount, address token)',
120
+ 'function lockdown(tuple(address token, address spender)[] approvals)',
121
+ 'function invalidateNonces(address token, address spender, uint48 newNonce)',
122
+ ],
123
+ },
124
+ {
125
+ name: 'Multicall3',
126
+ abi: [
127
+ 'function aggregate(tuple(address target, bytes callData)[] calls) payable',
128
+ 'function tryAggregate(bool requireSuccess, tuple(address target, bytes callData)[] calls) payable',
129
+ 'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable',
130
+ 'function aggregate3Value(tuple(address target, bool allowFailure, uint256 value, bytes callData)[] calls) payable',
131
+ ],
132
+ },
133
+ {
134
+ name: 'Disperse',
135
+ abi: ['function disperseEther(address[] recipients, uint256[] values) payable', 'function disperseToken(address token, address[] recipients, uint256[] values)'],
136
+ },
137
+ { name: 'Smart account', abi: ['function executeBatch(tuple(address target, uint256 value, bytes data)[] calls)'] },
138
+ ]
139
+
140
+ interface Entry {
141
+ fragment: FunctionFragment
142
+ iface: Interface
143
+ group: string
144
+ }
145
+
146
+ let table: Map<string, Entry> | null = null
147
+
148
+ function entries(): Map<string, Entry> {
149
+ if (!table) {
150
+ table = new Map()
151
+ for (const group of GROUPS) {
152
+ const iface = new Interface(group.abi)
153
+ iface.forEachFunction((fragment) => {
154
+ // 同一个选择器的第一个为准(Token 组里的 approve / transferFrom 同时是 ERC20 和 ERC721 的)
155
+ if (!table?.has(fragment.selector)) table?.set(fragment.selector, { fragment, iface, group: group.name })
156
+ })
157
+ }
158
+ }
159
+ return table
160
+ }
161
+
162
+ const typeOf = (method: string): DecodedType => {
163
+ if (/^swap|^exact(Input|Output)/.test(method)) return 'swap'
164
+ if (/Liquidity|^collect$|^burn$|^mint$|^modifyLiquidities$|^createAndInitializePool/.test(method)) return 'liquidity'
165
+ if (/^(transfer|safeTransfer|safeBatchTransfer|disperse)/.test(method)) return 'transfer'
166
+ if (/^(approve|permit|setApprovalForAll|increaseAllowance|decreaseAllowance|lockdown|invalidate)/.test(method)) return 'approve'
167
+ return 'contract'
168
+ }
169
+
170
+ /** 子调用里最重要的类型,决定外层 multicall 的类型 */
171
+ const PRIORITY: DecodedType[] = ['swap', 'liquidity', 'transfer', 'approve', 'contract', 'unknown']
172
+
173
+ /** Universal Router 的命令字节(低 6 位)→ 名字 */
174
+ const UR_COMMANDS: Record<number, string> = {
175
+ 0x00: 'V3_SWAP_EXACT_IN',
176
+ 0x01: 'V3_SWAP_EXACT_OUT',
177
+ 0x02: 'PERMIT2_TRANSFER_FROM',
178
+ 0x03: 'PERMIT2_PERMIT_BATCH',
179
+ 0x04: 'SWEEP',
180
+ 0x05: 'TRANSFER',
181
+ 0x06: 'PAY_PORTION',
182
+ 0x08: 'V2_SWAP_EXACT_IN',
183
+ 0x09: 'V2_SWAP_EXACT_OUT',
184
+ 0x0a: 'PERMIT2_PERMIT',
185
+ 0x0b: 'WRAP_ETH',
186
+ 0x0c: 'UNWRAP_WETH',
187
+ 0x0d: 'PERMIT2_TRANSFER_FROM_BATCH',
188
+ 0x0e: 'BALANCE_CHECK_ERC20',
189
+ 0x10: 'V4_SWAP',
190
+ 0x11: 'V3_POSITION_MANAGER_PERMIT',
191
+ 0x12: 'V3_POSITION_MANAGER_CALL',
192
+ 0x13: 'V4_INITIALIZE_POOL',
193
+ 0x14: 'V4_POSITION_MANAGER_CALL',
194
+ 0x21: 'EXECUTE_SUB_PLAN',
195
+ }
196
+
197
+ const urType = (name: string): DecodedType => (/SWAP/.test(name) ? 'swap' : /POSITION_MANAGER_CALL/.test(name) ? 'liquidity' : /PERMIT/.test(name) ? 'approve' : /TRANSFER|SWEEP|PAY_PORTION/.test(name) ? 'transfer' : 'contract')
198
+
199
+ const MAX_DEPTH = 2
200
+
201
+ /** 参数转成可 JSON 序列化的值(bigint → 字符串,Result → 数组 / 对象) */
202
+ function plain(value: unknown): unknown {
203
+ if (typeof value === 'bigint') return value.toString()
204
+ if (Array.isArray(value)) return value.map(plain)
205
+ if (value && typeof value === 'object' && typeof (value as { toObject?: unknown }).toObject === 'function') return plain((value as { toObject(): unknown }).toObject())
206
+ return value
207
+ }
208
+
209
+ const short = (a: string) => `${a.slice(0, 6)}…${a.slice(-4)}`
210
+
211
+ /**
212
+ * 解码一笔交易的 calldata。`to` 和 `chainId` 用来按内置协议表给 Router 标协议名。
213
+ *
214
+ * ```ts
215
+ * decodeCalldata(data, { to: router, chainId: 1 })
216
+ * // { type: 'swap', protocol: 'Uniswap SwapRouter02', method: 'exactInputSingle', summary: 'Uniswap SwapRouter02 exactInputSingle', args: {…} }
217
+ * ```
218
+ */
219
+ export function decodeCalldata(data: string | undefined, ctx: { to?: string; chainId?: number; value?: bigint } = {}, depth = 0): DecodedCall {
220
+ const hex = data && data !== '0x' ? data : '0x'
221
+ const name = ctx.to && ctx.chainId !== undefined ? knownSpenders(ctx.chainId).find((s) => s.address.toLowerCase() === ctx.to?.toLowerCase())?.name : undefined
222
+ if (hex === '0x') {
223
+ return ctx.value && ctx.value > 0n ? { type: 'transfer', method: 'transfer', summary: `Send native token${ctx.to ? ` to ${short(ctx.to)}` : ''}`, args: { to: ctx.to, value: ctx.value.toString() } } : { type: 'unknown', summary: 'Empty call' }
224
+ }
225
+ if (hex.length < 10) return { type: 'unknown', summary: 'Unknown call', args: { selector: hex } }
226
+ const selector = dataSlice(hex, 0, 4)
227
+ const entry = entries().get(selector)
228
+ if (!entry) return { type: 'unknown', ...(name ? { protocol: name } : {}), summary: `Unknown method ${selector}${name ? ` on ${name}` : ''}`, args: { selector } }
229
+ const method = entry.fragment.name
230
+ const args: Record<string, unknown> = {}
231
+ let parsed: Result
232
+ try {
233
+ parsed = entry.iface.decodeFunctionData(entry.fragment, hex)
234
+ // Result 按需解码,数据残缺时在读取参数时才报错
235
+ entry.fragment.inputs.forEach((input, i) => {
236
+ args[input.name || String(i)] = plain(parsed[i])
237
+ })
238
+ } catch {
239
+ return { type: 'unknown', ...(name ? { protocol: name } : {}), summary: `Undecodable ${method} call`, args: { selector, method } }
240
+ }
241
+ const protocol = name ?? (entry.group === 'Token' ? undefined : entry.group)
242
+ let type = typeOf(method)
243
+ const out: DecodedCall = { type, ...(protocol ? { protocol } : {}), method, summary: protocol ? `${protocol} ${method}` : method, args }
244
+
245
+ // 子调用:multicall / aggregate / executeBatch / Universal Router
246
+ if (depth < MAX_DEPTH) {
247
+ const inner = innerCalls(method, parsed, ctx.chainId, depth)
248
+ if (inner?.length) {
249
+ out.calls = inner
250
+ type = PRIORITY.find((t) => inner.some((c) => c.type === t)) ?? 'contract'
251
+ out.type = type
252
+ out.summary = `${protocol ? `${protocol} ` : ''}${method}: ${inner.map((c) => c.method ?? 'unknown').join(' + ')}`
253
+ }
254
+ }
255
+ return out
256
+ }
257
+
258
+ function innerCalls(method: string, parsed: ReadonlyArray<any>, chainId: number | undefined, depth: number): DecodedCall[] | null {
259
+ const sub = (to: string | undefined, data: string, value?: bigint) => decodeCalldata(data, { to, chainId, value }, depth + 1)
260
+ if (method === 'multicall') {
261
+ const list = parsed[parsed.length - 1] as string[]
262
+ return list.map((d) => sub(undefined, d))
263
+ }
264
+ if (method === 'aggregate' || method === 'tryAggregate' || method === 'aggregate3' || method === 'aggregate3Value') {
265
+ const calls = parsed[parsed.length - 1] as Array<{ target: string; callData: string; value?: bigint }>
266
+ return calls.map((c) => sub(c.target, c.callData, c.value))
267
+ }
268
+ if (method === 'executeBatch') return (parsed[0] as Array<{ target: string; value: bigint; data: string }>).map((c) => sub(c.target, c.data, c.value))
269
+ if (method === 'execute') {
270
+ const commands = (parsed[0] as string).slice(2).match(/../g) ?? []
271
+ return commands.map((byte) => {
272
+ const cmd = UR_COMMANDS[Number.parseInt(byte, 16) & 0x3f] ?? `COMMAND_0x${byte}`
273
+ return { type: urType(cmd), method: cmd, summary: cmd }
274
+ })
275
+ }
276
+ return null
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // revert 原因
281
+ // ---------------------------------------------------------------------------
282
+
283
+ const customErrors = new Interface([
284
+ // Universal Router
285
+ 'error ExecutionFailed(uint256 commandIndex, bytes message)',
286
+ 'error InvalidCommandType(uint256 commandType)',
287
+ 'error TransactionDeadlinePassed()',
288
+ 'error V3TooLittleReceived()',
289
+ 'error V3TooMuchRequested()',
290
+ 'error V2TooLittleReceived()',
291
+ 'error V2TooMuchRequested()',
292
+ 'error V3InvalidSwap()',
293
+ 'error InsufficientETH()',
294
+ 'error InsufficientToken()',
295
+ 'error BalanceTooLow()',
296
+ 'error InvalidBips()',
297
+ 'error LengthMismatch()',
298
+ // Permit2
299
+ 'error AllowanceExpired(uint256 deadline)',
300
+ 'error InsufficientAllowance(uint256 amount)',
301
+ 'error InvalidSigner()',
302
+ 'error InvalidNonce()',
303
+ 'error SignatureExpired(uint256 signatureDeadline)',
304
+ // OpenZeppelin 5
305
+ 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
306
+ 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
307
+ 'error ERC20InvalidSender(address sender)',
308
+ 'error ERC20InvalidReceiver(address receiver)',
309
+ 'error ERC721InsufficientApproval(address operator, uint256 tokenId)',
310
+ 'error ERC721NonexistentToken(uint256 tokenId)',
311
+ 'error OwnableUnauthorizedAccount(address account)',
312
+ 'error EnforcedPause()',
313
+ 'error ReentrancyGuardReentrantCall()',
314
+ 'error AddressEmptyCode(address target)',
315
+ 'error SafeERC20FailedOperation(address token)',
316
+ 'error FailedCall()',
317
+ ])
318
+
319
+ const PANIC: Record<string, string> = {
320
+ '1': 'assertion failed',
321
+ '17': 'arithmetic overflow / underflow',
322
+ '18': 'division by zero',
323
+ '33': 'invalid enum value',
324
+ '50': 'array index out of bounds',
325
+ '65': 'out of memory',
326
+ }
327
+
328
+ /**
329
+ * revert 数据 → 可读的原因:Error(string)、Panic(带含义),常见的自定义错误(Universal Router、Permit2、OpenZeppelin)带参数,
330
+ * 认不出的是 `custom error 0x选择器`。没有数据时返回 null
331
+ */
332
+ export function decodeRevert(data: string | null | undefined, depth = 0): string | null {
333
+ if (!data || data === '0x' || data.length < 10) return null
334
+ const basic = decodeRevertReason(data)
335
+ if (basic) {
336
+ const panic = /^Panic\((\d+)\)$/.exec(basic)
337
+ return panic ? `${basic}${PANIC[panic[1] as string] ? ` ${PANIC[panic[1] as string]}` : ''}` : basic
338
+ }
339
+ try {
340
+ const parsed = customErrors.parseError(data)
341
+ if (parsed) {
342
+ if (parsed.name === 'ExecutionFailed' && depth < 2) {
343
+ const inner = decodeRevert(String(parsed.args[1]), depth + 1)
344
+ return `ExecutionFailed(command ${parsed.args[0]}${inner ? `: ${inner}` : ''})`
345
+ }
346
+ return `${parsed.name}(${parsed.args.map((a: unknown) => String(a)).join(', ')})`
347
+ }
348
+ } catch {
349
+ // 落到下面
350
+ }
351
+ return `custom error ${dataSlice(data, 0, 4)}`
352
+ }
@@ -0,0 +1,63 @@
1
+ import { Interface, getAddress } from 'ethers'
2
+
3
+ import type { WriteRpc } from '../write/rpc.js'
4
+ import type { EntryPointVersion } from './userOp.js'
5
+
6
+ /** eth-infinitism 的 SimpleAccountFactory(account-abstraction v0.7 / v0.8),各主流链上地址相同 */
7
+ export const SIMPLE_ACCOUNT_FACTORY_V07 = '0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985'
8
+ export const SIMPLE_ACCOUNT_FACTORY_V08 = '0x13E9ed32155810FDbd067D4522C492D6f68E5944'
9
+
10
+ /**
11
+ * 账户类型:
12
+ * - 'simple':eth-infinitism SimpleAccount,工厂按 owner + salt 生成的合约账户(首次使用时随 UserOperation 部署)。账户地址不是 owner 的地址,资产要放在账户里
13
+ * - 'simple7702':钱包自己(EOA 通过 EIP-7702 委托给 Simple7702Account),UserOperation 的 sender 就是钱包地址,只能用 EntryPoint v0.8
14
+ */
15
+ export type SmartAccountKind = 'simple' | 'simple7702'
16
+
17
+ const factoryInterface = new Interface(['function createAccount(address owner, uint256 salt) returns (address)', 'function getAddress(address owner, uint256 salt) view returns (address)'])
18
+
19
+ const callTuple = '(address target, uint256 value, bytes data)[]'
20
+ const batchV08 = new Interface([`function executeBatch(${callTuple} calls)`])
21
+ const batchV07 = new Interface(['function executeBatch(address[] dest, uint256[] value, bytes[] func)'])
22
+
23
+ export interface BatchItem {
24
+ to: string
25
+ value: bigint
26
+ data: string
27
+ }
28
+
29
+ /** 账户执行一批调用的 calldata(v0.8 的账户用 Call[],v0.7 的 SimpleAccount 用三个数组) */
30
+ export function encodeExecuteBatch(version: EntryPointVersion, calls: readonly BatchItem[]): string {
31
+ if (version === '0.8') {
32
+ return batchV08.encodeFunctionData('executeBatch', [calls.map((c) => [c.to, c.value, c.data])])
33
+ }
34
+ return batchV07.encodeFunctionData('executeBatch', [calls.map((c) => c.to), calls.map((c) => c.value), calls.map((c) => c.data)])
35
+ }
36
+
37
+ export function defaultFactory(version: EntryPointVersion): string {
38
+ return version === '0.8' ? SIMPLE_ACCOUNT_FACTORY_V08 : SIMPLE_ACCOUNT_FACTORY_V07
39
+ }
40
+
41
+ export interface CounterfactualAccount {
42
+ address: string
43
+ deployed: boolean
44
+ factory: string
45
+ /** 还没部署时的 createAccount calldata */
46
+ factoryData: string
47
+ }
48
+
49
+ /** SimpleAccount 的地址(部署前就能算出来:向工厂 getAddress)和部署状态 */
50
+ export async function resolveSimpleAccount(rpc: WriteRpc, owner: string, version: EntryPointVersion, salt: bigint | number | string = 0n, factory = defaultFactory(version)): Promise<CounterfactualAccount> {
51
+ const factoryAddress = getAddress(factory)
52
+ if ((await rpc.code(factoryAddress)) === '0x') {
53
+ throw new Error(`The SimpleAccountFactory ${factoryAddress} is not deployed on chain ${rpc.chainId}; pass smartAccount.factory`)
54
+ }
55
+ const [address] = factoryInterface.decodeFunctionResult('getAddress', await rpc.call({ to: factoryAddress, data: factoryInterface.encodeFunctionData('getAddress', [owner, salt]) }))
56
+ const code = await rpc.code(address as string)
57
+ return { address: getAddress(address as string), deployed: code !== '0x', factory: factoryAddress, factoryData: factoryInterface.encodeFunctionData('createAccount', [owner, salt]) }
58
+ }
59
+
60
+ /** 查询账户地址(不发送任何东西),传入 owner 地址 */
61
+ export async function getSimpleAccountAddress(rpc: WriteRpc, owner: string, version: EntryPointVersion = '0.7', salt: bigint | number | string = 0n, factory?: string): Promise<string> {
62
+ return (await resolveSimpleAccount(rpc, owner, version, salt, factory)).address
63
+ }
@@ -0,0 +1,180 @@
1
+ import { fetchJson } from '../http.js'
2
+ import { DUMMY_SIGNATURE, hexToBigInt, userOpToRpc, type UserOperation } from './userOp.js'
3
+
4
+ export interface RpcClientOptions {
5
+ /** 自定义 fetch(测试、代理) */
6
+ fetch?: typeof fetch
7
+ /** 附加请求头(Authorization 等) */
8
+ headers?: Record<string, string>
9
+ }
10
+
11
+ /** 最小的 JSON-RPC 客户端:错误信息里只有域名,不带 URL(API Key 常在 URL 里) */
12
+ class JsonRpc {
13
+ #id = 0
14
+ constructor(
15
+ readonly url: string,
16
+ readonly options: RpcClientOptions = {},
17
+ ) {}
18
+
19
+ async request<T>(method: string, params: unknown[]): Promise<T> {
20
+ const body = await fetchJson<{ result?: T; error?: { code?: number; message?: string; data?: unknown } }>(
21
+ this.options.fetch ?? fetch,
22
+ this.url,
23
+ { method: 'POST', headers: { 'content-type': 'application/json', ...this.options.headers }, body: JSON.stringify({ jsonrpc: '2.0', id: ++this.#id, method, params }) },
24
+ 'secret',
25
+ )
26
+ if (body.error) {
27
+ throw new Error(`${method}: ${body.error.message ?? 'RPC error'}${body.error.code === undefined ? '' : ` (${body.error.code})`}`)
28
+ }
29
+ return body.result as T
30
+ }
31
+ }
32
+
33
+ /** bundler 返回的 UserOperation 回执 */
34
+ export interface UserOperationReceipt {
35
+ userOpHash: string
36
+ sender: string
37
+ success: boolean
38
+ /** 执行失败时的原因(revert 数据) */
39
+ reason?: string
40
+ actualGasCost: bigint
41
+ actualGasUsed: bigint
42
+ /** 打包它的那笔交易 */
43
+ transactionHash: string
44
+ blockNumber: number | null
45
+ }
46
+
47
+ export interface GasEstimate {
48
+ callGasLimit: bigint
49
+ verificationGasLimit: bigint
50
+ preVerificationGas: bigint
51
+ paymasterVerificationGasLimit?: bigint
52
+ paymasterPostOpGasLimit?: bigint
53
+ }
54
+
55
+ /**
56
+ * ERC-4337 bundler 客户端:eth_sendUserOperation / eth_estimateUserOperationGas / eth_getUserOperationReceipt / eth_supportedEntryPoints。
57
+ *
58
+ * ```ts
59
+ * const bundler = new BundlerClient('https://public.pimlico.io/v2/8453/rpc')
60
+ * await bundler.supportedEntryPoints()
61
+ * ```
62
+ */
63
+ export class BundlerClient {
64
+ readonly #rpc: JsonRpc
65
+ constructor(url: string, options: RpcClientOptions = {}) {
66
+ this.#rpc = new JsonRpc(url, options)
67
+ }
68
+
69
+ supportedEntryPoints(): Promise<string[]> {
70
+ return this.#rpc.request('eth_supportedEntryPoints', [])
71
+ }
72
+
73
+ chainId(): Promise<number> {
74
+ return this.#rpc.request<string>('eth_chainId', []).then(Number)
75
+ }
76
+
77
+ /** 估算 gas:signature 为空时用占位签名 */
78
+ async estimateUserOperationGas(op: UserOperation, entryPoint: string): Promise<GasEstimate> {
79
+ const rpcOp = userOpToRpc({ ...op, signature: op.signature === '0x' ? DUMMY_SIGNATURE : op.signature })
80
+ const r = await this.#rpc.request<Record<string, string>>('eth_estimateUserOperationGas', [rpcOp, entryPoint])
81
+ return {
82
+ callGasLimit: hexToBigInt(r.callGasLimit),
83
+ verificationGasLimit: hexToBigInt(r.verificationGasLimit),
84
+ preVerificationGas: hexToBigInt(r.preVerificationGas),
85
+ ...(r.paymasterVerificationGasLimit === undefined ? {} : { paymasterVerificationGasLimit: hexToBigInt(r.paymasterVerificationGasLimit) }),
86
+ ...(r.paymasterPostOpGasLimit === undefined ? {} : { paymasterPostOpGasLimit: hexToBigInt(r.paymasterPostOpGasLimit) }),
87
+ }
88
+ }
89
+
90
+ /** 提交,返回 userOpHash */
91
+ sendUserOperation(op: UserOperation, entryPoint: string): Promise<string> {
92
+ return this.#rpc.request('eth_sendUserOperation', [userOpToRpc(op), entryPoint])
93
+ }
94
+
95
+ /** 回执;还没上链为 null */
96
+ async getUserOperationReceipt(hash: string): Promise<UserOperationReceipt | null> {
97
+ const r = await this.#rpc.request<{ userOpHash: string; sender: string; success: boolean; reason?: string; actualGasCost: string; actualGasUsed: string; receipt: { transactionHash: string; blockNumber?: string } } | null>(
98
+ 'eth_getUserOperationReceipt',
99
+ [hash],
100
+ )
101
+ if (!r) {
102
+ return null
103
+ }
104
+ return {
105
+ userOpHash: r.userOpHash,
106
+ sender: r.sender,
107
+ success: r.success === true,
108
+ ...(r.reason ? { reason: r.reason } : {}),
109
+ actualGasCost: hexToBigInt(r.actualGasCost),
110
+ actualGasUsed: hexToBigInt(r.actualGasUsed),
111
+ transactionHash: r.receipt.transactionHash,
112
+ blockNumber: r.receipt.blockNumber ? Number(r.receipt.blockNumber) : null,
113
+ }
114
+ }
115
+
116
+ /** 轮询回执直到上链;超时返回 null(UserOperation 之后仍可能上链) */
117
+ async waitForUserOperation(hash: string, options: { timeout?: number; pollInterval?: number; signal?: AbortSignal } = {}): Promise<UserOperationReceipt | null> {
118
+ const { timeout = 120_000, pollInterval = 2_000, signal } = options
119
+ const deadline = Date.now() + timeout
120
+ while (!signal?.aborted) {
121
+ try {
122
+ const receipt = await this.getUserOperationReceipt(hash)
123
+ if (receipt) {
124
+ return receipt
125
+ }
126
+ } catch {
127
+ // bundler 暂时出错:下次再查
128
+ }
129
+ if (Date.now() >= deadline) {
130
+ break
131
+ }
132
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollInterval, Math.max(0, deadline - Date.now()))))
133
+ }
134
+ return null
135
+ }
136
+ }
137
+
138
+ /** paymaster 返回的字段,合并进 UserOperation */
139
+ export type PaymasterFields = Pick<UserOperation, 'paymaster' | 'paymasterData' | 'paymasterVerificationGasLimit' | 'paymasterPostOpGasLimit'>
140
+
141
+ export interface PaymasterContext {
142
+ entryPoint: string
143
+ chainId: number
144
+ /** 'stub':估算 gas 前,要占位数据;'final':gas 已定,要真正的签名数据 */
145
+ stage: 'stub' | 'final'
146
+ }
147
+
148
+ /** 自定义 paymaster:拿到 UserOperation 返回 paymaster 字段(verifying paymaster 自己签名、自家后端等) */
149
+ export type PaymasterHook = (op: UserOperation, context: PaymasterContext) => Promise<PaymasterFields> | PaymasterFields
150
+
151
+ /**
152
+ * ERC-7677 paymaster 服务客户端:pm_getPaymasterStubData(估算 gas 用的占位数据)/ pm_getPaymasterData(最终数据)。
153
+ * URL / Key 由应用提供。`client.hook` 可直接作为 `paymaster` 选项
154
+ */
155
+ export class PaymasterClient {
156
+ readonly #rpc: JsonRpc
157
+ constructor(
158
+ url: string,
159
+ /** 服务商要求的 context(如 Pimlico 的 `{ sponsorshipPolicyId }`) */
160
+ readonly context: Record<string, unknown> = {},
161
+ options: RpcClientOptions = {},
162
+ ) {
163
+ this.#rpc = new JsonRpc(url, options)
164
+ }
165
+
166
+ readonly hook: PaymasterHook = async (op, { entryPoint, chainId, stage }) => {
167
+ const method = stage === 'stub' ? 'pm_getPaymasterStubData' : 'pm_getPaymasterData'
168
+ const r = await this.#rpc.request<Record<string, string>>(method, [userOpToRpc(op), entryPoint, `0x${chainId.toString(16)}`, this.context])
169
+ // v0.6 的 paymasterAndData 格式不支持:只处理拆开的 v0.7 / v0.8 字段
170
+ if (!r.paymaster) {
171
+ throw new Error(`${method}: the response has no paymaster field (EntryPoint v0.7 / v0.8 format expected)`)
172
+ }
173
+ return {
174
+ paymaster: r.paymaster,
175
+ paymasterData: r.paymasterData ?? '0x',
176
+ paymasterVerificationGasLimit: hexToBigInt(r.paymasterVerificationGasLimit),
177
+ paymasterPostOpGasLimit: hexToBigInt(r.paymasterPostOpGasLimit),
178
+ }
179
+ }
180
+ }