Completes the mint-identity work. A mint url is a network locator and mints
move, but the url had become the de facto foreign key for nearly every
persisted row, so a url edit had to fan out across the schema and mostly did
not. Each table now references the mint by an identity that a move cannot
disturb, and setMintUrl shrinks to the two things that genuinely hold a url:
Mint.mintUrl and the proofs.mintUrl cache.
Which identity, per table
mintId (Mint.id) where the row needs the mint itself:
- onchain_mint_quotes. The critical one: a quote's address stays creditable for
as long as the mint exists (rows are never deleted), so the reference has to
outlive a move. It followed row.mintUrl, and the watcher swallows errors by
design — a renamed mint stranded deposits permanently and silently.
- reservations. A url edit racing an open send did not merely misfile the
proofs; commitReservation resolved the mint by url, so it threw "Mint not
found" and aborted the commit of an operation the mint had already performed.
- transactions. `mint` was two things at once, switched by status: a historical
record of where a finished payment happened, AND a live pointer dialled for an
open one. That conflation is why a rename had to rewrite in-flight rows —
rewriting the very column that records the past. mintId takes the identity
job, so `mint` is frozen as history and the status-scoped rewrite is retired.
No reference at all where the row already has a parent:
- inflight_requests, melt_recovery are CHILD rows of a transaction (their
primary key IS transactionId), so the parent owns "which mint". Their mintUrl
and keysetId copies had ZERO readers — the keyset that is used comes from
inside meltPreview. Both dropped; the one mint-scoped query joins through
transactions.mintId. Nothing left to go stale.
Mint.id gets referential authority only, never identity authority: findById
answers "which mint is this row about?" and must never answer "are these the
same mint?" — it is random and unrelated to the keys, so that question stays
with the keysets. The backfills are IS NULL-guarded so a resolved row can never
be re-pointed at whichever mint now answers an old url.
Backfills run from JS (v38 seed), not SQL: mints live in the MST/MMKV snapshot,
so nothing in SQL can map url -> id. Matching on url is trustworthy at exactly
that moment and no other — until now a url could not change without these rows
being rewritten to match. The join is spent once, at rest, instead of on every
rename.
Migration-system fixes found along the way
- _dbVersion is now DERIVED from the migration list. It was a hand-maintained
literal, and it was already wrong: it said 33 while migration 34 existed, so
34 would never have run. The failure is silent and asymmetric — fresh installs
build from schema.ts and are fine, while upgrading devices land on a schema
the code does not have. dbMigrationRegistry.test.ts pins this and the ordering
invariants.
- Migrations 26/28/29/31 built their tables from the LIVE schema constants. A
device replaying them would get today's shape, and the later ALTER adding the
column would fail with "duplicate column name" — breaking upgrades from
exactly the versions those migrations serve. Historical shapes are frozen
locally now, and the schema.ts header no longer recommends the sharing.
- rootStoreModelVersion was left at 37 while the seed guarded on < 38, so the
backfill would have re-run on every launch forever.
Tests: 421 pass. Six hand-mirrored suites had drifted from the schema they
claim to mirror; transactionsMintUrl.test.ts was worse — still passing while
testing a function this commit deletes, so it is removed. The mirroring pattern
is worth revisiting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>