import { BigNumber, PopulatedTransaction } from 'ethers'; import axios, { AxiosRequestConfig } from 'axios'; import { AlchemyApiConfig, TenderlyApiConfig } from '../types'; export class TransactionSimulator { private tenderlyUserName: string; private tenderlyProjectName: string; private tenderlyAccessKey: string; private alchemyConfig: AlchemyApiConfig; constructor( tenderlyConfig: TenderlyApiConfig, alchemyConfig: AlchemyApiConfig, ) { this.tenderlyUserName = tenderlyConfig.userName; this.tenderlyProjectName = tenderlyConfig.projectName; this.tenderlyAccessKey = tenderlyConfig.accessKey; this.alchemyConfig = alchemyConfig; } async simulateWithAlchemy( transaction: PopulatedTransaction, ): Promise { const chainId = transaction.chainId; if (chainId) { const url: string = this.alchemyConfig[chainId]; const options: AxiosRequestConfig = { method: 'POST', url: url, headers: { accept: 'application/json', 'content-type': 'application/json', }, data: { id: 1, jsonrpc: '2.0', method: 'alchemy_simulateExecution', params: [ { from: transaction.from, to: transaction.to, value: transaction.value, data: transaction.data, }, ], }, }; const response = await axios.request(options); const responseData = response.data; if (responseData.result) { const result = responseData.result; if (result.calls && result.calls.length > 0) { const call = result.calls[0]; if (call.gasUsed) { return BigNumber.from(call.gasUsed); } } } } return BigNumber.from(0); } async simulate( transaction: PopulatedTransaction, baseGasFees?: BigNumber | null, ): Promise { let gasPrice = 0; if (baseGasFees) { gasPrice = parseInt(baseGasFees.toString()); if (transaction.gasPrice) { gasPrice = parseInt(transaction.gasPrice.toString()); } else if (transaction.maxPriorityFeePerGas) { gasPrice = parseInt( baseGasFees.add(transaction.maxPriorityFeePerGas).toString(), ); } } const options: AxiosRequestConfig = { method: 'POST', url: `https://api.tenderly.co/api/v1/account/${this.tenderlyUserName}/project/${this.tenderlyProjectName}/simulate`, headers: { accept: 'application/json', 'content-type': 'application/json', 'X-Access-Key': this.tenderlyAccessKey as string, }, data: { /* Simulation Configuration */ save: false, // if true simulation is saved and shows up in the dashboard save_if_fails: true, // if true, reverting simulations show up in the dashboard simulation_type: 'full', // full or quick (full is default) network_id: transaction.chainId, // network to simulate on /* Standard EVM Transaction object */ from: transaction.from, to: transaction.to, input: transaction.data, gas: parseInt(transaction.gasLimit?.toString() || '80000000'), gas_price: gasPrice, value: 0, }, }; try { const response = await axios.request(options); // check response status code if 200 if (response.status === 200) { const data = response.data.transaction; return BigNumber.from(data.gas_used); } else { return await this.simulateWithAlchemy(transaction); } } catch(e) { return await this.simulateWithAlchemy(transaction); } } }