import { ethers } from 'ethers'; import { TransactionFailedEvent, TransactionGasError } from '../events'; import { TransactionGasOptimizer } from '../optimizer/gas-optimizer'; import { TransactionQueueHandler } from '../transaction-queue'; import { TransactionManagerResponse } from '../transaction-inputs/transaction-manager-response'; import { TransactionError, TransactionManagerQueueItem, TransactionState, } from '../types'; import { ErrorCode, NESTED_ERROR_CODES } from './constants'; import { HandleNestedErrors } from './handle-nested-error'; import { HandleTopLevelErrors } from './handle-top-level-errors'; import { ErrorParser } from './parser'; // class to handle transaction errors export class TransactionErrorHandler { private queueHandler: TransactionQueueHandler; private gasOptimizer: TransactionGasOptimizer; private errorParser: ErrorParser; private provider: ethers.providers.Provider; private topLevelErrorHandler: HandleTopLevelErrors; private nestedErrorHandler: HandleNestedErrors; constructor( queueHandler: TransactionQueueHandler, gasOptimizer: TransactionGasOptimizer, provider: ethers.providers.Provider, ) { this.queueHandler = queueHandler; this.gasOptimizer = gasOptimizer; this.errorParser = new ErrorParser(); this.provider = provider; this.topLevelErrorHandler = new HandleTopLevelErrors(this); this.nestedErrorHandler = new HandleNestedErrors(this); } private parseTransactionError(error: any): TransactionError { return this.errorParser.parse(error); } private isDroppedAndReplaced(e: any): boolean { return ( e?.code === ErrorCode.TRANSACTION_REPLACED && e?.replacement && (e?.reason === 'repriced' || e?.cancelled === false) ); } // Handling error public async handleError( error: any, transactionQueueItem: TransactionManagerQueueItem, ): Promise { try { const transactionError: TransactionError = this.parseTransactionError( error, ); let transactionResponse: | TransactionManagerResponse | undefined = await this.topLevelErrorHandler.handleTopLevelErrors( transactionQueueItem, transactionError, error, ); if (transactionResponse) { return transactionResponse; } // check if transaction is require transaction if ( transactionError.errorCodeNested == NESTED_ERROR_CODES.REQUIRE_TRANSACTION && error.error && error.data && error.data.message ) { // transaction failure execution being reverted return await this.handleTransactionFailure( transactionQueueItem, transactionError, ); } // check if transaction is already mined transactionResponse = await this.nestedErrorHandler.handleNestedErrors( transactionQueueItem, transactionError, ); if (transactionResponse) { return transactionResponse; } // check if transaction is out of gas if (error.transaction && error.receipt) { const transactionGasLimit = error.transaction.gasLimit; const receiptGasUsed = error.receipt.gasUsed; if (receiptGasUsed.gte(transactionGasLimit)) { return await this.handleGasTransactionError( transactionQueueItem, transactionError, ); } } if (transactionError.errorCodeTopLevel == 'TIMEOUT') { return await this.handleTimeoutError( transactionQueueItem, transactionError, ); } // check if transaction is dropped and replaced if (this.isDroppedAndReplaced(error)) { return await this.handleDroppedAndReplacedTransaction( error, transactionError, transactionQueueItem, ); } else { // handle transaction error return await this.handleTransactionFailure( transactionQueueItem, transactionError, ); } } catch (e) { // print exception trace console.error(e); const events = transactionQueueItem.transactionManagerResponse.events; events.transactionFailed({ transactionRequest: transactionQueueItem.transaction, transactionId: transactionQueueItem.id, transactionError: { error: e, }, } as TransactionFailedEvent); return transactionQueueItem.transactionManagerResponse; } } public async handleTimeoutError( transactionQueueItem: TransactionManagerQueueItem, transactionError: TransactionError, ): Promise { const response = transactionQueueItem.transactionManagerResponse; const events = response.events; events.transactionTimedOut({ transactionRequest: transactionQueueItem.transaction, transactionId: transactionQueueItem.id, }); // Optimized transaction this.gasOptimizer.updateTimeoutGasMultiplier( transactionQueueItem.transactionManagerResponse.transactionRequest, transactionQueueItem.transaction, ); transactionQueueItem = await this.gasOptimizer.optimize( transactionQueueItem, transactionError, ); if (response.status !== 'completed' && response.status !== 'failed') { this.queueHandler.addToPendingQueue(transactionQueueItem); } return response; } // handle transaction gas failure public async handleGasTransactionError( transactionQueueItem: TransactionManagerQueueItem, transactionError: TransactionError, ): Promise { const response = transactionQueueItem.transactionManagerResponse; const events = response.events; // handle transaction error events.transactionGasError({ transactionError, } as TransactionGasError); transactionQueueItem = await this.gasOptimizer.optimize( transactionQueueItem, transactionError, ); // check queued item is not set as failed if (response.status.indexOf('failed') > -1) { return response; } if (response.status !== 'completed' && response.status !== 'failed') { this.queueHandler.addToPendingQueue(transactionQueueItem); } return response; } // handle transaction failure public async handleTransactionFailure( transactionQueueItem: TransactionManagerQueueItem, transactionError: TransactionError, ): Promise { const transaction = transactionQueueItem.transaction; const response = transactionQueueItem.transactionManagerResponse; const events = response.events; events.transactionFailed({ transactionRequest: transaction, transactionError: transactionError, transactionId: transactionQueueItem.id, } as TransactionFailedEvent); return response; } // handle nonce expired transaction public async handleNonceExpiredTransaction( transactionQueueItem: TransactionManagerQueueItem, transactionError: TransactionError, ): Promise { const transaction = transactionQueueItem.transaction; // update transaction nonce transaction.nonce = transactionError.errorHash?.nonce; // resubmit transaction this.queueHandler.addToPendingQueue(transactionQueueItem); return transactionQueueItem.transactionManagerResponse; } // handle dropped and replaced transactions public async handleDroppedAndReplacedTransaction( error: any, transactionError: TransactionError, transactionQueueItem: TransactionManagerQueueItem, ): Promise { const response = transactionQueueItem.transactionManagerResponse; const events = response.events; const transaction = transactionQueueItem.transaction; const status: TransactionState = error.receipt.status === 0 ? 'Fail' : 'Success'; if (status == 'Success') { // send transaction executed status events.transactionExecuted({ transactionRequest: transaction, transactionReceipt: error.receipt, transactionId: transactionQueueItem.id, }); return transactionQueueItem.transactionManagerResponse; } else { // handle transaction error return await this.handleTransactionFailure( transactionQueueItem, transactionError, ); } } }