hardhat 3.12.0 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts +3 -2
  3. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts.map +1 -1
  4. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js +16 -7
  5. package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js.map +1 -1
  6. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.d.ts +7 -0
  7. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.d.ts.map +1 -1
  8. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.js +10 -0
  9. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.js.map +1 -1
  10. package/dist/src/internal/builtin-plugins/gas-analytics/index.d.ts.map +1 -1
  11. package/dist/src/internal/builtin-plugins/gas-analytics/index.js +12 -0
  12. package/dist/src/internal/builtin-plugins/gas-analytics/index.js.map +1 -1
  13. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts +5 -4
  14. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts.map +1 -1
  15. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js +56 -14
  16. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js.map +1 -1
  17. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts +2 -1
  18. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts.map +1 -1
  19. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js +18 -7
  20. package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js.map +1 -1
  21. package/dist/src/internal/builtin-plugins/network-manager/chain-descriptors.d.ts.map +1 -1
  22. package/dist/src/internal/builtin-plugins/network-manager/chain-descriptors.js +0 -17
  23. package/dist/src/internal/builtin-plugins/network-manager/chain-descriptors.js.map +1 -1
  24. package/dist/src/internal/builtin-plugins/network-manager/type-validation.d.ts.map +1 -1
  25. package/dist/src/internal/builtin-plugins/network-manager/type-validation.js +3 -1
  26. package/dist/src/internal/builtin-plugins/network-manager/type-validation.js.map +1 -1
  27. package/dist/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.d.ts.map +1 -1
  28. package/dist/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.js +10 -4
  29. package/dist/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.js.map +1 -1
  30. package/dist/src/internal/builtin-plugins/solidity/type-extensions.d.ts +2 -2
  31. package/dist/src/internal/builtin-plugins/solidity-test/helpers.d.ts.map +1 -1
  32. package/dist/src/internal/builtin-plugins/solidity-test/helpers.js +2 -2
  33. package/dist/src/internal/builtin-plugins/solidity-test/helpers.js.map +1 -1
  34. package/dist/src/internal/builtin-plugins/solidity-test/reporter.d.ts.map +1 -1
  35. package/dist/src/internal/builtin-plugins/solidity-test/reporter.js +5 -3
  36. package/dist/src/internal/builtin-plugins/solidity-test/reporter.js.map +1 -1
  37. package/dist/src/internal/cli/error-handling/error-handler.d.ts.map +1 -0
  38. package/dist/src/internal/cli/{error-handler.js → error-handling/error-handler.js} +13 -2
  39. package/dist/src/internal/cli/error-handling/error-handler.js.map +1 -0
  40. package/dist/src/internal/cli/error-handling/native-binding-error.d.ts +18 -0
  41. package/dist/src/internal/cli/error-handling/native-binding-error.d.ts.map +1 -0
  42. package/dist/src/internal/cli/error-handling/native-binding-error.js +57 -0
  43. package/dist/src/internal/cli/error-handling/native-binding-error.js.map +1 -0
  44. package/dist/src/internal/cli/hhu.js +1 -1
  45. package/dist/src/internal/cli/hhu.js.map +1 -1
  46. package/dist/src/internal/cli/main.js +1 -1
  47. package/dist/src/internal/cli/main.js.map +1 -1
  48. package/dist/src/internal/constants.d.ts +10 -0
  49. package/dist/src/internal/constants.d.ts.map +1 -1
  50. package/dist/src/internal/constants.js +10 -0
  51. package/dist/src/internal/constants.js.map +1 -1
  52. package/dist/src/internal/core/config.d.ts +14 -1
  53. package/dist/src/internal/core/config.d.ts.map +1 -1
  54. package/dist/src/internal/core/config.js +13 -5
  55. package/dist/src/internal/core/config.js.map +1 -1
  56. package/dist/src/internal/core/configuration-variables.d.ts.map +1 -1
  57. package/dist/src/internal/core/configuration-variables.js +3 -1
  58. package/dist/src/internal/core/configuration-variables.js.map +1 -1
  59. package/dist/src/types/config.d.ts +11 -0
  60. package/dist/src/types/config.d.ts.map +1 -1
  61. package/package.json +3 -3
  62. package/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts +26 -4
  63. package/src/internal/builtin-plugins/gas-analytics/helpers/utils.ts +15 -0
  64. package/src/internal/builtin-plugins/gas-analytics/index.ts +13 -0
  65. package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +90 -13
  66. package/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.ts +29 -6
  67. package/src/internal/builtin-plugins/network-manager/chain-descriptors.ts +0 -17
  68. package/src/internal/builtin-plugins/network-manager/type-validation.ts +5 -1
  69. package/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts +25 -5
  70. package/src/internal/builtin-plugins/solidity/type-extensions.ts +2 -2
  71. package/src/internal/builtin-plugins/solidity-test/helpers.ts +6 -2
  72. package/src/internal/builtin-plugins/solidity-test/reporter.ts +6 -7
  73. package/src/internal/cli/{error-handler.ts → error-handling/error-handler.ts} +20 -3
  74. package/src/internal/cli/error-handling/native-binding-error.ts +78 -0
  75. package/src/internal/cli/hhu.ts +1 -1
  76. package/src/internal/cli/main.ts +1 -1
  77. package/src/internal/constants.ts +11 -0
  78. package/src/internal/core/config.ts +35 -2
  79. package/src/internal/core/configuration-variables.ts +3 -1
  80. package/src/types/config.ts +12 -0
  81. package/templates/01-node-test-runner-viem/package.json +1 -1
  82. package/templates/02-mocha-ethers/package.json +1 -1
  83. package/templates/03-minimal/package.json +1 -1
  84. package/dist/src/internal/cli/error-handler.d.ts.map +0 -1
  85. package/dist/src/internal/cli/error-handler.js.map +0 -1
  86. /package/dist/src/internal/cli/{error-handler.d.ts → error-handling/error-handler.d.ts} +0 -0
@@ -13,6 +13,7 @@ import {
13
13
  remove,
14
14
  writeJsonFile,
15
15
  } from "@nomicfoundation/hardhat-utils/fs";
16
+ import { isObject } from "@nomicfoundation/hardhat-utils/lang";
16
17
  import { sanitizeFilename } from "@nomicfoundation/hardhat-utils/path";
17
18
 
18
19
  import {
@@ -20,10 +21,18 @@ import {
20
21
  parseFullyQualifiedName,
21
22
  } from "../../../utils/contract-names.js";
22
23
 
23
- import { formatSectionHeader, getUserFqn } from "./helpers/utils.js";
24
+ import {
25
+ formatSectionHeader,
26
+ getUserFqn,
27
+ isWithinTolerance,
28
+ } from "./helpers/utils.js";
24
29
 
25
30
  export const SNAPSHOT_CHEATCODES_DIR = "snapshots";
26
31
 
32
+ // Snapshot cheatcode values are uint256 decimal strings, so a stored value
33
+ // that doesn't match this can only come from a hand-edited or corrupted file.
34
+ const SNAPSHOT_VALUE_REGEX = /^\d+$/;
35
+
27
36
  export interface RenamedSnapshotGroup {
28
37
  original: string;
29
38
  sanitized: string;
@@ -64,8 +73,11 @@ export interface SnapshotCheatcode {
64
73
  export interface SnapshotCheatcodeChange {
65
74
  group: string;
66
75
  name: string;
67
- expected: number;
68
- actual: number;
76
+ // Raw snapshot values: uint256 decimal strings, kept as strings because
77
+ // they can exceed Number.MAX_SAFE_INTEGER and coercing them to `number`
78
+ // would round them.
79
+ expected: string;
80
+ actual: string;
69
81
  source: string;
70
82
  }
71
83
 
@@ -73,6 +85,10 @@ export interface SnapshotCheatcodesComparison {
73
85
  added: SnapshotCheatcode[];
74
86
  removed: SnapshotCheatcode[];
75
87
  changed: SnapshotCheatcodeChange[];
88
+ // Diffs within the allowed tolerance: they don't fail the check. They are
89
+ // included in the result for programmatic inspection, but are not shown in
90
+ // the default console output.
91
+ tolerated: SnapshotCheatcodeChange[];
76
92
  }
77
93
 
78
94
  export interface SnapshotCheatcodesCheckResult {
@@ -262,9 +278,9 @@ export async function readSnapshotCheatcodes(
262
278
  const snapshotGroup = entry.slice(0, -5); // remove .json extension
263
279
  const snapshotCheatcodesPath = getSnapshotCheatcodesPath(basePath, entry);
264
280
 
265
- let snapshot: Record<string, string>;
281
+ let parsedSnapshot: unknown;
266
282
  try {
267
- snapshot = await readJsonFile(snapshotCheatcodesPath);
283
+ parsedSnapshot = await readJsonFile(snapshotCheatcodesPath);
268
284
  } catch (error) {
269
285
  ensureError(error);
270
286
  throw new HardhatError(
@@ -274,6 +290,39 @@ export async function readSnapshotCheatcodes(
274
290
  );
275
291
  }
276
292
 
293
+ if (!isObject(parsedSnapshot)) {
294
+ throw new HardhatError(
295
+ HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR,
296
+ {
297
+ snapshotsPath: snapshotCheatcodesPath,
298
+ error: `Invalid snapshot file: expected a JSON object, got ${JSON.stringify(parsedSnapshot)}`,
299
+ },
300
+ );
301
+ }
302
+
303
+ // Snapshot cheatcode values are always machine-generated uint256
304
+ // decimal strings (vm.snapshotValue/vm.snapshotGas*), so anything else
305
+ // can only come from a hand-edited or corrupted file. This also rejects
306
+ // hand-written unquoted JSON numbers (`100` instead of `"100"`).
307
+ const snapshot: Record<string, string> = {};
308
+ for (const [name, value] of Object.entries(parsedSnapshot)) {
309
+ if (
310
+ typeof value !== "string" ||
311
+ !SNAPSHOT_VALUE_REGEX.test(value) ||
312
+ BigInt(value) >= 2n ** 256n
313
+ ) {
314
+ throw new HardhatError(
315
+ HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR,
316
+ {
317
+ snapshotsPath: snapshotCheatcodesPath,
318
+ error: `Invalid value ${JSON.stringify(value)} for "${name}". Snapshot values must be uint256 decimal integer strings`,
319
+ },
320
+ );
321
+ }
322
+
323
+ snapshot[name] = value;
324
+ }
325
+
277
326
  snapshots.set(snapshotGroup, snapshot);
278
327
  }
279
328
  }
@@ -295,10 +344,12 @@ export function stringifySnapshotCheatcodes(
295
344
  export function compareSnapshotCheatcodes(
296
345
  previousSnapshotsMap: SnapshotCheatcodesMap,
297
346
  currentSnapshotsMap: SnapshotCheatcodesWithMetadataMap,
347
+ tolerance: number,
298
348
  ): SnapshotCheatcodesComparison {
299
349
  const added: SnapshotCheatcode[] = [];
300
350
  const removed: SnapshotCheatcode[] = [];
301
351
  const changed: SnapshotCheatcodeChange[] = [];
352
+ const tolerated: SnapshotCheatcodeChange[] = [];
302
353
  const seenPreviousEntries = new Set<string>();
303
354
 
304
355
  for (const [group, currentSnapshots] of currentSnapshotsMap) {
@@ -316,13 +367,29 @@ export function compareSnapshotCheatcodes(
316
367
  seenPreviousEntries.add(key);
317
368
  const previousValue = previousSnapshots[name];
318
369
  if (previousValue !== currentEntry.value) {
319
- changed.push({
370
+ const change: SnapshotCheatcodeChange = {
320
371
  group,
321
372
  name,
322
- expected: Number(previousValue),
323
- actual: Number(currentEntry.value),
373
+ expected: previousValue,
374
+ actual: currentEntry.value,
324
375
  source: currentEntry.metadata.source,
325
- });
376
+ };
377
+
378
+ // Values above 2^53 lose precision when coerced to `number`, but
379
+ // the resulting relative error (~1e-14%) is far below any usable
380
+ // tolerance, so it can't change the outcome of this check.
381
+ if (
382
+ tolerance > 0 &&
383
+ isWithinTolerance(
384
+ Number(previousValue),
385
+ Number(currentEntry.value),
386
+ tolerance,
387
+ )
388
+ ) {
389
+ tolerated.push(change);
390
+ } else {
391
+ changed.push(change);
392
+ }
326
393
  }
327
394
  }
328
395
  }
@@ -347,12 +414,14 @@ export function compareSnapshotCheatcodes(
347
414
  added: added.sort(sortByKey),
348
415
  removed: removed.sort(sortByKey),
349
416
  changed: changed.sort(sortByKey),
417
+ tolerated: tolerated.sort(sortByKey),
350
418
  };
351
419
  }
352
420
 
353
421
  export async function checkSnapshotCheatcodes(
354
422
  basePath: string,
355
423
  suiteResults: SuiteResult[],
424
+ tolerance: number,
356
425
  ): Promise<SnapshotCheatcodesCheckResult> {
357
426
  const { snapshotCheatcodes, renamedGroups } = sanitizeSnapshotCheatcodes(
358
427
  extractSnapshotCheatcodes(suiteResults),
@@ -372,6 +441,7 @@ export async function checkSnapshotCheatcodes(
372
441
  added: [],
373
442
  removed: [],
374
443
  changed: [],
444
+ tolerated: [],
375
445
  },
376
446
  noBaseline,
377
447
  renamedGroups,
@@ -384,6 +454,7 @@ export async function checkSnapshotCheatcodes(
384
454
  const comparison = compareSnapshotCheatcodes(
385
455
  previousSnapshotCheatcodes,
386
456
  snapshotCheatcodes,
457
+ tolerance,
387
458
  );
388
459
 
389
460
  return {
@@ -480,12 +551,19 @@ export function printSnapshotCheatcodeChanges(
480
551
  logger(` ${change.group}#${change.name}`);
481
552
  logger(styleText("grey", ` (in ${change.source})`));
482
553
 
483
- const diff = change.actual - change.expected;
554
+ logger(styleText("grey", ` Expected: ${change.expected}`));
555
+
556
+ // Snapshot values are uint256 decimal strings that can exceed 2^53, so
557
+ // the exact diff needs BigInt.
558
+ const diff = BigInt(change.actual) - BigInt(change.expected);
559
+
484
560
  const formattedDiff = diff > 0 ? `Δ+${diff}` : `Δ${diff}`;
485
561
 
486
562
  let gasChange = `${formattedDiff}`;
487
- if (change.expected > 0) {
488
- const percent = (diff / change.expected) * 100;
563
+
564
+ const expected = Number(change.expected);
565
+ if (expected > 0) {
566
+ const percent = (Number(diff) / expected) * 100;
489
567
  const formattedPercent =
490
568
  percent >= 0 ? `+${percent.toFixed(2)}%` : `${percent.toFixed(2)}%`;
491
569
  gasChange = `${formattedPercent}, ${formattedDiff}`;
@@ -495,7 +573,6 @@ export function printSnapshotCheatcodeChanges(
495
573
  const formattedGasChange =
496
574
  diff < 0 ? styleText("green", gasChange) : styleText("red", gasChange);
497
575
 
498
- logger(styleText("grey", ` Expected: ${change.expected}`));
499
576
  logger(
500
577
  styleText("grey", ` Actual: ${change.actual} (`) +
501
578
  formattedGasChange +
@@ -31,6 +31,7 @@ import {
31
31
  interface GasAnalyticsTestActionArguments {
32
32
  snapshot: boolean;
33
33
  snapshotCheck: boolean;
34
+ tolerance: number;
34
35
  // Forwarded from the base `test solidity` task; used to detect scoped runs.
35
36
  grep?: string;
36
37
  grepExclude?: string;
@@ -50,6 +51,30 @@ export interface SnapshotCheckResult {
50
51
  const runSolidityTests: TaskOverrideActionFunction<
51
52
  GasAnalyticsTestActionArguments
52
53
  > = async (args, hre, runSuper) => {
54
+ // Validate the flags before runSuper: it runs the entire test suite, and a
55
+ // usage error should fail immediately, not after a potentially long run.
56
+ if (args.snapshot && args.snapshotCheck) {
57
+ throw new HardhatError(
58
+ HardhatError.ERRORS.CORE.SOLIDITY_TESTS.MUTUALLY_EXCLUSIVE_SNAPSHOT_FLAGS,
59
+ );
60
+ }
61
+
62
+ // Unreachable from the CLI (the FLOAT parser rejects negative/non-numeric
63
+ // values), but programmatic `task.run({ tolerance })` only validates
64
+ // `typeof === "number"`.
65
+ if (args.tolerance < 0 || !Number.isFinite(args.tolerance)) {
66
+ throw new HardhatError(
67
+ HardhatError.ERRORS.CORE.SOLIDITY_TESTS.INVALID_SNAPSHOT_TOLERANCE,
68
+ { value: args.tolerance },
69
+ );
70
+ }
71
+
72
+ if (args.tolerance !== 0 && !args.snapshotCheck) {
73
+ throw new HardhatError(
74
+ HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_TOLERANCE_REQUIRES_CHECK,
75
+ );
76
+ }
77
+
53
78
  const superResult: Result<SolidityTestRunResult, SolidityTestRunResult> =
54
79
  await runSuper(args);
55
80
  const testsPassed = superResult.success;
@@ -59,12 +84,6 @@ const runSolidityTests: TaskOverrideActionFunction<
59
84
  const suiteResults = solidityTestRunResult.suiteResults;
60
85
  const rootPath = hre.config.paths.root;
61
86
 
62
- if (args.snapshot && args.snapshotCheck) {
63
- throw new HardhatError(
64
- HardhatError.ERRORS.CORE.SOLIDITY_TESTS.MUTUALLY_EXCLUSIVE_SNAPSHOT_FLAGS,
65
- );
66
- }
67
-
68
87
  let snapshotCheckPassed = true;
69
88
  if (args.snapshot) {
70
89
  const snapshotResult = await handleSnapshot(
@@ -77,6 +96,7 @@ const runSolidityTests: TaskOverrideActionFunction<
77
96
  const snapshotCheckResult = await handleSnapshotCheck(
78
97
  rootPath,
79
98
  suiteResults,
99
+ args.tolerance,
80
100
  );
81
101
  logSnapshotCheckResult(
82
102
  snapshotCheckResult,
@@ -156,14 +176,17 @@ export function logSnapshotResult(
156
176
  export async function handleSnapshotCheck(
157
177
  basePath: string,
158
178
  suiteResults: SuiteResult[],
179
+ tolerance: number,
159
180
  ): Promise<SnapshotCheckResult> {
160
181
  const functionGasSnapshotsCheck = await checkFunctionGasSnapshots(
161
182
  basePath,
162
183
  suiteResults,
184
+ tolerance,
163
185
  );
164
186
  const snapshotCheatcodesCheck = await checkSnapshotCheatcodes(
165
187
  basePath,
166
188
  suiteResults,
189
+ tolerance,
167
190
  );
168
191
 
169
192
  return {
@@ -24,23 +24,6 @@ export const DEFAULT_CHAIN_DESCRIPTORS: ChainDescriptorsConfig = new Map([
24
24
  },
25
25
  },
26
26
  ],
27
- // holesky testnet
28
- [
29
- 17_000n,
30
- {
31
- name: "Holesky",
32
- chainType: L1_CHAIN_TYPE,
33
- blockExplorers: {
34
- etherscan: {
35
- url: "https://holesky.etherscan.io",
36
- },
37
- blockscout: {
38
- url: "https://eth-holesky.blockscout.com",
39
- apiUrl: "https://eth-holesky.blockscout.com/api",
40
- },
41
- },
42
- },
43
- ],
44
27
  // hoodi testnet
45
28
  [
46
29
  560_048n,
@@ -426,7 +426,11 @@ function refineEdrNetworkUserConfig(
426
426
  }
427
427
 
428
428
  const interval = network.mining?.interval;
429
- if (typeof interval === "number" || Array.isArray(interval)) {
429
+ // A scalar `interval: 0` disables interval mining entirely, so this validation is not appropriate
430
+ if (
431
+ (typeof interval === "number" && interval !== 0) ||
432
+ Array.isArray(interval)
433
+ ) {
430
434
  const minInterval =
431
435
  typeof interval === "number" ? interval : Math.min(...interval);
432
436
  if (
@@ -58,6 +58,16 @@ import {
58
58
 
59
59
  const NPM_PACKAGES_WITH_SIMULATED_PACKAGE_EXPORTS = new Set(["forge-std"]);
60
60
 
61
+ function isInputSourceNameWithinRoot(
62
+ inputSourceName: string,
63
+ inputSourceNameRoot: string,
64
+ ): boolean {
65
+ return (
66
+ inputSourceName === inputSourceNameRoot ||
67
+ inputSourceName.startsWith(`${inputSourceNameRoot}/`)
68
+ );
69
+ }
70
+
61
71
  export class ResolverImplementation implements Resolver {
62
72
  readonly #projectRoot: string;
63
73
  readonly #npmPackageGraph: RemappedNpmPackagesGraphImplementation;
@@ -174,7 +184,13 @@ export class ResolverImplementation implements Resolver {
174
184
  async #resolveProjectFile(
175
185
  absoluteFilePath: string,
176
186
  ): Promise<Result<ProjectResolvedFile, ProjectRootResolutionError>> {
177
- if (!absoluteFilePath.startsWith(this.#projectRoot)) {
187
+ const relativeFilePath = path.relative(this.#projectRoot, absoluteFilePath);
188
+
189
+ if (
190
+ relativeFilePath === ".." ||
191
+ relativeFilePath.startsWith(`..${path.sep}`) ||
192
+ path.isAbsolute(relativeFilePath)
193
+ ) {
178
194
  return {
179
195
  success: false,
180
196
  error: {
@@ -184,8 +200,6 @@ export class ResolverImplementation implements Resolver {
184
200
  };
185
201
  }
186
202
 
187
- const relativeFilePath = path.relative(this.#projectRoot, absoluteFilePath);
188
-
189
203
  // We first check if the file has already been resolved.
190
204
  //
191
205
  // Note that it may have received the right path, but with the wrong
@@ -438,7 +452,12 @@ export class ResolverImplementation implements Resolver {
438
452
 
439
453
  if (isRelativeImport) {
440
454
  // If the import is relative, it shouldn't leave its package
441
- if (!directImport.startsWith(from.package.inputSourceNameRoot)) {
455
+ if (
456
+ !isInputSourceNameWithinRoot(
457
+ directImport,
458
+ from.package.inputSourceNameRoot,
459
+ )
460
+ ) {
442
461
  return {
443
462
  success: false,
444
463
  error: {
@@ -451,7 +470,8 @@ export class ResolverImplementation implements Resolver {
451
470
 
452
471
  // It also shouldn't get into its package's node_modules
453
472
  if (
454
- directImport.startsWith(
473
+ isInputSourceNameWithinRoot(
474
+ directImport,
455
475
  sourceNamePathJoin(from.package.inputSourceNameRoot, "node_modules"),
456
476
  )
457
477
  ) {
@@ -20,7 +20,7 @@ declare module "../../../types/config.js" {
20
20
  * The types of the values don't matter; we use `true` as a convention.
21
21
  *
22
22
  * By default, only "solc" is provided. Plugins can extend this via
23
- * declaration merging to add new compiler types (e.g. "solx").
23
+ * declaration merging to add new compiler types (e.g. "slangSolx").
24
24
  */
25
25
  export interface SolidityCompilerTypeDefinitions {
26
26
  solc: true;
@@ -358,7 +358,7 @@ declare module "../../../types/hooks.js" {
358
358
  /**
359
359
  * Hook to obtain a Compiler instance for a given compiler configuration.
360
360
  * The default handler returns a solc compiler. Plugins can intercept to
361
- * return their own compiler (e.g. SolxCompiler for type: "solx").
361
+ * return their own compiler (e.g. SlangSolxCompiler for type: "slangSolx").
362
362
  *
363
363
  * @param context The hook context.
364
364
  * @param compilerConfig The compiler configuration to get a compiler for.
@@ -22,7 +22,10 @@ import {
22
22
  import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
23
23
  import { hexStringToBytes } from "@nomicfoundation/hardhat-utils/hex";
24
24
 
25
- import { DEFAULT_VERBOSITY, OPTIMISM_CHAIN_TYPE } from "../../constants.js";
25
+ import {
26
+ ALWAYS_COLLECT_STACK_TRACES_VERBOSITY,
27
+ OPTIMISM_CHAIN_TYPE,
28
+ } from "../../constants.js";
26
29
  import { resolveHardfork } from "../network-manager/config-resolution.js";
27
30
  import { hardhatHardforkToEdrSpecId } from "../network-manager/edr/utils/convert-to-edr.js";
28
31
  import { warnIfExperimentalHardfork } from "../network-manager/edr/utils/hardfork.js";
@@ -139,7 +142,8 @@ export async function solidityTestConfigToSolidityTestRunnerConfigArgs({
139
142
  }
140
143
  }
141
144
 
142
- const shouldAlwaysCollectStackTraces = verbosity > DEFAULT_VERBOSITY;
145
+ const shouldAlwaysCollectStackTraces =
146
+ verbosity >= ALWAYS_COLLECT_STACK_TRACES_VERBOSITY;
143
147
 
144
148
  return {
145
149
  projectRoot,
@@ -10,6 +10,7 @@ import { styleText } from "node:util";
10
10
  import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex";
11
11
 
12
12
  import { sendErrorTelemetry } from "../../cli/telemetry/error-reporter/reporter.js";
13
+ import { ALWAYS_COLLECT_STACK_TRACES_VERBOSITY } from "../../constants.js";
13
14
  import { SolidityTestStackTraceGenerationError } from "../network-manager/edr/stack-traces/stack-trace-generation-errors.js";
14
15
  import { encodeStackTraceEntry } from "../network-manager/edr/stack-traces/stack-trace-solidity-errors.js";
15
16
  import { formatTraces } from "../network-manager/edr/utils/trace-formatters.js";
@@ -17,6 +18,8 @@ import { formatTraces } from "../network-manager/edr/utils/trace-formatters.js";
17
18
  import { formatArtifactId } from "./formatters.js";
18
19
  import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js";
19
20
 
21
+ const RERUN_VERBOSITY_HINT = `Try rerunning your tests with -${"v".repeat(ALWAYS_COLLECT_STACK_TRACES_VERBOSITY)} or above.`;
22
+
20
23
  class Indenter {
21
24
  #indentation: number;
22
25
 
@@ -305,17 +308,13 @@ export async function* testReporter(
305
308
  yield* output(
306
309
  indenter.t`Stack Trace Warning: ${colorize("grey", "The test is not safe to replay because a fork url without a fork block number was provided.")}\n`,
307
310
  );
308
- yield* output(
309
- indenter.t`Try rerunning your tests with -vvv or above.\n`,
310
- );
311
+ yield* output(indenter.t`${RERUN_VERBOSITY_HINT}\n`);
311
312
  }
312
313
  if (stackTrace.impureCheatcodes.length > 0) {
313
314
  yield* output(
314
315
  indenter.t`Stack Trace Warning: ${colorize("grey", `The test is not safe to replay because it uses impure cheatcodes: ${stackTrace.impureCheatcodes.join(", ")}`)}\n`,
315
316
  );
316
- yield* output(
317
- indenter.t`Try rerunning your tests with -vvv or above.\n`,
318
- );
317
+ yield* output(indenter.t`${RERUN_VERBOSITY_HINT}\n`);
319
318
  }
320
319
  break;
321
320
  case "HeuristicFailed":
@@ -334,7 +333,7 @@ export async function* testReporter(
334
333
  yield* output(indenter.t`Counterexample:\n`);
335
334
  indenter.inc();
336
335
  for (const [key, value] of Object.entries(counterexample)) {
337
- const counterExampleDetails = `${key}: ${Buffer.isBuffer(value) ? bytesToHexString(value) : value}`;
336
+ const counterExampleDetails = `${key}: ${value instanceof Uint8Array ? bytesToHexString(value) : value}`;
338
337
  yield* output(
339
338
  indenter.t`${colorize("grey", counterExampleDetails)}\n`,
340
339
  );
@@ -1,4 +1,4 @@
1
- import type * as ClassifierT from "./telemetry/error-classification/classifier.js";
1
+ import type * as ClassifierT from "../telemetry/error-classification/classifier.js";
2
2
 
3
3
  import { styleText } from "node:util";
4
4
 
@@ -7,7 +7,9 @@ import {
7
7
  HardhatPluginError,
8
8
  } from "@nomicfoundation/hardhat-errors";
9
9
 
10
- import { HARDHAT_NAME, HARDHAT_WEBSITE_URL } from "../constants.js";
10
+ import { HARDHAT_NAME, HARDHAT_WEBSITE_URL } from "../../constants.js";
11
+
12
+ import { detectNativeBindingFailure } from "./native-binding-error.js";
11
13
 
12
14
  // The classifier may import many unrelated things top-level to do its job, so
13
15
  // we load it lazily.
@@ -165,9 +167,24 @@ async function getErrorWithCategory(error: Error): Promise<ErrorWithCategory> {
165
167
  };
166
168
  }
167
169
 
170
+ const nativeBindingFailure = detectNativeBindingFailure(error);
171
+ if (nativeBindingFailure !== undefined) {
172
+ return {
173
+ category: ErrorCategory.HARDHAT,
174
+ categorizedError: new HardhatError(
175
+ HardhatError.ERRORS.CORE.GENERAL.NATIVE_BINDING_LOAD_FAILED,
176
+ {
177
+ parentPackage: nativeBindingFailure.parentPackage,
178
+ missingPackage: nativeBindingFailure.missingPackage,
179
+ },
180
+ error,
181
+ ),
182
+ };
183
+ }
184
+
168
185
  if (classifierModule === undefined) {
169
186
  classifierModule =
170
- await import("./telemetry/error-classification/classifier.js");
187
+ await import("../telemetry/error-classification/classifier.js");
171
188
  }
172
189
 
173
190
  // Pass `ignoreDevelopmentTimeFilter=true` so the migration footer also shows
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The platform triple that napi-rs appends to the platform package name, e.g.
3
+ * `@nomicfoundation/edr-linux-x64-gnu`, `@scope/pkg-darwin-arm64`.
4
+ */
5
+ const PLATFORM_SUFFIX_REGEX =
6
+ /-(?:android|darwin|freebsd|linux|win32)-(?:arm|arm64|ia32|x64|riscv64|ppc64|s390x)(?:-(?:gnu|musl|msvc|eabi|gnueabihf))?$/;
7
+
8
+ /**
9
+ * Matches both the CJS (`Cannot find module 'x'`) and ESM
10
+ * (`Cannot find package 'x' imported from y`) forms.
11
+ */
12
+ const MISSING_MODULE_REGEX = /Cannot find (?:module|package) '([^']+)'/g;
13
+
14
+ /**
15
+ * The packages Hardhat itself ships a native binding for.
16
+ */
17
+ const HARDHAT_NATIVE_BINDING_PACKAGES = [
18
+ "@nomicfoundation/edr",
19
+ "@nomicfoundation/solidity-analyzer",
20
+ ];
21
+
22
+ export interface NativeBindingFailure {
23
+ /** The package whose loader failed, e.g. `@nomicfoundation/edr`. */
24
+ parentPackage: string;
25
+ /** The platform package that couldn't be found. */
26
+ missingPackage: string;
27
+ }
28
+
29
+ /**
30
+ * Detect whether a given error is a napi-rs native binding load failure.
31
+ *
32
+ * Packages like `@nomicfoundation/edr` and `@nomicfoundation/solidity-analyzer`
33
+ * ship one prebuilt binary per platform in a separate package, and their
34
+ * generated loader `require`s the one matching the current platform. When that
35
+ * package is missing from the installation the loader throws at import time, so
36
+ * the failure surfaces as an opaque `MODULE_NOT_FOUND` from deep inside
37
+ * `node_modules`.
38
+ */
39
+ export function detectNativeBindingFailure(
40
+ error: Error,
41
+ ): NativeBindingFailure | undefined {
42
+ const chain = errorChain(error);
43
+
44
+ // Some link in the chain names the missing platform package.
45
+ for (const chainedError of chain) {
46
+ for (const match of chainedError.message.matchAll(MISSING_MODULE_REGEX)) {
47
+ const moduleName = match[1];
48
+
49
+ if (!PLATFORM_SUFFIX_REGEX.test(moduleName)) {
50
+ continue;
51
+ }
52
+
53
+ const parentPackage = moduleName.replace(PLATFORM_SUFFIX_REGEX, "");
54
+
55
+ if (!HARDHAT_NATIVE_BINDING_PACKAGES.includes(parentPackage)) {
56
+ continue;
57
+ }
58
+
59
+ return { missingPackage: moduleName, parentPackage };
60
+ }
61
+ }
62
+
63
+ return undefined;
64
+ }
65
+
66
+ function errorChain(error: Error): Error[] {
67
+ const chain: Error[] = [];
68
+ const seen = new Set<unknown>();
69
+ let current: unknown = error;
70
+
71
+ while (current instanceof Error && !seen.has(current)) {
72
+ seen.add(current);
73
+ chain.push(current);
74
+ current = current.cause;
75
+ }
76
+
77
+ return chain;
78
+ }
@@ -19,7 +19,7 @@ import { globalFlag } from "../core/config.js";
19
19
  import { TaskManagerImplementation } from "../core/tasks/task-manager.js";
20
20
  import { getHardhatVersion } from "../utils/package.js";
21
21
 
22
- import { printErrorMessages } from "./error-handler.js";
22
+ import { printErrorMessages } from "./error-handling/error-handler.js";
23
23
  import { getGlobalHelpString } from "./help/get-global-help-string.js";
24
24
  import { getHelpString } from "./help/get-help-string.js";
25
25
  import {
@@ -28,7 +28,7 @@ import { warnAboutUnusedLoadedPlugins } from "../core/plugins/unused-plugins-war
28
28
  import { setGlobalHardhatRuntimeEnvironment } from "../global-hre-instance.js";
29
29
  import { createHardhatRuntimeEnvironment } from "../hre-initialization.js";
30
30
 
31
- import { printErrorMessages } from "./error-handler.js";
31
+ import { printErrorMessages } from "./error-handling/error-handler.js";
32
32
  import { getGlobalHelpString } from "./help/get-global-help-string.js";
33
33
  import { getHelpString } from "./help/get-help-string.js";
34
34
  import {
@@ -11,3 +11,14 @@ export const OPTIMISM_CHAIN_TYPE = "op";
11
11
  export const DEFAULT_NETWORK_NAME = "default";
12
12
 
13
13
  export const DEFAULT_VERBOSITY = 2;
14
+
15
+ /**
16
+ * The verbosity level (`-vvvvv`) at and above which stack traces are always
17
+ * collected. Below it, stack traces are produced by re-running the failing
18
+ * test with tracing enabled — much cheaper, but tests with non-deterministic
19
+ * side effects (impure cheatcodes, a fork pinned to `latest`) can't be
20
+ * replayed and report an `UnsafeToReplay` warning instead. Always-on
21
+ * collection records step traces for every test, which balloons memory on
22
+ * large suites, so it's reserved for the highest verbosity.
23
+ */
24
+ export const ALWAYS_COLLECT_STACK_TRACES_VERBOSITY = 5;