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>
- 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>
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>
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>
Tier 3 correctness hardening (two low-risk, high-value items; the reservation
db.transaction() rewrite remains a separate follow-up).
Migrations now fail loudly. runMigrations previously swallowed any batch error
with a low-severity log, so a failed migration left the app running on a schema
that didn't match the code — the silent-corruption class this whole effort is
about. It now throws (the batch is atomic, so a failure rolls back cleanly,
including the version bump, and the next launch retries from the same point).
This is consistent with instance.ts, where a failed CREATE TABLE already aborts
startup.
To make that safe, the v19 `DROP TABLE usersettings` becomes `DROP TABLE IF
EXISTS usersettings`. On devices that never had that table the DROP used to
error (swallowed), which also rolled back the version bump and left the
migration permanently stuck; making it defensive lets those devices migrate
forward cleanly. This is a behavior change for that specific upgrade cohort.
updateStatusesAsync no longer interpolates transaction ids into the SQL
(transactionIds.join(',')); the ids are bound as ? placeholders in both the
SELECT and UPDATE, matching the parameterized style used elsewhere. Empty input
now returns early instead of producing invalid `IN ()` SQL.
Tests: 125/125 pass, no regressions. (The migration runner itself has no unit
test because migrations.ts -> logService pulls Sentry/RN/store; covering it
would need a logger-injection refactor.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure reorganization behind the existing Database facade — no behavior change.
The 1646-line services/sqlite.ts is broken into services/db/:
errors.ts dbError() helper
connection.ts op-sqlite adapter (from previous commit)
schema.ts table column definitions (single source of truth)
migrations.ts _dbVersion, getDatabaseVersion, ordered migration registry
mappers.ts transaction row normalizers
instance.ts _db singleton, getInstance, schema bootstrap, cleanAll
transactionsRepo.ts / proofsRepo.ts / reservationsRepo.ts
index.ts assembles the Database facade, re-exports public types
services/sqlite.ts is now a thin `export * from './db'` barrel so every
existing import path (services, services/sqlite, ../../sqlite) resolves
unchanged. The Database.* contract and the exported types
(TransactionSearchFilters, LockedProofSnapshot, ReservationRow,
ReservationTransactionUpdate) are preserved.
Notable refactors folded in:
- schema single source of truth: proofs/reservations columns are defined
once; first-run CREATE and the v25 rebuild / v26 add are generated from
them so they cannot drift. PROOFS_COLUMN_NAMES (drives the v25 copy) is
tested to stay in sync with the table.
- migration if-chain replaced by an ordered MIGRATIONS registry; adding a
migration is now a one-line append.
- updateStatusesAsync and expireAllAfterRecovery now use getInstance()
instead of reaching for the module-global _db directly (the only two
functions that did; behavior-equivalent, more robust before init).
New: __tests__/dbSchema.test.ts validates the generated DDL against node:sqlite.
Tests: 125/125 pass (was 121 + 4 new), no regressions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces react-native-quick-sqlite (8.2.7) with @op-engineering/op-sqlite
(16.2.0). The motivating bug: quick-sqlite's JSI binding silently dropped
non-bindable JS objects (e.g. a cashu-ts Amount instance), shifting the
parameter array and corrupting proof rows with no exception.
A new connection adapter (services/db/connection.ts) is the single seam to
the native library and reconciles op-sqlite's API differences so sqlite.ts
is nearly untouched:
- op-sqlite execute() is async; sync is executeSync() -> our execute maps
to executeSync, executeAsync maps to execute
- executeBatch() is async-only with no insertId -> synchronous atomic batch
emulated with BEGIN/COMMIT/ROLLBACK over executeSync
- rows is a plain array -> re-wrapped into the WebSQL { _array, length,
item() } shape the existing query code expects
The adapter also centralizes parameter sanitization (sanitizeParams): every
bind is coerced deliberately (Date -> ISO, numeric-like objects -> Number) or
rejected loudly, so the silent-skip footgun is structurally impossible.
Also collapses 33 repetitive try/catch AppError blocks into a dbError()
helper, which uniformly passes deliberate AppErrors (e.g. NOTFOUND_ERROR)
through instead of flattening them to DATABASE_ERROR (-109 net lines).
Tests: 121/121 pass, no regressions. Note: native rebuild and on-device
verification of the minibits.db file location are still required before ship.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>