Commit Graph
521 Commits
Author SHA1 Message Date
minibits-cash 45cef1c60a New welcome screen 2026-07-08 15:44:11 +02:00
minibits-cash ddbd1a6848 Bump version 2026-06-27 23:56:24 +02:00
minibits-cash 81699251aa Fix fee estimation 2026-06-27 23:55:40 +02:00
minibits-cash 74c1b77765 Fix foreground service 2026-06-27 23:55:08 +02:00
minibits-cash 670c0f5c98 Fix layout 2026-06-16 23:10:11 +02:00
minibits-cash 33d7bc07c0 UX improvements (Wallet, NFC) 2026-06-14 23:25:15 +02:00
minibits-cash 4a73b4c877 NFC error handling 2026-06-12 21:52:55 +02:00
minibits-cash 3d1f303bda iOS NFC error handling 2026-06-10 22:43:14 +02:00
minibits-cashandClaude Opus 4.8 58e72a28a9 Merge branch 'fix/melt-change-dleq-recovery'
fix(melt): recover NUT-08 change when mint reorders signatures

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:58:00 +02:00
minibits-cashandClaude Opus 4.8 3caf54fa35 fix(melt): recover NUT-08 change when mint reorders signatures
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>
2026-06-09 21:55:04 +02:00
minibits-cash bbe8688249 Merge branch 'perf/snapshot-persistence-dedup'
Dedup redundant MMKV snapshot writes (serialized-payload equality guard) and
make the Mint derivation counter volatile so per-derivation bumps no longer
trigger whole-tree snapshot writes. SQLite remains the counter authority.
2026-06-09 15:31:12 +02:00
minibits-cashandClaude Opus 4.8 05c069e4f8 chore(logging): redact secrets in keyChain trace logs
Truncate or omit sensitive values in trace-level logs: Nostr private key,
base64 seed and auth token now log only an 8-char prefix, and saveWalletKeys
logs walletId instead of the full key material.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:30:43 +02:00
minibits-cashandClaude Opus 4.8 8f6e468361 perf(persistence): dedup MMKV snapshot writes + make derivation counter volatile
The root onSnapshot listener wrote the whole MST snapshot to MMKV on every
action, even when postProcessSnapshot stripped the only changed field
(proofs, transactions, counters, tokens) to a byte-identical payload — MST
still fires onSnapshot and re-serializes regardless. During wallet
transactions this produced a storm of redundant whole-tree writes.

Tier A: guard the write by comparing the serialized payload against the last
one and skipping identical writes (stringify once, persist the raw string via
saveString to avoid a second serialize). Measured ~77% of writes eliminated in
steady-state operation at ~0.25ms/fire overhead. Behind a toggleable
PROFILE_SNAPSHOT_PERSISTENCE constant: cumulative invocations/writes/skipped/
totalMs trace, off by default.

Tier B: move Mint derivation `counter` from a persisted prop (stripped to 0 in
postProcessSnapshot) to volatile state, so per-derivation bumps never invalidate
the root snapshot at all. The counter is already SQLite-mastered (mint_counters)
and hydrated on startup. Reconciled the paths that fed counters through a
snapshot — backup export re-injects live.counter (unchanged), import and the
v<33 migration now seed SQLite from the raw snapshot then re-hydrate the cache
(applySnapshot no longer loads the volatile value). Removed the now-dead
seedCountersToDatabase. No model version bump required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:27:48 +02:00
minibits-cashandClaude Opus 4.8 bf356f3ba7 Merge branch 'feat/counters-sqlite-migration'
Counters → SQLite migration: move mint counters, melt counter values, and
in-flight requests off MMKV/MST into SQLite with atomic write-through,
startup hydration, seeding, and tests; remove the old counterBackups.

NWC background optimization: process pushed commands directly (no WS
re-fetch), adaptive listener lifetime, lean cold-wake hydration, async
melt with lifetime-bound preimage wait, and coalesced keychain reads.

Release: bump to 0.4.3-beta.8 and upgrade HotUpdater 0.23.0 → 0.32.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:24:03 +02:00
minibits-cashandClaude Opus 4.8 d774fde9ce chore(release): bump to 0.4.3-beta.8; upgrade HotUpdater 0.23.0→0.32.0
Upgrade react-native HotUpdater (0.23.0 → 0.32.0): drops SWCompression /
BitByteData pods and bumps OpenSSL-Universal to 3.6.2000. Set the iOS
launch scheme to Release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:23:37 +02:00
minibits-cashandClaude Opus 4.8 24a9a85b9f perf(nwc): coalesce concurrent KeyChain reads in getCachedWalletKeys [Stage 4a]
Several NWC pushes can wake the app simultaneously (e.g. a wallet
connecting fires get_balance + list_transactions together, or rapid
zaps). Each caller previously raced past the walletKeys cache check
before any populated it, so each triggered its own ~2s cold KeyChain
read.

Park the in-flight read in a volatile promise; concurrent callers await
the same fetch instead of hitting secure storage independently. The
shared promise resolves to validated keys so the originator and all
awaiters get identical success/error. Volatile (never persisted) so a
fresh cold start still does exactly one fresh read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:09:29 +02:00
minibits-cashandClaude Opus 4.8 0d43095108 perf(nwc): async melt with lifetime-bound preimage wait [Stage 6]
NWC pay_invoice forced SYNCHRONOUS melt, holding the background context
(JS, foreground service, SyncQueue) for the entire lightning settlement —
the worst offender for the OS background budget, especially on iOS and for
slow/stuck payments.

Switch NWC melt to async (preferAsync: true, matching all other flows):
the mint ACKs immediately and the existing monitor (ws/poll) finalizes on
settlement. payInvoice then waits for the preimage only as long as the
background is alive — resolves on ev_asyncMeltResult (fast common case →
reply with preimage) or ev_nwcListenerClosing (Stage 2 teardown), bound by
a safety cap. No separate per-pay timeout.

If still pending at teardown: resolve to a typed 'pending' outcome (no
NIP-47 wire reply — never fabricate a false error/success). The payment
finalizes later via the monitor / next foreground; zaps confirm via the
NIP-57 receipt (recipient's zapper publishes 9735 at settlement,
independent of our reply); non-zap clients retry and the mint dedups by
payment hash. Daily limit reserved conservatively on the pending path.

- transferOperationApi: drop the nwcEvent sync/async distinction.
- events.ts: ev_nwcListenerClosing; notificationService emits it on close.
- NwcStore: waitForAsyncMeltResult, typed NwcPending outcome, dispatcher
  skips sending pending, settled path re-reads preimage/amounts from SQLite.

tsc clean, suite 15/163 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:12:18 +02:00
minibits-cashandClaude Opus 4.8 3b992723ff perf(nwc): lean cold-wake hydration (skip JWT + proofs) [Stage 4a]
Measurement showed cold-wake setupRootStore is dominated by the keychain
JWT load (~63ms fixed, NWC never uses it) and loadProofs (scales with
wallet size, builds an MST node per proof synchronously). Make the
background NWC wake skip both.

- setupRootStore: {skipTokens, skipProofs} options. skipProofs also skips
  orphan-reservation recovery (bundled with proof loading). Foreground
  app-open still runs a FULL setup and SQLite is authoritative, so a lean
  wake is reconciled when the user opens the app.
- notificationService._nwcRequestHandler: lean wake (skipTokens+skipProofs).
- ProofsStore.ensureProofsLoaded(): load proofs + orphan recovery on demand
  (no-op once hydrated). Mutating NWC commands (pay/multi_pay/make_invoice)
  call it before selecting proofs — covers follow-up commands too, so the
  worst case of a bug is a gracefully-failed pay, never a wrong spend.
- get_balance reads SQLite (proofsRepo.getMintBalanceWithMaxBalance) instead
  of the MST proof map; the per-connection daily-limit cap is unchanged.
- loadRecentTx kept (list_transactions still reads MST history; skipping it
  would force a rewrite for only ~20ms).

Read-only commands now answer with no proof/JWT load; pay loads proofs
lazily (its cost is intrinsic anyway). tsc clean, suite 15/163 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:01:27 +02:00
minibits-cashandClaude Opus 4.8 c515ffcc73 chore(nwc): separate keychain-token load from loadProofs timing [Stage 0]
The stateHydrated marker sat before loadTokensFromKeyChain, so the JWT
keychain load was misattributed into the loadProofs bucket. Add a
tokensLoaded marker and a distinct loadTokens phase so the breakdown is
accurate (matters for the Stage 4 decision and on heavier wallets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:22:22 +02:00
minibits-cashandClaude Opus 4.8 cc9f012c23 chore(nwc): instrument setupRootStore cold-hydration phases [Stage 0]
Isolate the per-phase timings (mmkvLoad, applySnapshot, loadProofs,
hydrateCounters, recoverOrphans, loadRecentTx, total) plus proofCount and
stateBytes into a single info-level summary, so a background NWC wake
shows exactly where cold-hydration time goes. Drives the Stage 4
lean-hydration decision (hypothesis: applySnapshot + loadProofs dominate).

Measurement only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:13:00 +02:00
minibits-cashandClaude Opus 4.8 489c44e4f4 perf(nwc): adaptive listener lifetime instead of flat 30s [Stage 2]
The NWC listener / Android foreground service was held for a fixed 30s
after every wake, even when a single quick command finished in ~2s.

Replace the flat setTimeout with an idle-aware poller that closes ~8s
after the SyncQueue goes idle (no NWC command queued or running), capped
at 30s. An in-flight task keeps the window open (never closes mid
pay_invoice) and each follow-up command extends it via queue activity —
so bursts still get the fast-follow-up benefit while the idle tail is
trimmed, cutting foreground-service / websocket hold time per wake.

Uses the existing SyncQueue.getAllTasksDetails(['idle','running']) count.
tsc clean, suite 15/163 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:08:42 +02:00
minibits-cashandClaude Opus 4.8 2f350a346c perf(nwc): process pushed command directly; drop WS re-fetch [Stage 1]
The FCM push payload already carries the NWC request event, but the
background handler discarded it and spun up a WebSocket subscription to
re-fetch the same event before processing — adding a relay round-trip to
the first (often only) command.

- NwcStore.receivePushedEvent: dedup-mark the pushed event, set the
  follow-up listener window from it, and dispatch it on the SyncQueue
  (connection-level handler, same as the WS path). Must run before the
  listener opens so the event can never be processed twice.
- notificationService._nwcRequestHandler: process the pushed event
  directly, THEN open the listener purely for follow-ups.
- index.js: remove the dead HANDLE_NWC_REQUEST_TASK foreground-service
  branch (no notification ever triggered it) + now-unused imports.
- fix a double eventsBatch.push in listenForNwcEvents.onevent.

First command no longer waits on a WS connect+subscribe+round-trip; the
listener still catches follow-ups. No state-model changes (still runs
after setupRootStore — that's Stage 4). tsc clean, suite 15/163 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:02:52 +02:00
minibits-cashandClaude Opus 4.8 3321096fe2 refactor(counters): remove counterBackups (SQLite retention supersedes it)
counterBackups preserved a removed mint's counters in MMKV for restore on
re-add. That's now redundant: mint_counters rows are never deleted on mint
removal and addMint restores via hydrateCountersFromDatabase. Keeping both
also caused a latent double-advance on re-add (updateMintCountersFromBackup
bumped SQLite on top of the retained row).

- MintsStore: remove counterBackups field, CounterBackupModel/CounterBackup
  type, addOrUpdateCounterBackup + updateMintCountersFromBackup, the call in
  removeMint/addMint, and the counterBackups loop in seedCountersToDatabase.
  removeMint now just detaches; addMint relies on hydrate.
- snapshot compat: MintsStore.preProcessSnapshot strips counterBackups from
  old snapshots so applySnapshot tolerates the removed field.
- backup export: drop the counterBackups field (old backups that contain it
  are stripped on import via the same preProcessSnapshot).
- one-time seed (rootStoreModelVersion 35->36): _runMigrations copies any
  removed-mint counters from the RAW pre-upgrade snapshot's counterBackups
  into SQLite, so a later re-add still restores them. Monotonic.

Full suite green (15 suites / 163 tests). counterBackups was the last
counter-adjacent field still in MMKV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:02:04 +02:00
minibits-cashandClaude Opus 4.8 14f8d1de8c feat(inflight): move inFlightRequests to SQLite (off-MST) [M2]
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>
2026-06-05 09:50:56 +02:00
minibits-cashandClaude Opus 4.8 8d91fad254 feat(melt): move meltCounterValues to SQLite (off-MST, durable) [M1]
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>
2026-06-05 09:36:53 +02:00
minibits-cashandClaude Opus 4.8 2d294c2200 docs(counters): clarify the complementary W1/W2 counter writes
The counter is persisted twice per reservation op, and the old comments
mis-described the roles (write-through as "best-effort", the commit upsert
as "the keystone"). Correct them to reflect the actual complementary design:

- W1 (Mint.persistCounter write-through): the PRIMARY persistence — fires
  the instant cashu derives, before the commit, so the advance is durable
  the moment the mint could have seen the outputs. Covers crash-before-commit
  and rollback (indices are consumed at the mint; rollback must not rewind).
  Errors are logged (→ Sentry in prod), not thrown.
- W2 (commitReservation counterUpdate): the atomic BACKSTOP — a monotonic
  no-op on the normal path (W1 already persisted), but if W1's write was
  dropped, folding the counter into the proof batch guarantees a committed
  proof can never outlive its counter advance.

Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:45:04 +02:00
minibits-cash b9ee74e631 Logs tuning 2026-06-04 15:46:22 +02:00
minibits-cashandClaude Opus 4.8 19bd77eddc refactor(counters): run one-time seed in _runMigrations; guard re-seed
Move the MMKV->SQLite counter copy out of the setupRootStore hot path
into _runMigrations (gated by version bump 32->33). The core path now only
does the every-launch hydrate, alongside loadProofsFromDatabase. Restructure
_runMigrations so each step is independent and the version is set once at
the end (a throwing step retries next launch; also fixes a latent quirk
where v29-31 users never had their version bumped).

The seed reads the LIVE MST counters, which still hold real values after
the snapshot strip (postProcessSnapshot strips counter from saves, not the
in-memory model). Hydrate stays every-launch — it is not a migration.

Safety for devices that ALREADY migrated to SQLite while still on model
v32 (SQLite populated, MMKV stripped to 0): the re-run of the migration
cannot reset their counters, guarded two ways —
  1. seedCountersToDatabase only seeds counters > 0 (never writes a
     stripped/zero value), and
  2. the repo upsert is monotonic (MAX), so a seed can never lower an
     existing SQLite counter.
Hydrate also restores the real values into the model before the seed runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:45:28 +02:00
minibits-cashandClaude Opus 4.8 8ffce9e46a test(counters): cover mint_counters invariants
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>
2026-06-04 15:23:23 +02:00
minibits-cashandClaude Opus 4.8 196caca42a fix(db): drop mint_counters + reservations in cleanAll
cleanAll only dropped transactions/proofs/dbversion, leaving the
mint_counters and reservations tables (added by later migrations) behind.
A wallet wipe would keep stale derivation counters, which the next launch
would hydrate. Drop them too (IF EXISTS, so an old DB lacking them doesn't
abort the atomic batch). Tables are recreated on next launch via the
IF-NOT-EXISTS schema bootstrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:17:50 +02:00
minibits-cashandClaude Opus 4.8 de85703b4d fix(mint-info): show live counter in debug JSON tree
MintInfoScreen renders getSnapshot(mint), and postProcessSnapshot zeros
`counter` in every snapshot (it is mastered in SQLite, not MMKV). The
debug tree therefore showed counter: 0 even though the live in-memory
counter is correct. Re-inject the live per-keyset value into the snapshot
before display, the same way the backup export does.

Display-only; no effect on derivation or persistence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:21:30 +02:00
minibits-cashandClaude Opus 4.8 276cece26a refactor(proofs): phase out addOrUpdate; single atomic write path (step 5)
ProofsStore.addOrUpdate was a second proof-creation path parallel to
commitReservation, with its own MST mirroring loop and its own
non-atomic Database.addOrUpdateProofs write + separate tx.update. That
left counter/proofs/tx as three separate writes on the recovery paths.

Convert all six add-only callers to the reservation pattern
(reserve([]) + commitReservation{newProofs, transactionUpdate}), so proofs
+ tx + keyset counter land in ONE SQLite transaction — same idiom topup
finalize already uses:
  - mintOperations.recoverMintQuote
  - meltOperations change-recovery
  - inFlightOperations receive-retry + topup-retry
  - SeedRecoveryScreen UNSPENT + PENDING

Then remove addOrUpdate entirely. Proof creation now flows through a
single, atomic, well-tested code path, shrinking the critical surface.
Multi-step seed-recovery tx.update() sequences collapse into one atomic
transactionUpdate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:53:05 +02:00
minibits-cashandClaude Opus 4.8 3cce38a2bd fix(counters): remove redundant in-method counter advances
Both proofsStore.commitReservation and proofsStore.addOrUpdate advanced
the keyset counter by the number of new spendable proofs. Under cashu-ts
v3.x this double-counts: the authoritative advance to reservedCounters.next
has already happened on the path that produced the proofs, and it covers
every index those proofs consumed. The extra +length only over-advanced
the counter (safe — never reuses — but wasteful, and it inflated the range
seed recovery must scan).

Audited every caller of both sites; each has an independent, sufficient
advance before reaching these methods:
  - commitReservation: send/receive/mint/melt/revert/in-flight all call a
    WalletStore op -> setProofsCounter(reservedCounters.next).
  - addOrUpdate: inflight/mint (setProofsCounter), melt-recovery (indices
    pre-reserved at prepare via meltPreview), seed-recovery (whole-interval
    advance in SeedRecoveryScreen).

The counter is now advanced in exactly one authoritative place per path,
and (for the reservation path) persisted atomically with the proofs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:14:43 +02:00
minibits-cashandClaude Opus 4.8 72de0a4c76 feat(counters): commit counter atomically with proofs (step 4)
Fold the derivation-counter advance into the same SQLite transaction as
the proof-commit batch, closing the proofs-table <-> mint_counters
atomicity window. A crash that committed new proofs but not the advanced
counter could otherwise let the next derivation reuse a blinded secret.

- countersRepo: extract buildCounterUpsert (monotonic upsert tuple) and
  reuse it in setCounter/seedCounters so the SQL lives in one place.
- reservationsRepo.commitReservation: accept counterUpdate[] and batch a
  monotonic upsert per keyset alongside the proof writes + tx update.
- ProofsStore.commitReservation: derive the per-keyset counter snapshot
  from the new proofs (a cashu proof.id IS its keyset id) and pass it to
  the atomic commit. Centralized in the single wrapper; no operation-API
  call sites change.

Value-preserving: persists the same counter the model already holds,
monotonically. The redundant in-method increments are removed separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:11:27 +02:00
minibits-cashandClaude Opus 4.8 2965f6df7a fix(import): write imported proofs with correct state column
ImportBackupScreen still called addOrUpdateProofs with the pre-v25
boolean signature (proofs, isPending, isSpent). After the v25 migration
replaced the isPending/isSpent columns with a single `state` TEXT column,
that 2nd boolean was written verbatim into `state` (stored as '0'/'1'),
so imported ecash never matched the `state IN ('UNSPENT','PENDING',
'SPENT')` filter in getProofs and silently disappeared from the balance
after the next app restart.

Pass the proper ProofState instead: 'UNSPENT' for the unspent set and
'PENDING' for the pending set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:41:06 +02:00
minibits-cashandClaude Opus 4.8 fd19807e18 feat(counters): strip counter from MMKV snapshot + keep backups correct
Make SQLite the sole persisted store for the derivation counter by
stripping `counter` from every model snapshot (postProcessSnapshot on
MintProofsCounter, mirroring how ProofsStore strips `proofs`). This stops
the foreground whole-tree MMKV save from ever writing a stale counter
back over the SQLite authority.

Because getSnapshot is shared by persistence AND the legitimate counter
consumers, re-inject the live value where it must survive:

- Backup export: re-inject live counters per keyset into the exported
  mints snapshot (exporting counter 0 would risk secret reuse on restore).
- Backup import: seed the imported counters into SQLite right after
  applySnapshot (monotonic).
- counterBackups: re-inject live counters when capturing a backup at mint
  removal, and hydrate from SQLite after a mint is (re-)added — SQLite
  retains counters by (mintUrl, keysetId) across removal, covering the
  re-add-after-restart case where the persisted backup reloaded as zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:35:38 +02:00
minibits-cashandClaude Opus 4.8 24b37ff2b5 feat(counters): hydrate from SQLite on foreground resume (step 3)
Hook counter reconciliation into the existing WalletScreen AppState
'active' handler. A background path may advance a counter in SQLite
while the app is backgrounded-but-alive; hydrating on resume (before
performChecks can derive proofs) keeps the in-memory cache from going
stale. Monotonic, so it only ever raises a live value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:25:28 +02:00
minibits-cashandClaude Opus 4.8 a8caa2a21f feat(counters): write-through + seed + startup hydrate (step 2)
Make SQLite the runtime authority for derivation counters while keeping
the MST counter as the in-memory cache every call site already reads.

- Mint: increaseProofsCounter/setProofsCounter now write through to the
  countersRepo (relative bump / monotonic set). A new hydrateCounterFromDb
  loads the authoritative value into the cache without writing back.
  decreaseProofsCounter stays MST-only (no callers; lowering is the safe
  direction the monotonic store ignores anyway).
- MintsStore: seedCountersToDatabase (idempotent MMKV->SQLite copy, incl.
  counterBackups) and hydrateCountersFromDatabase (SQLite -> cache).
- setupRootStore: seed then hydrate on startup, after proofs load. Both
  directions monotonic, so order-safe and a no-op after the first copy.

Counter still persists in the MMKV snapshot for now (kept identical via
write-through); stripping it is deferred to the counterBackups migration
to avoid breaking getSnapshot(proofsCounters). No behavior change for the
existing MST-driven flows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:22:05 +02:00
minibits-cashandClaude Opus 4.8 2b422ee569 feat(db): add mint_counters table + countersRepo (step 1)
Introduce a SQLite-authoritative store for per-keyset deterministic
derivation counters, previously held only in the MST MintProofsCounter
model and persisted to MMKV via the whole-tree snapshot.

- schema: new mint_counters table, PK (mintUrl, keysetId)
- migration v27: creates the table (empty; JS seed comes in step 2)
- countersRepo: getCounters/getCounter + monotonic setCounter, relative
  bumpCounter, and idempotent batched seedCounters
- facade: expose on Database.* with CounterRecord/CounterSeed types

Every write is monotonic (counter = MAX(existing, new)), the invariant
that makes the upcoming MMKV->SQLite seed and atomic write-back safe.
No existing code reads/writes the table yet; behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:13:48 +02:00
minibits-cash 093294e124 Fix update screen state update loop 2026-06-03 23:06:28 +02:00
minibits-cash c96ffca5b8 Nfc info message fix 2026-06-02 17:47:07 +02:00
minibits-cash 0caa93bbb7 Nfc write fix 2026-06-02 17:28:09 +02:00
minibits-cash ec76967a58 NFC fix of paying cashu pr from cold / warm start 2026-06-02 16:45:26 +02:00
minibits-cash 6581fed4d7 Fix patches v0.4.3-beta.3 2026-06-02 15:46:14 +02:00
minibits-cashandClaude Opus 4.8 cafef4f462 Render BottomModal in-tree via portal to fix edge-to-edge nav bar
Keep edgeToEdgeEnabled=true so the themed header draws behind the status
bar on pre-Android-15 devices (e.g. Moto G4). Android 15+ is edge-to-edge
OS-enforced regardless.

That flag also routes react-native Modal's native dialog window through RN
core's enableEdgeToEdge() (WindowUtil.kt), which hardcodes nav-bar contrast
enforcement and derives appearance from device dark mode, painting a grey
(white-in-dark) scrim on modal open/close. That code ships in the prebuilt
RN Android AAR and can't be patched.

Work around it without disabling the flag: on Android, BottomModal renders
in the app's React tree via @gorhom/portal (coverScreen={false}) instead of
as a native window, so it never hits that code path. A PortalProvider at the
app root keeps the modal overlaying the bottom tab bar. iOS keeps the native
modal path unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:34:31 +02:00
minibits-cash e36bd02bf8 Support edge to edge mode on older devices 2026-06-02 10:00:31 +02:00
minibits-cash aa8699e57f NFC HCE fix 2026-06-02 01:38:01 +02:00
minibits-cash 9f8884b7f4 NFC and inFlight fixes 2026-06-02 01:00:30 +02:00
minibits-cash 93d23b31d4 Bump build number 2026-06-01 16:50:49 +02:00
minibits-cash c17798be39 Scope NFC auto-rearm to android 2026-06-01 16:49:48 +02:00