Repository navigation
fix: reset nonceManager after failed sends - #4634
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: f0af207 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
jxom
force-pushed
the
jxom/fix-nonce-manager-reset
branch
from
May 18, 2026 02:22
8cde015 to
f0af207
Compare
Merged
aelmanaa
added a commit
to aelmanaa/hardhat-kms
that referenced
this pull request
Oct 3, 2026
…nces (#199) Closes #186 ## What A `connection.kms.getAccount` account signs transactions that viem fills and sends itself. Those sends bypassed the plugin's send lock and nonce high-water mark, so a concurrent send through the plugin from the same address could take the same nonce. This PR keeps the two paths on distinct nonces without rewriting any RPC answer: - **A viem `nonceManager` on the account.** In viem, `toAccount` copies `account.nonceManager`. `prepareTransactionRequest` calls `consume({ address, chainId, client })` once per send without a nonce. `sendTransaction` calls `reset({ address, chainId })` when the send fails. `consume` takes the account's send lock `chainId:address` and picks the nonce the way a plugin send would. It reads the pending count through the connection (the read passes through untouched), then goes past the high-water mark and past reserved nonces. - **Client through the connection** (transport `custom`): the send keeps the lock until its `eth_sendRawTransaction` goes out or viem resets it. A plugin send also keeps the lock from its fill to its broadcast. - **Client with its own transport** (`http`, `webSocket`, `fallback`): the lock is released at once, and the nonce is reserved. Plugin sends skip the reserved nonce without waiting. The reservation ends at `reset`, when a raw transaction with that nonce reaches the node, when a plugin send with the caller's nonce at or above it goes out, or after 60 s. Expiry is checked against the clock on each use, so there is no timer. A warning, once per process, says the broadcast is not ordered. - **`eth_sendRawTransaction`** and **`eth_sendRawTransactionSync`** (EIP-7966, sent by viem's `sendTransactionSync` and `writeContractSync`) from a KMS account of the connection are decoded with micro-eth-signer, which is already a dependency, and the sender is recovered. - If a library send holds the lock with that nonce, the transaction goes out as that holder. Otherwise it takes the lock around its one `next`. - The answer comes back unchanged. - When the node has the transaction (accepted, a receipt, mined with an error, "already known", or the sync method's timeout code 4, which means it is in the pool), the nonce raises the mark and ends its reservation. - If there is no answer, or an "I don't know" from a gateway, the transaction becomes the sender's uncertain transaction, and the next plugin send asks the node about it first. - Re-entrant sends fail at once. ## Changes from the first version, after review - The `eth_getTransactionCount` / `eth_fillTransaction` interception and the read leases are removed. Architecture rule 1 is restored: the only added inspected method is `eth_sendRawTransaction`, and its answer is unchanged. Pending and latest reads come back as the node gave them. - No send waits on a timer in the normal flow. The CLI test below reproduced exit code 13 with the first version and passes with this one. - A reverted estimate makes viem call `reset`, which releases the hold at once. Plain reads do nothing. Explicit-nonce plugin sends clear reservations. A stream of reads cannot starve anything. ## Changes after the second review - **`eth_sendRawTransactionSync`** takes the same branch as `eth_sendRawTransaction`. Before, viem's `sendTransactionSync` and `writeContractSync` consumed through the nonce manager and broadcast with the sync method, so the hold never ended (the next sends waited 60 s) and the nonce was not recorded. A receipt, or EIP-7966's timeout (code 4, in the pool), counts as the node having it; other errors and uncertain outcomes behave as for the async method. - **A reset no longer ends a hold that belongs to another consume.** Per-consume ownership keyed by `client.uid` is not possible: `consume` gets the client, but viem passes `reset` only `{ address, chainId }` (checked in 2.55.13, 2.56.0 and 2.57.1), so a reset cannot be matched to its consume, and routing by transport kind has the same gap. `resetLibraryNonce` now decides in order: an owed reset is used up; with no live reservation, the reset ends the hold; with a hold and reservations both open, reservations below the node's pending count are dropped first (their sends are past their broadcast), and a reservation that is left takes the reset while the hold stays. A wrong guess now costs at most the held send's 60 s, never a shared nonce. The reviewer's repro (X=0 reserved from an `http` client, Y=1 held from a `custom` client, X's reset) is a unit test: Y keeps the lock, the plugin send goes after Y, and the nonces are distinct. - **Late events after a 60 s expiry.** A held send's raw transaction now ends only the hold it matched when it arrived (the captured hold object, not the map entry), so a late raw transaction cannot end the next hold. A late `reset` still can, since it carries no identity; this is documented. - **`consume` settles an uncertain transaction first**, as a plugin send does, before it chooses the nonce. - **A raw transaction on a connection that never looked up its KMS addresses** still ends a library send's hold when one is active: the sender is recovered when any library hold exists, and the transaction counts only if a hold exists for that sender on that chain. No KMS call. A `custom` transport over another provider still leaves a 60 s hold that keeps the process alive; documented. - Docs: decision 0014's bullet rewritten, "never share a nonce" now "within the cases in Sending", the EDR error quote corrected, the sync actions and mixed transports documented, and the changeset tells npm users with an exact viem pin below 2.55.13 to bump it. ## Changes after the third review Two decisions from the third review: a plugin send keeps waiting behind a library send that holds the lock, and the viem floor is 2.55.13. - **(a) Floor 2.55.13.** The viem peer range in `packages/hardhat-kms/package.json` is `^2.55.13`. The changesets, `transactions.md`, `library-accounts.md`, decision 0014, the `core.account.viem-too-old` entry in the errors reference and both READMEs say 2.55.13, and describe the releases below it as "2.49.3 and earlier" and "2.50.3 to 2.55.11". `test:sdk-floors` installs viem 2.55.13. - **(b) Runtime version check.** pnpm and Yarn install a viem below the peer range with only a warning. `getAccount` now reads the version from viem's `package.json` and refuses one below 2.55.13 with `core.account.viem-too-old` ("connection.kms.getAccount needs viem 2.55.13 or later, and the project has viem 2.55.11. Upgrade viem to 2.55.13 or later"), before the key lookup and any KMS call. Pre-releases order as semver (`2.55.13-canary.0` is refused; `3.0.0-next.12` passes). A version it cannot read passes. The guard stays in `getAccount`, so sign-only use on old viem is refused too. The version comes from `viem/package.json`, which viem exports. `new BaseError("").version` would also work (a bare `X.Y.Z` in every release from 2.47.6 to 2.57.2), but constructing an error there fails the repo's error-catalogue check. The check reads the viem that hardhat-kms resolves; in a workspace where the client's code loads a different, nested viem, it can pass for a client below the floor. The docs call it best effort. - **(c) EDR and viem error text.** `transactions.md` and `library-accounts.md` quote EDR 0.22.0's own refusal ("Nonce too high. Expected nonce to be 0 but got 1. Note that transactions can't be queued when automining.") and say that viem's `NonceTooHighError` wraps it: a viem client prints "Nonce provided for the transaction is higher than the next one expected." with EDR's text after `Details:`. EDR queues a gap only with `mining.auto: false`. - **(d) Own-transport gap.** A client with `http(url)` gets a reserved nonce, and the plugin skips it. On an automining node, a plugin send that reaches the node first is refused. `library-accounts.md` documents this and the two fixes: send through `custom(connection.provider)`, or wait for the library send's receipt. A new integration test pins it, and the once-per-process own-transport warning now says an automining node can refuse the next plugin send. - **(e) Warnings.** When sends wait 5 s (`LIBRARY_WAIT_WARNING_MS`) behind a library send that holds the account's lock, a warning prints, at most once per hold and at most once per waiting send, with the checksummed account, the chain and the held nonce. When the 60 s limit ends a hold, a warning says the raw transaction did not reach the plugin (as with a `custom` transport over another provider), that if it is broadcast later it and the next send share a nonce and the node refuses one, and to send through `custom(connection.provider)`. Both link `library-accounts.md#warnings`. A wait behind a plugin send, a wait under 5 s, and a hold that ends by broadcast, `reset` or close print nothing. The test "warns and ends a hold before a send waiting behind it could stall" pins the order 5 s < 60 s < 120 s (`SEND_LOCK_STALL_MS`). - **(f) README.** Both READMEs explain why the floor is above hardhat-viem's `^2.47.6`: with viem 2.49.3 and earlier a failed library send never resets, so the next plugin send waits up to 60 s; with 2.50.3 to 2.55.11 a stray `reset` can end another send's hold. ## Changes after the fourth review - Version ranges read "2.49.3 and earlier" and "2.50.3 to 2.55.11" everywhere; `transactions.md` says 2.50.0 to 2.50.2 and 2.55.12 were never published to npm and cites wevm/viem#4634. - `core.account.viem-too-old`'s cause states what each range does to the user, and that npm refuses only by default (`--legacy-peer-deps` installs anyway). `core.account.viem-missing` now says "Install viem in the project", with npm, pnpm and Yarn commands in the fix line. - The 5 s warning prints at most once per hold (`waitWarned` on the hold) and at most once per waiting send, so a send that waits behind two holds in a row prints one. Both warnings print checksummed addresses, say what to do, and link `library-accounts.md#warnings`. The own-transport warning links the section on clients with their own transport. - `holdForLibrary`'s `end` uses one guard (the hold is still the current one), so a second call is a no-op and an old hold's `end` cannot remove a newer one; a test covers it. - New unit test: with reservations 5 (raw transaction refused) and 6 (in flight), a `reset` ends 5 first. Emptying `failReservation` now fails it. - `releaseReservationsBelow` and `releaseReservationsUpTo` lost a redundant branch and an optional chain. - `sdk-floors.yml` also runs on changes to `src/internal/rpc/**`, `test/floors/**`, `test/helpers/**` and the raw-send and library-send-cli tests. `testing.md` and `tooling.md` describe the viem floor run and the 2.55.11 refusal run; `tooling.md` lists `viem/` in Stryker's scope. - `library-accounts.md` and `transactions.md` say the version check reads the viem that hardhat-kms resolves, so a nested older copy in a workspace can pass; it is best effort. ## Changes after the fifth review - `sdk-floors.yml` also runs on changes to `src/internal/hook-handlers/network.ts` and `src/internal/crypto/address.ts`, which the floor tests run. - `testing.md` describes everything `send-guard.test.ts` covers: lock limits, reservations, library holds, both warnings and the order test. - The docs, TSDoc, changeset and this body say the 5 s warning prints at most once per hold and at most once per waiting send. The code is unchanged. - The Warnings section of `library-accounts.md` also lists the once-per-process own-transport warning, with its text. ## Where this departs from design (a) as specified **A plugin send waits for a library send that holds the lock through the connection.** Design (a) said a plugin send must never wait on a reservation, and would skip the reserved nonce instead. I built that first. On `edr-simulated`, all four concurrency tests failed. EDR refuses a nonce gap while automining ("Nonce too high. Expected nonce to be 0 but got 1. Note that transactions can't be queued when automining.", which viem wraps as "Nonce provided for the transaction is higher than the next one expected"), so a plugin send that skips ahead of a library send that has not yet broadcast is refused. The hold makes the library send behave like any other send of the account: it holds the lock from its nonce to its broadcast. The wait is bounded by that send's own work (gas estimate, KMS signature, broadcast), as with two plugin sends: - A reverted estimate releases the hold at once through `reset`. - The hold is never ended by a read. - Its 60 s safety limit (`LIBRARY_HOLD_MS`) uses a timer that keeps the process alive. It is cleared on every normal path, and it only matters when a raw transaction never reaches the plugin and viem never resets the send, for example with a `custom` transport over another provider. Design (a)'s skip-without-waiting behaviour is kept for clients with their own transport, where no broadcast ever reaches the plugin. **`reset` names no nonce and no client.** With holds, only one library send of an account holds the lock at a time. Two cases are counted per lock key: a `consume` that failed, and a held send whose broadcast failed. Each makes viem call `reset` later, and that call uses up one owed reset instead of ending the next send's hold. When a hold and own-transport reservations are both open, a reservation takes the reset (see above). Among reservations, `reset` ends the newest failed one, else the newest unsigned one, else the newest. **The viem peer floor goes from `^2.47.6` to `^2.55.13`.** Checked in the published packages: - 2.49.3 and earlier never call `reset`. - 2.50.3 to 2.55.11 call it ([wevm/viem#4634](wevm/viem#4634)), but can call it without a `consume`, when the send fails before the nonce is chosen. - From 2.55.13, `nonceManagerParameters` is set inside the wrapper around `consume` ([wevm/viem#4967](wevm/viem#4967); `sendTransaction.js:166-188`, identical in 2.55.13 and 2.56.0), and `reset` runs in the error path (`sendTransaction.js:242-243` in 2.57.1). 2.55.12 was never published. - `test:sdk-floors` now also runs `nonce-manager.test.ts`, `raw-send.test.ts` and `library-send-cli.test.ts` at the floor, then installs viem 2.55.11 and checks that `getAccount` refuses it. The plugin is not released yet. hardhat-viem's peer range is `^2.47.6`, so it accepts a viem below hardhat-kms's floor, and the project then gets a peer warning for hardhat-kms only. npm, by default, upgrades a range-pinned viem into the range or stops on an exact pin; pnpm and Yarn only warn, which is why `getAccount` also checks the version, as described in (b) above. ## Other decisions - **Cost.** A raw transaction is decoded only when the connection has already looked up its KMS addresses, or when a library send holds a lock, so it never causes a KMS call. Decoding and recovering the sender takes about 4 ms. A raw transaction from another sender takes no lock and no chain-id read. - **Raw transactions that are not decoded** (not a string, not hex, unsigned, or a blob transaction in its network form with sidecars, which micro-eth-signer does not decode) pass on unchanged. This is documented and tested. So does a raw transaction whose chain id cannot be read, or that cannot get the lock. - **Re-broadcasts.** A raw transaction gets no retry entry, and no `SendOutcomeUnknownError` wrapping. "already known" passes through unchanged and raises the mark. - **Re-entrancy.** A library send started inside a send from the same account fails at once in `consume`, with `core.account.nonce-reentrant`, and nothing is signed. A raw transaction in that context fails with `core.tx.raw-send-reentrant`. - **Holds are process-global and keyed like the lock**, so a raw transaction that reaches another connection to the same chain still ends its hold. Closing the connection ends its holds. - **Not covered** (documented): clients with their own transport (only the nonce is reserved), other processes, and transactions that viem signs with a caller nonce or that are prepared step by step, since those never call the nonce manager. Their raw transactions are still ordered and counted when they go through the connection. - The first-transaction warning is replaced by a warning for clients with their own transport. The KMS signing path is unchanged. Under `src/internal/viem/`, `account.ts` gains the nonce manager, and `signTransaction` notes the nonce it signed, after the signature. ## Tests - `test/integration/raw-send.test.ts` on `edr-simulated`: - a plugin send and a library send at the same time, with the mark on and off, in both orders, 10 rounds each; - two parallel library sends, 10 rounds; - over HTTP against the recording node, whose pending count stays at 0, 10 parallel rounds; - a library send whose estimate reverts, after which the plugin's next send takes under 1 s and gets nonce 0; - pending and latest counts that stay the node's while a nonce is reserved; - another sender's raw transaction, a re-broadcast, and the re-entrant case; - `library.sendTransactionSync(...)` through a test plugin listed after hardhat-kms in the chain, which answers `eth_sendRawTransactionSync` by forwarding it as `eth_sendRawTransaction` and returning the receipt (EDR does not implement the method), then a plugin send that must take under 1 s, with nonces 0 and 1; - a library send whose KMS signature fails, after which the plugin's next send takes under 1 s and gets nonce 0; - two HTTP connections to one simulated node behind a JSON-RPC server: the account comes from the first, its client sends through the second, which never looked up its KMS addresses, and the first connection's next plugin send takes under 1 s, with nonces 0 and 1. - `test/integration/library-send-cli.test.ts`: a real `hardhat run` script with top-level await, in a child process: `getAccount`, a pending read, a plugin send, a library send, and a plugin send. It must exit 0, with the read unchanged, nonces 0 to 2, and the first send under 1 s. Run against the first version, it exits 13 ("Detected unsettled top-level await"). - `test/unit/rpc/raw-send.test.ts`, with a fake node: - every broadcast outcome; - holds: held until the raw transaction, a second library send waiting, release at `reset`, a failed broadcast whose late `reset` ends no other hold, another nonce waiting behind the hold, release when the connection closes; - waiting for a plugin send in progress; - re-entrancy, `get`, a foreign chain, a closed connection; - own-transport reservations: skipped without waiting, `reset`, unsigned before signed, cleared by an explicit nonce, failed and accepted raw transactions; - a pending read during a held send; - the blob wrapper; - `eth_sendRawTransactionSync`: a receipt raises the mark and ends a hold; a timeout (code 4) counts as in the pool, ends the hold and owes its reset, so the next hold survives that reset; a refusal frees the nonce; another sender passes untouched; - the mixed-transport repro (X=0 reserved for an `http` client, Y=1 held, X's reset): Y keeps the lock, the plugin send waits for Y, and the nonces are distinct; a reservation the node already has is dropped first, so the hold's own reset ends the hold; a failed pending read keeps the reservations; - a `consume` after a broadcast with no answer looks the transaction up first and gives the next nonce; - a raw transaction on a second connection that never looked up its addresses ends the hold with no KMS call, and another sender's passes on. - `test/unit/rpc/send-guard.test.ts`: reservations (clock expiry with no timer, reset order, close, `hasReservations`, `releaseReservationsBelow`) and holds (lock kept, 60 s limit with fake timers, owed resets, close, choose failure). - `test/unit/viem/nonce-manager.test.ts`: drives viem's own `sendTransaction` over a fake EIP-1193 provider. `consume` is called once, `reset` is called after a failed estimate, and a send with a caller nonce never asks. It also covers the transport flag and closed connections. It runs at the viem floor too. - Third review round: - `test/integration/raw-send.test.ts`: a library client with `http(url)` against an automining node behind a JSON-RPC server, its KMS signature held, and a plugin send that the node refuses with EDR's "Nonce too high" text; after the library send's receipt, the next plugin send takes nonce 1. It passed 10 runs in a row. Holding the lock on the own-transport path instead (manual mutation) makes it fail. - `test/unit/viem/account.test.ts`: refusals for 2.55.12, 2.55.12-canary.1, 2.55.13-canary.0, 2.55.11, 2.54.99, 2.50.3, 2.47.6 and 1.99.99, with no key lookup or KMS call; passes for 2.55.13, 2.55.13+build.1, 2.55.14, 2.56.0, 2.57.2, 2.100.0, 3.0.0 and 3.0.0-next.12; unreadable versions pass (empty, `unknown`, `2.55`, `v2.55.12`, `2.55.12x`, a missing or malformed `package.json`); the version constant matches the peer range. - `test/floors/viem-below-floor.test.ts`, run only by `test:sdk-floors` after it installs viem 2.55.11: `getAccount` refuses the real install. - `test/unit/rpc/send-guard.test.ts`: the wait warning (once per wait, also across two holds in a row; none behind a plugin send, under 5 s, or after close), the 60 s warning (none when the hold ends first), and the order 5 s < 60 s < 120 s stall limit. - Checked against the previous head (`9e38e1c`, the source reverted, the new tests kept): the sync and second-connection integration tests fail with the plugin send taking about 60 s; the unit tests for the sync receipt and timeout, the mixed-transport repro, the failed pending read, the uncertain lookup in `consume` and the second-connection raw transaction fail. The KMS-failure integration test passes there too; it guards the reset path. - Manual mutation, against the integration tests: removing the `eth_sendRawTransaction` branch fails every concurrency test and the CLI test (holds are never ended by the broadcast). Releasing the lock at once instead of holding it fails the four `edr-simulated` concurrency tests and the parallel library test. Choosing the plain pending count instead of `nonceFor` fails the lagging-node test. - Stryker, fourth round: `src/internal/viem/account.ts` 100% (304 killed, 0 survivors). `src/internal/rpc/send-guard.ts` is outside the config's `mutate` list; run by hand it scores 93.17% (312 killed, 70 timeouts, 27 survived, 1 no coverage). The survivors in code this PR added are all equivalent: - lines 213 and 255, `holds.getStore() ?? []` becoming a one-string array: a string has no `key`, so it never matches a hold; - lines 338 and 342, `control.release?.()` and `control.acquired?.()` becoming plain calls: both are assigned synchronously by the `Promise` constructors before use; - line 345, `holding.then(async () => await acquired)` becoming `() => undefined`: `holding` settles only after `acquired`, so the race is already decided; - line 403, `owed === 1` becoming `false`: the count is set to 0 instead of deleted, and `takeOwedLibraryReset` reads a missing entry and 0 the same way. - About 10 other survivors are in code from `main` that this PR did not change, tracked in [#205](#205); the exact set varies with machine load, as mutants move between survived and timed out. - Previous rounds: Second round: 100% (1381 killed, 7 timeouts, 3 runtime errors, 0 survivors; `account.ts` 251 killed). The first run left 2 survivors and 1 uncovered mutant. I removed an unreachable branch and added a debug-line test, and the second run was clean. ## Docs - `library-accounts.md`: the warning ("within the cases in Sending") and the Sending section, covering the sync actions, mixed transports, what is covered, what is not, and the floor. - `transactions.md`: new section "Library accounts: nonces and raw transactions", including why the read interception was dropped, the reset order and why per-consume ownership is not possible, the sync method, and the EDR error quote. - `architecture.md`: rule 1 restored, rule 4 extended, the library account section and its diagram. - `rpc-methods.md`, `security-model.md` and `debug-output.md`. - Decision 0014 note, and the regenerated errors reference. - Both changesets: this one is minor, because it adds a public `nonceManager`, a new error id and a peer floor change. The `getAccount` changeset no longer mentions the removed warning, and it states the new floor. ## Gates Everything below was run at this head, rebased on `origin/main` `2745810`, on Node 24 unless noted. - `pnpm run check` and `pnpm test` pass. Core: 642 unit and 405 integration tests, 0 failures. The provider packages and the live helper tests pass too. - `pnpm run coverage`: core 97.88% lines and 97.55% branches; `account.ts` 100% lines, `send-guard.ts` 99.76% lines. - `pnpm run docs:check` and `pnpm run pkg:check` pass. - `pnpm run test:sdk-floors` passes with viem@2.55.13 and the cloud SDK floors, then installs viem 2.55.11 and `getAccount` refuses it. - Node 22.13.0 with `NODE_OPTIONS="--import tsx"`, as the CI leg runs it: the touched viem and raw-send unit tests, `send-guard.test.ts`, and the `raw-send`, `library-send-cli` and `no-viem` integration tests pass (169 tests). - Not rerun this round: `scripts/consumer-typecheck.ts` (no public type changed; it passed with TypeScript 5.9, 6.0 and 7.0 on `76a9d29`) and the fork live suite. - The prose gate is clean on the changed docs, the changesets, the commit message and this body. - CI is billing-blocked. ## Fork live (previous head, `165946d`) `pnpm run test:live` on the Sepolia fork, with the AWS, Google Cloud and Azure keys: 74 passed, 0 failed, 1 skipped (the real-Sepolia suite). In `get-account.live.test.ts`, the viem wallet client sends through `custom(connection.provider)`, so each provider's send went through the nonce manager's hold and the raw-transaction path with a real KMS key. The transaction matrix passed for all three, and no transaction reached the Sepolia RPC. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
nonceManager.reset()now clears the consumed nonce cache while preserving the internal cleanup path used byget(). Failed local-accountsendTransactionandsendTransactionSynccalls reset nonce manager state after submission errors before acceptance, so the next transaction re-reads the pending nonce instead of skipping ahead.sendTransactionSynckeeps the consumed nonce for reverted receipts, since those transactions were accepted and mined. Closes #4364.Validated with
pnpm exec tsc --project ./tsconfig.build.json --noEmit,pnpm exec biome check --write ...,git diff --check, and mockedsendTransaction/sendTransactionSyncfailure repros.pnpm test src/utils/nonceManager.test.tsis blocked locally by Anvil returning 400 onanvil_setBalanceduring suite setup.