Some mints (nutshell < 0.20.1) return paid-melt-quote change[]
signatures in an order that does not match the blank outputs the
wallet sent. cashu-ts OutputData.toProof assumes positional pairing
and throws on its DLEQ check, so mapping over the array aborted on the
first mismatch and the catch discarded ALL change — recording it as
fee. A user lost ~100k sats of change on a single transfer this way.
Add CashuUtils.recoverMeltChange, a resilient helper that:
- matches each signature to the blank whose blinded message makes the
mint's DLEQ proof verify (verifyDLEQProof as alignment oracle),
correctly re-pairing reordered change while keeping DLEQ as a hard
guarantee;
- on a genuine DLEQ failure (no blank verifies) falls back to
unblinding the positional blank WITHOUT DLEQ so funds are recovered
rather than dropped, logged at error level;
- returns stats so callers can surface anomalies.
Both melt finalize paths (transferOperationApi._finalizePaid and
meltOperations.handlePendingMeltTask) use the helper and append a
COMPLETED-status tx.data entry with the recovery stats when a genuine
error occurred (no-DLEQ fallback / unmatched) — benign reordering is
not flagged.
Covered by __tests__/recoverMeltChange.test.ts (in-order, shuffled,
fewer-sigs-than-blanks, duplicate denominations, no-DLEQ fallback,
unmatched).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate per-transaction in-flight request data (params for NUT-19
idempotent retry) from the MST MintProofsCounter to a dedicated SQLite
table, so retries work with no MST loaded — completing the off-MST set
needed for background NWC.
- schema/migration v29: inflight_requests (txId PK, mintUrl, keysetId,
request JSON); added to cleanAll.
- inFlightRepo: add (INSERT OR REPLACE = set semantics) / get /
getInFlightRequestsByMint / remove / seed (ON CONFLICT DO NOTHING).
- WalletStore: write/remove via Database (receive/send/mint paths).
- inFlightOperations: enumerate via a flat Database.getInFlightRequestsByMint
query instead of the mint.proofsCountersWithInFlightRequests nested loop;
removeInFlightRequest via Database; queue guard uses the DB count.
- Mint model: remove inFlightRequests map, InFlightRequestModel, all the
in-flight actions/views (counter + mint level). The InFlightRequest TYPE
is kept (WalletStore option signatures). MintProofsCounter is now just
{keyset, unit, counter}. migrateSnapshot strips inFlightRequests AND
meltCounterValues from old snapshots.
- one-time seed (rootStoreModelVersion 34->35): _runMigrations reads the
raw pre-upgrade snapshot for any in-flight requests; idempotent.
- tests: __tests__/inFlightRequests.test.ts.
Full suite green (15 suites / 163 tests). With M1+M2, the MintProofsCounter
sub-model now carries only the counter (itself SQLite-authoritative) — a
candidate to collapse later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate per-transaction melt recovery data (the serialized meltPreview)
from the MST MintProofsCounter (debounced MMKV) to a dedicated SQLite
table, so it can be written synchronously and read with no MST loaded.
Why: meltPreview is recovery-critical — it unblinds the change of a paid-
but-unconfirmed melt. It was persisted only via the batched whole-tree
MMKV snapshot, so a crash right after the payment was submitted could lose
it and the change ecash. It's also a prerequisite for off-MST background
melt (NWC pay_invoice).
- schema/migration v28: new melt_recovery table (txId PK, mintUrl,
keysetId, meltPreview JSON); added to cleanAll.
- meltRecoveryRepo: add (ON CONFLICT DO NOTHING — first preview wins,
matching the old "already tracked" guard) / get / remove / seed.
- WalletStore: write the preview synchronously via Database.addMeltRecovery
BEFORE completeMelt; remove on terminal success/failure.
- meltOperations / transferOperationApi: read/remove via Database instead
of the counter model; drop the now-needless counter fetch in those blocks.
- Mint model: remove meltCounterValues map, MeltCounterValueModel, the melt
actions/views, the dead counterAtMelt field, and serializeMeltPreview
(moved to cashuUtils). migrateSnapshot now STRIPS meltCounterValues from
old snapshots so applySnapshot tolerates the removed field.
- one-time seed (rootStoreModelVersion 33->34): _runMigrations reads the
RAW pre-upgrade snapshot (the model no longer holds it) and copies any
in-flight meltPreview into SQLite. Idempotent.
- tests: __tests__/meltRecovery.test.ts (JSON round-trip, first-wins,
remove, isolation, idempotent seed).
Full suite green (14 suites / 157 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add unit coverage for the derivation-counter migration (mirrors the
production SQL against node:sqlite, like proofReservation.test.ts):
- setCounter is monotonic — raises, ignores a lower value, no-ops on equal
- bumpCounter advances relatively; non-positive delta is a no-op
- (mintUrl, keysetId) primary key isolates keysets and mints
- seedCounters is idempotent and never regresses an advanced counter;
a too-high seed is kept (conservative-safe)
- the folded counterUpdate persists the counter atomically with new proofs,
rolls back with them on a failed batch, and stays monotonic in-batch
Locks in the no-secret-reuse safety property of the migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- testMatch now only picks up *.test/*.spec files so the i18n scripts
(run via `yarn test:i18n`) are no longer globbed as empty suites
- Allowlist react-native-flash-message for transform and map
react-native-quick-crypto to a Node crypto shim, unblocking the 11
cashuDleq tests that load it transitively via @scure/bip32
- Remove orphaned storage.test.ts (module + async-storage dep gone) and
the boilerplate App.test.tsx smoke test that mounted the full native tree
Suite now: 12 suites / 136 tests passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure reorganization behind the existing Database facade — no behavior change.
The 1646-line services/sqlite.ts is broken into services/db/:
errors.ts dbError() helper
connection.ts op-sqlite adapter (from previous commit)
schema.ts table column definitions (single source of truth)
migrations.ts _dbVersion, getDatabaseVersion, ordered migration registry
mappers.ts transaction row normalizers
instance.ts _db singleton, getInstance, schema bootstrap, cleanAll
transactionsRepo.ts / proofsRepo.ts / reservationsRepo.ts
index.ts assembles the Database facade, re-exports public types
services/sqlite.ts is now a thin `export * from './db'` barrel so every
existing import path (services, services/sqlite, ../../sqlite) resolves
unchanged. The Database.* contract and the exported types
(TransactionSearchFilters, LockedProofSnapshot, ReservationRow,
ReservationTransactionUpdate) are preserved.
Notable refactors folded in:
- schema single source of truth: proofs/reservations columns are defined
once; first-run CREATE and the v25 rebuild / v26 add are generated from
them so they cannot drift. PROOFS_COLUMN_NAMES (drives the v25 copy) is
tested to stay in sync with the table.
- migration if-chain replaced by an ordered MIGRATIONS registry; adding a
migration is now a one-line append.
- updateStatusesAsync and expireAllAfterRecovery now use getInstance()
instead of reaching for the module-global _db directly (the only two
functions that did; behavior-equivalent, more robust before init).
New: __tests__/dbSchema.test.ts validates the generated DDL against node:sqlite.
Tests: 125/125 pass (was 121 + 4 new), no regressions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>