@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.
- package/CHANGELOG.md +30 -0
- package/README.md +206 -1
- package/dist/dividends.d.ts +135 -0
- package/dist/dividends.d.ts.map +1 -0
- package/dist/dividends.js +253 -0
- package/dist/dividends.js.map +1 -0
- package/dist/history.d.ts +154 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +247 -0
- package/dist/history.js.map +1 -0
- package/dist/historySources.d.ts +32 -0
- package/dist/historySources.d.ts.map +1 -0
- package/dist/historySources.js +319 -0
- package/dist/historySources.js.map +1 -0
- package/dist/liquidity/v3.d.ts +2 -0
- package/dist/liquidity/v3.d.ts.map +1 -1
- package/dist/liquidity/v3.js +1 -1
- package/dist/liquidity/v3.js.map +1 -1
- package/dist/liquidity/v4.d.ts +21 -0
- package/dist/liquidity/v4.d.ts.map +1 -1
- package/dist/liquidity/v4.js +4 -3
- package/dist/liquidity/v4.js.map +1 -1
- package/dist/lp.d.ts +8 -0
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +17 -1
- package/dist/lp.js.map +1 -1
- package/dist/lpV4.d.ts +4 -0
- package/dist/lpV4.d.ts.map +1 -1
- package/dist/lpV4.js +27 -3
- package/dist/lpV4.js.map +1 -1
- package/dist/permit.d.ts +137 -0
- package/dist/permit.d.ts.map +1 -0
- package/dist/permit.js +355 -0
- package/dist/permit.js.map +1 -0
- package/dist/portfolio.d.ts +2 -0
- package/dist/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +9 -4
- package/dist/portfolio.js.map +1 -1
- package/dist/rewards/farms.d.ts +26 -0
- package/dist/rewards/farms.d.ts.map +1 -0
- package/dist/rewards/farms.js +210 -0
- package/dist/rewards/farms.js.map +1 -0
- package/dist/rewards/index.d.ts +42 -0
- package/dist/rewards/index.d.ts.map +1 -0
- package/dist/rewards/index.js +382 -0
- package/dist/rewards/index.js.map +1 -0
- package/dist/rewards/types.d.ts +170 -0
- package/dist/rewards/types.d.ts.map +1 -0
- package/dist/rewards/types.js +2 -0
- package/dist/rewards/types.js.map +1 -0
- package/dist/simulate.d.ts +186 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +559 -0
- package/dist/simulate.js.map +1 -0
- package/dist/simulateDecode.d.ts +33 -0
- package/dist/simulateDecode.d.ts.map +1 -0
- package/dist/simulateDecode.js +321 -0
- package/dist/simulateDecode.js.map +1 -0
- package/dist/smartAccount/account.d.ts +31 -0
- package/dist/smartAccount/account.d.ts.map +1 -0
- package/dist/smartAccount/account.js +33 -0
- package/dist/smartAccount/account.js.map +1 -0
- package/dist/smartAccount/clients.d.ts +77 -0
- package/dist/smartAccount/clients.d.ts.map +1 -0
- package/dist/smartAccount/clients.js +122 -0
- package/dist/smartAccount/clients.js.map +1 -0
- package/dist/smartAccount/eip5792.d.ts +77 -0
- package/dist/smartAccount/eip5792.d.ts.map +1 -0
- package/dist/smartAccount/eip5792.js +152 -0
- package/dist/smartAccount/eip5792.js.map +1 -0
- package/dist/smartAccount/send.d.ts +40 -0
- package/dist/smartAccount/send.d.ts.map +1 -0
- package/dist/smartAccount/send.js +271 -0
- package/dist/smartAccount/send.js.map +1 -0
- package/dist/smartAccount/userOp.d.ts +91 -0
- package/dist/smartAccount/userOp.d.ts.map +1 -0
- package/dist/smartAccount/userOp.js +124 -0
- package/dist/smartAccount/userOp.js.map +1 -0
- package/dist/subpaths/dividends.d.ts +28 -0
- package/dist/subpaths/dividends.d.ts.map +1 -0
- package/dist/subpaths/dividends.js +29 -0
- package/dist/subpaths/dividends.js.map +1 -0
- package/dist/subpaths/history.d.ts +22 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +23 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/permit.d.ts +14 -0
- package/dist/subpaths/permit.d.ts.map +1 -0
- package/dist/subpaths/permit.js +14 -0
- package/dist/subpaths/permit.js.map +1 -0
- package/dist/subpaths/price.js +2 -2
- package/dist/subpaths/price.js.map +1 -1
- package/dist/subpaths/rewards.d.ts +27 -0
- package/dist/subpaths/rewards.d.ts.map +1 -0
- package/dist/subpaths/rewards.js +25 -0
- package/dist/subpaths/rewards.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +25 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +28 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/smart-account.d.ts +24 -0
- package/dist/subpaths/smart-account.d.ts.map +1 -0
- package/dist/subpaths/smart-account.js +24 -0
- package/dist/subpaths/smart-account.js.map +1 -0
- package/dist/swap.d.ts +45 -5
- package/dist/swap.d.ts.map +1 -1
- package/dist/swap.js +108 -12
- package/dist/swap.js.map +1 -1
- package/dist/write/batch.d.ts +34 -2
- package/dist/write/batch.d.ts.map +1 -1
- package/dist/write/batch.js +22 -3
- package/dist/write/batch.js.map +1 -1
- package/dist/write/collect.d.ts +9 -0
- package/dist/write/collect.d.ts.map +1 -1
- package/dist/write/collect.js +41 -1
- package/dist/write/collect.js.map +1 -1
- package/dist/write/send.d.ts +4 -0
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js.map +1 -1
- package/dist/write/signer.d.ts +5 -1
- package/dist/write/signer.d.ts.map +1 -1
- package/dist/write/signer.js.map +1 -1
- package/dist/write/transfer.d.ts +5 -3
- package/dist/write/transfer.d.ts.map +1 -1
- package/dist/write/transfer.js +12 -6
- package/dist/write/transfer.js.map +1 -1
- package/package.json +35 -1
- package/src/dividends.ts +360 -0
- package/src/history.ts +367 -0
- package/src/historySources.ts +360 -0
- package/src/liquidity/v3.ts +1 -1
- package/src/liquidity/v4.ts +4 -3
- package/src/lp.ts +18 -1
- package/src/lpV4.ts +32 -2
- package/src/permit.ts +457 -0
- package/src/portfolio.ts +11 -4
- package/src/rewards/farms.ts +229 -0
- package/src/rewards/index.ts +431 -0
- package/src/rewards/types.ts +179 -0
- package/src/simulate.ts +727 -0
- package/src/simulateDecode.ts +352 -0
- package/src/smartAccount/account.ts +63 -0
- package/src/smartAccount/clients.ts +180 -0
- package/src/smartAccount/eip5792.ts +187 -0
- package/src/smartAccount/send.ts +341 -0
- package/src/smartAccount/userOp.ts +180 -0
- package/src/subpaths/dividends.ts +47 -0
- package/src/subpaths/history.ts +41 -0
- package/src/subpaths/permit.ts +39 -0
- package/src/subpaths/price.ts +2 -2
- package/src/subpaths/rewards.ts +46 -0
- package/src/subpaths/simulate.ts +51 -0
- package/src/subpaths/smart-account.ts +40 -0
- package/src/swap.ts +139 -18
- package/src/write/batch.ts +44 -5
- package/src/write/collect.ts +60 -1
- package/src/write/send.ts +4 -0
- package/src/write/signer.ts +5 -1
- 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
|
+
}
|