Skip to content

waitForReceipt can hang indefinitely when a transaction is dropped or replaced #522

@musks-suburbs

Description

@musks-suburbs

Overview

When applications submit an EVM transaction via the CDP SDK and then wait for confirmation, there are real-world cases where a transaction is never mined (dropped) or is replaced (e.g., sped up/canceled). In these scenarios, a receipt polling helper (or any internal confirmation loop) can hang indefinitely without surfacing a clear terminal state.

This issue proposes adding robust handling for dropped/replaced transactions so SDK consumers can reliably detect and recover.

Problem Description

In production, transactions may:

  • remain pending for a long time and eventually get dropped from mempool
  • be replaced by a new transaction with the same nonce (speed-up/cancel)
  • be mined on a different hash than the one originally returned

If waitForReceipt (or equivalent confirmation logic) only polls for a receipt by the original tx hash, it may never resolve, leaving apps stuck in a “pending” UI state and forcing developers to implement their own timeout/nonce tracking logic.

Expected Behavior

  • Confirmation helpers should not hang forever.
  • SDK should provide a deterministic outcome:
    • receipt returned on success
    • explicit error/terminal result when dropped, replaced, or timed out
  • Timeouts and polling intervals should be configurable with safe defaults.

Steps to reproduce

  1. Send a transaction using the SDK.
  2. Before it is mined, replace it using the same nonce (speed-up/cancel) from the wallet or another tool.
  3. Call a receipt-waiting helper using the original tx hash.
  4. Observe that the call may continue polling indefinitely without resolving.

Example code

```ts
import { CdpClient } from "@coinbase/cdp-sdk";

async function run() {
  const cdp = new CdpClient({ apiKey: process.env.CDP_API_KEY! });

  const txHash = await cdp.evm.sendTransaction({
    network: "base-sepolia",
    to: "0x0000000000000000000000000000000000000000",
    value: "0",
    data: "0x",
  });

  // If tx is replaced/dropped, this may never resolve today
  const receipt = await cdp.evm.waitForReceipt({
    network: "base-sepolia",
    txHash,
    // desired options: timeoutMs, pollIntervalMs, onProgress, etc.
  });

  console.log("Receipt:", receipt);
}

run().catch(console.error);

Proposed Solution

  • Add timeoutMs and pollIntervalMs options (if not present), with safe defaults.
  • Detect “replaced” behavior by optionally tracking:
  • sender address + nonce (if available from send call or via lookup)
  • mempool/chain queries to see if a different tx with same nonce was mined
  • Return a typed terminal result or throw a typed error (e.g., TxDroppedError, TxReplacedError, TxTimeoutError).
  • Document recommended handling patterns for UIs and backend workers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions