Claude/scan screen from to edit x36qp0 - #19
Open
ponthief wants to merge 111 commits into
Open
Conversation
Contacts were deduped only by SP address, so a user could save two contacts with the same name pointing at different addresses. Require the label to be unique per user+network (case-insensitive): reject a create or rename whose name is already used by a different contact, returning 400 with a clear message. The same address renaming itself is still fine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…36qp0 Enforce unique contact names within a user's address book
Add an optional `sp_address` to wallet creation. When the client derives the seed and keys on-device (see the Thrilla spKeys module), it sends only the public sp_address and the server stores it as-is — the server never sees the mnemonic or the private keys. The prefix is validated against the network. The legacy path (server generates, or imports+derives from a supplied mnemonic) is unchanged for older clients and the web app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…36qp0 Wallet create: accept client-derived sp_address (server never sees the seed)
The module imported Python's `random` (Mersenne Twister) but never used it — all EC math goes through coincurve/ecdsa. Dead imports of a non-crypto RNG in a key-handling file are a footgun; drop it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…36qp0 curve.py: remove unused non-crypto random import
BIP-352 reserves label m=0 for change; siLNt produced change at m=1 and
treated the unlabeled base address as "m=0". That's self-consistent within
siLNt (it scans m=1) but breaks the spec's cross-wallet guarantee: any other
BIP-352 wallet restoring the same seed scans m=0 for change and would miss
siLNt's change outputs.
Produce new change at the standard m=0, and keep scanning m=1 as a legacy
change index so existing wallets' historical change stays detectable (all
wallets are currently on Signet, so no mainnet migration is involved).
- scan.py: change label index 1 → 0; add BIP352_LEGACY_CHANGE_LABEL_INDICES
= [1]; scan set = {0 (change), 1 (legacy), 2, 3 (user labels)}.
- wallet.py: generate_labeled_sp_address now accepts m=0 (a real label tweak,
the change label — never handed out); change scriptPubKey derived at m=0.
- dust_check.py: classify both m=0 and legacy m=1 change as non-dust.
- views_api.py / crud.py: reserve both m=0 and m=1 from user label assignment
on both address-save paths; fix stale docs.
Verified the m=0 label tweak is byte-identical on the produce and scan sides,
so change created at m=0 is detected at m=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…36qp0 BIP-352: use m=0 for the change label (was m=1)
The per-transaction receiver scan loop was `while True`, so a maliciously crafted transaction packing many P2TR outputs to one scan key forced O(N^2) work (each match rescans the remaining outputs) — minutes of CPU per block. BIP-352 v1.1.0 caps a recipient group at K_max = 2323 outputs (the most that fit in a 100,000-vByte standard tx); stop scanning there to bound the work to O(N*K_max). Honest transactions never reach the cap, so conforming receivers are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
On a rescan, insert_utxos_for_wallet upserts (ON CONFLICT DO UPDATE) and preserves an existing spent state, so re-detecting an already-known UTXO changes nothing visible — no new coin, balance unchanged. But the scan counted every detected output (total_found += len(result)), so the UI's Progress stat, completion message, and toast reported e.g. "1 UTXO found" after a large rescan that actually found nothing new. insert_utxos_for_wallet now returns the number of rows that were genuinely newly inserted (existence pre-check), and the scan loop counts only those. Re-detected/updated rows no longer inflate the found count. Fixes the false "found" count on both the web and mobile clients (both read this counter). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…x36qp0 Scanning: BIP-352 K_max cap + fix false "found" count on rescans
Lets a user opt a wallet into server-side background scanning so a returning
user isn't faced with a huge catch-up. This is the BIP-352 "Remote Scanner"
model: the client uploads ONLY the wallet's scan key (a detection capability
— it can find payments but never spend), encrypted at rest. The spend key is
never uploaded; the background scanner derives the spend PUBLIC key from the
wallet's sp_address.
- migrations: m025 background_scan table (encrypted scan key; row present =
opted in; disabling deletes the key).
- crud: enable/disable/status/get-secret/list helpers; deleting a wallet also
drops its stored key.
- scan.py: scan_wallet can take a spend PUBLIC key directly (no spend secret
needed); scan_block's unused spend_secret param removed.
- views_api: GET/PUT/DELETE /wallet/{id}/background-scan (PUT validates the
uploaded key's pubkey matches the wallet's B_scan); run_background_scans()
sweeps opted-in wallets to the tip, skipping caught-up or mid-scan wallets.
- __init__: run the sweep on a 30-min timer alongside the other ext tasks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…x36qp0 Opt-in server-side background scanning (Remote Scanner)
When the opt-in background scan discovers new UTXOs for a wallet, push a "Payment received" notification to the owner's registered devices. - migrations: m026 fcm_tokens (device token -> user). - crud: register/remove/list FCM tokens. - helpers/fcm.py: FCM HTTP v1 sender (service-account OAuth via google-auth), prunes tokens FCM reports as unregistered. No-op unless SILNT_FCM_CREDENTIALS points at a service-account JSON, so the app is unaffected when push isn't set. - views_api: POST/DELETE /api/v1/fcm/token; run_background_scans fires a push (best-effort) when scan_wallet reports newly-found UTXOs. - pyproject: add google-auth. Client registers its token after login (separate Thrilla change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
A 32-byte scan key is 64 hex chars — exactly AES-block-aligned — so
AESCipher can leave a trailing padding block on the decrypted string,
making bytes.fromhex() fail ("non-hexadecimal number ... at position 64")
and every background scan for that wallet error out (the wallet then falls
behind). Extract the leading 64-hex on read, which also repairs already-
stored keys without the user re-enabling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…x36qp0 Push notifications for background-scan payments + fix stored scan-key parse
AESCipher can leave a residual padding block on the decrypted string when the plaintext was exactly block-aligned (a multiple of 16 bytes), which is what broke background-scan key parsing. Fix it once at the decrypt boundary instead of per-caller: strip a trailing run of N identical control bytes (0x01–0x10) equal to N — that's leftover padding, never data, since every value we encrypt is printable text. A cleanly-decrypted value never matches, so it's a no-op in the normal case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…x36qp0 Harden _pj_decrypt: strip residual PKCS7 padding centrally
…ayment alerts (#14) * Add FCM test-push endpoint for push diagnostics Add POST /api/v1/fcm/test which sends a diagnostic notification to the caller's registered devices and returns a structured report (credentials present, tokens registered, per-token send result). Lets a user verify the whole push pipeline without waiting for a real payment and the 30-min background sweep. Backed by a new send_fcm_report() helper mirroring send_fcm but returning counts and human-readable failure reasons; invalid tokens are still pruned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * FCM: report the specific credentials failure, resolve path at call-time The test endpoint previously collapsed three distinct failures into one misleading 'no credentials' line. Split them: env var unset, path set but file missing/unreadable, google-auth not installed, or an unparseable service-account file — each now returns its own explanation. Also resolve SILNT_FCM_CREDENTIALS at call-time (with an LNbits-settings fallback) instead of caching it at import, so a restart reliably picks up a freshly-added .env value regardless of import order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * FCM: resolve credentials path from LNbits .env file directly The test surfaced that some deployments read .env into pydantic settings without exporting it to os.environ, so os.environ.get() returned empty even though the var was in .env. Add a deployment-agnostic fallback that parses LNbits' .env directly (LNBITS_ENV_FILE, the startup dir, and the data-folder location), after checking the process env and settings object. The 'not found' diagnostic now lists every .env path it checked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * FCM: mint OAuth2 tokens with cryptography, drop google-auth dependency LNbits does not auto-install extension pyproject deps, so requiring google-auth meant a manual venv install. Replace it with a small service-account client that signs the JWT-bearer assertion using cryptography (already an LNbits dependency) and exchanges it at Google's token endpoint — same FCM HTTP v1 result, zero extra installs. Tokens are cached until ~60s before expiry. The load path now validates the file is a real service-account key and gives a clear error if google-services.json was supplied by mistake. Removed google-auth from pyproject dependencies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Route all SILNT_* env reads through a shared .env-aware resolver Some LNbits deployments load .env into pydantic settings without exporting it into os.environ, so os.environ.get() misses vars that are present in .env (this was why FCM credentials weren't seen). Extract the .env-fallback logic into helpers/appenv.silnt_env() and use it for SILNT_BITMAIL_DOMAIN and SILNT_DEVICE_COOKIE_DOMAIN as well, so every custom var resolves the same way: process env → LNbits .env file → default. FCM now shares this helper instead of its own private copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Notify on interactive-scan finds, not only the background sweep Opening the app triggers an interactive catch-up scan that almost always beats the 30-min background sweep to a new payment (and then advances last_scan_height so the sweep skips as already-caught-up). With the push wired only into the sweep, an app-open find never notified. Fire _notify_payment_found whenever any scan inserts new UTXOs. utxos_found counts only newly-inserted rows, so rescans of known blocks don't notify. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Notify with amount received instead of wallet balance The payment notification showed the total balance, which is noise. Report the amount just received instead: insert_utxos_for_wallet now returns the summed sats of newly-inserted rows, scan_wallet threads it out as amount_found (and into scan progress), and _notify_payment_found renders 'Received N sats in <wallet>'. Falls back to a count if the amount is unavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Make payment push generic to keep amount off Google/FCM FCM notification messages transit Google in plaintext, so putting the amount (and wallet title) in the push leaked payment metadata to Google. Send a generic 'You've received a new payment' with minimal data ({type: payment}), no amount/name/count. The exact amount is still shown in-app, composed locally from scan data and never sent via FCM. The server already sees amounts by design (background scanning); this specifically keeps them off Google's servers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq --------- Co-authored-by: Claude <noreply@anthropic.com>
ponthief
force-pushed
the
claude/scan-screen-from-to-edit-x36qp0
branch
from
August 9, 2026 05:52
6dd2799 to
355a76f
Compare
…#15) Poll each active network's chain tip every 60s and run the sweep the moment the tip advances, so a received payment is detected and pushed within ~a block rather than up to 30 minutes later. The tip poll is cheap — one request per network with opted-in wallets (new list_background_scan_networks query), not one per wallet. The 30-min interval is kept as a safety-net floor (newly opted-in wallets, missed tip updates, server restarts). Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq Co-authored-by: Claude <noreply@anthropic.com>
ponthief
force-pushed
the
claude/scan-screen-from-to-edit-x36qp0
branch
from
August 9, 2026 06:34
459805d to
6df321f
Compare
…s still scanned (#16) * Document + enforce the labeled-address cap ↔ scanned-index coupling MAX_ADDRESSES_PER_WALLET (2) and BIP352_LABELED_ADDRESS_INDICES ([2,3]) are implicitly coupled: the server assigns labeled indices lowest-free from 2 and the cap bounds the highest to cap+1, and every such index must be in the always-scanned set so a *deleted* labeled address stays detectable. Add cross-referencing comments in both spots and a fail-fast import-time assert so raising the cap without widening the scanned set (which would silently reintroduce the delete-then-miss-payment footgun) can't slip through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Clamp labeled-address index server-side to the always-scanned range Close the client-supplied-index escape from the cap/scanned-index coupling: a client could POST an explicit label_index outside BIP352_LABELED_ADDRESS_INDICES (e.g. m=50), persisting a labeled address that would silently stop being detected if later deleted. - preview: honor an explicit index only if it's a real, free labeled slot; otherwise fall back to the next free in-range slot (server regenerates the matching address). - save: reject an out-of-range/reserved explicit index (save has no keys to regenerate, so the client's sp_address is bound to its index and can't be safely remapped); None or a taken slot still auto-picks the next free in-range slot, which the per-wallet cap guarantees exists. Normal preview→save flow (server-assigned m=2/m=3) is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq --------- Co-authored-by: Claude <noreply@anthropic.com>
ponthief
force-pushed
the
claude/scan-screen-from-to-edit-x36qp0
branch
from
August 9, 2026 09:19
b2399ca to
8aee1e5
Compare
The verify and reset links were built from the request's Origin/Referer, falling back to the Host header. A browser sends Origin, so web links were fine; the mobile app's fetch sends no Origin, so the link fell back to the API host — which doesn't serve the web app's SPA /verify (and /reset) routes — and 404'd. Add appenv.frontend_base_url(): prefer a configured SILNT_FRONTEND_URL (canonical web-app base, resolved via the .env-aware silnt_env), falling back to the request origin/host only when unset. Use it for both the registration verify link and the password-reset link. Deployment: set SILNT_FRONTEND_URL=https://<thrilla-web-app> so mobile links resolve. Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq Co-authored-by: Claude <noreply@anthropic.com>
ponthief
force-pushed
the
claude/scan-screen-from-to-edit-x36qp0
branch
from
August 9, 2026 15:10
9ee095d to
8ce17c9
Compare
#18) DELETE /api/v1/background-scan/all deletes every background_scan row for the authenticated user's wallets in one call. Backs the mobile duress action, so a coerced unlock also removes the uploaded scan key from the server (not just the local spend/scan keys). Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq Co-authored-by: Claude <noreply@anthropic.com>
ponthief
force-pushed
the
claude/scan-screen-from-to-edit-x36qp0
branch
from
August 9, 2026 19:43
309a025 to
21b8815
Compare
…an path (#19) * Prototype: coincurve-backed EC ops for curve.py (constant-time, faster) curve.py implements secp256k1 in pure Python — point_add/point_mul with a Fermat modular inverse — and wallet.py runs it on secret scalars (scan/spend keys, ECDH) in the scan path. That is non-constant-time (secret-dependent timing) and ~100x slower than libsecp256k1. Add curve_native.py: drop-in point_add/point_mul/pubkey_point_gen_from_int backed by coincurve (already a dependency, wraps libsecp256k1 — constant-time, audited), same (x, y) Point interface, reducing scalars mod n so outputs match byte-for-byte for every practical input. Not wired in yet. _curve_equivalence_check.py fuzzes curve_native against the pure-Python curve across random vectors and asserts identical output (a mismatch would mean changed addresses/keys → unrecoverable wallets), then benchmarks the speedup. Flip wallet.py's EC imports to curve_native only after it reports OK on a host with coincurve installed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Make curve equivalence check fast + show progress The check looked hung: it generated every random point with the pure-Python point_mul (256 modular inversions each), thousands of times, with no output until the end — minutes of silence. Generate points via the fast coincurve path (its correctness is asserted separately by the pubkey_gen check), invoke the slow reference only ~60 times, and print per-phase progress so it's visibly alive. Runs in seconds now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq * Use coincurve EC ops in wallet.py (constant-time, faster) Equivalence check passed (all vectors match), so switch wallet.py's point_add / point_mul / pubkey_point_gen_from_int from the pure-Python curve.py to curve_native (libsecp256k1). This removes the secret-dependent timing on the scan/spend keys in the scan path and speeds up scanning substantially. Derived addresses/keys are unchanged (byte-for-byte identical outputs). curve.py is kept as-is: it still provides the non-EC helpers (bech32, serP, etc.) and serves as the reference for _curve_equivalence_check.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq --------- Co-authored-by: Claude <noreply@anthropic.com>
ponthief
force-pushed
the
claude/scan-screen-from-to-edit-x36qp0
branch
from
August 10, 2026 07:18
54b0996 to
83dabd3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.