561 Commits

Author SHA1 Message Date
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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-cash
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
minibits-cash
082d9ee18e NFC cold and hot start support 2026-06-01 16:33:53 +02:00
minibits-cash
a35168f945 Remove hot-updater/babel-plugin from babel config (gone in 0.32.0)
0.32.0 no longer exports ./babel-plugin (the package now only exports
the config entry), which broke the build with ERR_PACKAGE_PATH_NOT_EXPORTED.
The plugin's only job was replacing the __HOT_UPDATER_BUNDLE_ID build-time
constant; the 0.32.0 client reads the bundle ID from the native module
(getBundleId()) instead, so no babel plugin is required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:29:30 +02:00
minibits-cash
f41284888b Migrate to hot-updater 0.32.0 client API (init + checkForUpdate)
0.32.0 removed the per-call getUpdateSource() helper. The base URL and
auth header are now configured once via HotUpdater.init(), and
checkForUpdate() takes updateStrategy at the top level.

- App.tsx: add module-scope HotUpdater.init({ baseURL, requestHeaders })
  so it runs before any update check. init's requestHeaders are merged
  into every checkForUpdate call by the client.
- WalletScreen / SettingsScreen / UpdateScreen: replace
  checkForUpdate({ source: getUpdateSource(...), requestHeaders })
  with checkForUpdate({ updateStrategy: "fingerprint" }); drop the now
  unused getUpdateSource import and HOT_UPDATER_URL/HOT_UPDATER_API_KEY
  env imports (auth lives in init).

updateInfo.updateBundle() / HotUpdater.reload() are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:21:38 +02:00
minibits-cash
189132790d Add .hot-updater/log to gitignore (generated by hot-updater init)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:11:23 +02:00
minibits-cash
211f27f259 Upgrade hot-updater 0.23.1 -> 0.32.0
Bump all hot-updater packages to a consistent 0.32.0 (CLI,
@hot-updater/react-native, bare, firebase, sentry-plugin), replacing the
previous mixed set (bare was pinned at 0.20.11).

- Add @expo/fingerprint as an explicit devDependency: it is now an
  optional peer dep and is required by updateStrategy: "fingerprint".
- Remove patches/@hot-updater+bare+0.20.11.patch: the hermes-compiler
  path resolution it added is now upstream in bare 0.32.0
  (getHermesCompilerPackagePath).

hot-updater.config.ts needs no changes (firebaseStorage/firebaseDatabase
config shapes unchanged). The live 0.23.1 app runtime stays wire-compatible
with the redeployed 0.32.0 server (identical path-based update-check route).

Infra steps still required (run separately): npx hot-updater init to
refresh Firebase functions + Android native config, then a native rebuild
to enable bundle diffing for new installs.

Also drops dead commented-out @noble/curves Metro resolver workaround.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:06:35 +02:00
minibits-cash
58fb7b15b9 New native release prep, translation fixes 2026-06-01 13:21:37 +02:00
minibits-cash
6d4e9d67f7 Add PERF_TRACKING switch to toggle SQLite query timing logs
A single module-level boolean in connection.ts gates the per-query TRACE
timing. When false, the timer and log calls are skipped entirely (zero added
overhead). Defaults to true for on-device profiling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:23:03 +02:00
minibits-cash
b6d3257aeb Add TRACE-level query timing to the op-sqlite connection adapter
Every query funnels through the connection adapter, so instrumenting it once
times the whole DB layer. execute / executeAsync / executeBatch /
executeBatchAsync now log duration (ms), row or statement count, and the SQL
text at TRACE level — useful for profiling the op-sqlite migration on-device.

Only the SQL string (which carries `?` placeholders) is logged, never the
params, which may contain proof secrets. Uses performance.now() when available
(sub-ms resolution) and falls back to Date.now(); SQL is whitespace-collapsed
and truncated to one line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:42:06 +02:00
minibits-cash
d62ac12dfd Fix mintToCacheDTO call for cashu-ts v4 signature
cashu-ts v4 dropped the leading `unit` parameter from
CashuKeyChain.mintToCacheDTO; the signature is now
(mintUrl, allKeysets, allKeys). Both call sites still passed `unit` first,
shifting mintUrl into the allKeysets slot. At runtime mintToCacheDTO does
allKeysets.map(...) on what is actually the mintUrl string, throwing
"TypeError: undefined is not a function" — which surfaced as a failed topup
mint right after getCachedWalletKeys, before mintProofs logged its counter.

Verified against the installed runtime (cashu-ts 4.2.1, cashu-ts.es.js): the
impl takes 3 args and cashu-ts calls it internally as
mintToCacheDTO(this.mintUrl, keysets, keys). Sibling of the earlier
loadMintFromCache v4 signature fix (c61e73a); unrelated to the op-sqlite
migration (the DB layer raises AppError, never a bare TypeError).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 23:42:53 +02:00
minibits-cash
8e8960365d Commit recommended settings - new xcode version 2026-05-30 22:39:01 +02:00