diff --git a/bun.lockb b/bun.lockb index 9d4daed686..2670ca3a46 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/randomness-service/package.json b/packages/randomness-service/package.json index 1a20d42e92..b135403144 100644 --- a/packages/randomness-service/package.json +++ b/packages/randomness-service/package.json @@ -7,6 +7,7 @@ "module": "./dist/index.es.js", "dependencies": { "@happychain/transaction-manager": "workspace:^", + "@happychain/common": "workspace:^", "neverthrow": "^8.1.0", "viem": "^2.21.53", "zod": "^3.23.8" diff --git a/packages/randomness-service/src/CommitmentManager.ts b/packages/randomness-service/src/CommitmentManager.ts index bacc5059bf..901e3dfa48 100644 --- a/packages/randomness-service/src/CommitmentManager.ts +++ b/packages/randomness-service/src/CommitmentManager.ts @@ -1,23 +1,28 @@ import crypto from "node:crypto" +import type { UUID } from "@happychain/common" import type { Hex } from "viem" import { encodePacked, keccak256 } from "viem" interface Commitment { value: bigint commitment: Hex + transactionIntentId: UUID } export class CommitmentManager { private readonly map = new Map() - generateCommitmentForTimestamp(timestamp: bigint): Commitment { + generateCommitment(): Omit { const value = this.generateRandomness() const commitment = this.hashValue(value) const commitmentObject = { value, commitment } - this.map.set(timestamp, commitmentObject) return commitmentObject } + setCommitmentForTimestamp(timestamp: bigint, commitment: Commitment): void { + this.map.set(timestamp, commitment) + } + getCommitmentForTimestamp(timestamp: bigint): Commitment | undefined { return this.map.get(timestamp) } diff --git a/packages/randomness-service/src/index.ts b/packages/randomness-service/src/index.ts index b9b994bd13..8454fde8d2 100644 --- a/packages/randomness-service/src/index.ts +++ b/packages/randomness-service/src/index.ts @@ -1,4 +1,4 @@ -import { TransactionManager } from "@happychain/transaction-manager" +import { TransactionManager, TransactionStatus } from "@happychain/transaction-manager" import type { LatestBlock, Transaction } from "@happychain/transaction-manager" import { webSocket } from "viem" import { privateKeyToAccount } from "viem/accounts" @@ -36,12 +36,12 @@ class RandomnessService { this.txm.addTransactionCollector(this.onCollectTransactions.bind(this)) } - private onCollectTransactions(block: LatestBlock): Transaction[] { + private async onCollectTransactions(block: LatestBlock): Promise { const transactions: Transaction[] = [] // We try to commit the ramdomness POST_COMMIT_MARGIN to be safe that the transaction is included before the PRECOMMIT_DELAY const commitmentTimestamp = block.timestamp + env.PRECOMMIT_DELAY + env.POST_COMMIT_MARGIN - const commitment = this.commitmentManager.generateCommitmentForTimestamp(commitmentTimestamp) + const commitment = this.commitmentManager.generateCommitment() const commitmentTransaction = this.commitmentTransactionFactory.create( commitmentTimestamp, @@ -50,14 +50,23 @@ class RandomnessService { transactions.push(commitmentTransaction) + this.commitmentManager.setCommitmentForTimestamp(commitmentTimestamp, { + ...commitment, + transactionIntentId: commitmentTransaction.intentId, + }) + const revealValueCommitment = this.commitmentManager.getCommitmentForTimestamp(block.timestamp + env.TIME_BLOCK) if (revealValueCommitment) { - const revealValueTransaction = this.revealValueTransactionFactory.create( - block.timestamp + env.TIME_BLOCK, - revealValueCommitment.value, - ) - transactions.push(revealValueTransaction) + const transaction = await this.txm.getTransaction(revealValueCommitment.transactionIntentId) + + if (transaction?.status === TransactionStatus.Success) { + const revealValueTransaction = this.revealValueTransactionFactory.create( + block.timestamp + env.TIME_BLOCK, + revealValueCommitment.value, + ) + transactions.push(revealValueTransaction) + } } return transactions diff --git a/packages/transaction-manager/lib/EventBus.ts b/packages/transaction-manager/lib/EventBus.ts index 63e7cd19d4..212839a644 100644 --- a/packages/transaction-manager/lib/EventBus.ts +++ b/packages/transaction-manager/lib/EventBus.ts @@ -2,6 +2,7 @@ import EventEmitter from "eventemitter3" export enum Topics { NewBlock = "NewBlock", + TransactionStatusChanged = "TransactionStatusChanged", } export type EventBus = EventEmitter diff --git a/packages/transaction-manager/lib/HookManager.ts b/packages/transaction-manager/lib/HookManager.ts new file mode 100644 index 0000000000..6846062df2 --- /dev/null +++ b/packages/transaction-manager/lib/HookManager.ts @@ -0,0 +1,44 @@ +import { Topics, eventBus } from "./EventBus.js" +import type { Transaction } from "./Transaction.js" + +export enum TxmHookType { + All = "All", + TransactionStatusChanged = "TransactionStatusChanged", +} + +export type TxmHookPayload = { + type: TxmHookType + transaction: Transaction +} + +export type TxmHookHandler = (event: TxmHookPayload) => void + +export class HookManager { + private hooks: Record + + constructor() { + this.hooks = { + [TxmHookType.All]: [], + [TxmHookType.TransactionStatusChanged]: [], + } + eventBus.on(Topics.TransactionStatusChanged, this.onTransactionStatusChanged.bind(this)) + } + + public async addHook(handler: TxmHookHandler, type: TxmHookType): Promise { + if (!this.hooks[type]) { + this.hooks[type] = [] + } + this.hooks[type].push(handler) + } + + private async onTransactionStatusChanged(payload: { + transaction: Transaction + }): Promise { + this.hooks[TxmHookType.TransactionStatusChanged].concat(this.hooks[TxmHookType.All]).map((h) => + h({ + type: TxmHookType.TransactionStatusChanged, + transaction: payload.transaction, + }), + ) + } +} diff --git a/packages/transaction-manager/lib/Transaction.ts b/packages/transaction-manager/lib/Transaction.ts index 198386836b..bcdd1260c2 100644 --- a/packages/transaction-manager/lib/Transaction.ts +++ b/packages/transaction-manager/lib/Transaction.ts @@ -2,6 +2,7 @@ import { type UUID, bigIntReplacer, bigIntReviver, createUUID } from "@happychai import type { Insertable, Selectable } from "kysely" import type { Address, ContractFunctionArgs, Hash } from "viem" import type { LatestBlock } from "./BlockMonitor" +import { Topics, eventBus } from "./EventBus.js" import type { TransactionTable } from "./db/types.js" export enum TransactionStatus { @@ -113,6 +114,10 @@ export class Transaction { changeStatus(status: TransactionStatus): void { this.status = status + + eventBus.emit(Topics.TransactionStatusChanged, { + transaction: this, + }) } get attemptCount(): number { diff --git a/packages/transaction-manager/lib/TransactionCollector.ts b/packages/transaction-manager/lib/TransactionCollector.ts index 075e090624..d36c038a95 100644 --- a/packages/transaction-manager/lib/TransactionCollector.ts +++ b/packages/transaction-manager/lib/TransactionCollector.ts @@ -14,8 +14,9 @@ export class TransactionCollector { private async onNewBlock(block: LatestBlock) { const { maxFeePerGas, maxPriorityFeePerGas } = this.txmgr.gasPriceOracle.suggestGasForNextBlock() - const transactionsBatch = this.txmgr.collectors - .flatMap((c) => c(block)) + const transactionUnsorted = await Promise.all(this.txmgr.collectors.map((c) => c(block))) + const transactionsBatch = transactionUnsorted + .flat() .sort((a, b) => (a.deadline ?? Number.POSITIVE_INFINITY) - (b.deadline ?? Number.POSITIVE_INFINITY)) const saveResult = await this.txmgr.transactionRepository.saveTransactions(transactionsBatch) diff --git a/packages/transaction-manager/lib/TransactionManager.ts b/packages/transaction-manager/lib/TransactionManager.ts index 3985bad14e..d3f790cca6 100644 --- a/packages/transaction-manager/lib/TransactionManager.ts +++ b/packages/transaction-manager/lib/TransactionManager.ts @@ -1,6 +1,7 @@ import { type SafeViemPublicClient, type SafeViemWalletClient, + type UUID, convertToSafeViemPublicClient, convertToSafeViemWalletClient, } from "@happychain/common" @@ -9,6 +10,7 @@ import { ABIManager } from "./AbiManager.js" import { BlockMonitor, type LatestBlock } from "./BlockMonitor.js" import { GasEstimator } from "./GasEstimator.js" import { GasPriceOracle } from "./GasPriceOracle.js" +import { HookManager, type TxmHookHandler, type TxmHookType } from "./HookManager.js" import { NonceManager } from "./NonceManager.js" import type { Transaction } from "./Transaction.js" import { TransactionCollector } from "./TransactionCollector.js" @@ -74,7 +76,7 @@ export type TransactionManagerConfig = { gasEstimator?: GasEstimator } -export type TransactionOriginator = (block: LatestBlock) => Transaction[] +export type TransactionOriginator = (block: LatestBlock) => Promise export class TransactionManager { public readonly collectors: TransactionOriginator[] @@ -89,6 +91,7 @@ export class TransactionManager { public readonly transactionRepository: TransactionRepository public readonly transactionCollector: TransactionCollector public readonly transactionSubmitter: TransactionSubmitter + public readonly hookManager: HookManager public readonly id: string public readonly eip1559: EIP1559Parameters @@ -122,6 +125,7 @@ export class TransactionManager { this.transactionRepository = new TransactionRepository(this) this.transactionCollector = new TransactionCollector(this) this.transactionSubmitter = new TransactionSubmitter(this) + this.hookManager = new HookManager() this.id = _config.id this.eip1559 = _config.eip1559 || opStackDefaultEIP1559Parameters @@ -134,10 +138,29 @@ export class TransactionManager { this.blockTime = _config.blockTime || 2n } + /** + * Adds a collector to the transaction manager. + * A collector is a function that returns a list of transactions to be sent in the next block. + * It is important that the collector function is as fast as possible to avoid delays when sending transactions to the blockchain + * @param collector - The collector to add. + */ public addTransactionCollector(collector: TransactionOriginator): void { this.collectors.push(collector) } + /** + * Adds a hook to the hook manager. + * @param type - The type of hook to add. + * @param handler - The handler function to add. + */ + public async addHook(handler: TxmHookHandler, type: TxmHookType): Promise { + await this.hookManager.addHook(handler, type) + } + + public async getTransaction(txIntentId: UUID): Promise { + return this.transactionRepository.getTransaction(txIntentId) + } + public async start(): Promise { // Start the gas price oracle to prevent other parts of the application from calling `suggestGasForNextBlock` before the gas price oracle has initialized the gas price after processing the first block const priceOraclePromise = this.gasPriceOracle.start() diff --git a/packages/transaction-manager/lib/TransactionRepository.ts b/packages/transaction-manager/lib/TransactionRepository.ts index 1c6b9e94fe..2301115c4a 100644 --- a/packages/transaction-manager/lib/TransactionRepository.ts +++ b/packages/transaction-manager/lib/TransactionRepository.ts @@ -1,4 +1,5 @@ import { unknownToError } from "@happychain/common" +import type { UUID } from "@happychain/common" import { type Result, ResultAsync } from "neverthrow" import { NotFinalizedStatuses, Transaction } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" @@ -27,6 +28,22 @@ export class TransactionRepository { return [...this.notFinalizedTransactions] } + async getTransaction(intentId: UUID): Promise { + const cachedTransaction = this.notFinalizedTransactions.find((t) => t.intentId === intentId) + + if (cachedTransaction) { + return cachedTransaction + } + + const persistedTransaction = await db + .selectFrom("transaction") + .where("intentId", "=", intentId) + .selectAll() + .executeTakeFirst() + + return persistedTransaction ? Transaction.fromDbRow(persistedTransaction) : undefined + } + async saveTransactions(transactions: Transaction[]): Promise> { const result = await ResultAsync.fromPromise( db diff --git a/packages/transaction-manager/lib/index.ts b/packages/transaction-manager/lib/index.ts index 9620c954da..48451d3dc9 100644 --- a/packages/transaction-manager/lib/index.ts +++ b/packages/transaction-manager/lib/index.ts @@ -1,4 +1,5 @@ -export { Transaction } from "./Transaction.js" +export { Transaction, TransactionStatus } from "./Transaction.js" export { TransactionManager } from "./TransactionManager.js" export { GasEstimator, EstimateGasErrorCause } from "./GasEstimator.js" export type { LatestBlock } from "./BlockMonitor.js" +export { TxmHookType, type TxmHookHandler } from "./HookManager.js"