Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/server/schemas/tx-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
1 change: 1 addition & 0 deletions src/server/utils/transaction-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/shared/db/transactions/queue-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface QueueTxParams {
gas?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
gasFeeCeiling?: string;
value?: string;
};
}
Expand Down
1 change: 1 addition & 0 deletions src/shared/utils/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type InsertedTransaction = {
gasPrice?: bigint;
maxFeePerGas?: bigint;
maxPriorityFeePerGas?: bigint;
gasFeeCeiling?: bigint;
};
timeoutSeconds?: number;

Expand Down
81 changes: 67 additions & 14 deletions src/worker/tasks/send-transaction-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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);
Comment on lines +311 to +315
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we do this elsewhere? might be useful to abstract into some helper and explain this formula

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't do it anywhere else, if/when we do I'll extract it out


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);
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand Down