hardhat 3.12.0 → 3.13.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 (76) hide show
  1. package/CHANGELOG.md +16 -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 +3 -2
  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 +27 -6
  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/type-validation.d.ts.map +1 -1
  22. package/dist/src/internal/builtin-plugins/network-manager/type-validation.js +3 -1
  23. package/dist/src/internal/builtin-plugins/network-manager/type-validation.js.map +1 -1
  24. package/dist/src/internal/builtin-plugins/solidity-test/helpers.d.ts.map +1 -1
  25. package/dist/src/internal/builtin-plugins/solidity-test/helpers.js +2 -2
  26. package/dist/src/internal/builtin-plugins/solidity-test/helpers.js.map +1 -1
  27. package/dist/src/internal/builtin-plugins/solidity-test/reporter.d.ts.map +1 -1
  28. package/dist/src/internal/builtin-plugins/solidity-test/reporter.js +4 -2
  29. package/dist/src/internal/builtin-plugins/solidity-test/reporter.js.map +1 -1
  30. package/dist/src/internal/cli/error-handling/error-handler.d.ts.map +1 -0
  31. package/dist/src/internal/cli/{error-handler.js → error-handling/error-handler.js} +13 -2
  32. package/dist/src/internal/cli/error-handling/error-handler.js.map +1 -0
  33. package/dist/src/internal/cli/error-handling/native-binding-error.d.ts +18 -0
  34. package/dist/src/internal/cli/error-handling/native-binding-error.d.ts.map +1 -0
  35. package/dist/src/internal/cli/error-handling/native-binding-error.js +57 -0
  36. package/dist/src/internal/cli/error-handling/native-binding-error.js.map +1 -0
  37. package/dist/src/internal/cli/hhu.js +1 -1
  38. package/dist/src/internal/cli/hhu.js.map +1 -1
  39. package/dist/src/internal/cli/main.js +1 -1
  40. package/dist/src/internal/cli/main.js.map +1 -1
  41. package/dist/src/internal/constants.d.ts +10 -0
  42. package/dist/src/internal/constants.d.ts.map +1 -1
  43. package/dist/src/internal/constants.js +10 -0
  44. package/dist/src/internal/constants.js.map +1 -1
  45. package/dist/src/internal/core/config.d.ts +14 -1
  46. package/dist/src/internal/core/config.d.ts.map +1 -1
  47. package/dist/src/internal/core/config.js +13 -5
  48. package/dist/src/internal/core/config.js.map +1 -1
  49. package/dist/src/internal/core/configuration-variables.d.ts.map +1 -1
  50. package/dist/src/internal/core/configuration-variables.js +3 -1
  51. package/dist/src/internal/core/configuration-variables.js.map +1 -1
  52. package/dist/src/types/config.d.ts +11 -0
  53. package/dist/src/types/config.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts +26 -4
  56. package/src/internal/builtin-plugins/gas-analytics/helpers/utils.ts +15 -0
  57. package/src/internal/builtin-plugins/gas-analytics/index.ts +13 -0
  58. package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +38 -3
  59. package/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.ts +29 -6
  60. package/src/internal/builtin-plugins/network-manager/type-validation.ts +5 -1
  61. package/src/internal/builtin-plugins/solidity-test/helpers.ts +6 -2
  62. package/src/internal/builtin-plugins/solidity-test/reporter.ts +5 -6
  63. package/src/internal/cli/{error-handler.ts → error-handling/error-handler.ts} +20 -3
  64. package/src/internal/cli/error-handling/native-binding-error.ts +78 -0
  65. package/src/internal/cli/hhu.ts +1 -1
  66. package/src/internal/cli/main.ts +1 -1
  67. package/src/internal/constants.ts +11 -0
  68. package/src/internal/core/config.ts +35 -2
  69. package/src/internal/core/configuration-variables.ts +3 -1
  70. package/src/types/config.ts +12 -0
  71. package/templates/01-node-test-runner-viem/package.json +1 -1
  72. package/templates/02-mocha-ethers/package.json +1 -1
  73. package/templates/03-minimal/package.json +1 -1
  74. package/dist/src/internal/cli/error-handler.d.ts.map +0 -1
  75. package/dist/src/internal/cli/error-handler.js.map +0 -1
  76. /package/dist/src/internal/cli/{error-handler.d.ts → error-handling/error-handler.d.ts} +0 -0
@@ -7,6 +7,17 @@ export interface ConfigurationVariable {
7
7
  _type: "ConfigurationVariable";
8
8
  name: string;
9
9
  format?: string;
10
+ /**
11
+ * An optional fallback value, used only when the variable can't be resolved
12
+ * from any other source (e.g. an environment variable or the keystore).
13
+ *
14
+ * By default, the value is resolved from the environment variable first, then
15
+ * any plugin source (like hardhat-keystore), and finally this default. Plugins
16
+ * can customize this, but the built-in resolver and bundled plugins follow this
17
+ * order. If none of them provide a value and no default is set, resolving the
18
+ * variable throws.
19
+ */
20
+ default?: string;
10
21
  }
11
22
  /**
12
23
  * A resolved configuration variable.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,qCAAqC,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,+BAA+B,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE1B;;;;;OAKG;IACH,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7B;;;;OAIG;IACH,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC1C,gBAAgB,EAAE,qBAAqB,GAAG,MAAM,KAC7C,6BAA6B,CAAC;AAEnC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,qBAAqB,CAAC;CACrD;AAED;;GAEG;AAGH,MAAM,WAAW,mBAAmB;CAAG;AAEvC;;GAEG;AAGH,MAAM,WAAW,qBAAqB;CAAG;AAEzC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED;;GAEG;AAGH,MAAM,WAAW,eAAe;CAAG;AAEnC;;GAEG;AAGH,MAAM,WAAW,iBAAiB;CAAG"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,qCAAqC,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,+BAA+B,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE1B;;;;;OAKG;IACH,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7B;;;;OAIG;IACH,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC1C,gBAAgB,EAAE,qBAAqB,GAAG,MAAM,KAC7C,6BAA6B,CAAC;AAEnC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,qBAAqB,CAAC;CACrD;AAED;;GAEG;AAGH,MAAM,WAAW,mBAAmB;CAAG;AAEvC;;GAEG;AAGH,MAAM,WAAW,qBAAqB;CAAG;AAEzC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED;;GAEG;AAGH,MAAM,WAAW,eAAe;CAAG;AAEnC;;GAEG;AAGH,MAAM,WAAW,iBAAiB;CAAG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hardhat",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "description": "Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.",
5
5
  "homepage": "https://hardhat.org",
6
6
  "repository": {
@@ -17,7 +17,11 @@ import {
17
17
  parseFullyQualifiedName,
18
18
  } from "../../../utils/contract-names.js";
19
19
 
20
- import { formatSectionHeader, getUserFqn } from "./helpers/utils.js";
20
+ import {
21
+ formatSectionHeader,
22
+ getUserFqn,
23
+ isWithinTolerance,
24
+ } from "./helpers/utils.js";
21
25
 
22
26
  export const FUNCTION_GAS_SNAPSHOTS_FILE = ".gas-snapshot";
23
27
 
@@ -49,6 +53,10 @@ export interface FunctionGasSnapshotComparison {
49
53
  added: FunctionGasSnapshot[];
50
54
  removed: FunctionGasSnapshot[];
51
55
  changed: FunctionGasSnapshotChange[];
56
+ // Diffs within the allowed tolerance: they don't fail the check. They are
57
+ // included in the result for programmatic inspection, but are not shown in
58
+ // the default console output.
59
+ tolerated: FunctionGasSnapshotChange[];
52
60
  }
53
61
 
54
62
  export interface FunctionGasSnapshotChange {
@@ -242,6 +250,7 @@ export function parseFunctionGasSnapshots(
242
250
  export function compareFunctionGasSnapshots(
243
251
  previousSnapshots: FunctionGasSnapshot[],
244
252
  currentSnapshots: FunctionGasSnapshotWithMetadata[],
253
+ tolerance: number,
245
254
  ): FunctionGasSnapshotComparison {
246
255
  const previousSnapshotsMap = new Map(
247
256
  previousSnapshots.map((s) => [
@@ -252,6 +261,7 @@ export function compareFunctionGasSnapshots(
252
261
 
253
262
  const added: FunctionGasSnapshot[] = [];
254
263
  const changed: FunctionGasSnapshotChange[] = [];
264
+ const tolerated: FunctionGasSnapshotChange[] = [];
255
265
 
256
266
  for (const current of currentSnapshots) {
257
267
  const key = `${current.contractNameOrFqn}#${current.functionSig}`;
@@ -278,7 +288,7 @@ export function compareFunctionGasSnapshots(
278
288
  ? current.gasUsage.gas
279
289
  : current.gasUsage.medianGas;
280
290
 
281
- changed.push({
291
+ const change: FunctionGasSnapshotChange = {
282
292
  contractNameOrFqn: current.contractNameOrFqn,
283
293
  functionSig: current.functionSig,
284
294
  kind: currentKind,
@@ -287,14 +297,23 @@ export function compareFunctionGasSnapshots(
287
297
  runs:
288
298
  currentKind === "fuzz" ? Number(current.gasUsage.runs) : undefined,
289
299
  source: current.metadata.source,
290
- });
300
+ };
301
+
302
+ if (
303
+ tolerance > 0 &&
304
+ isWithinTolerance(change.expected, change.actual, tolerance)
305
+ ) {
306
+ tolerated.push(change);
307
+ } else {
308
+ changed.push(change);
309
+ }
291
310
  }
292
311
  previousSnapshotsMap.delete(key);
293
312
  }
294
313
 
295
314
  const removed = Array.from(previousSnapshotsMap.values());
296
315
 
297
- return { added, removed, changed };
316
+ return { added, removed, changed, tolerated };
298
317
  }
299
318
 
300
319
  export function hasGasUsageChanged(
@@ -315,6 +334,7 @@ export function hasGasUsageChanged(
315
334
  export async function checkFunctionGasSnapshots(
316
335
  basePath: string,
317
336
  suiteResults: SuiteResult[],
337
+ tolerance: number,
318
338
  ): Promise<FunctionGasSnapshotCheckResult> {
319
339
  const functionGasSnapshots = extractFunctionGasSnapshots(suiteResults);
320
340
 
@@ -332,6 +352,7 @@ export async function checkFunctionGasSnapshots(
332
352
  added: [],
333
353
  removed: [],
334
354
  changed: [],
355
+ tolerated: [],
335
356
  },
336
357
  noBaseline,
337
358
  };
@@ -343,6 +364,7 @@ export async function checkFunctionGasSnapshots(
343
364
  const comparison = compareFunctionGasSnapshots(
344
365
  previousFunctionGasSnapshots,
345
366
  functionGasSnapshots,
367
+ tolerance,
346
368
  );
347
369
 
348
370
  return {
@@ -84,6 +84,21 @@ export function median(values: number[]): number {
84
84
  : (sorted[mid - 1] + sorted[mid]) / 2;
85
85
  }
86
86
 
87
+ /**
88
+ * Whether `actual` drifted from `expected` by at most `tolerance` percent.
89
+ * Cross-multiplied instead of `(diff / expected) * 100 <= tolerance` to
90
+ * avoid floating-point errors from the division (e.g. `(49 / 700) * 100`
91
+ * is `7.000000000000001`, which would fail a tolerance of exactly 7).
92
+ */
93
+ export function isWithinTolerance(
94
+ expected: number,
95
+ actual: number,
96
+ tolerance: number,
97
+ ): boolean {
98
+ const diff = Math.abs(actual - expected);
99
+ return diff * 100 <= tolerance * Math.abs(expected);
100
+ }
101
+
87
102
  export function formatSectionHeader(
88
103
  sectionName: string,
89
104
  {
@@ -19,6 +19,13 @@ const hardhatPlugin: HardhatPlugin = definePlugin({
19
19
  description:
20
20
  "Check the snapshots match the stored values (Solidity tests only)",
21
21
  })
22
+ .addOption({
23
+ name: "tolerance",
24
+ description:
25
+ "Allowed drift percentage for --snapshot-check (Solidity tests only)",
26
+ type: ArgumentType.FLOAT,
27
+ defaultValue: 0,
28
+ })
22
29
  .setAction(async () => ({
23
30
  default: async (args, _hre, runSuper) => {
24
31
  // We don't need to do anything here, as the test task will forward
@@ -36,6 +43,12 @@ const hardhatPlugin: HardhatPlugin = definePlugin({
36
43
  name: "snapshotCheck",
37
44
  description: "Check the snapshots match the stored values",
38
45
  })
46
+ .addOption({
47
+ name: "tolerance",
48
+ description: "Allowed drift percentage for --snapshot-check",
49
+ type: ArgumentType.FLOAT,
50
+ defaultValue: 0,
51
+ })
39
52
  .setAction(
40
53
  async () => await import("./tasks/solidity-test/task-action.js"),
41
54
  )
@@ -20,7 +20,11 @@ import {
20
20
  parseFullyQualifiedName,
21
21
  } from "../../../utils/contract-names.js";
22
22
 
23
- import { formatSectionHeader, getUserFqn } from "./helpers/utils.js";
23
+ import {
24
+ formatSectionHeader,
25
+ getUserFqn,
26
+ isWithinTolerance,
27
+ } from "./helpers/utils.js";
24
28
 
25
29
  export const SNAPSHOT_CHEATCODES_DIR = "snapshots";
26
30
 
@@ -73,6 +77,10 @@ export interface SnapshotCheatcodesComparison {
73
77
  added: SnapshotCheatcode[];
74
78
  removed: SnapshotCheatcode[];
75
79
  changed: SnapshotCheatcodeChange[];
80
+ // Diffs within the allowed tolerance: they don't fail the check. They are
81
+ // included in the result for programmatic inspection, but are not shown in
82
+ // the default console output.
83
+ tolerated: SnapshotCheatcodeChange[];
76
84
  }
77
85
 
78
86
  export interface SnapshotCheatcodesCheckResult {
@@ -295,10 +303,12 @@ export function stringifySnapshotCheatcodes(
295
303
  export function compareSnapshotCheatcodes(
296
304
  previousSnapshotsMap: SnapshotCheatcodesMap,
297
305
  currentSnapshotsMap: SnapshotCheatcodesWithMetadataMap,
306
+ tolerance: number,
298
307
  ): SnapshotCheatcodesComparison {
299
308
  const added: SnapshotCheatcode[] = [];
300
309
  const removed: SnapshotCheatcode[] = [];
301
310
  const changed: SnapshotCheatcodeChange[] = [];
311
+ const tolerated: SnapshotCheatcodeChange[] = [];
302
312
  const seenPreviousEntries = new Set<string>();
303
313
 
304
314
  for (const [group, currentSnapshots] of currentSnapshotsMap) {
@@ -316,13 +326,27 @@ export function compareSnapshotCheatcodes(
316
326
  seenPreviousEntries.add(key);
317
327
  const previousValue = previousSnapshots[name];
318
328
  if (previousValue !== currentEntry.value) {
319
- changed.push({
329
+ const change: SnapshotCheatcodeChange = {
320
330
  group,
321
331
  name,
322
332
  expected: Number(previousValue),
323
333
  actual: Number(currentEntry.value),
324
334
  source: currentEntry.metadata.source,
325
- });
335
+ };
336
+
337
+ // The tolerance only applies when both values are strictly numeric;
338
+ // otherwise the exact string comparison stands. Snapshot cheatcode
339
+ // values can be arbitrary strings, unlike function gas snapshots.
340
+ if (
341
+ tolerance > 0 &&
342
+ isStrictlyNumeric(previousValue) &&
343
+ isStrictlyNumeric(currentEntry.value) &&
344
+ isWithinTolerance(change.expected, change.actual, tolerance)
345
+ ) {
346
+ tolerated.push(change);
347
+ } else {
348
+ changed.push(change);
349
+ }
326
350
  }
327
351
  }
328
352
  }
@@ -347,12 +371,21 @@ export function compareSnapshotCheatcodes(
347
371
  added: added.sort(sortByKey),
348
372
  removed: removed.sort(sortByKey),
349
373
  changed: changed.sort(sortByKey),
374
+ tolerated: tolerated.sort(sortByKey),
350
375
  };
351
376
  }
352
377
 
378
+ // The emptiness check is needed because `Number("")` is `0`, so a bare
379
+ // `Number.isFinite(Number(value))` would treat empty/whitespace values as
380
+ // numeric.
381
+ function isStrictlyNumeric(value: string): boolean {
382
+ return value.trim() !== "" && Number.isFinite(Number(value));
383
+ }
384
+
353
385
  export async function checkSnapshotCheatcodes(
354
386
  basePath: string,
355
387
  suiteResults: SuiteResult[],
388
+ tolerance: number,
356
389
  ): Promise<SnapshotCheatcodesCheckResult> {
357
390
  const { snapshotCheatcodes, renamedGroups } = sanitizeSnapshotCheatcodes(
358
391
  extractSnapshotCheatcodes(suiteResults),
@@ -372,6 +405,7 @@ export async function checkSnapshotCheatcodes(
372
405
  added: [],
373
406
  removed: [],
374
407
  changed: [],
408
+ tolerated: [],
375
409
  },
376
410
  noBaseline,
377
411
  renamedGroups,
@@ -384,6 +418,7 @@ export async function checkSnapshotCheatcodes(
384
418
  const comparison = compareSnapshotCheatcodes(
385
419
  previousSnapshotCheatcodes,
386
420
  snapshotCheatcodes,
421
+ tolerance,
387
422
  );
388
423
 
389
424
  return {
@@ -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 {
@@ -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 (
@@ -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":
@@ -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;
@@ -23,11 +23,33 @@ import {
23
23
 
24
24
  /**
25
25
  * Creates a configuration variable, which will be fetched at runtime.
26
+ *
27
+ * The second argument is an options object with an optional `format` (a
28
+ * template that must include the `{variable}` marker) and an optional
29
+ * `default` value. The default is used as a fallback when the variable can't
30
+ * be resolved from any other source (see {@link ConfigurationVariable.default}).
26
31
  */
27
32
  export function configVariable(
28
33
  name: string,
29
- format: string = CONFIGURATION_VARIABLE_MARKER,
34
+ options?: { format?: string; default?: string },
35
+ ): ConfigurationVariable;
36
+ /**
37
+ * @deprecated Passing the `format` as a string is deprecated. Pass an options
38
+ * object instead: `configVariable(name, { format })`.
39
+ */
40
+ export function configVariable(
41
+ name: string,
42
+ format: string,
43
+ ): ConfigurationVariable;
44
+ export function configVariable(
45
+ name: string,
46
+ formatOrOptions: string | { format?: string; default?: string } = {},
30
47
  ): ConfigurationVariable {
48
+ const { format = CONFIGURATION_VARIABLE_MARKER, default: defaultValue } =
49
+ typeof formatOrOptions === "string"
50
+ ? { format: formatOrOptions }
51
+ : formatOrOptions;
52
+
31
53
  if (!format.includes(CONFIGURATION_VARIABLE_MARKER)) {
32
54
  throw new HardhatError(
33
55
  HardhatError.ERRORS.CORE.GENERAL
@@ -35,7 +57,18 @@ export function configVariable(
35
57
  { format, marker: CONFIGURATION_VARIABLE_MARKER },
36
58
  );
37
59
  }
38
- return { _type: "ConfigurationVariable", name, format };
60
+
61
+ const configurationVariable: ConfigurationVariable = {
62
+ _type: "ConfigurationVariable",
63
+ name,
64
+ format,
65
+ };
66
+
67
+ if (defaultValue !== undefined) {
68
+ configurationVariable.default = defaultValue;
69
+ }
70
+
71
+ return configurationVariable;
39
72
  }
40
73
 
41
74
  /**
@@ -131,7 +131,9 @@ export class LazyResolvedConfigurationVariable extends BaseResolvedConfiguration
131
131
  "fetchValue",
132
132
  [this.#variable],
133
133
  async (_context, v) => {
134
- const value = process.env[v.name];
134
+ // Fall back to the default only when the env var is unset. An empty
135
+ // string still takes precedence
136
+ const value = process.env[v.name] ?? v.default;
135
137
 
136
138
  if (typeof value !== "string") {
137
139
  throw new HardhatError(