@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.
Files changed (42) hide show
  1. package/esm/cli/setupServer.js +20 -11
  2. package/esm/cli/setupServer.js.map +1 -1
  3. package/esm/executor/executor.js +20 -16
  4. package/esm/executor/executor.js.map +1 -1
  5. package/esm/executor/executorManager.d.ts +14 -85
  6. package/esm/executor/executorManager.js +114 -527
  7. package/esm/executor/executorManager.js.map +1 -1
  8. package/esm/executor/getBundleStatus.d.ts +25 -0
  9. package/esm/executor/getBundleStatus.js +35 -0
  10. package/esm/executor/getBundleStatus.js.map +1 -0
  11. package/esm/executor/userOpMonitor.d.ts +92 -0
  12. package/esm/executor/userOpMonitor.js +299 -0
  13. package/esm/executor/userOpMonitor.js.map +1 -0
  14. package/esm/mempool/mempool.d.ts +19 -16
  15. package/esm/mempool/mempool.js +48 -39
  16. package/esm/mempool/mempool.js.map +1 -1
  17. package/esm/rpc/methods/debug_bundler_sendBundleNow.js +9 -1
  18. package/esm/rpc/methods/debug_bundler_sendBundleNow.js.map +1 -1
  19. package/esm/rpc/methods/eth_getUserOperationReceipt.js +1 -1
  20. package/esm/rpc/methods/eth_getUserOperationReceipt.js.map +1 -1
  21. package/esm/rpc/methods/pimlico_getUserOperationStatus.d.ts +6 -6
  22. package/esm/rpc/rpcHandler.d.ts +4 -1
  23. package/esm/rpc/rpcHandler.js +3 -1
  24. package/esm/rpc/rpcHandler.js.map +1 -1
  25. package/esm/store/createMempoolStore.js +3 -3
  26. package/esm/store/createMempoolStore.js.map +1 -1
  27. package/esm/store/createRedisStore.d.ts +3 -3
  28. package/esm/store/createRedisStore.js.map +1 -1
  29. package/esm/store/createStore.d.ts +3 -3
  30. package/esm/store/createStore.js.map +1 -1
  31. package/esm/store/index.d.ts +8 -13
  32. package/esm/store/index.js.map +1 -1
  33. package/esm/types/mempool.d.ts +7 -29
  34. package/esm/types/mempool.js +1 -7
  35. package/esm/types/mempool.js.map +1 -1
  36. package/esm/types/schemas.d.ts +25 -57
  37. package/esm/types/schemas.js +4 -6
  38. package/esm/types/schemas.js.map +1 -1
  39. package/esm/utils/userop.d.ts +1 -30
  40. package/esm/utils/userop.js +62 -161
  41. package/esm/utils/userop.js.map +1 -1
  42. 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
- cachedLatestBlock = null;
28
- blockCacheTTL;
13
+ userOpMonitor;
14
+ unWatch;
29
15
  currentlyHandlingBlock = false;
30
- async getLatestBlockWithCache() {
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
- const opsCount = bundles
78
- .map(({ userOps }) => userOps.length)
79
- .reduce((a, b) => a + b);
80
- // Add timestamps for each task
81
- const timestamp = Date.now();
82
- this.opsCount.push(...Array(opsCount).fill(timestamp));
83
- // Send bundles to executor
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(handleBlock) {
63
+ startWatchingBlocks() {
96
64
  if (this.unWatch) {
97
65
  return;
98
66
  }
99
67
  this.unWatch = this.config.publicClient.watchBlockNumber({
100
- onBlockNumber: handleBlock,
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
- // Free wallet if no bundle was sent.
158
- if (bundleResult.status !== "submission_success") {
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
- // All ops failed simulation, drop them and return.
162
- if (bundleResult.status === "filterops_all_rejected") {
163
- const { rejectedUserOps } = bundleResult;
164
- await this.dropUserOps(entryPoint, rejectedUserOps);
165
- return undefined;
166
- }
167
- // Unhandled error during simulation, drop all ops.
168
- if (bundleResult.status === "filterops_unhandled_error") {
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
- version,
210
- userOps: userOpsBundled,
211
- submissionAttempts: 1
212
- },
213
- previousTransactionHashes: [],
214
- lastReplaced: Date.now(),
215
- timesPotentiallyIncluded: 0
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
- return undefined;
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
- this.logger.debug({ blockNumber }, "handling block");
431
- const dumpSubmittedEntries = async () => {
432
- const submittedEntries = [];
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 [gasPriceParams, networkBaseFee] = await Promise.all([
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
- const transactionInfos = getTransactionsFromUserOperationEntries(await dumpSubmittedEntries());
458
- await Promise.all(transactionInfos.map(async (txInfo) => {
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 isMaxFeeTooLow = maxFeePerGas < gasPriceParams.maxFeePerGas;
462
- const isPriorityFeeTooLow = maxPriorityFeePerGas < gasPriceParams.maxPriorityFeePerGas;
463
- const isStuck = Date.now() - txInfo.lastReplaced >
464
- this.config.resubmitStuckTimeout;
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
- txInfo,
468
- gasPriceParams,
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
- txInfo,
477
- gasPriceParams,
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({ txInfo, gasPriceParams, networkBaseFee, reason }) {
487
- // Setup vars
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: gasPriceParams,
228
+ networkGasPrice,
493
229
  networkBaseFee,
494
230
  userOpBundle: bundle,
495
231
  nonce: transactionRequest.nonce
496
232
  });
497
- // Free wallet and return if potentially included too many times.
498
- if (txInfo.timesPotentiallyIncluded >= 3) {
499
- this.removeSubmitted(entryPoint, bundle.userOps);
500
- this.logger.warn({
501
- oldTxHash,
502
- userOps: getUserOpHashes(bundleResult.rejectedUserOps)
503
- }, "transaction potentially already included too many times, removing");
504
- await this.senderManager.markWalletProcessed(txInfo.executor);
505
- return;
506
- }
507
- // Free wallet if no bundle was sent or potentially included.
508
- if (bundleResult.status !== "submission_success") {
509
- await this.senderManager.markWalletProcessed(txInfo.executor);
510
- }
511
- // Check if the transaction is potentially included.
512
- const nonceTooLow = bundleResult.status === "submission_generic_error" &&
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
- this.metrics.replacedTransactions
530
- .labels({ reason, status: replaceStatus })
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
- ...txInfo,
263
+ ...submittedBundle,
585
264
  transactionRequest: newTransactionRequest,
586
265
  transactionHash: newTxHash,
587
266
  previousTransactionHashes: [
588
- txInfo.transactionHash,
589
- ...txInfo.previousTransactionHashes
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
- await this.markUserOperationsAsReplaced(userOpsReplaced, newTxInfo);
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