Skip to content

fix(waitForTransactionReceipt): detect dropped transactions instead of polling indefinitely#4342

Open
Kropiunig wants to merge 2 commits intowevm:mainfrom
Kropiunig:fix/waitForTransactionReceipt-dropped-tx-detection
Open

fix(waitForTransactionReceipt): detect dropped transactions instead of polling indefinitely#4342
Kropiunig wants to merge 2 commits intowevm:mainfrom
Kropiunig:fix/waitForTransactionReceipt-dropped-tx-detection

Conversation

@Kropiunig
Copy link
Copy Markdown
Contributor

Problem

waitForTransactionReceipt polls indefinitely when a transaction has been dropped from the mempool. This happens in a common real-world scenario during gas spikes:

  1. User sends a transaction with low gas fees
  2. Network congestion causes the transaction to be evicted from the mempool
  3. getTransaction(hash) starts returning TransactionNotFoundError (the node no longer knows about the pending tx)
  4. getTransactionReceipt(hash) also fails (tx was never mined)
  5. The catch block at the !transaction check silently returns, continuing the poll loop
  6. Result: Every block triggers a full withRetry cycle (default 6 retries with exponential backoff ≈ 13s), producing ~6 wasted RPC calls per cycle, until the 180s timeout fires with a generic WaitForTransactionReceiptTimeoutError

If timeout is set to 0 / undefined, the promise never settles.

This is #3875. The issue also affects the "speed-up" flow: when a user replaces a dropped tx with a same-nonce tx that gets mined under a different hash, the original waitForTransactionReceipt can't detect the replacement because transaction was never populated (the evicted tx was gone before the first getTransaction call succeeded).

Solution

Track consecutive blocks where neither getTransaction nor getTransactionReceipt can locate the transaction. After retryCount such blocks (default 6, matching the per-block retry semantics), reject with a new TransactionDroppedError instead of polling forever.

TransactionDroppedError: Transaction with hash "0x..." could not be found
on the network. The transaction may have been dropped from the mempool.

This can happen when:
- The transaction fee was too low and it was evicted from the mempool during a gas spike
- The transaction was replaced by another transaction with the same nonce

If the transaction was replaced, use `waitForTransactionReceipt` with the
new transaction hash instead.

Design decisions

  • Scoped to checkReplacement: true (the default): Drop detection only activates when replacement checking is enabled, because that's the code path that calls getTransaction. When checkReplacement: false, the function still falls through to the timeout — this preserves backward compatibility for callers that explicitly opted out of replacement checking.

  • Reuses retryCount as the block threshold: Rather than introducing a new parameter, the existing retryCount (default 6) doubles as the number of consecutive blocks to tolerate before considering the tx dropped. This keeps the API surface unchanged while providing reasonable defaults (~72s on mainnet at 12s blocks).

  • No false positives for slow RPCs: If a slow RPC eventually returns the transaction (e.g., after 2-3 blocks of lag), transaction gets populated and notFoundCount becomes irrelevant — the normal replacement detection path takes over.

Changes

File Change
src/errors/transaction.ts New TransactionDroppedError with actionable meta messages
src/actions/public/waitForTransactionReceipt.ts notFoundCount tracker + drop detection in the !transaction catch branch
src/index.ts Export TransactionDroppedError and TransactionDroppedErrorType
src/actions/public/waitForTransactionReceipt.test.ts 3 new tests: drop detection, slow-RPC tolerance, checkReplacement: false bypass

4 files changed, ~150 lines added.

Tests

dropped transactions > rejects with TransactionDroppedError when transaction is evicted from mempool
Mocks both getTransaction and getTransactionReceipt to consistently throw, simulating a fully evicted tx. Asserts the promise rejects with TransactionDroppedError within the retry threshold.

dropped transactions > does not reject prematurely when transaction appears after initial failures
Mocks getTransaction to fail twice then fall through to the real implementation. Sends and mines a real transaction. Asserts the receipt resolves successfully despite the initial not-found responses (slow RPC simulation).

dropped transactions > checkReplacement: false does not trigger drop detection
With replacement checking disabled, the function should fall through to the timeout rather than the drop detection path. Asserts WaitForTransactionReceiptTimeoutError is thrown, not TransactionDroppedError.

Future work

The remaining gap from #3875 is nonce-based replacement detection for evicted transactions: when the original tx was evicted before getTransaction could read it, we don't know the sender address or nonce, so we can't search recent blocks for a same-nonce replacement. This could be addressed by adding optional from / nonce parameters to WaitForTransactionReceiptParameters in a follow-up PR.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 15, 2026

🦋 Changeset detected

Latest commit: 0bc57ba

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
viem Patch

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

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 15, 2026

@Kropiunig is attempting to deploy a commit to the Wevm Team on Vercel.

A member of the Team first needs to authorize it.

@Kropiunig
Copy link
Copy Markdown
Contributor Author

Hi @jxom — pinging for visibility. This PR adds dropped-transaction detection to waitForTransactionReceipt so it doesn't poll indefinitely when a transaction is replaced or dropped. CI is passing. Would appreciate a review when you have a moment!

@jxom jxom force-pushed the main branch 2 times, most recently from bdf8494 to 69e0d6e Compare March 13, 2026 22:42
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 8, 2026

Open in StackBlitz

npm i https://pkg.pr.new/viem@4342

commit: 0bc57ba

@Kropiunig
Copy link
Copy Markdown
Contributor Author

Added the missing changeset. The Test Local failures are pre-existing (same evm_setIntervalMining errors on main and in recently merged PRs like #4472). Ready for review whenever you have a moment @jxom.

…f polling forever

When a transaction is evicted from the mempool (e.g. during a gas spike
or when replaced by a same-nonce tx with higher fees), both
getTransaction and getTransactionReceipt fail on every poll cycle. The
current code silently returns in this case, creating an infinite loop
of wasted RPC calls until the 180s timeout fires.

This adds a `notFoundCount` tracker that increments each time the
transaction cannot be found. After `retryCount` consecutive blocks
without locating it, the promise rejects with a new
`TransactionDroppedError` that explains what happened and how to
recover.

The detection only activates when `checkReplacement` is true (the
default), preserving backward compatibility for callers that disable
replacement checking.

Closes wevm#3875
@Kropiunig Kropiunig force-pushed the fix/waitForTransactionReceipt-dropped-tx-detection branch from f78f8fb to 0bc57ba Compare April 27, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant