UNPKG

hardhat-gas-reporter

Version:
191 lines (173 loc) 5.96 kB
import path from "path"; import _ from "lodash"; import {parse, visit} from "@solidity-parser/parser"; import { Interface } from "@ethersproject/abi"; import { EthereumProvider, HardhatRuntimeEnvironment } from "hardhat/types"; import { getHashedFunctionSignature } from "../utils/sources"; import { RemoteContract, ContractInfo, GasReporterOptions } from "../types"; /** * Filters out contracts to exclude from report * @param {string} qualifiedName HRE artifact identifier * @param {string[]} skippable excludeContracts option values * @return {boolean} */ function shouldSkipContract( qualifiedName: string, skippable: string[] ): boolean { for (const item of skippable) { if (qualifiedName.includes(item)) { return true; } } return false; } /** * Fetches remote bytecode at address and hashes it so these addresses can be * added to the tracking in the collector * @param {EGRAsyncApiProvider} provider * @param {RemoteContract[] = []} remoteContracts * @return {Promise<RemoteContract[]>} */ export async function getResolvedRemoteContracts( provider: EthereumProvider, remoteContracts: RemoteContract[] = [] ): Promise<RemoteContract[]> { const { default: sha1 } = await import("sha1"); for (const contract of remoteContracts) { try { contract.bytecode = await provider.send("eth_getCode", [contract.address, "latest"]); contract.deployedBytecode = contract.bytecode; contract.bytecodeHash = sha1(contract.bytecode!); } catch (error: any) { console.log( `hardhat-gas-reporter:warning: failed to fetch bytecode for remote contract: ${contract.name}` ); console.log(`Error was: ${error}\n`); } } return remoteContracts; } /** * Loads and processes artifacts * @param {HardhatRuntimeEnvironment} hre * @param {GasReporterOptions[]} options * @return {ContractInfo[]} */ export async function getContracts( hre: HardhatRuntimeEnvironment, options: GasReporterOptions, ): Promise<ContractInfo[]> { const visited = {}; const contracts: ContractInfo[] = []; const resolvedRemoteContracts = await getResolvedRemoteContracts( hre.network.provider, options.remoteContracts ); const resolvedQualifiedNames = await hre.artifacts.getAllFullyQualifiedNames(); for (const qualifiedName of resolvedQualifiedNames) { if (shouldSkipContract(qualifiedName, options.excludeContracts!)) { continue; } let name: string; let artifact = await hre.artifacts.readArtifact(qualifiedName); // Prefer simple names try { artifact = await hre.artifacts.readArtifact(artifact.contractName); name = artifact.contractName; } catch (e) { name = path.relative(hre.config.paths.sources, qualifiedName);; } const excludedMethods = await getExcludedMethodKeys( hre, options, artifact.abi, name, qualifiedName, visited ); contracts.push({ name, excludedMethods, artifact: { abi: artifact.abi, bytecode: artifact.bytecode, deployedBytecode: artifact.deployedBytecode, }, }); } for (const remoteContract of resolvedRemoteContracts) { contracts.push({ name: remoteContract.name, excludedMethods: [], // no source artifact: { abi: remoteContract.abi, address: remoteContract.address, bytecode: remoteContract.bytecode, bytecodeHash: remoteContract.bytecodeHash, deployedBytecode: remoteContract.deployedBytecode, }, } as ContractInfo); } return contracts; } /** * Parses each file in a contract's dependency tree to identify public StateVariables and * add them to a list of methods to exclude from the report. Enabled when * `excludeAutoGeneratedGetters` and `reportPureAndViewMethods` are both true. * * TODO: warn when files don't parse * * @param {HardhatRuntimeEnvironment} hre * @param {GasReporterOptions} options * @param {any[]} abi * @param {string} name * @param {string} qualifiedName * @param {[key: string]: string[]} visited (cache) * @returns {Promise<string[]>} */ async function getExcludedMethodKeys( hre: HardhatRuntimeEnvironment, options: GasReporterOptions, abi: any[], contractName: string, contractQualifiedName: string, visited: {[key: string]: string[]} ): Promise<string[]> { const excludedMethods = new Set(); if (options.reportPureAndViewMethods && options.excludeAutoGeneratedGetters) { const info = await hre.artifacts.getBuildInfo(contractQualifiedName); const functions = new Interface(abi).functions if (info && info.input && info.input.sources) { _.forEach(info?.input.sources, (source, sourceKey) => { // Cache dependency sources if (!visited[sourceKey]){ visited[sourceKey] = []; } else { visited[sourceKey].forEach(_name => { if (!excludedMethods.has(_name)){ excludedMethods.add(`${contractName}_${getHashedFunctionSignature(_name)}`) } }) return; }; try { const ast = parse(source.content, {tolerant: true}); visit(ast, { StateVariableDeclaration (node) { const publicVars = node.variables.filter(({ visibility }) => visibility === 'public'); publicVars.forEach(_var => { const formattedName = Object.keys(functions).find(key => functions[key].name === _var.name); if (formattedName){ visited[sourceKey].push(formattedName); excludedMethods.add(`${contractName}_${getHashedFunctionSignature(formattedName)}`) } }) } }) } catch (err) { /* ignore */ } }); } } return Array.from(excludedMethods) as string[]; }