@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,187 @@
1
+ import type { BatchCallOptions, NormalizedCall } from '../write/batch.js'
2
+ import { sendEach } from '../write/batch.js'
3
+ import { WriteRpc, failureText } from '../write/rpc.js'
4
+ import { quantity } from './userOp.js'
5
+ import { emptyOutcome, toWriteResult, type TxOutcome, type WriteResult } from '../write/send.js'
6
+
7
+ /**
8
+ * 浏览器钱包的提供者,三种都可以:EIP-1193 provider(`window.ethereum`,有 request)、
9
+ * ethers 的 BrowserProvider(有 send)、ethers 的 JsonRpcSigner(用它背后的 provider)
10
+ */
11
+ export type Eip1193Like =
12
+ | { request(args: { method: string; params?: unknown[] | object }): Promise<unknown> }
13
+ | { send(method: string, params: unknown[]): Promise<unknown> }
14
+ | { provider: { send(method: string, params: unknown[]): Promise<unknown> } | { request(args: { method: string; params?: unknown[] | object }): Promise<unknown> } }
15
+
16
+ export type RequestFn = (method: string, params: unknown[]) => Promise<unknown>
17
+
18
+ /** 统一成 (method, params) => Promise */
19
+ export function toRequest(p: Eip1193Like): RequestFn {
20
+ const direct = p as { request?: unknown; send?: unknown; provider?: unknown }
21
+ if (typeof direct.request === 'function') {
22
+ return (method, params) => (p as { request(args: { method: string; params: unknown[] }): Promise<unknown> }).request({ method, params })
23
+ }
24
+ if (typeof direct.send === 'function') {
25
+ return (method, params) => (p as { send(method: string, params: unknown[]): Promise<unknown> }).send(method, params)
26
+ }
27
+ if (direct.provider) {
28
+ return toRequest(direct.provider as Eip1193Like)
29
+ }
30
+ throw new Error('Expected an EIP-1193 provider, an ethers BrowserProvider or a JsonRpcSigner')
31
+ }
32
+
33
+ /** 钱包对某条链的能力(wallet_getCapabilities) */
34
+ export interface WalletCapabilities {
35
+ /** 'supported':能原子批量;'ready':钱包会在用户同意后升级账户;'unsupported':不支持 */
36
+ atomic: 'supported' | 'ready' | 'unsupported'
37
+ /** 钱包是否支持 paymasterService(代付 gas)能力 */
38
+ paymasterService: boolean
39
+ /** 钱包返回的原始能力 */
40
+ raw: Record<string, unknown>
41
+ }
42
+
43
+ const chainHex = (chainId: number) => quantity(chainId)
44
+
45
+ /** 查询钱包能力;钱包不支持 EIP-5792(报错)时返回 null */
46
+ export async function getWalletCapabilities(provider: Eip1193Like, address: string, chainId: number): Promise<WalletCapabilities | null> {
47
+ let all: Record<string, Record<string, any>> | null
48
+ try {
49
+ all = (await toRequest(provider)('wallet_getCapabilities', [address, [chainHex(chainId)]])) as Record<string, Record<string, any>> | null
50
+ } catch {
51
+ return null
52
+ }
53
+ const raw = all?.[chainHex(chainId)] ?? all?.[String(chainId)] ?? Object.entries(all ?? {}).find(([k]) => k.toLowerCase() === chainHex(chainId).toLowerCase())?.[1] ?? {}
54
+ const status = raw.atomic?.status ?? (raw.atomicBatch?.supported ? 'supported' : 'unsupported')
55
+ return { atomic: status === 'supported' || status === 'ready' ? status : 'unsupported', paymasterService: Boolean(raw.paymasterService?.supported), raw }
56
+ }
57
+
58
+ export interface SendCallsRequest {
59
+ from: string
60
+ chainId: number
61
+ calls: ReadonlyArray<{ to: string; value?: bigint; data?: string }>
62
+ /** 必须原子执行(钱包不能原子就拒绝)。默认 true */
63
+ atomicRequired?: boolean
64
+ /** 钱包能力扩展(如 `{ paymasterService: { url } }` 让钱包找 paymaster 代付 gas) */
65
+ capabilities?: Record<string, unknown>
66
+ }
67
+
68
+ /** wallet_sendCalls:返回调用批次的 id(用 getCallsStatus 查询) */
69
+ export async function sendCalls(provider: Eip1193Like, req: SendCallsRequest): Promise<string> {
70
+ const result = (await toRequest(provider)('wallet_sendCalls', [
71
+ {
72
+ version: '2.0.0',
73
+ from: req.from,
74
+ chainId: chainHex(req.chainId),
75
+ atomicRequired: req.atomicRequired ?? true,
76
+ calls: req.calls.map((c) => ({ to: c.to, ...(c.value ? { value: quantity(c.value) } : {}), data: c.data ?? '0x' })),
77
+ ...(req.capabilities ? { capabilities: req.capabilities } : {}),
78
+ },
79
+ ])) as string | { id: string }
80
+ return typeof result === 'string' ? result : result.id
81
+ }
82
+
83
+ export interface CallsStatus {
84
+ status: 'pending' | 'confirmed' | 'failed' | 'reverted' | 'partial'
85
+ atomic: boolean
86
+ receipts: Array<{ transactionHash: string; blockNumber: number | null; gasUsed: bigint | null; success: boolean }>
87
+ }
88
+
89
+ /** wallet_getCallsStatus(兼容 v1 的字符串状态和 v2 的 100 / 200 / 400 / 500 / 600 状态码) */
90
+ export async function getCallsStatus(provider: Eip1193Like, id: string): Promise<CallsStatus> {
91
+ const r = (await toRequest(provider)('wallet_getCallsStatus', [id])) as { status: number | string; atomic?: boolean; receipts?: Array<{ transactionHash: string; blockNumber?: string; gasUsed?: string; status?: string }> }
92
+ const code = typeof r.status === 'number' ? r.status : r.status === 'CONFIRMED' ? 200 : r.status === 'PENDING' ? 100 : 400
93
+ const status = code < 200 ? 'pending' : code < 300 ? 'confirmed' : code < 500 ? 'failed' : code === 500 ? 'reverted' : 'partial'
94
+ const receipts = (r.receipts ?? []).map((x) => ({
95
+ transactionHash: x.transactionHash,
96
+ blockNumber: x.blockNumber ? Number(x.blockNumber) : null,
97
+ gasUsed: x.gasUsed ? BigInt(x.gasUsed) : null,
98
+ success: x.status === undefined || Number(x.status) === 1,
99
+ }))
100
+ return { status, atomic: r.atomic ?? true, receipts }
101
+ }
102
+
103
+ /** 轮询到有结果;超时返回最后一次的 pending */
104
+ export async function waitForCalls(provider: Eip1193Like, id: string, options: { timeout?: number; pollInterval?: number; signal?: AbortSignal } = {}): Promise<CallsStatus> {
105
+ const { timeout = 180_000, pollInterval = 2_000, signal } = options
106
+ const deadline = Date.now() + timeout
107
+ let last: CallsStatus = { status: 'pending', atomic: true, receipts: [] }
108
+ while (!signal?.aborted) {
109
+ try {
110
+ last = await getCallsStatus(provider, id)
111
+ if (last.status !== 'pending') return last
112
+ } catch {
113
+ // 钱包暂时没有这个 id 的状态:下次再查
114
+ }
115
+ if (Date.now() >= deadline) break
116
+ await new Promise((resolve) => setTimeout(resolve, Math.min(pollInterval, Math.max(0, deadline - Date.now()))))
117
+ }
118
+ return last
119
+ }
120
+
121
+ /**
122
+ * '5792' 模式:钱包支持原子批量就用 wallet_sendCalls(一次确认、一笔交易原子执行);
123
+ * 不支持(钱包不实现 EIP-5792,或链上账户不能原子批量)时退回逐笔发送(每笔一次确认),除非 atomicRequired
124
+ */
125
+ export async function send5792(rpc: WriteRpc, calls: readonly NormalizedCall[], options: BatchCallOptions): Promise<WriteResult[]> {
126
+ const first = calls[0]
127
+ if (!first) return []
128
+ if (calls.some((c) => c.signer.address !== first.signer.address)) {
129
+ throw new Error("mode '5792' sends from one browser wallet; split calls per wallet")
130
+ }
131
+ const provider = options.eip1193 ?? (first.signer.signer as { provider?: Eip1193Like }).provider
132
+ if (!provider) {
133
+ throw new Error("mode '5792' requires a browser wallet signer (ethers JsonRpcSigner) or the eip1193 option")
134
+ }
135
+ const caps = await getWalletCapabilities(provider, first.signer.address, rpc.chainId)
136
+ const atomic = caps !== null && caps.atomic !== 'unsupported'
137
+ if (!atomic) {
138
+ if (options.atomicRequired) {
139
+ return calls.map((c, i) => toWriteResult(i, c.signer.address, c.to, emptyOutcome('failed', caps ? 'the wallet cannot execute batches atomically on this chain' : 'the wallet does not support EIP-5792 (wallet_getCapabilities)')))
140
+ }
141
+ // 退回逐笔发送:钱包要能直接发送交易
142
+ const results = await sendEach(rpc, calls, options)
143
+ return results.map((r) => ({ ...r, atomic: false }))
144
+ }
145
+ const results = calls.map((c, i) => toWriteResult(i, c.signer.address, c.to, emptyOutcome('skipped')))
146
+ const report = (outcome: TxOutcome, perCall?: (i: number) => Partial<TxOutcome>) => {
147
+ calls.forEach((c, i) => {
148
+ results[i] = { ...toWriteResult(i, c.signer.address, c.to, { ...outcome, ...perCall?.(i) }), atomic: true }
149
+ options.onUpdate?.(results[i] as WriteResult)
150
+ })
151
+ }
152
+ if (options.dryRun) {
153
+ report(emptyOutcome('simulated'))
154
+ return results
155
+ }
156
+ try {
157
+ const id = await sendCalls(provider, {
158
+ from: first.signer.address,
159
+ chainId: rpc.chainId,
160
+ calls: calls.map((c) => ({ to: c.to, value: c.value, data: c.data })),
161
+ atomicRequired: true,
162
+ capabilities: options.capabilities,
163
+ })
164
+ report({ ...emptyOutcome('sent'), hash: null })
165
+ if (options.wait === false) return results
166
+ const status = await waitForCalls(provider, id, options)
167
+ const receipt = status.receipts[0]
168
+ const base: TxOutcome = { ...emptyOutcome('pending', 'not confirmed in time; query wallet_getCallsStatus with the id later'), hash: receipt?.transactionHash ?? null }
169
+ if (status.status === 'confirmed' || status.status === 'reverted' || status.status === 'partial') {
170
+ report({
171
+ ...base,
172
+ status: status.status === 'confirmed' && receipt?.success !== false ? 'confirmed' : 'reverted',
173
+ error: status.status === 'confirmed' ? null : 'execution reverted',
174
+ blockNumber: receipt?.blockNumber ?? null,
175
+ gasUsed: receipt?.gasUsed ?? null,
176
+ })
177
+ } else if (status.status === 'failed') {
178
+ report({ ...base, status: 'failed', error: 'the wallet reported that the batch failed' })
179
+ } else {
180
+ report(base)
181
+ }
182
+ } catch (err) {
183
+ const rejected = (err as { code?: number } | null)?.code === 4001 || (err as { code?: string } | null)?.code === 'ACTION_REJECTED'
184
+ report(emptyOutcome('failed', rejected ? 'rejected by user' : failureText(err)))
185
+ }
186
+ return results
187
+ }
@@ -0,0 +1,341 @@
1
+ import { Interface, getAddress, getBytes, type Authorization } from 'ethers'
2
+
3
+ import { decodeRevertReason } from '../errors.js'
4
+ import { SIMPLE_7702_ACCOUNT, delegationOf, failedIndex, type BatchCallOptions, type NormalizedCall } from '../write/batch.js'
5
+ import { WriteRpc, failureText } from '../write/rpc.js'
6
+ import { FeeSource, emptyOutcome, mapPool, sendJobs, toWriteResult, type TxJob, type TxOutcome, type WriteResult } from '../write/send.js'
7
+ import { resolveSigner, type ResolvedSigner, type SignerInput } from '../write/signer.js'
8
+ import { encodeExecuteBatch, resolveSimpleAccount, type SmartAccountKind } from './account.js'
9
+ import { BundlerClient, PaymasterClient, type GasEstimate, type PaymasterHook } from './clients.js'
10
+ import {
11
+ DUMMY_SIGNATURE,
12
+ EIP7702_MARKER,
13
+ entryPointAddress,
14
+ entryPointInterface,
15
+ getUserOpHash,
16
+ packUserOp,
17
+ userOpTypedData,
18
+ type EntryPointVersion,
19
+ type UserOperation,
20
+ } from './userOp.js'
21
+
22
+ export interface SmartAccountOptions {
23
+ /** 账户类型,默认 'simple'(见 SmartAccountKind);7702 模式固定为 'simple7702' */
24
+ account?: SmartAccountKind
25
+ /** EntryPoint 版本:'simple' 默认 '0.7','simple7702' 只能 '0.8' */
26
+ entryPoint?: EntryPointVersion
27
+ /** 'simple' 账户的 salt,默认 0(同一个 owner 可以有多个账户) */
28
+ salt?: bigint | number | string
29
+ /** 'simple' 账户的工厂,默认 eth-infinitism 的 SimpleAccountFactory */
30
+ factory?: string
31
+ /** bundler:URL 或 BundlerClient。UserOperation 交给它打包上链 */
32
+ bundler?: string | BundlerClient
33
+ /** paymaster(代付 gas):ERC-7677 客户端,或自定义函数 */
34
+ paymaster?: PaymasterClient | PaymasterHook
35
+ /**
36
+ * 不用 bundler,自己发 EntryPoint.handleOps 交易:true 表示用账户的 owner 钱包发(owner 付交易 gas),
37
+ * 或传另一个钱包 / 私钥(它付 gas,即 sponsor)。这种方式 UserOperation 的 maxFeePerGas 为 0,账户本身不扣 gas
38
+ */
39
+ selfBundle?: SignerInput | true
40
+ /** 覆盖 gas 预估(自己打包时的 callGasLimit 等) */
41
+ gas?: Partial<GasEstimate>
42
+ /** 把 7702 授权交给 bundler 时的委托合约、是否替换已有委托,同 BatchCallOptions.delegate / redelegate(由调用方填) */
43
+ delegate?: string
44
+ redelegate?: boolean
45
+ }
46
+
47
+ export interface SmartSendOptions extends BatchCallOptions {
48
+ smartAccount?: SmartAccountOptions
49
+ }
50
+
51
+ const FALLBACK_CALL_GAS = 300_000n
52
+ const PER_CALL_OVERHEAD = 15_000n
53
+ const VERIFICATION_GAS = 200_000n
54
+ const DEPLOY_VERIFICATION_GAS = 700_000n
55
+ const PRE_VERIFICATION_GAS = 100_000n
56
+
57
+ type Ctx = {
58
+ rpc: WriteRpc
59
+ options: SmartSendOptions
60
+ so: SmartAccountOptions
61
+ kind: SmartAccountKind
62
+ version: EntryPointVersion
63
+ entryPoint: string
64
+ delegate: string
65
+ fees: FeeSource
66
+ bundler: BundlerClient | null
67
+ paymaster: PaymasterHook | null
68
+ }
69
+
70
+ /** 一笔 UserOperation 的结果(对应一批调用) */
71
+ interface OpOutcome extends TxOutcome {
72
+ userOpHash: string | null
73
+ /** 账户(sender)地址 */
74
+ sender: string
75
+ }
76
+
77
+ function paymasterHook(p: SmartAccountOptions['paymaster']): PaymasterHook | null {
78
+ if (!p) return null
79
+ return typeof p === 'function' ? p : p.hook
80
+ }
81
+
82
+ /**
83
+ * 智能账户批量执行:同一钱包的调用打成一个(按 batchSize 拆分)UserOperation,由 bundler 或自己(handleOps)打包上链,一个 UserOperation 里的调用原子执行。
84
+ * 结果的 gasUsed / fee 是 UserOperation 的 actualGasUsed / actualGasCost(账户或 paymaster 付的),不含打包交易其余部分
85
+ */
86
+ export async function sendSmartBatch(rpc: WriteRpc, calls: readonly NormalizedCall[], options: SmartSendOptions): Promise<WriteResult[]> {
87
+ const so = options.smartAccount ?? {}
88
+ const kind = so.account ?? 'simple'
89
+ const version = so.entryPoint ?? (kind === 'simple7702' ? '0.8' : '0.7')
90
+ if (kind === 'simple7702' && version !== '0.8') {
91
+ throw new Error("The 7702 account (Simple7702Account) only works with EntryPoint v0.8")
92
+ }
93
+ if (!so.bundler && !so.selfBundle) {
94
+ throw new Error('smartAccount.bundler (a bundler URL) or smartAccount.selfBundle is required')
95
+ }
96
+ const ctx: Ctx = {
97
+ rpc,
98
+ options,
99
+ so,
100
+ kind,
101
+ version,
102
+ entryPoint: entryPointAddress(version),
103
+ delegate: getAddress(so.delegate ?? options.delegate ?? SIMPLE_7702_ACCOUNT),
104
+ fees: new FeeSource(rpc, options),
105
+ bundler: so.selfBundle ? null : typeof so.bundler === 'string' ? new BundlerClient(so.bundler) : (so.bundler ?? null),
106
+ paymaster: paymasterHook(so.paymaster),
107
+ }
108
+ const results: WriteResult[] = calls.map((c, i) => toWriteResult(i, c.signer.address, c.to, emptyOutcome('skipped')))
109
+ const report = (indexes: readonly number[], outcome: OpOutcome, failed?: { index: number; reason: string }) => {
110
+ indexes.forEach((i, k) => {
111
+ const c = calls[i] as NormalizedCall
112
+ const o = failed ? (k === failed.index ? { ...outcome, error: failed.reason } : outcome) : outcome
113
+ results[i] = { ...toWriteResult(i, outcome.sender, c.to, o), userOpHash: outcome.userOpHash, atomic: true }
114
+ options.onUpdate?.(results[i] as WriteResult)
115
+ })
116
+ }
117
+
118
+ const byWallet = new Map<string, number[]>()
119
+ calls.forEach((c, i) => byWallet.set(c.signer.address.toLowerCase(), [...(byWallet.get(c.signer.address.toLowerCase()) ?? []), i]))
120
+ const batchSize = Math.max(1, options.batchSize ?? 50)
121
+ // 同一个 sponsor 钱包发所有 handleOps:并发会抢 nonce,按顺序发
122
+ await mapPool([...byWallet.values()], so.selfBundle && so.selfBundle !== true ? 1 : (options.concurrency ?? 8), async (indexes) => {
123
+ const signer = (calls[indexes[0] as number] as NormalizedCall).signer
124
+ let stopped = false
125
+ for (let start = 0; start < indexes.length; start += batchSize) {
126
+ const chunk = indexes.slice(start, start + batchSize)
127
+ if (stopped || options.signal?.aborted) {
128
+ report(chunk, { ...emptyOutcome('skipped', options.signal?.aborted ? 'aborted' : 'skipped: a previous batch from this wallet failed'), userOpHash: null, sender: signer.address })
129
+ continue
130
+ }
131
+ const last = start + batchSize >= indexes.length
132
+ let outcome: OpOutcome
133
+ try {
134
+ outcome = await sendOp(ctx, signer, chunk.map((i) => calls[i] as NormalizedCall), last ? options.wait !== false : true)
135
+ } catch (err) {
136
+ outcome = { ...emptyOutcome('failed', failureText(err)), userOpHash: null, sender: signer.address }
137
+ }
138
+ report(chunk, outcome, outcome.status === 'reverted' && outcome.revertData ? (failedIndex(outcome.revertData, chunk.length) ?? undefined) : undefined)
139
+ if (outcome.status !== 'confirmed' && outcome.status !== 'simulated' && !options.continueOnError) stopped = true
140
+ }
141
+ })
142
+ return results
143
+ }
144
+
145
+ /** 账户信息:sender、是否要部署 / 授权 */
146
+ interface AccountInfo {
147
+ sender: string
148
+ factory?: string
149
+ factoryData?: string
150
+ /** 账户上已经有代码(可以估算整批的 gas) */
151
+ live: boolean
152
+ /** 需要带 7702 授权 */
153
+ needsAuth: boolean
154
+ }
155
+
156
+ async function accountInfo(ctx: Ctx, signer: ResolvedSigner): Promise<AccountInfo> {
157
+ const { rpc } = ctx
158
+ if (ctx.kind === 'simple') {
159
+ const a = await resolveSimpleAccount(rpc, signer.address, ctx.version, ctx.so.salt, ctx.so.factory)
160
+ return { sender: a.address, live: a.deployed, needsAuth: false, ...(a.deployed ? {} : { factory: a.factory, factoryData: a.factoryData }) }
161
+ }
162
+ if (!signer.local || typeof signer.signer.authorize !== 'function') {
163
+ throw new Error('The 7702 account requires a signer that can sign EIP-7702 authorizations locally (a private key or an ethers Wallet)')
164
+ }
165
+ if ((await rpc.code(ctx.delegate)) === '0x') {
166
+ throw new Error(`The delegate contract ${ctx.delegate} is not deployed on chain ${rpc.chainId}`)
167
+ }
168
+ const { delegated, to } = await delegationOf(rpc, signer.address)
169
+ if (delegated && to !== ctx.delegate && !(ctx.so.redelegate ?? ctx.options.redelegate)) {
170
+ throw new Error(`${signer.address} is already delegated to ${to}; pass redelegate: true to replace it`)
171
+ }
172
+ const needsAuth = !delegated || to !== ctx.delegate
173
+ return { sender: signer.address, live: !needsAuth, needsAuth, ...(needsAuth ? { factory: EIP7702_MARKER, factoryData: '0x' } : {}) }
174
+ }
175
+
176
+ async function readNonce(ctx: Ctx, sender: string): Promise<bigint> {
177
+ const data = await ctx.rpc.call({ to: ctx.entryPoint, data: entryPointInterface.encodeFunctionData('getNonce', [sender, 0]) })
178
+ return entryPointInterface.decodeFunctionResult('getNonce', data)[0] as bigint
179
+ }
180
+
181
+ /** 没有 bundler 时估算 gas:整批能执行时估整批(从 EntryPoint 调账户),否则逐个调用估算后相加 */
182
+ async function estimateCallGas(ctx: Ctx, info: AccountInfo, callData: string, items: readonly NormalizedCall[]): Promise<bigint> {
183
+ const multiplier = BigInt(Math.round((ctx.options.gasMultiplier ?? 1.2) * 1000))
184
+ if (info.live) {
185
+ return (await ctx.rpc.estimateGas({ from: ctx.entryPoint, to: info.sender, data: callData })) * multiplier / 1000n + PER_CALL_OVERHEAD
186
+ }
187
+ let total = 0n
188
+ for (const [i, c] of items.entries()) {
189
+ try {
190
+ total += await ctx.rpc.estimateGas({ from: info.sender, to: c.to, data: c.data, value: c.value })
191
+ } catch (err) {
192
+ // 依赖前面调用结果的(先 approve 再兑换)单独估算会失败;第一个调用失败说明批量本身就会失败
193
+ if (i === 0) throw err
194
+ total += FALLBACK_CALL_GAS
195
+ }
196
+ total += PER_CALL_OVERHEAD
197
+ }
198
+ return (total * multiplier) / 1000n
199
+ }
200
+
201
+ async function signOp(ctx: Ctx, signer: ResolvedSigner, op: UserOperation, hash: string): Promise<string> {
202
+ if (ctx.version === '0.8') {
203
+ if (typeof signer.signer.signTypedData !== 'function') throw new Error('The signer does not support signTypedData')
204
+ const { domain, types, message } = userOpTypedData(op, ctx.entryPoint, ctx.rpc.chainId, ctx.delegate)
205
+ return signer.signer.signTypedData(domain, types, message)
206
+ }
207
+ if (typeof signer.signer.signMessage !== 'function') throw new Error('The signer does not support signMessage')
208
+ return signer.signer.signMessage(getBytes(hash))
209
+ }
210
+
211
+ async function signAuth(ctx: Ctx, signer: ResolvedSigner, nonce: number): Promise<Authorization> {
212
+ return (signer.signer.authorize as NonNullable<typeof signer.signer.authorize>)({ address: ctx.delegate, nonce, chainId: ctx.rpc.chainId })
213
+ }
214
+
215
+ async function sendOp(ctx: Ctx, signer: ResolvedSigner, items: readonly NormalizedCall[], wait: boolean): Promise<OpOutcome> {
216
+ const { rpc, options } = ctx
217
+ const info = await accountInfo(ctx, signer)
218
+ const callData = encodeExecuteBatch(ctx.version, items)
219
+ const selfBundle = Boolean(ctx.so.selfBundle)
220
+ const fee = selfBundle ? null : await ctx.fees.get()
221
+ const op: UserOperation = {
222
+ sender: info.sender,
223
+ nonce: await readNonce(ctx, info.sender),
224
+ ...(info.factory ? { factory: info.factory, factoryData: info.factoryData } : {}),
225
+ callData,
226
+ callGasLimit: 0n,
227
+ verificationGasLimit: 0n,
228
+ preVerificationGas: 0n,
229
+ maxFeePerGas: fee?.perGas ?? 0n,
230
+ maxPriorityFeePerGas: fee ? (fee.type === 2 ? (fee.maxPriorityFeePerGas as bigint) : fee.perGas) : 0n,
231
+ signature: DUMMY_SIGNATURE,
232
+ }
233
+ const pmContext = { entryPoint: ctx.entryPoint, chainId: rpc.chainId }
234
+ const hook = ctx.paymaster
235
+
236
+ // gas:bundler 估算,或本地估算
237
+ if (ctx.bundler) {
238
+ if (info.needsAuth) {
239
+ // bundler 是别人:授权 nonce 就是钱包当前的 nonce
240
+ op.eip7702Auth = await signAuth(ctx, signer, await rpc.nonce(signer.address))
241
+ }
242
+ if (hook) Object.assign(op, await hook(op, { ...pmContext, stage: 'stub' }))
243
+ Object.assign(op, await ctx.bundler.estimateUserOperationGas(op, ctx.entryPoint))
244
+ } else {
245
+ if (hook) Object.assign(op, await hook(op, { ...pmContext, stage: 'stub' }))
246
+ op.callGasLimit = await estimateCallGas(ctx, info, callData, items)
247
+ op.verificationGasLimit = info.factory ? DEPLOY_VERIFICATION_GAS : VERIFICATION_GAS
248
+ op.preVerificationGas = PRE_VERIFICATION_GAS
249
+ }
250
+ Object.assign(op, ctx.so.gas)
251
+ if (hook) Object.assign(op, await hook({ ...op, signature: DUMMY_SIGNATURE }, { ...pmContext, stage: 'final' }))
252
+
253
+ const delegate = info.needsAuth || ctx.kind === 'simple7702' ? ctx.delegate : undefined
254
+ const hash = getUserOpHash(op, ctx.entryPoint, rpc.chainId, ctx.version, delegate)
255
+ op.signature = await signOp(ctx, signer, op, hash)
256
+
257
+ if (ctx.bundler) {
258
+ if (options.dryRun) return { ...emptyOutcome('simulated'), gasLimit: op.callGasLimit + op.verificationGasLimit + op.preVerificationGas, userOpHash: hash, sender: info.sender }
259
+ const sent = await ctx.bundler.sendUserOperation(op, ctx.entryPoint)
260
+ if (sent.toLowerCase() !== hash.toLowerCase()) {
261
+ throw new Error(`The bundler returned a different userOpHash (${sent}) than computed locally (${hash}); not tracking it`)
262
+ }
263
+ if (!wait) return { ...emptyOutcome('sent'), userOpHash: hash, sender: info.sender }
264
+ const receipt = await ctx.bundler.waitForUserOperation(hash, options)
265
+ if (!receipt) return { ...emptyOutcome('pending', 'not confirmed in time; the UserOperation may still be included'), userOpHash: hash, sender: info.sender }
266
+ return {
267
+ ...emptyOutcome(receipt.success ? 'confirmed' : 'reverted', receipt.success ? null : 'execution reverted'),
268
+ hash: receipt.transactionHash,
269
+ blockNumber: receipt.blockNumber,
270
+ gasUsed: receipt.actualGasUsed,
271
+ fee: receipt.actualGasCost,
272
+ revertData: receipt.reason && receipt.reason.startsWith('0x') ? receipt.reason : null,
273
+ userOpHash: hash,
274
+ sender: info.sender,
275
+ }
276
+ }
277
+
278
+ // 自己打包:bundler 钱包发一笔 handleOps(带 7702 授权时一起带上)
279
+ const bundlerSigner = ctx.so.selfBundle === true ? signer : await resolveSigner(ctx.so.selfBundle as SignerInput)
280
+ if (!bundlerSigner.local) throw new Error('selfBundle requires a signer that can sign transactions locally (a private key or an ethers Wallet)')
281
+ const packed = packUserOp(op)
282
+ const data = entryPointInterface.encodeFunctionData('handleOps', [[packed], bundlerSigner.address])
283
+ const job: TxJob = {
284
+ signer: bundlerSigner,
285
+ build: async (nonce) => {
286
+ let authorizationList: Authorization[] | undefined
287
+ if (info.needsAuth) {
288
+ // 钱包自己发这笔交易时,交易先把 nonce 加一再处理授权
289
+ const authNonce = bundlerSigner.address === signer.address ? (nonce as number) + 1 : await rpc.nonce(signer.address)
290
+ authorizationList = [await signAuth(ctx, signer, authNonce)]
291
+ }
292
+ return { to: ctx.entryPoint, data, value: 0n, authorizationList }
293
+ },
294
+ }
295
+ const [txOutcome] = await sendJobs(rpc, [job], { ...options, wait }, ctx.fees)
296
+ const outcome = txOutcome as TxOutcome
297
+ if (outcome.status === 'failed' && outcome.revertData) {
298
+ outcome.error = decodeFailedOp(outcome.revertData) ?? outcome.error
299
+ }
300
+ const result: OpOutcome = { ...outcome, userOpHash: hash, sender: info.sender }
301
+ if (outcome.status === 'confirmed' && outcome.hash) {
302
+ // 交易成功不代表 UserOperation 成功:看 UserOperationEvent
303
+ const receipt = await rpc.receipt(outcome.hash)
304
+ const event = receipt?.logs.map((l) => parseLog(l)).find((e) => e?.name === 'UserOperationEvent' && String(e.args[0]).toLowerCase() === hash.toLowerCase())
305
+ if (!event) {
306
+ return { ...result, status: 'reverted', error: 'the UserOperation was not executed by the EntryPoint' }
307
+ }
308
+ const reason = receipt?.logs.map((l) => parseLog(l)).find((e) => e?.name === 'UserOperationRevertReason' && String(e.args[0]).toLowerCase() === hash.toLowerCase())
309
+ result.gasUsed = event.args[6] as bigint
310
+ result.fee = event.args[5] as bigint
311
+ if (event.args[4] !== true) {
312
+ const revert = reason ? String(reason.args[3]) : null
313
+ return { ...result, status: 'reverted', error: 'execution reverted', revertData: revert }
314
+ }
315
+ }
316
+ return result
317
+ }
318
+
319
+ const logInterface = new Interface(entryPointInterface.fragments)
320
+
321
+ function parseLog(log: { topics: readonly string[]; data: string }) {
322
+ try {
323
+ return logInterface.parseLog({ topics: [...log.topics], data: log.data })
324
+ } catch {
325
+ return null
326
+ }
327
+ }
328
+
329
+ /** EntryPoint 的 FailedOp(opIndex, reason):reason 形如 'AA24 signature error' */
330
+ function decodeFailedOp(data: string): string | null {
331
+ try {
332
+ const e = entryPointInterface.parseError(data)
333
+ if (e?.name === 'FailedOp' || e?.name === 'FailedOpWithRevert') {
334
+ const inner = e.name === 'FailedOpWithRevert' ? decodeRevertReason(String(e.args[2])) : null
335
+ return `EntryPoint rejected the UserOperation: ${e.args[1]}${inner ? ` (${inner})` : ''}`
336
+ }
337
+ } catch {
338
+ // 不是 EntryPoint 的错误
339
+ }
340
+ return null
341
+ }