From f1e09f4fdb023a8094de8b55ecb034845d05ed9d Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Fri, 4 Jul 2025 14:27:51 +0530 Subject: [PATCH] feat: add gasFeeCeiling to transaction overrides and handling logic --- src/server/schemas/tx-overrides.ts | 7 ++ src/server/utils/transaction-overrides.ts | 1 + src/shared/db/transactions/queue-tx.ts | 1 + src/shared/utils/transaction/types.ts | 1 + src/worker/tasks/send-transaction-worker.ts | 81 +++++++++++++++++---- 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/server/schemas/tx-overrides.ts b/src/server/schemas/tx-overrides.ts index cef4a1574..cb4c88171 100644 --- a/src/server/schemas/tx-overrides.ts +++ b/src/server/schemas/tx-overrides.ts @@ -23,6 +23,13 @@ export const txOverridesSchema = Type.Object({ ...WeiAmountStringSchema, description: "Maximum priority fee per gas", }), + + gasFeeCeiling: Type.Optional({ + ...WeiAmountStringSchema, + description: + "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.", + }), + timeoutSeconds: Type.Optional( Type.Integer({ examples: ["7200"], diff --git a/src/server/utils/transaction-overrides.ts b/src/server/utils/transaction-overrides.ts index 06575dc49..8292abb56 100644 --- a/src/server/utils/transaction-overrides.ts +++ b/src/server/utils/transaction-overrides.ts @@ -20,6 +20,7 @@ export const parseTransactionOverrides = ( gasPrice: maybeBigInt(overrides.gasPrice), maxFeePerGas: maybeBigInt(overrides.maxFeePerGas), maxPriorityFeePerGas: maybeBigInt(overrides.maxPriorityFeePerGas), + gasFeeCeiling: maybeBigInt(overrides.gasFeeCeiling), }, timeoutSeconds: overrides.timeoutSeconds, // `value` may not be in the overrides object. diff --git a/src/shared/db/transactions/queue-tx.ts b/src/shared/db/transactions/queue-tx.ts index 508e5207e..50e95d2c8 100644 --- a/src/shared/db/transactions/queue-tx.ts +++ b/src/shared/db/transactions/queue-tx.ts @@ -23,6 +23,7 @@ interface QueueTxParams { gas?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; + gasFeeCeiling?: string; value?: string; }; } diff --git a/src/shared/utils/transaction/types.ts b/src/shared/utils/transaction/types.ts index f45c947ba..978882196 100644 --- a/src/shared/utils/transaction/types.ts +++ b/src/shared/utils/transaction/types.ts @@ -45,6 +45,7 @@ export type InsertedTransaction = { gasPrice?: bigint; maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint; + gasFeeCeiling?: bigint; }; timeoutSeconds?: number; diff --git a/src/worker/tasks/send-transaction-worker.ts b/src/worker/tasks/send-transaction-worker.ts index 836798c9c..21615b5a4 100644 --- a/src/worker/tasks/send-transaction-worker.ts +++ b/src/worker/tasks/send-transaction-worker.ts @@ -210,7 +210,9 @@ const _sendUserOp = async ( }); accountFactoryAddress = getAddress(onchainAccountFactoryAddress); } catch (error) { - const errorMessage = `${wrapError(error, "RPC").message} Failed to find factory address for account`; + const errorMessage = `${ + wrapError(error, "RPC").message + } Failed to find factory address for account`; const erroredTransaction: ErroredTransaction = { ...queuedTransaction, status: "errored", @@ -233,7 +235,9 @@ const _sendUserOp = async ( chain, ); } catch (error) { - const errorMessage = `${wrapError(error, "RPC").message} Failed to find entrypoint address for account factory`; + const errorMessage = `${ + wrapError(error, "RPC").message + } Failed to find entrypoint address for account factory`; const erroredTransaction: ErroredTransaction = { ...queuedTransaction, status: "errored", @@ -300,18 +304,36 @@ const _sendUserOp = async ( return erroredTransaction; } + // Handle if `gasFeeCeiling` is overridden. + // Delay the job if the estimated cost is higher than the gas fee ceiling. + const gasFeeCeiling = overrides?.gasFeeCeiling; + if (typeof gasFeeCeiling !== "undefined") { + const estimatedCost = + unsignedUserOp.maxFeePerGas * + (unsignedUserOp.callGasLimit + + unsignedUserOp.preVerificationGas + + unsignedUserOp.verificationGasLimit); + + if (estimatedCost > gasFeeCeiling) { + const retryAt = _minutesFromNow(5); + job.log( + `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}]`, + ); + // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying + await job.moveToDelayed(retryAt.getTime(), token); + // throwing delayed error is required to notify bullmq worker not to complete or fail the job + throw new DelayedError("Delaying job due to gas fee override"); + } + } + // Handle if `maxFeePerGas` is overridden. // Set it if the transaction will be sent, otherwise delay the job. - if ( - typeof overrides?.maxFeePerGas !== "undefined" && - unsignedUserOp.maxFeePerGas - ) { - if (overrides.maxFeePerGas > unsignedUserOp.maxFeePerGas) { - unsignedUserOp.maxFeePerGas = overrides.maxFeePerGas; - } else { + const overrideMaxFeePerGas = overrides?.maxFeePerGas; + if (typeof overrideMaxFeePerGas !== "undefined") { + if (unsignedUserOp.maxFeePerGas > overrideMaxFeePerGas) { const retryAt = _minutesFromNow(5); job.log( - `Override gas fee (${overrides.maxFeePerGas}) is lower than onchain fee (${unsignedUserOp.maxFeePerGas}). Delaying job until ${retryAt}.`, + `Override gas fee (${overrideMaxFeePerGas}) is lower than onchain fee (${unsignedUserOp.maxFeePerGas}). Delaying job until ${retryAt}.`, ); // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying await job.moveToDelayed(retryAt.getTime(), token); @@ -331,7 +353,9 @@ const _sendUserOp = async ( userOp: unsignedUserOp, }); } catch (error) { - const errorMessage = `${wrapError(error, "Bundler").message} Failed to sign prepared userop`; + const errorMessage = `${ + wrapError(error, "Bundler").message + } Failed to sign prepared userop`; const erroredTransaction: ErroredTransaction = { ...queuedTransaction, status: "errored", @@ -356,7 +380,9 @@ const _sendUserOp = async ( }, }); } catch (error) { - const errorMessage = `${wrapError(error, "Bundler").message} Failed to bundle userop`; + const errorMessage = `${ + wrapError(error, "Bundler").message + } Failed to bundle userop`; const erroredTransaction: ErroredTransaction = { ...queuedTransaction, status: "errored", @@ -478,6 +504,32 @@ const _sendTransaction = async ( } } + // Handle if `gasFeeCeiling` is overridden. + // Delay the job if the estimated cost is higher than the gas fee ceiling. + const gasFeeCeiling = overrides?.gasFeeCeiling; + if (typeof gasFeeCeiling !== "undefined") { + let estimatedCost = 0n; + + if (populatedTransaction.maxFeePerGas) { + estimatedCost = + populatedTransaction.maxFeePerGas * populatedTransaction.gas; + } else if (populatedTransaction.gasPrice) { + estimatedCost = populatedTransaction.gas * populatedTransaction.gasPrice; + } + + // in case neither of the estimations work, the estimatedCost will be 0n, so this check should not pass, and transaction remains unaffected + if (estimatedCost > gasFeeCeiling) { + const retryAt = _minutesFromNow(5); + job.log( + `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}]`, + ); + // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying + await job.moveToDelayed(retryAt.getTime(), token); + // throwing delayed error is required to notify bullmq worker not to complete or fail the job + throw new DelayedError("Delaying job due to gas fee override"); + } + } + // Acquire an unused nonce for this transaction. const { nonce, isRecycledNonce } = await acquireNonce({ queueId, @@ -495,8 +547,9 @@ const _sendTransaction = async ( // This call throws if the RPC rejects the transaction. let transactionHash: Hex; try { - const sendTransactionResult = - await account.sendTransaction(populatedTransaction); + const sendTransactionResult = await account.sendTransaction( + populatedTransaction, + ); transactionHash = sendTransactionResult.transactionHash; } catch (error: unknown) { // If the nonce is already seen onchain (nonce too low) or in mempool (replacement underpriced),