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
73 changes: 73 additions & 0 deletions src/lib/transaction/get-transaction-receipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import assert from "node:assert";
import { eth_getTransactionReceipt, getRpcClient } from "thirdweb";
import type { UserOperationReceipt } from "thirdweb/dist/types/wallets/smart/types";
import type { TransactionReceipt } from "thirdweb/transaction";
import { getUserOpReceiptRaw } from "thirdweb/wallets/smart";
import { getChain } from "../../utils/chain";
import { thirdwebClient } from "../../utils/sdk";
import type { AnyTransaction } from "../../utils/transaction/types";

/**
* Returns the transaction receipt for a given transaction, or null if not found.
* @param transaction
* @returns TransactionReceipt | null
*/
export async function getReceiptForEOATransaction(
transaction: AnyTransaction,
): Promise<TransactionReceipt | null> {
assert(!transaction.isUserOp);

if (!("sentTransactionHashes" in transaction)) {
return null;
}

const rpcRequest = getRpcClient({
client: thirdwebClient,
chain: await getChain(transaction.chainId),
});

// Get the receipt for each transaction hash (in batches).
// Return if any receipt is found.
const BATCH_SIZE = 10;
for (
let i = 0;
i < transaction.sentTransactionHashes.length;
i += BATCH_SIZE
) {
const batch = transaction.sentTransactionHashes.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(
batch.map((hash) => eth_getTransactionReceipt(rpcRequest, { hash })),
);

for (const result of results) {
if (result.status === "fulfilled") {
return result.value;
}
}
}

return null;
}

/**
* Returns the user operation receipt for a given transaction, or null if not found.
* The transaction receipt is available in the result under `result.receipt`.
* @param transaction
* @returns UserOperationReceipt | null
*/
export async function getReceiptForUserOp(
transaction: AnyTransaction,
): Promise<UserOperationReceipt | null> {
assert(transaction.isUserOp);

if (!("userOpHash" in transaction)) {
return null;
}

const receipt = await getUserOpReceiptRaw({
client: thirdwebClient,
chain: await getChain(transaction.chainId),
userOpHash: transaction.userOpHash,
});
return receipt ?? null;
}
8 changes: 4 additions & 4 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ import { cancelTransaction } from "./transaction/cancel";
import { getAllTransactions } from "./transaction/getAll";
import { getAllDeployedContracts } from "./transaction/getAllDeployedContracts";
import { retryTransaction } from "./transaction/retry";
import { retryFailedTransaction } from "./transaction/retry-failed";
import { retryFailedTransactionRoute } from "./transaction/retry-failed";
import { checkTxStatus } from "./transaction/status";
import { syncRetryTransaction } from "./transaction/syncRetry";
import { syncRetryTransactionRoute } from "./transaction/sync-retry";
import { createWebhookRoute } from "./webhooks/create";
import { getWebhooksEventTypes } from "./webhooks/events";
import { getAllWebhooksData } from "./webhooks/getAll";
Expand Down Expand Up @@ -224,8 +224,8 @@ export async function withRoutes(fastify: FastifyInstance) {
await fastify.register(checkTxStatus);
await fastify.register(getAllDeployedContracts);
await fastify.register(retryTransaction);
await fastify.register(syncRetryTransaction);
await fastify.register(retryFailedTransaction);
await fastify.register(syncRetryTransactionRoute);
await fastify.register(retryFailedTransactionRoute);
await fastify.register(cancelTransaction);
await fastify.register(sendSignedTransaction);
await fastify.register(sendSignedUserOp);
Expand Down
78 changes: 29 additions & 49 deletions src/server/routes/transaction/retry-failed.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { Type, type Static } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { eth_getTransactionReceipt, getRpcClient } from "thirdweb";
import { TransactionDB } from "../../../db/transactions/db";
import { getChain } from "../../../utils/chain";
import { thirdwebClient } from "../../../utils/sdk";
import {
getReceiptForEOATransaction,
getReceiptForUserOp,
} from "../../../lib/transaction/get-transaction-receipt";
import type { QueuedTransaction } from "../../../utils/transaction/types";
import { MineTransactionQueue } from "../../../worker/queues/mineTransactionQueue";
import { SendTransactionQueue } from "../../../worker/queues/sendTransactionQueue";
import { createCustomError } from "../../middleware/error";
Expand All @@ -26,13 +28,12 @@ export const responseBodySchema = Type.Object({

responseBodySchema.example = {
result: {
message:
"Transaction queued for retry with queueId: a20ed4ce-301d-4251-a7af-86bd88f6c015",
message: "Sent transaction to be retried.",
status: "success",
},
};

export async function retryFailedTransaction(fastify: FastifyInstance) {
export async function retryFailedTransactionRoute(fastify: FastifyInstance) {
fastify.route<{
Body: Static<typeof requestBodySchema>;
Reply: Static<typeof responseBodySchema>;
Expand Down Expand Up @@ -63,69 +64,48 @@ export async function retryFailedTransaction(fastify: FastifyInstance) {
}
if (transaction.status !== "errored") {
throw createCustomError(
`Transaction cannot be retried because status: ${transaction.status}`,
`Cannot retry a transaction with status ${transaction.status}.`,
StatusCodes.BAD_REQUEST,
"TRANSACTION_CANNOT_BE_RETRIED",
);
}

if (transaction.isUserOp) {
const receipt = transaction.isUserOp
? await getReceiptForUserOp(transaction)
: await getReceiptForEOATransaction(transaction);
if (receipt) {
throw createCustomError(
"Transaction cannot be retried because it is a userop",
"Cannot retry a transaction that is already mined.",
StatusCodes.BAD_REQUEST,
"TRANSACTION_CANNOT_BE_RETRIED",
);
}

const rpcRequest = getRpcClient({
client: thirdwebClient,
chain: await getChain(transaction.chainId),
});

// if transaction has sentTransactionHashes, we need to check if any of them are mined
if ("sentTransactionHashes" in transaction) {
const receiptPromises = transaction.sentTransactionHashes.map(
(hash) => {
// if receipt is not found, it will throw an error
// so we catch it and return null
return eth_getTransactionReceipt(rpcRequest, {
hash,
}).catch(() => null);
},
);

const receipts = await Promise.all(receiptPromises);

// If any of the transactions are mined, we should not retry.
const minedReceipt = receipts.find((receipt) => !!receipt);

if (minedReceipt) {
throw createCustomError(
`Transaction cannot be retried because it has already been mined with hash: ${minedReceipt.transactionHash}`,
StatusCodes.BAD_REQUEST,
"TRANSACTION_CANNOT_BE_RETRIED",
);
}
}

// Remove existing jobs.
const sendJob = await SendTransactionQueue.q.getJob(
SendTransactionQueue.jobId({
queueId: transaction.queueId,
resendCount: 0,
}),
);
if (sendJob) {
await sendJob.remove();
}
await sendJob?.remove();

const mineJob = await MineTransactionQueue.q.getJob(
MineTransactionQueue.jobId({
queueId: transaction.queueId,
}),
);
if (mineJob) {
await mineJob.remove();
}
await mineJob?.remove();

// Reset the failed job as "queued" and re-enqueue it.
const { errorMessage, ...omitted } = transaction;
const queuedTransaction: QueuedTransaction = {
...omitted,
status: "queued",
queuedAt: new Date(),
resendCount: 0,
};
await TransactionDB.set(queuedTransaction);

await SendTransactionQueue.add({
queueId: transaction.queueId,
Expand All @@ -134,7 +114,7 @@ export async function retryFailedTransaction(fastify: FastifyInstance) {

reply.status(StatusCodes.OK).send({
result: {
message: `Transaction queued for retry with queueId: ${queueId}`,
message: "Sent transaction to be retried.",
status: "success",
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { toSerializableTransaction } from "thirdweb";
import { TransactionDB } from "../../../db/transactions/db";
import { getReceiptForEOATransaction } from "../../../lib/transaction/get-transaction-receipt";
import { getAccount } from "../../../utils/account";
import { getBlockNumberish } from "../../../utils/block";
import { getChain } from "../../../utils/chain";
Expand All @@ -15,7 +16,6 @@ import { createCustomError } from "../../middleware/error";
import { TransactionHashSchema } from "../../schemas/address";
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";

// INPUT
const requestBodySchema = Type.Object({
queueId: Type.String({
description: "Transaction queue ID",
Expand All @@ -25,7 +25,6 @@ const requestBodySchema = Type.Object({
maxPriorityFeePerGas: Type.Optional(Type.String()),
});

// OUTPUT
export const responseBodySchema = Type.Object({
result: Type.Object({
transactionHash: TransactionHashSchema,
Expand All @@ -39,7 +38,7 @@ responseBodySchema.example = {
},
};

export async function syncRetryTransaction(fastify: FastifyInstance) {
export async function syncRetryTransactionRoute(fastify: FastifyInstance) {
fastify.route<{
Body: Static<typeof requestBodySchema>;
Reply: Static<typeof responseBodySchema>;
Expand Down Expand Up @@ -69,6 +68,7 @@ export async function syncRetryTransaction(fastify: FastifyInstance) {
"TRANSACTION_NOT_FOUND",
);
}

if (transaction.isUserOp || !("nonce" in transaction)) {
throw createCustomError(
"Transaction cannot be retried.",
Expand All @@ -77,6 +77,15 @@ export async function syncRetryTransaction(fastify: FastifyInstance) {
);
}

const receipt = await getReceiptForEOATransaction(transaction);
if (receipt) {
throw createCustomError(
"Cannot retry a transaction that is already mined.",
StatusCodes.BAD_REQUEST,
"TRANSACTION_CANNOT_BE_RETRIED",
);
}

const { chainId, from } = transaction;

// Prepare transaction.
Expand Down
2 changes: 0 additions & 2 deletions src/utils/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ export type QueuedTransaction = InsertedTransaction & {
queuedAt: Date;
value: bigint;
data?: Hex;

manuallyResentAt?: Date;
};

// SentTransaction has been submitted to RPC successfully.
Expand Down
Loading
Loading