diff --git a/src/utils/error.ts b/src/utils/error.ts index c7b619c7..f524c967 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -16,11 +16,9 @@ const _parseMessage = (error: unknown): string | null => { export const isNonceAlreadyUsedError = (error: unknown) => { const message = _parseMessage(error); - const errorPhrases = ["nonce too low", "already known"]; - if (message) { - return errorPhrases.some((phrase) => - message.toLowerCase().includes(phrase), + return ( + message.includes("nonce too low") || message.includes("already known") ); } diff --git a/src/worker/tasks/sendTransactionWorker.ts b/src/worker/tasks/sendTransactionWorker.ts index 783530ba..b0d62f2a 100644 --- a/src/worker/tasks/sendTransactionWorker.ts +++ b/src/worker/tasks/sendTransactionWorker.ts @@ -22,6 +22,7 @@ import { TransactionDB } from "../../db/transactions/db"; import { acquireNonce, addSentNonce, + deleteNoncesForBackendWallets, recycleNonce, syncLatestNonceFromOnchainIfHigher, } from "../../db/wallets/walletNonce"; @@ -279,10 +280,7 @@ const _sendTransaction = async ( const { queueId, chainId, from, to, overrides } = queuedTransaction; const chain = await getChain(chainId); - const account = await getAccount({ - chainId: chainId, - from: from, - }); + const account = await getAccount({ chainId, from }); // Populate the transaction to resolve gas values. // This call throws if the execution would be reverted. @@ -352,30 +350,24 @@ const _sendTransaction = async ( await account.sendTransaction(populatedTransaction); transactionHash = sendTransactionResult.transactionHash; } catch (error: 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)) { - const result = await syncLatestNonceFromOnchainIfHigher(chainId, from); - job.log(`Re-synced nonce: ${result}`); - } else { - // Otherwise this nonce is not used yet. Recycle it to be used by a future transaction. - job.log(`Recycling nonce: ${nonce}`); - await recycleNonce(chainId, from, nonce); - } - - // Do not retry errors that are expected to be rejected by RPC again. if (isInsufficientFundsError(error)) { - const { name, nativeCurrency } = await getChainMetadata(chain); + // Insufficient funds. Do not retry const { gas, value = 0n } = populatedTransaction; + const { name, nativeCurrency } = await getChainMetadata(chain); + + // This and other pending transactions will fail. + // Reset the nonce state for this wallet. The first transaction after the wallet is funded will resync the nonce. + if (value === 0n) { + await deleteNoncesForBackendWallets([{ chainId, walletAddress: from }]); + } + const gasPrice = - populatedTransaction.gasPrice ?? populatedTransaction.maxFeePerGas; - - 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}.`; + populatedTransaction.gasPrice ?? + populatedTransaction.maxFeePerGas ?? + 0n; + const minGasTokens = toTokens(gas * gasPrice + value, 18); + const errorMessage = `Insufficient funds in ${account.address} on ${name}. Transaction requires > ${minGasTokens} ${nativeCurrency.symbol}.`; + return { ...queuedTransaction, status: "errored", @@ -383,6 +375,16 @@ const _sendTransaction = async ( } satisfies ErroredTransaction; } + if (isNonceAlreadyUsedError(error) || isReplacementGasFeeTooLow(error)) { + // Nonce is already used (onchain or in mempool). Resync to correct the DB nonce. + const result = await syncLatestNonceFromOnchainIfHigher(chainId, from); + job.log(`Re-synced nonce: ${result}`); + } else { + // Other error: assume the nonce is not used. Recycle it to be used by a future transaction. + job.log(`Recycling nonce: ${nonce}`); + await recycleNonce(chainId, from, nonce); + } + throw wrapError(error, "RPC"); }