Hardhat Network and Ethers.js: Transaction Retry Logic and Nonce Synchronization
0 reputation · 12 Jun 2023, 18:31 UTC
0 reputation · 12 Jun 2023, 18:31 UTC
When integrating Ethers.js with the Hardhat Network, the provider manages the transaction lifecycle by wrapping RPC calls. A challenge arises when implementing custom retry logic for transactions that fail to reach a confirmed state due to network timeouts or provider-level interruptions.
The Hardhat Network enforces strict sequential nonce management. If a transaction is submitted and successfully included in a block, any subsequent retry using the same nonce is rejected by the EVM. Conversely, incrementing the nonce for a retry to bypass this rejection can create gaps in the sequence, potentially stalling future transactions from the same account.
Given that Hardhat does not provide a native retry mechanism, the responsibility falls on the client-side implementation to determine if a transaction was actually processed before attempting a resubmission.
32275 reputation · 13 Jun 2023, 01:08 UTC
1. How to confirm a transaction was actually mined before retrying?
provider.sendTransaction(tx), wait for the txHash and then call provider.waitForTransaction(txHash, 1) (or simply await provider.getTransactionReceipt(txHash)). If a receipt is returned and receipt.status === 1, the transaction reached a confirmed state.null or the call throws a timeout or execution reverted error, treat the transaction as “unconfirmed” and consider a retry.2. Recommended retry strategy that avoids nonce gaps
const nonce = await provider.getTransactionCount(address, "pending");
This returns the number of transactions that are already queued or mined, ensuring you use the next free nonce.getTransactionCount(..., "pending") handle it. This keeps the nonce sequence continuous even after a reset or a fork.hre.network.reset()), the nonce counter clears. Run a fresh getTransactionCount(..., "pending") immediately after to sync.nonce before each send. Verify that Hardhat increments correctly.timeout on waitForTransaction and confirm your retry logic re‑fetches the nonce.hre.network.reset(), call provider.getTransactionCount(address, "pending") and ensure it returns 0.Could you share how your current retry loop is structured? Knowing whether you use waitForTransaction or getTransactionReceipt directly will help fine‑tune the guidance.
Use comments to ask for clarification. Post a solution as an answer.
32,275 reputation · 13 Jun 2023, 06:07 UTC
The existing answer correctly recommends getTransactionCount(address, "pending"), but it's worth noting that Hardhat's handling of a dropped/pending transaction differs across versions. In Hardhat ≤ 2.19, a transaction that times out in the mempool may not release its nonce until the next block is mined, whereas newer releases (2.20+) drop it immediately on RPC timeout. This means a retry that re-fetches pending nonce too quickly can still collide with the stuck transaction on older versions.
Because the provider's internal pending queue is not standardized across Ethers.js v5/v6, the only reliable signal that a nonce is consumed is a non-null receipt from getTransactionReceipt(hash). If the receipt is absent after a reasonable timeout, treat the nonce as potentially reserved and either wait for the next block or explicitly call hre.network.provider.send("hardhat_dropTransaction", [hash]) (available in recent Hardhat) before resubmitting with a fresh pending nonce.
When firing multiple transactions concurrently from the same account, getTransactionCount(..., "pending") called in rapid succession can return the same value. Assign each transaction an explicit, incrementing nonce derived from a single pending fetch, then track receipts individually to avoid gaps.