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
4 changes: 2 additions & 2 deletions apps/randomness/src/CustomGasEstimator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
DefaultGasLimitEstimator,
type EstimateGasErrorCause,
type EstimateGasError,
type Transaction,
type TransactionManager,
} from "@happy.tech/txm"
Expand All @@ -10,7 +10,7 @@ export class CustomGasEstimator extends DefaultGasLimitEstimator {
override async estimateGas(
transactionManager: TransactionManager,
transaction: Transaction,
): Promise<Result<bigint, EstimateGasErrorCause>> {
): Promise<Result<bigint, EstimateGasError>> {
// These values are based on benchmarks from Anvil.
// An extra margin is added to prevent errors in the randomness service due to minor contract changes.

Expand Down
3 changes: 3 additions & 0 deletions apps/randomness/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class RandomnessService {
this.txm.addTransactionOriginator(this.onCollectTransactions.bind(this))
this.txm.addHook(TxmHookType.TransactionStatusChanged, this.onTransactionStatusChange.bind(this))
this.txm.addHook(TxmHookType.NewBlock, this.onNewBlock.bind(this))
this.txm.addHook(TxmHookType.TransactionSubmissionFailed, (_, description) => {
console.error(description)
})

// Synchronize the retrieval of new Drand beacons with the Drand network to request them as soon as they become available.
const periodMs = Number(env.EVM_DRAND_PERIOD_SECONDS) * MS_IN_SECOND
Expand Down
1 change: 1 addition & 0 deletions packages/txm/lib/EventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum Topics {
NewBlock = "NewBlock",
TransactionStatusChanged = "TransactionStatusChanged",
TransactionSaveFailed = "TransactionSaveFailed",
TransactionSubmissionFailed = "TransactionSubmissionFailed",
}

export type EventBus = EventEmitter<Topics>
Expand Down
25 changes: 18 additions & 7 deletions packages/txm/lib/GasEstimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import type { Transaction } from "./Transaction.js"
import type { TransactionManager } from "./TransactionManager.js"

export enum EstimateGasErrorCause {
ABINotFound = "ABINotFound",
ClientError = "ClientError",
EstimateGasABINotFound = "EstimateGasABINotFound",
EstimateGasClientError = "EstimateGasClientError",
}

export interface EstimateGasError {
cause: EstimateGasErrorCause
description: string
}

/**
Expand Down Expand Up @@ -33,7 +38,7 @@ export interface GasEstimator {
estimateGas(
transactionManager: TransactionManager,
transaction: Transaction,
): Promise<Result<bigint, EstimateGasErrorCause>>
): Promise<Result<bigint, EstimateGasError>>
}

/**
Expand All @@ -47,7 +52,7 @@ export class DefaultGasLimitEstimator implements GasEstimator {
public async estimateGas(
transactionManager: TransactionManager,
transaction: Transaction,
): Promise<Result<bigint, EstimateGasErrorCause>> {
): Promise<Result<bigint, EstimateGasError>> {
return this.simulateTransactionForGas(transactionManager, transaction)
}

Expand All @@ -59,11 +64,14 @@ export class DefaultGasLimitEstimator implements GasEstimator {
protected async simulateTransactionForGas(
transactionManager: TransactionManager,
transaction: Transaction,
): Promise<Result<bigint, EstimateGasErrorCause>> {
): Promise<Result<bigint, EstimateGasError>> {
const abi = transactionManager.abiManager.get(transaction.contractName)

if (!abi) {
return err(EstimateGasErrorCause.ABINotFound)
return err({
cause: EstimateGasErrorCause.EstimateGasABINotFound,
description: `ABI not found for contract ${transaction.contractName}`,
})
}

const functionName = transaction.functionName
Expand All @@ -78,7 +86,10 @@ export class DefaultGasLimitEstimator implements GasEstimator {
})

if (gasResult.isErr()) {
return err(EstimateGasErrorCause.ClientError)
return err({
cause: EstimateGasErrorCause.EstimateGasClientError,
description: `Failed to estimate gas for transaction ${transaction.intentId}. Details: ${gasResult.error}`,
})
}

return ok(gasResult.value)
Expand Down
49 changes: 45 additions & 4 deletions packages/txm/lib/HookManager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { LatestBlock } from "./BlockMonitor"
import { Topics, eventBus } from "./EventBus.js"
import type { Transaction } from "./Transaction.js"
import type { AttemptSubmissionErrorCause } from "./TransactionSubmitter"

export enum TxmHookType {
All = "All",
TransactionStatusChanged = "TransactionStatusChanged",
TransactionSaveFailed = "TransactionSaveFailed",
NewBlock = "NewBlock",
TransactionSubmissionFailed = "TransactionSubmissionFailed",
}

export type TxmTransactionStatusChangedHookPayload = {
Expand All @@ -24,11 +26,29 @@ export type TxmTransactionSaveFailedHookPayload = {
transaction: Transaction
}

export type TxmTransactionSubmissionFailedHookPayload = {
type: TxmHookType.TransactionSubmissionFailed
transaction: Transaction
description: string
cause: AttemptSubmissionErrorCause
}

export type TxmHookPayload =
| TxmTransactionStatusChangedHookPayload
| TxmNewBlockHookPayload
| TxmTransactionSaveFailedHookPayload
| TxmTransactionSubmissionFailedHookPayload

export type TxmHooksRecord = {
[TxmHookType.All]: ((event: { payload: LatestBlock | Transaction; type: TxmHookType }) => void)[]
[TxmHookType.All]: ((event: TxmHookPayload) => void)[]
[TxmHookType.TransactionStatusChanged]: ((transaction: Transaction) => void)[]
[TxmHookType.TransactionSaveFailed]: ((transaction: Transaction) => void)[]
[TxmHookType.NewBlock]: ((block: LatestBlock) => void)[]
[TxmHookType.TransactionSubmissionFailed]: ((
transaction: Transaction,
description: string,
cause: AttemptSubmissionErrorCause,
) => void)[]
}

export type TxmHookHandler<T extends TxmHookType = TxmHookType.All> = TxmHooksRecord[T][number]
Expand All @@ -53,10 +73,12 @@ export class HookManager {
[TxmHookType.TransactionStatusChanged]: [],
[TxmHookType.TransactionSaveFailed]: [],
[TxmHookType.NewBlock]: [],
[TxmHookType.TransactionSubmissionFailed]: [],
}
eventBus.on(Topics.TransactionStatusChanged, this.onTransactionStatusChanged.bind(this))
eventBus.on(Topics.TransactionSaveFailed, this.onTransactionSaveFailed.bind(this))
eventBus.on(Topics.NewBlock, this.onNewBlock.bind(this))
eventBus.on(Topics.TransactionSubmissionFailed, this.onTransactionSubmissionFailed.bind(this))
}

public async addHook<T extends TxmHookType>(type: T, handler: TxmHookHandler<T>): Promise<void> {
Expand All @@ -72,7 +94,7 @@ export class HookManager {
this.hooks[TxmHookType.All].forEach((handler) =>
handler({
type: TxmHookType.TransactionStatusChanged,
payload: payload.transaction,
transaction: payload.transaction,
}),
)
}
Expand All @@ -85,7 +107,7 @@ export class HookManager {
this.hooks[TxmHookType.All].forEach((handler) =>
handler({
type: TxmHookType.TransactionSaveFailed,
payload: payload.transaction,
transaction: payload.transaction,
}),
)
}
Expand All @@ -96,7 +118,26 @@ export class HookManager {
this.hooks[TxmHookType.All].forEach((handler) =>
handler({
type: TxmHookType.NewBlock,
payload: block,
block,
}),
)
}

private async onTransactionSubmissionFailed(payload: {
transaction: Transaction
description: string
cause: AttemptSubmissionErrorCause
}): Promise<void> {
this.hooks[TxmHookType.TransactionSubmissionFailed].forEach((handler) =>
handler(payload.transaction, payload.description, payload.cause),
)

this.hooks[TxmHookType.All].forEach((handler) =>
handler({
type: TxmHookType.TransactionSubmissionFailed,
transaction: payload.transaction,
description: payload.description,
cause: payload.cause,
}),
)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/txm/lib/TransactionCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export class TransactionCollector {
})

if (submissionResult.isErr() && !submissionResult.error.flushed) {
eventBus.emit(Topics.TransactionSubmissionFailed, {
transaction,
description: submissionResult.error.description,
cause: submissionResult.error.cause,
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good idea to also pass the cause from the AttemptSubmissionErrorCause enum (and export that publicly) so that might enable finer-grained reaction to this without needing to parse the description.

this.txmgr.nonceManager.returnNonce(nonce)
}
}),
Expand Down
31 changes: 26 additions & 5 deletions packages/txm/lib/TransactionSubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum AttemptSubmissionErrorCause {

export type AttemptSubmissionError = {
cause: AttemptSubmissionErrorCause | EstimateGasErrorCause
description: string
flushed: boolean
}

Expand Down Expand Up @@ -72,7 +73,11 @@ export class TransactionSubmitter {

if (!abi) {
console.error(`ABI not found for contract ${transaction.contractName}`)
return err({ cause: AttemptSubmissionErrorCause.ABINotFound, flushed: false })
return err({
cause: AttemptSubmissionErrorCause.ABINotFound,
description: `ABI not found for contract ${transaction.contractName}`,
flushed: false,
})
}

const functionName = transaction.functionName
Expand All @@ -82,7 +87,11 @@ export class TransactionSubmitter {
const gasResult = await this.txmgr.gasEstimator.estimateGas(this.txmgr, transaction)

if (gasResult.isErr()) {
return err({ cause: gasResult.error, flushed: false })
return err({
cause: gasResult.error.cause,
description: gasResult.error.description,
flushed: false,
})
}

const gas = gasResult.value
Expand All @@ -103,7 +112,11 @@ export class TransactionSubmitter {
const signedTransactionResult = await this.txmgr.viemWallet.safeSignTransaction(transactionRequest)

if (signedTransactionResult.isErr()) {
return err({ cause: AttemptSubmissionErrorCause.FailedToSignTransaction, flushed: false })
return err({
cause: AttemptSubmissionErrorCause.FailedToSignTransaction,
description: `Failed to sign transaction ${transaction.intentId}. Details: ${signedTransactionResult.error}`,
flushed: false,
})
}

const signedTransaction = signedTransactionResult.value
Expand All @@ -123,15 +136,23 @@ export class TransactionSubmitter {

if (updateResult.isErr()) {
transaction.removeAttempt(hash)
return err({ cause: AttemptSubmissionErrorCause.FailedToUpdate, flushed: false })
return err({
cause: AttemptSubmissionErrorCause.FailedToUpdate,
description: `Failed to update transaction ${transaction.intentId}. Details: ${updateResult.error}`,
flushed: false,
})
}

const sendRawTransactionResult = await this.txmgr.viemWallet.safeSendRawTransaction({
serializedTransaction: signedTransaction,
})

if (sendRawTransactionResult.isErr()) {
return err({ cause: AttemptSubmissionErrorCause.FailedToSendRawTransaction, flushed: true })
return err({
cause: AttemptSubmissionErrorCause.FailedToSendRawTransaction,
description: `Failed to send raw transaction ${transaction.intentId}. Details: ${sendRawTransactionResult.error}`,
flushed: true,
})
}

return ok(undefined)
Expand Down
7 changes: 6 additions & 1 deletion packages/txm/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export { Transaction, TransactionStatus, type TransactionConstructorConfig } from "./Transaction.js"
export { TransactionManager, type TransactionManagerConfig, type TransactionOriginator } from "./TransactionManager.js"
export type { Abi } from "viem"
export { DefaultGasLimitEstimator, EstimateGasErrorCause, type GasEstimator } from "./GasEstimator.js"
export {
DefaultGasLimitEstimator,
type GasEstimator,
type EstimateGasError,
} from "./GasEstimator.js"
export type { LatestBlock } from "./BlockMonitor.js"
export { TxmHookType, type TxmHookHandler } from "./HookManager.js"
export type { AttemptSubmissionErrorCause } from "./TransactionSubmitter.js"