Skip to content

Commit 067d5da

Browse files
authored
feat: add gasFeeCeiling to transaction overrides and handling logic (#905)
1 parent 9a8a054 commit 067d5da

File tree

5 files changed

+77
-14
lines changed

5 files changed

+77
-14
lines changed

src/server/schemas/tx-overrides.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ export const txOverridesSchema = Type.Object({
2323
...WeiAmountStringSchema,
2424
description: "Maximum priority fee per gas",
2525
}),
26+
27+
gasFeeCeiling: Type.Optional({
28+
...WeiAmountStringSchema,
29+
description:
30+
"Maximum gas fee for the transaction. This is the total maximum gas fee you are willing to pay for the transaction. If the chain gas conditions are worse than this, the transaction will be delayed until the gas conditions are better. If chain gas conditions are better than this, the transaction will be sent immediately. This value is only used to determine if the transaction should be delayed or sent immediately, and is not used to calculate the actual gas fee for the transaction.",
31+
}),
32+
2633
timeoutSeconds: Type.Optional(
2734
Type.Integer({
2835
examples: ["7200"],

src/server/utils/transaction-overrides.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const parseTransactionOverrides = (
2020
gasPrice: maybeBigInt(overrides.gasPrice),
2121
maxFeePerGas: maybeBigInt(overrides.maxFeePerGas),
2222
maxPriorityFeePerGas: maybeBigInt(overrides.maxPriorityFeePerGas),
23+
gasFeeCeiling: maybeBigInt(overrides.gasFeeCeiling),
2324
},
2425
timeoutSeconds: overrides.timeoutSeconds,
2526
// `value` may not be in the overrides object.

src/shared/db/transactions/queue-tx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface QueueTxParams {
2323
gas?: string;
2424
maxFeePerGas?: string;
2525
maxPriorityFeePerGas?: string;
26+
gasFeeCeiling?: string;
2627
value?: string;
2728
};
2829
}

src/shared/utils/transaction/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type InsertedTransaction = {
4545
gasPrice?: bigint;
4646
maxFeePerGas?: bigint;
4747
maxPriorityFeePerGas?: bigint;
48+
gasFeeCeiling?: bigint;
4849
};
4950
timeoutSeconds?: number;
5051

src/worker/tasks/send-transaction-worker.ts

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ const _sendUserOp = async (
210210
});
211211
accountFactoryAddress = getAddress(onchainAccountFactoryAddress);
212212
} catch (error) {
213-
const errorMessage = `${wrapError(error, "RPC").message} Failed to find factory address for account`;
213+
const errorMessage = `${
214+
wrapError(error, "RPC").message
215+
} Failed to find factory address for account`;
214216
const erroredTransaction: ErroredTransaction = {
215217
...queuedTransaction,
216218
status: "errored",
@@ -233,7 +235,9 @@ const _sendUserOp = async (
233235
chain,
234236
);
235237
} catch (error) {
236-
const errorMessage = `${wrapError(error, "RPC").message} Failed to find entrypoint address for account factory`;
238+
const errorMessage = `${
239+
wrapError(error, "RPC").message
240+
} Failed to find entrypoint address for account factory`;
237241
const erroredTransaction: ErroredTransaction = {
238242
...queuedTransaction,
239243
status: "errored",
@@ -300,18 +304,36 @@ const _sendUserOp = async (
300304
return erroredTransaction;
301305
}
302306

307+
// Handle if `gasFeeCeiling` is overridden.
308+
// Delay the job if the estimated cost is higher than the gas fee ceiling.
309+
const gasFeeCeiling = overrides?.gasFeeCeiling;
310+
if (typeof gasFeeCeiling !== "undefined") {
311+
const estimatedCost =
312+
unsignedUserOp.maxFeePerGas *
313+
(unsignedUserOp.callGasLimit +
314+
unsignedUserOp.preVerificationGas +
315+
unsignedUserOp.verificationGasLimit);
316+
317+
if (estimatedCost > gasFeeCeiling) {
318+
const retryAt = _minutesFromNow(5);
319+
job.log(
320+
`Override gas fee ceiling (${gasFeeCeiling}) is lower than onchain estimated cost (${estimatedCost}). Delaying job until ${retryAt}. [callGasLimit: ${unsignedUserOp.callGasLimit}, preVerificationGas: ${unsignedUserOp.preVerificationGas}, verificationGasLimit: ${unsignedUserOp.verificationGasLimit}, maxFeePerGas: ${unsignedUserOp.maxFeePerGas}]`,
321+
);
322+
// token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying
323+
await job.moveToDelayed(retryAt.getTime(), token);
324+
// throwing delayed error is required to notify bullmq worker not to complete or fail the job
325+
throw new DelayedError("Delaying job due to gas fee override");
326+
}
327+
}
328+
303329
// Handle if `maxFeePerGas` is overridden.
304330
// Set it if the transaction will be sent, otherwise delay the job.
305-
if (
306-
typeof overrides?.maxFeePerGas !== "undefined" &&
307-
unsignedUserOp.maxFeePerGas
308-
) {
309-
if (overrides.maxFeePerGas > unsignedUserOp.maxFeePerGas) {
310-
unsignedUserOp.maxFeePerGas = overrides.maxFeePerGas;
311-
} else {
331+
const overrideMaxFeePerGas = overrides?.maxFeePerGas;
332+
if (typeof overrideMaxFeePerGas !== "undefined") {
333+
if (unsignedUserOp.maxFeePerGas > overrideMaxFeePerGas) {
312334
const retryAt = _minutesFromNow(5);
313335
job.log(
314-
`Override gas fee (${overrides.maxFeePerGas}) is lower than onchain fee (${unsignedUserOp.maxFeePerGas}). Delaying job until ${retryAt}.`,
336+
`Override gas fee (${overrideMaxFeePerGas}) is lower than onchain fee (${unsignedUserOp.maxFeePerGas}). Delaying job until ${retryAt}.`,
315337
);
316338
// token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying
317339
await job.moveToDelayed(retryAt.getTime(), token);
@@ -331,7 +353,9 @@ const _sendUserOp = async (
331353
userOp: unsignedUserOp,
332354
});
333355
} catch (error) {
334-
const errorMessage = `${wrapError(error, "Bundler").message} Failed to sign prepared userop`;
356+
const errorMessage = `${
357+
wrapError(error, "Bundler").message
358+
} Failed to sign prepared userop`;
335359
const erroredTransaction: ErroredTransaction = {
336360
...queuedTransaction,
337361
status: "errored",
@@ -356,7 +380,9 @@ const _sendUserOp = async (
356380
},
357381
});
358382
} catch (error) {
359-
const errorMessage = `${wrapError(error, "Bundler").message} Failed to bundle userop`;
383+
const errorMessage = `${
384+
wrapError(error, "Bundler").message
385+
} Failed to bundle userop`;
360386
const erroredTransaction: ErroredTransaction = {
361387
...queuedTransaction,
362388
status: "errored",
@@ -478,6 +504,32 @@ const _sendTransaction = async (
478504
}
479505
}
480506

507+
// Handle if `gasFeeCeiling` is overridden.
508+
// Delay the job if the estimated cost is higher than the gas fee ceiling.
509+
const gasFeeCeiling = overrides?.gasFeeCeiling;
510+
if (typeof gasFeeCeiling !== "undefined") {
511+
let estimatedCost = 0n;
512+
513+
if (populatedTransaction.maxFeePerGas) {
514+
estimatedCost =
515+
populatedTransaction.maxFeePerGas * populatedTransaction.gas;
516+
} else if (populatedTransaction.gasPrice) {
517+
estimatedCost = populatedTransaction.gas * populatedTransaction.gasPrice;
518+
}
519+
520+
// in case neither of the estimations work, the estimatedCost will be 0n, so this check should not pass, and transaction remains unaffected
521+
if (estimatedCost > gasFeeCeiling) {
522+
const retryAt = _minutesFromNow(5);
523+
job.log(
524+
`Override gas fee ceiling (${gasFeeCeiling}) is lower than onchain estimated cost (${estimatedCost}). Delaying job until ${retryAt}. [gas: ${populatedTransaction.gas}, gasPrice: ${populatedTransaction.gasPrice}, maxFeePerGas: ${populatedTransaction.maxFeePerGas}]`,
525+
);
526+
// token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying
527+
await job.moveToDelayed(retryAt.getTime(), token);
528+
// throwing delayed error is required to notify bullmq worker not to complete or fail the job
529+
throw new DelayedError("Delaying job due to gas fee override");
530+
}
531+
}
532+
481533
// Acquire an unused nonce for this transaction.
482534
const { nonce, isRecycledNonce } = await acquireNonce({
483535
queueId,
@@ -495,8 +547,9 @@ const _sendTransaction = async (
495547
// This call throws if the RPC rejects the transaction.
496548
let transactionHash: Hex;
497549
try {
498-
const sendTransactionResult =
499-
await account.sendTransaction(populatedTransaction);
550+
const sendTransactionResult = await account.sendTransaction(
551+
populatedTransaction,
552+
);
500553
transactionHash = sendTransactionResult.transactionHash;
501554
} catch (error: unknown) {
502555
// If the nonce is already seen onchain (nonce too low) or in mempool (replacement underpriced),

0 commit comments

Comments
 (0)