Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/hardhat-ethers/test/helpers/artifact-manager-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@
return artifact;
}

public async tryToReadArtifact<ContractNameT extends string>(
contractNameOrFullyQualifiedName: ContractNameT,
): Promise<GetArtifactByName<ContractNameT> | 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<string> {
Expand All @@ -60,6 +77,17 @@
);
}

public async tryToGetArtifactPath(
contractNameOrFullyQualifiedName: string,

Check failure on line 81 in packages/hardhat-ethers/test/helpers/artifact-manager-mock.ts

View workflow job for this annotation

GitHub Actions / [hardhat-ethers] lint

'contractNameOrFullyQualifiedName' is defined but never used. Allowed unused args must match /^_/u
): Promise<string | undefined> {
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR,
{
message: "Not implemented in MockArtifactManager",
},
);
Comment on lines +83 to +88
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.
}

public async artifactExists(
_contractNameOrFullyQualifiedName: string,
): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ export class ArtifactManagerImplementation implements ArtifactManager {
return readJsonFile(artifactPath);
}

public async tryToReadArtifact<
ContractNameT extends StringWithArtifactContractNamesAutocompletion,
>(
contractNameOrFullyQualifiedName: ContractNameT,
): Promise<GetArtifactByName<ContractNameT> | undefined> {
const artifactPath = await this.tryToGetArtifactPath(
contractNameOrFullyQualifiedName,
);

if (artifactPath === undefined) {
return undefined;
}

return readJsonFile(artifactPath);
}

public async getArtifactPath(
contractNameOrFullyQualifiedName: string,
): Promise<string> {
Expand All @@ -78,25 +94,35 @@ export class ArtifactManagerImplementation implements ArtifactManager {
return artifactPath;
}

public async artifactExists(
public async tryToGetArtifactPath(
contractNameOrFullyQualifiedName: string,
): Promise<boolean> {
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<string | undefined> {
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<boolean> {
const artifactPath = await this.tryToGetArtifactPath(
contractNameOrFullyQualifiedName,
);
return artifactPath !== undefined;
}

public async getBuildInfoId(
Expand Down Expand Up @@ -159,6 +185,32 @@ export class ArtifactManagerImplementation implements ArtifactManager {
async #getFullyQualifiedName(
contractNameOrFullyQualifiedName: string,
): Promise<string> {
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<string>;
bareNameToFullyQualifiedNameMap: Map<string, ReadonlySet<string>>;
}
> {
const { bareNameToFullyQualifiedNameMap, allFullyQualifiedNames } =
await this.#getFsData();

Expand All @@ -167,23 +219,21 @@ export class ArtifactManagerImplementation implements ArtifactManager {
return contractNameOrFullyQualifiedName;
}

this.#throwNotFoundError(
contractNameOrFullyQualifiedName,
bareNameToFullyQualifiedNameMap.keys(),
return {
allFullyQualifiedNames,
);
bareNameToFullyQualifiedNameMap,
};
}

const fqns = bareNameToFullyQualifiedNameMap.get(
contractNameOrFullyQualifiedName,
);

if (fqns === undefined || fqns.size === 0) {
this.#throwNotFoundError(
contractNameOrFullyQualifiedName,
bareNameToFullyQualifiedNameMap.keys(),
return {
bareNameToFullyQualifiedNameMap,
allFullyQualifiedNames,
);
};
}

if (fqns.size !== 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,31 @@ class LazyArtifactManager implements ArtifactManager {
return artifactManager.readArtifact(contractNameOrFullyQualifiedName);
}

public async tryToReadArtifact<
ContractNameT extends StringWithArtifactContractNamesAutocompletion,
>(
contractNameOrFullyQualifiedName: ContractNameT,
): Promise<GetArtifactByName<ContractNameT> | undefined> {
const artifactManager = await this.#getArtifactManager();
return artifactManager.tryToReadArtifact(contractNameOrFullyQualifiedName);
}

public async getArtifactPath(
contractNameOrFullyQualifiedName: string,
): Promise<string> {
const artifactManager = await this.#getArtifactManager();
return artifactManager.getArtifactPath(contractNameOrFullyQualifiedName);
}

public async tryToGetArtifactPath(
contractNameOrFullyQualifiedName: string,
): Promise<string | undefined> {
const artifactManager = await this.#getArtifactManager();
return artifactManager.tryToGetArtifactPath(
contractNameOrFullyQualifiedName,
);
}

public async artifactExists(
contractNameOrFullyQualifiedName: string,
): Promise<boolean> {
Expand Down
35 changes: 35 additions & 0 deletions packages/hardhat/src/types/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ export interface ArtifactManager {
contractNameOrFullyQualifiedName: ContractNameT,
): Promise<GetArtifactByName<ContractNameT>>;

/**
* 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
Comment on lines +83 to +84
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.
* 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<GetArtifactByName<ContractNameT> | undefined>;
Comment on lines +93 to +97
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.

/**
* Returns the absolute path to the given artifact.
*
Expand All @@ -86,6 +107,20 @@ export interface ArtifactManager {
*/
getArtifactPath(contractNameOrFullyQualifiedName: string): Promise<string>;

/**
* 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<string | undefined>;
Comment on lines +120 to +122
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.

/**
* Returns true if an artifact exists.
*
Expand Down
28 changes: 28 additions & 0 deletions packages/hardhat/test/test-helpers/mock-artifact-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ export class MockArtifactManager implements ArtifactManager {
return artifact as GetArtifactByName<ContractNameT>;
}

public async tryToReadArtifact<
ContractNameT extends StringWithArtifactContractNamesAutocompletion,
>(
contractNameOrFullyQualifiedName: ContractNameT,
): Promise<GetArtifactByName<ContractNameT> | 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<ContractNameT>;
}

public async getArtifactPath(
_contractNameOrFullyQualifiedName: string,
): Promise<string> {
Expand All @@ -51,6 +68,17 @@ export class MockArtifactManager implements ArtifactManager {
);
}

public async tryToGetArtifactPath(
_contractNameOrFullyQualifiedName: string,
): Promise<string | undefined> {
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.NOT_IMPLEMENTED_ERROR,
{
message: "Not implemented in MockArtifactManager",
},
);
Comment on lines +72 to +79
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.
}

public async artifactExists(
_contractNameOrFullyQualifiedName: string,
): Promise<boolean> {
Expand Down
Loading