From 37f62e938827a29e700f3b4202a673ac764b12d2 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Wed, 6 Nov 2024 11:30:04 +0800 Subject: [PATCH 1/5] chore: Show min amount needed if out of funds --- src/utils/error.ts | 26 ----------------------- src/worker/tasks/sendTransactionWorker.ts | 18 ++++++++++++++-- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/utils/error.ts b/src/utils/error.ts index 595c8c3a7..3dcc432c9 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,10 +1,6 @@ import { ethers } from "ethers"; -import { getChainMetadata } from "thirdweb/chains"; import { stringify } from "thirdweb/utils"; -import { getChain } from "./chain"; import { isEthersErrorCode } from "./ethers"; -import { doSimulateTransaction } from "./transaction/simulateQueuedTransaction"; -import type { AnyTransaction } from "./transaction/types"; export const prettifyError = (error: unknown): string => { if (error instanceof Error) { @@ -13,28 +9,6 @@ export const prettifyError = (error: unknown): string => { return stringify(error); }; -export const prettifyTransactionError = async ( - transaction: AnyTransaction, - error: Error, -): Promise => { - if (!transaction.isUserOp) { - if (isInsufficientFundsError(error)) { - const chain = await getChain(transaction.chainId); - const metadata = await getChainMetadata(chain); - return `Insufficient ${metadata.nativeCurrency?.symbol} on ${metadata.name} in ${transaction.from}.`; - } - - if (isEthersErrorCode(error, ethers.errors.UNPREDICTABLE_GAS_LIMIT)) { - const simulateError = await doSimulateTransaction(transaction); - if (simulateError) { - return simulateError; - } - } - } - - return error.message; -}; - const _parseMessage = (error: unknown): string | null => { return error && typeof error === "object" && "message" in error ? (error.message as string).toLowerCase() diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index 08b4f570b..cb68d493a 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -6,8 +6,10 @@ import { getContract, readContract, toSerializableTransaction, + toTokens, type Hex, } from "thirdweb"; +import { getChainMetadata } from "thirdweb/chains"; import { stringify } from "thirdweb/utils"; import type { Account } from "thirdweb/wallets"; import { @@ -32,10 +34,10 @@ import { getChain } from "../../utils/chain"; import { msSince } from "../../utils/date"; import { env } from "../../utils/env"; import { + isInsufficientFundsError, isNonceAlreadyUsedError, isReplacementGasFeeTooLow, prettifyError, - prettifyTransactionError, } from "../../utils/error"; import { getChecksumAddress } from "../../utils/primitiveTypes"; import { recordMetrics } from "../../utils/prometheus"; @@ -380,6 +382,18 @@ const _sendTransaction = async ( job.log(`Recycling nonce: ${nonce}`); await recycleNonce(chainId, from, nonce); } + + // Prettify "out of funds" error. + if (isInsufficientFundsError(error)) { + const gasPrice = + populatedTransaction.gasPrice ?? populatedTransaction.maxFeePerGas; + if (gasPrice) { + const chainMetadata = await getChainMetadata(chain); + const minGasTokens = toTokens(populatedTransaction.gas * gasPrice, 18); + throw `Insufficient funds in ${account.address} on ${chainMetadata.name}. Transaction requires ${minGasTokens} ${chainMetadata.nativeCurrency.symbol}.`; + } + } + throw error; } @@ -572,7 +586,7 @@ export const initSendTransactionWorker = () => { const erroredTransaction: ErroredTransaction = { ...transaction, status: "errored", - errorMessage: await prettifyTransactionError(transaction, error), + errorMessage: error.message, }; job.log(`Transaction errored: ${stringify(erroredTransaction)}`); From 584b1a23ba9aa53fec9d1322eadbab124646aa9e Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 7 Nov 2024 09:56:05 +0800 Subject: [PATCH 2/5] dont retry if out of funds --- src/utils/error.ts | 14 ++++---- src/worker/queues/queues.ts | 2 +- src/worker/tasks/sendTransactionWorker.ts | 44 +++++++++++------------ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/utils/error.ts b/src/utils/error.ts index 3dcc432c9..d20de1614 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -2,12 +2,14 @@ import { ethers } from "ethers"; import { stringify } from "thirdweb/utils"; import { isEthersErrorCode } from "./ethers"; -export const prettifyError = (error: unknown): string => { - if (error instanceof Error) { - return error.message; - } - return stringify(error); -}; +/** + * Pretty print an error with an optional contextual prefix ("[RpcError] \"). + */ +export const prettifyError = ( + error: unknown, + prefix?: "RPC" | "Bundler", +): string => + `[${prefix}] ${error instanceof Error ? error.message : stringify(error)}`; const _parseMessage = (error: unknown): string | null => { return error && typeof error === "object" && "message" in error diff --git a/src/worker/queues/queues.ts b/src/worker/queues/queues.ts index f0c1e8456..d22534c37 100644 --- a/src/worker/queues/queues.ts +++ b/src/worker/queues/queues.ts @@ -1,4 +1,4 @@ -import { Job, JobsOptions, Worker } from "bullmq"; +import type { Job, JobsOptions, Worker } from "bullmq"; import { env } from "../../utils/env"; import { logger } from "../../utils/logger"; diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index cb68d493a..d58624b16 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -246,15 +246,11 @@ const _sendUserOp = async ( waitForDeployment: false, })) as UserOperation; // TODO support entrypoint v0.7 accounts } catch (e) { - const erroredTransaction: ErroredTransaction = { + return { ...queuedTransaction, status: "errored", - errorMessage: prettifyError(e), - }; - job.log( - `Failed to populate transaction: ${erroredTransaction.errorMessage}`, - ); - return erroredTransaction; + errorMessage: prettifyError(e, "Bundler"), + } satisfies ErroredTransaction; } job.log(`Populated userOp: ${stringify(signedUserOp)}`); @@ -327,15 +323,11 @@ const _sendTransaction = async ( }, }); } catch (e: unknown) { - const erroredTransaction: ErroredTransaction = { + return { ...queuedTransaction, status: "errored", - errorMessage: prettifyError(e), - }; - job.log( - `Failed to populate transaction: ${erroredTransaction.errorMessage}`, - ); - return erroredTransaction; + errorMessage: prettifyError(e, "RPC"), + } satisfies ErroredTransaction; } // Handle if `maxFeePerGas` is overridden. @@ -371,10 +363,10 @@ const _sendTransaction = async ( const sendTransactionResult = await account.sendTransaction(populatedTransaction); transactionHash = sendTransactionResult.transactionHash; - } catch (error: unknown) { + } catch (e: unknown) { // If the nonce is already seen onchain (nonce too low) or in mempool (replacement underpriced), // correct the DB nonce. - if (isNonceAlreadyUsedError(error) || isReplacementGasFeeTooLow(error)) { + if (isNonceAlreadyUsedError(e) || isReplacementGasFeeTooLow(e)) { const result = await syncLatestNonceFromOnchainIfHigher(chainId, from); job.log(`Re-synced nonce: ${result}`); } else { @@ -383,18 +375,26 @@ const _sendTransaction = async ( await recycleNonce(chainId, from, nonce); } - // Prettify "out of funds" error. - if (isInsufficientFundsError(error)) { + // Do not retry errors that are expected to be rejected by RPC again. + if (isInsufficientFundsError(e)) { const gasPrice = populatedTransaction.gasPrice ?? populatedTransaction.maxFeePerGas; + + let errorMessage = prettifyError(e, "RPC"); if (gasPrice) { - const chainMetadata = await getChainMetadata(chain); - const minGasTokens = toTokens(populatedTransaction.gas * gasPrice, 18); - throw `Insufficient funds in ${account.address} on ${chainMetadata.name}. Transaction requires ${minGasTokens} ${chainMetadata.nativeCurrency.symbol}.`; + const { gas, value = 0n } = populatedTransaction; + const { name, nativeCurrency } = await getChainMetadata(chain); + const minGasTokens = toTokens(gas * gasPrice + value, 18); + errorMessage = `Insufficient funds in ${account.address} on ${name}. Transaction requires > ${minGasTokens} ${nativeCurrency.symbol}.`; } + return { + ...queuedTransaction, + status: "errored", + errorMessage, + } satisfies ErroredTransaction; } - throw error; + throw prettifyError(e, "RPC"); } await addSentNonce(chainId, from, nonce); From 2deedb6d9b283f1bb17119d10a70ad1b071c0b69 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 7 Nov 2024 10:09:59 +0800 Subject: [PATCH 3/5] wrap error --- src/utils/error.ts | 13 +++++-------- src/worker/tasks/sendTransactionWorker.ts | 11 ++++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/utils/error.ts b/src/utils/error.ts index d20de1614..e67f3d46f 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -2,14 +2,11 @@ import { ethers } from "ethers"; import { stringify } from "thirdweb/utils"; import { isEthersErrorCode } from "./ethers"; -/** - * Pretty print an error with an optional contextual prefix ("[RpcError] \"). - */ -export const prettifyError = ( - error: unknown, - prefix?: "RPC" | "Bundler", -): string => - `[${prefix}] ${error instanceof Error ? error.message : stringify(error)}`; +export const wrapError = (error: unknown, prefix: "RPC" | "Bundler") => + new Error(`[${prefix}] ${prettifyError(error)}`); + +export const prettifyError = (error: unknown): string => + error instanceof Error ? error.message : stringify(error); const _parseMessage = (error: unknown): string | null => { return error && typeof error === "object" && "message" in error diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index d58624b16..54bb51b89 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -38,6 +38,7 @@ import { isNonceAlreadyUsedError, isReplacementGasFeeTooLow, prettifyError, + wrapError, } from "../../utils/error"; import { getChecksumAddress } from "../../utils/primitiveTypes"; import { recordMetrics } from "../../utils/prometheus"; @@ -249,7 +250,7 @@ const _sendUserOp = async ( return { ...queuedTransaction, status: "errored", - errorMessage: prettifyError(e, "Bundler"), + errorMessage: wrapError(e, "Bundler").message, } satisfies ErroredTransaction; } @@ -326,7 +327,7 @@ const _sendTransaction = async ( return { ...queuedTransaction, status: "errored", - errorMessage: prettifyError(e, "RPC"), + errorMessage: wrapError(e, "RPC").message, } satisfies ErroredTransaction; } @@ -380,7 +381,7 @@ const _sendTransaction = async ( const gasPrice = populatedTransaction.gasPrice ?? populatedTransaction.maxFeePerGas; - let errorMessage = prettifyError(e, "RPC"); + let errorMessage = prettifyError(e); if (gasPrice) { const { gas, value = 0n } = populatedTransaction; const { name, nativeCurrency } = await getChainMetadata(chain); @@ -394,7 +395,7 @@ const _sendTransaction = async ( } satisfies ErroredTransaction; } - throw prettifyError(e, "RPC"); + throw wrapError(e, "RPC"); } await addSentNonce(chainId, from, nonce); @@ -480,7 +481,7 @@ const _resendTransaction = async ( job.log("A pending transaction exists with >= gas fees. Do not resend."); return null; } - throw error; + throw wrapError(error, "RPC"); } return { From fd635299d15ec5a5f8d1f23ac16aca375cd4d6b4 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 7 Nov 2024 10:11:37 +0800 Subject: [PATCH 4/5] error --- src/worker/tasks/sendTransactionWorker.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index 54bb51b89..20c0f0247 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -246,11 +246,11 @@ const _sendUserOp = async ( // we don't want this behavior in the engine context waitForDeployment: false, })) as UserOperation; // TODO support entrypoint v0.7 accounts - } catch (e) { + } catch (error) { return { ...queuedTransaction, status: "errored", - errorMessage: wrapError(e, "Bundler").message, + errorMessage: wrapError(error, "Bundler").message, } satisfies ErroredTransaction; } @@ -364,10 +364,10 @@ const _sendTransaction = async ( const sendTransactionResult = await account.sendTransaction(populatedTransaction); transactionHash = sendTransactionResult.transactionHash; - } catch (e: unknown) { + } catch (error: unknown) { // If the nonce is already seen onchain (nonce too low) or in mempool (replacement underpriced), // correct the DB nonce. - if (isNonceAlreadyUsedError(e) || isReplacementGasFeeTooLow(e)) { + if (isNonceAlreadyUsedError(error) || isReplacementGasFeeTooLow(error)) { const result = await syncLatestNonceFromOnchainIfHigher(chainId, from); job.log(`Re-synced nonce: ${result}`); } else { @@ -377,11 +377,11 @@ const _sendTransaction = async ( } // Do not retry errors that are expected to be rejected by RPC again. - if (isInsufficientFundsError(e)) { + if (isInsufficientFundsError(error)) { const gasPrice = populatedTransaction.gasPrice ?? populatedTransaction.maxFeePerGas; - let errorMessage = prettifyError(e); + let errorMessage = prettifyError(error); if (gasPrice) { const { gas, value = 0n } = populatedTransaction; const { name, nativeCurrency } = await getChainMetadata(chain); @@ -395,7 +395,7 @@ const _sendTransaction = async ( } satisfies ErroredTransaction; } - throw wrapError(e, "RPC"); + throw wrapError(error, "RPC"); } await addSentNonce(chainId, from, nonce); From d99eff4791ca6cdfd7cbe7d36d218e3e21cc5582 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Thu, 7 Nov 2024 10:19:25 +0800 Subject: [PATCH 5/5] update custom message --- src/worker/tasks/sendTransactionWorker.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index 20c0f0247..6b3d091c9 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -37,7 +37,6 @@ import { isInsufficientFundsError, isNonceAlreadyUsedError, isReplacementGasFeeTooLow, - prettifyError, wrapError, } from "../../utils/error"; import { getChecksumAddress } from "../../utils/primitiveTypes"; @@ -378,16 +377,17 @@ const _sendTransaction = async ( // Do not retry errors that are expected to be rejected by RPC again. if (isInsufficientFundsError(error)) { + const { name, nativeCurrency } = await getChainMetadata(chain); + const { gas, value = 0n } = populatedTransaction; const gasPrice = populatedTransaction.gasPrice ?? populatedTransaction.maxFeePerGas; - let errorMessage = prettifyError(error); - if (gasPrice) { - const { gas, value = 0n } = populatedTransaction; - const { name, nativeCurrency } = await getChainMetadata(chain); - const minGasTokens = toTokens(gas * gasPrice + value, 18); - errorMessage = `Insufficient funds in ${account.address} on ${name}. Transaction requires > ${minGasTokens} ${nativeCurrency.symbol}.`; - } + const minGasTokens = gasPrice + ? toTokens(gas * gasPrice + value, 18) + : null; + const errorMessage = minGasTokens + ? `Insufficient funds in ${account.address} on ${name}. Transaction requires > ${minGasTokens} ${nativeCurrency.symbol}.` + : `Insufficient funds in ${account.address} on ${name}. Transaction requires more ${nativeCurrency.symbol}.`; return { ...queuedTransaction, status: "errored",