diff --git a/packages/hardhat-ethers/test/helpers/artifact-manager-mock.ts b/packages/hardhat-ethers/test/helpers/artifact-manager-mock.ts index 2e1b15b8cbe..d9eef4ad789 100644 --- a/packages/hardhat-ethers/test/helpers/artifact-manager-mock.ts +++ b/packages/hardhat-ethers/test/helpers/artifact-manager-mock.ts @@ -49,6 +49,23 @@ export class MockArtifactManager implements ArtifactManager { return artifact; } + public async tryToReadArtifact( + contractNameOrFullyQualifiedName: ContractNameT, + ): Promise | undefined> { + const artifactFileName = this.#artifactsPaths.get( + contractNameOrFullyQualifiedName, + ); + + if (artifactFileName === undefined) { + return undefined; + } + + const artifact = (await import(`./artifacts/${artifactFileName}.ts`)) + .CONTRACT; + + return artifact; + } + public async getArtifactPath( _contractNameOrFullyQualifiedName: string, ): Promise { @@ -60,6 +77,17 @@ export class MockArtifactManager implements ArtifactManager { ); } + public async tryToGetArtifactPath( + contractNameOrFullyQualifiedName: string, + ): Promise { + throw new HardhatError( + HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR, + { + message: "Not implemented in MockArtifactManager", + }, + ); + } + public async artifactExists( _contractNameOrFullyQualifiedName: string, ): Promise { diff --git a/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts b/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts index c13f5d46475..a6ec7f368a7 100644 --- a/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts +++ b/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts @@ -60,6 +60,22 @@ export class ArtifactManagerImplementation implements ArtifactManager { return readJsonFile(artifactPath); } + public async tryToReadArtifact< + ContractNameT extends StringWithArtifactContractNamesAutocompletion, + >( + contractNameOrFullyQualifiedName: ContractNameT, + ): Promise | undefined> { + const artifactPath = await this.tryToGetArtifactPath( + contractNameOrFullyQualifiedName, + ); + + if (artifactPath === undefined) { + return undefined; + } + + return readJsonFile(artifactPath); + } + public async getArtifactPath( contractNameOrFullyQualifiedName: string, ): Promise { @@ -78,25 +94,35 @@ export class ArtifactManagerImplementation implements ArtifactManager { return artifactPath; } - public async artifactExists( + public async tryToGetArtifactPath( contractNameOrFullyQualifiedName: string, - ): Promise { - try { - // This throw if the artifact doesn't exist - await this.getArtifactPath(contractNameOrFullyQualifiedName); - - return true; - } catch (error) { - if (HardhatError.isHardhatError(error)) { - if ( - error.number === HardhatError.ERRORS.CORE.ARTIFACTS.NOT_FOUND.number - ) { - return false; - } - } + ): Promise { + const fqn = await this.#tryToGetFullyQualifiedName( + contractNameOrFullyQualifiedName, + ); - throw error; + if (typeof fqn === "string") { + const { fullyQualifiedNameToArtifactPath } = await this.#getFsData(); + + const artifactPath = fullyQualifiedNameToArtifactPath.get(fqn); + assertHardhatInvariant( + artifactPath !== undefined, + "Artifact path should be defined", + ); + + return artifactPath; } + + return undefined; + } + + public async artifactExists( + contractNameOrFullyQualifiedName: string, + ): Promise { + const artifactPath = await this.tryToGetArtifactPath( + contractNameOrFullyQualifiedName, + ); + return artifactPath !== undefined; } public async getBuildInfoId( @@ -159,6 +185,32 @@ export class ArtifactManagerImplementation implements ArtifactManager { async #getFullyQualifiedName( contractNameOrFullyQualifiedName: string, ): Promise { + const fqn = await this.#tryToGetFullyQualifiedName( + contractNameOrFullyQualifiedName, + ); + + if (typeof fqn === "string") { + return fqn; + } + + const { allFullyQualifiedNames, bareNameToFullyQualifiedNameMap } = fqn; + + this.#throwNotFoundError( + contractNameOrFullyQualifiedName, + bareNameToFullyQualifiedNameMap.keys(), + allFullyQualifiedNames, + ); + } + + async #tryToGetFullyQualifiedName( + contractNameOrFullyQualifiedName: string, + ): Promise< + | string + | { + allFullyQualifiedNames: ReadonlySet; + bareNameToFullyQualifiedNameMap: Map>; + } + > { const { bareNameToFullyQualifiedNameMap, allFullyQualifiedNames } = await this.#getFsData(); @@ -167,11 +219,10 @@ export class ArtifactManagerImplementation implements ArtifactManager { return contractNameOrFullyQualifiedName; } - this.#throwNotFoundError( - contractNameOrFullyQualifiedName, - bareNameToFullyQualifiedNameMap.keys(), + return { allFullyQualifiedNames, - ); + bareNameToFullyQualifiedNameMap, + }; } const fqns = bareNameToFullyQualifiedNameMap.get( @@ -179,11 +230,10 @@ export class ArtifactManagerImplementation implements ArtifactManager { ); if (fqns === undefined || fqns.size === 0) { - this.#throwNotFoundError( - contractNameOrFullyQualifiedName, - bareNameToFullyQualifiedNameMap.keys(), + return { + bareNameToFullyQualifiedNameMap, allFullyQualifiedNames, - ); + }; } if (fqns.size !== 1) { diff --git a/packages/hardhat/src/internal/builtin-plugins/artifacts/hook-handlers/hre.ts b/packages/hardhat/src/internal/builtin-plugins/artifacts/hook-handlers/hre.ts index e60e827ccd0..4ea30485e9c 100644 --- a/packages/hardhat/src/internal/builtin-plugins/artifacts/hook-handlers/hre.ts +++ b/packages/hardhat/src/internal/builtin-plugins/artifacts/hook-handlers/hre.ts @@ -23,6 +23,15 @@ class LazyArtifactManager implements ArtifactManager { return artifactManager.readArtifact(contractNameOrFullyQualifiedName); } + public async tryToReadArtifact< + ContractNameT extends StringWithArtifactContractNamesAutocompletion, + >( + contractNameOrFullyQualifiedName: ContractNameT, + ): Promise | undefined> { + const artifactManager = await this.#getArtifactManager(); + return artifactManager.tryToReadArtifact(contractNameOrFullyQualifiedName); + } + public async getArtifactPath( contractNameOrFullyQualifiedName: string, ): Promise { @@ -30,6 +39,15 @@ class LazyArtifactManager implements ArtifactManager { return artifactManager.getArtifactPath(contractNameOrFullyQualifiedName); } + public async tryToGetArtifactPath( + contractNameOrFullyQualifiedName: string, + ): Promise { + const artifactManager = await this.#getArtifactManager(); + return artifactManager.tryToGetArtifactPath( + contractNameOrFullyQualifiedName, + ); + } + public async artifactExists( contractNameOrFullyQualifiedName: string, ): Promise { diff --git a/packages/hardhat/src/types/artifacts.ts b/packages/hardhat/src/types/artifacts.ts index a569bfa96b7..bb99935fe60 100644 --- a/packages/hardhat/src/types/artifacts.ts +++ b/packages/hardhat/src/types/artifacts.ts @@ -75,6 +75,27 @@ export interface ArtifactManager { contractNameOrFullyQualifiedName: ContractNameT, ): Promise>; + /** + * Tries to read an artifact, returning `undefined` if it doesn't exist. + * + * Use this instead of `readArtifact` if you want to avoid constructing an error when the artifact doesn't exist, which can be expensive if it happens often. + * + * @param contractNameOrFullyQualifiedName The name of the contract. + * It can be a contract bare contract name (e.g. "Token") if it's + * unique in your project, or a fully qualified contract name + * (e.g. "contract/token.sol:Token") otherwise. TypeScript's language server + * autocompletes the names of the contracts that have already been built. If + * your contract name isn't in the list, you can still use it, and/or run + * `hardhat build` to get it in the list. + * @throws Throws an error if a non-unique contract name is used, + * indicating which fully qualified names can be used instead. + */ + tryToReadArtifact< + ContractNameT extends StringWithArtifactContractNamesAutocompletion, + >( + contractNameOrFullyQualifiedName: ContractNameT, + ): Promise | undefined>; + /** * Returns the absolute path to the given artifact. * @@ -86,6 +107,20 @@ export interface ArtifactManager { */ getArtifactPath(contractNameOrFullyQualifiedName: string): Promise; + /** + * Tries to get the absolute path to the given artifact, returning `undefined` if it doesn't exist. + * + * Use this instead of `getArtifactPath` if you want to avoid constructing an error when the artifact doesn't exist, which can be expensive if it happens often. + * + * @param contractNameOrFullyQualifiedName The name or fully qualified name + * of the contract. + * @throws Throws an error if a non-unique contract name is used, + * indicating which fully qualified names can be used instead. + */ + tryToGetArtifactPath( + contractNameOrFullyQualifiedName: string, + ): Promise; + /** * Returns true if an artifact exists. * diff --git a/packages/hardhat/test/test-helpers/mock-artifact-manager.ts b/packages/hardhat/test/test-helpers/mock-artifact-manager.ts index a0fc5aca512..3e898941977 100644 --- a/packages/hardhat/test/test-helpers/mock-artifact-manager.ts +++ b/packages/hardhat/test/test-helpers/mock-artifact-manager.ts @@ -40,6 +40,23 @@ export class MockArtifactManager implements ArtifactManager { return artifact as GetArtifactByName; } + public async tryToReadArtifact< + ContractNameT extends StringWithArtifactContractNamesAutocompletion, + >( + contractNameOrFullyQualifiedName: ContractNameT, + ): Promise | undefined> { + const artifact = this.#artifacts.get(contractNameOrFullyQualifiedName); + + if (artifact === undefined) { + return undefined; + } + + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- + We are asserting that the artifact is of the correct type, which won't be + really used during tests. */ + return artifact as GetArtifactByName; + } + public async getArtifactPath( _contractNameOrFullyQualifiedName: string, ): Promise { @@ -51,6 +68,17 @@ export class MockArtifactManager implements ArtifactManager { ); } + public async tryToGetArtifactPath( + _contractNameOrFullyQualifiedName: string, + ): Promise { + throw new HardhatError( + HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR, + { + message: "Not implemented in MockArtifactManager", + }, + ); + } + public async artifactExists( _contractNameOrFullyQualifiedName: string, ): Promise {