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.
- package/CHANGELOG.md +34 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts +3 -2
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js +16 -7
- package/dist/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.d.ts +7 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.js +10 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/helpers/utils.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/index.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/index.js +12 -0
- package/dist/src/internal/builtin-plugins/gas-analytics/index.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts +5 -4
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js +56 -14
- package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts +2 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js +18 -7
- package/dist/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.js.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/chain-descriptors.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/chain-descriptors.js +0 -17
- package/dist/src/internal/builtin-plugins/network-manager/chain-descriptors.js.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/type-validation.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/network-manager/type-validation.js +3 -1
- package/dist/src/internal/builtin-plugins/network-manager/type-validation.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.js +10 -4
- package/dist/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity/type-extensions.d.ts +2 -2
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.js +2 -2
- package/dist/src/internal/builtin-plugins/solidity-test/helpers.js.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/reporter.d.ts.map +1 -1
- package/dist/src/internal/builtin-plugins/solidity-test/reporter.js +5 -3
- package/dist/src/internal/builtin-plugins/solidity-test/reporter.js.map +1 -1
- package/dist/src/internal/cli/error-handling/error-handler.d.ts.map +1 -0
- package/dist/src/internal/cli/{error-handler.js → error-handling/error-handler.js} +13 -2
- package/dist/src/internal/cli/error-handling/error-handler.js.map +1 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.d.ts +18 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.d.ts.map +1 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.js +57 -0
- package/dist/src/internal/cli/error-handling/native-binding-error.js.map +1 -0
- package/dist/src/internal/cli/hhu.js +1 -1
- package/dist/src/internal/cli/hhu.js.map +1 -1
- package/dist/src/internal/cli/main.js +1 -1
- package/dist/src/internal/cli/main.js.map +1 -1
- package/dist/src/internal/constants.d.ts +10 -0
- package/dist/src/internal/constants.d.ts.map +1 -1
- package/dist/src/internal/constants.js +10 -0
- package/dist/src/internal/constants.js.map +1 -1
- package/dist/src/internal/core/config.d.ts +14 -1
- package/dist/src/internal/core/config.d.ts.map +1 -1
- package/dist/src/internal/core/config.js +13 -5
- package/dist/src/internal/core/config.js.map +1 -1
- package/dist/src/internal/core/configuration-variables.d.ts.map +1 -1
- package/dist/src/internal/core/configuration-variables.js +3 -1
- package/dist/src/internal/core/configuration-variables.js.map +1 -1
- package/dist/src/types/config.d.ts +11 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/internal/builtin-plugins/gas-analytics/function-gas-snapshots.ts +26 -4
- package/src/internal/builtin-plugins/gas-analytics/helpers/utils.ts +15 -0
- package/src/internal/builtin-plugins/gas-analytics/index.ts +13 -0
- package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +90 -13
- package/src/internal/builtin-plugins/gas-analytics/tasks/solidity-test/task-action.ts +29 -6
- package/src/internal/builtin-plugins/network-manager/chain-descriptors.ts +0 -17
- package/src/internal/builtin-plugins/network-manager/type-validation.ts +5 -1
- package/src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.ts +25 -5
- package/src/internal/builtin-plugins/solidity/type-extensions.ts +2 -2
- package/src/internal/builtin-plugins/solidity-test/helpers.ts +6 -2
- package/src/internal/builtin-plugins/solidity-test/reporter.ts +6 -7
- package/src/internal/cli/{error-handler.ts → error-handling/error-handler.ts} +20 -3
- package/src/internal/cli/error-handling/native-binding-error.ts +78 -0
- package/src/internal/cli/hhu.ts +1 -1
- package/src/internal/cli/main.ts +1 -1
- package/src/internal/constants.ts +11 -0
- package/src/internal/core/config.ts +35 -2
- package/src/internal/core/configuration-variables.ts +3 -1
- package/src/types/config.ts +12 -0
- package/templates/01-node-test-runner-viem/package.json +1 -1
- package/templates/02-mocha-ethers/package.json +1 -1
- package/templates/03-minimal/package.json +1 -1
- package/dist/src/internal/cli/error-handler.d.ts.map +0 -1
- package/dist/src/internal/cli/error-handler.js.map +0 -1
- /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 {
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
281
|
+
let parsedSnapshot: unknown;
|
|
266
282
|
try {
|
|
267
|
-
|
|
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
|
-
|
|
370
|
+
const change: SnapshotCheatcodeChange = {
|
|
320
371
|
group,
|
|
321
372
|
name,
|
|
322
|
-
expected:
|
|
323
|
-
actual:
|
|
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
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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. "
|
|
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.
|
|
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 {
|
|
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 =
|
|
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}: ${
|
|
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 "
|
|
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 "
|
|
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("
|
|
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
|
+
}
|
package/src/internal/cli/hhu.ts
CHANGED
|
@@ -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 {
|
package/src/internal/cli/main.ts
CHANGED
|
@@ -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;
|