UNPKG

8.46 kBPlain TextView Raw
1import fs from "fs"
2import path from "path"
3import sha1 from "sha1"
4import arrayUniq from "array-uniq";
5import { TASK_TEST_RUN_MOCHA_TESTS } from "hardhat/builtin-tasks/task-names";
6import { task, subtask } from "hardhat/config";
7import { HARDHAT_NETWORK_NAME } from "hardhat/plugins";
8import { globSync } from "hardhat/internal/util/glob";
9import {
10 BackwardsCompatibilityProviderAdapter
11} from "hardhat/internal/core/providers/backwards-compatibility"
12
13import {
14 EGRDataCollectionProvider,
15 EGRAsyncApiProvider
16} from "./providers";
17
18import {
19 HardhatArguments,
20 HttpNetworkConfig,
21 NetworkConfig,
22 EthereumProvider,
23 HardhatRuntimeEnvironment,
24 Artifact,
25 Artifacts
26} from "hardhat/types";
27
28import "./type-extensions"
29import { EthGasReporterConfig, EthGasReporterOutput, RemoteContract } from "./types";
30import { TASK_GAS_REPORTER_MERGE, TASK_GAS_REPORTER_MERGE_REPORTS } from "./task-names";
31import { mergeReports } from "./merge-reports";
32
33const { parseSoliditySources, setGasAndPriceRates } = require('eth-gas-reporter/lib/utils');
34const InternalReporterConfig = require('eth-gas-reporter/lib/config');
35
36let mochaConfig;
37let resolvedQualifiedNames: string[]
38let resolvedRemoteContracts: RemoteContract[] = [];
39
40/**
41 * Filters out contracts to exclude from report
42 * @param {string} qualifiedName HRE artifact identifier
43 * @param {string[]} skippable excludeContracts option values
44 * @return {boolean}
45 */
46function shouldSkipContract(qualifiedName: string, skippable: string[]): boolean {
47 for (const item of skippable){
48 if (qualifiedName.includes(item)) return true;
49 }
50 return false;
51}
52
53/**
54 * Method passed to eth-gas-reporter to resolve artifact resources. Loads
55 * and processes JSON artifacts
56 * @param {HardhatRuntimeEnvironment} hre.artifacts
57 * @param {String[]} skippable contract *not* to track
58 * @return {object[]} objects w/ abi and bytecode
59 */
60function getContracts(artifacts: Artifacts, skippable: string[] = []) : any[] {
61 const contracts = [];
62
63 for (const qualifiedName of resolvedQualifiedNames) {
64 if (shouldSkipContract(qualifiedName, skippable)){
65 continue;
66 }
67
68 let name: string;
69 let artifact = artifacts.readArtifactSync(qualifiedName)
70
71 // Prefer simple names
72 try {
73 artifact = artifacts.readArtifactSync(artifact.contractName);
74 name = artifact.contractName;
75 } catch (e) {
76 name = qualifiedName;
77 }
78
79 contracts.push({
80 name: name,
81 artifact: {
82 abi: artifact.abi,
83 bytecode: artifact.bytecode,
84 deployedBytecode: artifact.deployedBytecode
85 }
86 });
87 }
88
89 for (const remoteContract of resolvedRemoteContracts){
90 contracts.push({
91 name: remoteContract.name,
92 artifact: {
93 abi: remoteContract.abi,
94 bytecode: remoteContract.bytecode,
95 bytecodeHash: remoteContract.bytecodeHash,
96 deployedBytecode: remoteContract.deployedBytecode
97 }
98 })
99 }
100 return contracts;
101}
102
103/**
104 * Sets reporter options to pass to eth-gas-reporter:
105 * > url to connect to client with
106 * > artifact format (hardhat)
107 * > solc compiler info
108 * @param {HardhatRuntimeEnvironment} hre
109 * @return {EthGasReporterConfig}
110 */
111function getDefaultOptions(hre: HardhatRuntimeEnvironment): EthGasReporterConfig {
112 const defaultUrl = "http://localhost:8545";
113 const defaultCompiler = hre.config.solidity.compilers[0]
114
115 let url: any;
116 // Resolve URL
117 if ((<HttpNetworkConfig>hre.network.config).url) {
118 url = (<HttpNetworkConfig>hre.network.config).url;
119 } else {
120 url = defaultUrl;
121 }
122
123 return {
124 enabled: true,
125 url: <string>url,
126 metadata: {
127 compiler: {
128 version: defaultCompiler.version
129 },
130 settings: {
131 optimizer: {
132 enabled: defaultCompiler.settings.optimizer.enabled,
133 runs: defaultCompiler.settings.optimizer.runs
134 }
135 }
136 }
137 }
138}
139
140/**
141 * Merges GasReporter defaults with user's GasReporter config
142 * @param {HardhatRuntimeEnvironment} hre
143 * @return {any}
144 */
145function getOptions(hre: HardhatRuntimeEnvironment): any {
146 return { ...getDefaultOptions(hre), ...(hre.config as any).gasReporter };
147}
148
149/**
150 * Fetches remote bytecode at address and hashes it so these addresses can be
151 * added to the tracking at eth-gas-reporter synchronously on init.
152 * @param {EGRAsyncApiProvider} provider
153 * @param {RemoteContract[] = []} remoteContracts
154 * @return {Promise<RemoteContract[]>}
155 */
156async function getResolvedRemoteContracts(
157 provider: EGRAsyncApiProvider,
158 remoteContracts: RemoteContract[] = []
159) : Promise <RemoteContract[]> {
160 for (const contract of remoteContracts){
161 let code;
162 try {
163 contract.bytecode = await provider.getCode(contract.address);
164 contract.deployedBytecode = contract.bytecode;
165 contract.bytecodeHash = sha1(contract.bytecode);
166 } catch (error){
167 console.log(`Warning: failed to fetch bytecode for remote contract: ${contract.name}`)
168 console.log(`Error was: ${error}\n`);
169 }
170 }
171 return remoteContracts;
172}
173
174/**
175 * Overrides TASK_TEST_RUN_MOCHA_TEST to (conditionally) use eth-gas-reporter as
176 * the mocha test reporter and passes mocha relevant options. These are listed
177 * on the `gasReporter` of the user's config.
178 */
179subtask(TASK_TEST_RUN_MOCHA_TESTS).setAction(
180 async (args: any, hre, runSuper) => {
181 let options = getOptions(hre);
182 options.getContracts = getContracts.bind(null, hre.artifacts, options.excludeContracts);
183
184 if (options.enabled) {
185 // Fetch data from gas and coin price providers
186 options = new InternalReporterConfig(options);
187 await setGasAndPriceRates(options);
188
189 mochaConfig = hre.config.mocha || {};
190 mochaConfig.reporter = "eth-gas-reporter";
191 mochaConfig.reporterOptions = options;
192
193 if (hre.network.name === HARDHAT_NETWORK_NAME || options.fast){
194 const wrappedDataProvider= new EGRDataCollectionProvider(hre.network.provider,mochaConfig);
195 hre.network.provider = new BackwardsCompatibilityProviderAdapter(wrappedDataProvider);
196
197 const asyncProvider = new EGRAsyncApiProvider(hre.network.provider);
198 resolvedRemoteContracts = await getResolvedRemoteContracts(
199 asyncProvider,
200 options.remoteContracts
201 );
202
203 mochaConfig.reporterOptions.provider = asyncProvider;
204 mochaConfig.reporterOptions.blockLimit = (<any>hre.network.config).blockGasLimit as number;
205 mochaConfig.attachments = {};
206 }
207
208 hre.config.mocha = mochaConfig;
209 resolvedQualifiedNames = await hre.artifacts.getAllFullyQualifiedNames();
210 }
211
212 return runSuper();
213 }
214);
215
216subtask(TASK_GAS_REPORTER_MERGE_REPORTS)
217 .addOptionalVariadicPositionalParam(
218 "inputFiles",
219 "Path of several gasReporterOutput.json files to merge",
220 []
221 )
222 .setAction(async ({ inputFiles }: { inputFiles: string[] }) => {
223 const reports = inputFiles.map((input) => JSON.parse(fs.readFileSync(input, "utf-8")));
224 return mergeReports(reports);
225 })
226
227/**
228 * Task for merging multiple gasReporterOutput.json files generated by eth-gas-reporter
229 * This task is necessary when we want to generate different parts of the reports
230 * parallelized on different jobs, then merge the results and upload it to codechecks.
231 * Gas Report JSON file schema: https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/gasReporterOutput.md
232 */
233task(TASK_GAS_REPORTER_MERGE)
234 .addOptionalParam(
235 "output",
236 "Target file to save the merged report",
237 "gasReporterOutput.json"
238 )
239 .addVariadicPositionalParam(
240 "input",
241 "A list of gasReporterOutput.json files generated by eth-gas-reporter. Files can be defined using glob patterns"
242 )
243 .setAction(async (taskArguments, { run }) => {
244 const output = path.resolve(process.cwd(), taskArguments.output);
245
246 // Parse input files and calculate glob patterns
247 const inputFiles = arrayUniq<string>(taskArguments.input.map(globSync).flat())
248 .map(inputFile => path.resolve(inputFile));
249
250 if (inputFiles.length === 0) {
251 throw new Error(`No files found for the given input: ${taskArguments.input.join(" ")}`);
252 }
253
254 console.log(`Merging ${inputFiles.length} input files:`);
255 inputFiles.forEach(inputFile => {
256 console.log(" - ", inputFile);
257 });
258
259 console.log("\nOutput: ", output);
260
261 const result = await run(TASK_GAS_REPORTER_MERGE_REPORTS, { inputFiles });
262
263 fs.writeFileSync(output, JSON.stringify(result), "utf-8");
264 });