npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@valve-tech/tx-tracker

v0.20.0

Published

Per-tx state machine for EVM chains: emits neutral observations (`seen-in-mempool`, `seen-in-block`, `replaced-by`, `vanished-from-block`, `unseen-for-N-blocks`, etc.) so wallet UIs, indexers, and relays can write their own interpretations on top. Three c

Readme

@valve-tech/tx-tracker

Per-tx state machine for EVM chains. Emits neutral observationsseen-in-mempool, seen-in-block, replaced-by, vanished-from-block, unseen-for-N-blocks, signal-degraded, signal-recovered, stopped — so wallet UIs, indexers, and relays can write their own interpretations on top. The package itself never says "confirmed" or "stuck"; it gives you the data to decide.

See docs/tx-tracker-spec.md for the full design contract.

Why this exists

Tx-tracking on EVM is unforgiving:

  • Three different consumer shapes (wallet UI, indexer, relay) want the same underlying observations but very different consumption ergonomics.
  • Five state transitions (pending, mined, replaced, dropped, reorged) plus their authoritative-vs-degraded sources.
  • Per-method capability variance — some upstreams gate txpool_content, some allow eth_subscribe('newHeads') but not newPendingTransactions, some only offer HTTP.
  • No silent downgrade — a tracker that says "your tx is mined" when the WS dropped and the receipt poll happens to still see the old block is lying. Every event in this package carries a source discriminator ('subscription' / 'block-poll' / 'mempool-snapshot' / 'receipt-poll') so consumers know how authoritative it is.

This package handles all of it as one push-based core with three thin adapters (callback / async iterator / snapshot).

Install

yarn add @valve-tech/tx-tracker @valve-tech/chain-source viem

@valve-tech/chain-source is a runtime dependency — the tracker consumes its block + mempool stream rather than re-implementing the poll loop. viem ^2.0.0 is the only external peer.

Quick start

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { createChainSource } from '@valve-tech/chain-source'
import { createTxTracker } from '@valve-tech/tx-tracker'

const client  = createPublicClient({ chain: mainnet, transport: http() })
const source  = createChainSource({ client })
const tracker = createTxTracker({ source, chainId: 1 })

source.start(); tracker.start()

// Async iterator — recommended for new code:
for await (const event of tracker.track('0xabc...')) {
  if (event.kind === 'seen-in-block' && event.confirmations >= 6) break
}

Three consumption shapes

All three back the same internal Subscriptions<TxEvent> per hash, so they see consistent state. Pick whichever fits the call site.

1. Snapshot — sub-millisecond, returns null if not tracked

const status = tracker.getTxStatus(hash)
if (status?.lastSeenInBlock?.confirmations >= 6) {
  // do confirmed-thing
}

2. Callback — returns an unsubscribe handle

const unsub = tracker.subscribe(hash, (event) => {
  if (event.kind === 'seen-in-block') showConfirmation(event)
  if (event.kind === 'replaced-by')   showReplacement(event)
  if (event.kind === 'unseen-for-N-blocks' && event.blocks >= 30) showStuckHint()
})
// later
unsub()

3. Async iterator — recommended for new code

for await (const event of tracker.track(hash)) {
  switch (event.kind) {
    case 'seen-in-block':
      if (event.confirmations >= 6) return  // exits the loop
      break
    case 'replaced-by':
      reportReplacement(event.replacementHash)
      return
    case 'stopped':
      return  // tracker shut down or retention expired
  }
}

The iterator stops cleanly on tracker shutdown, retention expiry, or explicit unsubscribe. No event-listener leaks — for await cleanup runs the iterator's return() automatically when the loop exits.

What you'll likely want to do

"Confirm a tx"

for await (const event of tracker.track(hash)) {
  if (event.kind === 'seen-in-block' && event.confirmations >= confirmsRequired) {
    return event   // confirmed
  }
  if (event.kind === 'replaced-by' || event.kind === 'unseen-for-N-blocks') {
    throw new Error(`tx didn't confirm: ${event.kind}`)
  }
}

waitForTransaction(hash, { confirmations, source }) ships as a one-shot helper if you don't need the per-event control:

import { waitForTransaction } from '@valve-tech/tx-tracker'

const event = await waitForTransaction(hash, { source, chainId: 1, confirmations: 6 })

"Detect a stuck tx and prompt the user to bump"

const unsub = tracker.subscribe(hash, (event) => {
  if (event.kind === 'unseen-for-N-blocks' && event.blocks >= 30) {
    promptUserToBump()           // 30 blocks ≈ 6 min on Ethereum
  }
})

unseenThresholdBlocks (default 30) controls when the unseen-for-N-blocks event fires. Tune lower for fast L2s, higher for slow chains.

"Watch all txs from an address (indexer-style bulk)"

const sub = tracker.trackFromAddress(treasuryAddress, { durable: true })

// Raw match stream:
for await (const m of sub.events()) {
  console.log('match', m.hash, m.bucket)
}

// Per-hash event stream (auto-tracked by default):
sub.subscribe((event) => {
  if (event.kind === 'seen-in-block') ingestConfirmed(event)
})

sub.stop()  // stops match stream; does NOT stop already-auto-tracked per-hash subs

trackFromAddress / trackToAddress / trackPredicate — capped at maxBulkSubscriptions: 16 by default. Per-hash auto-tracking can be disabled via { autoTrack: false } for replay-only consumers.

"Detect a replacement (speed-up / cancel)"

for await (const event of tracker.track(hash)) {
  if (event.kind === 'replaced-by') {
    // event.replacementHash is the new tx; event.replacementBlockNumber
    // is null until the replacement itself mines.
    console.log(`replaced by ${event.replacementHash}`)
    break
  }
}

Replacement detection runs nonce-watching on the same sender + nonce. Works for both speed-up (same nonce, higher tip) and cancel (self-send to clear the slot).

Configuration patterns

| Setting | Default | Tune up for | Tune down for | |---|---|---|---| | reorgDepthBlocks | 12 | Weak-finality chains (PoW, small validator sets) | High-finality chains; only care about shallow reorgs | | unseenThresholdBlocks | 30 | Slow chains (Ethereum: ~6 min) | Fast L2s | | lostSignalPolicy | 'emit-uncertain' | (default — loud is correct) | 'silent' for wallets that don't want capability-churn UI flicker | | createInMemoryStore({ retentionBlocks }) | 64 | Indexers replaying long windows | Wallet UIs | | createInMemoryStore({ eventLogCapacity }) | 256 | Heavy catch-up on restart | Memory-constrained mobile / edge |

reorgDepthBlocks and retention are in block-units, not seconds — reorg safety is a depth invariant. See spec §10.1.

Composing with @valve-tech/gas-oracle

One ChainSource shared across both — one upstream RPC poll cycle:

import { createChainSource } from '@valve-tech/chain-source'
import { createGasOracle }   from '@valve-tech/gas-oracle'
import { createTxTracker }   from '@valve-tech/tx-tracker'

const source  = createChainSource({ client })
const oracle  = createGasOracle({ source, chainId: 1 })
const tracker = createTxTracker({ source, chainId: 1 })

source.start(); oracle.start(); tracker.start()
// ↑ ONE upstream poll cycle. Two derived views.

Each surface owns its own lifecycle — oracle.stop() does not stop the source or the tracker. The owner of the source (whoever called createChainSource) calls source.stop() when the process shuts down.

For React in-flight tx UIs, @valve-tech/tx-flight-react wraps tracker + wallet-adapter into a Provider + headless components, so you don't have to wire any of this by hand for the UI side.

Capability disclosure (the no-silent-downgrade rule)

tracker.capabilities() forwards the source's snapshot:

{
  newHeads:                'subscription' | 'poll-only' | 'unavailable'
  newPendingTransactions:  'subscription' | 'poll-only' | 'unavailable'
  txpoolContent:           'available' | 'gated'
  receiptByHash:           'available' | 'unavailable'
  reprobeOnReconnect:      boolean
}

When capabilities change mid-tracking (WS dropped, txpool gated, etc.), the tracker emits signal-degraded / signal-recovered per affected key. Consumers that need hard inclusion guarantees filter to event.source === 'subscription'. Consumers that just want "best available" data ignore the discriminator.

lostSignalPolicy: 'emit-uncertain' (the default) is the loud-is-right choice — UI consumers want to surface "we lost push to the chain; falling back to poll" so the user knows the bar might be lagging. Switch to 'silent' when you genuinely don't care about capability churn (some indexers, server-side relays).

Wire format

All numeric fields are bigint (block numbers, fees, timestamps). JSON.stringify(event) will throw without hex-encoding at the wire boundary. Durable store implementers MUST hex-encode ('0x' + n.toString(16)) on write and decode on read. The default in-memory store keeps bigint end-to-end.

Examples

Runnable scripts (live under the gas-oracle examples directory — the toolkit hosts shared examples there):

  • examples/07-tx-tracker.ts — minimal tracker, no oracle (async iterator)
  • examples/08-tx-tracker-with-oracle.ts — shared ChainSource between gas-oracle + tracker
  • examples/09-bulk-from-address.ts — indexer-style bulk subscription

Run with yarn tsx examples/07-tx-tracker.ts.

For AI agents

This package ships an AGENTS.md reference (full surface in tabular form, capability matrix, every event variant) and a skills/ directory for Claude Code / Cursor skill files shipped in the npm tarball. After install, both are reachable at:

  • node_modules/@valve-tech/tx-tracker/AGENTS.md
  • node_modules/@valve-tech/tx-tracker/skills/tx-tracker-integration/SKILL.md

The skill triggers on imports of @valve-tech/tx-tracker and on phrases like "track this transaction", "watch tx hash", "detect stuck transactions", "watch for replaced txs". It includes a decision tree for picking among the three consumption shapes, anti-patterns to flag in user code, and the canonical composition pattern with gas-oracle.

Verifying provenance

v0.6.0+ ships with SLSA provenance attestation:

npm view @valve-tech/tx-tracker@latest --json | jq .dist.attestations
npm audit signatures

The attestation links the published tarball to the GitHub Actions workflow run that built it.

For AI agents

Machine-readable integration skills ship in this tarball under skills/. Run npx @valve-tech/agent-skills install to copy all installed @valve-tech/* skills into .claude/skills/, or read them in place at node_modules/@valve-tech/tx-tracker/skills/.

License

MIT