hardhat-gas-reporter
Version:
Gas Analytics plugin for Hardhat
280 lines • 11.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GasData = void 0;
const sha1_1 = __importDefault(require("sha1"));
const abi_1 = require("@ethersproject/abi");
const ui_1 = require("../utils/ui");
const sources_1 = require("../utils/sources");
const gas_1 = require("../utils/gas");
/**
* Data store written to by Collector and consumed by output formatters.
*/
class GasData {
constructor(methods, deployments) {
this.addressCache = {};
this.methods = (methods) ? methods : {};
this.deployments = (deployments) ? deployments : [];
this.codeHashMap = {};
}
/**
* Sets up data structures to store deployments and methods gas usage
* @param {EthereumProvider} provider
* @param {ContractInfo[]} contracts
* @returns
*/
initialize(provider, contracts) {
this.provider = provider;
for (const item of contracts) {
const contract = {
name: item.name,
bytecode: item.artifact.bytecode,
deployedBytecode: item.artifact.deployedBytecode,
gasData: [],
callData: []
};
this.deployments.push(contract);
if (item.artifact.bytecodeHash) {
this.trackNameByPreloadedAddress(item.name, item.artifact.address, item.artifact.bytecodeHash);
}
// Decode, getMethodIDs
const methodIDs = {};
let methods;
try {
methods = new abi_1.Interface(item.artifact.abi).functions;
}
catch (err) {
(0, ui_1.warnEthers)(contract.name, err);
return;
}
// Generate sighashes and remap ethers to something similar
// to abiDecoder.getMethodIDs
Object.keys(methods).forEach(key => {
const sighash = (0, sources_1.getHashedFunctionSignature)(key);
// @ts-ignore
methodIDs[sighash] = { fnSig: key, ...methods[key] };
});
// Create Method Map;
Object.keys(methodIDs).forEach(key => {
const isInterface = item.artifact.bytecode === "0x";
const isCall = methodIDs[key].constant;
const methodHasName = methodIDs[key].name !== undefined;
const contractScopedKey = `${contract.name}_${key}`;
if (methodHasName && !isInterface && !item.excludedMethods.includes(contractScopedKey)) {
this.methods[contractScopedKey] = {
key,
isCall,
contract: contract.name,
method: methodIDs[key].name,
fnSig: methodIDs[key].fnSig,
intrinsicGas: [],
callData: [],
gasData: [],
numberOfCalls: 0
};
}
});
}
}
/**
* Calculate gas deltas compared to previous data, if applicable
* @param {GasData} previousData previous gas data
*/
addDeltas(previousData) {
Object.keys(this.methods).forEach(key => {
if (!previousData.methods[key])
return;
const currentMethod = this.methods[key];
const prevMethod = previousData.methods[key];
this._calculateDeltas(prevMethod, currentMethod);
});
this.deployments.forEach((currentDeployment) => {
const prevDeployment = previousData.deployments.find((d) => d.name === currentDeployment.name);
if (!prevDeployment)
return;
this._calculateDeltas(prevDeployment, currentDeployment);
});
}
/**
* Map a contract name to pre-generated hash of the code stored at an address
* @param {String} name contract name
* @param {String} address contract address
*/
trackNameByPreloadedAddress(name, address, hash) {
if (this.addressIsCached(address))
return;
this.codeHashMap[hash] = name;
this.addressCache[address] = name;
}
/**
* Map a contract name to the sha1 hash of the code stored at an address
* @param {String} name contract name
* @param {String} address contract address
*/
async trackNameByAddress(name, address) {
if (this.addressIsCached(address))
return;
const code = await this.provider.send("eth_getCode", [address, "latest"]);
const hash = code ? (0, sha1_1.default)(code) : null;
this.addressCache[address] = name;
if (hash !== null)
this.codeHashMap[hash] = name;
}
/**
* Get the name of the contract stored at contract address
* @param {string | null} contract address
* @return {Promse<string | null>} contract name
*/
async getNameByAddress(address) {
if (!address)
return null;
if (this.addressIsCached(address)) {
return this.addressCache[address];
}
const code = await this.provider.send("eth_getCode", [address, "latest"]);
const hash = code ? (0, sha1_1.default)(code) : null;
return (hash !== null) ? this.codeHashMap[hash] : null;
}
/**
* Compares existing contract binaries to the input code for a
* new deployment transaction and returns the relevant contract.
* Ignores interfaces.
* @param {String} input tx.input
* @return {Deployment | null} this.deployments entry
*/
getContractByDeploymentInput(input) {
if (!input)
return null;
const matches = this.deployments.filter(item => (0, sources_1.matchBinaries)(input, item.bytecode));
// Filter interfaces
if (matches && (matches.length > 0)) {
const match = matches.find(item => item.deployedBytecode !== "0x");
return (match !== undefined) ? match : null;
}
else {
return null;
}
}
/**
* Compares code at an address to the deployedBytecode for all
* compiled contracts and returns the relevant item.
* Ignores interfaces.
* @param {String} code result of web3.eth.getCode
* @return {Deployment | null} this.deployments entry
*/
getContractByDeployedBytecode(code) {
if (!code)
return null;
const matches = this.deployments.filter(item => (0, sources_1.matchBinaries)(code, item.deployedBytecode));
// Filter interfaces
if (matches && (matches.length > 0)) {
const match = matches.find(item => item.deployedBytecode !== "0x");
return (match !== undefined) ? match : null;
}
else {
return null;
}
}
/**
* Returns all contracts with a method matching the requested signature
* @param {String} signature method signature hash
* @return {MethodDataItem[]} this.method entries array
*/
getAllContractsWithMethod(signature) {
return Object.values(this.methods).filter((el) => el.key === signature);
}
addressIsCached(address) {
if (address === null)
return false;
return Object.keys(this.addressCache).includes(address);
}
resetAddressCache() {
this.addressCache = {};
}
/**
* Calculates summary and price data for methods and deployments data after it's all
* been collected
*/
async runAnalysis(hre, options) {
const block = await hre.network.provider.send("eth_getBlockByNumber", ["latest", false]);
const blockGasLimit = parseInt(block.gasLimit);
let methodsExecutionTotal = 0;
let methodsCalldataTotal = 0;
let deploymentsExecutionTotal = 0;
let deploymentsCalldataTotal = 0;
/* Methods */
for (const key of Object.keys(this.methods)) {
const method = this.methods[key];
if (method.gasData.length > 0) {
this._processItemData(method, options);
methodsExecutionTotal += method.executionGasAverage;
methodsCalldataTotal += method.calldataGasAverage;
}
}
/* Deployments */
for (const deployment of this.deployments) {
if (deployment.gasData.length !== 0) {
this._processItemData(deployment, options);
deployment.percent = (0, gas_1.gasToPercentOfLimit)(deployment.executionGasAverage, blockGasLimit);
deploymentsExecutionTotal += deployment.executionGasAverage;
deploymentsCalldataTotal += deployment.calldataGasAverage;
}
}
hre.__hhgrec.blockGasLimit = blockGasLimit;
hre.__hhgrec.methodsTotalGas = methodsExecutionTotal;
hre.__hhgrec.deploymentsTotalGas = deploymentsExecutionTotal;
hre.__hhgrec.methodsTotalCost = this._getCost(methodsExecutionTotal, methodsCalldataTotal, options);
hre.__hhgrec.deploymentsTotalCost = this._getCost(deploymentsExecutionTotal, deploymentsCalldataTotal, options);
}
/**
* Calculates execution and calldata gas averages, min/max and currency cost for method
* and deployment data
* @param {MethodDataItem | Deployment} item
* @param {GasReporterOptions} options
*/
_processItemData(item, options) {
const executionTotal = item.gasData.reduce((acc, datum) => acc + datum, 0);
item.executionGasAverage = Math.round(executionTotal / item.gasData.length);
const calldataTotal = item.callData.reduce((acc, datum) => acc + datum, 0);
item.calldataGasAverage = Math.round(calldataTotal / item.gasData.length);
item.cost = this._getCost(item.executionGasAverage, item.calldataGasAverage, options);
const sortedData = item.gasData.sort((a, b) => a - b);
item.min = sortedData[0];
item.max = sortedData[sortedData.length - 1];
}
/**
* Optionally calculates the total currency cost of execution and calldata gas usage
* @param {number} executionGas
* @param {number} calldataGas
* @param {GasReporterOptions} options
* @returns
*/
_getCost(executionGas, calldataGas, options) {
return (options.tokenPrice && options.gasPrice)
? (0, gas_1.gasToCost)(executionGas, calldataGas, options)
: undefined;
}
/**
* Calculate gas deltas for a given method or deployment item
* @param {MethodDataItem | Deployment} prev
* @param {MethodDataItem | Deployment} current
*/
_calculateDeltas(prev, current) {
if (current.min !== undefined && prev.min !== undefined) {
current.minDelta = current.min - prev.min;
}
if (current.max !== undefined && prev.max !== undefined) {
current.maxDelta = current.max - prev.max;
}
if (current.executionGasAverage !== undefined && prev.executionGasAverage !== undefined) {
current.executionGasAverageDelta = current.executionGasAverage - prev.executionGasAverage;
}
if (current.calldataGasAverage !== undefined && prev.calldataGasAverage !== undefined) {
current.calldataGasAverageDelta = current.calldataGasAverage - prev.calldataGasAverage;
}
}
}
exports.GasData = GasData;
//# sourceMappingURL=gasData.js.map