Skip to content
Closed
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
23 changes: 23 additions & 0 deletions src/server/schemas/transaction/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Type } from "@sinclair/typebox";

const BaseDelaySchema = Type.Object({
timestamp: Type.String({
description: "ISO timestamp when the delay occurred",
}),
});

// Specific delay schemas with their required properties
const GasDelaySchema = Type.Intersect([
BaseDelaySchema,
Type.Object({
reason: Type.Literal("max_fee_per_gas_too_low"),
requestedMaxFeePerGas: Type.String({
description: "maxFeePerGas requested by the user",
}),
currentMaxFeePerGas: Type.String({
description: "maxFeePerGas on chain",
}),
}),
]);

export const TransactionDelaySchema = Type.Union([GasDelaySchema]);
22 changes: 21 additions & 1 deletion src/server/schemas/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Hex } from "thirdweb";
import { stringify } from "thirdweb/utils";
import type { AnyTransaction } from "../../../shared/utils/transaction/types";
import { AddressSchema, TransactionHashSchema } from "../address";
import { TransactionDelaySchema } from "./delay";

export const TransactionSchema = Type.Object({
queueId: Type.Union([
Expand Down Expand Up @@ -220,6 +221,10 @@ export const TransactionSchema = Type.Object({
),
Type.Null(),
]),
delays: Type.Array(TransactionDelaySchema, {
description:
"Array of deliberate transaction processing delays delays initiated by the worker due to user specified conditions",
}),
});

export const toTransactionSchema = (
Expand Down Expand Up @@ -304,6 +309,20 @@ export const toTransactionSchema = (
return transaction.overrides?.maxPriorityFeePerGas?.toString() ?? null;
};

const resolveDelays = (): Static<typeof TransactionDelaySchema>[] => {
return transaction.delays.map((delay) => {
switch (delay.reason) {
case "max_fee_per_gas_too_low":
return {
reason: "max_fee_per_gas_too_low",
timestamp: delay.timestamp.toISOString(),
requestedMaxFeePerGas: delay.requestedMaxFeePerGas.toString(),
currentMaxFeePerGas: delay.currentMaxFeePerGas.toString(),
};
}
});
};

return {
queueId: transaction.queueId,
status: transaction.status,
Expand Down Expand Up @@ -338,7 +357,7 @@ export const toTransactionSchema = (
? transaction.cancelledAt.toISOString()
: null,
errorMessage:
"errorMessage" in transaction ? (transaction.errorMessage ?? null) : null,
"errorMessage" in transaction ? transaction.errorMessage ?? null : null,
sentAtBlockNumber:
"sentAtBlock" in transaction ? Number(transaction.sentAtBlock) : null,
blockNumber:
Expand Down Expand Up @@ -373,6 +392,7 @@ export const toTransactionSchema = (
"userOpHash" in transaction ? (transaction.userOpHash as Hex) : null,

batchOperations: resolveBatchOperations(),
delays: resolveDelays(),

// Deprecated
retryGasValues: null,
Expand Down
2 changes: 1 addition & 1 deletion src/shared/utils/transaction/insert-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
WalletDetailsError,
type ParsedWalletDetails,
} from "../../../shared/db/wallets/get-wallet-details";
import { doesChainSupportService } from "../../lib/chain/chain-capabilities";
import { createCustomError } from "../../../server/middleware/error";
import { SendTransactionQueue } from "../../../worker/queues/send-transaction-queue";
import { getChecksumAddress } from "../primitive-types";
Expand Down Expand Up @@ -50,6 +49,7 @@ export const insertTransaction = async (
queueId,
queuedAt: new Date(),
resendCount: 0,
delays: [],

from: getChecksumAddress(insertedTransaction.from),
to: getChecksumAddress(insertedTransaction.to),
Expand Down
10 changes: 10 additions & 0 deletions src/shared/utils/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ export type BatchOperation = {
functionArgs?: unknown[];
};

// we want to support more reasons for delays in the future, this will become a discriminated union type
export type TransactionDelay = {
reason: "max_fee_per_gas_too_low";
timestamp: Date;

requestedMaxFeePerGas: bigint;
currentMaxFeePerGas: bigint;
};

// InsertedTransaction is the raw input from the caller.
export type InsertedTransaction = {
isUserOp: boolean;
Expand Down Expand Up @@ -66,6 +75,7 @@ export type InsertedTransaction = {
export type QueuedTransaction = InsertedTransaction & {
status: "queued";

delays: TransactionDelay[];
resendCount: number;
queueId: string;
queuedAt: Date;
Expand Down
22 changes: 17 additions & 5 deletions src/worker/tasks/send-transaction-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const handler: Processor<string, void, string> = async (job: Job<string>) => {
}

let resultTransaction:
| QueuedTransaction // Transaction delayed and will be retried.
| SentTransaction // Transaction sent successfully.
| ErroredTransaction // Transaction failed and will not be retried.
| null; // No attempt to send is made.
Expand Down Expand Up @@ -282,7 +283,7 @@ const _sendUserOp = async (
const _sendTransaction = async (
job: Job,
queuedTransaction: QueuedTransaction,
): Promise<SentTransaction | ErroredTransaction | null> => {
): Promise<QueuedTransaction | SentTransaction | ErroredTransaction | null> => {
assert(!queuedTransaction.isUserOp);

if (_hasExceededTimeout(queuedTransaction)) {
Expand Down Expand Up @@ -372,7 +373,15 @@ const _sendTransaction = async (
`Override gas fee (${overrides.maxFeePerGas}) is lower than onchain fee (${populatedTransaction.maxFeePerGas}). Delaying job until ${retryAt}.`,
);
await job.moveToDelayed(retryAt.getTime());
return null;

queuedTransaction.delays.push({
reason: "max_fee_per_gas_too_low",
currentMaxFeePerGas: populatedTransaction.maxFeePerGas,
requestedMaxFeePerGas: overrides.maxFeePerGas,
timestamp: new Date(),
});

return queuedTransaction;
}
}

Expand All @@ -384,15 +393,18 @@ const _sendTransaction = async (
});
populatedTransaction.nonce = nonce;
job.log(
`Populated transaction (isRecycledNonce=${isRecycledNonce}): ${stringify(populatedTransaction)}`,
`Populated transaction (isRecycledNonce=${isRecycledNonce}): ${stringify(
populatedTransaction,
)}`,
);

// Send transaction to RPC.
// 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
Loading