# Changelog

All notable changes to `@valve-tech/tx-tracker` are documented in
this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [0.24.0] — 2026-08-03

### Notes

- Synchronized release — no changes to this package. Bumped in
  lockstep with the rest of the toolkit.

## [0.23.0] — 2026-08-01

### Notes

- Synchronized release — no changes to this package. Bumped in
  lockstep with the rest of the toolkit.

## [0.22.1] — 2026-07-23

### Added

- `createMultiChainTracker` — multi-chain coordinator (spec §18.3): a
  thin multiplexer that manages one `TxTracker` per `chainId` and fans
  events in/out. Delegation (`track` / `subscribe` / `getTxStatus` /
  `tracker`) routes by chainId and throws `UnknownChainIdError` on a
  miss; `subscribeAll` fans in every chain's events tagged as
  `MultiChainTxEvent { chainId, event }`; `trackFromAddress` /
  `trackToAddress` / `trackPredicate` fan out to every registered
  chain and return a combined `MultiChainTxSubscription`. The
  per-chain `TxEvent` envelope is unchanged — no persisted-type
  drift. Each chain keeps its own `ChainSource`, store, and state
  machine; the coordinator adds routing only.

## [0.21.0] — 2026-07-15

### Changed

- Declared `engines.node` as `>=20`. The packages are CI-tested on Node
  20, 22, and 24; this makes the supported range explicit for consumers.

## [0.20.0] — 2026-06-26

### Notes

- Synchronized release — no changes to this package. Bumped in
  lockstep with the rest of the toolkit.

## [0.19.0] — 2026-06-21

### Fixed

- Integration skill (2026-06-12 audit): rewrote the speed-up workflow,
  which was built on a nonexistent `tracker.on('stuck', ...)` API — it
  now uses `tracker.subscribe` keyed on `unseen-for-N-blocks` plus the
  package's own `replaceTransaction` helper; corrected the rotted "never
  emits a confirmed event" claim (v0.15 added opt-in
  `confirmed-terminal` via `confirmationsForTerminal`, REQUIRED for
  long-lived stores); completed the event-kind list (`left-mempool`,
  `confirmed-terminal`); added the one-shot helpers
  (`waitForTransaction` / `watchTransaction` / `waitForPending` /
  `replaceTransaction` / `createTxGroup` /
  `createLocalStorageTrackerStore`) to the decision tree and triggers;
  documented the `receipt-poll-fallback` `lostSignalPolicy` variant,
  `statusPollEveryBlocks`, and `confirmationsForTerminal` in the tuning
  table; fixed the inverted `unseenThresholdBlocks` tuning advice;
  replaced non-shipped/repo-relative pointers with GitHub URLs; removed
  the rotted version pin; trimmed the description under 1024 chars.

## [0.18.0] — 2026-06-01

### Notes

- Synchronized release — no changes to this package. Republished at
  0.18.0 alongside the rest of the toolkit; the substantive additions
  are two new packages joining the release line:
  `@valve-tech/auth-lite` (SIWE-lite authentication: server-issued
  nonce + client `personal_sign` + server recover) and
  `@valve-tech/wallet-crypto` (deterministic wallet-derived AES-GCM
  encryption keys + authenticated envelopes). See those packages'
  CHANGELOGs for details.

## [0.17.0] — 2026-05-30

### Notes

- Synchronized release — no changes to this package. Republished at
  0.17.0 alongside the rest of the toolkit; the substantive changes
  are in `@valve-tech/wallet-adapter` (new optional
  `TrackedTx.readOnly` field) and `@valve-tech/tx-flight-react`
  (new `addByHash({ readOnly, submittedAt })` inputs for tracking
  relayer-submitted transactions). See the respective CHANGELOGs for
  details.

## [0.16.0] — 2026-05-15

### Added

- **`CreateTxTrackerOptions.logger`** — optional
  `(level, message, meta?) => void` callback for non-fatal
  observability events. Same shape as the v0.16 chain-source logger;
  consumers wire one callback for both. The tracker calls it at
  narrowly-chosen decision points: rehydration count on
  `tracker.start()`, dedup-migration writes (when v0.15 self-healing
  collapsed historical persisted-subscription duplicates), retention
  expiry firings. Errors continue to flow through `onError`; the
  logger covers the "what did the tracker decide" question.

### Notes

- 4 new tests covering logger plumbing: rehydration count emitted at
  `info`, dedup migration logged on legacy stores, retention expiry
  surfaced when records cross the terminal+retentionBlocks window,
  missing-logger default doesn't crash the dispatch path. 338 total
  tx-tracker tests (was 334). Coverage remains 100/100/100/100.
- The chain-source v0.16 adaptive scheduler propagates naturally to
  tx-tracker consumers via the shared source — tracker dispatch ran
  per-block-tick before and continues to do so, but those ticks now
  fire around the actual block cadence rather than on a fixed
  `pollIntervalMs` interval. The downstream effect: fewer wasted
  per-record status polls / receipt polls / probe-Mined / probe-
  Transaction dispatches when the chain is idle.

## [0.15.0] — 2026-05-14

### Added

- **`CreateTxTrackerOptions.confirmationsForTerminal: number | null`**
  (default `null`). When set to a positive integer, a record
  transitions to terminal (`terminalAtBlockNumber` set to the current
  block) the first block its `lastSeenInBlock.confirmations` reaches
  the threshold. Closes the long-standing gap where a normally-mined
  tx never went terminal — retention enforcement only fired on
  replacement and unseen-for-N-blocks paths, so successful txs
  accumulated in long-lived stores forever. Records with 5,000+
  confirmations were observed in production localStorage dumps; the
  new option lets consumers opt in to retire them via the normal
  retention pipeline. Recommended value: `≥ reorgDepthBlocks` (default
  12) so a same-height reorg can't briefly unmine a "terminal" tx.
  Validation: must be a positive integer or null/undefined; zero,
  negative, and non-integer values throw at construction.
- **`TxEventConfirmedTerminal` event kind** — `{ kind: 'confirmed-terminal',
  confirmations }` fires once when the threshold is reached. Consumer-
  facing signal that the record is now safe to forget. Carries the
  triggering confirmations count for downstream gating.
- **`TrackOptions.subscriptionId?: string`** — caller-provided stable
  identifier for the persisted subscription (only meaningful when
  `durable: true`). Repeated calls with the same id are idempotent.
  Without an explicit id, the tracker auto-dedups by `(durable,
  selector)`: a second subscribe on the same hash with `durable: true`
  reuses the prior persisted entry rather than appending. Closes the
  bug where React component remounts and page reloads accumulated
  structurally-identical persisted entries across reloads, each
  driving the same downstream fan-out N times.
- **`createLocalStorageTrackerStore({ keyPrefix, storage?, eventLogCapacity?,
  cleanupLegacyPrefixes? })`** — first-party TxTrackerStore implementation
  backed by `localStorage` (or any `Storage`-shaped object). Records
  stored under `{keyPrefix}:{chainId}:{hash}`; eventlogs under
  `{keyPrefix}:eventlog:{chainId}:{hash}`. Bigint fields round-trip
  losslessly via a sentinel-tagged JSON replacer/reviver.
  **`delete()` clears BOTH the record AND the eventlog** — the
  canonical bug class consumer-side localStorage stores routinely got
  wrong, leaving orphaned eventlogs that never expired.
  `cleanupLegacyPrefixes` deletes prior-prefix keys on construction for
  prefix-bump migrations. Defensive against `setItem` failures (quota
  exceeded, browser disabled storage) — surfaces as a rejected promise
  so the tracker's `onError` can route it cleanly.
- **`deleteKeysStartingWith(storage, prefix)`** — standalone helper for
  prefix cleanup without instantiating the full store.
- **Self-healing rehydration dedup.** On `tracker.start()`,
  `dedupPersistedSubscriptions` runs on the rehydrated record's
  subscription list; when the count shrinks (legacy duplicates
  collapsed), the cleaned record is immediately written back via
  `store.put`. Defensive against legacy flat-shape entries (pre-current
  `selector` nesting) — those pass through untouched rather than
  crashing the dedup helper.

### Changed

- **`runReceiptPollFallback` skips silently when `caps.ready === false`**.
  Pre-v0.15, the conservative pre-probe `receiptByHash: 'unavailable'`
  default tripped the "permanently unavailable" warning gate, firing
  once per tracker instance even when the RPC ultimately supported the
  method. The warning now waits for probe completion (`caps.ready === true`)
  before deciding; pre-probe ticks no-op.
- **`TxTrackerStore.delete` docstring tightened** to make explicit that
  implementations must clear ALL state associated with the hash — the
  record, the eventlog, any other per-hash keys. The first-party
  `createInMemoryStore` and `createLocalStorageTrackerStore` already
  enforce this; consumer implementations must do the same to avoid the
  orphan-eventlog leak class.

### Notes

- The `confirmationsForTerminal` transition fires from both Path 1
  (fresh inclusion at confirmations=1, relevant only when threshold=1)
  and Path 2 (confirmation bump from prior block) in
  `decideBlockObservation`. Terminal is anchored on the current block
  (matching the existing terminal arms which all anchor on
  detection-block, not origin-block) — retention math is consistent
  across all four arms.
- The status-poll path's mined emit (added in v0.14) hardcodes
  `confirmations: 1` and doesn't participate in the new transition.
  This is correct: status-poll mined observations are one-shot per
  inclusion-block; confirmation accrual happens via block-poll's
  Path 2, which is where the threshold is checked.
- 40 new tests across `tracker.test.ts`, `local-storage-store.test.ts`,
  and `events.test.ts`. 334 total tx-tracker tests (was 294). Coverage
  remains 100/100/100/100.

## [0.14.0] — 2026-05-14

### Added

- **`CreateTxTrackerOptions.statusPollEveryBlocks`** — default `1`
  (every block tick). Controls cadence for a new default-on per-hash
  status poll via `source.getTransaction(hash)`
  (`eth_getTransactionByHash`). Set `0` to disable. `2`/`3`/etc. for
  less-frequent cadences on consumers tracking many hashes.
  Returns are treated as authoritative for the tx's current state:
  pending (no `blockHash`) emits `seen-in-mempool`; mined (with
  `blockHash`) emits `seen-in-block`. Both flow through the existing
  event pipeline with `source: 'receipt-poll'` (see chain-source
  v0.14.0 EventSource doc widening). The path is NOT permitted to
  drive reorg / vanished-from-block events (spec §12.3) — divergence
  detection stays anchored on the source's block stream.
- **`TrackOptions.probeTransaction`** — per-subscription consumer
  fallback called only when `source.getTransaction` returns null.
  Use to consult a different RPC, multi-RPC fan-out, an indexer with
  mempool support, or a commercial mempool service. First-set wins
  across subscribes (mirrors `lostSignalPolicy` / `probeMined`).
- `ProbeTransaction` type exported from the package root. Function
  shape `(hash: Hash) => Promise<RawTx | null>`. RawTx is the existing
  chain-source primitive — consumers don't need to construct a
  custom shape; whatever they get back from
  `eth_getTransactionByHash` drops in directly.

### Notes

- Motivation: `txpool_content` is gated on many public RPC gateways
  (PulseChain et al), and even when available it only reflects the
  polled node's local mempool — partial-visibility failures look
  identical to "the tx was dropped" from the tracker's perspective.
  `eth_getTransactionByHash` is universally exposed and queries the
  node's indexed-tx store, so it sees txs the node has seen
  referenced even if they're not currently in the local pool. Per-hash
  polling is also dramatically lighter than full `txpool_content`
  snapshots — one tx record vs the entire pending pool. Both
  properties make per-hash status the right primitive for "is this
  tx alive?" on consumer-grade RPCs.
- **Load-bearing side effect: identity priming.** The status-poll
  caches `(from, nonce)` on the first pending observation. This
  unblocks replacement detection for txs that were never visible in
  any local mempool snapshot — `replaced-by` previously couldn't
  fire because the original's identity was never cached. Critical
  for chains where partial mempool visibility is the norm.
- The status-poll path is idempotent within a streak: one
  `seen-in-mempool` emit per pending streak (tracked via internal
  `statusPollLastEmittedKind` flag). On a pending→mined transition
  the dedup flag resets, so a later pending streak (rare — e.g.
  post-replacement) would re-emit cleanly.
- Cross-path duplicate handling: if `txpool_content` snapshots ARE
  available AND status-poll is also running, both paths can emit
  `seen-in-mempool` for the same observation. Both are truthful;
  consumers wanting unique delivery dedupe on `(kind, hash)` at the
  edge.
- Errors on `source.getTransaction` route through `onError` but the
  dispatch falls through to `probeTransaction` if one is attached —
  a transiently-failing source doesn't block the consumer's fallback.
  Probe errors route through `onError` and short-circuit.
- Cost characterization: with default `statusPollEveryBlocks: 1` and
  a consumer tracking N hashes, the additional RPC load is N calls
  per block tick. Consumers tracking >100 hashes may want
  `statusPollEveryBlocks: 2` or higher; consumers on rate-limited
  RPCs may want `0` (opt out entirely).
- Closures aren't serialized — durable records rehydrated from the
  store start with `probeTransaction: null` until a fresh subscribe
  re-binds one. Same precedent as `probeMined` and predicate
  selectors (spec §13.2).
- 18 new `tracker.test.ts` cases pin the path's contract: happy
  emit (mined + pending), null no-op, source-throw falls through to
  probe, probe-throw routes via onError, probe-throw-no-onError
  swallowed, pending idempotency, pending→mined transition resets
  dedup, height-ordering block-poll-wins, identity-check rejects
  orphaned mid-await emits, bad blockNumber routes via onError,
  probeTransaction first-set-wins, `statusPollEveryBlocks: 0`
  disables, `statusPollEveryBlocks: 3` skips ticks, defensive
  identity caching when tx lacks from/nonce, identity priming
  unblocks `replaced-by` for previously-invisible originals.

## [0.13.0] — 2026-05-12

### Added

- **`TrackOptions.probeMined`** — per-subscription consumer-supplied
  mined-detection probe. Generalizes the lesson behind
  `receipt-poll-fallback`: any signal that can answer
  "is hash X included, and if so where?" can feed observations into
  the tracker's event pipeline. The probe runs every block tick for
  every record that has one attached; whichever path reports
  inclusion first wins via the existing height-ordering rule on
  `lastSeenInBlock.blockNumber`. Probe-derived observations emit
  `seen-in-block` with `source: 'receipt-poll'` (the chain-source
  v0.13.0 doc widening covers the discriminator's new meaning).
  First-set-wins across multiple subscribes on the same hash
  (mirrors `lostSignalPolicy`). The probe is NOT permitted to drive
  reorg / vanished-from-block events (spec §12.3) — divergence
  detection stays anchored on the source's block stream where
  parent-hash chains are authoritative. No capability gate (the
  consumer's probe IS the authority); no tick counter (debounce
  internally if needed).
- `ProbeMined` and `ProbeMinedResult` types exported from the
  package root. Minimal return shape `{ blockHash, blockNumber }` —
  `transactionIndex` is hardcoded to `0` and `confirmations` to `1`
  on emit (the tracker's authoritative tip is what computes the
  confirmations count on subsequent blocks; letting the probe
  supply it would either duplicate the tracker's math or contradict
  it).

### Notes

- Use case: consumers with a server-side block indexer (Ponder,
  Subsquid, custom Postgres) that observes inclusion before the
  client's RPC block-poll tick. Pre-v0.13, the parallel-poll
  workaround ran a separate state machine outside the tracker and
  bypassed reorg, retention, replacement, and identity-race
  handling for the indexer's faster mined signal. The probe folds
  that signal into the tracker's pipeline so all those properties
  carry over automatically.
- Closures are not serialized — durable records rehydrated from the
  store on `tracker.start()` start with no probe attached until a
  fresh `subscribe(hash, cb, { probeMined })` re-binds one. Same
  precedent as predicate selectors (spec §13.2).
- 11 new `tracker.test.ts` cases pin the probe path's contract:
  happy emit + status mirror, returning null, throw routes through
  `onError`, throw with no `onError` is swallowed, block-poll wins
  height-ordering race, probe wins on strictly-newer block,
  first-set-wins across subscribes, default `null` probe early-
  returns the dispatch, identity check rejects mid-await orphaned
  emits, probe runs every block (no tick counter / no capability
  gate), reorg invalidates probe-reported inclusion.

## [0.12.0] — 2026-05-11

### Notes

- Synchronized release — no consumer-visible changes to this
  package's published surface. Bumped in lockstep alongside the
  v0.12.0 feature work in `@valve-tech/chain-source` (new
  `getBlockByHash` API) and `@valve-tech/gas-oracle` (reorg-side
  ring-lifecycle backfill that uses it).

## [0.11.2] — 2026-05-11

### Fixed

- **Posture-consistency follow-up to v0.11.1.** Two additional
  strict-null read sites on persisted `TxStatus` fields were
  structurally identical to the v0.11.0 crash hazard, but on fields
  that have been present since v0.3.x — so not triggerable on any
  current consumer's data, but the same shape would silently fail
  if a future schema migration ever dropped or renamed the field.
  Both sites tightened to defensive shape checks for consistency
  with v0.11.1's posture:
  - `tracker.ts:834` — stale-block guard now uses
    `typeof recordedSince === 'bigint'` (was `recordedSince !== null`).
    Without the fix, an absent `lastObservedAtBlock` would coerce
    `undefined > blockNumber` to `false` and silently defang the
    concurrency guard.
  - `observations.ts:274` — unseen-for-N-blocks gate now uses
    `firstObservedAtBlock == null` (loose; was `=== null`).
    Companion `unseenStreak` arithmetic at line 282 wraps the read
    in `?? 0` so legacy data with `unseenStreak` undefined doesn't
    write `NaN` into the patched record.
- Surfaced by an upgrade-hazard re-review of the persistence-touching
  code paths after the v0.11.1 incident. Both fixes are defensive,
  not bug-for-current-data — no consumer migration required.

### Added

- `makeLegacyTxStatus(overrides?, omit?)` test helper in
  `tracker.test.ts`. Builds a `TxStatus` shape with arbitrary
  fields omitted, simulating records persisted by earlier toolkit
  versions. Carries a JSDoc explaining the wire-shape evolution
  discipline and pointing at the
  `feedback_persisted_type_evolution.md` lessons-learned note.
  The v0.11.1 regression test and the two new v0.11.2
  posture-consistency tests both consume it; future tests of
  persisted-type evolution should reach for this primitive first.

## [0.11.1] — 2026-05-11

### Fixed

- **Upgrade-path crash on the first block tick after upgrading a
  persistent store from ≤0.10 to 0.11.0.** v0.11.0 added
  `TxStatus.terminalAtBlockNumber: bigint | null`, but records
  persisted by ≤0.10 stores have the field absent (undefined at
  runtime). The retention-enforcement check used `t !== null`
  (strict), so `undefined` slipped past the guard and threw
  `TypeError: Cannot mix BigInt and other types, use explicit
  conversions` at `undefined + BigInt(retentionBlocks)` —
  **uncaught inside `Subscriptions.emit`**, silently halting the
  in-flight block-tick fanout. Downstream `seen-in-block` /
  `unseen-for-N-blocks` / `replaced-by` / `vanished-from-block`
  events for that tick were dropped on the floor; consumer-visible
  symptom was tx-flight UIs stalled on "pending" indefinitely after
  upgrade.
  - `tracker.ts`'s retention guard now uses `typeof t === 'bigint'`
    instead of `t !== null` — also defensive against future store
    implementations that round-trip bigints as strings without a
    reviver.
  - `observations.ts`'s two patch-guard sites (unseen-for-N-blocks
    terminal emit, mempool-side replaced-by terminal emit) now use
    `== null` (loose) instead of `=== null` so a legacy record that
    *just* reaches terminal gets its `terminalAtBlockNumber`
    backfilled (pre-fix the patch was gated on strict null and
    silently never set, so the record would re-trip the retention
    crash every block).
  - Regression tests for both paths.

Consumers running v0.11.0 with **any persistent store** (localStorage,
IndexedDB, Redis, SQLite, custom `TxTrackerStore`) and durable
subscriptions from a ≤0.10 run should upgrade. No migration step
required — the fix is purely the upstream contract treating
`undefined` and `null` as equivalent "in-flight" sentinels.

## [0.11.0] — 2026-05-11

### Added

- `TxTracker.ready(): Promise<void>` — resolves when the
  durable-subscription rehydration triggered by the most recent
  `start()` has completed. Indexer / relay consumers should
  `await tracker.ready()` before assuming the tracked-set is fully
  restored from the store. Resolves cleanly even on listDurable
  errors (which are routed through `onError`).
- `CreateTxTrackerOptions.retentionBlocks?: number` — default `64`,
  spec §10. How many blocks past a terminal state (`replaced-by` or
  `unseen-for-N-blocks` emitted) the tracker keeps a record before
  emitting `Stopped({ reason: 'retention-expired' })` and dropping
  it. Pass the same value to your store implementation so persisted
  retention matches in-memory.
- `TxStatus.terminalAtBlockNumber: bigint | null` — populated when a
  hash reaches a terminal-and-finalized state. Anchors the retention
  countdown per spec §10. Null while the hash is still in flight.
- `findBulkSubBySelector(bulkSubs, selector)` exported from the
  package root (extracted from the tracker's runBulkOnBlock /
  Mempool path; pure, unit-tested in isolation).

### Fixed

- **Durable subscriptions are now rehydrated on start() (audit #1).**
  Records persisted via `subscribe(hash, cb, { durable: true })` are
  re-registered against the source when a fresh tracker starts up
  against the same store — fixes silent data loss across process
  restart for indexer / relay consumers. Previously, the write side
  of durable persistence worked but the read side was missing
  entirely.
- **Retention enforcement now actually fires (audit #2).** The
  `'retention-expired'` reason on `Stopped` was declared but never
  emitted; durable records grew unbounded in both `tracked` and the
  store. Records past `terminalAtBlockNumber + retentionBlocks` are
  now reaped on each block tick with `Stopped({ reason:
  'retention-expired' })` emitted to per-hash subs + globalSubs,
  followed by `store.delete`.
- **Bulk subscriptions are torn down cleanly on tracker.stop()
  (audit #3 lock-in).** Bulks' `stopped` flag is set during stop,
  making their async iterators yield `done: true` on the next
  `next()`. Most of the original audit-#3 concern about leaked
  per-hash subscriptions is subsumed by the retention fix (audit
  #2): auto-tracked records now reach retention-expired cleanup
  rather than persisting forever.
- **`replaced-by` no longer fires twice for one logical replacement
  (audit #4).** Previously the mempool side emitted with
  `replacementBlockNumber: null`, then the block side re-emitted
  with the inclusion block. The block path now patches status with
  the now-known blockNumber but suppresses the duplicate event when
  the replacement hash matches what mempool already surfaced.
- **Receipt-poll-fallback identity race (audit #6).** Records
  unsubscribed AND re-subscribed (under the same hash) while
  `getReceipt` was in-flight no longer leak phantom `seen-in-block`
  events onto `subscribeAll`. Post-await check now compares record
  identity (`tracked.get(hash) === record`), not just presence
  (`tracked.has(hash)`).
- **Defensive null-on-miss in bulk-sub lookup (audit #7
  hardening).** `findBulkSubBySelector` returns `null` instead of
  throwing when the registry is mutated mid-fanout. Currently
  unreachable from the public API (matchSubs has no sync
  subscribers), but a future internal change that adds one would
  otherwise crash the entire emit loop.

### Documented

- `subscribeAll` callbacks deliberately survive `stop()`/`start()`
  cycles (audit #8 lock-in). Long-lived analytics consumers wire
  one callback at construction and continue receiving events across
  restart. Comment added in `stop()`; behavior locked in by test.

## [0.10.1] — 2026-05-08

Synchronized release — no changes to this package. Republished at
0.10.1 alongside the rest of the toolkit; v0.10.0 only got
trueblocks-sdk publishing wrong (missing `repository` field tripped
provenance validation), so the rest of the line had to bump to
re-sync.

## [0.10.0] — 2026-05-08

Synchronized release — no changes to this package. Republished at
0.10.0 alongside the rest of the toolkit. The minor bump (rather
than patch) reflects the addition of a new sibling package,
`@valve-tech/trueblocks-sdk`, to the synced release line.

## [0.9.3] — 2026-05-08

Synchronized release — no changes to this package. Republished at
0.9.3 alongside the rest of the toolkit so all six packages share
one synced version line on npm. v0.9.2 had published this package
successfully but skipped `tx-flight-react` (workflow file was
missing a publish step); v0.9.3 fixes that and re-publishes
everything from one tag.

## [0.9.2] — 2026-05-08

Synchronized release — no changes to this package. Companion fix
to v0.9.1: the root `build` script now uses `--topological-dev`
so workspace `devDependencies` (added to `tx-flight-react` in
v0.9.1) actually drive build ordering. First version of the v0.9.x
line on npm for this package, but the toolkit-wide v0.9.x line
didn't reach all six packages until v0.9.3.

## [0.9.1] — 2026-05-08

*Not published — the Release workflow's Build step failed for the
same reason as v0.9.0. Superseded by v0.9.2.* Synchronized release;
no changes to this package itself.

## [0.9.0] — 2026-05-08

Synchronized release — no changes to this package. Bumped in lockstep
with the rest of the toolkit, alongside the new
`@valve-tech/tx-flight-react` package. *Not published — the Release
workflow's build step failed before publish; superseded by v0.9.1.*

## [0.8.1] — 2026-05-07

### Added
- `chainId` option on `watchTransaction` / `waitForTransaction` / `waitForPending`. Echoes through to `event.chainId` so consumers fanning multiple watchers into a single multi-chain stream can disambiguate. Falls back to `client.chain?.id`, then `0`.

### Changed
- `replaceTransaction` no longer passes `chain: null`, which silently disabled viem's `assertChainId` network-mismatch check. Now defers to `walletClient.chain` (passing `null` only when the wallet has no chain set, the only case viem requires it). `original.chainId` is threaded through as the top-level `chainId` field — the field is now load-bearing for EIP-155 signing and chain assertion. If a caller's wallet client and `original.chainId` disagree, viem throws.

### Removed
- Dead `_eventSource` parameters from internal `runBulkOnBlock` / `runBulkOnMempool`. No effect on public API.

### Notes
- Code-quality cleanup: removed three unreachable defensive guards from `tracker.ts` and `reorg.ts` that were marked with `c8 ignore` annotations. One pre-existing annotation remains (`tracker.ts:1083` invariant throw, kept for TS return-type narrowing). 100/100/100/100 coverage holds.

## [0.8.0] — 2026-05-06

### Added
- `lostSignalPolicy: { strategy: 'receipt-poll-fallback', pollEveryBlocks: N }` runtime. Closes the type-vs-runtime gap from v0.3.x — when a tracked subscription is in a degraded state, the tracker fetches `getReceipt` every N block ticks and emits `seen-in-block` with `source: 'receipt-poll'` on hit. Capability gate: requires `receiptByHash === 'available'`.
- `withReceipts: true` opt-in receipt enrichment on `TrackOptions`. When set, the tracker pre-fetches the receipt before the per-record block decision and attaches it to `seen-in-block` events via the new `TxEventSeenInBlock.receipt` field. One emit per inclusion — receipt is on the first event, not a follow-up.
- `tracker.group(hashes, options?)` — cross-tx correlation (spec §18.1). Emits `group-progress` / `group-complete` / `group-failed` / `group-stopped` derived from per-member event streams. Replacement does NOT auto-promote.
- `watchTransaction({ client, hash, ... })` — one-shot callback convenience export.
- `waitForTransaction({ client, hash, ... })` — Promise variant of `watchTransaction`. Resolves with discriminated-union outcome (`mined` / `dropped` / `replaced` / `failed`).
- `waitForPending({ client, hash, timeoutBlocks })` — Promise that resolves on first `seen-in-mempool`; rejects with typed `WaitForPendingTimeoutError` if the hash isn't observed within `timeoutBlocks`.
- `replaceTransaction({ original, walletClient, newGas })` — same-nonce replacement primitive. Caller-provides-newGas keeps tx-tracker independent of `@valve-tech/gas-oracle`.

### Changed
- `onBlock` is now async to support pre-fetching receipts before the per-record decision. Stale-block guard added against the resulting interleave window so a delayed pre-fetch can't clobber state advanced by a concurrent block tick.
- `decideBlockObservation` accepts an optional `prefetchedReceipts: ReadonlyMap<Hash, TransactionReceipt>` parameter (backward-compatible — existing callsites omit it).

## [0.7.0] — 2026-05-06

> **The implementation lands.** This is the first release of
> `@valve-tech/tx-tracker` with a real public surface. Prior versions
> (v0.0.1 → v0.6.0) were stubs reserving the npm name. The full
> design contract is at `docs/tx-tracker-spec.md` in the
> `valve-tech/evm-toolkit` repo.

### Added

- **Per-tx state machine** (`createTxTracker`) consuming a
  `ChainSource` for upstream block + mempool signals. Three
  consumption shapes over one push-based core: `getTxStatus(hash)`
  for the cached snapshot, `subscribe(hash, cb)` for callback-style,
  and `track(hash)` for the async-iterator shape — all three back
  onto the same internal stream so they see consistent state.
- **`TxEvent` discriminated union** (spec §6) with neutral
  observation kinds: `started`, `seen-in-mempool`, `left-mempool`,
  `seen-in-block`, `vanished-from-block`, `replaced-by`,
  `unseen-for-N-blocks`, `signal-degraded`, `signal-recovered`,
  `stopped`. Every event carries an envelope (`hash`, `chainId`,
  `source`, `at: { blockNumber, timestamp }`) so consumers can
  apply policy (`'confirmed'`, `'stuck'`, etc.) in their own UX
  voice without the tracker prejudging.
- **`TxTrackerStore` interface + `createInMemoryStore` default**
  (spec §9, §10). Block-unit retention (`retentionBlocks: 64` by
  default — reorg safety is a depth invariant, not a wall-clock
  invariant), bounded per-hash audit log (`eventLogCapacity: 256`
  by default) for catch-up replay.
- **Reorg detector** (`detectDivergences`, spec §12) — pure function
  over `BlockSample[]` that flags same-height different-hash
  divergences within `reorgDepthBlocks` (default 12). Ring is
  conservative about heights with no canonical entry — a partial
  canonical sequence does not nuke unrelated ring entries.
- **Bulk subscriptions** (spec §11): `trackFromAddress`,
  `trackToAddress`, `trackPredicate`. Auto-tracks matched hashes
  by default (`autoTrackMatched: true`) so the per-hash event
  stream is available too. Capped at `maxBulkSubscriptions: 16`.
- **Capability disclosure** — `tracker.capabilities()` forwards the
  source's snapshot. `signal-degraded` / `signal-recovered` events
  fire on every tracked hash when source-level capability
  transitions cross authority boundaries.
- **Replacement detection** — caches `(from, nonce)` on first
  observation and emits `replaced-by` when a different hash with
  the same identity appears (mempool: `replacementBlockNumber: null`;
  block: filled-in block number).
- **`subscribeAll(cb)`** — global stream of every event the tracker
  emits, useful for indexers piping to a single sink.
- **`AGENTS.md`** + **`skills/tx-tracker-integration/SKILL.md`** for AI
  agents working in downstream projects that import the package. Both
  ship in the npm tarball; the SKILL.md trigger phrases catch
  "track this transaction," "watch tx hash," "stuck transaction," and
  composition questions with `@valve-tech/gas-oracle`.

### Changed

- **Coverage hardening pre-1.0.** Eliminated dead defensive branches
  in `reorg.ts` (sort-comparator equal-key arms unreachable after
  the dedup `filter`; `?? 0n` defaults unreachable after the empty-
  array early return). Tightened `capabilityRank`'s input type from
  `string` to a `CapabilityValue` union literal so the switch is
  exhaustive without a default arm.
- **Test suite up from 75 → 95 tests** (+20). New coverage:
  `trackToAddress`, per-subscription `lostSignalPolicy` overrides,
  durable-subscription persistence (with stub stores), predicate-
  selector + `durable: true` warning, store.appendEvent / store.put
  failure routing through `onError`, bad-block-number handling,
  async iterator queue-vs-waiter ordering and early-break cleanup,
  bulk async iterator drain via `sub.stop()`, multi-sub-on-same-hash
  cleanup semantics, idempotent `stop` / `unsub` / `sub.stop`,
  reorg handler skipping records without `lastSeenInBlock`,
  `findReplacement` raw-nonce fallback when `BigInt()` throws,
  `lifecycle: 'lazy'` accepts-the-option contract.
- Coverage went **89.23% / 78.59% / 92.13% / 91.84%** stmts / branches
  / funcs / lines → **96.13% / 88.93% / 98.87% / 97.61%**, then to
  **97.22% / 92.7% / 98.9% / 98.12%** after the per-record decision
  logic was extracted into pure functions (see "Refactor" below).

### Refactor

- **Per-record decision logic extracted from `tracker.ts` into a new
  pure module `observations.ts`.** The previous shape — two giant
  closures inside `onBlock` / `onMempool` mutating shared state and
  emitting events as a side effect — was a pile of conditionals that
  could only be tested by spinning up the full state machine through
  a stub source. Now `decideBlockObservation` and
  `decideMempoolObservation` are pure functions: literal inputs in,
  `{ events, statusPatch, identityPatch, inMempoolPatch }` out. The
  orchestrator in `tracker.ts` shrank to "compute envelope, loop
  records, call decision fn, merge patch, emit events." Same shape
  as the rest of the toolkit (`reducePollInputs` pure / poll loop
  stateful in gas-oracle; math pure / source stateful in chain-source).
  - **`observations.ts` lands at 100% statements / 100% branches**
    (67/67 stmts, 55/55 branches) covered by 33 fixture-driven unit
    tests in `observations.test.ts`. Each per-record decision arm
    has a dedicated test with literal inputs — no async, no stubs,
    no shared state.
  - `tracker.ts` shrank from 374 statements → 344 (the extracted
    code is gone) and is now mostly orchestration; its branch
    coverage rose from 86.69% → 88.75%.
  - `findReplacement` (closure-based) replaced with pure
    `findReplacementInMempool(snapshot, identity, originalHash)`.
    `cacheIdentityFromTx` (mutation-based) replaced with pure
    `cacheIdentity(current, tx)` returning a patch.
  - **No behavior change.** All 95 pre-refactor integration tests
    continue to pass unchanged; the refactor is internal-only and
    the public API surface is identical.
  - Tracker test suite: 95 → 133 (+38: 33 from `observations.test.ts`
    plus 5 new tracker integration tests covering reorg height-mismatch
    skip and async-iterator multi-waiter drain paths).

### Notes

- Implements spec §5–§12 minus the `'receipt-poll-fallback'`
  lostSignalPolicy strategy (the type is accepted; the runtime
  falls back to `'emit-uncertain'` and a follow-up PR adds the
  per-block receipt fetch path).
- Predicate bulk selectors are silently non-durable per spec §13.2
  (closures don't survive a process boundary). The tracker logs a
  warning via `onError` when a `predicate` selector is registered
  with `durable: true` and persists everything else about the
  selector.
- `gas-oracle` and `tx-tracker` remain siblings — neither imports
  the other; both consume `@valve-tech/chain-source` directly.

## [0.6.0] — 2026-05-05

### Notes

- Synchronized release — no functional changes to this package
  (still a stub on npm). Bumped in lockstep with
  `@valve-tech/chain-source@0.6.0` (block-stream dedup + head-probe
  gating in the source tick) and `@valve-tech/gas-oracle@0.6.0`
  (now consumes ChainSource via `source?: ChainSource`). The
  tx-tracker implementation track lands in a future minor — this
  version exists to keep the synced version line consistent across
  the toolkit.

## [0.5.0] — 2026-05-05

### Notes

- Synchronized release — no changes to this package (still an
  `export {}` stub). Bumped in lockstep with
  `@valve-tech/wallet-adapter@0.5.0`, whose enriched `WriteHookParams`
  / `WritePhaseEvent` shapes are the contract this package will fire
  `onDropped` and `onReplaced` against once it ships. See the
  wallet-adapter changelog for the migration details.

## [0.4.1] — 2026-05-04

### Notes

- Synchronized release — no changes to this package. Bumped in
  lockstep with `@valve-tech/wallet-adapter@0.4.1` which fixes the
  `workspace:^` leak in its published manifest. See that package's
  changelog for details.

## [0.4.0] — 2026-05-04

### Notes

- Synchronized release — no changes to this package. Bumped in
  lockstep with the rest of the toolkit, which adds two new packages:
  `@valve-tech/viem-errors` (cause-chain error utilities) and
  `@valve-tech/wallet-adapter` (wallet contract + lifecycle hooks).
  The contract additions in `wallet-adapter` (notably `onDropped` /
  `onReplaced` hooks plus the `WritePhase` discriminated union) are
  designed to be the consumer-facing surface that this tracker fires
  against once its v0.3.x implementation lands.

## [0.3.1] — 2026-05-04

> **First fully-synchronized release.** Part of the
> `valve-tech/evm-toolkit` v0.3.1 synchronized release line. All
> three packages in the toolkit (`@valve-tech/chain-source`,
> `@valve-tech/gas-oracle`, `@valve-tech/tx-tracker`) ship in
> lockstep from this version onwards under a single `vX.Y.Z` tag.

### Notes

- v0.3.1 contents are byte-identical to the planned v0.3.0 — still
  a name reservation and minimal scaffold (the `index` exports
  nothing). The actual per-tx state machine (the `TxEvent`
  discriminated union, `TxTrackerStore` interface + in-memory
  default, bulk-subscription matchers, reorg detector, three
  consumption shapes) lands in subsequent 0.3.x releases per the
  design contract in
  [`docs/tx-tracker-spec.md`](https://github.com/valve-tech/evm-toolkit/blob/main/docs/tx-tracker-spec.md).
- v0.3.0 was tagged but did not publish to npm — the toolkit-wide
  release workflow failed at the gas-oracle publish step (OIDC
  trusted-publisher mismatch from the repo rename) and aborted
  before reaching this package. The publisher record was fixed and
  v0.3.1 re-releases all three packages.
- `viem ^2.0.0` is the only peer dependency. The dependency on
  `@valve-tech/chain-source` will be declared once the implementation
  actually imports it (subsequent 0.3.x release).

## [0.3.0] — 2026-05-04 — *unpublished; superseded by 0.3.1*

> Tagged but never published to npm — the toolkit's release workflow
> aborted before reaching this package's publish step (see Notes
> above). Superseded by v0.3.1 which carries identical content.

## [0.0.1] — 2026-05-04 — *initial name-reservation publish*

> Manually published from a maintainer's machine during the toolkit
> rename + first-publish setup. No content — `index` exports nothing.
> Superseded by the v0.3.x synchronized line.
