import ethers, { PopulatedTransaction, providers, utils, Wallet } from 'ethers'; import { TransactionExecutedEvent, TransactionManagerEventEmitter, TransactionMaxRetriesReachedEvent, TransactionQueuedEvent, TransactionTimedOutEvent, } from './events'; import { TransactionManagerRequest } from './transaction-inputs/transaction-manager-request'; import { TransactionManagerResponse } from './transaction-inputs/transaction-manager-response'; import { TransactionQueueHandler } from './transaction-queue'; import { AlchemyApiConfig, TenderlyApiConfig, TransactionManagerQueueItem } from './types'; import { keccak256 } from 'ethers/lib/utils'; export class TransactionManager { private nonce: number; private provider: providers.Provider; private maxLiveTransactions: number; private maxRetries: number; private transactionQueueHandler: TransactionQueueHandler; private txTimeoutInSeconds = 100000; private intervalBetweenTransactions = 500; private wallet: Wallet; private gasMultiplier: number; private alchemyConfig: AlchemyApiConfig; constructor( wallet: Wallet, maxLiveTransactions: number, maxRetries: number, txTimeoutInSeconds: number, tenderlyApiConfig: TenderlyApiConfig, alchemyConfig: AlchemyApiConfig, gasMultiplier?: number, ) { this.nonce = 0; this.provider = wallet.provider; this.maxLiveTransactions = maxLiveTransactions; this.maxRetries = maxRetries; this.wallet = wallet; if (gasMultiplier) { this.gasMultiplier = gasMultiplier; } else { this.gasMultiplier = 1; } this.txTimeoutInSeconds = txTimeoutInSeconds; this.alchemyConfig = alchemyConfig; this.transactionQueueHandler = new TransactionQueueHandler( this.provider, this.wallet, this.txTimeoutInSeconds, this.gasMultiplier, tenderlyApiConfig, this.alchemyConfig, ); } public async initialize(chainId: number): Promise { await this.transactionQueueHandler.initialize(chainId); } // create transaction id public createTransactionId(transactionRequest: PopulatedTransaction): string { const { to, data, chainId } = transactionRequest; const transactionHash = keccak256( utils.solidityPack(['address', 'bytes', 'uint256'], [to, data, chainId]), ); return transactionHash; } // listen to queued event public async onTransactionQueued( event: TransactionQueuedEvent, ): Promise { if (event.isLiveQueue) { await this.processLiveQueue(); } else { if ( this.transactionQueueHandler.getPendingQueueLength() > 0 && !this.transactionQueueHandler.isTransactionRunning() ) { await this.processPendingQueue(); } } } // on transaction execution completed trigger transaction from queue public async onTransactionExecuted(): Promise { process.env.DEBUG == 'true' && console.log('Transaction Executed'); if ( this.transactionQueueHandler.getPendingQueueLength() > 0 && !this.transactionQueueHandler.isTransactionRunning() ) { await this.processPendingQueue(); } } // entrypoint for transaction manager to queue transaction public async queueTransaction( wrappedTransaction: TransactionManagerRequest, priority: number, ): Promise { // Get the raw transaction details const transaction: PopulatedTransaction = await wrappedTransaction.getTransaction(); // Create an event emitter to handle transaction events const transactionEvents: TransactionManagerEventEmitter = new TransactionManagerEventEmitter(); // generate random id in string for transaction const id = this.createTransactionId(transaction); // Create a TransactionManagerResponse to handle transaction responses const transactionManagerResponse: TransactionManagerResponse = new TransactionManagerResponse( transaction, transactionEvents, id, wrappedTransaction, this.transactionQueueHandler.getGasOptimizer(), ); // Create a queue item with the transaction, response, and priority const queueItem: TransactionManagerQueueItem = { transaction: transaction, transactionManagerResponse, priority: priority, retries: 0, id, }; // read to queued and completed event transactionManagerResponse.events.on( 'transactionQueued', async (event: TransactionQueuedEvent) => { await this.onTransactionQueued(event); }, ); // register executed event as well transactionManagerResponse.events.on( 'transactionExecuted', async (event: TransactionExecutedEvent) => { process.env.DEBUG == 'true' && console.log( 'Transaction Executed', event.transactionReceipt?.transactionHash, ); await this.onTransactionExecuted(); }, ); // Process the transaction return this.processTransaction(queueItem); } /** * Processes the transaction by adding it to the live queue * or the pending queue depending on the number of live transactions * currently being processed. * * @param transactionQueueItem the transaction queue item to process * @returns the transaction queue item's transaction manager response */ private async processTransaction( transactionQueueItem: TransactionManagerQueueItem, ): Promise { // get singed transaction from TransactionRequest const transaction = transactionQueueItem.transaction; // get the nonce from the provider const nonce = await this.provider.getTransactionCount( transaction.from || '', ); // if the nonce is greater than the current nonce, update the current nonce if (nonce > this.nonce) { this.nonce = nonce; } // set the current transaction's nonce to the current nonce transaction.nonce = this.nonce; // increment the current nonce this.nonce++; // Check if max live transactions is reached if ( this.transactionQueueHandler.getLiveQueueLength() >= this.maxLiveTransactions ) { // Add the transaction to the pending queue // trigger transaction Queued event this.transactionQueueHandler.addToPendingQueue(transactionQueueItem); // Sort the pending queue by descending priority this.transactionQueueHandler.sortPendingQueueByPriority(); } else { // Add transaction to live queue but execution as well as there is a possibility that the transaction can ran parallely this.transactionQueueHandler.addToLiveQueue(transactionQueueItem); } // return transactionResponse; return transactionQueueItem.transactionManagerResponse; } private triggerTransactionTimeoutEvent( emitter: TransactionManagerEventEmitter, transaction: PopulatedTransaction, transactionId: string, ) { // trigger transaction timed out event const timedOutEvent: TransactionTimedOutEvent = { transactionRequest: transaction, transactionId, }; emitter.transactionTimedOut(timedOutEvent); } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } private async processLiveQueue(): Promise { // Check if max retries is reached const pendingTransaction = this.transactionQueueHandler.shiftLiveQueue(); if (pendingTransaction) { process.env.DEBUG == 'true' && console.log( `Waiting for transaction to be started for ${this.intervalBetweenTransactions} ms`, ); await this.delay(this.intervalBetweenTransactions); try { const response = await this.transactionQueueHandler.processLiveQueueItem( pendingTransaction, ); return response; } catch (e) { // add timeout transaction to pending queue this.transactionQueueHandler.addToPendingQueue(pendingTransaction); // Sort the pending queue by descending priority this.transactionQueueHandler.sortPendingQueueByPriority(); } } return; } /** * Process the queue */ private async processPendingQueue(): Promise { // Check if max retries is reached const pendingTransaction = this.transactionQueueHandler.shiftPendingQueue(); if (pendingTransaction) { // wait for transaction to be started process.env.DEBUG == 'true' && console.log( `Waiting for transaction to be started for ${this.intervalBetweenTransactions} ms`, ); await this.delay(this.intervalBetweenTransactions); const events = pendingTransaction.transactionManagerResponse.events; if (pendingTransaction.retries >= this.maxRetries) { // trigger transaction max retry reached event const maxRetryReached: TransactionMaxRetriesReachedEvent = { transactionRequest: pendingTransaction.transaction, retryCount: pendingTransaction.retries, transactionId: pendingTransaction.id, }; events.transactionMaxRetriesReached(maxRetryReached); return; } // Retry transaction pendingTransaction.retries++; this.transactionQueueHandler.sortPendingQueueByPriority(); try { // Process the pending transaction const transactionResponse = await this.transactionQueueHandler.processPendingQueueItem( pendingTransaction, ); // Return the transaction response return transactionResponse; } catch (e) { console.log(`Transaction timed out: ${e}`); // trigger transaction timed out event this.triggerTransactionTimeoutEvent( events, pendingTransaction.transaction, pendingTransaction.id, ); // add timeout transaction to pending queue this.transactionQueueHandler.addToPendingQueue(pendingTransaction); // Sort the pending queue by descending priority this.transactionQueueHandler.sortPendingQueueByPriority(); } } else { return; } } // implement wait for transaction manager to process all transactions }