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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Relocate per-transaction in-flight request data (params for NUT-19
idempotent retry) from the MST MintProofsCounter to a dedicated SQLite
table, so retries work with no MST loaded — completing the off-MST set
needed for background NWC.
- schema/migration v29: inflight_requests (txId PK, mintUrl, keysetId,
request JSON); added to cleanAll.
- inFlightRepo: add (INSERT OR REPLACE = set semantics) / get /
getInFlightRequestsByMint / remove / seed (ON CONFLICT DO NOTHING).
- WalletStore: write/remove via Database (receive/send/mint paths).
- inFlightOperations: enumerate via a flat Database.getInFlightRequestsByMint
query instead of the mint.proofsCountersWithInFlightRequests nested loop;
removeInFlightRequest via Database; queue guard uses the DB count.
- Mint model: remove inFlightRequests map, InFlightRequestModel, all the
in-flight actions/views (counter + mint level). The InFlightRequest TYPE
is kept (WalletStore option signatures). MintProofsCounter is now just
{keyset, unit, counter}. migrateSnapshot strips inFlightRequests AND
meltCounterValues from old snapshots.
- one-time seed (rootStoreModelVersion 34->35): _runMigrations reads the
raw pre-upgrade snapshot for any in-flight requests; idempotent.
- tests: __tests__/inFlightRequests.test.ts.
Full suite green (15 suites / 163 tests). With M1+M2, the MintProofsCounter
sub-model now carries only the counter (itself SQLite-authoritative) — a
candidate to collapse later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate per-transaction melt recovery data (the serialized meltPreview)
from the MST MintProofsCounter (debounced MMKV) to a dedicated SQLite
table, so it can be written synchronously and read with no MST loaded.
Why: meltPreview is recovery-critical — it unblinds the change of a paid-
but-unconfirmed melt. It was persisted only via the batched whole-tree
MMKV snapshot, so a crash right after the payment was submitted could lose
it and the change ecash. It's also a prerequisite for off-MST background
melt (NWC pay_invoice).
- schema/migration v28: new melt_recovery table (txId PK, mintUrl,
keysetId, meltPreview JSON); added to cleanAll.
- meltRecoveryRepo: add (ON CONFLICT DO NOTHING — first preview wins,
matching the old "already tracked" guard) / get / remove / seed.
- WalletStore: write the preview synchronously via Database.addMeltRecovery
BEFORE completeMelt; remove on terminal success/failure.
- meltOperations / transferOperationApi: read/remove via Database instead
of the counter model; drop the now-needless counter fetch in those blocks.
- Mint model: remove meltCounterValues map, MeltCounterValueModel, the melt
actions/views, the dead counterAtMelt field, and serializeMeltPreview
(moved to cashuUtils). migrateSnapshot now STRIPS meltCounterValues from
old snapshots so applySnapshot tolerates the removed field.
- one-time seed (rootStoreModelVersion 33->34): _runMigrations reads the
RAW pre-upgrade snapshot (the model no longer holds it) and copies any
in-flight meltPreview into SQLite. Idempotent.
- tests: __tests__/meltRecovery.test.ts (JSON round-trip, first-wins,
remove, isolation, idempotent seed).
Full suite green (14 suites / 157 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>