Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions packages/randomness-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 7 additions & 2 deletions packages/randomness-service/src/CommitmentManager.ts
Original file line number Diff line number Diff line change
@@ -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<bigint, Commitment>()

generateCommitmentForTimestamp(timestamp: bigint): Commitment {
generateCommitment(): Omit<Commitment, "transactionIntentId"> {
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)
}
Expand Down
25 changes: 17 additions & 8 deletions packages/randomness-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -36,12 +36,12 @@ class RandomnessService {
this.txm.addTransactionCollector(this.onCollectTransactions.bind(this))
}

private onCollectTransactions(block: LatestBlock): Transaction[] {
private async onCollectTransactions(block: LatestBlock): Promise<Transaction[]> {
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,
Expand All @@ -50,14 +50,23 @@ class RandomnessService {

transactions.push(commitmentTransaction)

this.commitmentManager.setCommitmentForTimestamp(commitmentTimestamp, {
...commitment,
transactionIntentId: commitmentTransaction.intentId,
})
Comment thread
norswap marked this conversation as resolved.

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
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-manager/lib/EventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EventEmitter from "eventemitter3"

export enum Topics {
NewBlock = "NewBlock",
TransactionStatusChanged = "TransactionStatusChanged",
}

export type EventBus = EventEmitter<Topics>
Expand Down
44 changes: 44 additions & 0 deletions packages/transaction-manager/lib/HookManager.ts
Original file line number Diff line number Diff line change
@@ -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<TxmHookType, TxmHookHandler[]>

constructor() {
this.hooks = {
[TxmHookType.All]: [],
[TxmHookType.TransactionStatusChanged]: [],
}
eventBus.on(Topics.TransactionStatusChanged, this.onTransactionStatusChanged.bind(this))
}

public async addHook(handler: TxmHookHandler, type: TxmHookType): Promise<void> {
if (!this.hooks[type]) {
this.hooks[type] = []
}
this.hooks[type].push(handler)
}

private async onTransactionStatusChanged(payload: {
transaction: Transaction
}): Promise<void> {
this.hooks[TxmHookType.TransactionStatusChanged].concat(this.hooks[TxmHookType.All]).map((h) =>
h({
type: TxmHookType.TransactionStatusChanged,
transaction: payload.transaction,
}),
)
}
}
5 changes: 5 additions & 0 deletions packages/transaction-manager/lib/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -113,6 +114,10 @@ export class Transaction {

changeStatus(status: TransactionStatus): void {
this.status = status

eventBus.emit(Topics.TransactionStatusChanged, {
transaction: this,
})
}

get attemptCount(): number {
Expand Down
5 changes: 3 additions & 2 deletions packages/transaction-manager/lib/TransactionCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 24 additions & 1 deletion packages/transaction-manager/lib/TransactionManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type SafeViemPublicClient,
type SafeViemWalletClient,
type UUID,
convertToSafeViemPublicClient,
convertToSafeViemWalletClient,
} from "@happychain/common"
Expand All @@ -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"
Expand Down Expand Up @@ -74,7 +76,7 @@ export type TransactionManagerConfig = {
gasEstimator?: GasEstimator
}

export type TransactionOriginator = (block: LatestBlock) => Transaction[]
export type TransactionOriginator = (block: LatestBlock) => Promise<Transaction[]>

export class TransactionManager {
public readonly collectors: TransactionOriginator[]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Comment thread
norswap marked this conversation as resolved.
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<void> {
await this.hookManager.addHook(handler, type)
}

public async getTransaction(txIntentId: UUID): Promise<Transaction | undefined> {
return this.transactionRepository.getTransaction(txIntentId)
}

public async start(): Promise<void> {
// 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()
Expand Down
17 changes: 17 additions & 0 deletions packages/transaction-manager/lib/TransactionRepository.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -27,6 +28,22 @@ export class TransactionRepository {
return [...this.notFinalizedTransactions]
}

async getTransaction(intentId: UUID): Promise<Transaction | undefined> {
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<Result<void, Error>> {
const result = await ResultAsync.fromPromise(
db
Expand Down
3 changes: 2 additions & 1 deletion packages/transaction-manager/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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"