Skip to content

Commit

Permalink
use transaction receipt helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
arcoraven committed Nov 28, 2024
1 parent 4f5170a commit 55ccd0d
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 169 deletions.
64 changes: 64 additions & 0 deletions src/lib/transaction/get-transaction-receipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 getTransactionReceiptFromEOATransaction(
transaction: AnyTransaction,
): Promise<TransactionReceipt | null> {
assert(!transaction.isUserOp);

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

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

const results = await Promise.allSettled(
transaction.sentTransactionHashes.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 getUserOpReceiptFromTransaction(
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;
}
6 changes: 3 additions & 3 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ import { cancelTransaction } from "./transaction/cancel";
import { getAllTransactions } from "./transaction/getAll";
import { getAllDeployedContracts } from "./transaction/getAllDeployedContracts";
import { retryTransaction } from "./transaction/retry";
import { retryFailedTransactionRoute } from "./transaction/retryFailed";
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 @@ -222,7 +222,7 @@ 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(syncRetryTransactionRoute);
await fastify.register(retryFailedTransactionRoute);
await fastify.register(cancelTransaction);
await fastify.register(sendSignedTransaction);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Type, type Static } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import assert from "node:assert";
import { eth_getTransactionReceipt, getRpcClient } from "thirdweb";
import { getUserOpReceiptRaw } from "thirdweb/dist/types/wallets/smart/lib/bundler";
import { TransactionDB } from "../../../db/transactions/db";
import { getChain } from "../../../utils/chain";
import { thirdwebClient } from "../../../utils/sdk";
import type { ErroredTransaction } from "../../../utils/transaction/types";
import {
getTransactionReceiptFromEOATransaction,
getUserOpReceiptFromTransaction,
} 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 Down Expand Up @@ -71,35 +70,41 @@ export async function retryFailedTransactionRoute(fastify: FastifyInstance) {
);
}

const isMined = transaction.isUserOp
? await isUserOpMined(transaction)
: await isTransactionMined(transaction);
if (isMined) {
const receipt = transaction.isUserOp
? await getUserOpReceiptFromTransaction(transaction)
: await getTransactionReceiptFromEOATransaction(transaction);
if (receipt) {
throw createCustomError(
"Cannot retry a transaction that is already mined.",
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",
resendCount: 0,
};
await TransactionDB.set(queuedTransaction);

await SendTransactionQueue.add({
queueId: transaction.queueId,
Expand All @@ -115,44 +120,3 @@ export async function retryFailedTransactionRoute(fastify: FastifyInstance) {
},
});
}

async function isTransactionMined(transaction: ErroredTransaction) {
assert(!transaction.isUserOp);

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

const rpcRequest = getRpcClient({
client: thirdwebClient,
chain: await getChain(transaction.chainId),
});
const promises = transaction.sentTransactionHashes.map((hash) =>
eth_getTransactionReceipt(rpcRequest, { hash }),
);
const results = await Promise.allSettled(promises);

// If any eth_getTransactionReceipt call succeeded, a valid transaction receipt was found.
for (const result of results) {
if (result.status === "fulfilled" && !!result.value.blockNumber) {
return true;
}
}

return false;
}

async function isUserOpMined(transaction: ErroredTransaction) {
assert(transaction.isUserOp);

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

const userOpReceiptRaw = await getUserOpReceiptRaw({
client: thirdwebClient,
chain: await getChain(transaction.chainId),
userOpHash: transaction.userOpHash,
});
return !!userOpReceiptRaw;
}
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 { getTransactionReceiptFromEOATransaction } 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,16 @@ export async function syncRetryTransaction(fastify: FastifyInstance) {
);
}

const receipt =
await getTransactionReceiptFromEOATransaction(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 @@ -57,8 +57,6 @@ export type QueuedTransaction = InsertedTransaction & {
queuedAt: Date;
value: bigint;
data?: Hex;

manuallyResentAt?: Date;
};

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

0 comments on commit 55ccd0d

Please sign in to comment.