import { BigNumber, PopulatedTransaction } from 'ethers'; import { TransactionManagerRequest } from '../transaction-inputs/transaction-manager-request'; import { ContractMethodLastGasParams, GasOptimizationResult } from '../types'; import { calculateMultiplier, getGasMultiplier, reduceGasLimits, MAX_GAS_MULTIPLIER, } from '../utils'; export class GasTracker { private contractMethodGasUsedTracker: Map< string, ContractMethodLastGasParams > = new Map(); private gasMultiplier: number; constructor(gasMultiplier: number) { this.contractMethodGasUsedTracker = new Map< string, ContractMethodLastGasParams >(); this.gasMultiplier = gasMultiplier; } private getKey( transactionRequest: TransactionManagerRequest, transaction: PopulatedTransaction, ): string { let trackerKey = `${transaction.to}-${transactionRequest.method}`; if (transactionRequest?.metaData?.trackingId) { trackerKey = `${trackerKey}-${transactionRequest.metaData.trackingId}`; } return trackerKey; } private getKeyFromParams( contractAddress: string, methodName: string, trackingId?: string, ) { // Set a key to uniquely identify the contract method let key = `${contractAddress}-${methodName}`; // if trackingId is provided add it if (trackingId) { key = `${key}-${trackingId}`; } return key; } private getGasParams(key: string): ContractMethodLastGasParams | undefined { if (this.contractMethodGasUsedTracker.has(key)) { return this.contractMethodGasUsedTracker.get(key); } return undefined; } private updateGasParams( key: string, gasParams: ContractMethodLastGasParams, ): ContractMethodLastGasParams | undefined { console.log( '🚀 ~ file: gas-tracker.ts:62 ~ GasTracker ~ gasParams:', gasParams, ); console.log('🚀 ~ file: gas-tracker.ts:62 ~ GasTracker ~ key:', key); const oldGasParams = this.getGasParams(key); if (oldGasParams) { this.contractMethodGasUsedTracker.set(key, { ...oldGasParams, ...gasParams, }); } else { this.contractMethodGasUsedTracker.set(key, gasParams); } const updatedGasParams = this.contractMethodGasUsedTracker.get(key); console.log( '🚀 ~ file: gas-tracker.ts:78 ~ GasTracker ~ updatedGasParams: Updated gas params for key ', key, updatedGasParams, ); return updatedGasParams; } public updateTimeoutGasMultiplier( transactionRequest: TransactionManagerRequest, transaction: PopulatedTransaction, ): void { const trackerKey = this.getKey(transactionRequest, transaction); // check if contractGasUsed tracker has the data const contractGasUsed = this.getGasParams(trackerKey); if (contractGasUsed) { contractGasUsed.gasMultiplier = calculateMultiplier( contractGasUsed.lastGasLimit || transaction.gasLimit || BigNumber.from('0'), contractGasUsed.gasMultiplier || 1, contractGasUsed.simulatedGasLimit, ); console.log( 'Resetting in case of transaction timeouts ', trackerKey, ' multiplier ', contractGasUsed.gasMultiplier, ); this.updateGasParams(trackerKey, contractGasUsed); } } public handleMaxRetryReached( transactionRequest: TransactionManagerRequest, transaction: PopulatedTransaction, ): void { // reduce the gas limits as retry may raised them to a certain extent const trackerKey = this.getKey(transactionRequest, transaction); // check if contractGasUsed tracker has the data const contractGasUsed = this.getGasParams(trackerKey); if (contractGasUsed) { const updatedGasLimits: { gasLimit?: BigNumber; gasMultiplier: number; } = reduceGasLimits(contractGasUsed, this.gasMultiplier); console.log('Timeout issue reduce gas to lower levels'); console.log('Reduced gas limits from ', contractGasUsed.lastGasLimit, ' to ', updatedGasLimits.gasLimit, ' for key ', trackerKey); contractGasUsed.lastGasLimit = updatedGasLimits.gasLimit; contractGasUsed.gasMultiplier = updatedGasLimits.gasMultiplier; this.updateGasParams(trackerKey, contractGasUsed); } } 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, ); // Set a key to uniquely identify the contract method const key = this.getKeyFromParams( contractAddress || '', methodName || '', trackingId, ); // If the contract address, method name, gas used, and gas limit are defined, run the following code if (contractAddress && methodName && gasUsed && gasLimit) { // Calculate the gas delta const gasDelta = gasUsed.mul(BigNumber.from(10).pow(20)).div(gasLimit); // Calculate the gas percentage const gasPercentage = parseInt(gasDelta.toString()) / 10 ** 18; // If the gas percentage is above the threshold, increase the gas multiplier const gasUsedObj = this.getGasParams(key); if (gasUsedObj) { // set gas used and last gas limit if ( gasUsedObj.gasMultiplier && gasUsedObj.gasMultiplier < MAX_GAS_MULTIPLIER && isTransactionFailed === true ) { gasUsedObj.gasMultiplier = calculateMultiplier( gasLimit, gasUsedObj.gasMultiplier, ); } else if ( gasUsedObj.gasMultiplier && gasUsed && isTransactionFailed !== true ) { { process.env.DEBUG === 'true' && console.log('Testing the gas multiplier'); } gasUsedObj.gasMultiplier = getGasMultiplier( gasUsedObj.gasMultiplier, gasUsed, gasPercentage, gasLimit, ); } // Add the updated gas used object to the contract method gas used tracker this.updateGasParams(key, { gasUsed: gasUsed, lastGasLimit: gasLimit, usePercent: gasPercentage, gasMultiplier: gasUsedObj.gasMultiplier, simulatedGasLimit: gasUsedObj.simulatedGasLimit, }); } else { // set gas used and last gas limit this.updateGasParams(key, { gasUsed: gasUsed, lastGasLimit: gasLimit, usePercent: gasPercentage, }); } } } 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 const key = this.getKeyFromParams( contractAddress || '', methodName || '', trackingId, ); // If the contract method gas used tracker has the key, return the gas multiplier const gasUsedObj = this.getGasParams(key); if (gasUsedObj) { return gasUsedObj; } } return undefined; } public getGasLimitFromTracker( contractAddress?: string, methodName?: string, trackingId?: string, ): BigNumber | 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 const key = this.getKeyFromParams( contractAddress || '', methodName || '', trackingId, ); console.log( '🚀 ~ file: gas-optimizer.ts:186 ~ TransactionGasOptimizer ~ key:', key, ); // If the contract method gas used tracker has the key, return the gas multiplier const gasUsedObj = this.getGasParams(key); if (gasUsedObj) { console.log( '🚀 ~ file: gas-optimizer.ts:196 ~ TransactionGasOptimizer ~ gasUsedObj.lastGasLimit:', gasUsedObj.lastGasLimit, ' Key ', key, ); return gasUsedObj.lastGasLimit; } return undefined; } // Return the default gas multiplier return undefined; } public updateOptimizedGasParamsOnTracer( transaction: PopulatedTransaction, transactionRequest: TransactionManagerRequest, optimizationResult: GasOptimizationResult, ): void { const trackerKey = this.getKey(transactionRequest, transaction); console.log( `Trying to update gas limit to ${optimizationResult.gasLimit} for ${trackerKey}`, ); // check if contractGasUsed tracker has the data const contractGasUsed = this.getGasParams(trackerKey); if (contractGasUsed) { contractGasUsed.lastGasLimit = optimizationResult.gasLimit; contractGasUsed.simulatedGasLimit = optimizationResult.simulatedGasLimit; console.log( `Last gas limit updated to ${optimizationResult.gasLimit} for ${trackerKey}`, ); this.updateGasParams(trackerKey, contractGasUsed); } } public getTransactionGasMultiplier( transactionRequest: TransactionManagerRequest, transaction: PopulatedTransaction, ): number { const trackerKey = this.getKey(transactionRequest, transaction); // check if contractGasUsed tracker has the data const contractGasUsed = this.getGasParams(trackerKey); // get gas multiplier from contract gas used tracker and check weather it is larger than global gas multiplier if (contractGasUsed && contractGasUsed.gasMultiplier) { return contractGasUsed.gasMultiplier; } // get gas multiplier from transaction request and check weather it is larger than global gas multiplier if ( transactionRequest.gasMultiplier && transactionRequest.gasMultiplier > this.gasMultiplier ) { // set the gas multiplier to gas used tracker this.updateGasParams(trackerKey, { gasMultiplier: transactionRequest.gasMultiplier, }); return transactionRequest.gasMultiplier; } else { // Return gas multiplier from current global settings if (this.gasMultiplier) { // set the gas multiplier to gas used tracker this.updateGasParams(trackerKey, { gasMultiplier: this.gasMultiplier, }); } return this.gasMultiplier; } } }