@pimlico/alto 0.0.0-main.20250620T170042 → 0.0.0-main.20250622T171316
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/esm/cli/setupServer.js +20 -11
- package/esm/cli/setupServer.js.map +1 -1
- package/esm/executor/executor.js +20 -16
- package/esm/executor/executor.js.map +1 -1
- package/esm/executor/executorManager.d.ts +14 -85
- package/esm/executor/executorManager.js +114 -527
- package/esm/executor/executorManager.js.map +1 -1
- package/esm/executor/getBundleStatus.d.ts +25 -0
- package/esm/executor/getBundleStatus.js +35 -0
- package/esm/executor/getBundleStatus.js.map +1 -0
- package/esm/executor/userOpMonitor.d.ts +92 -0
- package/esm/executor/userOpMonitor.js +299 -0
- package/esm/executor/userOpMonitor.js.map +1 -0
- package/esm/mempool/mempool.d.ts +19 -16
- package/esm/mempool/mempool.js +48 -39
- package/esm/mempool/mempool.js.map +1 -1
- package/esm/rpc/methods/debug_bundler_sendBundleNow.js +9 -1
- package/esm/rpc/methods/debug_bundler_sendBundleNow.js.map +1 -1
- package/esm/rpc/methods/eth_getUserOperationReceipt.js +1 -1
- package/esm/rpc/methods/eth_getUserOperationReceipt.js.map +1 -1
- package/esm/rpc/methods/pimlico_getUserOperationStatus.d.ts +6 -6
- package/esm/rpc/rpcHandler.d.ts +4 -1
- package/esm/rpc/rpcHandler.js +3 -1
- package/esm/rpc/rpcHandler.js.map +1 -1
- package/esm/store/createMempoolStore.js +3 -3
- package/esm/store/createMempoolStore.js.map +1 -1
- package/esm/store/createRedisStore.d.ts +3 -3
- package/esm/store/createRedisStore.js.map +1 -1
- package/esm/store/createStore.d.ts +3 -3
- package/esm/store/createStore.js.map +1 -1
- package/esm/store/index.d.ts +8 -13
- package/esm/store/index.js.map +1 -1
- package/esm/types/mempool.d.ts +7 -29
- package/esm/types/mempool.js +1 -7
- package/esm/types/mempool.js.map +1 -1
- package/esm/types/schemas.d.ts +25 -57
- package/esm/types/schemas.js +4 -6
- package/esm/types/schemas.js.map +1 -1
- package/esm/utils/userop.d.ts +1 -30
- package/esm/utils/userop.js +62 -161
- package/esm/utils/userop.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import { EntryPointV06Abi } from "../types/index.js";
|
|
2
|
-
import { getAAError, getBundleStatus, parseUserOperationReceipt, jsonStringifyWithBigint } from "../utils/index.js";
|
|
3
|
-
import { TransactionReceiptNotFoundError, getAbiItem, NonceTooLowError } from "viem";
|
|
4
|
-
import { BaseError } from "abitype";
|
|
5
|
-
import { getUserOpHashes } from "./utils.js";
|
|
6
|
-
function getTransactionsFromUserOperationEntries(submittedOps) {
|
|
7
|
-
const transactionInfos = submittedOps.map((userOpInfo) => userOpInfo.transactionInfo);
|
|
8
|
-
// Remove duplicates
|
|
9
|
-
return Array.from(new Set(transactionInfos));
|
|
10
|
-
}
|
|
11
1
|
const SCALE_FACTOR = 10; // Interval increases by 10ms per task per minute
|
|
12
2
|
const RPM_WINDOW = 60000; // 1 minute window in ms
|
|
13
3
|
export class ExecutorManager {
|
|
@@ -15,47 +5,26 @@ export class ExecutorManager {
|
|
|
15
5
|
config;
|
|
16
6
|
executor;
|
|
17
7
|
mempool;
|
|
18
|
-
monitor;
|
|
19
8
|
logger;
|
|
20
9
|
metrics;
|
|
21
|
-
reputationManager;
|
|
22
|
-
unWatch;
|
|
23
10
|
gasPriceManager;
|
|
24
|
-
eventManager;
|
|
25
11
|
opsCount = [];
|
|
26
12
|
bundlingMode;
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
userOpMonitor;
|
|
14
|
+
unWatch;
|
|
29
15
|
currentlyHandlingBlock = false;
|
|
30
|
-
|
|
31
|
-
const now = Date.now();
|
|
32
|
-
if (this.cachedLatestBlock &&
|
|
33
|
-
now - this.cachedLatestBlock.timestamp < this.blockCacheTTL) {
|
|
34
|
-
// Use cached block number if it's still valid
|
|
35
|
-
this.logger.debug("Using cached block number");
|
|
36
|
-
return this.cachedLatestBlock.value;
|
|
37
|
-
}
|
|
38
|
-
// Otherwise fetch a new block number and cache it
|
|
39
|
-
const latestBlock = await this.config.publicClient.getBlockNumber();
|
|
40
|
-
this.cachedLatestBlock = { value: latestBlock, timestamp: now };
|
|
41
|
-
this.logger.debug("Fetched and cached new block number");
|
|
42
|
-
return latestBlock;
|
|
43
|
-
}
|
|
44
|
-
constructor({ config, executor, mempool, monitor, reputationManager, metrics, gasPriceManager, eventManager, senderManager }) {
|
|
16
|
+
constructor({ config, executor, mempool, metrics, gasPriceManager, senderManager, userOpMonitor }) {
|
|
45
17
|
this.config = config;
|
|
46
|
-
this.reputationManager = reputationManager;
|
|
47
18
|
this.executor = executor;
|
|
48
19
|
this.mempool = mempool;
|
|
49
|
-
this.monitor = monitor;
|
|
50
20
|
this.logger = config.getLogger({ module: "executor_manager" }, {
|
|
51
21
|
level: config.executorLogLevel || config.logLevel
|
|
52
22
|
});
|
|
53
23
|
this.metrics = metrics;
|
|
54
24
|
this.gasPriceManager = gasPriceManager;
|
|
55
|
-
this.eventManager = eventManager;
|
|
56
25
|
this.senderManager = senderManager;
|
|
57
|
-
this.blockCacheTTL = config.blockNumberCacheTtl;
|
|
58
26
|
this.bundlingMode = this.config.bundleMode;
|
|
27
|
+
this.userOpMonitor = userOpMonitor;
|
|
59
28
|
if (this.bundlingMode === "auto") {
|
|
60
29
|
this.autoScalingBundling();
|
|
61
30
|
}
|
|
@@ -74,14 +43,13 @@ export class ExecutorManager {
|
|
|
74
43
|
this.opsCount = this.opsCount.filter((timestamp) => now - timestamp < RPM_WINDOW);
|
|
75
44
|
const bundles = await this.mempool.getBundles();
|
|
76
45
|
if (bundles.length > 0) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
bundles.map((bundle) => this.sendBundleToExecutor(bundle));
|
|
46
|
+
// Count total ops and add timestamps
|
|
47
|
+
const totalOps = bundles.reduce((sum, bundle) => sum + bundle.userOps.length, 0);
|
|
48
|
+
this.opsCount.push(...Array(totalOps).fill(Date.now()));
|
|
49
|
+
}
|
|
50
|
+
// Send bundles to executor
|
|
51
|
+
for (const bundle of bundles) {
|
|
52
|
+
this.sendBundleToExecutor(bundle);
|
|
85
53
|
}
|
|
86
54
|
const rpm = this.opsCount.length;
|
|
87
55
|
// Calculate next interval with linear scaling
|
|
@@ -92,12 +60,14 @@ export class ExecutorManager {
|
|
|
92
60
|
setTimeout(this.autoScalingBundling.bind(this), nextInterval);
|
|
93
61
|
}
|
|
94
62
|
}
|
|
95
|
-
startWatchingBlocks(
|
|
63
|
+
startWatchingBlocks() {
|
|
96
64
|
if (this.unWatch) {
|
|
97
65
|
return;
|
|
98
66
|
}
|
|
99
67
|
this.unWatch = this.config.publicClient.watchBlockNumber({
|
|
100
|
-
onBlockNumber:
|
|
68
|
+
onBlockNumber: async (blockNumber) => {
|
|
69
|
+
await this.handleBlock(blockNumber);
|
|
70
|
+
},
|
|
101
71
|
onError: (error) => {
|
|
102
72
|
this.logger.error({ error }, "error while watching blocks");
|
|
103
73
|
},
|
|
@@ -106,19 +76,6 @@ export class ExecutorManager {
|
|
|
106
76
|
});
|
|
107
77
|
this.logger.debug("started watching blocks");
|
|
108
78
|
}
|
|
109
|
-
// Debug endpoint
|
|
110
|
-
async sendBundleNow() {
|
|
111
|
-
const bundles = await this.mempool.getBundles(1);
|
|
112
|
-
const bundle = bundles[0];
|
|
113
|
-
if (bundles.length === 0 || bundle.userOps.length === 0) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const txHash = await this.sendBundleToExecutor(bundle);
|
|
117
|
-
if (!txHash) {
|
|
118
|
-
throw new Error("no tx hash");
|
|
119
|
-
}
|
|
120
|
-
return txHash;
|
|
121
|
-
}
|
|
122
79
|
async getBaseFee() {
|
|
123
80
|
if (this.config.legacyTransactions) {
|
|
124
81
|
return 0n;
|
|
@@ -142,9 +99,13 @@ export class ExecutorManager {
|
|
|
142
99
|
return [];
|
|
143
100
|
});
|
|
144
101
|
if (!gasPriceParams || nonce === undefined) {
|
|
145
|
-
await this.resubmitUserOperations(userOps, entryPoint, "Failed to get nonce and gas parameters for bundling");
|
|
146
102
|
// Free executor if failed to get initial params.
|
|
147
103
|
await this.senderManager.markWalletProcessed(wallet);
|
|
104
|
+
await this.mempool.resubmitUserOps({
|
|
105
|
+
userOps,
|
|
106
|
+
entryPoint,
|
|
107
|
+
reason: "Failed to get nonce and gas parameters for bundling"
|
|
108
|
+
});
|
|
148
109
|
return undefined;
|
|
149
110
|
}
|
|
150
111
|
const bundleResult = await this.executor.bundle({
|
|
@@ -154,426 +115,144 @@ export class ExecutorManager {
|
|
|
154
115
|
networkBaseFee: baseFee,
|
|
155
116
|
nonce
|
|
156
117
|
});
|
|
157
|
-
|
|
158
|
-
|
|
118
|
+
if (!bundleResult.success) {
|
|
119
|
+
const { rejectedUserOps, recoverableOps, reason } = bundleResult;
|
|
120
|
+
// Free wallet as no bundle was sent.
|
|
159
121
|
await this.senderManager.markWalletProcessed(wallet);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const rejectedUserOps = userOps.map((userOp) => ({
|
|
170
|
-
...userOp,
|
|
171
|
-
reason: "filterOps simulation error"
|
|
172
|
-
}));
|
|
173
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
174
|
-
this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc();
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
// Resubmit if executor has insufficient funds.
|
|
178
|
-
if (bundleResult.status === "submission_insufficient_funds_error") {
|
|
179
|
-
const { userOpsToBundle, rejectedUserOps } = bundleResult;
|
|
180
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
181
|
-
await this.resubmitUserOperations(userOpsToBundle, entryPoint, "Executor has insufficient funds");
|
|
182
|
-
this.metrics.bundlesSubmitted.labels({ status: "resubmit" }).inc();
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
// Encountered unhandled error during bundle submission.
|
|
186
|
-
if (bundleResult.status === "submission_generic_error") {
|
|
187
|
-
const { rejectedUserOps, userOpsToBundle, reason } = bundleResult;
|
|
188
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
189
|
-
// NOTE: these ops passed validation, so we can try resubmitting them
|
|
190
|
-
await this.resubmitUserOperations(userOpsToBundle, entryPoint, reason instanceof BaseError
|
|
191
|
-
? reason.name
|
|
192
|
-
: "Encountered unhandled error during bundle submission");
|
|
193
|
-
this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc();
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
if (bundleResult.status === "submission_success") {
|
|
197
|
-
let { userOpsBundled, rejectedUserOps, transactionRequest, transactionHash } = bundleResult;
|
|
198
|
-
// Increment submission attempts for all userOps submitted.
|
|
199
|
-
userOpsBundled = userOpsBundled.map((userOpInfo) => ({
|
|
200
|
-
...userOpInfo,
|
|
201
|
-
submissionAttempts: userOpInfo.submissionAttempts + 1
|
|
202
|
-
}));
|
|
203
|
-
const transactionInfo = {
|
|
204
|
-
executor: wallet,
|
|
205
|
-
transactionHash,
|
|
206
|
-
transactionRequest,
|
|
207
|
-
bundle: {
|
|
122
|
+
// Drop rejected ops
|
|
123
|
+
await this.mempool.dropUserOps(entryPoint, rejectedUserOps);
|
|
124
|
+
this.metrics.userOperationsSubmitted
|
|
125
|
+
.labels({ status: "failed" })
|
|
126
|
+
.inc(rejectedUserOps.length);
|
|
127
|
+
// Handle recoverable ops
|
|
128
|
+
if (recoverableOps.length) {
|
|
129
|
+
await this.mempool.resubmitUserOps({
|
|
130
|
+
userOps: recoverableOps,
|
|
208
131
|
entryPoint,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
await this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo);
|
|
218
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
219
|
-
this.metrics.bundlesSubmitted.labels({ status: "success" }).inc();
|
|
220
|
-
return transactionHash;
|
|
132
|
+
reason
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (reason === "filterops_failed" || reason === "generic_error") {
|
|
136
|
+
this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc();
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
221
139
|
}
|
|
222
|
-
|
|
140
|
+
// Success case
|
|
141
|
+
let { userOpsBundled, rejectedUserOps, transactionRequest, transactionHash } = bundleResult;
|
|
142
|
+
// Increment submission attempts for all userOps submitted.
|
|
143
|
+
userOpsBundled = userOpsBundled.map((userOpInfo) => ({
|
|
144
|
+
...userOpInfo,
|
|
145
|
+
submissionAttempts: userOpInfo.submissionAttempts + 1
|
|
146
|
+
}));
|
|
147
|
+
const submittedBundle = {
|
|
148
|
+
executor: wallet,
|
|
149
|
+
transactionHash,
|
|
150
|
+
transactionRequest,
|
|
151
|
+
bundle: {
|
|
152
|
+
entryPoint,
|
|
153
|
+
version,
|
|
154
|
+
userOps: userOpsBundled,
|
|
155
|
+
submissionAttempts: 1
|
|
156
|
+
},
|
|
157
|
+
previousTransactionHashes: [],
|
|
158
|
+
lastReplaced: Date.now()
|
|
159
|
+
};
|
|
160
|
+
this.userOpMonitor.trackBundle(submittedBundle);
|
|
161
|
+
await this.mempool.markUserOpsAsSubmitted({
|
|
162
|
+
userOps: submittedBundle.bundle.userOps,
|
|
163
|
+
entryPoint: submittedBundle.bundle.entryPoint,
|
|
164
|
+
transactionHash: submittedBundle.transactionHash
|
|
165
|
+
});
|
|
166
|
+
// Start watching blocks after marking operations as submitted
|
|
167
|
+
this.startWatchingBlocks();
|
|
168
|
+
await this.mempool.dropUserOps(entryPoint, rejectedUserOps);
|
|
169
|
+
this.metrics.bundlesSubmitted.labels({ status: "success" }).inc();
|
|
170
|
+
return transactionHash;
|
|
223
171
|
}
|
|
224
172
|
stopWatchingBlocks() {
|
|
225
173
|
if (this.unWatch) {
|
|
226
|
-
this.logger.debug("stopped watching blocks");
|
|
227
174
|
this.unWatch();
|
|
228
175
|
this.unWatch = undefined;
|
|
229
176
|
}
|
|
230
177
|
}
|
|
231
|
-
// update the current status of the bundling transaction/s
|
|
232
|
-
async refreshTransactionStatus(transactionInfo) {
|
|
233
|
-
const { transactionHash: currentTxhash, bundle, previousTransactionHashes } = transactionInfo;
|
|
234
|
-
const { userOps, entryPoint } = bundle;
|
|
235
|
-
const txHashesToCheck = [currentTxhash, ...previousTransactionHashes];
|
|
236
|
-
const transactionDetails = await Promise.all(txHashesToCheck.map(async (transactionHash) => ({
|
|
237
|
-
transactionHash,
|
|
238
|
-
...(await getBundleStatus({
|
|
239
|
-
transactionHash,
|
|
240
|
-
bundle: transactionInfo.bundle,
|
|
241
|
-
publicClient: this.config.publicClient,
|
|
242
|
-
logger: this.logger
|
|
243
|
-
}))
|
|
244
|
-
})));
|
|
245
|
-
// first check if bundling txs returns status "mined", if not, check for reverted
|
|
246
|
-
const mined = transactionDetails.find(({ bundlingStatus }) => bundlingStatus.status === "included");
|
|
247
|
-
const reverted = transactionDetails.find(({ bundlingStatus }) => bundlingStatus.status === "reverted");
|
|
248
|
-
const finalizedTransaction = mined ?? reverted;
|
|
249
|
-
if (!finalizedTransaction) {
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const { bundlingStatus, transactionHash, blockNumber } = finalizedTransaction;
|
|
253
|
-
// Free executor if tx landed onchain
|
|
254
|
-
if (bundlingStatus.status !== "not_found") {
|
|
255
|
-
await this.senderManager.markWalletProcessed(transactionInfo.executor);
|
|
256
|
-
}
|
|
257
|
-
if (bundlingStatus.status === "included") {
|
|
258
|
-
const { userOperationDetails } = bundlingStatus;
|
|
259
|
-
await this.markUserOpsIncluded(userOps, entryPoint, blockNumber, transactionHash, userOperationDetails);
|
|
260
|
-
}
|
|
261
|
-
if (bundlingStatus.status === "reverted") {
|
|
262
|
-
await Promise.all(userOps.map(async (userOpInfo) => {
|
|
263
|
-
const { userOpHash } = userOpInfo;
|
|
264
|
-
await this.checkFrontrun({
|
|
265
|
-
entryPoint,
|
|
266
|
-
userOpHash,
|
|
267
|
-
transactionHash,
|
|
268
|
-
blockNumber
|
|
269
|
-
});
|
|
270
|
-
}));
|
|
271
|
-
await this.removeSubmitted(entryPoint, userOps);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async checkFrontrun({ userOpHash, entryPoint, transactionHash, blockNumber }) {
|
|
275
|
-
const unwatch = this.config.publicClient.watchBlockNumber({
|
|
276
|
-
onBlockNumber: async (currentBlockNumber) => {
|
|
277
|
-
if (currentBlockNumber > blockNumber + 1n) {
|
|
278
|
-
try {
|
|
279
|
-
const userOperationReceipt = await this.getUserOperationReceipt(userOpHash);
|
|
280
|
-
if (userOperationReceipt) {
|
|
281
|
-
const transactionHash = userOperationReceipt.receipt.transactionHash;
|
|
282
|
-
const blockNumber = userOperationReceipt.receipt.blockNumber;
|
|
283
|
-
await this.mempool.removeSubmitted({
|
|
284
|
-
entryPoint,
|
|
285
|
-
userOpHash
|
|
286
|
-
});
|
|
287
|
-
await this.monitor.setUserOperationStatus(userOpHash, {
|
|
288
|
-
status: "included",
|
|
289
|
-
transactionHash
|
|
290
|
-
});
|
|
291
|
-
this.eventManager.emitFrontranOnChain(userOpHash, transactionHash, blockNumber);
|
|
292
|
-
this.logger.info({
|
|
293
|
-
userOpHash,
|
|
294
|
-
transactionHash
|
|
295
|
-
}, "user op frontrun onchain");
|
|
296
|
-
this.metrics.userOperationsOnChain
|
|
297
|
-
.labels({ status: "frontran" })
|
|
298
|
-
.inc(1);
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
await this.monitor.setUserOperationStatus(userOpHash, {
|
|
302
|
-
status: "failed",
|
|
303
|
-
transactionHash
|
|
304
|
-
});
|
|
305
|
-
this.eventManager.emitFailedOnChain(userOpHash, transactionHash, blockNumber);
|
|
306
|
-
this.logger.info({
|
|
307
|
-
userOpHash,
|
|
308
|
-
transactionHash
|
|
309
|
-
}, "user op failed onchain");
|
|
310
|
-
this.metrics.userOperationsOnChain
|
|
311
|
-
.labels({ status: "reverted" })
|
|
312
|
-
.inc(1);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
catch (error) {
|
|
316
|
-
this.logger.error({
|
|
317
|
-
userOpHash,
|
|
318
|
-
transactionHash,
|
|
319
|
-
error
|
|
320
|
-
}, "Error checking frontrun status");
|
|
321
|
-
// Still mark as failed since we couldn't verify inclusion
|
|
322
|
-
await this.monitor.setUserOperationStatus(userOpHash, {
|
|
323
|
-
status: "failed",
|
|
324
|
-
transactionHash
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
unwatch();
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
async getUserOperationReceipt(userOperationHash) {
|
|
333
|
-
const userOperationEventAbiItem = getAbiItem({
|
|
334
|
-
abi: EntryPointV06Abi,
|
|
335
|
-
name: "UserOperationEvent"
|
|
336
|
-
});
|
|
337
|
-
let fromBlock = undefined;
|
|
338
|
-
let toBlock = undefined;
|
|
339
|
-
if (this.config.maxBlockRange !== undefined) {
|
|
340
|
-
const latestBlock = await this.getLatestBlockWithCache();
|
|
341
|
-
fromBlock = latestBlock - BigInt(this.config.maxBlockRange);
|
|
342
|
-
if (fromBlock < 0n) {
|
|
343
|
-
fromBlock = 0n;
|
|
344
|
-
}
|
|
345
|
-
toBlock = "latest";
|
|
346
|
-
}
|
|
347
|
-
const filterResult = await this.config.publicClient.getLogs({
|
|
348
|
-
address: this.config.entrypoints,
|
|
349
|
-
event: userOperationEventAbiItem,
|
|
350
|
-
fromBlock,
|
|
351
|
-
toBlock,
|
|
352
|
-
args: {
|
|
353
|
-
userOpHash: userOperationHash
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
this.logger.debug({
|
|
357
|
-
filterResult: filterResult.length,
|
|
358
|
-
userOperationEvent: filterResult.length === 0
|
|
359
|
-
? undefined
|
|
360
|
-
: filterResult[0].transactionHash
|
|
361
|
-
}, "filter result length");
|
|
362
|
-
if (filterResult.length === 0) {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
const userOperationEvent = filterResult[0];
|
|
366
|
-
// throw if any of the members of userOperationEvent are undefined
|
|
367
|
-
if (userOperationEvent.args.actualGasCost === undefined ||
|
|
368
|
-
userOperationEvent.args.sender === undefined ||
|
|
369
|
-
userOperationEvent.args.nonce === undefined ||
|
|
370
|
-
userOperationEvent.args.userOpHash === undefined ||
|
|
371
|
-
userOperationEvent.args.success === undefined ||
|
|
372
|
-
userOperationEvent.args.paymaster === undefined ||
|
|
373
|
-
userOperationEvent.args.actualGasUsed === undefined) {
|
|
374
|
-
throw new Error("userOperationEvent has undefined members");
|
|
375
|
-
}
|
|
376
|
-
const txHash = userOperationEvent.transactionHash;
|
|
377
|
-
if (txHash === null) {
|
|
378
|
-
// transaction pending
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
const getTransactionReceipt = async (txHash) => {
|
|
382
|
-
while (true) {
|
|
383
|
-
try {
|
|
384
|
-
const transactionReceipt = await this.config.publicClient.getTransactionReceipt({
|
|
385
|
-
hash: txHash
|
|
386
|
-
});
|
|
387
|
-
let effectiveGasPrice = transactionReceipt.effectiveGasPrice ??
|
|
388
|
-
transactionReceipt.gasPrice ??
|
|
389
|
-
undefined;
|
|
390
|
-
if (effectiveGasPrice === undefined) {
|
|
391
|
-
const tx = await this.config.publicClient.getTransaction({
|
|
392
|
-
hash: txHash
|
|
393
|
-
});
|
|
394
|
-
effectiveGasPrice = tx.gasPrice ?? undefined;
|
|
395
|
-
}
|
|
396
|
-
if (effectiveGasPrice) {
|
|
397
|
-
transactionReceipt.effectiveGasPrice = effectiveGasPrice;
|
|
398
|
-
}
|
|
399
|
-
return transactionReceipt;
|
|
400
|
-
}
|
|
401
|
-
catch (e) {
|
|
402
|
-
if (e instanceof TransactionReceiptNotFoundError) {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
throw e;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
const receipt = await getTransactionReceipt(txHash);
|
|
410
|
-
const logs = receipt.logs;
|
|
411
|
-
if (logs.some((log) => log.blockHash === null ||
|
|
412
|
-
log.blockNumber === null ||
|
|
413
|
-
log.transactionIndex === null ||
|
|
414
|
-
log.transactionHash === null ||
|
|
415
|
-
log.logIndex === null ||
|
|
416
|
-
log.topics.length === 0)) {
|
|
417
|
-
// transaction pending
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
const userOperationReceipt = parseUserOperationReceipt(userOperationHash, receipt);
|
|
421
|
-
return userOperationReceipt;
|
|
422
|
-
}
|
|
423
178
|
async handleBlock(blockNumber) {
|
|
424
|
-
// Update the cached block number whenever we receive a new block
|
|
425
|
-
this.cachedLatestBlock = { value: blockNumber, timestamp: Date.now() };
|
|
426
179
|
if (this.currentlyHandlingBlock) {
|
|
427
180
|
return;
|
|
428
181
|
}
|
|
429
182
|
this.currentlyHandlingBlock = true;
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
for (const entryPoint of this.config.entrypoints) {
|
|
434
|
-
const entries = await this.mempool.dumpSubmittedOps(entryPoint);
|
|
435
|
-
submittedEntries.push(...entries);
|
|
436
|
-
}
|
|
437
|
-
return submittedEntries;
|
|
438
|
-
};
|
|
439
|
-
const submittedEntries = await dumpSubmittedEntries();
|
|
440
|
-
if (submittedEntries.length === 0) {
|
|
183
|
+
// Process the block and get the results
|
|
184
|
+
const pendingBundles = await this.userOpMonitor.processBlock(blockNumber);
|
|
185
|
+
if (pendingBundles.length === 0) {
|
|
441
186
|
this.stopWatchingBlocks();
|
|
442
187
|
this.currentlyHandlingBlock = false;
|
|
443
188
|
return;
|
|
444
189
|
}
|
|
445
|
-
// refresh op statuses
|
|
446
|
-
const ops = await dumpSubmittedEntries();
|
|
447
|
-
const txs = getTransactionsFromUserOperationEntries(ops);
|
|
448
|
-
await Promise.all(txs.map((txInfo) => this.refreshTransactionStatus(txInfo)));
|
|
449
190
|
// for all still not included check if needs to be replaced (based on gas price)
|
|
450
|
-
const [
|
|
191
|
+
const [networkGasPrice, networkBaseFee] = await Promise.all([
|
|
451
192
|
this.gasPriceManager.tryGetNetworkGasPrice().catch(() => ({
|
|
452
193
|
maxFeePerGas: 0n,
|
|
453
194
|
maxPriorityFeePerGas: 0n
|
|
454
195
|
})),
|
|
455
196
|
this.getBaseFee().catch(() => 0n)
|
|
456
197
|
]);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const { transactionRequest } = txInfo;
|
|
198
|
+
await Promise.all(pendingBundles.map(async (submittedBundle) => {
|
|
199
|
+
const { transactionRequest, lastReplaced } = submittedBundle;
|
|
460
200
|
const { maxFeePerGas, maxPriorityFeePerGas } = transactionRequest;
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
const isStuck = Date.now() -
|
|
464
|
-
|
|
465
|
-
if (isMaxFeeTooLow || isPriorityFeeTooLow) {
|
|
201
|
+
const isGasPriceTooLow = maxFeePerGas < networkGasPrice.maxFeePerGas ||
|
|
202
|
+
maxPriorityFeePerGas < networkGasPrice.maxPriorityFeePerGas;
|
|
203
|
+
const isStuck = Date.now() - lastReplaced > this.config.resubmitStuckTimeout;
|
|
204
|
+
if (isGasPriceTooLow) {
|
|
466
205
|
await this.replaceTransaction({
|
|
467
|
-
|
|
468
|
-
|
|
206
|
+
submittedBundle,
|
|
207
|
+
networkGasPrice,
|
|
469
208
|
networkBaseFee,
|
|
470
209
|
reason: "gas_price"
|
|
471
210
|
});
|
|
472
|
-
return;
|
|
473
211
|
}
|
|
474
|
-
if (isStuck) {
|
|
212
|
+
else if (isStuck) {
|
|
475
213
|
await this.replaceTransaction({
|
|
476
|
-
|
|
477
|
-
|
|
214
|
+
submittedBundle,
|
|
215
|
+
networkGasPrice,
|
|
478
216
|
networkBaseFee,
|
|
479
217
|
reason: "stuck"
|
|
480
218
|
});
|
|
481
|
-
return;
|
|
482
219
|
}
|
|
483
220
|
}));
|
|
484
221
|
this.currentlyHandlingBlock = false;
|
|
485
222
|
}
|
|
486
|
-
async replaceTransaction({
|
|
487
|
-
|
|
488
|
-
const { bundle, executor, transactionRequest, transactionHash: oldTxHash } = txInfo;
|
|
223
|
+
async replaceTransaction({ submittedBundle, networkGasPrice, networkBaseFee, reason }) {
|
|
224
|
+
const { bundle, executor, transactionRequest, transactionHash: oldTxHash } = submittedBundle;
|
|
489
225
|
const { entryPoint } = bundle;
|
|
490
226
|
const bundleResult = await this.executor.bundle({
|
|
491
227
|
executor: executor,
|
|
492
|
-
networkGasPrice
|
|
228
|
+
networkGasPrice,
|
|
493
229
|
networkBaseFee,
|
|
494
230
|
userOpBundle: bundle,
|
|
495
231
|
nonce: transactionRequest.nonce
|
|
496
232
|
});
|
|
497
|
-
//
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}, "
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
bundleResult.reason instanceof NonceTooLowError;
|
|
514
|
-
const allOpsFailedSimulation = bundleResult.status === "filterops_all_rejected" &&
|
|
515
|
-
bundleResult.rejectedUserOps.every((op) => op.reason === "AA25 invalid account nonce" ||
|
|
516
|
-
op.reason === "AA10 sender already constructed");
|
|
517
|
-
const potentiallyIncluded = nonceTooLow || allOpsFailedSimulation;
|
|
518
|
-
// log metrics
|
|
519
|
-
const replaceStatus = (() => {
|
|
520
|
-
switch (true) {
|
|
521
|
-
case potentiallyIncluded:
|
|
522
|
-
return "potentially_already_included";
|
|
523
|
-
case bundleResult?.status === "submission_success":
|
|
524
|
-
return "replaced";
|
|
525
|
-
default:
|
|
526
|
-
return "failed";
|
|
233
|
+
// Handle case where no bundle was sent.
|
|
234
|
+
if (!bundleResult.success) {
|
|
235
|
+
// Free wallet as no bundle was sent.
|
|
236
|
+
await this.senderManager.markWalletProcessed(executor);
|
|
237
|
+
await this.userOpMonitor.stopTrackingBundle(submittedBundle);
|
|
238
|
+
const { rejectedUserOps, recoverableOps, reason } = bundleResult;
|
|
239
|
+
this.logger.warn({ oldTxHash, reason }, "failed to replace transaction");
|
|
240
|
+
// Drop rejected ops
|
|
241
|
+
await this.mempool.dropUserOps(entryPoint, rejectedUserOps);
|
|
242
|
+
// Handle recoverable ops
|
|
243
|
+
if (recoverableOps.length) {
|
|
244
|
+
await this.mempool.resubmitUserOps({
|
|
245
|
+
userOps: recoverableOps,
|
|
246
|
+
entryPoint,
|
|
247
|
+
reason
|
|
248
|
+
});
|
|
527
249
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
.inc();
|
|
532
|
-
if (potentiallyIncluded) {
|
|
533
|
-
this.logger.info({
|
|
534
|
-
oldTxHash,
|
|
535
|
-
userOpHashes: getUserOpHashes(bundleResult.rejectedUserOps)
|
|
536
|
-
}, "transaction potentially already included");
|
|
537
|
-
txInfo.timesPotentiallyIncluded += 1;
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
if (bundleResult.status === "filterops_unhandled_error") {
|
|
541
|
-
const { rejectedUserOps } = bundleResult;
|
|
542
|
-
await this.failedToReplaceTransaction({
|
|
543
|
-
entryPoint,
|
|
544
|
-
oldTxHash,
|
|
545
|
-
reason: "filterOps simulation error",
|
|
546
|
-
rejectedUserOps
|
|
547
|
-
});
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
if (bundleResult.status === "filterops_all_rejected") {
|
|
551
|
-
await this.failedToReplaceTransaction({
|
|
552
|
-
entryPoint,
|
|
553
|
-
oldTxHash,
|
|
554
|
-
reason: "all ops failed simulation",
|
|
555
|
-
rejectedUserOps: bundleResult.rejectedUserOps
|
|
556
|
-
});
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (bundleResult.status === "submission_generic_error") {
|
|
560
|
-
const { reason, rejectedUserOps } = bundleResult;
|
|
561
|
-
const submissionFailureReason = reason instanceof BaseError ? reason.name : "INTERNAL FAILURE";
|
|
562
|
-
await this.failedToReplaceTransaction({
|
|
563
|
-
oldTxHash,
|
|
564
|
-
rejectedUserOps,
|
|
565
|
-
reason: submissionFailureReason,
|
|
566
|
-
entryPoint
|
|
567
|
-
});
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
if (bundleResult.status === "submission_insufficient_funds_error") {
|
|
571
|
-
const { userOpsToBundle, rejectedUserOps } = bundleResult;
|
|
572
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
573
|
-
await this.resubmitUserOperations(userOpsToBundle, entryPoint, "Executor has insufficient funds");
|
|
574
|
-
this.metrics.bundlesSubmitted.labels({ status: "resubmit" }).inc();
|
|
250
|
+
this.metrics.replacedTransactions
|
|
251
|
+
.labels({ reason, status: "failed" })
|
|
252
|
+
.inc();
|
|
575
253
|
return;
|
|
576
254
|
}
|
|
255
|
+
// Success case
|
|
577
256
|
const { rejectedUserOps, userOpsBundled, transactionRequest: newTransactionRequest, transactionHash: newTxHash } = bundleResult;
|
|
578
257
|
// Increment submission attempts for all replaced userOps
|
|
579
258
|
const userOpsReplaced = userOpsBundled.map((userOpInfo) => ({
|
|
@@ -581,12 +260,12 @@ export class ExecutorManager {
|
|
|
581
260
|
submissionAttempts: userOpInfo.submissionAttempts + 1
|
|
582
261
|
}));
|
|
583
262
|
const newTxInfo = {
|
|
584
|
-
...
|
|
263
|
+
...submittedBundle,
|
|
585
264
|
transactionRequest: newTransactionRequest,
|
|
586
265
|
transactionHash: newTxHash,
|
|
587
266
|
previousTransactionHashes: [
|
|
588
|
-
|
|
589
|
-
...
|
|
267
|
+
submittedBundle.transactionHash,
|
|
268
|
+
...submittedBundle.previousTransactionHashes
|
|
590
269
|
],
|
|
591
270
|
lastReplaced: Date.now(),
|
|
592
271
|
bundle: {
|
|
@@ -595,9 +274,10 @@ export class ExecutorManager {
|
|
|
595
274
|
submissionAttempts: bundle.submissionAttempts + 1
|
|
596
275
|
}
|
|
597
276
|
};
|
|
598
|
-
|
|
277
|
+
// Replace existing submitted bundle with new one
|
|
278
|
+
this.userOpMonitor.trackBundle(newTxInfo);
|
|
599
279
|
// Drop all userOperations that were rejected during simulation.
|
|
600
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
280
|
+
await this.mempool.dropUserOps(entryPoint, rejectedUserOps);
|
|
601
281
|
this.logger.info({
|
|
602
282
|
oldTxHash,
|
|
603
283
|
newTxHash,
|
|
@@ -605,98 +285,5 @@ export class ExecutorManager {
|
|
|
605
285
|
}, "replaced transaction");
|
|
606
286
|
return;
|
|
607
287
|
}
|
|
608
|
-
async markUserOperationsAsReplaced(userOpsReplaced, newTxInfo) {
|
|
609
|
-
// Mark as replaced in mempool
|
|
610
|
-
await Promise.all(userOpsReplaced.map(async (userOpInfo) => {
|
|
611
|
-
await this.mempool.replaceSubmitted({
|
|
612
|
-
userOpInfo,
|
|
613
|
-
transactionInfo: newTxInfo
|
|
614
|
-
});
|
|
615
|
-
}));
|
|
616
|
-
}
|
|
617
|
-
async markUserOperationsAsSubmitted(userOpInfos, transactionInfo) {
|
|
618
|
-
await Promise.all(userOpInfos.map(async (userOpInfo) => {
|
|
619
|
-
const { userOpHash } = userOpInfo;
|
|
620
|
-
await this.mempool.markSubmitted({
|
|
621
|
-
userOpHash,
|
|
622
|
-
transactionInfo
|
|
623
|
-
});
|
|
624
|
-
this.startWatchingBlocks(this.handleBlock.bind(this));
|
|
625
|
-
this.metrics.userOperationsSubmitted
|
|
626
|
-
.labels({ status: "success" })
|
|
627
|
-
.inc();
|
|
628
|
-
}));
|
|
629
|
-
}
|
|
630
|
-
async resubmitUserOperations(userOps, entryPoint, reason) {
|
|
631
|
-
await Promise.all(userOps.map(async (userOpInfo) => {
|
|
632
|
-
const { userOpHash, userOp } = userOpInfo;
|
|
633
|
-
this.logger.warn({
|
|
634
|
-
userOpHash,
|
|
635
|
-
reason
|
|
636
|
-
}, "resubmitting user operation");
|
|
637
|
-
await this.mempool.removeProcessing({ entryPoint, userOpHash });
|
|
638
|
-
await this.mempool.add(userOp, entryPoint);
|
|
639
|
-
this.metrics.userOperationsResubmitted.inc();
|
|
640
|
-
}));
|
|
641
|
-
}
|
|
642
|
-
async failedToReplaceTransaction({ oldTxHash, rejectedUserOps, reason, entryPoint }) {
|
|
643
|
-
this.logger.warn({ oldTxHash, reason }, "failed to replace transaction");
|
|
644
|
-
await this.dropUserOps(entryPoint, rejectedUserOps);
|
|
645
|
-
}
|
|
646
|
-
async removeSubmitted(entryPoint, userOps) {
|
|
647
|
-
await Promise.all(userOps.map(async (userOpInfo) => {
|
|
648
|
-
const { userOpHash } = userOpInfo;
|
|
649
|
-
await this.mempool.removeSubmitted({ entryPoint, userOpHash });
|
|
650
|
-
}));
|
|
651
|
-
}
|
|
652
|
-
async markUserOpsIncluded(userOps, entryPoint, blockNumber, transactionHash, userOperationDetails) {
|
|
653
|
-
await Promise.all(userOps.map(async (userOpInfo) => {
|
|
654
|
-
this.metrics.userOperationsOnChain
|
|
655
|
-
.labels({ status: "included" })
|
|
656
|
-
.inc();
|
|
657
|
-
const { userOpHash, userOp, submissionAttempts } = userOpInfo;
|
|
658
|
-
const opDetails = userOperationDetails[userOpHash];
|
|
659
|
-
const firstSubmitted = userOpInfo.addedToMempool;
|
|
660
|
-
this.metrics.userOperationInclusionDuration.observe((Date.now() - firstSubmitted) / 1000);
|
|
661
|
-
// Track the number of submission attempts for included ops
|
|
662
|
-
this.metrics.userOperationsSubmissionAttempts.observe(submissionAttempts);
|
|
663
|
-
await this.mempool.removeSubmitted({ entryPoint, userOpHash });
|
|
664
|
-
this.reputationManager.updateUserOperationIncludedStatus(userOp, entryPoint, opDetails.accountDeployed);
|
|
665
|
-
if (opDetails.status === "succesful") {
|
|
666
|
-
this.eventManager.emitIncludedOnChain(userOpHash, transactionHash, blockNumber);
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
this.eventManager.emitExecutionRevertedOnChain(userOpHash, transactionHash, opDetails.revertReason || "0x", blockNumber);
|
|
670
|
-
}
|
|
671
|
-
await this.monitor.setUserOperationStatus(userOpHash, {
|
|
672
|
-
status: "included",
|
|
673
|
-
transactionHash
|
|
674
|
-
});
|
|
675
|
-
this.logger.info({
|
|
676
|
-
opHash: userOpHash,
|
|
677
|
-
transactionHash
|
|
678
|
-
}, "user op included");
|
|
679
|
-
}));
|
|
680
|
-
}
|
|
681
|
-
async dropUserOps(entryPoint, rejectedUserOps) {
|
|
682
|
-
await Promise.all(rejectedUserOps.map(async (rejectedUserOp) => {
|
|
683
|
-
const { userOp, reason, userOpHash } = rejectedUserOp;
|
|
684
|
-
await this.mempool.removeProcessing({ entryPoint, userOpHash });
|
|
685
|
-
await this.mempool.removeSubmitted({ entryPoint, userOpHash });
|
|
686
|
-
this.eventManager.emitDropped(userOpHash, reason, getAAError(reason));
|
|
687
|
-
await this.monitor.setUserOperationStatus(userOpHash, {
|
|
688
|
-
status: "rejected",
|
|
689
|
-
transactionHash: null
|
|
690
|
-
});
|
|
691
|
-
this.logger.warn({
|
|
692
|
-
userOperation: jsonStringifyWithBigint(userOp),
|
|
693
|
-
userOpHash,
|
|
694
|
-
reason
|
|
695
|
-
}, "user operation rejected");
|
|
696
|
-
this.metrics.userOperationsSubmitted
|
|
697
|
-
.labels({ status: "failed" })
|
|
698
|
-
.inc();
|
|
699
|
-
}));
|
|
700
|
-
}
|
|
701
288
|
}
|
|
702
289
|
//# sourceMappingURL=executorManager.js.map
|