diff --git a/packages/transaction-manager/lib/Transaction.ts b/packages/transaction-manager/lib/Transaction.ts index bcdd1260c2..6775a2d689 100644 --- a/packages/transaction-manager/lib/Transaction.ts +++ b/packages/transaction-manager/lib/Transaction.ts @@ -50,6 +50,10 @@ export class Transaction { readonly attempts: Attempt[] + createdAt: Date + + updatedAt: Date + /** * Stores additional information for the transaction. * Enables originators to provide extra details, such as gas limits, which can be leveraged by customizable services. @@ -66,6 +70,8 @@ export class Transaction { deadline, status, attempts, + createdAt, + updatedAt, metadata, }: { intentId?: UUID @@ -77,6 +83,8 @@ export class Transaction { deadline?: number status?: TransactionStatus attempts?: Attempt[] + createdAt?: Date + updatedAt?: Date metadata?: Record }) { this.intentId = intentId ?? createUUID() @@ -88,11 +96,14 @@ export class Transaction { this.deadline = deadline this.status = status ?? TransactionStatus.Pending this.attempts = attempts ?? [] + this.createdAt = createdAt ?? new Date() + this.updatedAt = updatedAt ?? new Date() this.metadata = metadata } addAttempt(attempt: Attempt): void { this.attempts.push(attempt) + this.updatedAt = new Date() } removeAttempt(hash: Hash): void { @@ -100,6 +111,7 @@ export class Transaction { if (index > -1) { this.attempts.splice(index, 1) } + this.updatedAt = new Date() } getInAirAttempts(): Attempt[] { @@ -114,7 +126,7 @@ export class Transaction { changeStatus(status: TransactionStatus): void { this.status = status - + this.updatedAt = new Date() eventBus.emit(Topics.TransactionStatusChanged, { transaction: this, }) @@ -140,6 +152,8 @@ export class Transaction { status: this.status, attempts: JSON.stringify(this.attempts, bigIntReplacer), metadata: this.metadata ? JSON.stringify(this.metadata, bigIntReplacer) : undefined, + createdAt: this.createdAt.getTime(), + updatedAt: this.updatedAt.getTime(), } } @@ -149,6 +163,8 @@ export class Transaction { args: JSON.parse(row.args, bigIntReviver), attempts: JSON.parse(row.attempts, bigIntReviver), metadata: row.metadata ? JSON.parse(row.metadata, bigIntReviver) : undefined, + createdAt: new Date(row.createdAt), + updatedAt: new Date(row.updatedAt), }) } } diff --git a/packages/transaction-manager/lib/TransactionManager.ts b/packages/transaction-manager/lib/TransactionManager.ts index d3f790cca6..f6ef369326 100644 --- a/packages/transaction-manager/lib/TransactionManager.ts +++ b/packages/transaction-manager/lib/TransactionManager.ts @@ -68,6 +68,13 @@ export type TransactionManagerConfig = { */ blockTime?: bigint + /** + * The time (in milliseconds) after which finalized transactions are purged from the database. + * If finalizedTransactionPurgeTime is 0, finalized transactions are not purged from the database. + * Defaults to 2 minutes. + */ + finalizedTransactionPurgeTime?: number + /** * The gas estimator to use for estimating the gas limit of a transaction. * You can provide your own implementation to override the default one. @@ -99,6 +106,7 @@ export class TransactionManager { public readonly maxPriorityFeePerGas: bigint public readonly rpcAllowDebug: boolean public readonly blockTime: bigint + public readonly finalizedTransactionPurgeTime: number constructor(_config: TransactionManagerConfig) { this.collectors = [] @@ -136,6 +144,7 @@ export class TransactionManager { this.rpcAllowDebug = _config.rpcAllowDebug || false this.blockTime = _config.blockTime || 2n + this.finalizedTransactionPurgeTime = _config.finalizedTransactionPurgeTime || 2 * 60 * 1000 } /** diff --git a/packages/transaction-manager/lib/TransactionRepository.ts b/packages/transaction-manager/lib/TransactionRepository.ts index 2301115c4a..9f1f2ee3ce 100644 --- a/packages/transaction-manager/lib/TransactionRepository.ts +++ b/packages/transaction-manager/lib/TransactionRepository.ts @@ -1,6 +1,7 @@ import { unknownToError } from "@happychain/common" import type { UUID } from "@happychain/common" import { type Result, ResultAsync } from "neverthrow" +import { Topics, eventBus } from "./EventBus.js" import { NotFinalizedStatuses, Transaction } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" import { db } from "./db/driver.js" @@ -22,6 +23,10 @@ export class TransactionRepository { .execute() this.notFinalizedTransactions = transactionRows.map((row) => Transaction.fromDbRow(row)) + + if (this.transactionManager.finalizedTransactionPurgeTime > 0) { + eventBus.on(Topics.NewBlock, this.purgeFinalizedTransactions.bind(this)) + } } getNotFinalizedTransactions(): Transaction[] { @@ -72,6 +77,11 @@ export class TransactionRepository { .execute(), unknownToError, ) + + this.notFinalizedTransactions = this.notFinalizedTransactions.filter((transaction) => + NotFinalizedStatuses.includes(transaction.status), + ) + return result.map(() => undefined) } @@ -89,6 +99,11 @@ export class TransactionRepository { }), unknownToError, ) + + this.notFinalizedTransactions = this.notFinalizedTransactions.filter((transaction) => + NotFinalizedStatuses.includes(transaction.status), + ) + return result } @@ -103,4 +118,12 @@ export class TransactionRepository { (n) => !this.notFinalizedTransactions.some((t) => t.attempts.some((a) => a.nonce === n)), ) } + + async purgeFinalizedTransactions() { + await db + .deleteFrom("transaction") + .where("status", "not in", NotFinalizedStatuses) + .where("updatedAt", "<", Date.now() - this.transactionManager.finalizedTransactionPurgeTime) + .execute() + } } diff --git a/packages/transaction-manager/lib/db/types.ts b/packages/transaction-manager/lib/db/types.ts index 7f545486f4..e5b71b1589 100644 --- a/packages/transaction-manager/lib/db/types.ts +++ b/packages/transaction-manager/lib/db/types.ts @@ -13,6 +13,8 @@ export interface TransactionTable { status: TransactionStatus attempts: string metadata: string | undefined + createdAt: number + updatedAt: number } export interface Database { diff --git a/packages/transaction-manager/migrations/Migration20241111223000.js b/packages/transaction-manager/migrations/Migration20241111223000.js new file mode 100644 index 0000000000..97e24c6ba7 --- /dev/null +++ b/packages/transaction-manager/migrations/Migration20241111223000.js @@ -0,0 +1,10 @@ +/* + SQLite does not have native time types. The SQL interface allows arbitrary type names including "DATE" and "DATETIME", + but this is invalid in this API, and results in "NUMERIC" affinity instead of "INTEGER" affinity, + which is the one we want here +*/ +export async function up(db) { + await db.schema.alterTable("transaction").addColumn("createdAt", "integer").execute() + + await db.schema.alterTable("transaction").addColumn("updatedAt", "integer").execute() +}