From 6dfa35c9ee53b0def3f7c3acbea6ddab98b3bc56 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:41:53 +0000 Subject: [PATCH 01/20] poll for `revert` and `waitForTransaction` --- .../src/internal/matchers/emit.ts | 2 +- .../src/internal/matchers/reverted/revert.ts | 26 +++++--- .../src/internal/matchers/reverted/utils.ts | 4 +- .../test/matchers/events.ts | 55 +++++++++++++++++ .../test/matchers/reverted/revert.ts | 26 ++++++++ .../hardhat-ethers-provider.ts | 59 ++++++++++++++++--- 6 files changed, 154 insertions(+), 18 deletions(-) diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts index 76cf4da944e..dd29087df04 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts @@ -32,7 +32,7 @@ async function waitForPendingTransaction( chaiAssert.fail(`"${JSON.stringify(tx)}" is not a valid transaction`); } - return provider.getTransactionReceipt(hash); + return provider.waitForTransaction(hash); } export function supportEmit( diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts index e8c3bb90f92..a19afe92281 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts @@ -45,16 +45,28 @@ export function supportRevert( ); } - const receipt = await getTransactionReceipt(ethers, hash); - - if (receipt === null) { - // If the receipt is null, maybe the string is a bytes32 string - if (isBytes32String(hash)) { + // If the input is a raw string that is also a valid bytes32-encoded + // string, it might not be a real tx hash. Do a one-shot check to + // avoid polling forever. + if (typeof value === "string" && isBytes32String(hash)) { + const oneShotReceipt = + await ethers.provider.getTransactionReceipt(hash); + + if (oneShotReceipt === null) { assert(false, "Expected transaction to be reverted"); return; } + + assert( + oneShotReceipt.status === 0, + "Expected transaction to be reverted", + "Expected transaction NOT to be reverted", + ); + return; } + const receipt = await waitForTransactionReceipt(ethers, hash); + assertIsNotNull( receipt, "Transaction's receipt cannot be fetched from the network", @@ -126,8 +138,8 @@ export function supportRevert( ); } -async function getTransactionReceipt(ethers: HardhatEthers, hash: string) { - return ethers.provider.getTransactionReceipt(hash); +async function waitForTransactionReceipt(ethers: HardhatEthers, hash: string) { + return ethers.provider.waitForTransaction(hash); } function isTransactionResponse(x: unknown): x is { hash: string } { diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/utils.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/utils.ts index c716d6b99e7..4a3676d2243 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/utils.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/utils.ts @@ -79,7 +79,7 @@ export function decodeReturnData(returnData: string): DecodedReturnData { try { chaiAssert.fail( - `There was an error decoding "${encodedReason}" as a "string. Reason: ${cause.message}"`, + `There was an error decoding "${encodedReason}" as "string". Reason: ${cause.message}`, ); } catch (e) { ensureError(e); @@ -102,7 +102,7 @@ export function decodeReturnData(returnData: string): DecodedReturnData { try { chaiAssert.fail( - `There was an error decoding "${encodedReason}" as a "uint256. Reason: ${cause.message}"`, + `There was an error decoding "${encodedReason}" as "uint256". Reason: ${cause.message}`, ); } catch (e) { ensureError(e); diff --git a/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts b/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts index 2c9955c682e..268d3099b18 100644 --- a/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts +++ b/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts @@ -5,6 +5,7 @@ import type { OverrideEventContract, } from "../helpers/contracts.js"; import type { HardhatEthers } from "@nomicfoundation/hardhat-ethers/types"; +import type { EthereumProvider } from "hardhat/types/providers"; import { before, beforeEach, describe, it } from "node:test"; @@ -919,5 +920,59 @@ describe(".to.emit (contract events)", { timeout: 60000 }, () => { ); }); }); + + describe("When automining is disabled", () => { + let provider: EthereumProvider; + + before(async () => { + ({ provider } = await initEnvironment("events")); + }); + + it("should wait for the tx to be mined and detect the event", async () => { + await provider.request({ + method: "evm_setAutomine", + params: [false], + }); + + try { + const tx = contract.emitWithoutArgs(); + + const emitPromise = expect(tx).to.emit(contract, "WithoutArgs"); + + await provider.request({ method: "hardhat_mine", params: [] }); + + await emitPromise; + } finally { + await provider.request({ + method: "evm_setAutomine", + params: [true], + }); + } + }); + + it("should wait for the tx to be mined and verify event args", async () => { + await provider.request({ + method: "evm_setAutomine", + params: [false], + }); + + try { + const tx = contract.emitUint(1); + + const emitPromise = expect(tx) + .to.emit(contract, "WithUintArg") + .withArgs(1); + + await provider.request({ method: "hardhat_mine", params: [] }); + + await emitPromise; + } finally { + await provider.request({ + method: "evm_setAutomine", + params: [true], + }); + } + }); + }); } }); diff --git a/v-next/hardhat-ethers-chai-matchers/test/matchers/reverted/revert.ts b/v-next/hardhat-ethers-chai-matchers/test/matchers/reverted/revert.ts index abb8a331cec..f5d697e71ba 100644 --- a/v-next/hardhat-ethers-chai-matchers/test/matchers/reverted/revert.ts +++ b/v-next/hardhat-ethers-chai-matchers/test/matchers/reverted/revert.ts @@ -470,5 +470,31 @@ describe("INTEGRATION: Revert", { timeout: 60000 }, () => { expect.fail("Expected an exception but none was thrown"); }); }); + + describe("When automining is disabled", () => { + it("should wait for the tx to be mined and detect the revert", async () => { + await provider.request({ + method: "evm_setAutomine", + params: [false], + }); + + try { + const tx = await matchers.revertsWithoutReason({ + gasLimit: 1_000_000, + }); + + const revertPromise = expect(tx).to.be.revert(ethers); + + await provider.request({ method: "hardhat_mine", params: [] }); + + await revertPromise; + } finally { + await provider.request({ + method: "evm_setAutomine", + params: [true], + }); + } + }); + }); } }); diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index bdd6acfd13d..a409181ccb6 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -467,16 +467,59 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { } public async waitForTransaction( - _hash: string, + hash: string, _confirms?: number | undefined, - _timeout?: number | undefined, + timeout?: number | undefined, ): Promise { - throw new HardhatError( - HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED, - { - method: "HardhatEthersProvider.waitForTransaction", - }, - ); + const confirms = _confirms ?? 1; + + if (confirms === 0) { + return this.getTransactionReceipt(hash); + } + + return new Promise((resolve, reject) => { + let timeoutTimer: NodeJS.Timeout | undefined; + let pollingTimeout: NodeJS.Timeout | undefined; + + if (timeout !== undefined && timeout > 0) { + timeoutTimer = setTimeout(() => { + clearTimeout(pollingTimeout); + resolve(null); + }, timeout); + } + + const poll = async () => { + try { + const receipt = await this.getTransactionReceipt(hash); + + if (receipt !== null) { + if (timeoutTimer !== undefined) { + clearTimeout(timeoutTimer); + } + + resolve(receipt); + + return; + } + + const _isHardhatNetwork = await this.#isHardhatNetwork(); + const pollingInterval = _isHardhatNetwork ? 50 : 500; + + clearTimeout(pollingTimeout); + pollingTimeout = setTimeout(poll, pollingInterval); + } catch (e) { + ensureError(e); + + if (timeoutTimer !== undefined) { + clearTimeout(timeoutTimer); + } + + reject(e); + } + }; + + void poll(); + }); } public async waitForBlock( From f144be3c2104523cc31b4d65b623251519faca4a Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:42:19 +0000 Subject: [PATCH 02/20] tmp: bug repro --- .../example-project/test/mocha/mocha-test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/v-next/example-project/test/mocha/mocha-test.ts b/v-next/example-project/test/mocha/mocha-test.ts index a54f8b6d4c0..e42214c4c03 100644 --- a/v-next/example-project/test/mocha/mocha-test.ts +++ b/v-next/example-project/test/mocha/mocha-test.ts @@ -40,3 +40,51 @@ describe("Rocket test", () => { expect(await rocket.status()).to.equal("lift-off"); }); }); + +describe("Matchers without automining", () => { + it("emit should wait for the tx to be mined", async () => { + const { ethers, provider } = await hre.network.connect(); + + const Rocket = await ethers.getContractFactory("Rocket"); + const rocket = await Rocket.deploy("Apollo 11"); + + await provider.request({ method: "evm_setAutomine", params: [false] }); + + try { + const tx = await rocket.launch(); + const emitPromise = expect(tx).to.emit(rocket, "LaunchWithoutArgs"); + + await provider.request({ method: "hardhat_mine", params: [] }); + await emitPromise; + } finally { + await provider.request({ method: "evm_setAutomine", params: [true] }); + } + }); + + it("revert should wait for the tx to be mined", async () => { + const { ethers, provider } = await hre.network.connect(); + + const FailingContract = await ethers.getContractFactory("FailingContract"); + const failing = await FailingContract.deploy(); + + await provider.request({ method: "evm_setAutomine", params: [false] }); + + try { + // Send the tx manually because fail() is pure, which makes ethers + // use staticCall instead of sending a real transaction. + const [signer] = await ethers.getSigners(); + const tx = await signer.sendTransaction({ + to: await failing.getAddress(), + data: failing.interface.encodeFunctionData("fail"), + gasLimit: 1_000_000, + }); + + const revertPromise = expect(tx).to.be.revert(ethers); + + await provider.request({ method: "hardhat_mine", params: [] }); + await revertPromise; + } finally { + await provider.request({ method: "evm_setAutomine", params: [true] }); + } + }); +}); From 08a5c718ed121a5157e48fecea411fcc1108f8b0 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:58:29 +0000 Subject: [PATCH 03/20] add unit tests for `waitForTransaction` --- .../test/hardhat-ethers-provider.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts index 9a95e208957..5c278e43934 100644 --- a/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts @@ -962,6 +962,48 @@ describe("hardhat ethers provider", () => { }); }); + describe("waitForTransaction", () => { + it("should wait for a transaction and return its receipt", async () => { + const signer = await ethers.provider.getSigner(0); + const tx = await signer.sendTransaction({ to: signer.address }); + + const receipt = await ethers.provider.waitForTransaction(tx.hash); + + assertIsNotNull(receipt); + assert.equal(receipt.hash, tx.hash); + assert.equal(receipt.status, 1); + }); + + it("should return the receipt immediately when confirms is 0", async () => { + const signer = await ethers.provider.getSigner(0); + const tx = await signer.sendTransaction({ to: signer.address }); + + const receipt = await ethers.provider.waitForTransaction(tx.hash, 0); + + assertIsNotNull(receipt); + assert.equal(receipt.hash, tx.hash); + }); + + it("should return null when confirms is 0 and the transaction doesn't exist", async () => { + const receipt = await ethers.provider.waitForTransaction( + "0x0000000000000000000000000000000000000000000000000000000000000000", + 0, + ); + + assert.equal(receipt === null, true); + }); + + it("should resolve to null when the timeout is reached", async () => { + const receipt = await ethers.provider.waitForTransaction( + "0x0000000000000000000000000000000000000000000000000000000000000000", + 1, + 100, + ); + + assert.equal(receipt === null, true); + }); + }); + describe("getLogs", () => { // keccak("Inc()") const INC_EVENT_TOPIC = From 670fd5b08d49084d76779be1a1046c2013bf228a Mon Sep 17 00:00:00 2001 From: Christopher Dedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:06:21 +0100 Subject: [PATCH 04/20] Create honest-spies-play.md --- .changeset/honest-spies-play.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/honest-spies-play.md diff --git a/.changeset/honest-spies-play.md b/.changeset/honest-spies-play.md new file mode 100644 index 00000000000..8ef717f7a8e --- /dev/null +++ b/.changeset/honest-spies-play.md @@ -0,0 +1,6 @@ +--- +"@nomicfoundation/hardhat-ethers-chai-matchers": patch +"@nomicfoundation/hardhat-ethers": patch +--- + +Added support to `hardhat-ethers-chai-matchers` for networks that do not support `automine` ([7952](https://github.com/NomicFoundation/hardhat/issues/7952)). From df7f24a27d01c5b49379c7a1b12b507d734395d7 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:29:57 +0000 Subject: [PATCH 05/20] Add peer bump files --- .changeset/brave-tigers-wave.md | 5 +++++ .peer-bumps.json | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-tigers-wave.md diff --git a/.changeset/brave-tigers-wave.md b/.changeset/brave-tigers-wave.md new file mode 100644 index 00000000000..3e5cfba11a1 --- /dev/null +++ b/.changeset/brave-tigers-wave.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/hardhat-ethers": patch +--- + +Implemented `HardhatEthersProvider.waitForTransaction` with polling support for `non-automining` networks ([#7952](https://github.com/NomicFoundation/hardhat/issues/7952)). diff --git a/.peer-bumps.json b/.peer-bumps.json index 5ed8663a7f8..e26de1de7ac 100644 --- a/.peer-bumps.json +++ b/.peer-bumps.json @@ -6,5 +6,11 @@ "v-next/hardhat/templates", "v-next/config" ], - "bumps": [] + "bumps": [ + { + "package": "@nomicfoundation/hardhat-ethers-chai-matchers", + "peer": "@nomicfoundation/hardhat-ethers", + "reason": "Requires waitForTransaction implementation from hardhat-ethers for non-automining network support (#7952)" + } + ] } From df3c60ffb6bcfe8404481e68e325f7150fa44807 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:34:07 +0000 Subject: [PATCH 06/20] update changeset for hh-chai --- .changeset/honest-spies-play.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/honest-spies-play.md b/.changeset/honest-spies-play.md index 8ef717f7a8e..734ad606192 100644 --- a/.changeset/honest-spies-play.md +++ b/.changeset/honest-spies-play.md @@ -1,6 +1,5 @@ --- "@nomicfoundation/hardhat-ethers-chai-matchers": patch -"@nomicfoundation/hardhat-ethers": patch --- Added support to `hardhat-ethers-chai-matchers` for networks that do not support `automine` ([7952](https://github.com/NomicFoundation/hardhat/issues/7952)). From 6a5fe85541e0f60b448e5dae0603ac8fc713899d Mon Sep 17 00:00:00 2001 From: Christopher Dedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:48:39 +0100 Subject: [PATCH 07/20] Update .changeset/brave-tigers-wave.md Co-authored-by: John Kane --- .changeset/brave-tigers-wave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/brave-tigers-wave.md b/.changeset/brave-tigers-wave.md index 3e5cfba11a1..a79809bab68 100644 --- a/.changeset/brave-tigers-wave.md +++ b/.changeset/brave-tigers-wave.md @@ -2,4 +2,4 @@ "@nomicfoundation/hardhat-ethers": patch --- -Implemented `HardhatEthersProvider.waitForTransaction` with polling support for `non-automining` networks ([#7952](https://github.com/NomicFoundation/hardhat/issues/7952)). +Added `HardhatEthersProvider.waitForTransaction` to proved polling support for `non-automining` networks ([#7952](https://github.com/NomicFoundation/hardhat/issues/7952)). From eb16806069ca877fd79b29c8154c3ad873471dc6 Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 10:26:17 +0000 Subject: [PATCH 08/20] chore: tweak changeset description --- .changeset/brave-tigers-wave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/brave-tigers-wave.md b/.changeset/brave-tigers-wave.md index a79809bab68..d7c7acddbec 100644 --- a/.changeset/brave-tigers-wave.md +++ b/.changeset/brave-tigers-wave.md @@ -2,4 +2,4 @@ "@nomicfoundation/hardhat-ethers": patch --- -Added `HardhatEthersProvider.waitForTransaction` to proved polling support for `non-automining` networks ([#7952](https://github.com/NomicFoundation/hardhat/issues/7952)). +Added `HardhatEthersProvider.waitForTransaction` to provide polling support for `non-automining` networks ([#7952](https://github.com/NomicFoundation/hardhat/issues/7952)). From 4ddbc68a13de58564c93585dcaef694e7299ada0 Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 10:32:52 +0000 Subject: [PATCH 09/20] refactor: rename confirms variable to meet usage convention --- .../hardhat-ethers-provider/hardhat-ethers-provider.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index a409181ccb6..9795f8ea3d3 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -468,12 +468,12 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { public async waitForTransaction( hash: string, - _confirms?: number | undefined, + confirms?: number | undefined, timeout?: number | undefined, ): Promise { - const confirms = _confirms ?? 1; + const resolvedConfirms = confirms ?? 1; - if (confirms === 0) { + if (resolvedConfirms === 0) { return this.getTransactionReceipt(hash); } From 9cda25f606215a20e92e6bff3003b1d45ce2d19a Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 10:37:09 +0000 Subject: [PATCH 10/20] refactor: extract constant --- .../hardhat-ethers-provider/hardhat-ethers-provider.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index 9795f8ea3d3..7edde7da324 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -54,6 +54,9 @@ import { HardhatEthersSigner } from "../signers/signers.js"; const log = debug("hardhat:hardhat-ethers:provider"); +// The default number of confirmations when waiting for a transaction +const DEFAULT_TRANSACTION_CONFIRMS = 1; + interface ListenerItem { listener: Listener; once: boolean; @@ -471,7 +474,7 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { confirms?: number | undefined, timeout?: number | undefined, ): Promise { - const resolvedConfirms = confirms ?? 1; + const resolvedConfirms = confirms ?? DEFAULT_TRANSACTION_CONFIRMS; if (resolvedConfirms === 0) { return this.getTransactionReceipt(hash); From f2234be352e1eb73e1243de77ceefdc3d9fec0ce Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 11:00:12 +0000 Subject: [PATCH 11/20] refactor: tweak to add not case message This is to bring this assertion in line with the other `assert` assertions in the file. --- .../src/internal/matchers/reverted/revert.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts index a19afe92281..044b3386e0f 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts @@ -53,7 +53,12 @@ export function supportRevert( await ethers.provider.getTransactionReceipt(hash); if (oneShotReceipt === null) { - assert(false, "Expected transaction to be reverted"); + assert( + false, + "Expected transaction to be reverted", + "Expected transaction NOT to be reverted", + ); + return; } @@ -62,6 +67,7 @@ export function supportRevert( "Expected transaction to be reverted", "Expected transaction NOT to be reverted", ); + return; } From d3eef666666cbe4300ad1fdfbc96513e30c25585 Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 11:34:12 +0000 Subject: [PATCH 12/20] fix: avoid race condition between poll and timeout --- .../hardhat-ethers-provider/hardhat-ethers-provider.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index 7edde7da324..82824b02cfa 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -481,21 +481,29 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { } return new Promise((resolve, reject) => { + let cancelled = false; let timeoutTimer: NodeJS.Timeout | undefined; let pollingTimeout: NodeJS.Timeout | undefined; if (timeout !== undefined && timeout > 0) { timeoutTimer = setTimeout(() => { + cancelled = true; clearTimeout(pollingTimeout); resolve(null); }, timeout); } const poll = async () => { + if (cancelled) { + return; + } + try { const receipt = await this.getTransactionReceipt(hash); if (receipt !== null) { + cancelled = true; + if (timeoutTimer !== undefined) { clearTimeout(timeoutTimer); } @@ -513,6 +521,8 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { } catch (e) { ensureError(e); + cancelled = true; + if (timeoutTimer !== undefined) { clearTimeout(timeoutTimer); } From fb64d995086d10a5035be470a71b75520bf3b97d Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 11:37:25 +0000 Subject: [PATCH 13/20] needless whitespace for my clarity --- .../hardhat-ethers-provider/hardhat-ethers-provider.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index 82824b02cfa..f165bcc8d09 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -488,7 +488,9 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { if (timeout !== undefined && timeout > 0) { timeoutTimer = setTimeout(() => { cancelled = true; + clearTimeout(pollingTimeout); + resolve(null); }, timeout); } @@ -517,6 +519,7 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { const pollingInterval = _isHardhatNetwork ? 50 : 500; clearTimeout(pollingTimeout); + pollingTimeout = setTimeout(poll, pollingInterval); } catch (e) { ensureError(e); From cfea545cc460b31739020a9109a241c0e0630cfd Mon Sep 17 00:00:00 2001 From: John Kane Date: Thu, 5 Mar 2026 11:39:45 +0000 Subject: [PATCH 14/20] Update v-next/hardhat-ethers/test/hardhat-ethers-provider.ts --- v-next/hardhat-ethers/test/hardhat-ethers-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts index 5c278e43934..dca2ef0f688 100644 --- a/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts @@ -997,7 +997,7 @@ describe("hardhat ethers provider", () => { const receipt = await ethers.provider.waitForTransaction( "0x0000000000000000000000000000000000000000000000000000000000000000", 1, - 100, + 1, // 1 millisecond timeout ); assert.equal(receipt === null, true); From 7b0535e1df85dbe75863a9d5e6cbc74c5464d509 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:33:38 +0000 Subject: [PATCH 15/20] add PR bumps --- .peer-bumps.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.peer-bumps.json b/.peer-bumps.json index 5ed8663a7f8..e26de1de7ac 100644 --- a/.peer-bumps.json +++ b/.peer-bumps.json @@ -6,5 +6,11 @@ "v-next/hardhat/templates", "v-next/config" ], - "bumps": [] + "bumps": [ + { + "package": "@nomicfoundation/hardhat-ethers-chai-matchers", + "peer": "@nomicfoundation/hardhat-ethers", + "reason": "Requires waitForTransaction implementation from hardhat-ethers for non-automining network support (#7952)" + } + ] } From e52c9683e57a06ad7d99a6ba7f170c8fccfa6be7 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:18:52 +0000 Subject: [PATCH 16/20] guard against invalid and ambiguous tx hash strings in emit matcher --- .../src/internal/matchers/emit.ts | 26 +++++++++++++++++++ .../src/internal/matchers/reverted/revert.ts | 7 ++--- .../test/matchers/events.ts | 22 ++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts index 7a89ab3734d..51bf4c0cf89 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts @@ -6,7 +6,9 @@ import type { Transaction } from "ethers/transaction"; import util from "node:util"; +import { isHash } from "@nomicfoundation/hardhat-utils/eth"; import { assert as chaiAssert, AssertionError } from "chai"; +import { decodeBytes32String } from "ethers/abi"; import { ASSERTION_ABORTED, EMIT_MATCHER } from "../constants.js"; import { assertArgsArraysEqual, assertIsNotNull } from "../utils/asserts.js"; @@ -32,6 +34,21 @@ async function waitForPendingTransaction( chaiAssert.fail(`"${JSON.stringify(tx)}" is not a valid transaction`); } + if (typeof tx === "string") { + if (!isHash(hash)) { + chaiAssert.fail( + `Expected a valid transaction hash, but got "${hash}"`, + ); + } + + // If the input is a raw string that is also a valid bytes32-encoded + // string, it might not be a real tx hash. Do a one-shot check to + // avoid polling forever. + if (isBytes32String(hash)) { + return provider.getTransactionReceipt(hash); + } + } + return provider.waitForTransaction(hash); } @@ -229,3 +246,12 @@ const tryAssertArgsArraysEqual = ( } emitted "${eventName}" events`, ); }; + +function isBytes32String(v: string): boolean { + try { + decodeBytes32String(v); + return true; + } catch { + return false; + } +} diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts index 044b3386e0f..3b12871f1f6 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/reverted/revert.ts @@ -1,5 +1,6 @@ import type { HardhatEthers } from "@nomicfoundation/hardhat-ethers/types"; +import { isHash } from "@nomicfoundation/hardhat-utils/eth"; import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex"; import { assert as chaiAssert } from "chai"; @@ -39,7 +40,7 @@ export function supportRevert( if (isTransactionResponse(value) || typeof value === "string") { const hash = typeof value === "string" ? value : value.hash; - if (!isValidTransactionHash(hash)) { + if (!isHash(hash)) { chaiAssert.fail( `Expected a valid transaction hash, but got "${hash}"`, ); @@ -169,10 +170,6 @@ function isTransactionReceipt(x: unknown): x is { status: number } { return false; } -function isValidTransactionHash(x: string): boolean { - return /0x[0-9a-fA-F]{64}/.test(x); -} - function isBytes32String(v: string): boolean { try { parseBytes32String(v); diff --git a/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts b/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts index 27eaedb88e3..6104518b905 100644 --- a/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts +++ b/v-next/hardhat-ethers-chai-matchers/test/matchers/events.ts @@ -897,6 +897,28 @@ describe(".to.emit (contract events)", { timeout: 60000 }, () => { await expect(tx.hash).to.emit(contract, "WithoutArgs"); }); + it("With an invalid transaction hash string", async () => { + await assertRejects( + () => expect("0x123").to.emit(contract, "WithoutArgs"), + (e) => + e.message.includes( + 'Expected a valid transaction hash, but got "0x123"', + ), + "Expected invalid transaction hash error message", + ); + }); + + it("With a bytes32-encoded string that is not a real tx hash", async () => { + await expect( + expect( + "0x3230323400000000000000000000000000000000000000000000000000000000", + ).to.emit(contract, "WithoutArgs"), + ).to.be.eventually.rejectedWith( + AssertionError, + "Transaction's receipt cannot be fetched from the network", + ); + }); + describe("When event is overloaded", () => { it("should fail when the event name is ambiguous", async () => { await expect( From 3361fb06841edfd3e18033ee34621158f826be3c Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:50:39 +0000 Subject: [PATCH 17/20] extract `_isHardhatNetwork` from loop --- .../hardhat-ethers-provider/hardhat-ethers-provider.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index f165bcc8d09..2ef063a9767 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -480,6 +480,8 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { return this.getTransactionReceipt(hash); } + const pollingInterval = (await this.#isHardhatNetwork()) ? 50 : 500; + return new Promise((resolve, reject) => { let cancelled = false; let timeoutTimer: NodeJS.Timeout | undefined; @@ -515,9 +517,6 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { return; } - const _isHardhatNetwork = await this.#isHardhatNetwork(); - const pollingInterval = _isHardhatNetwork ? 50 : 500; - clearTimeout(pollingTimeout); pollingTimeout = setTimeout(poll, pollingInterval); From 9acae43c3124aeda2f3db2852ecd5efa42e69c46 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:53:42 +0000 Subject: [PATCH 18/20] honour confirms parameter in waitForTransaction --- .../hardhat-ethers-provider.ts | 23 +++++--- .../test/hardhat-ethers-provider.ts | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts index 2ef063a9767..4b71e3359e7 100644 --- a/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/src/internal/hardhat-ethers-provider/hardhat-ethers-provider.ts @@ -505,16 +505,25 @@ export class HardhatEthersProvider implements HardhatEthersProviderI { try { const receipt = await this.getTransactionReceipt(hash); - if (receipt !== null) { - cancelled = true; + // Wait for the required confirmation depth before resolving, + // so callers relying on confirmations for reorg safety aren't + // given a receipt that could still be reverted. + if (receipt !== null && receipt.blockNumber !== null) { + const latestBlockNumber = await this.getBlockNumber(); + const confirmations = latestBlockNumber - receipt.blockNumber + 1; - if (timeoutTimer !== undefined) { - clearTimeout(timeoutTimer); - } + if (confirmations >= resolvedConfirms) { + cancelled = true; + + if (timeoutTimer !== undefined) { + clearTimeout(timeoutTimer); + } - resolve(receipt); + clearTimeout(pollingTimeout); + resolve(receipt); - return; + return; + } } clearTimeout(pollingTimeout); diff --git a/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts b/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts index dca2ef0f688..249c7e717e1 100644 --- a/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts +++ b/v-next/hardhat-ethers/test/hardhat-ethers-provider.ts @@ -19,6 +19,7 @@ import { assertIsNotNull, assertWithin, initializeTestEthers, + sleep, } from "./helpers/helpers.js"; describe("hardhat ethers provider", () => { @@ -1002,6 +1003,62 @@ describe("hardhat ethers provider", () => { assert.equal(receipt === null, true); }); + + it("should wait for the specified number of confirmations before resolving", async () => { + const confirms = 3; + const signer = await ethers.provider.getSigner(0); + + // disable automining so we control block production + await ethereumProvider.request({ + method: "evm_setAutomine", + params: [false], + }); + + const tx = await signer.sendTransaction({ to: signer.address }); + + let resolved = false; + const waitPromise = ethers.provider + .waitForTransaction(tx.hash, confirms) + .then((r) => { + resolved = true; + return r; + }); + + // mine the transaction into a block (1 confirmation) + await ethereumProvider.request({ method: "hardhat_mine" }); + + // give the polling loop time to check + await sleep(100); + assert.equal( + resolved, + false, + "should not resolve with only 1 confirmation", + ); + + // mine a second block (2 confirmations) + await ethereumProvider.request({ method: "hardhat_mine" }); + await sleep(100); + assert.equal( + resolved, + false, + "should not resolve with only 2 confirmations", + ); + + // mine a third block (3 confirmations) — should now resolve + await ethereumProvider.request({ method: "hardhat_mine" }); + + const receipt = await waitPromise; + assert.equal(resolved, true); + assertIsNotNull(receipt); + assert.equal(receipt.hash, tx.hash); + assert.equal(receipt.status, 1); + + // restore automining + await ethereumProvider.request({ + method: "evm_setAutomine", + params: [true], + }); + }); }); describe("getLogs", () => { From cc0c382b5ddf7ed4be24b2f7bc75f2ed112828d2 Mon Sep 17 00:00:00 2001 From: ChristopherDedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:54:05 +0000 Subject: [PATCH 19/20] lint:fix --- .../src/internal/matchers/emit.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts index 51bf4c0cf89..22b55389fee 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts @@ -36,9 +36,7 @@ async function waitForPendingTransaction( if (typeof tx === "string") { if (!isHash(hash)) { - chaiAssert.fail( - `Expected a valid transaction hash, but got "${hash}"`, - ); + chaiAssert.fail(`Expected a valid transaction hash, but got "${hash}"`); } // If the input is a raw string that is also a valid bytes32-encoded From a8baa1a21d64b792c20c134a912127be22264903 Mon Sep 17 00:00:00 2001 From: John Kane Date: Wed, 25 Mar 2026 12:46:34 +0000 Subject: [PATCH 20/20] refactor: use util version of `parseBytes32String` --- .../src/internal/matchers/emit.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts index 22b55389fee..6dca3f45702 100644 --- a/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts +++ b/v-next/hardhat-ethers-chai-matchers/src/internal/matchers/emit.ts @@ -8,13 +8,14 @@ import util from "node:util"; import { isHash } from "@nomicfoundation/hardhat-utils/eth"; import { assert as chaiAssert, AssertionError } from "chai"; -import { decodeBytes32String } from "ethers/abi"; import { ASSERTION_ABORTED, EMIT_MATCHER } from "../constants.js"; import { assertArgsArraysEqual, assertIsNotNull } from "../utils/asserts.js"; import { buildAssert } from "../utils/build-assert.js"; import { preventAsyncMatcherChaining } from "../utils/prevent-chaining.js"; +import { parseBytes32String } from "./reverted/utils.js"; + export const EMIT_CALLED = "emitAssertionCalled"; async function waitForPendingTransaction( @@ -247,7 +248,7 @@ const tryAssertArgsArraysEqual = ( function isBytes32String(v: string): boolean { try { - decodeBytes32String(v); + parseBytes32String(v); return true; } catch { return false;