import { TransactionReceipt, TransactionResponse, } from '@ethersproject/abstract-provider'; import { BigNumber, PopulatedTransaction } from 'ethers'; import { TransactionManagerEventEmitter, TransactionConfirmedEvent, TransactionFailedEvent, TransactionMaxRetriesReachedEvent, TransactionQueuedEvent, TransactionRetriedEvent, TransactionExecutedEvent, TransactionGasError, TransactionStarted, TransactionCompletedEvent, } from '../events'; import { once } from 'events'; import { TransactionError, TransactionManagerState, TransactionManagerTxResponse, } from '../types'; import { TransactionManagerRequest } from './transaction-manager-request'; import { TransactionGasOptimizer } from '../optimizer'; export class TransactionManagerResponse { private transaction: PopulatedTransaction; private transactionResponse: TransactionResponse; private transactionReceipt: TransactionReceipt | undefined; public transactionRequest: TransactionManagerRequest; public events: TransactionManagerEventEmitter; private retryCount = 0; private transactionErrors: TransactionError; public status: TransactionManagerState; public transactionId: string; private gasMultiplier: number; private simulatedGasLimit: BigNumber; constructor( transaction: PopulatedTransaction, events: TransactionManagerEventEmitter, transactionId: string, transactionRequest: TransactionManagerRequest, gasOptimizer: TransactionGasOptimizer, ) { this.transaction = transaction; // store the transaction request this.transactionRequest = transactionRequest; // store the events emitter this.events = events; // handling transactionQueue event this.events.on('transactionQueued', (event: TransactionQueuedEvent) => { process.env.DEBUG == 'true' && console.log( 'Transaction queued : ', event.transactionId, event.isLiveQueue ? ' Live queue' : ' Pending queue', ); this.transaction = event.transactionRequest; this.status = 'queued'; }); // handling transactionConfirmed event this.events.on( 'transactionConfirmed', (event: TransactionConfirmedEvent) => { process.env.DEBUG == 'true' && console.log( 'Transaction confirmed Id: ', event.transactionId, ' Transaction hash : ', event.transactionResponse.hash, ); this.transaction = event.transactionRequest; this.transactionResponse = event.transactionResponse; this.status = 'confirmed'; }, ); this.events.on('transactionStarted', (event: TransactionStarted) => { process.env.DEBUG == 'true' && console.log('Transaction started Id:', event.transactionId); this.transaction = event.transactionRequest; this.status = 'started'; }); // handling transactionFailed events this.events.on('transactionFailed', (event: TransactionFailedEvent) => { process.env.DEBUG == 'true' && console.log( `Transaction failed Id: ${event.transactionId} Error : Code ${event.transactionError.errorCodeTopLevel}-Nested code ${event.transactionError.errorCodeNested} Message ${event.transactionError.errorMessage} Complete error ${event.transactionError.error}}`, ); this.transaction = event.transactionRequest; this.transactionErrors = event.transactionError; this.status = 'failed'; this.events.emit('transactionCompleted', { transactionId: event.transactionId, }); }); // handling transactionRetried events this.events.on('transactionRetried', (event: TransactionRetriedEvent) => { process.env.DEBUG == 'true' && console.log( 'Transaction retried Id: ', event.transactionId, ' retry count :', event.retryCount, ); this.transaction = event.transactionRequest; this.status = 'retried'; this.retryCount = event.retryCount; }); // handling transactionMaxRetriesReached events this.events.on( 'transactionMaxRetriesReached', (event: TransactionMaxRetriesReachedEvent) => { process.env.DEBUG == 'true' && console.log( 'Transaction max retries reached', event.transactionId, ' retry count :', event.retryCount, ); this.transaction = event.transactionRequest; gasOptimizer.handleMaxRetryReached( this.transactionRequest, event.transactionRequest, ) this.status = 'maxRetriesReached'; this.retryCount = event.retryCount; this.events.emit('transactionCompleted', { transactionId: event.transactionId, }); }, ); // handling transactionExecuted events this.events.on('transactionExecuted', (event: TransactionExecutedEvent) => { process.env.DEBUG == 'true' && console.log( 'Transaction executed', event.transactionId, ' transaction hash :', event.transactionReceipt?.transactionHash, 'Block number :', event.transactionReceipt?.blockNumber, ); this.transaction = event.transactionRequest; // get tracking id if provided on transaction const trackingId = this.transactionRequest?.metaData?.trackingId; // set gas settings after transaction executions gasOptimizer.updateContractMethodGasUsed( event.transactionRequest?.to, this.transactionRequest.method, event.transactionReceipt?.gasUsed, event.transactionRequest.gasLimit, trackingId, this.transactionRequest.isTransactionFailed(event.transactionReceipt), ); const gasDetails = gasOptimizer.getGasDetailsFromTracker( event.transactionRequest?.to, this.transactionRequest.method, this.transactionRequest?.metaData?.trackingId, ); if (gasDetails) { this.simulatedGasLimit = gasDetails.simulatedGasLimit || BigNumber.from(0); this.gasMultiplier = gasDetails.gasMultiplier || 0; } this.transactionReceipt = event.transactionReceipt; this.status = 'executed'; this.events.emit('transactionCompleted', { transactionId: event.transactionId, } as TransactionCompletedEvent); }); this.events.on('transactionGasError', (event: TransactionGasError) => { process.env.DEBUG == 'true' && console.log( `Transaction failed Id: ${event.transactionId} Error : Code ${event.transactionError.errorCodeTopLevel}-Nested code ${event.transactionError.errorCodeNested} Message ${event.transactionError.errorMessage}}`, ); this.transaction = event.transactionRequest; this.transactionErrors = event.transactionError; this.status = 'gasError'; }); // check transaction completed event this.events.on( 'transactionCompleted', (event: TransactionCompletedEvent) => { process.env.DEBUG == 'true' && console.log('Transaction completed', event.transactionId); this.status = 'completed'; }, ); this.transactionId = transactionId; } // wait for transaction to complete public async wait(timeout: number): Promise { await once(this.events, 'transactionCompleted'); let timer: NodeJS.Timeout | null = null; let interval: NodeJS.Timer | null = null; let receipt = this.status == 'completed'; const response = { transactionId: this.transactionId, transaction: this.transaction, transactionResponse: this.transactionResponse, transactionReceipt: this.transactionReceipt, retryCount: this.retryCount, transactionErrors: this.transactionErrors, status: this.status, gasMultiplier: this.gasMultiplier, simulatedGasLimit: this.simulatedGasLimit, }; return await new Promise((resolve) => { interval = setInterval(async () => { receipt = this.status == 'completed'; if (receipt) { if (timer) { clearTimeout(timer); } if (interval) { clearInterval(interval); } resolve(response); } }, 1000); timer = setTimeout(() => { if (interval) { clearInterval(interval); } if (timer) { clearTimeout(timer); } resolve(response); throw new Error(`Transaction timeout ${timeout} ms`); }, timeout); }); } }