diff --git a/apps/randomness/src/CustomGasEstimator.ts b/apps/randomness/src/CustomGasEstimator.ts index f88a4fb24b..cecdac0524 100644 --- a/apps/randomness/src/CustomGasEstimator.ts +++ b/apps/randomness/src/CustomGasEstimator.ts @@ -1,6 +1,6 @@ import { DefaultGasLimitEstimator, - type EstimateGasErrorCause, + type EstimateGasError, type Transaction, type TransactionManager, } from "@happy.tech/txm" @@ -10,7 +10,7 @@ export class CustomGasEstimator extends DefaultGasLimitEstimator { override async estimateGas( transactionManager: TransactionManager, transaction: Transaction, - ): Promise> { + ): Promise> { // 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. diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index 8350e95489..a91e1b31fa 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -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 diff --git a/packages/txm/lib/EventBus.ts b/packages/txm/lib/EventBus.ts index 3b411e7c07..058bb2c4ae 100644 --- a/packages/txm/lib/EventBus.ts +++ b/packages/txm/lib/EventBus.ts @@ -4,6 +4,7 @@ export enum Topics { NewBlock = "NewBlock", TransactionStatusChanged = "TransactionStatusChanged", TransactionSaveFailed = "TransactionSaveFailed", + TransactionSubmissionFailed = "TransactionSubmissionFailed", } export type EventBus = EventEmitter diff --git a/packages/txm/lib/GasEstimator.ts b/packages/txm/lib/GasEstimator.ts index 9a77464e99..d6fb3c7163 100644 --- a/packages/txm/lib/GasEstimator.ts +++ b/packages/txm/lib/GasEstimator.ts @@ -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 } /** @@ -33,7 +38,7 @@ export interface GasEstimator { estimateGas( transactionManager: TransactionManager, transaction: Transaction, - ): Promise> + ): Promise> } /** @@ -47,7 +52,7 @@ export class DefaultGasLimitEstimator implements GasEstimator { public async estimateGas( transactionManager: TransactionManager, transaction: Transaction, - ): Promise> { + ): Promise> { return this.simulateTransactionForGas(transactionManager, transaction) } @@ -59,11 +64,14 @@ export class DefaultGasLimitEstimator implements GasEstimator { protected async simulateTransactionForGas( transactionManager: TransactionManager, transaction: Transaction, - ): Promise> { + ): Promise> { 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 @@ -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) diff --git a/packages/txm/lib/HookManager.ts b/packages/txm/lib/HookManager.ts index 720faf982e..f191b94b70 100644 --- a/packages/txm/lib/HookManager.ts +++ b/packages/txm/lib/HookManager.ts @@ -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 = { @@ -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 = TxmHooksRecord[T][number] @@ -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(type: T, handler: TxmHookHandler): Promise { @@ -72,7 +94,7 @@ export class HookManager { this.hooks[TxmHookType.All].forEach((handler) => handler({ type: TxmHookType.TransactionStatusChanged, - payload: payload.transaction, + transaction: payload.transaction, }), ) } @@ -85,7 +107,7 @@ export class HookManager { this.hooks[TxmHookType.All].forEach((handler) => handler({ type: TxmHookType.TransactionSaveFailed, - payload: payload.transaction, + transaction: payload.transaction, }), ) } @@ -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 { + 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, }), ) } diff --git a/packages/txm/lib/TransactionCollector.ts b/packages/txm/lib/TransactionCollector.ts index e44815d887..703f7fa32d 100644 --- a/packages/txm/lib/TransactionCollector.ts +++ b/packages/txm/lib/TransactionCollector.ts @@ -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, + }) this.txmgr.nonceManager.returnNonce(nonce) } }), diff --git a/packages/txm/lib/TransactionSubmitter.ts b/packages/txm/lib/TransactionSubmitter.ts index 8974121d96..1c196d80a9 100644 --- a/packages/txm/lib/TransactionSubmitter.ts +++ b/packages/txm/lib/TransactionSubmitter.ts @@ -22,6 +22,7 @@ export enum AttemptSubmissionErrorCause { export type AttemptSubmissionError = { cause: AttemptSubmissionErrorCause | EstimateGasErrorCause + description: string flushed: boolean } @@ -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 @@ -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 @@ -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 @@ -123,7 +136,11 @@ 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({ @@ -131,7 +148,11 @@ export class TransactionSubmitter { }) 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) diff --git a/packages/txm/lib/index.ts b/packages/txm/lib/index.ts index 8bb89e41dd..1211ec2110 100644 --- a/packages/txm/lib/index.ts +++ b/packages/txm/lib/index.ts @@ -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"