From edf131d602753f7229002d84b4d74415e9f9836c Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 30 Mar 2026 17:53:57 +0000 Subject: [PATCH] feat: add fallible methods to artifact manager The existing methods always throw, which can be expensive when failures are common, especially if the thrown error is not even used. The new methods avoid throwing and instead return `undefined`. --- .../test/helpers/artifact-manager-mock.ts | 28 ++++++ .../artifacts/artifact-manager.ts | 98 ++++++++++++++----- .../artifacts/hook-handlers/hre.ts | 18 ++++ packages/hardhat/src/types/artifacts.ts | 35 +++++++ .../test-helpers/mock-artifact-manager.ts | 28 ++++++ 5 files changed, 183 insertions(+), 24 deletions(-) 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 {