Skip to content

Commit

Permalink
feat: Add retryFailedTransaction route to transaction API (#632)
Browse files Browse the repository at this point in the history
* feat: Add retryFailedTransaction route to transaction API

* fix: Update retryFailedTransaction route URL to /transaction/retry-failed

* feat: Refactor retryFailedTransaction route to improve error handling

* feat: Register retryFailedTransaction route in transaction API

* allow retrying queued -> failed transactions
  • Loading branch information
d4mr authored Aug 30, 2024
1 parent ee21c4a commit 9fd4d9c
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { cancelTransaction } from "./transaction/cancel";
import { getAllTx } from "./transaction/getAll";
import { getAllDeployedContracts } from "./transaction/getAllDeployedContracts";
import { retryTransaction } from "./transaction/retry";
import { retryFailedTransaction } from "./transaction/retry-failed";
import { checkTxStatus } from "./transaction/status";
import { syncRetryTransaction } from "./transaction/syncRetry";
import { createWebhook } from "./webhooks/create";
Expand Down Expand Up @@ -216,6 +217,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
await fastify.register(getAllDeployedContracts);
await fastify.register(retryTransaction);
await fastify.register(syncRetryTransaction);
await fastify.register(retryFailedTransaction);
await fastify.register(cancelTransaction);
await fastify.register(sendSignedTransaction);
await fastify.register(sendSignedUserOp);
Expand Down
135 changes: 135 additions & 0 deletions src/server/routes/transaction/retry-failed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Static, Type } from "@sinclair/typebox";
import { 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 { SendTransactionQueue } from "../../../worker/queues/sendTransactionQueue";
import { createCustomError } from "../../middleware/error";
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";

const requestBodySchema = Type.Object({
queueId: Type.String({
description: "Transaction queue ID",
examples: ["9eb88b00-f04f-409b-9df7-7dcc9003bc35"],
}),
});

export const responseBodySchema = Type.Object({
result: Type.Object({
message: Type.String(),
status: Type.String(),
}),
});

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

export async function retryFailedTransaction(fastify: FastifyInstance) {
fastify.route<{
Body: Static<typeof requestBodySchema>;
Reply: Static<typeof responseBodySchema>;
}>({
method: "POST",
url: "/transaction/retry-failed",
schema: {
summary: "Retry failed transaction",
description: "Retry a failed transaction",
tags: ["Transaction"],
operationId: "retryFailed",
body: requestBodySchema,
response: {
...standardResponseSchema,
[StatusCodes.OK]: responseBodySchema,
},
},
handler: async (request, reply) => {
const { queueId } = request.body;

const transaction = await TransactionDB.get(queueId);
if (!transaction) {
throw createCustomError(
"Transaction not found.",
StatusCodes.BAD_REQUEST,
"TRANSACTION_NOT_FOUND",
);
}
if (transaction.status !== "errored") {
throw createCustomError(
`Transaction cannot be retried because status: ${transaction.status}`,
StatusCodes.BAD_REQUEST,
"TRANSACTION_CANNOT_BE_RETRIED",
);
}

// temp do not handle userop
if (transaction.isUserOp) {
throw createCustomError(
`Transaction cannot be retried because it is a userop`,
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",
);
}
}

const job = await SendTransactionQueue.q.getJob(
SendTransactionQueue.jobId({
queueId: transaction.queueId,
resendCount: 0,
}),
);

if (job) {
await job.remove();
}

await SendTransactionQueue.add({
queueId: transaction.queueId,
resendCount: 0,
});

reply.status(StatusCodes.OK).send({
result: {
message: `Transaction queued for retry with queueId: ${queueId}`,
status: "success",
},
});
},
});
}
8 changes: 7 additions & 1 deletion src/utils/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export type QueuedTransaction = InsertedTransaction & {
queuedAt: Date;
value: bigint;
data?: Hex;

manuallyResentAt?: Date;
};

// SentTransaction has been submitted to RPC successfully.
Expand Down Expand Up @@ -83,7 +85,11 @@ export type MinedTransaction = (

// ErroredTransaction received an error before or while sending to RPC.
// A transaction that reverted onchain is not considered "errored".
export type ErroredTransaction = Omit<QueuedTransaction, "status"> & {
export type ErroredTransaction = (
| Omit<QueuedTransaction, "status">
| Omit<_SentTransactionEOA, "status">
| Omit<_SentTransactionUserOp, "status">
) & {
status: "errored";

errorMessage: string;
Expand Down
11 changes: 10 additions & 1 deletion src/worker/tasks/sendTransactionWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ const handler: Processor<any, void, string> = async (job: Job<string>) => {
// An errored queued transaction (resendCount = 0) is safe to retry: the transaction wasn't sent to RPC.
if (transaction.status === "errored" && resendCount === 0) {
transaction = {
...transaction,
...{
...transaction,
nonce: undefined,
errorMessage: undefined,
gas: undefined,
gasPrice: undefined,
maxFeePerGas: undefined,
maxPriorityFeePerGas: undefined,
},
status: "queued",
manuallyResentAt: new Date(),
} satisfies QueuedTransaction;
}

Expand Down

0 comments on commit 9fd4d9c

Please sign in to comment.