Skip to content

feat: add fallible methods to artifact manager#8095

Closed
Wodann wants to merge 1 commit intomainfrom
feat/fallible-artifact-methods
Closed

feat: add fallible methods to artifact manager#8095
Wodann wants to merge 1 commit intomainfrom
feat/fallible-artifact-methods

Conversation

@Wodann
Copy link
Copy Markdown
Member

@Wodann Wodann commented Mar 30, 2026

  • Because this PR includes a bug fix, relevant tests have been included.
  • Because this PR includes a new feature, the change was previously discussed on an Issue or with someone from the team.
  • I didn't do anything of this.

The existing ArtifactManager 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.

The Openzeppelin Contracts suffer from this problem, as they have a test suite that auto-generates test vectors for as many artifacts as possible (for more info, see 1 & 2.

As seen in this profiling run, Hardhat's editDistance function takes the 4th largest amount of time.

image

In total, the #getFullyQualifiedName function is consuming about 6.3% of the OZ test time:

image

There are several reasons for this becoming a hot path despite being error handling code:

  1. OpenZeppelin causes 6006 calls to #getSimilarStrings due to missing artifacts
  2. #getSimilarStrings is called with 764+ names to compare against
  3. editDistance is called with names ranging in length from 3 to 39.
  4. editDistance uses Levenshtein algorithm to determine the edit distance. The standard implementation has a time complexity of O(m×n)

After optimisation, we're almost spending no time (i.e. <0.1%) on this code path anymore:

image

The runtime of npx hardhat test mocha --no-compile went from 1:31.84 to 1:22.23, a reduction of 9.61s (-10.5%).

The accompanying change in OpenZeppelin Contracts can be found here.

This PR also speeds up the artifactExists implementation shipped with Hardhat by not requiring the generation of the (later discarded) error for non-existent artifacts.

Considerations

  • I considered alternatives to the Levenshtein algorithm, like Jaro-Winkler Similarity which has O(m+n) time complexity; but given that OpenZeppelin never actually uses the generated error, I thought it would be a better alternative to introduce APIs that signal the possibility of the file not existing.
  • An alternative optimisation would be to change the #throwNotFoundError to return a cheap intermediate error type that needs to be converted to a full, expensive-to-calculate Hardhat error (with suggestion) at a later time. That intermediate error type would contain the following information:
    {
      allFullyQualifiedNames: ReadonlySet<string>;
      bareNameToFullyQualifiedNameMap: Map<string, ReadonlySet<string>>;
    }
    Plugins (or users) that directly interface with ArtifactManager as a "library API" would then have control over whether to use the error or not. End-user application like Hardhat's CLI would then invoke the error conversion on the intermediate error type.

@Wodann Wodann requested a review from alcuadrado March 30, 2026 18:15
@Wodann Wodann self-assigned this Mar 30, 2026
Copilot AI review requested due to automatic review settings March 30, 2026 18:15
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 30, 2026

⚠️ No Changeset found

Latest commit: edf131d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds fallible ArtifactManager APIs to avoid expensive error construction (e.g., editDistance-based suggestions) on common “missing artifact” paths.

Changes:

  • Introduces tryToReadArtifact and tryToGetArtifactPath returning undefined when the artifact doesn’t exist.
  • Refactors artifactExists to use tryToGetArtifactPath instead of relying on exceptions.
  • Updates LazyArtifactManager forwarding and test mocks to satisfy the extended interface.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
v-next/hardhat/test/test-helpers/mock-artifact-manager.ts Adds fallible methods to the Hardhat test mock.
v-next/hardhat/src/types/artifacts.ts Extends the public ArtifactManager interface with fallible APIs and docs.
v-next/hardhat/src/internal/builtin-plugins/artifacts/hook-handlers/hre.ts Forwards new fallible methods through the lazy HRE artifact manager.
v-next/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts Implements fallible path/artifact lookup and optimizes artifactExists.
v-next/hardhat-ethers/test/helpers/artifact-manager-mock.ts Adds fallible tryToReadArtifact and stubs tryToGetArtifactPath in ethers tests.

Comment on lines +83 to +84
* @param contractNameOrFullyQualifiedName The name of the contract.
* It can be a contract bare contract name (e.g. "Token") if it's
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phrase 'a contract bare contract name' is grammatically incorrect and a bit confusing. Consider changing it to 'a bare contract name' to match typical terminology and improve readability.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +79
_contractNameOrFullyQualifiedName: string,
): Promise<string | undefined> {
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR,
{
message: "Not implemented in MockArtifactManager",
},
);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tryToGetArtifactPath is intended to be a fallible, non-throwing API for the common 'missing artifact' case. Having the mock always throw defeats the purpose and can break tests that switch to these new methods for performance. Consider implementing minimal behavior (e.g., return undefined when missing and a deterministic fake path when present) or at least returning undefined unconditionally if path resolution is irrelevant in these tests.

Suggested change
_contractNameOrFullyQualifiedName: string,
): Promise<string | undefined> {
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR,
{
message: "Not implemented in MockArtifactManager",
},
);
contractNameOrFullyQualifiedName: string,
): Promise<string | undefined> {
const artifact = this.#artifacts.get(contractNameOrFullyQualifiedName);
if (artifact === undefined) {
return undefined;
}
// Return a deterministic fake path for tests when the artifact exists.
return `${contractNameOrFullyQualifiedName}.json`;

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +88
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR,
{
message: "Not implemented in MockArtifactManager",
},
);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the Hardhat test mock: a tryTo* API should not throw for the normal 'not found' flow. In this mock, consider mapping contractNameOrFullyQualifiedName to the existing #artifactsPaths entries and returning undefined when absent, to better reflect the production contract and avoid test brittleness.

Suggested change
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR,
{
message: "Not implemented in MockArtifactManager",
},
);
const artifactFileName = this.#artifactsPaths.get(
contractNameOrFullyQualifiedName,
);
if (artifactFileName === undefined) {
return undefined;
}
return artifactFileName;

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +97
tryToReadArtifact<
ContractNameT extends StringWithArtifactContractNamesAutocompletion,
>(
contractNameOrFullyQualifiedName: ContractNameT,
): Promise<GetArtifactByName<ContractNameT> | undefined>;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding methods to the exported ArtifactManager interface is a breaking change for downstream/custom implementations (they must implement the new methods to typecheck). If external implementers are expected, consider providing a backwards-compatible path (e.g., a separate extended interface, or making these optional with clear fallback behavior) to reduce upgrade friction.

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +122
tryToGetArtifactPath(
contractNameOrFullyQualifiedName: string,
): Promise<string | undefined>;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding methods to the exported ArtifactManager interface is a breaking change for downstream/custom implementations (they must implement the new methods to typecheck). If external implementers are expected, consider providing a backwards-compatible path (e.g., a separate extended interface, or making these optional with clear fallback behavior) to reduce upgrade friction.

Copilot uses AI. Check for mistakes.
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`.
@Wodann Wodann force-pushed the feat/fallible-artifact-methods branch from d3f371a to edf131d Compare April 2, 2026 18:46
@Wodann
Copy link
Copy Markdown
Member Author

Wodann commented Apr 13, 2026

@alcuadrado does #8122 make this PR obsolete?

@alcuadrado
Copy link
Copy Markdown
Member

Oops, sorry I forgot to mention that. Yes. We decided to go for the option that doesn't modify the API. Once again, amazing finding!

@alcuadrado alcuadrado closed this Apr 13, 2026
@Wodann Wodann deleted the feat/fallible-artifact-methods branch May 6, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants