Commit Graph
507 Commits
Author SHA1 Message Date
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
minibits-cash 082d9ee18e NFC cold and hot start support 2026-06-01 16:33:53 +02:00
minibits-cashandClaude Opus 4.8 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-cashandClaude Opus 4.8 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-cashandClaude Opus 4.8 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-cashandClaude Opus 4.8 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-cashandClaude Opus 4.8 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-cashandClaude Opus 4.8 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-cashandClaude Opus 4.8 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
minibits-cash 16700a439f NFC discovery fix 2026-05-30 22:34:06 +02:00
minibits-cashandClaude Opus 4.8 7bf7685ec6 Fix jest test suite: scope testMatch, mock quick-crypto, drop dead tests
- 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>
2026-05-30 22:31:36 +02:00
minibits-cashandClaude Opus 4.8 90b9c8043f Open the db at quick-sqlite's legacy path so existing data is found
op-sqlite's default database directory differs from react-native-quick-sqlite's
on BOTH platforms, so a naive swap would open an empty db and orphan every
existing wallet:

  platform | quick-sqlite (old)        | op-sqlite default
  iOS      | NSDocumentDirectory       | NSLibraryDirectory
  Android  | filesDir (getFilesDir)    | databases/ (getDatabasePath)

The connection adapter now passes an absolute `location` matching the old
directory — IOS_DOCUMENT_PATH on iOS, ANDROID_FILES_PATH (= context.filesDir)
on Android. op-sqlite fully overrides its base path when location starts with
'/' (verified in cpp/OPSqlite.cpp open proxy), so the file resolves to exactly
<Documents|files>/minibits.db — the same file quick-sqlite created. No copy or
data migration needed; the -wal/-shm sidecars in the same dir are picked up too.
A caller-supplied location still wins over this default.

Must still be confirmed on a device: upgrade an install with existing data and
verify the balance/history persist (db.getDbPath() prints the resolved path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 22:19:42 +02:00
minibits-cashandClaude Opus 4.8 245ffd943e Split getDatabaseVersion into a pure read and an explicit seed
getDatabaseVersion previously read the version and, on a fresh database,
also inserted the seed row as a side effect. Split into:
  - readDatabaseVersion(db): pure read, returns null when unseeded
  - seedDatabaseVersion(db): explicit write
  - getDatabaseVersion(db): pure, reports _dbVersion when unseeded (kept on the
    facade for display callers)

First-run seeding now happens explicitly in _createOrUpdateSchema. Behavior is
unchanged: fresh installs seed at _dbVersion and skip migrations; existing
installs read their stored version and migrate. A "get" no longer mutates.

Tests: 125/125 pass, no regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 22:00:09 +02:00