UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

681 lines (679 loc) 29.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getArtifactFromContractOutput = exports.Artifacts = void 0; const debug_1 = __importDefault(require("debug")); const fs_extra_1 = __importDefault(require("fs-extra")); const os = __importStar(require("os")); const path = __importStar(require("path")); const promises_1 = __importDefault(require("fs/promises")); const contract_names_1 = require("../utils/contract-names"); const source_names_1 = require("../utils/source-names"); const constants_1 = require("./constants"); const errors_1 = require("./core/errors"); const errors_list_1 = require("./core/errors-list"); const hash_1 = require("./util/hash"); const fs_utils_1 = require("./util/fs-utils"); const log = (0, debug_1.default)("hardhat:core:artifacts"); class Artifacts { constructor(_artifactsPath) { this._artifactsPath = _artifactsPath; // Undefined means that the cache is disabled. this._cache = { artifactNameToArtifactPathCache: new Map(), artifactFQNToBuildInfoPathCache: new Map(), }; this._validArtifacts = []; } addValidArtifacts(validArtifacts) { this._validArtifacts.push(...validArtifacts); } async readArtifact(name) { const artifactPath = await this._getArtifactPath(name); return fs_extra_1.default.readJson(artifactPath); } readArtifactSync(name) { const artifactPath = this._getArtifactPathSync(name); return fs_extra_1.default.readJsonSync(artifactPath); } async artifactExists(name) { let artifactPath; try { artifactPath = await this._getArtifactPath(name); } catch (e) { if (errors_1.HardhatError.isHardhatError(e)) { return false; } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw e; } return fs_extra_1.default.pathExists(artifactPath); } async getAllFullyQualifiedNames() { const paths = await this.getArtifactPaths(); return paths.map((p) => this._getFullyQualifiedNameFromPath(p)).sort(); } async getBuildInfo(fullyQualifiedName) { let buildInfoPath = this._cache?.artifactFQNToBuildInfoPathCache.get(fullyQualifiedName); if (buildInfoPath === undefined) { const artifactPath = this.formArtifactPathFromFullyQualifiedName(fullyQualifiedName); const debugFilePath = this._getDebugFilePath(artifactPath); buildInfoPath = await this._getBuildInfoFromDebugFile(debugFilePath); if (buildInfoPath === undefined) { return undefined; } this._cache?.artifactFQNToBuildInfoPathCache.set(fullyQualifiedName, buildInfoPath); } return fs_extra_1.default.readJSON(buildInfoPath); } getBuildInfoSync(fullyQualifiedName) { let buildInfoPath = this._cache?.artifactFQNToBuildInfoPathCache.get(fullyQualifiedName); if (buildInfoPath === undefined) { const artifactPath = this.formArtifactPathFromFullyQualifiedName(fullyQualifiedName); const debugFilePath = this._getDebugFilePath(artifactPath); buildInfoPath = this._getBuildInfoFromDebugFileSync(debugFilePath); if (buildInfoPath === undefined) { return undefined; } this._cache?.artifactFQNToBuildInfoPathCache.set(fullyQualifiedName, buildInfoPath); } return fs_extra_1.default.readJSONSync(buildInfoPath); } async getArtifactPaths() { const cached = this._cache?.artifactPaths; if (cached !== undefined) { return cached; } const paths = await (0, fs_utils_1.getAllFilesMatching)(this._artifactsPath, (f) => this._isArtifactPath(f)); const result = paths.sort(); if (this._cache !== undefined) { this._cache.artifactPaths = result; } return result; } async getBuildInfoPaths() { const cached = this._cache?.buildInfoPaths; if (cached !== undefined) { return cached; } const paths = await (0, fs_utils_1.getAllFilesMatching)(path.join(this._artifactsPath, constants_1.BUILD_INFO_DIR_NAME), (f) => f.endsWith(".json")); const result = paths.sort(); if (this._cache !== undefined) { this._cache.buildInfoPaths = result; } return result; } async getDebugFilePaths() { const cached = this._cache?.debugFilePaths; if (cached !== undefined) { return cached; } const paths = await (0, fs_utils_1.getAllFilesMatching)(path.join(this._artifactsPath), (f) => f.endsWith(".dbg.json")); const result = paths.sort(); if (this._cache !== undefined) { this._cache.debugFilePaths = result; } return result; } async saveArtifactAndDebugFile(artifact, pathToBuildInfo) { try { // artifact const fullyQualifiedName = (0, contract_names_1.getFullyQualifiedName)(artifact.sourceName, artifact.contractName); const artifactPath = this.formArtifactPathFromFullyQualifiedName(fullyQualifiedName); await fs_extra_1.default.ensureDir(path.dirname(artifactPath)); await Promise.all([ fs_extra_1.default.writeJSON(artifactPath, artifact, { spaces: 2, }), (async () => { if (pathToBuildInfo === undefined) { return; } // save debug file const debugFilePath = this._getDebugFilePath(artifactPath); const debugFile = this._createDebugFile(artifactPath, pathToBuildInfo); await fs_extra_1.default.writeJSON(debugFilePath, debugFile, { spaces: 2, }); })(), ]); } finally { this.clearCache(); } } async saveBuildInfo(solcVersion, solcLongVersion, input, output) { try { const buildInfoDir = path.join(this._artifactsPath, constants_1.BUILD_INFO_DIR_NAME); await fs_extra_1.default.ensureDir(buildInfoDir); const buildInfoName = this._getBuildInfoName(solcVersion, solcLongVersion, input); const buildInfo = this._createBuildInfo(buildInfoName, solcVersion, solcLongVersion, input, output); const buildInfoPath = path.join(buildInfoDir, `${buildInfoName}.json`); // JSON.stringify of the entire build info can be really slow // in larger projects, so we stringify per part and incrementally create // the JSON in the file. // // We split this code into different curly-brace-enclosed scopes so that // partial JSON strings get out of scope sooner and hence can be reclaimed // by the GC if needed. const file = await promises_1.default.open(buildInfoPath, "w"); try { { const withoutOutput = JSON.stringify({ ...buildInfo, output: undefined, }); // We write the JSON (without output) except the last } await file.write(withoutOutput.slice(0, -1)); } { const outputWithoutSourcesAndContracts = JSON.stringify({ ...buildInfo.output, sources: undefined, contracts: undefined, }); // We start writing the output await file.write(',"output":'); // Write the output object except for the last } await file.write(outputWithoutSourcesAndContracts.slice(0, -1)); // If there were other field apart from sources and contracts we need // a comma if (outputWithoutSourcesAndContracts.length > 2) { await file.write(","); } } // Writing the sources await file.write('"sources":{'); let isFirst = true; for (const [name, value] of Object.entries(buildInfo.output.sources ?? {})) { if (isFirst) { isFirst = false; } else { await file.write(","); } await file.write(`${JSON.stringify(name)}:${JSON.stringify(value)}`); } // Close sources object await file.write("}"); // Writing the contracts await file.write(',"contracts":{'); isFirst = true; for (const [name, value] of Object.entries(buildInfo.output.contracts ?? {})) { if (isFirst) { isFirst = false; } else { await file.write(","); } await file.write(`${JSON.stringify(name)}:${JSON.stringify(value)}`); } // close contracts object await file.write("}"); // close output object await file.write("}"); // close build info object await file.write("}"); } finally { await file.close(); } return buildInfoPath; } finally { this.clearCache(); } } /** * Remove all artifacts that don't correspond to the current solidity files */ async removeObsoleteArtifacts() { // We clear the cache here, as we want to be sure this runs correctly this.clearCache(); try { const validArtifactPaths = await Promise.all(this._validArtifacts.flatMap(({ sourceName, artifacts }) => artifacts.map((artifactName) => this._getArtifactPath((0, contract_names_1.getFullyQualifiedName)(sourceName, artifactName))))); const validArtifactsPathsSet = new Set(validArtifactPaths); for (const { sourceName, artifacts } of this._validArtifacts) { for (const artifactName of artifacts) { validArtifactsPathsSet.add(this.formArtifactPathFromFullyQualifiedName((0, contract_names_1.getFullyQualifiedName)(sourceName, artifactName))); } } const existingArtifactsPaths = await this.getArtifactPaths(); await Promise.all(existingArtifactsPaths .filter((artifactPath) => !validArtifactsPathsSet.has(artifactPath)) .map((artifactPath) => this._removeArtifactFiles(artifactPath))); await this._removeObsoleteBuildInfos(); } finally { // We clear the cache here, as this may have non-existent paths now this.clearCache(); } } /** * Returns the absolute path to the given artifact * @throws {HardhatError} If the name is not fully qualified. */ formArtifactPathFromFullyQualifiedName(fullyQualifiedName) { const { sourceName, contractName } = (0, contract_names_1.parseFullyQualifiedName)(fullyQualifiedName); return path.join(this._artifactsPath, sourceName, `${contractName}.json`); } clearCache() { // Avoid accidentally re-enabling the cache if (this._cache === undefined) { return; } this._cache = { artifactFQNToBuildInfoPathCache: new Map(), artifactNameToArtifactPathCache: new Map(), }; } disableCache() { this._cache = undefined; } /** * Remove all build infos that aren't used by any debug file */ async _removeObsoleteBuildInfos() { const debugFiles = await this.getDebugFilePaths(); const buildInfos = await Promise.all(debugFiles.map(async (debugFile) => { const buildInfoFile = await this._getBuildInfoFromDebugFile(debugFile); if (buildInfoFile !== undefined) { return path.resolve(path.dirname(debugFile), buildInfoFile); } })); const filteredBuildInfos = buildInfos.filter((bf) => typeof bf === "string"); const validBuildInfos = new Set(filteredBuildInfos); const buildInfoFiles = await this.getBuildInfoPaths(); await Promise.all(buildInfoFiles .filter((buildInfoFile) => !validBuildInfos.has(buildInfoFile)) .map(async (buildInfoFile) => { log(`Removing buildInfo '${buildInfoFile}'`); await fs_extra_1.default.unlink(buildInfoFile); })); } _getBuildInfoName(solcVersion, solcLongVersion, input) { const json = JSON.stringify({ _format: constants_1.BUILD_INFO_FORMAT_VERSION, solcVersion, solcLongVersion, input, }); return (0, hash_1.createNonCryptographicHashBasedIdentifier)(Buffer.from(json)).toString("hex"); } /** * Returns the absolute path to the artifact that corresponds to the given * name. * * If the name is fully qualified, the path is computed from it. If not, an * artifact that matches the given name is searched in the existing artifacts. * If there is an ambiguity, an error is thrown. * * @throws {HardhatError} with descriptor: * - {@link ERRORS.ARTIFACTS.WRONG_CASING} if the path case doesn't match the one in the filesystem. * - {@link ERRORS.ARTIFACTS.MULTIPLE_FOUND} if there are multiple artifacts matching the given contract name. * - {@link ERRORS.ARTIFACTS.NOT_FOUND} if the artifact is not found. */ async _getArtifactPath(name) { const cached = this._cache?.artifactNameToArtifactPathCache.get(name); if (cached !== undefined) { return cached; } let result; if ((0, contract_names_1.isFullyQualifiedName)(name)) { result = await this._getValidArtifactPathFromFullyQualifiedName(name); } else { const files = await this.getArtifactPaths(); result = this._getArtifactPathFromFiles(name, files); } this._cache?.artifactNameToArtifactPathCache.set(name, result); return result; } _createBuildInfo(id, solcVersion, solcLongVersion, input, output) { return { id, _format: constants_1.BUILD_INFO_FORMAT_VERSION, solcVersion, solcLongVersion, input, output, }; } _createDebugFile(artifactPath, pathToBuildInfo) { const relativePathToBuildInfo = path.relative(path.dirname(artifactPath), pathToBuildInfo); const debugFile = { _format: constants_1.DEBUG_FILE_FORMAT_VERSION, buildInfo: relativePathToBuildInfo, }; return debugFile; } _getArtifactPathsSync() { const cached = this._cache?.artifactPaths; if (cached !== undefined) { return cached; } const paths = (0, fs_utils_1.getAllFilesMatchingSync)(this._artifactsPath, (f) => this._isArtifactPath(f)); const result = paths.sort(); if (this._cache !== undefined) { this._cache.artifactPaths = result; } return result; } /** * Sync version of _getArtifactPath */ _getArtifactPathSync(name) { const cached = this._cache?.artifactNameToArtifactPathCache.get(name); if (cached !== undefined) { return cached; } let result; if ((0, contract_names_1.isFullyQualifiedName)(name)) { result = this._getValidArtifactPathFromFullyQualifiedNameSync(name); } else { const files = this._getArtifactPathsSync(); result = this._getArtifactPathFromFiles(name, files); } this._cache?.artifactNameToArtifactPathCache.set(name, result); return result; } /** * DO NOT DELETE OR CHANGE * * use this.formArtifactPathFromFullyQualifiedName instead * @deprecated until typechain migrates to public version * @see https://github.com/dethcrypto/TypeChain/issues/544 */ _getArtifactPathFromFullyQualifiedName(fullyQualifiedName) { const { sourceName, contractName } = (0, contract_names_1.parseFullyQualifiedName)(fullyQualifiedName); return path.join(this._artifactsPath, sourceName, `${contractName}.json`); } /** * Returns the absolute path to the artifact that corresponds to the given * fully qualified name. * @param fullyQualifiedName The fully qualified name of the contract. * @returns The absolute path to the artifact. * @throws {HardhatError} with descriptor: * - {@link ERRORS.CONTRACT_NAMES.INVALID_FULLY_QUALIFIED_NAME} If the name is not fully qualified. * - {@link ERRORS.ARTIFACTS.WRONG_CASING} If the path case doesn't match the one in the filesystem. * - {@link ERRORS.ARTIFACTS.NOT_FOUND} If the artifact is not found. */ async _getValidArtifactPathFromFullyQualifiedName(fullyQualifiedName) { const artifactPath = this.formArtifactPathFromFullyQualifiedName(fullyQualifiedName); try { const trueCasePath = path.join(this._artifactsPath, await (0, fs_utils_1.getFileTrueCase)(this._artifactsPath, path.relative(this._artifactsPath, artifactPath))); if (artifactPath !== trueCasePath) { throw new errors_1.HardhatError(errors_list_1.ERRORS.ARTIFACTS.WRONG_CASING, { correct: this._getFullyQualifiedNameFromPath(trueCasePath), incorrect: fullyQualifiedName, }); } return trueCasePath; } catch (e) { if (e instanceof fs_utils_1.FileNotFoundError) { return this._handleWrongArtifactForFullyQualifiedName(fullyQualifiedName); } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw e; } } _getAllContractNamesFromFiles(files) { return files.map((file) => { const fqn = this._getFullyQualifiedNameFromPath(file); return (0, contract_names_1.parseFullyQualifiedName)(fqn).contractName; }); } _getAllFullyQualifiedNamesSync() { const paths = this._getArtifactPathsSync(); return paths.map((p) => this._getFullyQualifiedNameFromPath(p)).sort(); } _formatSuggestions(names, contractName) { switch (names.length) { case 0: return ""; case 1: return `Did you mean "${names[0]}"?`; default: return `We found some that were similar: ${names.map((n) => ` * ${n}`).join(os.EOL)} Please replace "${contractName}" for the correct contract name wherever you are trying to read its artifact. `; } } /** * @throws {HardhatError} with a list of similar contract names. */ _handleWrongArtifactForFullyQualifiedName(fullyQualifiedName) { const names = this._getAllFullyQualifiedNamesSync(); const similarNames = this._getSimilarContractNames(fullyQualifiedName, names); throw new errors_1.HardhatError(errors_list_1.ERRORS.ARTIFACTS.NOT_FOUND, { contractName: fullyQualifiedName, suggestion: this._formatSuggestions(similarNames, fullyQualifiedName), }); } /** * @throws {HardhatError} with a list of similar contract names. */ _handleWrongArtifactForContractName(contractName, files) { const names = this._getAllContractNamesFromFiles(files); let similarNames = this._getSimilarContractNames(contractName, names); if (similarNames.length > 1) { similarNames = this._filterDuplicatesAsFullyQualifiedNames(files, similarNames); } throw new errors_1.HardhatError(errors_list_1.ERRORS.ARTIFACTS.NOT_FOUND, { contractName, suggestion: this._formatSuggestions(similarNames, contractName), }); } /** * If the project has these contracts: * - 'contracts/Greeter.sol:Greeter' * - 'contracts/Meeter.sol:Greeter' * - 'contracts/Greater.sol:Greater' * And the user tries to get an artifact with the name 'Greter', then * the suggestions will be 'Greeter', 'Greeter', and 'Greater'. * * We don't want to show duplicates here, so we use FQNs for those. The * suggestions will then be: * - 'contracts/Greeter.sol:Greeter' * - 'contracts/Meeter.sol:Greeter' * - 'Greater' */ _filterDuplicatesAsFullyQualifiedNames(files, similarNames) { const outputNames = []; const groups = similarNames.reduce((obj, cur) => { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions obj[cur] = obj[cur] ? obj[cur] + 1 : 1; return obj; }, {}); for (const [name, occurrences] of Object.entries(groups)) { if (occurrences > 1) { for (const file of files) { if (path.basename(file) === `${name}.json`) { outputNames.push(this._getFullyQualifiedNameFromPath(file)); } } continue; } outputNames.push(name); } return outputNames; } /** * * @param givenName can be FQN or contract name * @param names MUST match type of givenName (i.e. array of FQN's if givenName is FQN) * @returns */ _getSimilarContractNames(givenName, names) { let shortestDistance = constants_1.EDIT_DISTANCE_THRESHOLD; let mostSimilarNames = []; for (const name of names) { const distance = (0, contract_names_1.findDistance)(givenName, name); if (distance < shortestDistance) { shortestDistance = distance; mostSimilarNames = [name]; continue; } if (distance === shortestDistance) { mostSimilarNames.push(name); continue; } } return mostSimilarNames; } _getValidArtifactPathFromFullyQualifiedNameSync(fullyQualifiedName) { const artifactPath = this.formArtifactPathFromFullyQualifiedName(fullyQualifiedName); try { const trueCasePath = path.join(this._artifactsPath, (0, fs_utils_1.getFileTrueCaseSync)(this._artifactsPath, path.relative(this._artifactsPath, artifactPath))); if (artifactPath !== trueCasePath) { throw new errors_1.HardhatError(errors_list_1.ERRORS.ARTIFACTS.WRONG_CASING, { correct: this._getFullyQualifiedNameFromPath(trueCasePath), incorrect: fullyQualifiedName, }); } return trueCasePath; } catch (e) { if (e instanceof fs_utils_1.FileNotFoundError) { return this._handleWrongArtifactForFullyQualifiedName(fullyQualifiedName); } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw e; } } _getDebugFilePath(artifactPath) { return artifactPath.replace(/\.json$/, ".dbg.json"); } /** * Gets the path to the artifact file for the given contract name. * @throws {HardhatError} with descriptor: * - {@link ERRORS.ARTIFACTS.NOT_FOUND} if there are no artifacts matching the given contract name. * - {@link ERRORS.ARTIFACTS.MULTIPLE_FOUND} if there are multiple artifacts matching the given contract name. */ _getArtifactPathFromFiles(contractName, files) { const matchingFiles = files.filter((file) => { return path.basename(file) === `${contractName}.json`; }); if (matchingFiles.length === 0) { return this._handleWrongArtifactForContractName(contractName, files); } if (matchingFiles.length > 1) { const candidates = matchingFiles.map((file) => this._getFullyQualifiedNameFromPath(file)); throw new errors_1.HardhatError(errors_list_1.ERRORS.ARTIFACTS.MULTIPLE_FOUND, { contractName, candidates: candidates.join(os.EOL), }); } return matchingFiles[0]; } /** * Returns the FQN of a contract giving the absolute path to its artifact. * * For example, given a path like * `/path/to/project/artifacts/contracts/Foo.sol/Bar.json`, it'll return the * FQN `contracts/Foo.sol:Bar` */ _getFullyQualifiedNameFromPath(absolutePath) { const sourceName = (0, source_names_1.replaceBackslashes)(path.relative(this._artifactsPath, path.dirname(absolutePath))); const contractName = path.basename(absolutePath).replace(".json", ""); return (0, contract_names_1.getFullyQualifiedName)(sourceName, contractName); } /** * Remove the artifact file and its debug file. */ async _removeArtifactFiles(artifactPath) { await fs_extra_1.default.remove(artifactPath); const debugFilePath = this._getDebugFilePath(artifactPath); await fs_extra_1.default.remove(debugFilePath); } /** * Given the path to a debug file, returns the absolute path to its * corresponding build info file if it exists, or undefined otherwise. */ async _getBuildInfoFromDebugFile(debugFilePath) { if (await fs_extra_1.default.pathExists(debugFilePath)) { const { buildInfo } = await fs_extra_1.default.readJson(debugFilePath); return path.resolve(path.dirname(debugFilePath), buildInfo); } return undefined; } /** * Sync version of _getBuildInfoFromDebugFile */ _getBuildInfoFromDebugFileSync(debugFilePath) { if (fs_extra_1.default.pathExistsSync(debugFilePath)) { const { buildInfo } = fs_extra_1.default.readJsonSync(debugFilePath); return path.resolve(path.dirname(debugFilePath), buildInfo); } return undefined; } _isArtifactPath(file) { return (file.endsWith(".json") && file !== path.join(this._artifactsPath, "package.json") && !file.startsWith(path.join(this._artifactsPath, constants_1.BUILD_INFO_DIR_NAME)) && !file.endsWith(".dbg.json")); } } exports.Artifacts = Artifacts; /** * Retrieves an artifact for the given `contractName` from the compilation output. * * @param sourceName The contract's source name. * @param contractName the contract's name. * @param contractOutput the contract's compilation output as emitted by `solc`. */ function getArtifactFromContractOutput(sourceName, contractName, contractOutput) { const evmBytecode = contractOutput.evm?.bytecode; let bytecode = evmBytecode?.object ?? ""; if (bytecode.slice(0, 2).toLowerCase() !== "0x") { bytecode = `0x${bytecode}`; } const evmDeployedBytecode = contractOutput.evm?.deployedBytecode; let deployedBytecode = evmDeployedBytecode?.object ?? ""; if (deployedBytecode.slice(0, 2).toLowerCase() !== "0x") { deployedBytecode = `0x${deployedBytecode}`; } const linkReferences = evmBytecode?.linkReferences ?? {}; const deployedLinkReferences = evmDeployedBytecode?.linkReferences ?? {}; return { _format: constants_1.ARTIFACT_FORMAT_VERSION, contractName, sourceName, abi: contractOutput.abi, bytecode, deployedBytecode, linkReferences, deployedLinkReferences, }; } exports.getArtifactFromContractOutput = getArtifactFromContractOutput; //# sourceMappingURL=artifacts.js.map