diff --git a/packages/txm/lib/NonceManager.ts b/packages/txm/lib/NonceManager.ts index 861986f2b7..c381a87bbf 100644 --- a/packages/txm/lib/NonceManager.ts +++ b/packages/txm/lib/NonceManager.ts @@ -28,12 +28,14 @@ import type { TransactionManager } from "./TransactionManager" export class NonceManager { private txmgr: TransactionManager private nonce!: number - private returnedNonceQueue!: number[] + maxExecutedNonce: number + constructor(_transactionManager: TransactionManager) { this.txmgr = _transactionManager this.returnedNonceQueue = [] + this.maxExecutedNonce = 0 } public async start() { @@ -41,9 +43,10 @@ export class NonceManager { const blockchainNonce = await this.txmgr.viemClient.getTransactionCount({ address: address, - blockTag: "pending", }) + this.maxExecutedNonce = blockchainNonce + const highestDbNonce = this.txmgr.transactionRepository.getHighestNonce() if (!highestDbNonce || highestDbNonce < blockchainNonce) { @@ -77,4 +80,14 @@ export class NonceManager { this.returnedNonceQueue.splice(index, 0, nonce) } } + + public async resync() { + const address = this.txmgr.viemWallet.account.address + + const blockchainNonce = await this.txmgr.viemClient.getTransactionCount({ + address: address, + }) + + this.maxExecutedNonce = blockchainNonce + } } diff --git a/packages/txm/lib/Transaction.ts b/packages/txm/lib/Transaction.ts index b90384d40a..0dda4ea2c6 100644 --- a/packages/txm/lib/Transaction.ts +++ b/packages/txm/lib/Transaction.ts @@ -26,6 +26,11 @@ export enum TransactionStatus { * The transaction has expired, and we cancelled it to save gas, preventing it from being included on-chain and potentially reverting or executing actions that are no longer relevant. */ Cancelled = "Cancelled", + /** + * The transaction's inclusion was interrupted because an external transaction using the same nonce was processed. + * To retry including this transaction, it must be resubmitted by a {@link TransactionOriginator}. + */ + Interrupted = "Interrupted", /** * The transaction has been included onchain and its execution was successful. */ diff --git a/packages/txm/lib/TransactionCollector.ts b/packages/txm/lib/TransactionCollector.ts index 917e502224..22e75d52a8 100644 --- a/packages/txm/lib/TransactionCollector.ts +++ b/packages/txm/lib/TransactionCollector.ts @@ -1,7 +1,7 @@ import { LogTag, Logger } from "@happy.tech/common" import type { LatestBlock } from "./BlockMonitor.js" import { Topics, eventBus } from "./EventBus.js" -import { AttemptType } from "./Transaction.js" +import { AttemptType, TransactionStatus } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" /** @@ -45,6 +45,10 @@ export class TransactionCollector { transactionsBatch.map(async (transaction) => { const nonce = this.txmgr.nonceManager.requestNonce() + if (transaction.status === TransactionStatus.Interrupted) { + transaction.changeStatus(TransactionStatus.Pending) + } + const submissionResult = await this.txmgr.transactionSubmitter.attemptSubmission(transaction, { type: AttemptType.Original, nonce, diff --git a/packages/txm/lib/TransactionSubmitter.ts b/packages/txm/lib/TransactionSubmitter.ts index a8e27232b4..69a9357b41 100644 --- a/packages/txm/lib/TransactionSubmitter.ts +++ b/packages/txm/lib/TransactionSubmitter.ts @@ -2,7 +2,7 @@ import { LogTag } from "@happy.tech/common" import { Logger } from "@happy.tech/common" import { type Result, err, ok } from "neverthrow" import type { Hash, Hex, TransactionRequestEIP1559 } from "viem" -import { encodeFunctionData, keccak256 } from "viem" +import { TransactionRejectedRpcError, encodeFunctionData, keccak256 } from "viem" import type { EstimateGasErrorCause } from "./GasEstimator.js" import { type Attempt, AttemptType, type Transaction } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" @@ -150,6 +150,12 @@ export class TransactionSubmitter { }) if (sendRawTransactionResult.isErr()) { + if ( + sendRawTransactionResult.error instanceof TransactionRejectedRpcError && + sendRawTransactionResult.error.message.includes("nonce too low") + ) { + this.txmgr.nonceManager.resync() + } return err({ cause: AttemptSubmissionErrorCause.FailedToSendRawTransaction, description: `Failed to send raw transaction ${transaction.intentId}. Details: ${sendRawTransactionResult.error}`, diff --git a/packages/txm/lib/TxMonitor.ts b/packages/txm/lib/TxMonitor.ts index 7bd78ad8cd..f2a1b4d6ab 100644 --- a/packages/txm/lib/TxMonitor.ts +++ b/packages/txm/lib/TxMonitor.ts @@ -120,6 +120,18 @@ export class TxMonitor { return } + const nonce = transaction.lastAttempt?.nonce + + if (nonce === undefined) { + console.error(`Transaction ${transaction.intentId} inconsistent state: no nonce found`) + return + } + + if (nonce <= this.transactionManager.nonceManager.maxExecutedNonce) { + transaction.changeStatus(TransactionStatus.Interrupted) + return + } + return await (transaction.isExpired(block, this.transactionManager.blockTime) ? this.handleExpiredTransaction(transaction) : this.handleStuckTransaction(transaction))