import { BigNumber, ethers, PopulatedTransaction, providers } from 'ethers'; import { CUSTOM_ERROR_CODES, WEI_MULTIPLIER } from '../error-handler/constants'; import { TransactionErrorHandler } from '../error-handler/error-handler'; import { TransactionManagerRequest } from '../transaction-inputs/transaction-manager-request'; import { AlchemyApiConfig, ContractMethodLastGasParams, GasOptimizationResult, TenderlyApiConfig, TransactionError, TransactionManagerQueueItem } from '../types'; import { GasOperations } from './gas-helper'; import { GasStation, GasStationParameters } from './gas-station'; import { GasTracker } from './gas-tracker'; // Class to handle gas optimization for transactions export class TransactionGasOptimizer { private provider: providers.Provider; private gasPrice: BigNumber; private gasOperations: GasOperations; private errorHandler: TransactionErrorHandler; private maxFeePerGas: BigNumber; private maxPriorityFeePerGas: BigNumber; private baseFeePerGas: BigNumber; private transactionGasLimitTracker: Map = new Map< string, BigNumber >(); private gasStation: GasStation; private gasParams: GasStationParameters; private chainId: number; private gasMultiplier: number; private gasTracker: GasTracker; private alchemyConfig: AlchemyApiConfig; // store contracts gas used and last gas limit used. It will help to keep gas factor constant constructor( provider: providers.Provider, gasMultiplier: number, tenderlyApiConfig: TenderlyApiConfig, alchemyConfig: AlchemyApiConfig ) { this.provider = provider; this.gasPrice = ethers.BigNumber.from(0); this.maxFeePerGas = ethers.BigNumber.from(0); this.maxPriorityFeePerGas = ethers.BigNumber.from(0); this.gasOperations = new GasOperations(this.provider, tenderlyApiConfig, alchemyConfig); this.gasStation = new GasStation(this.provider); this.gasMultiplier = gasMultiplier; this.gasTracker = new GasTracker(gasMultiplier); this.alchemyConfig = alchemyConfig; } public async initialize(chainId: number): Promise { this.chainId = chainId; await this.updateStationParams(chainId); await this.updateLatestType2GasLimits(); } private async updateStationParams(chainId: number) { const gasParams: GasStationParameters = await this.gasStation.fetchGasStationParameters( chainId, ); this.gasParams = gasParams; } public async handleMaxRetryReached( transactionRequest: TransactionManagerRequest, transaction: PopulatedTransaction ): Promise { this.gasTracker.handleMaxRetryReached(transactionRequest, transaction); } public updateTimeoutGasMultiplier( transactionRequest: TransactionManagerRequest, transaction: PopulatedTransaction, ): void { this.gasTracker.updateTimeoutGasMultiplier(transactionRequest, transaction); } // update contract method gas used and last gas limit used public updateContractMethodGasUsed( contractAddress?: string, methodName?: string, gasUsed?: BigNumber, gasLimit?: BigNumber, trackingId?: string, isTransactionFailed = false, ): void { console.log( '🚀 ~ file: gas-optimizer.ts:78 ~ TransactionGasOptimizer ~ isTransactionFailed:', isTransactionFailed, ); this.gasTracker.updateContractMethodGasUsed( contractAddress, methodName, gasUsed, gasLimit, trackingId, isTransactionFailed, ); } public getGasDetailsFromTracker( contractAddress?: string, methodName?: string, trackingId?: string, ): ContractMethodLastGasParams | undefined { // If the contract address and method name are defined, run the following code if (contractAddress && methodName) { // Set a key to uniquely identify the contract method return this.gasTracker.getGasDetailsFromTracker( contractAddress, methodName, trackingId, ); } return undefined; } private async updateLatestType2GasLimits() { const block: providers.Block = await this.provider.getBlock('latest'); const baseFeePerGas = block.baseFeePerGas; // update max fee per gas if (baseFeePerGas) { this.baseFeePerGas = baseFeePerGas; } // update max priority fee per gas // const feeData: FeeData = await this.provider.getFeeData(); // update max fee per gas process.env.DEBUG == 'true' && console.log( 'Max fee per gas from gas station (GWEI): ', this.gasParams.standard.maxFee, ); if (this.gasParams.standard.maxFee) { this.maxFeePerGas = BigNumber.from( Math.ceil(this.gasParams.standard.maxFee), ).mul(BigNumber.from(WEI_MULTIPLIER)); } process.env.DEBUG == 'true' && console.log( 'Max priority fees from gas station (GWEI): ', this.gasParams.standard.maxPriorityFee, ); // update max priority fee per gas if (this.gasParams.standard.maxPriorityFee) { this.maxPriorityFeePerGas = BigNumber.from( Math.ceil(this.gasParams.standard.maxPriorityFee), ).mul(BigNumber.from(WEI_MULTIPLIER)); } const maxFeePerGasAsCurrentBlock = this.maxPriorityFeePerGas.add( this.baseFeePerGas, ); process.env.DEBUG == 'true' && console.log( 'Max fee per gas as current block: (GWEI) ', maxFeePerGasAsCurrentBlock .div(BigNumber.from(WEI_MULTIPLIER)) .toNumber(), ); // check if max fee per gas is greater than current block max fee per gas if (this.maxFeePerGas.lt(maxFeePerGasAsCurrentBlock)) { this.maxFeePerGas = maxFeePerGasAsCurrentBlock; } process.env.DEBUG == 'true' && console.log( 'Final max fee per gas (GWEI) set: ', this.maxFeePerGas.div(BigNumber.from(WEI_MULTIPLIER)).toNumber(), ); // update gas price this.gasPrice = await this.provider.getGasPrice(); } // update transaction error handler public updateTransactionErrorHandler( errorHandler: TransactionErrorHandler, ): void { this.errorHandler = errorHandler; } // update gas price public async increaseGasPrice( transaction: PopulatedTransaction, ): Promise { // update current gas price this.gasPrice = await this.provider.getGasPrice(); // get updated gas price from gas operations const updatedGasPrice = await this.gasOperations.increaseGasPrice( this.gasPrice, ); // update gas price if updated gas price is greater than current gas price if (this.gasPrice.lt(updatedGasPrice)) { this.gasPrice = updatedGasPrice; } // get gas price transaction.gasPrice = this.gasPrice; return transaction; } // update max fee per gas public async increaseMaxFeePerGas( transaction: PopulatedTransaction, ): Promise { // update current max fee per gas // if base fee per gas is greater than max fee per gas if (this.baseFeePerGas && this.baseFeePerGas.gt(this.maxFeePerGas)) { // set max fee per gas to base fee per gas process.env.DEBUG == 'true' && console.log( `Multiplier: ${this.baseFeePerGas.mul( 2, )} 2x Base Fee Per Gas from previous block: ${this.baseFeePerGas}`, ); this.maxFeePerGas = this.baseFeePerGas.mul(2); transaction.maxFeePerGas = this.maxFeePerGas; } return transaction; } // update gas limit public async optimizeGasLimit( transaction: PopulatedTransaction, incrementGasLimit: boolean, transactionRequest: TransactionManagerRequest, gasMultiplier: number, ): Promise { const optimizationResult: GasOptimizationResult = await this.gasOperations.increaseGasLimit( transaction, incrementGasLimit, transactionRequest, gasMultiplier, this.gasMultiplier ); return optimizationResult; } private async optimizeGasParams(transaction: PopulatedTransaction) { // // get current block gas limit // const block: providers.Block = await this.provider.getBlock('latest'); // if transaction has max fee per gas if (transaction.maxFeePerGas) { if ( transaction.maxFeePerGas.gt(this.maxFeePerGas) || transaction.maxFeePerGas.lt(this.maxFeePerGas) ) { transaction.maxFeePerGas = this.maxFeePerGas; } } // if transaction has max priority fee per gas if (transaction.maxPriorityFeePerGas) { if ( transaction.maxPriorityFeePerGas.gt(this.maxPriorityFeePerGas) || transaction.maxPriorityFeePerGas.lt(this.maxPriorityFeePerGas) ) { transaction.maxPriorityFeePerGas = this.maxPriorityFeePerGas; } } // remove gas price if max fee per gas is present if (transaction.maxFeePerGas) { // transaction.gasPrice = undefined; delete transaction['gasPrice']; } else { // update gas price if (transaction.gasPrice) { if (transaction.gasPrice.lt(this.gasPrice)) { transaction.gasPrice = this.gasPrice; } } else { transaction.gasPrice = this.gasPrice; } } return transaction; } // If the transaction error is a gas error, update the gas limits and retry the transaction. private async handleGasErrors( transaction: PopulatedTransaction, transactionError?: TransactionError, ) { if (transactionError) { if ( transactionError.gasErrorCode && transactionError.gasErrorCode === CUSTOM_ERROR_CODES.MAX_FEE_PER_GAS_LESS_THAN_BLOCK_BASE_FEE ) { await this.initialize(this.chainId); } // Skip gas price optimization if max fee per gas is set if (!transaction.maxFeePerGas) { // set gasPrice from transaction transaction = await this.increaseGasPrice(transaction); } if ( transactionError.gasErrorCode && transactionError.gasErrorCode === CUSTOM_ERROR_CODES.GAS_LIMIT_TOO_LOW ) { await this.initialize(this.chainId); // set higher max fee per gas if base gas price has been exceeded transaction = await this.increaseMaxFeePerGas(transaction); } } return transaction; } // perform gas optimization public async optimize( transactionQueueItem: TransactionManagerQueueItem, transactionError?: TransactionError, ): Promise { await this.initialize(this.chainId); let transaction: PopulatedTransaction = transactionQueueItem.transaction; const transactionRequest = transactionQueueItem.transactionManagerResponse.transactionRequest; // Get the current gas limit for this transaction let lastGasLimit = this.gasTracker.getGasLimitFromTracker( transaction.to, transactionRequest.method, transactionRequest.metaData?.trackingId, ); console.log('Last gs limit picked from tracker ', lastGasLimit); if (!lastGasLimit) { lastGasLimit = this.transactionGasLimitTracker.get( transactionQueueItem.id, ); // If there is a gas limit, set it to the new value if (lastGasLimit) { transaction.gasLimit = lastGasLimit; } } else { transaction.gasLimit = lastGasLimit; } // check for the error code and get the fee data transaction = await this.handleGasErrors(transaction, transactionError); // optimize gas parameters present in current transaction transaction = await this.optimizeGasParams(transaction); // dont set gasLimit if max fee per gas is present // if (transaction.type && transaction.type === 2) { // update gas limit // transaction = await this.optimizeGasLimit(transaction); // delete transaction['gasLimit']; // } else { try { let incrementGasLimit = false; if (transactionError) { incrementGasLimit = true; } const transactionGasMultiplier = this.gasTracker.getTransactionGasMultiplier( transactionRequest, transaction, ); const optimizationResult: GasOptimizationResult = await this.optimizeGasLimit( transaction, incrementGasLimit, transactionRequest, transactionGasMultiplier, ); transaction.gasLimit = optimizationResult.gasLimit; // update gas limit and simulated gas limit to transaction this.gasTracker.updateOptimizedGasParamsOnTracer( transaction, transactionRequest, optimizationResult, ); } catch (error) { console.log( '🚀 ~ file: gas-optimizer.ts:393 ~ TransactionGasOptimizer ~ error:', error, ); transactionQueueItem.transaction = transaction; // await this.errorHandler.handleError(error, transactionQueueItem); throw new Error(error); } // } // place higher gas price // update gas params at last in case any gas parameters has been changed during gas limit calculations await this.initialize(this.chainId); transaction = await this.optimizeGasParams(transaction); // update transaction transactionQueueItem.transaction = transaction; // If there is a gas limit, set it to the new value if (transaction.gasLimit) { process.env.DEBUG == 'true' && console.log( `Gas limit set for transaction ${transactionQueueItem.id} to ${transaction.gasLimit}`, ); this.transactionGasLimitTracker.set( transactionQueueItem.id, transaction.gasLimit, ); } return transactionQueueItem; } }