From 4f5170a964e953b6c3470ab0326bc068de101d8e Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 28 Nov 2024 15:17:34 +0800 Subject: [PATCH 1/6] feat: Allow retrying a failed userop --- src/server/routes/index.ts | 4 +- .../{retry-failed.ts => retryFailed.ts} | 95 +++++++++++-------- 2 files changed, 57 insertions(+), 42 deletions(-) rename src/server/routes/transaction/{retry-failed.ts => retryFailed.ts} (60%) diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 4345859ca..7f174ce1d 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -102,7 +102,7 @@ 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/retryFailed"; import { checkTxStatus } from "./transaction/status"; import { syncRetryTransaction } from "./transaction/syncRetry"; import { createWebhookRoute } from "./webhooks/create"; @@ -223,7 +223,7 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(getAllDeployedContracts); await fastify.register(retryTransaction); await fastify.register(syncRetryTransaction); - await fastify.register(retryFailedTransaction); + await fastify.register(retryFailedTransactionRoute); await fastify.register(cancelTransaction); await fastify.register(sendSignedTransaction); await fastify.register(sendSignedUserOp); diff --git a/src/server/routes/transaction/retry-failed.ts b/src/server/routes/transaction/retryFailed.ts similarity index 60% rename from src/server/routes/transaction/retry-failed.ts rename to src/server/routes/transaction/retryFailed.ts index 0227f2ca3..12077546f 100644 --- a/src/server/routes/transaction/retry-failed.ts +++ b/src/server/routes/transaction/retryFailed.ts @@ -1,10 +1,13 @@ -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 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 { MineTransactionQueue } from "../../../worker/queues/mineTransactionQueue"; import { SendTransactionQueue } from "../../../worker/queues/sendTransactionQueue"; import { createCustomError } from "../../middleware/error"; @@ -26,13 +29,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; Reply: Static; @@ -63,51 +65,23 @@ 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 isMined = transaction.isUserOp + ? await isUserOpMined(transaction) + : await isTransactionMined(transaction); + if (isMined) { 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", - ); - } - } - const sendJob = await SendTransactionQueue.q.getJob( SendTransactionQueue.jobId({ queueId: transaction.queueId, @@ -134,10 +108,51 @@ 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", }, }); }, }); } + +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; +} From 55ccd0df519914852eca8ba983e7110bf4967540 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 28 Nov 2024 18:17:22 +0800 Subject: [PATCH 2/6] use transaction receipt helpers --- .../transaction/get-transaction-receipt.ts | 64 ++++++++ src/server/routes/index.ts | 6 +- .../{retryFailed.ts => retry-failed.ts} | 78 +++------ .../{syncRetry.ts => sync-retry.ts} | 16 +- src/utils/transaction/types.ts | 2 - src/worker/tasks/mineTransactionWorker.ts | 152 ++++++++---------- src/worker/tasks/sendTransactionWorker.ts | 19 +-- 7 files changed, 168 insertions(+), 169 deletions(-) create mode 100644 src/lib/transaction/get-transaction-receipt.ts rename src/server/routes/transaction/{retryFailed.ts => retry-failed.ts} (62%) rename src/server/routes/transaction/{syncRetry.ts => sync-retry.ts} (89%) diff --git a/src/lib/transaction/get-transaction-receipt.ts b/src/lib/transaction/get-transaction-receipt.ts new file mode 100644 index 000000000..7e8c86fc6 --- /dev/null +++ b/src/lib/transaction/get-transaction-receipt.ts @@ -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 { + 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 { + 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; +} diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 7f174ce1d..56da7142e 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -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"; @@ -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); diff --git a/src/server/routes/transaction/retryFailed.ts b/src/server/routes/transaction/retry-failed.ts similarity index 62% rename from src/server/routes/transaction/retryFailed.ts rename to src/server/routes/transaction/retry-failed.ts index 12077546f..6ab35a1a2 100644 --- a/src/server/routes/transaction/retryFailed.ts +++ b/src/server/routes/transaction/retry-failed.ts @@ -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"; @@ -71,10 +70,10 @@ 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, @@ -82,24 +81,30 @@ export async function retryFailedTransactionRoute(fastify: FastifyInstance) { ); } + // 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, @@ -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; -} diff --git a/src/server/routes/transaction/syncRetry.ts b/src/server/routes/transaction/sync-retry.ts similarity index 89% rename from src/server/routes/transaction/syncRetry.ts rename to src/server/routes/transaction/sync-retry.ts index 7185c2802..9f069c7a1 100644 --- a/src/server/routes/transaction/syncRetry.ts +++ b/src/server/routes/transaction/sync-retry.ts @@ -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"; @@ -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", @@ -25,7 +25,6 @@ const requestBodySchema = Type.Object({ maxPriorityFeePerGas: Type.Optional(Type.String()), }); -// OUTPUT export const responseBodySchema = Type.Object({ result: Type.Object({ transactionHash: TransactionHashSchema, @@ -39,7 +38,7 @@ responseBodySchema.example = { }, }; -export async function syncRetryTransaction(fastify: FastifyInstance) { +export async function syncRetryTransactionRoute(fastify: FastifyInstance) { fastify.route<{ Body: Static; Reply: Static; @@ -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.", @@ -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. diff --git a/src/utils/transaction/types.ts b/src/utils/transaction/types.ts index c4591ce8f..912952d89 100644 --- a/src/utils/transaction/types.ts +++ b/src/utils/transaction/types.ts @@ -57,8 +57,6 @@ export type QueuedTransaction = InsertedTransaction & { queuedAt: Date; value: bigint; data?: Hex; - - manuallyResentAt?: Date; }; // SentTransaction has been submitted to RPC successfully. diff --git a/src/worker/tasks/mineTransactionWorker.ts b/src/worker/tasks/mineTransactionWorker.ts index bed88e803..a3afe5a1d 100644 --- a/src/worker/tasks/mineTransactionWorker.ts +++ b/src/worker/tasks/mineTransactionWorker.ts @@ -3,17 +3,19 @@ import assert from "node:assert"; import superjson from "superjson"; import { eth_getBalance, - eth_getTransactionByHash, - eth_getTransactionReceipt, getAddress, getRpcClient, toTokens, type Address, } from "thirdweb"; import { stringify } from "thirdweb/utils"; -import { getUserOpReceipt, getUserOpReceiptRaw } from "thirdweb/wallets/smart"; +import { getUserOpReceipt } from "thirdweb/wallets/smart"; import { TransactionDB } from "../../db/transactions/db"; import { recycleNonce, removeSentNonce } from "../../db/wallets/walletNonce"; +import { + getTransactionReceiptFromEOATransaction, + getUserOpReceiptFromTransaction, +} from "../../lib/transaction/get-transaction-receipt"; import { WebhooksEventTypes } from "../../schema/webhooks"; import { getBlockNumberish } from "../../utils/block"; import { getConfig } from "../../utils/cache/getConfig"; @@ -65,7 +67,6 @@ const handler: Processor = async (job: Job) => { } if (!resultTransaction) { - job.log("Transaction is not mined yet. Check again later..."); throw new Error("NOT_CONFIRMED_YET"); } @@ -121,72 +122,62 @@ const _mineTransaction = async ( ): Promise => { assert(!sentTransaction.isUserOp); - const { queueId, chainId, sentTransactionHashes, sentAtBlock, resendCount } = - sentTransaction; - - // Check all sent transaction hashes since any of them might succeed. - const rpcRequest = getRpcClient({ - client: thirdwebClient, - chain: await getChain(chainId), - }); - job.log(`Mining transactionHashes: ${sentTransactionHashes}`); - const receiptResults = await Promise.allSettled( - sentTransactionHashes.map((hash) => - eth_getTransactionReceipt(rpcRequest, { hash }), - ), - ); + const receipt = + await getTransactionReceiptFromEOATransaction(sentTransaction); - // This transaction is mined if any receipt is found. - for (const result of receiptResults) { - if (result.status === "fulfilled") { - const receipt = result.value; - job.log(`Found receipt on block ${receipt.blockNumber}.`); - - const removed = await removeSentNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - - logger({ - level: "debug", - message: `[mineTransactionWorker] Removed nonce ${sentTransaction.nonce} from nonce-sent set: ${removed}`, - service: "worker", - }); + if (receipt) { + job.log( + `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, + ); - const errorMessage = - receipt.status === "reverted" - ? "The transaction failed onchain. See: https://portal.thirdweb.com/engine/troubleshooting" - : undefined; + const removed = await removeSentNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + logger({ + level: "debug", + message: `[mineTransactionWorker] Removed nonce ${sentTransaction.nonce} from nonce-sent set: ${removed}`, + service: "worker", + }); - return { - ...sentTransaction, - status: "mined", - transactionHash: receipt.transactionHash, - minedAt: new Date(), - minedAtBlock: receipt.blockNumber, - transactionType: receipt.type, - onchainStatus: receipt.status, - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, - cumulativeGasUsed: receipt.cumulativeGasUsed, - errorMessage, - }; - } + // Though the transaction is mined successfully, set an error message if the transaction failed onchain. + const errorMessage = + receipt.status === "reverted" + ? "The transaction failed onchain. See: https://portal.thirdweb.com/engine/troubleshooting" + : undefined; + + return { + ...sentTransaction, + status: "mined", + transactionHash: receipt.transactionHash, + minedAt: new Date(), + minedAtBlock: receipt.blockNumber, + transactionType: receipt.type, + onchainStatus: receipt.status, + gasUsed: receipt.gasUsed, + effectiveGasPrice: receipt.effectiveGasPrice, + cumulativeGasUsed: receipt.cumulativeGasUsed, + errorMessage, + }; } + // Else the transaction is not mined yet. + job.log( + `Transaction is not mined yet. Check again later. sentTransactionHashes=${sentTransaction.sentTransactionHashes}`, + ); // Resend the transaction (after some initial delay). const config = await getConfig(); - const blockNumber = await getBlockNumberish(chainId); - const ellapsedBlocks = blockNumber - sentAtBlock; + const blockNumber = await getBlockNumberish(sentTransaction.chainId); + const ellapsedBlocks = blockNumber - sentTransaction.sentAtBlock; if (ellapsedBlocks >= config.minEllapsedBlocksBeforeRetry) { job.log( - `Resending transaction after ${ellapsedBlocks} blocks. blockNumber=${blockNumber} sentAtBlock=${sentAtBlock}`, + `Resending transaction after ${ellapsedBlocks} blocks. blockNumber=${blockNumber} sentAtBlock=${sentTransaction.sentAtBlock}`, ); await SendTransactionQueue.add({ - queueId, - resendCount: resendCount + 1, + queueId: sentTransaction.queueId, + resendCount: sentTransaction.resendCount + 1, }); } @@ -199,47 +190,36 @@ const _mineUserOp = async ( ): Promise => { assert(sentTransaction.isUserOp); - const { chainId, userOpHash } = sentTransaction; - const chain = await getChain(chainId); - - job.log(`Mining userOpHash: ${userOpHash}`); - const userOpReceiptRaw = await getUserOpReceiptRaw({ - client: thirdwebClient, - chain, - userOpHash, - }); - - if (!userOpReceiptRaw) { + const userOpReceipt = await getUserOpReceiptFromTransaction(sentTransaction); + if (!userOpReceipt) { + job.log( + `UserOp is not mined yet. Check again later. userOpHash=${sentTransaction.userOpHash}`, + ); return null; } + const { receipt } = userOpReceipt; - const { transactionHash } = userOpReceiptRaw.receipt; - job.log(`Found transactionHash: ${transactionHash}`); - - const rpcRequest = getRpcClient({ client: thirdwebClient, chain }); - const transaction = await eth_getTransactionByHash(rpcRequest, { - hash: transactionHash, - }); - const receipt = await eth_getTransactionReceipt(rpcRequest, { - hash: transaction.hash, - }); + job.log( + `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, + ); let errorMessage: string | undefined; // if the userOpReceipt is not successful, try to get the parsed userOpReceipt // we expect this to fail, but we want the error message if it does - if (!userOpReceiptRaw.success) { + if (!userOpReceipt.success) { try { + const chain = await getChain(sentTransaction.chainId); const userOpReceipt = await getUserOpReceipt({ client: thirdwebClient, chain, - userOpHash, + userOpHash: sentTransaction.userOpHash, }); - await job.log(`Found userOpReceipt: ${userOpReceipt}`); + job.log(`Found userOpReceipt: ${userOpReceipt}`); } catch (e) { if (e instanceof Error) { errorMessage = e.message; - await job.log(`Failed to get userOpReceipt: ${e.message}`); + job.log(`Failed to get userOpReceipt: ${e.message}`); } else { throw e; } @@ -253,13 +233,13 @@ const _mineUserOp = async ( minedAt: new Date(), minedAtBlock: receipt.blockNumber, transactionType: receipt.type, - onchainStatus: userOpReceiptRaw.success ? "success" : "reverted", + onchainStatus: userOpReceipt.success ? "success" : "reverted", gasUsed: receipt.gasUsed, effectiveGasPrice: receipt.effectiveGasPrice, gas: receipt.gasUsed, cumulativeGasUsed: receipt.cumulativeGasUsed, - sender: userOpReceiptRaw.sender as Address, - nonce: userOpReceiptRaw.nonce.toString(), + sender: userOpReceipt.sender as Address, + nonce: userOpReceipt.nonce.toString(), errorMessage, }; }; diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index 803de187b..9c8a07f21 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -68,29 +68,12 @@ const handler: Processor = async (job: Job) => { job.data, ); - let transaction = await TransactionDB.get(queueId); + const transaction = await TransactionDB.get(queueId); if (!transaction) { job.log(`Invalid transaction state: ${stringify(transaction)}`); return; } - // The transaction may be errored if it is manually retried. - // For example, the developer retried all failed transactions during an RPC outage. - // An errored queued transaction (resendCount = 0) is safe to retry: the transaction wasn't sent to RPC. - if (transaction.status === "errored" && resendCount === 0) { - const { errorMessage, ...omitted } = transaction; - transaction = { - ...omitted, - status: "queued", - resendCount: 0, - queueId: transaction.queueId, - queuedAt: transaction.queuedAt, - value: transaction.value, - data: transaction.data, - manuallyResentAt: new Date(), - } satisfies QueuedTransaction; - } - let resultTransaction: | SentTransaction // Transaction sent successfully. | ErroredTransaction // Transaction failed and will not be retried. From ad39e9a2de0b6eb4cbb0a8c86775747a42563737 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Fri, 6 Dec 2024 05:00:18 +0800 Subject: [PATCH 3/6] query hashes in batches --- .../transaction/get-transaction-receipt.ts | 75 ++- src/server/routes/transaction/retry-failed.ts | 184 +++--- src/server/routes/transaction/sync-retry.ts | 203 +++--- src/worker/tasks/mineTransactionWorker.ts | 599 +++++++++--------- 4 files changed, 534 insertions(+), 527 deletions(-) diff --git a/src/lib/transaction/get-transaction-receipt.ts b/src/lib/transaction/get-transaction-receipt.ts index 7e8c86fc6..b95e108c8 100644 --- a/src/lib/transaction/get-transaction-receipt.ts +++ b/src/lib/transaction/get-transaction-receipt.ts @@ -12,32 +12,41 @@ import type { AnyTransaction } from "../../utils/transaction/types"; * @param transaction * @returns TransactionReceipt | null */ -export async function getTransactionReceiptFromEOATransaction( - transaction: AnyTransaction, +export async function getReceiptForEOATransaction( + transaction: AnyTransaction, ): Promise { - assert(!transaction.isUserOp); + assert(!transaction.isUserOp); - if (!("sentTransactionHashes" in transaction)) { - return null; - } + if (!("sentTransactionHashes" in transaction)) { + return null; + } - const rpcRequest = getRpcClient({ - client: thirdwebClient, - chain: await getChain(transaction.chainId), - }); + const rpcRequest = getRpcClient({ + client: thirdwebClient, + chain: await getChain(transaction.chainId), + }); - const results = await Promise.allSettled( - transaction.sentTransactionHashes.map((hash) => - eth_getTransactionReceipt(rpcRequest, { hash }), - ), - ); + // 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; + for (const result of results) { + if (result.status === "fulfilled") { + return result.value; + } + } + } + + return null; } /** @@ -46,19 +55,19 @@ export async function getTransactionReceiptFromEOATransaction( * @param transaction * @returns UserOperationReceipt | null */ -export async function getUserOpReceiptFromTransaction( - transaction: AnyTransaction, +export async function getReceiptForUserOp( + transaction: AnyTransaction, ): Promise { - assert(transaction.isUserOp); + assert(transaction.isUserOp); - if (!("userOpHash" in transaction)) { - return null; - } + if (!("userOpHash" in transaction)) { + return null; + } - const receipt = await getUserOpReceiptRaw({ - client: thirdwebClient, - chain: await getChain(transaction.chainId), - userOpHash: transaction.userOpHash, - }); - return receipt ?? null; + const receipt = await getUserOpReceiptRaw({ + client: thirdwebClient, + chain: await getChain(transaction.chainId), + userOpHash: transaction.userOpHash, + }); + return receipt ?? null; } diff --git a/src/server/routes/transaction/retry-failed.ts b/src/server/routes/transaction/retry-failed.ts index 6ab35a1a2..e720fa83a 100644 --- a/src/server/routes/transaction/retry-failed.ts +++ b/src/server/routes/transaction/retry-failed.ts @@ -3,8 +3,8 @@ import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { TransactionDB } from "../../../db/transactions/db"; import { - getTransactionReceiptFromEOATransaction, - getUserOpReceiptFromTransaction, + getReceiptForEOATransaction, + getReceiptForUserOp, } from "../../../lib/transaction/get-transaction-receipt"; import type { QueuedTransaction } from "../../../utils/transaction/types"; import { MineTransactionQueue } from "../../../worker/queues/mineTransactionQueue"; @@ -13,110 +13,110 @@ 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"], - }), + 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(), - }), + result: Type.Object({ + message: Type.String(), + status: Type.String(), + }), }); responseBodySchema.example = { - result: { - message: "Sent transaction to be retried.", - status: "success", - }, + result: { + message: "Sent transaction to be retried.", + status: "success", + }, }; export async function retryFailedTransactionRoute(fastify: FastifyInstance) { - fastify.route<{ - Body: Static; - Reply: Static; - }>({ - 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; + fastify.route<{ + Body: Static; + Reply: Static; + }>({ + 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( - `Cannot retry a transaction with status ${transaction.status}.`, - StatusCodes.BAD_REQUEST, - "TRANSACTION_CANNOT_BE_RETRIED", - ); - } + 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( + `Cannot retry a transaction with status ${transaction.status}.`, + StatusCodes.BAD_REQUEST, + "TRANSACTION_CANNOT_BE_RETRIED", + ); + } - 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", - ); - } + const receipt = transaction.isUserOp + ? await getReceiptForUserOp(transaction) + : await getReceiptForEOATransaction(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, - }), - ); - await sendJob?.remove(); + // Remove existing jobs. + const sendJob = await SendTransactionQueue.q.getJob( + SendTransactionQueue.jobId({ + queueId: transaction.queueId, + resendCount: 0, + }), + ); + await sendJob?.remove(); - const mineJob = await MineTransactionQueue.q.getJob( - MineTransactionQueue.jobId({ - queueId: transaction.queueId, - }), - ); - await mineJob?.remove(); + const mineJob = await MineTransactionQueue.q.getJob( + MineTransactionQueue.jobId({ + queueId: transaction.queueId, + }), + ); + 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); + // 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, - resendCount: 0, - }); + await SendTransactionQueue.add({ + queueId: transaction.queueId, + resendCount: 0, + }); - reply.status(StatusCodes.OK).send({ - result: { - message: "Sent transaction to be retried.", - status: "success", - }, - }); - }, - }); + reply.status(StatusCodes.OK).send({ + result: { + message: "Sent transaction to be retried.", + status: "success", + }, + }); + }, + }); } diff --git a/src/server/routes/transaction/sync-retry.ts b/src/server/routes/transaction/sync-retry.ts index 9f069c7a1..708cfe66a 100644 --- a/src/server/routes/transaction/sync-retry.ts +++ b/src/server/routes/transaction/sync-retry.ts @@ -3,7 +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 { getReceiptForEOATransaction } from "../../../lib/transaction/get-transaction-receipt"; import { getAccount } from "../../../utils/account"; import { getBlockNumberish } from "../../../utils/block"; import { getChain } from "../../../utils/chain"; @@ -17,122 +17,121 @@ import { TransactionHashSchema } from "../../schemas/address"; import { standardResponseSchema } from "../../schemas/sharedApiSchemas"; const requestBodySchema = Type.Object({ - queueId: Type.String({ - description: "Transaction queue ID", - examples: ["9eb88b00-f04f-409b-9df7-7dcc9003bc35"], - }), - maxFeePerGas: Type.Optional(Type.String()), - maxPriorityFeePerGas: Type.Optional(Type.String()), + queueId: Type.String({ + description: "Transaction queue ID", + examples: ["9eb88b00-f04f-409b-9df7-7dcc9003bc35"], + }), + maxFeePerGas: Type.Optional(Type.String()), + maxPriorityFeePerGas: Type.Optional(Type.String()), }); export const responseBodySchema = Type.Object({ - result: Type.Object({ - transactionHash: TransactionHashSchema, - }), + result: Type.Object({ + transactionHash: TransactionHashSchema, + }), }); responseBodySchema.example = { - result: { - transactionHash: - "0xc3b437073c164c33f95065fb325e9bc419f306cb39ae8b4ca233f33efaa74ead", - }, + result: { + transactionHash: + "0xc3b437073c164c33f95065fb325e9bc419f306cb39ae8b4ca233f33efaa74ead", + }, }; export async function syncRetryTransactionRoute(fastify: FastifyInstance) { - fastify.route<{ - Body: Static; - Reply: Static; - }>({ - method: "POST", - url: "/transaction/sync-retry", - schema: { - summary: "Retry transaction (synchronous)", - description: - "Retry a transaction with updated gas settings. Blocks until the transaction is mined or errors.", - tags: ["Transaction"], - operationId: "syncRetry", - body: requestBodySchema, - response: { - ...standardResponseSchema, - [StatusCodes.OK]: responseBodySchema, - }, - }, - handler: async (request, reply) => { - const { queueId, maxFeePerGas, maxPriorityFeePerGas } = request.body; + fastify.route<{ + Body: Static; + Reply: Static; + }>({ + method: "POST", + url: "/transaction/sync-retry", + schema: { + summary: "Retry transaction (synchronous)", + description: + "Retry a transaction with updated gas settings. Blocks until the transaction is mined or errors.", + tags: ["Transaction"], + operationId: "syncRetry", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseBodySchema, + }, + }, + handler: async (request, reply) => { + const { queueId, maxFeePerGas, maxPriorityFeePerGas } = request.body; - const transaction = await TransactionDB.get(queueId); - if (!transaction) { - throw createCustomError( - "Transaction not found.", - StatusCodes.BAD_REQUEST, - "TRANSACTION_NOT_FOUND", - ); - } + const transaction = await TransactionDB.get(queueId); + if (!transaction) { + throw createCustomError( + "Transaction not found.", + StatusCodes.BAD_REQUEST, + "TRANSACTION_NOT_FOUND", + ); + } - if (transaction.isUserOp || !("nonce" in transaction)) { - throw createCustomError( - "Transaction cannot be retried.", - StatusCodes.BAD_REQUEST, - "TRANSACTION_CANNOT_BE_RETRIED", - ); - } + if (transaction.isUserOp || !("nonce" in transaction)) { + throw createCustomError( + "Transaction cannot be retried.", + StatusCodes.BAD_REQUEST, + "TRANSACTION_CANNOT_BE_RETRIED", + ); + } - 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 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; + const { chainId, from } = transaction; - // Prepare transaction. - const populatedTransaction = await toSerializableTransaction({ - from: getChecksumAddress(from), - transaction: { - client: thirdwebClient, - chain: await getChain(chainId), - ...transaction, - // Explicitly reuse the same nonce the transaction had previously acquired. - nonce: transaction.nonce, - maxFeePerGas: maybeBigInt(maxFeePerGas), - maxPriorityFeePerGas: maybeBigInt(maxPriorityFeePerGas), - }, - }); + // Prepare transaction. + const populatedTransaction = await toSerializableTransaction({ + from: getChecksumAddress(from), + transaction: { + client: thirdwebClient, + chain: await getChain(chainId), + ...transaction, + // Explicitly reuse the same nonce the transaction had previously acquired. + nonce: transaction.nonce, + maxFeePerGas: maybeBigInt(maxFeePerGas), + maxPriorityFeePerGas: maybeBigInt(maxPriorityFeePerGas), + }, + }); - // Send transaction. - const account = await getAccount({ chainId, from }); - const { transactionHash } = - await account.sendTransaction(populatedTransaction); + // Send transaction. + const account = await getAccount({ chainId, from }); + const { transactionHash } = + await account.sendTransaction(populatedTransaction); - // Update state if the send was successful. - const sentTransaction: SentTransaction = { - ...transaction, - status: "sent", - resendCount: transaction.resendCount + 1, - sentAt: new Date(), - sentAtBlock: await getBlockNumberish(chainId), - sentTransactionHashes: [ - ...transaction.sentTransactionHashes, - transactionHash, - ], - gas: populatedTransaction.gas, - gasPrice: populatedTransaction.gasPrice, - maxFeePerGas: populatedTransaction.maxFeePerGas, - maxPriorityFeePerGas: populatedTransaction.maxPriorityFeePerGas, - }; - await TransactionDB.set(sentTransaction); - await MineTransactionQueue.add({ queueId: sentTransaction.queueId }); - await enqueueTransactionWebhook(sentTransaction); + // Update state if the send was successful. + const sentTransaction: SentTransaction = { + ...transaction, + status: "sent", + resendCount: transaction.resendCount + 1, + sentAt: new Date(), + sentAtBlock: await getBlockNumberish(chainId), + sentTransactionHashes: [ + ...transaction.sentTransactionHashes, + transactionHash, + ], + gas: populatedTransaction.gas, + gasPrice: populatedTransaction.gasPrice, + maxFeePerGas: populatedTransaction.maxFeePerGas, + maxPriorityFeePerGas: populatedTransaction.maxPriorityFeePerGas, + }; + await TransactionDB.set(sentTransaction); + await MineTransactionQueue.add({ queueId: sentTransaction.queueId }); + await enqueueTransactionWebhook(sentTransaction); - reply.status(StatusCodes.OK).send({ - result: { - transactionHash, - }, - }); - }, - }); + reply.status(StatusCodes.OK).send({ + result: { + transactionHash, + }, + }); + }, + }); } diff --git a/src/worker/tasks/mineTransactionWorker.ts b/src/worker/tasks/mineTransactionWorker.ts index a3afe5a1d..ae258bf0e 100644 --- a/src/worker/tasks/mineTransactionWorker.ts +++ b/src/worker/tasks/mineTransactionWorker.ts @@ -2,19 +2,19 @@ import { Worker, type Job, type Processor } from "bullmq"; import assert from "node:assert"; import superjson from "superjson"; import { - eth_getBalance, - getAddress, - getRpcClient, - toTokens, - type Address, + eth_getBalance, + getAddress, + getRpcClient, + toTokens, + type Address, } from "thirdweb"; import { stringify } from "thirdweb/utils"; import { getUserOpReceipt } from "thirdweb/wallets/smart"; import { TransactionDB } from "../../db/transactions/db"; import { recycleNonce, removeSentNonce } from "../../db/wallets/walletNonce"; import { - getTransactionReceiptFromEOATransaction, - getUserOpReceiptFromTransaction, + getReceiptForEOATransaction, + getReceiptForUserOp, } from "../../lib/transaction/get-transaction-receipt"; import { WebhooksEventTypes } from "../../schema/webhooks"; import { getBlockNumberish } from "../../utils/block"; @@ -29,15 +29,15 @@ import { recordMetrics } from "../../utils/prometheus"; import { redis } from "../../utils/redis/redis"; import { thirdwebClient } from "../../utils/sdk"; import type { - ErroredTransaction, - MinedTransaction, - SentTransaction, + ErroredTransaction, + MinedTransaction, + SentTransaction, } from "../../utils/transaction/types"; import { enqueueTransactionWebhook } from "../../utils/transaction/webhook"; import { reportUsage } from "../../utils/usage"; import { - MineTransactionQueue, - type MineTransactionData, + MineTransactionQueue, + type MineTransactionData, } from "../queues/mineTransactionQueue"; import { SendTransactionQueue } from "../queues/sendTransactionQueue"; import { SendWebhookQueue } from "../queues/sendWebhookQueue"; @@ -48,313 +48,312 @@ import { SendWebhookQueue } from "../queues/sendWebhookQueue"; * If an EOA transaction is not mined after some time, resend it. */ const handler: Processor = async (job: Job) => { - const { queueId } = superjson.parse(job.data); - - // Assert valid transaction state. - const sentTransaction = await TransactionDB.get(queueId); - if (sentTransaction?.status !== "sent") { - job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); - return; - } - - // MinedTransaction = the transaction or userOp was mined. - // null = the transaction or userOp is not yet mined. - let resultTransaction: MinedTransaction | null; - if (sentTransaction.isUserOp) { - resultTransaction = await _mineUserOp(job, sentTransaction); - } else { - resultTransaction = await _mineTransaction(job, sentTransaction); - } - - if (!resultTransaction) { - throw new Error("NOT_CONFIRMED_YET"); - } - - if (resultTransaction.status === "mined") { - await TransactionDB.set(resultTransaction); - await enqueueTransactionWebhook(resultTransaction); - await _notifyIfLowBalance(resultTransaction); - await _reportUsageSuccess(resultTransaction); - recordMetrics({ - event: "transaction_mined", - params: { - chainId: resultTransaction.chainId.toString(), - queuedToMinedDurationSeconds: - msSince(resultTransaction.queuedAt) / 1000, - durationSeconds: msSince(resultTransaction.sentAt) / 1000, - walletAddress: getAddress(resultTransaction.from), - }, - }); - } + const { queueId } = superjson.parse(job.data); + + // Assert valid transaction state. + const sentTransaction = await TransactionDB.get(queueId); + if (sentTransaction?.status !== "sent") { + job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); + return; + } + + // MinedTransaction = the transaction or userOp was mined. + // null = the transaction or userOp is not yet mined. + let resultTransaction: MinedTransaction | null; + if (sentTransaction.isUserOp) { + resultTransaction = await _mineUserOp(job, sentTransaction); + } else { + resultTransaction = await _mineTransaction(job, sentTransaction); + } + + if (!resultTransaction) { + throw new Error("NOT_CONFIRMED_YET"); + } + + if (resultTransaction.status === "mined") { + await TransactionDB.set(resultTransaction); + await enqueueTransactionWebhook(resultTransaction); + await _notifyIfLowBalance(resultTransaction); + await _reportUsageSuccess(resultTransaction); + recordMetrics({ + event: "transaction_mined", + params: { + chainId: resultTransaction.chainId.toString(), + queuedToMinedDurationSeconds: + msSince(resultTransaction.queuedAt) / 1000, + durationSeconds: msSince(resultTransaction.sentAt) / 1000, + walletAddress: getAddress(resultTransaction.from), + }, + }); + } }; const _reportUsageSuccess = async (minedTransaction: MinedTransaction) => { - const chain = await getChain(minedTransaction.chainId); - reportUsage([ - { - action: "mine_tx", - input: { - ...minedTransaction, - provider: chain.rpc, - msSinceQueue: msSince(minedTransaction.queuedAt), - msSinceSend: msSince(minedTransaction.sentAt), - }, - }, - ]); + const chain = await getChain(minedTransaction.chainId); + reportUsage([ + { + action: "mine_tx", + input: { + ...minedTransaction, + provider: chain.rpc, + msSinceQueue: msSince(minedTransaction.queuedAt), + msSinceSend: msSince(minedTransaction.sentAt), + }, + }, + ]); }; const _reportUsageError = (erroredTransaction: ErroredTransaction) => { - reportUsage([ - { - action: "error_tx", - input: { - ...erroredTransaction, - msSinceQueue: msSince(erroredTransaction.queuedAt), - }, - error: erroredTransaction.errorMessage, - }, - ]); + reportUsage([ + { + action: "error_tx", + input: { + ...erroredTransaction, + msSinceQueue: msSince(erroredTransaction.queuedAt), + }, + error: erroredTransaction.errorMessage, + }, + ]); }; const _mineTransaction = async ( - job: Job, - sentTransaction: SentTransaction, + job: Job, + sentTransaction: SentTransaction, ): Promise => { - assert(!sentTransaction.isUserOp); - - const receipt = - await getTransactionReceiptFromEOATransaction(sentTransaction); - - if (receipt) { - job.log( - `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, - ); - - const removed = await removeSentNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - logger({ - level: "debug", - message: `[mineTransactionWorker] Removed nonce ${sentTransaction.nonce} from nonce-sent set: ${removed}`, - service: "worker", - }); - - // Though the transaction is mined successfully, set an error message if the transaction failed onchain. - const errorMessage = - receipt.status === "reverted" - ? "The transaction failed onchain. See: https://portal.thirdweb.com/engine/troubleshooting" - : undefined; - - return { - ...sentTransaction, - status: "mined", - transactionHash: receipt.transactionHash, - minedAt: new Date(), - minedAtBlock: receipt.blockNumber, - transactionType: receipt.type, - onchainStatus: receipt.status, - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, - cumulativeGasUsed: receipt.cumulativeGasUsed, - errorMessage, - }; - } - - // Else the transaction is not mined yet. - job.log( - `Transaction is not mined yet. Check again later. sentTransactionHashes=${sentTransaction.sentTransactionHashes}`, - ); - - // Resend the transaction (after some initial delay). - const config = await getConfig(); - const blockNumber = await getBlockNumberish(sentTransaction.chainId); - const ellapsedBlocks = blockNumber - sentTransaction.sentAtBlock; - if (ellapsedBlocks >= config.minEllapsedBlocksBeforeRetry) { - job.log( - `Resending transaction after ${ellapsedBlocks} blocks. blockNumber=${blockNumber} sentAtBlock=${sentTransaction.sentAtBlock}`, - ); - await SendTransactionQueue.add({ - queueId: sentTransaction.queueId, - resendCount: sentTransaction.resendCount + 1, - }); - } - - return null; + assert(!sentTransaction.isUserOp); + + const receipt = await getReceiptForEOATransaction(sentTransaction); + + if (receipt) { + job.log( + `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, + ); + + const removed = await removeSentNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + logger({ + level: "debug", + message: `[mineTransactionWorker] Removed nonce ${sentTransaction.nonce} from nonce-sent set: ${removed}`, + service: "worker", + }); + + // Though the transaction is mined successfully, set an error message if the transaction failed onchain. + const errorMessage = + receipt.status === "reverted" + ? "The transaction failed onchain. See: https://portal.thirdweb.com/engine/troubleshooting" + : undefined; + + return { + ...sentTransaction, + status: "mined", + transactionHash: receipt.transactionHash, + minedAt: new Date(), + minedAtBlock: receipt.blockNumber, + transactionType: receipt.type, + onchainStatus: receipt.status, + gasUsed: receipt.gasUsed, + effectiveGasPrice: receipt.effectiveGasPrice, + cumulativeGasUsed: receipt.cumulativeGasUsed, + errorMessage, + }; + } + + // Else the transaction is not mined yet. + job.log( + `Transaction is not mined yet. Check again later. sentTransactionHashes=${sentTransaction.sentTransactionHashes}`, + ); + + // Resend the transaction (after some initial delay). + const config = await getConfig(); + const blockNumber = await getBlockNumberish(sentTransaction.chainId); + const ellapsedBlocks = blockNumber - sentTransaction.sentAtBlock; + if (ellapsedBlocks >= config.minEllapsedBlocksBeforeRetry) { + job.log( + `Resending transaction after ${ellapsedBlocks} blocks. blockNumber=${blockNumber} sentAtBlock=${sentTransaction.sentAtBlock}`, + ); + await SendTransactionQueue.add({ + queueId: sentTransaction.queueId, + resendCount: sentTransaction.resendCount + 1, + }); + } + + return null; }; const _mineUserOp = async ( - job: Job, - sentTransaction: SentTransaction, + job: Job, + sentTransaction: SentTransaction, ): Promise => { - assert(sentTransaction.isUserOp); - - const userOpReceipt = await getUserOpReceiptFromTransaction(sentTransaction); - if (!userOpReceipt) { - job.log( - `UserOp is not mined yet. Check again later. userOpHash=${sentTransaction.userOpHash}`, - ); - return null; - } - const { receipt } = userOpReceipt; - - job.log( - `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, - ); - - let errorMessage: string | undefined; - - // if the userOpReceipt is not successful, try to get the parsed userOpReceipt - // we expect this to fail, but we want the error message if it does - if (!userOpReceipt.success) { - try { - const chain = await getChain(sentTransaction.chainId); - const userOpReceipt = await getUserOpReceipt({ - client: thirdwebClient, - chain, - userOpHash: sentTransaction.userOpHash, - }); - job.log(`Found userOpReceipt: ${userOpReceipt}`); - } catch (e) { - if (e instanceof Error) { - errorMessage = e.message; - job.log(`Failed to get userOpReceipt: ${e.message}`); - } else { - throw e; - } - } - } - - return { - ...sentTransaction, - status: "mined", - transactionHash: receipt.transactionHash, - minedAt: new Date(), - minedAtBlock: receipt.blockNumber, - transactionType: receipt.type, - onchainStatus: userOpReceipt.success ? "success" : "reverted", - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, - gas: receipt.gasUsed, - cumulativeGasUsed: receipt.cumulativeGasUsed, - sender: userOpReceipt.sender as Address, - nonce: userOpReceipt.nonce.toString(), - errorMessage, - }; + assert(sentTransaction.isUserOp); + + const userOpReceipt = await getReceiptForUserOp(sentTransaction); + if (!userOpReceipt) { + job.log( + `UserOp is not mined yet. Check again later. userOpHash=${sentTransaction.userOpHash}`, + ); + return null; + } + const { receipt } = userOpReceipt; + + job.log( + `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, + ); + + let errorMessage: string | undefined; + + // if the userOpReceipt is not successful, try to get the parsed userOpReceipt + // we expect this to fail, but we want the error message if it does + if (!userOpReceipt.success) { + try { + const chain = await getChain(sentTransaction.chainId); + const userOpReceipt = await getUserOpReceipt({ + client: thirdwebClient, + chain, + userOpHash: sentTransaction.userOpHash, + }); + job.log(`Found userOpReceipt: ${userOpReceipt}`); + } catch (e) { + if (e instanceof Error) { + errorMessage = e.message; + job.log(`Failed to get userOpReceipt: ${e.message}`); + } else { + throw e; + } + } + } + + return { + ...sentTransaction, + status: "mined", + transactionHash: receipt.transactionHash, + minedAt: new Date(), + minedAtBlock: receipt.blockNumber, + transactionType: receipt.type, + onchainStatus: userOpReceipt.success ? "success" : "reverted", + gasUsed: receipt.gasUsed, + effectiveGasPrice: receipt.effectiveGasPrice, + gas: receipt.gasUsed, + cumulativeGasUsed: receipt.cumulativeGasUsed, + sender: userOpReceipt.sender as Address, + nonce: userOpReceipt.nonce.toString(), + errorMessage, + }; }; const _notifyIfLowBalance = async (transaction: MinedTransaction) => { - const { isUserOp, chainId, from } = transaction; - if (isUserOp) { - // Skip for userOps since they may not use the wallet's gas balance. - return; - } - - try { - const webhooks = await getWebhooksByEventType( - WebhooksEventTypes.BACKEND_WALLET_BALANCE, - ); - if (webhooks.length === 0) { - // Skip if no webhooks configured. - return; - } - - // Set a key with 5min TTL if it doesn't exist. - // This effectively throttles this check once every 5min. - const throttleKey = `webhook:${WebhooksEventTypes.BACKEND_WALLET_BALANCE}:${chainId}:${from}`; - const isThrottled = - (await redis.set(throttleKey, "", "EX", 5 * 60, "NX")) === null; - if (isThrottled) { - return; - } - - // Get the current wallet balance. - const rpcRequest = getRpcClient({ - client: thirdwebClient, - chain: await getChain(chainId), - }); - const currentBalance = await eth_getBalance(rpcRequest, { - address: from, - }); - - const config = await getConfig(); - if (currentBalance >= BigInt(config.minWalletBalance)) { - // Skip if the balance is above the alert threshold. - return; - } - - await SendWebhookQueue.enqueueWebhook({ - type: WebhooksEventTypes.BACKEND_WALLET_BALANCE, - body: { - chainId, - walletAddress: from, - minimumBalance: config.minWalletBalance, - currentBalance: currentBalance.toString(), - message: `LowBalance: The backend wallet ${from} on chain ${chainId} has ${toTokens(currentBalance, 18)} gas remaining.`, - }, - }); - } catch (e) { - logger({ - level: "warn", - message: `[mineTransactionWorker] Error sending low balance notification: ${prettifyError(e)}`, - service: "worker", - }); - } + const { isUserOp, chainId, from } = transaction; + if (isUserOp) { + // Skip for userOps since they may not use the wallet's gas balance. + return; + } + + try { + const webhooks = await getWebhooksByEventType( + WebhooksEventTypes.BACKEND_WALLET_BALANCE, + ); + if (webhooks.length === 0) { + // Skip if no webhooks configured. + return; + } + + // Set a key with 5min TTL if it doesn't exist. + // This effectively throttles this check once every 5min. + const throttleKey = `webhook:${WebhooksEventTypes.BACKEND_WALLET_BALANCE}:${chainId}:${from}`; + const isThrottled = + (await redis.set(throttleKey, "", "EX", 5 * 60, "NX")) === null; + if (isThrottled) { + return; + } + + // Get the current wallet balance. + const rpcRequest = getRpcClient({ + client: thirdwebClient, + chain: await getChain(chainId), + }); + const currentBalance = await eth_getBalance(rpcRequest, { + address: from, + }); + + const config = await getConfig(); + if (currentBalance >= BigInt(config.minWalletBalance)) { + // Skip if the balance is above the alert threshold. + return; + } + + await SendWebhookQueue.enqueueWebhook({ + type: WebhooksEventTypes.BACKEND_WALLET_BALANCE, + body: { + chainId, + walletAddress: from, + minimumBalance: config.minWalletBalance, + currentBalance: currentBalance.toString(), + message: `LowBalance: The backend wallet ${from} on chain ${chainId} has ${toTokens(currentBalance, 18)} gas remaining.`, + }, + }); + } catch (e) { + logger({ + level: "warn", + message: `[mineTransactionWorker] Error sending low balance notification: ${prettifyError(e)}`, + service: "worker", + }); + } }; // Must be explicitly called for the worker to run on this host. export const initMineTransactionWorker = () => { - const _worker = new Worker(MineTransactionQueue.q.name, handler, { - concurrency: env.CONFIRM_TRANSACTION_QUEUE_CONCURRENCY, - connection: redis, - settings: { - backoffStrategy: (attemptsMade: number) => { - // Retries at 2s, 4s, 6s, ..., 18s, 20s, 20s, 20s, ... - return Math.min(attemptsMade * 2_000, 20_000); - }, - }, - }); - - // If a transaction fails to mine after all retries, set it as errored and release the nonce. - _worker.on("failed", async (job: Job | undefined) => { - if (job && job.attemptsMade === job.opts.attempts) { - const { queueId } = superjson.parse(job.data); - - const sentTransaction = await TransactionDB.get(queueId); - if (sentTransaction?.status !== "sent") { - job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); - return; - } - - const erroredTransaction: ErroredTransaction = { - ...sentTransaction, - status: "errored", - errorMessage: "Transaction timed out.", - }; - job.log(`Transaction timed out: ${stringify(erroredTransaction)}`); - - await TransactionDB.set(erroredTransaction); - await enqueueTransactionWebhook(erroredTransaction); - _reportUsageError(erroredTransaction); - - if (!sentTransaction.isUserOp) { - // Release the nonce to allow it to be reused or cancelled. - job.log( - `Recycling nonce and removing from nonce-sent: ${sentTransaction.nonce}`, - ); - await recycleNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - - await removeSentNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - } - } - }); + const _worker = new Worker(MineTransactionQueue.q.name, handler, { + concurrency: env.CONFIRM_TRANSACTION_QUEUE_CONCURRENCY, + connection: redis, + settings: { + backoffStrategy: (attemptsMade: number) => { + // Retries at 2s, 4s, 6s, ..., 18s, 20s, 20s, 20s, ... + return Math.min(attemptsMade * 2_000, 20_000); + }, + }, + }); + + // If a transaction fails to mine after all retries, set it as errored and release the nonce. + _worker.on("failed", async (job: Job | undefined) => { + if (job && job.attemptsMade === job.opts.attempts) { + const { queueId } = superjson.parse(job.data); + + const sentTransaction = await TransactionDB.get(queueId); + if (sentTransaction?.status !== "sent") { + job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); + return; + } + + const erroredTransaction: ErroredTransaction = { + ...sentTransaction, + status: "errored", + errorMessage: "Transaction timed out.", + }; + job.log(`Transaction timed out: ${stringify(erroredTransaction)}`); + + await TransactionDB.set(erroredTransaction); + await enqueueTransactionWebhook(erroredTransaction); + _reportUsageError(erroredTransaction); + + if (!sentTransaction.isUserOp) { + // Release the nonce to allow it to be reused or cancelled. + job.log( + `Recycling nonce and removing from nonce-sent: ${sentTransaction.nonce}`, + ); + await recycleNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + + await removeSentNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + } + } + }); }; From ca10168e4661057fe61cdce3ba228f658fa7541f Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Fri, 6 Dec 2024 09:22:21 +0800 Subject: [PATCH 4/6] linting --- src/server/routes/transaction/retry-failed.ts | 184 ++++++++-------- src/server/routes/transaction/sync-retry.ts | 200 +++++++++--------- 2 files changed, 192 insertions(+), 192 deletions(-) diff --git a/src/server/routes/transaction/retry-failed.ts b/src/server/routes/transaction/retry-failed.ts index e720fa83a..d00ecbc2d 100644 --- a/src/server/routes/transaction/retry-failed.ts +++ b/src/server/routes/transaction/retry-failed.ts @@ -3,8 +3,8 @@ import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { TransactionDB } from "../../../db/transactions/db"; import { - getReceiptForEOATransaction, - getReceiptForUserOp, + getReceiptForEOATransaction, + getReceiptForUserOp, } from "../../../lib/transaction/get-transaction-receipt"; import type { QueuedTransaction } from "../../../utils/transaction/types"; import { MineTransactionQueue } from "../../../worker/queues/mineTransactionQueue"; @@ -13,110 +13,110 @@ 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"], - }), + 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(), - }), + result: Type.Object({ + message: Type.String(), + status: Type.String(), + }), }); responseBodySchema.example = { - result: { - message: "Sent transaction to be retried.", - status: "success", - }, + result: { + message: "Sent transaction to be retried.", + status: "success", + }, }; export async function retryFailedTransactionRoute(fastify: FastifyInstance) { - fastify.route<{ - Body: Static; - Reply: Static; - }>({ - 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; + fastify.route<{ + Body: Static; + Reply: Static; + }>({ + 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( - `Cannot retry a transaction with status ${transaction.status}.`, - StatusCodes.BAD_REQUEST, - "TRANSACTION_CANNOT_BE_RETRIED", - ); - } + 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( + `Cannot retry a transaction with status ${transaction.status}.`, + StatusCodes.BAD_REQUEST, + "TRANSACTION_CANNOT_BE_RETRIED", + ); + } - const receipt = transaction.isUserOp - ? await getReceiptForUserOp(transaction) - : await getReceiptForEOATransaction(transaction); - if (receipt) { - throw createCustomError( - "Cannot retry a transaction that is already mined.", - StatusCodes.BAD_REQUEST, - "TRANSACTION_CANNOT_BE_RETRIED", - ); - } + const receipt = transaction.isUserOp + ? await getReceiptForUserOp(transaction) + : await getReceiptForEOATransaction(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, - }), - ); - await sendJob?.remove(); + // Remove existing jobs. + const sendJob = await SendTransactionQueue.q.getJob( + SendTransactionQueue.jobId({ + queueId: transaction.queueId, + resendCount: 0, + }), + ); + await sendJob?.remove(); - const mineJob = await MineTransactionQueue.q.getJob( - MineTransactionQueue.jobId({ - queueId: transaction.queueId, - }), - ); - await mineJob?.remove(); + const mineJob = await MineTransactionQueue.q.getJob( + MineTransactionQueue.jobId({ + queueId: transaction.queueId, + }), + ); + 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); + // 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, - resendCount: 0, - }); + await SendTransactionQueue.add({ + queueId: transaction.queueId, + resendCount: 0, + }); - reply.status(StatusCodes.OK).send({ - result: { - message: "Sent transaction to be retried.", - status: "success", - }, - }); - }, - }); + reply.status(StatusCodes.OK).send({ + result: { + message: "Sent transaction to be retried.", + status: "success", + }, + }); + }, + }); } diff --git a/src/server/routes/transaction/sync-retry.ts b/src/server/routes/transaction/sync-retry.ts index 708cfe66a..3eabdbd1e 100644 --- a/src/server/routes/transaction/sync-retry.ts +++ b/src/server/routes/transaction/sync-retry.ts @@ -17,121 +17,121 @@ import { TransactionHashSchema } from "../../schemas/address"; import { standardResponseSchema } from "../../schemas/sharedApiSchemas"; const requestBodySchema = Type.Object({ - queueId: Type.String({ - description: "Transaction queue ID", - examples: ["9eb88b00-f04f-409b-9df7-7dcc9003bc35"], - }), - maxFeePerGas: Type.Optional(Type.String()), - maxPriorityFeePerGas: Type.Optional(Type.String()), + queueId: Type.String({ + description: "Transaction queue ID", + examples: ["9eb88b00-f04f-409b-9df7-7dcc9003bc35"], + }), + maxFeePerGas: Type.Optional(Type.String()), + maxPriorityFeePerGas: Type.Optional(Type.String()), }); export const responseBodySchema = Type.Object({ - result: Type.Object({ - transactionHash: TransactionHashSchema, - }), + result: Type.Object({ + transactionHash: TransactionHashSchema, + }), }); responseBodySchema.example = { - result: { - transactionHash: - "0xc3b437073c164c33f95065fb325e9bc419f306cb39ae8b4ca233f33efaa74ead", - }, + result: { + transactionHash: + "0xc3b437073c164c33f95065fb325e9bc419f306cb39ae8b4ca233f33efaa74ead", + }, }; export async function syncRetryTransactionRoute(fastify: FastifyInstance) { - fastify.route<{ - Body: Static; - Reply: Static; - }>({ - method: "POST", - url: "/transaction/sync-retry", - schema: { - summary: "Retry transaction (synchronous)", - description: - "Retry a transaction with updated gas settings. Blocks until the transaction is mined or errors.", - tags: ["Transaction"], - operationId: "syncRetry", - body: requestBodySchema, - response: { - ...standardResponseSchema, - [StatusCodes.OK]: responseBodySchema, - }, - }, - handler: async (request, reply) => { - const { queueId, maxFeePerGas, maxPriorityFeePerGas } = request.body; + fastify.route<{ + Body: Static; + Reply: Static; + }>({ + method: "POST", + url: "/transaction/sync-retry", + schema: { + summary: "Retry transaction (synchronous)", + description: + "Retry a transaction with updated gas settings. Blocks until the transaction is mined or errors.", + tags: ["Transaction"], + operationId: "syncRetry", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseBodySchema, + }, + }, + handler: async (request, reply) => { + const { queueId, maxFeePerGas, maxPriorityFeePerGas } = request.body; - const transaction = await TransactionDB.get(queueId); - if (!transaction) { - throw createCustomError( - "Transaction not found.", - StatusCodes.BAD_REQUEST, - "TRANSACTION_NOT_FOUND", - ); - } + const transaction = await TransactionDB.get(queueId); + if (!transaction) { + throw createCustomError( + "Transaction not found.", + StatusCodes.BAD_REQUEST, + "TRANSACTION_NOT_FOUND", + ); + } - if (transaction.isUserOp || !("nonce" in transaction)) { - throw createCustomError( - "Transaction cannot be retried.", - StatusCodes.BAD_REQUEST, - "TRANSACTION_CANNOT_BE_RETRIED", - ); - } + if (transaction.isUserOp || !("nonce" in transaction)) { + throw createCustomError( + "Transaction cannot be retried.", + StatusCodes.BAD_REQUEST, + "TRANSACTION_CANNOT_BE_RETRIED", + ); + } - 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 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; + const { chainId, from } = transaction; - // Prepare transaction. - const populatedTransaction = await toSerializableTransaction({ - from: getChecksumAddress(from), - transaction: { - client: thirdwebClient, - chain: await getChain(chainId), - ...transaction, - // Explicitly reuse the same nonce the transaction had previously acquired. - nonce: transaction.nonce, - maxFeePerGas: maybeBigInt(maxFeePerGas), - maxPriorityFeePerGas: maybeBigInt(maxPriorityFeePerGas), - }, - }); + // Prepare transaction. + const populatedTransaction = await toSerializableTransaction({ + from: getChecksumAddress(from), + transaction: { + client: thirdwebClient, + chain: await getChain(chainId), + ...transaction, + // Explicitly reuse the same nonce the transaction had previously acquired. + nonce: transaction.nonce, + maxFeePerGas: maybeBigInt(maxFeePerGas), + maxPriorityFeePerGas: maybeBigInt(maxPriorityFeePerGas), + }, + }); - // Send transaction. - const account = await getAccount({ chainId, from }); - const { transactionHash } = - await account.sendTransaction(populatedTransaction); + // Send transaction. + const account = await getAccount({ chainId, from }); + const { transactionHash } = + await account.sendTransaction(populatedTransaction); - // Update state if the send was successful. - const sentTransaction: SentTransaction = { - ...transaction, - status: "sent", - resendCount: transaction.resendCount + 1, - sentAt: new Date(), - sentAtBlock: await getBlockNumberish(chainId), - sentTransactionHashes: [ - ...transaction.sentTransactionHashes, - transactionHash, - ], - gas: populatedTransaction.gas, - gasPrice: populatedTransaction.gasPrice, - maxFeePerGas: populatedTransaction.maxFeePerGas, - maxPriorityFeePerGas: populatedTransaction.maxPriorityFeePerGas, - }; - await TransactionDB.set(sentTransaction); - await MineTransactionQueue.add({ queueId: sentTransaction.queueId }); - await enqueueTransactionWebhook(sentTransaction); + // Update state if the send was successful. + const sentTransaction: SentTransaction = { + ...transaction, + status: "sent", + resendCount: transaction.resendCount + 1, + sentAt: new Date(), + sentAtBlock: await getBlockNumberish(chainId), + sentTransactionHashes: [ + ...transaction.sentTransactionHashes, + transactionHash, + ], + gas: populatedTransaction.gas, + gasPrice: populatedTransaction.gasPrice, + maxFeePerGas: populatedTransaction.maxFeePerGas, + maxPriorityFeePerGas: populatedTransaction.maxPriorityFeePerGas, + }; + await TransactionDB.set(sentTransaction); + await MineTransactionQueue.add({ queueId: sentTransaction.queueId }); + await enqueueTransactionWebhook(sentTransaction); - reply.status(StatusCodes.OK).send({ - result: { - transactionHash, - }, - }); - }, - }); + reply.status(StatusCodes.OK).send({ + result: { + transactionHash, + }, + }); + }, + }); } From 3f981f35a4fcba55af11757413e53aa9015107e1 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Fri, 6 Dec 2024 09:25:05 +0800 Subject: [PATCH 5/6] lint --- src/worker/tasks/mineTransactionWorker.ts | 598 +++++++++++----------- 1 file changed, 299 insertions(+), 299 deletions(-) diff --git a/src/worker/tasks/mineTransactionWorker.ts b/src/worker/tasks/mineTransactionWorker.ts index ae258bf0e..1d3ce45f9 100644 --- a/src/worker/tasks/mineTransactionWorker.ts +++ b/src/worker/tasks/mineTransactionWorker.ts @@ -2,19 +2,19 @@ import { Worker, type Job, type Processor } from "bullmq"; import assert from "node:assert"; import superjson from "superjson"; import { - eth_getBalance, - getAddress, - getRpcClient, - toTokens, - type Address, + eth_getBalance, + getAddress, + getRpcClient, + toTokens, + type Address, } from "thirdweb"; import { stringify } from "thirdweb/utils"; import { getUserOpReceipt } from "thirdweb/wallets/smart"; import { TransactionDB } from "../../db/transactions/db"; import { recycleNonce, removeSentNonce } from "../../db/wallets/walletNonce"; import { - getReceiptForEOATransaction, - getReceiptForUserOp, + getReceiptForEOATransaction, + getReceiptForUserOp, } from "../../lib/transaction/get-transaction-receipt"; import { WebhooksEventTypes } from "../../schema/webhooks"; import { getBlockNumberish } from "../../utils/block"; @@ -29,15 +29,15 @@ import { recordMetrics } from "../../utils/prometheus"; import { redis } from "../../utils/redis/redis"; import { thirdwebClient } from "../../utils/sdk"; import type { - ErroredTransaction, - MinedTransaction, - SentTransaction, + ErroredTransaction, + MinedTransaction, + SentTransaction, } from "../../utils/transaction/types"; import { enqueueTransactionWebhook } from "../../utils/transaction/webhook"; import { reportUsage } from "../../utils/usage"; import { - MineTransactionQueue, - type MineTransactionData, + MineTransactionQueue, + type MineTransactionData, } from "../queues/mineTransactionQueue"; import { SendTransactionQueue } from "../queues/sendTransactionQueue"; import { SendWebhookQueue } from "../queues/sendWebhookQueue"; @@ -48,312 +48,312 @@ import { SendWebhookQueue } from "../queues/sendWebhookQueue"; * If an EOA transaction is not mined after some time, resend it. */ const handler: Processor = async (job: Job) => { - const { queueId } = superjson.parse(job.data); - - // Assert valid transaction state. - const sentTransaction = await TransactionDB.get(queueId); - if (sentTransaction?.status !== "sent") { - job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); - return; - } - - // MinedTransaction = the transaction or userOp was mined. - // null = the transaction or userOp is not yet mined. - let resultTransaction: MinedTransaction | null; - if (sentTransaction.isUserOp) { - resultTransaction = await _mineUserOp(job, sentTransaction); - } else { - resultTransaction = await _mineTransaction(job, sentTransaction); - } - - if (!resultTransaction) { - throw new Error("NOT_CONFIRMED_YET"); - } - - if (resultTransaction.status === "mined") { - await TransactionDB.set(resultTransaction); - await enqueueTransactionWebhook(resultTransaction); - await _notifyIfLowBalance(resultTransaction); - await _reportUsageSuccess(resultTransaction); - recordMetrics({ - event: "transaction_mined", - params: { - chainId: resultTransaction.chainId.toString(), - queuedToMinedDurationSeconds: - msSince(resultTransaction.queuedAt) / 1000, - durationSeconds: msSince(resultTransaction.sentAt) / 1000, - walletAddress: getAddress(resultTransaction.from), - }, - }); - } + const { queueId } = superjson.parse(job.data); + + // Assert valid transaction state. + const sentTransaction = await TransactionDB.get(queueId); + if (sentTransaction?.status !== "sent") { + job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); + return; + } + + // MinedTransaction = the transaction or userOp was mined. + // null = the transaction or userOp is not yet mined. + let resultTransaction: MinedTransaction | null; + if (sentTransaction.isUserOp) { + resultTransaction = await _mineUserOp(job, sentTransaction); + } else { + resultTransaction = await _mineTransaction(job, sentTransaction); + } + + if (!resultTransaction) { + throw new Error("NOT_CONFIRMED_YET"); + } + + if (resultTransaction.status === "mined") { + await TransactionDB.set(resultTransaction); + await enqueueTransactionWebhook(resultTransaction); + await _notifyIfLowBalance(resultTransaction); + await _reportUsageSuccess(resultTransaction); + recordMetrics({ + event: "transaction_mined", + params: { + chainId: resultTransaction.chainId.toString(), + queuedToMinedDurationSeconds: + msSince(resultTransaction.queuedAt) / 1000, + durationSeconds: msSince(resultTransaction.sentAt) / 1000, + walletAddress: getAddress(resultTransaction.from), + }, + }); + } }; const _reportUsageSuccess = async (minedTransaction: MinedTransaction) => { - const chain = await getChain(minedTransaction.chainId); - reportUsage([ - { - action: "mine_tx", - input: { - ...minedTransaction, - provider: chain.rpc, - msSinceQueue: msSince(minedTransaction.queuedAt), - msSinceSend: msSince(minedTransaction.sentAt), - }, - }, - ]); + const chain = await getChain(minedTransaction.chainId); + reportUsage([ + { + action: "mine_tx", + input: { + ...minedTransaction, + provider: chain.rpc, + msSinceQueue: msSince(minedTransaction.queuedAt), + msSinceSend: msSince(minedTransaction.sentAt), + }, + }, + ]); }; const _reportUsageError = (erroredTransaction: ErroredTransaction) => { - reportUsage([ - { - action: "error_tx", - input: { - ...erroredTransaction, - msSinceQueue: msSince(erroredTransaction.queuedAt), - }, - error: erroredTransaction.errorMessage, - }, - ]); + reportUsage([ + { + action: "error_tx", + input: { + ...erroredTransaction, + msSinceQueue: msSince(erroredTransaction.queuedAt), + }, + error: erroredTransaction.errorMessage, + }, + ]); }; const _mineTransaction = async ( - job: Job, - sentTransaction: SentTransaction, + job: Job, + sentTransaction: SentTransaction, ): Promise => { - assert(!sentTransaction.isUserOp); - - const receipt = await getReceiptForEOATransaction(sentTransaction); - - if (receipt) { - job.log( - `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, - ); - - const removed = await removeSentNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - logger({ - level: "debug", - message: `[mineTransactionWorker] Removed nonce ${sentTransaction.nonce} from nonce-sent set: ${removed}`, - service: "worker", - }); - - // Though the transaction is mined successfully, set an error message if the transaction failed onchain. - const errorMessage = - receipt.status === "reverted" - ? "The transaction failed onchain. See: https://portal.thirdweb.com/engine/troubleshooting" - : undefined; - - return { - ...sentTransaction, - status: "mined", - transactionHash: receipt.transactionHash, - minedAt: new Date(), - minedAtBlock: receipt.blockNumber, - transactionType: receipt.type, - onchainStatus: receipt.status, - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, - cumulativeGasUsed: receipt.cumulativeGasUsed, - errorMessage, - }; - } - - // Else the transaction is not mined yet. - job.log( - `Transaction is not mined yet. Check again later. sentTransactionHashes=${sentTransaction.sentTransactionHashes}`, - ); - - // Resend the transaction (after some initial delay). - const config = await getConfig(); - const blockNumber = await getBlockNumberish(sentTransaction.chainId); - const ellapsedBlocks = blockNumber - sentTransaction.sentAtBlock; - if (ellapsedBlocks >= config.minEllapsedBlocksBeforeRetry) { - job.log( - `Resending transaction after ${ellapsedBlocks} blocks. blockNumber=${blockNumber} sentAtBlock=${sentTransaction.sentAtBlock}`, - ); - await SendTransactionQueue.add({ - queueId: sentTransaction.queueId, - resendCount: sentTransaction.resendCount + 1, - }); - } - - return null; + assert(!sentTransaction.isUserOp); + + const receipt = await getReceiptForEOATransaction(sentTransaction); + + if (receipt) { + job.log( + `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, + ); + + const removed = await removeSentNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + logger({ + level: "debug", + message: `[mineTransactionWorker] Removed nonce ${sentTransaction.nonce} from nonce-sent set: ${removed}`, + service: "worker", + }); + + // Though the transaction is mined successfully, set an error message if the transaction failed onchain. + const errorMessage = + receipt.status === "reverted" + ? "The transaction failed onchain. See: https://portal.thirdweb.com/engine/troubleshooting" + : undefined; + + return { + ...sentTransaction, + status: "mined", + transactionHash: receipt.transactionHash, + minedAt: new Date(), + minedAtBlock: receipt.blockNumber, + transactionType: receipt.type, + onchainStatus: receipt.status, + gasUsed: receipt.gasUsed, + effectiveGasPrice: receipt.effectiveGasPrice, + cumulativeGasUsed: receipt.cumulativeGasUsed, + errorMessage, + }; + } + + // Else the transaction is not mined yet. + job.log( + `Transaction is not mined yet. Check again later. sentTransactionHashes=${sentTransaction.sentTransactionHashes}`, + ); + + // Resend the transaction (after some initial delay). + const config = await getConfig(); + const blockNumber = await getBlockNumberish(sentTransaction.chainId); + const ellapsedBlocks = blockNumber - sentTransaction.sentAtBlock; + if (ellapsedBlocks >= config.minEllapsedBlocksBeforeRetry) { + job.log( + `Resending transaction after ${ellapsedBlocks} blocks. blockNumber=${blockNumber} sentAtBlock=${sentTransaction.sentAtBlock}`, + ); + await SendTransactionQueue.add({ + queueId: sentTransaction.queueId, + resendCount: sentTransaction.resendCount + 1, + }); + } + + return null; }; const _mineUserOp = async ( - job: Job, - sentTransaction: SentTransaction, + job: Job, + sentTransaction: SentTransaction, ): Promise => { - assert(sentTransaction.isUserOp); - - const userOpReceipt = await getReceiptForUserOp(sentTransaction); - if (!userOpReceipt) { - job.log( - `UserOp is not mined yet. Check again later. userOpHash=${sentTransaction.userOpHash}`, - ); - return null; - } - const { receipt } = userOpReceipt; - - job.log( - `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, - ); - - let errorMessage: string | undefined; - - // if the userOpReceipt is not successful, try to get the parsed userOpReceipt - // we expect this to fail, but we want the error message if it does - if (!userOpReceipt.success) { - try { - const chain = await getChain(sentTransaction.chainId); - const userOpReceipt = await getUserOpReceipt({ - client: thirdwebClient, - chain, - userOpHash: sentTransaction.userOpHash, - }); - job.log(`Found userOpReceipt: ${userOpReceipt}`); - } catch (e) { - if (e instanceof Error) { - errorMessage = e.message; - job.log(`Failed to get userOpReceipt: ${e.message}`); - } else { - throw e; - } - } - } - - return { - ...sentTransaction, - status: "mined", - transactionHash: receipt.transactionHash, - minedAt: new Date(), - minedAtBlock: receipt.blockNumber, - transactionType: receipt.type, - onchainStatus: userOpReceipt.success ? "success" : "reverted", - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, - gas: receipt.gasUsed, - cumulativeGasUsed: receipt.cumulativeGasUsed, - sender: userOpReceipt.sender as Address, - nonce: userOpReceipt.nonce.toString(), - errorMessage, - }; + assert(sentTransaction.isUserOp); + + const userOpReceipt = await getReceiptForUserOp(sentTransaction); + if (!userOpReceipt) { + job.log( + `UserOp is not mined yet. Check again later. userOpHash=${sentTransaction.userOpHash}`, + ); + return null; + } + const { receipt } = userOpReceipt; + + job.log( + `Found receipt. transactionHash=${receipt.transactionHash} block=${receipt.blockNumber}`, + ); + + let errorMessage: string | undefined; + + // if the userOpReceipt is not successful, try to get the parsed userOpReceipt + // we expect this to fail, but we want the error message if it does + if (!userOpReceipt.success) { + try { + const chain = await getChain(sentTransaction.chainId); + const userOpReceipt = await getUserOpReceipt({ + client: thirdwebClient, + chain, + userOpHash: sentTransaction.userOpHash, + }); + job.log(`Found userOpReceipt: ${userOpReceipt}`); + } catch (e) { + if (e instanceof Error) { + errorMessage = e.message; + job.log(`Failed to get userOpReceipt: ${e.message}`); + } else { + throw e; + } + } + } + + return { + ...sentTransaction, + status: "mined", + transactionHash: receipt.transactionHash, + minedAt: new Date(), + minedAtBlock: receipt.blockNumber, + transactionType: receipt.type, + onchainStatus: userOpReceipt.success ? "success" : "reverted", + gasUsed: receipt.gasUsed, + effectiveGasPrice: receipt.effectiveGasPrice, + gas: receipt.gasUsed, + cumulativeGasUsed: receipt.cumulativeGasUsed, + sender: userOpReceipt.sender as Address, + nonce: userOpReceipt.nonce.toString(), + errorMessage, + }; }; const _notifyIfLowBalance = async (transaction: MinedTransaction) => { - const { isUserOp, chainId, from } = transaction; - if (isUserOp) { - // Skip for userOps since they may not use the wallet's gas balance. - return; - } - - try { - const webhooks = await getWebhooksByEventType( - WebhooksEventTypes.BACKEND_WALLET_BALANCE, - ); - if (webhooks.length === 0) { - // Skip if no webhooks configured. - return; - } - - // Set a key with 5min TTL if it doesn't exist. - // This effectively throttles this check once every 5min. - const throttleKey = `webhook:${WebhooksEventTypes.BACKEND_WALLET_BALANCE}:${chainId}:${from}`; - const isThrottled = - (await redis.set(throttleKey, "", "EX", 5 * 60, "NX")) === null; - if (isThrottled) { - return; - } - - // Get the current wallet balance. - const rpcRequest = getRpcClient({ - client: thirdwebClient, - chain: await getChain(chainId), - }); - const currentBalance = await eth_getBalance(rpcRequest, { - address: from, - }); - - const config = await getConfig(); - if (currentBalance >= BigInt(config.minWalletBalance)) { - // Skip if the balance is above the alert threshold. - return; - } - - await SendWebhookQueue.enqueueWebhook({ - type: WebhooksEventTypes.BACKEND_WALLET_BALANCE, - body: { - chainId, - walletAddress: from, - minimumBalance: config.minWalletBalance, - currentBalance: currentBalance.toString(), - message: `LowBalance: The backend wallet ${from} on chain ${chainId} has ${toTokens(currentBalance, 18)} gas remaining.`, - }, - }); - } catch (e) { - logger({ - level: "warn", - message: `[mineTransactionWorker] Error sending low balance notification: ${prettifyError(e)}`, - service: "worker", - }); - } + const { isUserOp, chainId, from } = transaction; + if (isUserOp) { + // Skip for userOps since they may not use the wallet's gas balance. + return; + } + + try { + const webhooks = await getWebhooksByEventType( + WebhooksEventTypes.BACKEND_WALLET_BALANCE, + ); + if (webhooks.length === 0) { + // Skip if no webhooks configured. + return; + } + + // Set a key with 5min TTL if it doesn't exist. + // This effectively throttles this check once every 5min. + const throttleKey = `webhook:${WebhooksEventTypes.BACKEND_WALLET_BALANCE}:${chainId}:${from}`; + const isThrottled = + (await redis.set(throttleKey, "", "EX", 5 * 60, "NX")) === null; + if (isThrottled) { + return; + } + + // Get the current wallet balance. + const rpcRequest = getRpcClient({ + client: thirdwebClient, + chain: await getChain(chainId), + }); + const currentBalance = await eth_getBalance(rpcRequest, { + address: from, + }); + + const config = await getConfig(); + if (currentBalance >= BigInt(config.minWalletBalance)) { + // Skip if the balance is above the alert threshold. + return; + } + + await SendWebhookQueue.enqueueWebhook({ + type: WebhooksEventTypes.BACKEND_WALLET_BALANCE, + body: { + chainId, + walletAddress: from, + minimumBalance: config.minWalletBalance, + currentBalance: currentBalance.toString(), + message: `LowBalance: The backend wallet ${from} on chain ${chainId} has ${toTokens(currentBalance, 18)} gas remaining.`, + }, + }); + } catch (e) { + logger({ + level: "warn", + message: `[mineTransactionWorker] Error sending low balance notification: ${prettifyError(e)}`, + service: "worker", + }); + } }; // Must be explicitly called for the worker to run on this host. export const initMineTransactionWorker = () => { - const _worker = new Worker(MineTransactionQueue.q.name, handler, { - concurrency: env.CONFIRM_TRANSACTION_QUEUE_CONCURRENCY, - connection: redis, - settings: { - backoffStrategy: (attemptsMade: number) => { - // Retries at 2s, 4s, 6s, ..., 18s, 20s, 20s, 20s, ... - return Math.min(attemptsMade * 2_000, 20_000); - }, - }, - }); - - // If a transaction fails to mine after all retries, set it as errored and release the nonce. - _worker.on("failed", async (job: Job | undefined) => { - if (job && job.attemptsMade === job.opts.attempts) { - const { queueId } = superjson.parse(job.data); - - const sentTransaction = await TransactionDB.get(queueId); - if (sentTransaction?.status !== "sent") { - job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); - return; - } - - const erroredTransaction: ErroredTransaction = { - ...sentTransaction, - status: "errored", - errorMessage: "Transaction timed out.", - }; - job.log(`Transaction timed out: ${stringify(erroredTransaction)}`); - - await TransactionDB.set(erroredTransaction); - await enqueueTransactionWebhook(erroredTransaction); - _reportUsageError(erroredTransaction); - - if (!sentTransaction.isUserOp) { - // Release the nonce to allow it to be reused or cancelled. - job.log( - `Recycling nonce and removing from nonce-sent: ${sentTransaction.nonce}`, - ); - await recycleNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - - await removeSentNonce( - sentTransaction.chainId, - sentTransaction.from, - sentTransaction.nonce, - ); - } - } - }); + const _worker = new Worker(MineTransactionQueue.q.name, handler, { + concurrency: env.CONFIRM_TRANSACTION_QUEUE_CONCURRENCY, + connection: redis, + settings: { + backoffStrategy: (attemptsMade: number) => { + // Retries at 2s, 4s, 6s, ..., 18s, 20s, 20s, 20s, ... + return Math.min(attemptsMade * 2_000, 20_000); + }, + }, + }); + + // If a transaction fails to mine after all retries, set it as errored and release the nonce. + _worker.on("failed", async (job: Job | undefined) => { + if (job && job.attemptsMade === job.opts.attempts) { + const { queueId } = superjson.parse(job.data); + + const sentTransaction = await TransactionDB.get(queueId); + if (sentTransaction?.status !== "sent") { + job.log(`Invalid transaction state: ${stringify(sentTransaction)}`); + return; + } + + const erroredTransaction: ErroredTransaction = { + ...sentTransaction, + status: "errored", + errorMessage: "Transaction timed out.", + }; + job.log(`Transaction timed out: ${stringify(erroredTransaction)}`); + + await TransactionDB.set(erroredTransaction); + await enqueueTransactionWebhook(erroredTransaction); + _reportUsageError(erroredTransaction); + + if (!sentTransaction.isUserOp) { + // Release the nonce to allow it to be reused or cancelled. + job.log( + `Recycling nonce and removing from nonce-sent: ${sentTransaction.nonce}`, + ); + await recycleNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + + await removeSentNonce( + sentTransaction.chainId, + sentTransaction.from, + sentTransaction.nonce, + ); + } + } + }); }; From 9dd8683f91fac70ea3400acfc153fd3262ed1752 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Fri, 6 Dec 2024 10:50:21 +0800 Subject: [PATCH 6/6] set queuedAt to now --- src/server/routes/transaction/retry-failed.ts | 1 + test/e2e/tests/routes/write.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/routes/transaction/retry-failed.ts b/src/server/routes/transaction/retry-failed.ts index d00ecbc2d..5ac43c43b 100644 --- a/src/server/routes/transaction/retry-failed.ts +++ b/src/server/routes/transaction/retry-failed.ts @@ -102,6 +102,7 @@ export async function retryFailedTransactionRoute(fastify: FastifyInstance) { const queuedTransaction: QueuedTransaction = { ...omitted, status: "queued", + queuedAt: new Date(), resendCount: 0, }; await TransactionDB.set(queuedTransaction); diff --git a/test/e2e/tests/routes/write.test.ts b/test/e2e/tests/routes/write.test.ts index 1cad9a98d..d891055d6 100644 --- a/test/e2e/tests/routes/write.test.ts +++ b/test/e2e/tests/routes/write.test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, expect, test } from "bun:test"; import assert from "node:assert"; -import { type Address, stringToHex } from "thirdweb"; +import { stringToHex, type Address } from "thirdweb"; import { zeroAddress } from "viem"; import type { ApiError } from "../../../../sdk/dist/thirdweb-dev-engine.cjs.js"; import { CONFIG } from "../../config"; @@ -69,7 +69,7 @@ describe("/contract/write route", () => { expect(writeTransactionStatus.minedAt).toBeDefined(); }); - test.only("Write to a contract with untyped args", async () => { + test("Write to a contract with untyped args", async () => { const res = await engine.deploy.deployNftDrop( CONFIG.CHAIN.id.toString(), backendWallet,