hardhat 3.15.0 → 3.17.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 (85) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/src/internal/builtin-plugins/network-manager/accounts/derive-private-keys.d.ts.map +1 -1
  3. package/dist/src/internal/builtin-plugins/network-manager/accounts/derive-private-keys.js +0 -1
  4. package/dist/src/internal/builtin-plugins/network-manager/accounts/derive-private-keys.js.map +1 -1
  5. package/dist/src/internal/builtin-plugins/network-manager/config-resolution.d.ts.map +1 -1
  6. package/dist/src/internal/builtin-plugins/network-manager/config-resolution.js +1 -0
  7. package/dist/src/internal/builtin-plugins/network-manager/config-resolution.js.map +1 -1
  8. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.d.ts +1 -0
  9. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.d.ts.map +1 -1
  10. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.js +2 -0
  11. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.js.map +1 -1
  12. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.d.ts +15 -0
  13. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.d.ts.map +1 -1
  14. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.js +43 -5
  15. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.js.map +1 -1
  16. package/dist/src/internal/builtin-plugins/network-manager/edr/genesis-state.js +4 -4
  17. package/dist/src/internal/builtin-plugins/network-manager/edr/genesis-state.js.map +1 -1
  18. package/dist/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.d.ts +3 -2
  19. package/dist/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.d.ts.map +1 -1
  20. package/dist/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.js +13 -5
  21. package/dist/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.js.map +1 -1
  22. package/dist/src/internal/builtin-plugins/network-manager/http-provider.js +1 -1
  23. package/dist/src/internal/builtin-plugins/network-manager/http-provider.js.map +1 -1
  24. package/dist/src/internal/builtin-plugins/network-manager/provider-errors.d.ts +20 -0
  25. package/dist/src/internal/builtin-plugins/network-manager/provider-errors.d.ts.map +1 -1
  26. package/dist/src/internal/builtin-plugins/network-manager/provider-errors.js +24 -0
  27. package/dist/src/internal/builtin-plugins/network-manager/provider-errors.js.map +1 -1
  28. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.d.ts.map +1 -1
  29. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.js +10 -13
  30. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.js.map +1 -1
  31. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.d.ts +1 -1
  32. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.d.ts.map +1 -1
  33. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.js +2 -2
  34. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.js.map +1 -1
  35. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.d.ts +11 -3
  36. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.d.ts.map +1 -1
  37. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.js +53 -13
  38. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.js.map +1 -1
  39. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers-array.d.ts.map +1 -1
  40. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers-array.js +19 -1
  41. package/dist/src/internal/builtin-plugins/network-manager/request-handlers/handlers-array.js.map +1 -1
  42. package/dist/src/internal/builtin-plugins/network-manager/type-extensions/config.d.ts +2 -0
  43. package/dist/src/internal/builtin-plugins/network-manager/type-extensions/config.d.ts.map +1 -1
  44. package/dist/src/internal/builtin-plugins/network-manager/type-validation.d.ts.map +1 -1
  45. package/dist/src/internal/builtin-plugins/network-manager/type-validation.js +1 -0
  46. package/dist/src/internal/builtin-plugins/network-manager/type-validation.js.map +1 -1
  47. package/dist/src/internal/builtin-plugins/node/json-rpc/handler.js +12 -1
  48. package/dist/src/internal/builtin-plugins/node/json-rpc/handler.js.map +1 -1
  49. package/dist/src/internal/builtin-plugins/solidity-test/eip712/canonicalize.d.ts +1 -1
  50. package/dist/src/internal/builtin-plugins/solidity-test/eip712/canonicalize.d.ts.map +1 -1
  51. package/dist/src/internal/builtin-plugins/solidity-test/eip712/canonicalize.js +13 -11
  52. package/dist/src/internal/builtin-plugins/solidity-test/eip712/canonicalize.js.map +1 -1
  53. package/dist/src/internal/builtin-plugins/solidity-test/formatters.js +10 -2
  54. package/dist/src/internal/builtin-plugins/solidity-test/formatters.js.map +1 -1
  55. package/dist/src/internal/core/configuration-variables.d.ts +8 -0
  56. package/dist/src/internal/core/configuration-variables.d.ts.map +1 -1
  57. package/dist/src/internal/core/configuration-variables.js +9 -9
  58. package/dist/src/internal/core/configuration-variables.js.map +1 -1
  59. package/dist/src/internal/edr/exports.d.ts +2 -0
  60. package/dist/src/internal/edr/exports.d.ts.map +1 -0
  61. package/dist/src/internal/edr/exports.js +2 -0
  62. package/dist/src/internal/edr/exports.js.map +1 -0
  63. package/package.json +6 -5
  64. package/src/internal/builtin-plugins/network-manager/accounts/derive-private-keys.ts +0 -1
  65. package/src/internal/builtin-plugins/network-manager/config-resolution.ts +1 -0
  66. package/src/internal/builtin-plugins/network-manager/edr/edr-constants.ts +3 -0
  67. package/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts +52 -1
  68. package/src/internal/builtin-plugins/network-manager/edr/genesis-state.ts +5 -5
  69. package/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts +17 -3
  70. package/src/internal/builtin-plugins/network-manager/http-provider.ts +1 -1
  71. package/src/internal/builtin-plugins/network-manager/provider-errors.ts +30 -0
  72. package/src/internal/builtin-plugins/network-manager/request-handlers/handlers/accounts/local-accounts.ts +10 -26
  73. package/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/automatic-gas-handler.ts +3 -1
  74. package/src/internal/builtin-plugins/network-manager/request-handlers/handlers/gas/multiplied-gas-estimation.ts +73 -18
  75. package/src/internal/builtin-plugins/network-manager/request-handlers/handlers-array.ts +24 -0
  76. package/src/internal/builtin-plugins/network-manager/type-extensions/config.ts +2 -0
  77. package/src/internal/builtin-plugins/network-manager/type-validation.ts +6 -0
  78. package/src/internal/builtin-plugins/node/json-rpc/handler.ts +15 -1
  79. package/src/internal/builtin-plugins/solidity-test/eip712/canonicalize.ts +13 -11
  80. package/src/internal/builtin-plugins/solidity-test/formatters.ts +14 -2
  81. package/src/internal/core/configuration-variables.ts +25 -10
  82. package/src/internal/edr/exports.ts +1 -0
  83. package/templates/01-node-test-runner-viem/package.json +1 -1
  84. package/templates/02-mocha-ethers/package.json +2 -2
  85. package/templates/03-minimal/package.json +1 -1
@@ -101,3 +101,33 @@ export class UnknownError extends ProviderError {
101
101
  super(message, UnknownError.CODE, parent);
102
102
  }
103
103
  }
104
+
105
+ /**
106
+ * Thrown by `eth_estimateGas` when the network's `gasEstimationMode` is
107
+ * `"noInternalOutOfGas"` and an internal call runs out of gas even when the
108
+ * transaction is executed with the maximum amount of gas available, meaning
109
+ * that no gas limit can prevent the internal out-of-gas error.
110
+ */
111
+ export class InternalCallOutOfGasError extends ProviderError {
112
+ public static readonly CODE = -32000;
113
+
114
+ constructor(
115
+ message: string = "Gas estimation failed: an internal call runs out of gas regardless of the transaction's gas limit, so no reliable estimate exists. Set an explicit gas limit for the transaction, or set the network's gasEstimationMode to 'topLevelSuccess' to only require the top-level call to succeed.",
116
+ parent?: Error,
117
+ ) {
118
+ super(message, InternalCallOutOfGasError.CODE, parent);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Detects the error data of EDR's estimation failure for the
124
+ * `NoInternalOutOfGas` gas estimation mode, reported when an internal call
125
+ * runs out of gas even when the transaction is executed with the maximum
126
+ * amount of gas available.
127
+ *
128
+ * The `reason` discriminator it looks for is also what `hardhat node` puts on
129
+ * the wire, so clients of an http connection can recognize the failure.
130
+ */
131
+ export function isInternalCallOutOfGasErrorData(errorData: unknown): boolean {
132
+ return isObject(errorData) && errorData.reason === "InternalCallOutOfGas";
133
+ }
@@ -6,7 +6,6 @@ import type {
6
6
  import type { RequestHandler } from "../../types.js";
7
7
  import type { RpcTransactionRequest } from "@nomicfoundation/hardhat-zod-utils/rpc";
8
8
  import type * as MicroEthSignerT from "micro-eth-signer";
9
- import type * as MicroEthSignerTypedDataT from "micro-eth-signer/typed-data";
10
9
 
11
10
  import {
12
11
  assertHardhatInvariant,
@@ -32,7 +31,6 @@ import {
32
31
 
33
32
  // micro-eth-signer is known to be slow to load, so we lazy load it
34
33
  let microEthSigner: typeof MicroEthSignerT | undefined;
35
- let microEthSignerTypedData: typeof MicroEthSignerTypedDataT | undefined;
36
34
 
37
35
  import { getRequestParams } from "../../../json-rpc.js";
38
36
  import { ChainId } from "../chain-id/chain-id.js";
@@ -129,19 +127,14 @@ export class LocalAccountsHandler extends ChainId implements RequestHandler {
129
127
  );
130
128
  }
131
129
 
132
- if (microEthSignerTypedData === undefined) {
133
- microEthSignerTypedData =
134
- await import("micro-eth-signer/typed-data");
130
+ if (microEthSigner === undefined) {
131
+ microEthSigner = await import("micro-eth-signer");
135
132
  }
136
133
 
137
134
  const privateKey = await this.#getPrivateKeyForAddress(address);
138
135
  return this.#createJsonRpcResponse(
139
136
  jsonRpcRequest.id,
140
- microEthSignerTypedData.personal.sign(
141
- data,
142
- privateKey,
143
- EXTRA_ENTROPY,
144
- ),
137
+ microEthSigner.eip191Signer.sign(data, privateKey, EXTRA_ENTROPY),
145
138
  );
146
139
  }
147
140
  }
@@ -159,19 +152,14 @@ export class LocalAccountsHandler extends ChainId implements RequestHandler {
159
152
  );
160
153
  }
161
154
 
162
- if (microEthSignerTypedData === undefined) {
163
- microEthSignerTypedData =
164
- await import("micro-eth-signer/typed-data");
155
+ if (microEthSigner === undefined) {
156
+ microEthSigner = await import("micro-eth-signer");
165
157
  }
166
158
 
167
159
  const privateKey = await this.#getPrivateKeyForAddress(address);
168
160
  return this.#createJsonRpcResponse(
169
161
  jsonRpcRequest.id,
170
- microEthSignerTypedData.personal.sign(
171
- data,
172
- privateKey,
173
- EXTRA_ENTROPY,
174
- ),
162
+ microEthSigner.eip191Signer.sign(data, privateKey, EXTRA_ENTROPY),
175
163
  );
176
164
  }
177
165
  }
@@ -201,17 +189,13 @@ export class LocalAccountsHandler extends ChainId implements RequestHandler {
201
189
  // if we don't manage the address, the method is forwarded
202
190
  const privateKey = await this.#getPrivateKeyForAddressOrNull(address);
203
191
  if (privateKey !== null) {
204
- if (microEthSignerTypedData === undefined) {
205
- microEthSignerTypedData = await import("micro-eth-signer/typed-data");
192
+ if (microEthSigner === undefined) {
193
+ microEthSigner = await import("micro-eth-signer");
206
194
  }
207
195
 
208
196
  return this.#createJsonRpcResponse(
209
197
  jsonRpcRequest.id,
210
- microEthSignerTypedData.signTyped(
211
- typedMessage,
212
- privateKey,
213
- EXTRA_ENTROPY,
214
- ),
198
+ microEthSigner.signTyped(typedMessage, privateKey, EXTRA_ENTROPY),
215
199
  );
216
200
  }
217
201
  }
@@ -482,7 +466,7 @@ export class LocalAccountsHandler extends ChainId implements RequestHandler {
482
466
 
483
467
  const signedTransaction = transaction.signBy(privateKey, EXTRA_ENTROPY);
484
468
 
485
- return signedTransaction.toRawBytes();
469
+ return signedTransaction.toBytes();
486
470
  }
487
471
 
488
472
  #createJsonRpcResponse(
@@ -30,8 +30,10 @@ export class AutomaticGasHandler
30
30
  constructor(
31
31
  provider: EthereumProvider,
32
32
  gasMultiplier: number = DEFAULT_GAS_MULTIPLIER,
33
+ fallbackGas?: bigint,
34
+ isBlockGasLimitEnforced?: () => boolean,
33
35
  ) {
34
- super(provider, gasMultiplier);
36
+ super(provider, gasMultiplier, fallbackGas, isBlockGasLimitEnforced);
35
37
  }
36
38
 
37
39
  public isSupportedMethod(jsonRpcRequest: JsonRpcRequest): boolean {
@@ -1,27 +1,48 @@
1
1
  import type { EthereumProvider } from "../../../../../../types/providers.js";
2
2
 
3
3
  import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
4
+ import { min } from "@nomicfoundation/hardhat-utils/bigint";
4
5
  import { ensureError } from "@nomicfoundation/hardhat-utils/error";
5
6
  import {
6
7
  hexStringToNumber,
7
8
  numberToHexString,
8
9
  } from "@nomicfoundation/hardhat-utils/hex";
9
10
 
11
+ import { InternalCallOutOfGasError } from "../../../provider-errors.js";
12
+
13
+ /**
14
+ * The fraction of the observed block gas limit that gets cached and used as the
15
+ * gas limit ceiling. The cached value is deliberately lower than what the
16
+ * latest block reported, to leave room for the block gas limit varying
17
+ * slightly between blocks after we cached it.
18
+ */
19
+ export const BLOCK_GAS_LIMIT_SAFETY_FACTOR = 0.95;
20
+
10
21
  /**
11
22
  * This class handles gas estimation for transactions by applying a multiplier to the estimated gas value.
12
23
  * It requests a gas estimation from the provider and multiplies it by a predefined gas multiplier, ensuring the gas does not exceed the block's gas limit.
13
- * If an execution error occurs, the method returns the block's gas limit instead.
14
- * The block gas limit is cached after the first retrieval to optimize performance.
24
+ * If the estimation fails because an internal call runs out of gas regardless of the gas limit, the method returns the fallback gas limit, capped to the current block gas limit on networks that enforce one, or rethrows when no fallback gas limit is known.
25
+ * If any other error whose message contains "execution error" occurs, the method returns the capped block gas limit instead. Every other error is rethrown.
26
+ * The capped block gas limit is the latest block's gas limit scaled by `BLOCK_GAS_LIMIT_SAFETY_FACTOR`, cached after the first retrieval to optimize performance, except when capping the fallback gas limit, which reads the pending block's unscaled one.
15
27
  */
16
28
  export abstract class MultipliedGasEstimation {
17
29
  readonly #provider: EthereumProvider;
18
30
  readonly #gasMultiplier: number;
31
+ readonly #fallbackGas: bigint | undefined;
32
+ readonly #isBlockGasLimitEnforced: () => boolean;
19
33
 
20
34
  #blockGasLimit: number | undefined;
21
35
 
22
- constructor(provider: EthereumProvider, gasMultiplier: number) {
36
+ constructor(
37
+ provider: EthereumProvider,
38
+ gasMultiplier: number,
39
+ fallbackGas?: bigint,
40
+ isBlockGasLimitEnforced: () => boolean = () => true,
41
+ ) {
23
42
  this.#provider = provider;
24
43
  this.#gasMultiplier = gasMultiplier;
44
+ this.#fallbackGas = fallbackGas;
45
+ this.#isBlockGasLimitEnforced = isBlockGasLimitEnforced;
25
46
  }
26
47
 
27
48
  protected async getMultipliedGasEstimation(params: any[]): Promise<string> {
@@ -52,6 +73,34 @@ export abstract class MultipliedGasEstimation {
52
73
  } catch (error) {
53
74
  ensureError(error);
54
75
 
76
+ // We fall back instead of failing the transaction, because the user
77
+ // never asked for this estimation. The fallback is the gas limit the
78
+ // network itself uses for requests with no gas field. Connections that
79
+ // don't expose it (e.g. an http connection to a `hardhat node` server)
80
+ // get no fallback, and the estimation error reaches the user instead of
81
+ // a guessed limit that may not suit the remote network: the error names
82
+ // both remedies, an explicit gas limit or the topLevelSuccess mode.
83
+ //
84
+ // The cap reads the pending block's limit fresh rather than the cache:
85
+ // evm_setBlockGasLimit may have lowered it, and only the pending block
86
+ // reflects that before the next block is mined.
87
+ //
88
+ // Networks that don't enforce a block gas limit are exempt: their
89
+ // headers still report one, so capping would shrink the fallback to a
90
+ // limit nothing rejects it for.
91
+ if (
92
+ error instanceof InternalCallOutOfGasError &&
93
+ this.#fallbackGas !== undefined
94
+ ) {
95
+ if (!this.#isBlockGasLimitEnforced()) {
96
+ return numberToHexString(this.#fallbackGas);
97
+ }
98
+
99
+ const blockGasLimit = BigInt(await this.#fetchBlockGasLimit("pending"));
100
+
101
+ return numberToHexString(min(this.#fallbackGas, blockGasLimit));
102
+ }
103
+
55
104
  if (error.message.toLowerCase().includes("execution error")) {
56
105
  const blockGasLimitTmp = await this.#getBlockGasLimit();
57
106
  return numberToHexString(blockGasLimitTmp);
@@ -63,25 +112,31 @@ export abstract class MultipliedGasEstimation {
63
112
 
64
113
  async #getBlockGasLimit(): Promise<number> {
65
114
  if (this.#blockGasLimit === undefined) {
66
- const latestBlock = await this.#provider.request({
67
- method: "eth_getBlockByNumber",
68
- params: ["latest", false],
69
- });
70
-
71
- assertHardhatInvariant(
72
- typeof latestBlock === "object" &&
73
- latestBlock !== null &&
74
- "gasLimit" in latestBlock &&
75
- typeof latestBlock.gasLimit === "string",
76
- "latestBlock should have a gasLimit",
77
- );
78
-
79
- const fetchedGasLimit = hexStringToNumber(latestBlock.gasLimit);
115
+ const fetchedGasLimit = await this.#fetchBlockGasLimit("latest");
80
116
 
81
117
  // We store a lower value in case the gas limit varies slightly
82
- this.#blockGasLimit = Math.floor(fetchedGasLimit * 0.95);
118
+ this.#blockGasLimit = Math.floor(
119
+ fetchedGasLimit * BLOCK_GAS_LIMIT_SAFETY_FACTOR,
120
+ );
83
121
  }
84
122
 
85
123
  return this.#blockGasLimit;
86
124
  }
125
+
126
+ async #fetchBlockGasLimit(blockTag: "latest" | "pending"): Promise<number> {
127
+ const block = await this.#provider.request({
128
+ method: "eth_getBlockByNumber",
129
+ params: [blockTag, false],
130
+ });
131
+
132
+ assertHardhatInvariant(
133
+ typeof block === "object" &&
134
+ block !== null &&
135
+ "gasLimit" in block &&
136
+ typeof block.gasLimit === "string",
137
+ `the ${blockTag} block should have a gasLimit`,
138
+ );
139
+
140
+ return hexStringToNumber(block.gasLimit);
141
+ }
87
142
  }
@@ -63,10 +63,34 @@ export async function createHandlersArray<
63
63
  }
64
64
 
65
65
  if (networkConfig.gas === undefined || networkConfig.gas === "auto") {
66
+ // On EDR networks, if the gas estimation fails because an internal call
67
+ // runs out of gas regardless of the gas limit, we fall back to the same
68
+ // default transaction gas limit that the network uses for requests
69
+ // without a gas field. The EdrProvider exposes the exact value it was
70
+ // configured with, so the fallback always matches the provider's actual
71
+ // default. Providers that don't expose it (e.g. HTTP connections) get no
72
+ // fallback, and the handler surfaces the estimation error instead of
73
+ // guessing a limit that may not suit the remote network.
74
+ const provider = networkConnection.provider;
75
+ const fallbackGas =
76
+ "defaultTransactionGasLimit" in provider &&
77
+ typeof provider.defaultTransactionGasLimit === "bigint"
78
+ ? provider.defaultTransactionGasLimit
79
+ : undefined;
80
+
81
+ // The fallback only gets capped on networks that enforce a block gas
82
+ // limit. Read lazily, as enforcement can turn on at runtime; providers
83
+ // that don't report it are conservatively assumed to enforce one.
84
+ const isBlockGasLimitEnforced = () =>
85
+ !("isBlockGasLimitEnforced" in provider) ||
86
+ provider.isBlockGasLimitEnforced !== false;
87
+
66
88
  requestHandlers.push(
67
89
  new AutomaticGasHandler(
68
90
  networkConnection.provider,
69
91
  networkConfig.gasMultiplier,
92
+ fallbackGas,
93
+ isBlockGasLimitEnforced,
70
94
  ),
71
95
  );
72
96
  } else {
@@ -110,6 +110,7 @@ declare module "../../../../types/config.js" {
110
110
  blockGasLimit?: number | bigint | false;
111
111
  coinbase?: string;
112
112
  forking?: EdrNetworkForkingUserConfig;
113
+ gasEstimationMode?: "topLevelSuccess" | "noInternalOutOfGas";
113
114
  hardfork?: string;
114
115
  initialBaseFeePerGas?: number | bigint;
115
116
  initialDate?: string | Date;
@@ -253,6 +254,7 @@ declare module "../../../../types/config.js" {
253
254
  blockGasLimit?: bigint | false;
254
255
  coinbase: Uint8Array;
255
256
  forking?: EdrNetworkForkingConfig;
257
+ gasEstimationMode: "topLevelSuccess" | "noInternalOutOfGas";
256
258
  hardfork: string;
257
259
  initialBaseFeePerGas?: bigint;
258
260
  initialDate: string | Date;
@@ -319,6 +319,12 @@ const edrNetworkUserConfigSchema = z.object({
319
319
  blockGasLimit: z.optional(gasLimitWithDisableUserConfigSchema),
320
320
  coinbase: z.optional(z.string()),
321
321
  forking: z.optional(edrNetworkForkingUserConfigSchema),
322
+ gasEstimationMode: z.optional(
323
+ unionType(
324
+ [z.literal("topLevelSuccess"), z.literal("noInternalOutOfGas")],
325
+ "Expected 'topLevelSuccess' or 'noInternalOutOfGas'",
326
+ ),
327
+ ),
322
328
  hardfork: z.optional(z.string()),
323
329
  initialBaseFeePerGas: z.optional(gasUnitUserConfigSchema),
324
330
  initialDate: z.optional(
@@ -256,6 +256,7 @@ const _handleError = (error: Error): JsonRpcResponse => {
256
256
  // that would be lost when wrapped in InternalError below.
257
257
  const txHash = extractTxHash(error);
258
258
  const returnData = extractReturnData(error);
259
+ const reason = extractErrorReason(error);
259
260
 
260
261
  // Check if this is a revert error (code 3) matching the geth/anvil convention.
261
262
  const isRevertError =
@@ -271,9 +272,12 @@ const _handleError = (error: Error): JsonRpcResponse => {
271
272
 
272
273
  // Revert errors (code 3): return raw hex to match the geth/anvil convention
273
274
  // that viem/ethers/web3.js rely on. Other errors keep the wrapper for diagnostics.
275
+ // `reason` is the discriminator clients use to recognize specific failures
276
+ // (e.g. "InternalCallOutOfGas") once the error class itself is lost to
277
+ // serialization, so it must survive it.
274
278
  const data: unknown = isRevertError
275
279
  ? returnData
276
- : { message: error.message, txHash, data: returnData };
280
+ : { message: error.message, txHash, data: returnData, reason };
277
281
 
278
282
  const response: FailedJsonRpcResponse = {
279
283
  jsonrpc: "2.0",
@@ -318,3 +322,13 @@ function extractReturnData(error: Error): string | undefined {
318
322
  return error.data.data;
319
323
  }
320
324
  }
325
+
326
+ function extractErrorReason(error: Error): string | undefined {
327
+ if (
328
+ "data" in error &&
329
+ isObject(error.data) &&
330
+ typeof error.data.reason === "string"
331
+ ) {
332
+ return error.data.reason;
333
+ }
334
+ }
@@ -6,7 +6,7 @@ export const SELECTED_CONFLICT_REMEDIATION =
6
6
  "Rename one of the structs, or scope your `test.solidity.eip712Types.include` / `exclude` globs in `hardhat.config.ts` so that only one of them is selected.";
7
7
 
8
8
  export const DEPENDENCY_CONFLICT_REMEDIATION =
9
- "Rename one of the structs so the name has a single definition. The clashing name is pulled in as a dependency of a selected struct, so if you don't need that selected struct, deselecting it with your `test.solidity.eip712Types.include` / `exclude` globs in `hardhat.config.ts` also resolves the conflict.";
9
+ "Rename one of the structs so the name has a single definition. The clashing name is pulled in as a dependency of a selected struct, but none of its definitions is in a selected source, so you can also resolve the conflict with your `test.solidity.eip712Types.include` / `exclude` globs in `hardhat.config.ts`: include the source containing the canonical definition so it wins the clash, or deselect the struct that depends on it if you don't need it.";
10
10
 
11
11
  /**
12
12
  * Produces the flat list of canonical EIP-712 type strings expected by EDR.
@@ -220,12 +220,14 @@ function transitiveDeps(
220
220
  * - Both sides selected: throw immediately — genuinely ambiguous which to
221
221
  * emit.
222
222
  * - Current side non-selected: defer; the stored definition keeps the name.
223
- * Throw only if the name is reachable from a selected struct (its
224
- * transitive deps), since then which copy got inlined would depend on
225
- * iteration order. Names unreachable from the selected set are silently
226
- * deduped (first wins), so a non-selected struct that merely shares a
227
- * name with a selected root or an unreferenced struct never aborts the
228
- * run.
223
+ * Throw only if the name has no selected definition AND is reachable from
224
+ * a selected struct (its transitive deps), since only then which copy got
225
+ * inlined would depend on iteration order. A name with a selected
226
+ * definition always resolves to it deterministically (selected structs
227
+ * come first), so a non-selected struct that shares a name with a
228
+ * selected one never aborts the run — even when that name is also a
229
+ * dependency of another selected struct. Names unreachable from the
230
+ * selected set are likewise silently deduped (first wins).
229
231
  */
230
232
  function indexByName(
231
233
  structs: CollectedStruct[],
@@ -276,9 +278,9 @@ function indexByName(
276
278
 
277
279
  // Non-selected side, so the stored definition (selected if the name has
278
280
  // one, since selected sources come first) keeps the name. Defer: throw
279
- // later only if the name is reachable from a selected struct. Sharing a
280
- // name with a selected root or an unreferenced struct is harmless and
281
- // must not abort.
281
+ // later only if the name has no selected definition and is reachable from
282
+ // a selected struct. Sharing a name with a selected definition or an
283
+ // unreferenced struct is harmless and must not abort.
282
284
  if (!deferredConflicts.has(struct.name)) {
283
285
  deferredConflicts.set(struct.name, {
284
286
  firstSource: sourceByName.get(struct.name) ?? "<unknown>",
@@ -290,7 +292,7 @@ function indexByName(
290
292
  if (deferredConflicts.size > 0) {
291
293
  const reachable = reachableFromSelected(byName, selectedNames);
292
294
  for (const [name, sources] of deferredConflicts) {
293
- if (reachable.has(name)) {
295
+ if (reachable.has(name) && !selectedNames.has(name)) {
294
296
  throw new HardhatError(
295
297
  HardhatError.ERRORS.CORE.SOLIDITY_TESTS.EIP712_DUPLICATE_STRUCT_NAME,
296
298
  { name, ...sources, remediation: DEPENDENCY_CONFLICT_REMEDIATION },
@@ -32,11 +32,23 @@ export function formatInlineConfigErrors(
32
32
  return `- ${sourceName}: ${formatInlineConfigSourceProblem(error.problem)}`;
33
33
  }
34
34
 
35
- return `- ${sourceName}:${error.line}: ${error.contract}.${error.function}: ${formatInlineConfigDirectiveProblem(error.problem)}`;
35
+ return `- ${sourceName}:${error.line}: ${formatDirectiveOrigin(error.contract, error.function)}: ${formatInlineConfigDirectiveProblem(error.problem)}`;
36
36
  })
37
37
  .join("\n");
38
38
  }
39
39
 
40
+ /**
41
+ * Names where a directive was found: `Contract.testFn` for a function-level
42
+ * directive, and just `Contract` for a contract-level one, which EDR reports
43
+ * without a function.
44
+ */
45
+ function formatDirectiveOrigin(
46
+ contract: string,
47
+ functionName: string | undefined,
48
+ ): string {
49
+ return functionName === undefined ? contract : `${contract}.${functionName}`;
50
+ }
51
+
40
52
  function formatInlineConfigDirectiveProblem(
41
53
  problem: InlineConfigDirectiveProblem,
42
54
  ): string {
@@ -65,6 +77,6 @@ function formatInlineConfigSourceProblem(
65
77
  case "InlineConfigSourceFileNotFound":
66
78
  return `the source file could not be read at "${problem.path}": ${problem.reason}`;
67
79
  case "InlineConfigDirectiveLocation":
68
- return `a directive of ${problem.contract}.${problem.function} could not be located: ${problem.reason}`;
80
+ return `a directive of ${formatDirectiveOrigin(problem.contract, problem.function)} could not be located: ${problem.reason}`;
69
81
  }
70
82
  }
@@ -32,6 +32,13 @@ abstract class BaseResolvedConfigurationVariable implements ResolvedConfiguratio
32
32
 
33
33
  protected abstract _getRawValue(): Promise<string>;
34
34
 
35
+ /**
36
+ * A description of this variable, used to identify it in error messages.
37
+ *
38
+ * Resolved values may be secrets, so they must never be included in errors.
39
+ */
40
+ protected abstract _getDescription(): string;
41
+
35
42
  constructor(public readonly format: string) {
36
43
  assertHardhatInvariant(
37
44
  this.format.includes(CONFIGURATION_VARIABLE_MARKER),
@@ -57,9 +64,10 @@ abstract class BaseResolvedConfigurationVariable implements ResolvedConfiguratio
57
64
  new URL(value);
58
65
  return value;
59
66
  } catch (_error) {
60
- throw new HardhatError(HardhatError.ERRORS.CORE.GENERAL.INVALID_URL, {
61
- url: value,
62
- });
67
+ throw new HardhatError(
68
+ HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG_VARIABLE_URL,
69
+ { configVariable: this._getDescription() },
70
+ );
63
71
  }
64
72
  }
65
73
 
@@ -69,9 +77,10 @@ abstract class BaseResolvedConfigurationVariable implements ResolvedConfiguratio
69
77
  try {
70
78
  return BigInt(value);
71
79
  } catch (_error) {
72
- throw new HardhatError(HardhatError.ERRORS.CORE.GENERAL.INVALID_BIGINT, {
73
- value,
74
- });
80
+ throw new HardhatError(
81
+ HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG_VARIABLE_BIGINT,
82
+ { configVariable: this._getDescription() },
83
+ );
75
84
  }
76
85
  }
77
86
 
@@ -81,10 +90,8 @@ abstract class BaseResolvedConfigurationVariable implements ResolvedConfiguratio
81
90
  return normalizeHexString(value);
82
91
  } catch {
83
92
  throw new HardhatError(
84
- HardhatError.ERRORS.CORE.GENERAL.INVALID_HEX_STRING,
85
- {
86
- value,
87
- },
93
+ HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG_VARIABLE_HEX_STRING,
94
+ { configVariable: this._getDescription() },
88
95
  );
89
96
  }
90
97
  }
@@ -112,6 +119,10 @@ export class LazyResolvedConfigurationVariable extends BaseResolvedConfiguration
112
119
  }
113
120
  }
114
121
 
122
+ protected _getDescription(): string {
123
+ return `the configuration variable "${this.name}"`;
124
+ }
125
+
115
126
  protected async _getRawValue(): Promise<string> {
116
127
  // Env vars take precedence over every configurationVariables plugin hook
117
128
  // (e.g. keystore). Skip the hook chain entirely when the env var is set so
@@ -157,6 +168,10 @@ export class FixedValueConfigurationVariable extends BaseResolvedConfigurationVa
157
168
  this.#value = value;
158
169
  }
159
170
 
171
+ protected _getDescription(): string {
172
+ return "an inline configuration value";
173
+ }
174
+
160
175
  protected async _getRawValue(): Promise<string> {
161
176
  return this.#value;
162
177
  }
@@ -0,0 +1 @@
1
+ export { keccak256 } from "@nomicfoundation/edr";
@@ -5,7 +5,7 @@
5
5
  "description": "A TypeScript Hardhat project using Node Test Runner and Viem",
6
6
  "type": "module",
7
7
  "devDependencies": {
8
- "hardhat": "workspace:^3.15.0",
8
+ "hardhat": "workspace:^3.17.0",
9
9
  "@nomicfoundation/hardhat-toolbox-viem": "workspace:^5.0.7",
10
10
  "@nomicfoundation/hardhat-ignition": "workspace:^3.1.8",
11
11
  "@types/node": "^22.8.5",
@@ -5,9 +5,9 @@
5
5
  "description": "A TypeScript Hardhat project using Mocha and Ethers.js",
6
6
  "type": "module",
7
7
  "devDependencies": {
8
- "hardhat": "workspace:^3.15.0",
8
+ "hardhat": "workspace:^3.17.0",
9
9
  "@nomicfoundation/hardhat-toolbox-mocha-ethers": "workspace:^3.0.7",
10
- "@nomicfoundation/hardhat-ethers": "workspace:^4.0.15",
10
+ "@nomicfoundation/hardhat-ethers": "workspace:^4.1.0",
11
11
  "@nomicfoundation/hardhat-ignition": "workspace:^3.1.8",
12
12
  "@types/chai": "^5.2.3",
13
13
  "@types/chai-as-promised": "^8.0.1",
@@ -5,7 +5,7 @@
5
5
  "description": "A minimal Hardhat project",
6
6
  "type": "module",
7
7
  "devDependencies": {
8
- "hardhat": "workspace:^3.15.0",
8
+ "hardhat": "workspace:^3.17.0",
9
9
  "@types/node": "^22.8.5",
10
10
  "typescript": "~6.0.3"
11
11
  }