Skip to content

Claude/scan screen from to edit x36qp0 - #19

Open
ponthief wants to merge 108 commits into
lnbits:mainfrom
ponthief:claude/scan-screen-from-to-edit-x36qp0
Open

Claude/scan screen from to edit x36qp0#19
ponthief wants to merge 108 commits into
lnbits:mainfrom
ponthief:claude/scan-screen-from-to-edit-x36qp0

Conversation

@ponthief

Copy link
Copy Markdown

No description provided.

ponthief and others added 25 commits July 26, 2026 11:56
…e-sqfxw0

Add GET /bip353/available for live username availability checks
The WalletAccount model defaulted last_scan_height to 1 while the DB
column (migrations.py) defaults to 0. Since create_silnt_wallet inserts
the model, freshly-created wallets persisted 1, so the app reported
"Scanned to: block 1" for a wallet that had never been scanned. Default
to 0 (never scanned) to match the schema and the resume logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
Both routes defaulted an absent `network` to "signet", so a mainnet
client that omitted it was served signet's chain tip / min_scan_height /
dust threshold. Reject a missing or empty network with 400 instead, so
the mismatch fails loud rather than returning wrong-network data. Admin
and fee endpoints keep their existing defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…36qp0

Fix network scoping for chain tip/config and last_scan_height default
sp_contacts was keyed only by user_id, so a user with wallets on more
than one network (e.g. a mainnet build reusing a signet test account)
saw every network's contacts — mainnet listed signet recipients.

Add a `network` column (migration m024, existing rows backfilled to
signet since they predate mainnet), filter list/dedup/touch by it, and
require an explicit `network` on GET/POST /contacts (400 when missing),
matching the oracle/tip + config contract. The send path passes the
wallet's network when bumping last_used_at.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BizgTtdeH233srEaDHytBq
…36qp0

Scope the SP contacts address book per network
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
ponthief force-pushed the claude/scan-screen-from-to-edit-x36qp0 branch from 6dd2799 to 355a76f Compare August 9, 2026 05:52
…#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
ponthief force-pushed the claude/scan-screen-from-to-edit-x36qp0 branch from 459805d to 6df321f Compare August 9, 2026 06:34
…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
ponthief force-pushed the claude/scan-screen-from-to-edit-x36qp0 branch from b2399ca to 8aee1e5 Compare August 9, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants