@pimlico/alto 0.0.0-main.20250203T212650 → 0.0.0-main.20250205T142138

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 (91) hide show
  1. package/esm/cli/config/bundler.d.ts +0 -6
  2. package/esm/cli/config/bundler.js +0 -1
  3. package/esm/cli/config/bundler.js.map +1 -1
  4. package/esm/cli/config/options.js +0 -6
  5. package/esm/cli/config/options.js.map +1 -1
  6. package/esm/cli/setupServer.js +5 -5
  7. package/esm/cli/setupServer.js.map +1 -1
  8. package/esm/executor/executor.d.ts +34 -44
  9. package/esm/executor/executor.js +103 -463
  10. package/esm/executor/executor.js.map +1 -1
  11. package/esm/executor/executorManager.d.ts +20 -10
  12. package/esm/executor/executorManager.js +371 -310
  13. package/esm/executor/executorManager.js.map +1 -1
  14. package/esm/executor/filterOpsAndEStimateGas.d.ts +28 -0
  15. package/esm/executor/filterOpsAndEStimateGas.js +191 -0
  16. package/esm/executor/filterOpsAndEStimateGas.js.map +1 -0
  17. package/esm/executor/senderManager.d.ts +2 -0
  18. package/esm/executor/senderManager.js +32 -0
  19. package/esm/executor/senderManager.js.map +1 -1
  20. package/esm/executor/utils.d.ts +21 -20
  21. package/esm/executor/utils.js +46 -185
  22. package/esm/executor/utils.js.map +1 -1
  23. package/esm/handlers/eventManager.d.ts +4 -1
  24. package/esm/handlers/eventManager.js +10 -8
  25. package/esm/handlers/eventManager.js.map +1 -1
  26. package/esm/mempool/mempool.d.ts +15 -11
  27. package/esm/mempool/mempool.js +207 -176
  28. package/esm/mempool/mempool.js.map +1 -1
  29. package/esm/mempool/store.d.ts +7 -7
  30. package/esm/mempool/store.js +13 -12
  31. package/esm/mempool/store.js.map +1 -1
  32. package/esm/rpc/nonceQueuer.js +2 -2
  33. package/esm/rpc/nonceQueuer.js.map +1 -1
  34. package/esm/rpc/rpcHandler.js +27 -31
  35. package/esm/rpc/rpcHandler.js.map +1 -1
  36. package/esm/types/mempool.d.ts +40 -41
  37. package/esm/types/mempool.js.map +1 -1
  38. package/esm/types/schemas.d.ts +0 -4
  39. package/esm/types/schemas.js.map +1 -1
  40. package/esm/utils/userop.d.ts +8 -3
  41. package/esm/utils/userop.js +5 -3
  42. package/esm/utils/userop.js.map +1 -1
  43. package/lib/cli/config/bundler.d.ts +0 -6
  44. package/lib/cli/config/bundler.js +0 -1
  45. package/lib/cli/config/bundler.js.map +1 -1
  46. package/lib/cli/config/options.js +0 -6
  47. package/lib/cli/config/options.js.map +1 -1
  48. package/lib/cli/setupServer.js +5 -5
  49. package/lib/cli/setupServer.js.map +1 -1
  50. package/lib/executor/executor.d.ts +34 -44
  51. package/lib/executor/executor.js +100 -460
  52. package/lib/executor/executor.js.map +1 -1
  53. package/lib/executor/executorManager.d.ts +20 -10
  54. package/lib/executor/executorManager.js +370 -309
  55. package/lib/executor/executorManager.js.map +1 -1
  56. package/lib/executor/filterOpsAndEStimateGas.d.ts +28 -0
  57. package/lib/executor/filterOpsAndEStimateGas.js +218 -0
  58. package/lib/executor/filterOpsAndEStimateGas.js.map +1 -0
  59. package/lib/executor/senderManager.d.ts +2 -0
  60. package/lib/executor/senderManager.js +32 -0
  61. package/lib/executor/senderManager.js.map +1 -1
  62. package/lib/executor/utils.d.ts +21 -20
  63. package/lib/executor/utils.js +49 -186
  64. package/lib/executor/utils.js.map +1 -1
  65. package/lib/handlers/eventManager.d.ts +4 -1
  66. package/lib/handlers/eventManager.js +10 -8
  67. package/lib/handlers/eventManager.js.map +1 -1
  68. package/lib/mempool/mempool.d.ts +15 -11
  69. package/lib/mempool/mempool.js +206 -175
  70. package/lib/mempool/mempool.js.map +1 -1
  71. package/lib/mempool/store.d.ts +7 -7
  72. package/lib/mempool/store.js +13 -12
  73. package/lib/mempool/store.js.map +1 -1
  74. package/lib/rpc/nonceQueuer.js +1 -1
  75. package/lib/rpc/nonceQueuer.js.map +1 -1
  76. package/lib/rpc/rpcHandler.js +26 -30
  77. package/lib/rpc/rpcHandler.js.map +1 -1
  78. package/lib/types/mempool.d.ts +40 -41
  79. package/lib/types/mempool.js.map +1 -1
  80. package/lib/types/schemas.d.ts +0 -4
  81. package/lib/types/schemas.js.map +1 -1
  82. package/lib/utils/userop.d.ts +8 -3
  83. package/lib/utils/userop.js +7 -5
  84. package/lib/utils/userop.js.map +1 -1
  85. package/package.json +1 -1
  86. package/esm/executor/types.d.ts +0 -25
  87. package/esm/executor/types.js +0 -2
  88. package/esm/executor/types.js.map +0 -1
  89. package/lib/executor/types.d.ts +0 -25
  90. package/lib/executor/types.js +0 -3
  91. package/lib/executor/types.js.map +0 -1
@@ -1,16 +1,19 @@
1
1
  import { EntryPointV06Abi } from "../types/index.js";
2
2
  import { getAAError, getBundleStatus, parseUserOperationReceipt, scaleBigIntByPercent } from "../utils/index.js";
3
- import { TransactionReceiptNotFoundError, getAbiItem } from "viem";
4
- function getTransactionsFromUserOperationEntries(entries) {
5
- return Array.from(new Set(entries.map((entry) => {
6
- return entry.transactionInfo;
7
- })));
3
+ import { TransactionReceiptNotFoundError, getAbiItem, InsufficientFundsError, 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));
8
10
  }
9
11
  const MIN_INTERVAL = 100; // 0.1 seconds (100ms)
10
12
  const MAX_INTERVAL = 1000; // Capped at 1 second (1000ms)
11
13
  const SCALE_FACTOR = 10; // Interval increases by 5ms per task per minute
12
14
  const RPM_WINDOW = 60000; // 1 minute window in ms
13
15
  export class ExecutorManager {
16
+ senderManager;
14
17
  config;
15
18
  executor;
16
19
  mempool;
@@ -24,7 +27,7 @@ export class ExecutorManager {
24
27
  eventManager;
25
28
  opsCount = [];
26
29
  bundlingMode;
27
- constructor({ config, executor, mempool, monitor, reputationManager, metrics, gasPriceManager, eventManager }) {
30
+ constructor({ config, executor, mempool, monitor, reputationManager, metrics, gasPriceManager, eventManager, senderManager }) {
28
31
  this.config = config;
29
32
  this.reputationManager = reputationManager;
30
33
  this.executor = executor;
@@ -36,6 +39,7 @@ export class ExecutorManager {
36
39
  this.metrics = metrics;
37
40
  this.gasPriceManager = gasPriceManager;
38
41
  this.eventManager = eventManager;
42
+ this.senderManager = senderManager;
39
43
  this.bundlingMode = this.config.bundleMode;
40
44
  if (this.bundlingMode === "auto") {
41
45
  this.autoScalingBundling();
@@ -53,12 +57,18 @@ export class ExecutorManager {
53
57
  async autoScalingBundling() {
54
58
  const now = Date.now();
55
59
  this.opsCount = this.opsCount.filter((timestamp) => now - timestamp < RPM_WINDOW);
56
- const opsToBundle = await this.getOpsToBundle();
57
- if (opsToBundle.length > 0) {
58
- const opsCount = opsToBundle.length;
60
+ const bundles = await this.mempool.getBundles();
61
+ if (bundles.length > 0) {
62
+ const opsCount = bundles
63
+ .map(({ userOps }) => userOps.length)
64
+ .reduce((a, b) => a + b);
65
+ // Add timestamps for each task
59
66
  const timestamp = Date.now();
60
- this.opsCount.push(...Array(opsCount).fill(timestamp)); // Add timestamps for each task
61
- await this.bundle(opsToBundle);
67
+ this.opsCount.push(...Array(opsCount).fill(timestamp));
68
+ // Send bundles to executor
69
+ await Promise.all(bundles.map(async (bundle) => {
70
+ await this.sendBundleToExecutor(bundle);
71
+ }));
62
72
  }
63
73
  const rpm = this.opsCount.length;
64
74
  // Calculate next interval with linear scaling
@@ -69,145 +79,106 @@ export class ExecutorManager {
69
79
  setTimeout(this.autoScalingBundling.bind(this), nextInterval);
70
80
  }
71
81
  }
72
- async getOpsToBundle() {
73
- const opsToBundle = [];
74
- while (true) {
75
- const ops = await this.mempool.process(this.config.maxGasPerBundle, 1);
76
- if (ops?.length > 0) {
77
- opsToBundle.push(ops);
78
- }
79
- else {
80
- break;
81
- }
82
- }
83
- if (opsToBundle.length === 0) {
84
- return [];
85
- }
86
- return opsToBundle;
87
- }
88
- async bundleNow() {
89
- const ops = await this.mempool.process(this.config.maxGasPerBundle, 1);
90
- if (ops.length === 0) {
82
+ // Debug endpoint
83
+ async sendBundleNow() {
84
+ const bundles = await this.mempool.getBundles(1);
85
+ const bundle = bundles[0];
86
+ if (bundle.userOps.length === 0) {
91
87
  throw new Error("no ops to bundle");
92
88
  }
93
- const opEntryPointMap = new Map();
94
- for (const op of ops) {
95
- if (!opEntryPointMap.has(op.entryPoint)) {
96
- opEntryPointMap.set(op.entryPoint, []);
97
- }
98
- opEntryPointMap.get(op.entryPoint)?.push(op.userOperation);
99
- }
100
- const txHashes = [];
101
- await Promise.all(this.config.entrypoints.map(async (entryPoint) => {
102
- const ops = opEntryPointMap.get(entryPoint);
103
- if (ops) {
104
- const txHash = await this.sendToExecutor(entryPoint, ops);
105
- if (!txHash) {
106
- throw new Error("no tx hash");
107
- }
108
- txHashes.push(txHash);
109
- }
110
- else {
111
- this.logger.warn({ entryPoint }, "no user operations for entry point");
112
- }
113
- }));
114
- return txHashes;
115
- }
116
- async sendToExecutor(entryPoint, mempoolOps) {
117
- const ops = mempoolOps.map((op) => op);
118
- const bundles = [];
119
- if (ops.length > 0) {
120
- bundles.push(await this.executor.bundle(entryPoint, ops));
121
- }
122
- for (const bundle of bundles) {
123
- const isBundleSuccess = bundle.every((result) => result.status === "success");
124
- const isBundleResubmit = bundle.every((result) => result.status === "resubmit");
125
- const isBundleFailed = bundle.every((result) => result.status === "failure");
126
- if (isBundleSuccess) {
127
- this.metrics.bundlesSubmitted
128
- .labels({ status: "success" })
129
- .inc();
130
- }
131
- if (isBundleResubmit) {
132
- this.metrics.bundlesSubmitted
133
- .labels({ status: "resubmit" })
134
- .inc();
135
- }
136
- if (isBundleFailed) {
137
- this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc();
138
- }
139
- }
140
- const results = bundles.flat();
141
- const filteredOutOps = mempoolOps.length - results.length;
142
- if (filteredOutOps > 0) {
143
- this.logger.debug({ filteredOutOps }, "user operations filtered out");
144
- this.metrics.userOperationsSubmitted
145
- .labels({ status: "filtered" })
146
- .inc(filteredOutOps);
147
- }
148
- let txHash = undefined;
149
- for (const result of results) {
150
- if (result.status === "success") {
151
- const res = result.value;
152
- this.mempool.markSubmitted(res.userOperation.userOperationHash, res.transactionInfo);
153
- this.monitor.setUserOperationStatus(res.userOperation.userOperationHash, {
154
- status: "submitted",
155
- transactionHash: res.transactionInfo.transactionHash
156
- });
157
- txHash = res.transactionInfo.transactionHash;
158
- this.startWatchingBlocks(this.handleBlock.bind(this));
159
- this.metrics.userOperationsSubmitted
160
- .labels({ status: "success" })
161
- .inc();
162
- }
163
- if (result.status === "failure") {
164
- const { userOpHash, reason } = result.error;
165
- this.mempool.removeProcessing(userOpHash);
166
- this.eventManager.emitDropped(userOpHash, reason, getAAError(reason));
167
- this.monitor.setUserOperationStatus(userOpHash, {
168
- status: "rejected",
169
- transactionHash: null
170
- });
171
- this.logger.warn({
172
- userOperation: JSON.stringify(result.error.userOperation, (_k, v) => typeof v === "bigint" ? v.toString() : v),
173
- userOpHash,
174
- reason
175
- }, "user operation rejected");
176
- this.metrics.userOperationsSubmitted
177
- .labels({ status: "failed" })
178
- .inc();
179
- }
180
- if (result.status === "resubmit") {
181
- this.logger.info({
182
- userOpHash: result.info.userOpHash,
183
- reason: result.info.reason
184
- }, "resubmitting user operation");
185
- this.mempool.removeProcessing(result.info.userOpHash);
186
- this.mempool.add(result.info.userOperation, result.info.entryPoint);
187
- this.metrics.userOperationsResubmitted.inc();
188
- }
89
+ const txHash = await this.sendBundleToExecutor(bundle);
90
+ if (!txHash) {
91
+ throw new Error("no tx hash");
189
92
  }
190
93
  return txHash;
191
94
  }
192
- async bundle(opsToBundle = []) {
193
- await Promise.all(opsToBundle.map(async (ops) => {
194
- const opEntryPointMap = new Map();
195
- for (const op of ops) {
196
- if (!opEntryPointMap.has(op.entryPoint)) {
197
- opEntryPointMap.set(op.entryPoint, []);
198
- }
199
- opEntryPointMap.get(op.entryPoint)?.push(op.userOperation);
200
- }
201
- await Promise.all(this.config.entrypoints.map(async (entryPoint) => {
202
- const userOperations = opEntryPointMap.get(entryPoint);
203
- if (userOperations) {
204
- await this.sendToExecutor(entryPoint, userOperations);
205
- }
206
- else {
207
- this.logger.warn({ entryPoint }, "no user operations for entry point");
208
- }
209
- }));
210
- }));
95
+ async sendBundleToExecutor(userOpBundle) {
96
+ const { entryPoint, userOps, version } = userOpBundle;
97
+ if (userOps.length === 0) {
98
+ return undefined;
99
+ }
100
+ const wallet = await this.senderManager.getWallet();
101
+ const [gasPriceParams, nonce] = await Promise.all([
102
+ this.gasPriceManager.tryGetNetworkGasPrice(),
103
+ this.config.publicClient.getTransactionCount({
104
+ address: wallet.address,
105
+ blockTag: "latest"
106
+ })
107
+ ]).catch((_) => {
108
+ return [];
109
+ });
110
+ if (!gasPriceParams || nonce === undefined) {
111
+ this.resubmitUserOperations(userOps, entryPoint, "Failed to get nonce and gas parameters for bundling");
112
+ // Free executor if failed to get initial params.
113
+ this.senderManager.markWalletProcessed(wallet);
114
+ return undefined;
115
+ }
116
+ const bundleResult = await this.executor.bundle({
117
+ executor: wallet,
118
+ userOpBundle,
119
+ nonce,
120
+ gasPriceParams,
121
+ isReplacementTx: false
122
+ });
123
+ // Free wallet if no bundle was sent.
124
+ if (bundleResult.status !== "bundle_success") {
125
+ this.senderManager.markWalletProcessed(wallet);
126
+ }
127
+ // All ops failed simulation, drop them and return.
128
+ if (bundleResult.status === "all_ops_failed_simulation") {
129
+ const { rejectedUserOps } = bundleResult;
130
+ this.dropUserOps(rejectedUserOps);
131
+ return undefined;
132
+ }
133
+ // Unhandled error during simulation, drop all ops.
134
+ if (bundleResult.status === "unhandled_simulation_failure") {
135
+ const { rejectedUserOps } = bundleResult;
136
+ this.dropUserOps(rejectedUserOps);
137
+ this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc();
138
+ return undefined;
139
+ }
140
+ // Resubmit if executor has insufficient funds.
141
+ if (bundleResult.status === "bundle_submission_failure" &&
142
+ bundleResult.reason instanceof InsufficientFundsError) {
143
+ const { reason, userOpsToBundle, rejectedUserOps } = bundleResult;
144
+ this.dropUserOps(rejectedUserOps);
145
+ this.resubmitUserOperations(userOpsToBundle, entryPoint, reason.name);
146
+ this.metrics.bundlesSubmitted.labels({ status: "resubmit" }).inc();
147
+ return undefined;
148
+ }
149
+ // Encountered unhandled error during bundle simulation.
150
+ if (bundleResult.status === "bundle_submission_failure") {
151
+ const { rejectedUserOps, userOpsToBundle, reason } = bundleResult;
152
+ this.dropUserOps(rejectedUserOps);
153
+ // NOTE: these ops passed validation, so we can try resubmitting them
154
+ this.resubmitUserOperations(userOpsToBundle, entryPoint, reason instanceof BaseError
155
+ ? reason.name
156
+ : "Encountered unhandled error during bundle simulation");
157
+ this.metrics.bundlesSubmitted.labels({ status: "failed" }).inc();
158
+ return undefined;
159
+ }
160
+ if (bundleResult.status === "bundle_success") {
161
+ const { userOpsBundled, rejectedUserOps, transactionRequest, transactionHash } = bundleResult;
162
+ const transactionInfo = {
163
+ executor: wallet,
164
+ transactionHash,
165
+ transactionRequest,
166
+ bundle: {
167
+ entryPoint,
168
+ version,
169
+ userOps: userOpsBundled
170
+ },
171
+ previousTransactionHashes: [],
172
+ lastReplaced: Date.now(),
173
+ firstSubmitted: Date.now(),
174
+ timesPotentiallyIncluded: 0
175
+ };
176
+ this.markUserOperationsAsSubmitted(userOpsBundled, transactionInfo);
177
+ this.dropUserOps(rejectedUserOps);
178
+ this.metrics.bundlesSubmitted.labels({ status: "success" }).inc();
179
+ return transactionHash;
180
+ }
181
+ return undefined;
211
182
  }
212
183
  startWatchingBlocks(handleBlock) {
213
184
  if (this.unWatch) {
@@ -215,18 +186,6 @@ export class ExecutorManager {
215
186
  }
216
187
  this.unWatch = this.config.publicClient.watchBlocks({
217
188
  onBlock: handleBlock,
218
- // onBlock: async (block) => {
219
- // // Use an arrow function to ensure correct binding of `this`
220
- // this.checkAndReplaceTransactions(block)
221
- // .then(() => {
222
- // this.logger.trace("block handled")
223
- // // Handle the resolution of the promise here, if needed
224
- // })
225
- // .catch((error) => {
226
- // // Handle any errors that occur during the execution of the promise
227
- // this.logger.error({ error }, "error while handling block")
228
- // })
229
- // },
230
189
  onError: (error) => {
231
190
  this.logger.error({ error }, "error while watching blocks");
232
191
  },
@@ -244,94 +203,71 @@ export class ExecutorManager {
244
203
  }
245
204
  }
246
205
  // update the current status of the bundling transaction/s
247
- async refreshTransactionStatus(entryPoint, transactionInfo) {
248
- const { transactionHash: currentTransactionHash, userOperationInfos: opInfos, previousTransactionHashes, isVersion06 } = transactionInfo;
249
- const txHashesToCheck = [
250
- currentTransactionHash,
251
- ...previousTransactionHashes
252
- ];
206
+ async refreshTransactionStatus(transactionInfo) {
207
+ const { transactionHash: currentTxhash, bundle, previousTransactionHashes } = transactionInfo;
208
+ const { userOps, entryPoint } = bundle;
209
+ const txHashesToCheck = [currentTxhash, ...previousTransactionHashes];
253
210
  const transactionDetails = await Promise.all(txHashesToCheck.map(async (transactionHash) => ({
254
211
  transactionHash,
255
- ...(await getBundleStatus(isVersion06, transactionHash, this.config.publicClient, this.logger, entryPoint))
212
+ ...(await getBundleStatus({
213
+ transactionHash,
214
+ bundle: transactionInfo.bundle,
215
+ publicClient: this.config.publicClient,
216
+ logger: this.logger
217
+ }))
256
218
  })));
257
219
  // first check if bundling txs returns status "mined", if not, check for reverted
258
220
  const mined = transactionDetails.find(({ bundlingStatus }) => bundlingStatus.status === "included");
259
221
  const reverted = transactionDetails.find(({ bundlingStatus }) => bundlingStatus.status === "reverted");
260
222
  const finalizedTransaction = mined ?? reverted;
261
223
  if (!finalizedTransaction) {
262
- for (const { userOperationHash } of opInfos) {
263
- this.logger.trace({
264
- userOperationHash,
265
- currentTransactionHash
266
- }, "user op still pending");
267
- }
268
224
  return;
269
225
  }
270
226
  const { bundlingStatus, transactionHash, blockNumber } = finalizedTransaction;
271
- if (bundlingStatus.status === "included") {
272
- this.metrics.userOperationsOnChain
273
- .labels({ status: bundlingStatus.status })
274
- .inc(opInfos.length);
275
- const { userOperationDetails } = bundlingStatus;
276
- opInfos.map((opInfo) => {
277
- const { userOperation, userOperationHash, entryPoint, firstSubmitted } = opInfo;
278
- const opDetails = userOperationDetails[userOperationHash];
279
- this.metrics.userOperationInclusionDuration.observe((Date.now() - firstSubmitted) / 1000);
280
- this.mempool.removeSubmitted(userOperationHash);
281
- this.reputationManager.updateUserOperationIncludedStatus(userOperation, entryPoint, opDetails.accountDeployed);
282
- if (opDetails.status === "succesful") {
283
- this.eventManager.emitIncludedOnChain(userOperationHash, transactionHash, blockNumber);
284
- }
285
- else {
286
- this.eventManager.emitExecutionRevertedOnChain(userOperationHash, transactionHash, opDetails.revertReason || "0x", blockNumber);
287
- }
288
- this.monitor.setUserOperationStatus(userOperationHash, {
289
- status: "included",
290
- transactionHash
291
- });
292
- this.logger.info({
293
- userOperationHash,
294
- transactionHash
295
- }, "user op included");
296
- });
297
- this.executor.markWalletProcessed(transactionInfo.executor);
298
- }
299
- else if (bundlingStatus.status === "reverted" &&
300
- bundlingStatus.isAA95) {
227
+ // TODO: there has to be a better way of solving onchain AA95 errors.
228
+ if (bundlingStatus.status === "reverted" && bundlingStatus.isAA95) {
301
229
  // resubmit with more gas when bundler encounters AA95
302
230
  transactionInfo.transactionRequest.gas = scaleBigIntByPercent(transactionInfo.transactionRequest.gas, this.config.aa95GasMultiplier);
303
231
  transactionInfo.transactionRequest.nonce += 1;
304
232
  await this.replaceTransaction(transactionInfo, "AA95");
233
+ return;
234
+ }
235
+ // Free executor if tx landed onchain
236
+ if (bundlingStatus.status !== "not_found") {
237
+ this.senderManager.markWalletProcessed(transactionInfo.executor);
305
238
  }
306
- else {
307
- await Promise.all(opInfos.map(({ userOperationHash }) => {
239
+ if (bundlingStatus.status === "included") {
240
+ const { userOperationDetails } = bundlingStatus;
241
+ this.markUserOpsIncluded(userOps, entryPoint, blockNumber, transactionHash, userOperationDetails);
242
+ }
243
+ if (bundlingStatus.status === "reverted") {
244
+ await Promise.all(userOps.map((userOpInfo) => {
245
+ const { userOpHash } = userOpInfo;
308
246
  this.checkFrontrun({
309
- userOperationHash,
247
+ userOpHash,
310
248
  transactionHash,
311
249
  blockNumber
312
250
  });
313
251
  }));
314
- opInfos.map(({ userOperationHash }) => {
315
- this.mempool.removeSubmitted(userOperationHash);
316
- });
317
- this.executor.markWalletProcessed(transactionInfo.executor);
252
+ this.removeSubmitted(userOps);
318
253
  }
319
254
  }
320
- checkFrontrun({ userOperationHash, transactionHash, blockNumber }) {
255
+ checkFrontrun({ userOpHash, transactionHash, blockNumber }) {
321
256
  const unwatch = this.config.publicClient.watchBlockNumber({
322
257
  onBlockNumber: async (currentBlockNumber) => {
323
258
  if (currentBlockNumber > blockNumber + 1n) {
324
- const userOperationReceipt = await this.getUserOperationReceipt(userOperationHash);
259
+ const userOperationReceipt = await this.getUserOperationReceipt(userOpHash);
325
260
  if (userOperationReceipt) {
326
261
  const transactionHash = userOperationReceipt.receipt.transactionHash;
327
262
  const blockNumber = userOperationReceipt.receipt.blockNumber;
328
- this.monitor.setUserOperationStatus(userOperationHash, {
263
+ this.mempool.removeSubmitted(userOpHash);
264
+ this.monitor.setUserOperationStatus(userOpHash, {
329
265
  status: "included",
330
266
  transactionHash
331
267
  });
332
- this.eventManager.emitFrontranOnChain(userOperationHash, transactionHash, blockNumber);
268
+ this.eventManager.emitFrontranOnChain(userOpHash, transactionHash, blockNumber);
333
269
  this.logger.info({
334
- userOpHash: userOperationHash,
270
+ userOpHash,
335
271
  transactionHash
336
272
  }, "user op frontrun onchain");
337
273
  this.metrics.userOperationsOnChain
@@ -339,13 +275,13 @@ export class ExecutorManager {
339
275
  .inc(1);
340
276
  }
341
277
  else {
342
- this.monitor.setUserOperationStatus(userOperationHash, {
343
- status: "rejected",
278
+ this.monitor.setUserOperationStatus(userOpHash, {
279
+ status: "failed",
344
280
  transactionHash
345
281
  });
346
- this.eventManager.emitFailedOnChain(userOperationHash, transactionHash, blockNumber);
282
+ this.eventManager.emitFailedOnChain(userOpHash, transactionHash, blockNumber);
347
283
  this.logger.info({
348
- userOpHash: userOperationHash,
284
+ userOpHash,
349
285
  transactionHash
350
286
  }, "user op failed onchain");
351
287
  this.metrics.userOperationsOnChain
@@ -448,28 +384,6 @@ export class ExecutorManager {
448
384
  const userOperationReceipt = parseUserOperationReceipt(userOperationHash, receipt);
449
385
  return userOperationReceipt;
450
386
  }
451
- async refreshUserOperationStatuses() {
452
- const ops = this.mempool.dumpSubmittedOps();
453
- const opEntryPointMap = new Map();
454
- for (const op of ops) {
455
- if (!opEntryPointMap.has(op.userOperation.entryPoint)) {
456
- opEntryPointMap.set(op.userOperation.entryPoint, []);
457
- }
458
- opEntryPointMap.get(op.userOperation.entryPoint)?.push(op);
459
- }
460
- await Promise.all(this.config.entrypoints.map(async (entryPoint) => {
461
- const ops = opEntryPointMap.get(entryPoint);
462
- if (ops) {
463
- const txs = getTransactionsFromUserOperationEntries(ops);
464
- await Promise.all(txs.map(async (txInfo) => {
465
- await this.refreshTransactionStatus(entryPoint, txInfo);
466
- }));
467
- }
468
- else {
469
- this.logger.warn({ entryPoint }, "no user operations for entry point");
470
- }
471
- }));
472
- }
473
387
  async handleBlock(block) {
474
388
  if (this.currentlyHandlingBlock) {
475
389
  return;
@@ -483,101 +397,248 @@ export class ExecutorManager {
483
397
  return;
484
398
  }
485
399
  // refresh op statuses
486
- await this.refreshUserOperationStatuses();
400
+ const ops = this.mempool.dumpSubmittedOps();
401
+ const txs = getTransactionsFromUserOperationEntries(ops);
402
+ await Promise.all(txs.map((txInfo) => this.refreshTransactionStatus(txInfo)));
487
403
  // for all still not included check if needs to be replaced (based on gas price)
488
- let gasPriceParameters;
489
- try {
490
- gasPriceParameters =
491
- await this.gasPriceManager.tryGetNetworkGasPrice();
492
- }
493
- catch {
494
- gasPriceParameters = {
495
- maxFeePerGas: 0n,
496
- maxPriorityFeePerGas: 0n
497
- };
498
- }
499
- this.logger.trace({ gasPriceParameters }, "fetched gas price parameters");
404
+ const gasPriceParameters = await this.gasPriceManager
405
+ .tryGetNetworkGasPrice()
406
+ .catch(() => ({
407
+ maxFeePerGas: 0n,
408
+ maxPriorityFeePerGas: 0n
409
+ }));
500
410
  const transactionInfos = getTransactionsFromUserOperationEntries(this.mempool.dumpSubmittedOps());
501
411
  await Promise.all(transactionInfos.map(async (txInfo) => {
502
- if (txInfo.transactionRequest.maxFeePerGas >=
503
- gasPriceParameters.maxFeePerGas &&
504
- txInfo.transactionRequest.maxPriorityFeePerGas >=
505
- gasPriceParameters.maxPriorityFeePerGas) {
412
+ const { transactionRequest } = txInfo;
413
+ const { maxFeePerGas, maxPriorityFeePerGas } = transactionRequest;
414
+ const isMaxFeeTooLow = maxFeePerGas < gasPriceParameters.maxFeePerGas;
415
+ const isPriorityFeeTooLow = maxPriorityFeePerGas <
416
+ gasPriceParameters.maxPriorityFeePerGas;
417
+ const isStuck = Date.now() - txInfo.lastReplaced >
418
+ this.config.resubmitStuckTimeout;
419
+ if (isMaxFeeTooLow || isPriorityFeeTooLow) {
420
+ await this.replaceTransaction(txInfo, "gas_price");
506
421
  return;
507
422
  }
508
- await this.replaceTransaction(txInfo, "gas_price");
509
- }));
510
- // for any left check if enough time has passed, if so replace
511
- const transactionInfos2 = getTransactionsFromUserOperationEntries(this.mempool.dumpSubmittedOps());
512
- await Promise.all(transactionInfos2.map(async (txInfo) => {
513
- if (Date.now() - txInfo.lastReplaced <
514
- this.config.resubmitStuckTimeout) {
423
+ if (isStuck) {
424
+ await this.replaceTransaction(txInfo, "stuck");
515
425
  return;
516
426
  }
517
- await this.replaceTransaction(txInfo, "stuck");
518
427
  }));
519
428
  this.currentlyHandlingBlock = false;
520
429
  }
521
430
  async replaceTransaction(txInfo, reason) {
522
- let replaceResult = undefined;
523
- try {
524
- replaceResult = await this.executor.replaceTransaction(txInfo);
525
- }
526
- finally {
527
- this.metrics.replacedTransactions
528
- .labels({ reason, status: replaceResult?.status || "failed" })
529
- .inc();
530
- }
531
- if (replaceResult.status === "failed") {
532
- txInfo.userOperationInfos.map((opInfo) => {
533
- const userOperation = opInfo.userOperation;
534
- this.eventManager.emitDropped(opInfo.userOperationHash, "Failed to replace transaction");
535
- this.logger.warn({
536
- userOperation: JSON.stringify(userOperation, (_k, v) => typeof v === "bigint" ? v.toString() : v),
537
- userOpHash: opInfo.userOperationHash,
538
- reason
539
- }, "user operation rejected");
540
- this.mempool.removeSubmitted(opInfo.userOperationHash);
431
+ // Setup vars
432
+ const { bundle, executor, transactionRequest, transactionHash: oldTxHash } = txInfo;
433
+ const { userOps } = bundle;
434
+ const gasPriceParameters = await this.gasPriceManager
435
+ .tryGetNetworkGasPrice()
436
+ .catch((_) => {
437
+ return undefined;
438
+ });
439
+ if (!gasPriceParameters) {
440
+ const rejectedUserOps = userOps.map((userOpInfo) => ({
441
+ ...userOpInfo,
442
+ reason: "Failed to get network gas price during replacement"
443
+ }));
444
+ this.failedToReplaceTransaction({
445
+ rejectedUserOps,
446
+ oldTxHash,
447
+ reason: "Failed to get network gas price during replacement"
541
448
  });
542
- this.logger.warn({ oldTxHash: txInfo.transactionHash, reason }, "failed to replace transaction");
449
+ // Free executor if failed to get initial params.
450
+ this.senderManager.markWalletProcessed(txInfo.executor);
543
451
  return;
544
452
  }
545
- if (replaceResult.status === "potentially_already_included") {
546
- this.logger.info({ oldTxHash: txInfo.transactionHash, reason }, "transaction potentially already included");
547
- txInfo.timesPotentiallyIncluded += 1;
453
+ const bundleResult = await this.executor.bundle({
454
+ executor: executor,
455
+ userOpBundle: bundle,
456
+ nonce: transactionRequest.nonce,
457
+ gasPriceParams: {
458
+ maxFeePerGas: scaleBigIntByPercent(gasPriceParameters.maxFeePerGas, 115n),
459
+ maxPriorityFeePerGas: scaleBigIntByPercent(gasPriceParameters.maxPriorityFeePerGas, 115n)
460
+ },
461
+ gasLimitSuggestion: transactionRequest.gas,
462
+ isReplacementTx: true
463
+ });
464
+ // Free wallet and return if potentially included too many times.
465
+ if (txInfo.timesPotentiallyIncluded >= 3) {
548
466
  if (txInfo.timesPotentiallyIncluded >= 3) {
549
- txInfo.userOperationInfos.map((opInfo) => {
550
- this.mempool.removeSubmitted(opInfo.userOperationHash);
551
- });
552
- this.executor.markWalletProcessed(txInfo.executor);
553
- this.logger.warn({ oldTxHash: txInfo.transactionHash, reason }, "transaction potentially already included too many times, removing");
467
+ this.removeSubmitted(bundle.userOps);
468
+ this.logger.warn({
469
+ oldTxHash,
470
+ userOps: getUserOpHashes(bundleResult.rejectedUserOps)
471
+ }, "transaction potentially already included too many times, removing");
554
472
  }
473
+ this.senderManager.markWalletProcessed(txInfo.executor);
555
474
  return;
556
475
  }
557
- const newTxInfo = replaceResult.transactionInfo;
558
- const missingOps = txInfo.userOperationInfos.filter((info) => !newTxInfo.userOperationInfos
559
- .map((ni) => ni.userOperationHash)
560
- .includes(info.userOperationHash));
561
- const matchingOps = txInfo.userOperationInfos.filter((info) => newTxInfo.userOperationInfos
562
- .map((ni) => ni.userOperationHash)
563
- .includes(info.userOperationHash));
564
- matchingOps.map((opInfo) => {
565
- this.mempool.replaceSubmitted(opInfo, newTxInfo);
566
- });
567
- missingOps.map((opInfo) => {
568
- this.mempool.removeSubmitted(opInfo.userOperationHash);
569
- this.logger.warn({
570
- oldTxHash: txInfo.transactionHash,
571
- newTxHash: newTxInfo.transactionHash,
572
- reason
573
- }, "missing op in new tx");
476
+ // Free wallet if no bundle was sent or potentially included.
477
+ if (bundleResult.status !== "bundle_success") {
478
+ this.senderManager.markWalletProcessed(txInfo.executor);
479
+ }
480
+ // Check if the transaction is potentially included.
481
+ const nonceTooLow = bundleResult.status === "bundle_submission_failure" &&
482
+ bundleResult.reason instanceof NonceTooLowError;
483
+ const allOpsFailedSimulation = bundleResult.status === "all_ops_failed_simulation" &&
484
+ bundleResult.rejectedUserOps.every((op) => op.reason === "AA25 invalid account nonce" ||
485
+ op.reason === "AA10 sender already constructed");
486
+ const potentiallyIncluded = nonceTooLow || allOpsFailedSimulation;
487
+ // log metrics
488
+ const replaceStatus = (() => {
489
+ switch (true) {
490
+ case potentiallyIncluded:
491
+ return "potentially_already_included";
492
+ case bundleResult?.status === "bundle_success":
493
+ return "replaced";
494
+ default:
495
+ return "failed";
496
+ }
497
+ })();
498
+ this.metrics.replacedTransactions
499
+ .labels({ reason, status: replaceStatus })
500
+ .inc();
501
+ if (potentiallyIncluded) {
502
+ this.logger.info({
503
+ oldTxHash,
504
+ userOpHashes: getUserOpHashes(bundleResult.rejectedUserOps)
505
+ }, "transaction potentially already included");
506
+ txInfo.timesPotentiallyIncluded += 1;
507
+ return;
508
+ }
509
+ if (bundleResult.status === "unhandled_simulation_failure") {
510
+ const { rejectedUserOps, reason } = bundleResult;
511
+ this.failedToReplaceTransaction({
512
+ oldTxHash,
513
+ reason,
514
+ rejectedUserOps
515
+ });
516
+ return;
517
+ }
518
+ if (bundleResult.status === "all_ops_failed_simulation") {
519
+ this.failedToReplaceTransaction({
520
+ oldTxHash,
521
+ reason: "all ops failed simulation",
522
+ rejectedUserOps: bundleResult.rejectedUserOps
523
+ });
524
+ return;
525
+ }
526
+ if (bundleResult.status === "bundle_submission_failure") {
527
+ const { reason, rejectedUserOps } = bundleResult;
528
+ const submissionFailureReason = reason instanceof BaseError ? reason.name : "INTERNAL FAILURE";
529
+ this.failedToReplaceTransaction({
530
+ oldTxHash,
531
+ rejectedUserOps,
532
+ reason: submissionFailureReason
533
+ });
534
+ return;
535
+ }
536
+ const { rejectedUserOps, userOpsBundled, transactionRequest: newTransactionRequest, transactionHash: newTxHash } = bundleResult;
537
+ const userOpsReplaced = userOpsBundled;
538
+ const newTxInfo = {
539
+ ...txInfo,
540
+ transactionRequest: newTransactionRequest,
541
+ transactionHash: newTxHash,
542
+ previousTransactionHashes: [
543
+ txInfo.transactionHash,
544
+ ...txInfo.previousTransactionHashes
545
+ ],
546
+ lastReplaced: Date.now(),
547
+ bundle: {
548
+ ...txInfo.bundle,
549
+ userOps: userOpsReplaced
550
+ }
551
+ };
552
+ userOpsReplaced.map((userOp) => {
553
+ this.mempool.replaceSubmitted(userOp, newTxInfo);
574
554
  });
555
+ // Drop all userOperations that were rejected during simulation.
556
+ this.dropUserOps(rejectedUserOps);
575
557
  this.logger.info({
576
- oldTxHash: txInfo.transactionHash,
577
- newTxHash: newTxInfo.transactionHash,
558
+ oldTxHash,
559
+ newTxHash,
578
560
  reason
579
561
  }, "replaced transaction");
580
562
  return;
581
563
  }
564
+ markUserOperationsAsSubmitted(userOpInfos, transactionInfo) {
565
+ userOpInfos.map((userOpInfo) => {
566
+ const { userOpHash } = userOpInfo;
567
+ this.mempool.markSubmitted(userOpHash, transactionInfo);
568
+ this.startWatchingBlocks(this.handleBlock.bind(this));
569
+ this.metrics.userOperationsSubmitted
570
+ .labels({ status: "success" })
571
+ .inc();
572
+ });
573
+ }
574
+ resubmitUserOperations(userOps, entryPoint, reason) {
575
+ userOps.map((userOpInfo) => {
576
+ const { userOpHash, userOp } = userOpInfo;
577
+ this.logger.info({
578
+ userOpHash,
579
+ reason
580
+ }, "resubmitting user operation");
581
+ this.mempool.removeProcessing(userOpHash);
582
+ this.mempool.add(userOp, entryPoint);
583
+ this.metrics.userOperationsResubmitted.inc();
584
+ });
585
+ }
586
+ failedToReplaceTransaction({ oldTxHash, rejectedUserOps, reason }) {
587
+ this.logger.warn({ oldTxHash, reason }, "failed to replace transaction");
588
+ this.dropUserOps(rejectedUserOps);
589
+ }
590
+ removeSubmitted(userOps) {
591
+ userOps.map((userOpInfo) => {
592
+ const { userOpHash } = userOpInfo;
593
+ this.mempool.removeSubmitted(userOpHash);
594
+ });
595
+ }
596
+ markUserOpsIncluded(userOps, entryPoint, blockNumber, transactionHash, userOperationDetails) {
597
+ userOps.map((userOpInfo) => {
598
+ this.metrics.userOperationsOnChain
599
+ .labels({ status: "included" })
600
+ .inc();
601
+ const { userOpHash, userOp } = userOpInfo;
602
+ const opDetails = userOperationDetails[userOpHash];
603
+ const firstSubmitted = userOpInfo.addedToMempool;
604
+ this.metrics.userOperationInclusionDuration.observe((Date.now() - firstSubmitted) / 1000);
605
+ this.mempool.removeSubmitted(userOpHash);
606
+ this.reputationManager.updateUserOperationIncludedStatus(userOp, entryPoint, opDetails.accountDeployed);
607
+ if (opDetails.status === "succesful") {
608
+ this.eventManager.emitIncludedOnChain(userOpHash, transactionHash, blockNumber);
609
+ }
610
+ else {
611
+ this.eventManager.emitExecutionRevertedOnChain(userOpHash, transactionHash, opDetails.revertReason || "0x", blockNumber);
612
+ }
613
+ this.monitor.setUserOperationStatus(userOpHash, {
614
+ status: "included",
615
+ transactionHash
616
+ });
617
+ this.logger.info({
618
+ opHash: userOpHash,
619
+ transactionHash
620
+ }, "user op included");
621
+ });
622
+ }
623
+ dropUserOps(rejectedUserOps) {
624
+ rejectedUserOps.map((rejectedUserOp) => {
625
+ const { userOp, reason, userOpHash } = rejectedUserOp;
626
+ this.mempool.removeProcessing(userOpHash);
627
+ this.mempool.removeSubmitted(userOpHash);
628
+ this.eventManager.emitDropped(userOpHash, reason, getAAError(reason));
629
+ this.monitor.setUserOperationStatus(userOpHash, {
630
+ status: "rejected",
631
+ transactionHash: null
632
+ });
633
+ this.logger.warn({
634
+ userOperation: JSON.stringify(userOp, (_k, v) => typeof v === "bigint" ? v.toString() : v),
635
+ userOpHash,
636
+ reason
637
+ }, "user operation rejected");
638
+ this.metrics.userOperationsSubmitted
639
+ .labels({ status: "failed" })
640
+ .inc();
641
+ });
642
+ }
582
643
  }
583
644
  //# sourceMappingURL=executorManager.js.map