aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/sync.js
Commit message (Collapse)AuthorAgeFilesLines
* Sync: stop resurrecting stale pending txs from other devices' snapshots.Adam Soltys2026-09-101-2/+12
| | | | | | | | | | | | | | | | A 5,000-sat send built on 2026-09-06 that the network never took sat in the history as a pending withdrawal for four days on every device. The local scan prunes such rows (explorer 404 / absent from address history), but the boot-time cross-device history union re-adopted it from a stale device slot's core snapshot, and the device then republished it — a phantom that no explorer knows, immortal across devices. The union now adopts a remote PENDING row only while it is under a day old (it has to prove itself to the local scan); confirmed rows still union as before, and the device's own rows are untouched. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* Sync: oversized domains ride Blossom blobs — plus a coinos-hosted server.Adam Soltys2026-09-091-10/+103
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A wallet with deep Ark coins had a 633KB ark sync domain against the relay's 40KB budget: the publish silently failed forever, the slot stayed stale, and every boot retried. Relays can't carry it (64KB event cap) and neither can NIP-44 (64KB plaintext), so a domain over budget now goes to Blossom (nostr blob storage, BUD-01/02/06/11/12): sealed into an envelope ("CSB1" + xchacha20poly1305 under an HKDF of the wallet's NIP-44 self-key), uploaded to the configured servers, and the kind-30078 event carries only { blob: { sha256, size, servers } }. Readers resolve pointers from their own outbox copy first, else from the pointer's servers plus the configured list, hash-verified. Small domains are unchanged. The outbox uploads before it publishes (upload/delete authorizations are kind-24242 events signed at enqueue time, so delivery after logout still holds no key), keeps a local copy for recovery, and deletes superseded blobs once the replacement is acknowledged. Servers: Settings → Nostr → Sync storage (any Blossom server; a typed list is kept), else the user's BUD-03 list (kind 10063) with the coinos server as mirror, else blossom.coinos.io. blossom/server.js is that default: a Bun Blossom server that stores ONLY these envelopes (magic check → 415 for anything else), serves every blob as an octet-stream attachment with nosniff, requires signed auth for upload/list/delete, and enforces per-blob size, per-key quota (oldest evicted), rate limits, a disk cap and unused-blob expiry — it cannot serve as a file host. tools/sync-blob-test.js spawns it and covers the policy, the outbox flow and a fresh device resolving a pointer. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* Durable sync outbox: fix the fresh-device phantom "Sent" that showed a zero ↵Adam Soltys2026-09-061-22/+45
| | | | | | | | | | | | | | | | | | balance (authored by codex). Root cause of the "signed in and my balance is 0 with a huge Sent I never made": a device's snapshot carrying its change/new coins never reached the relay. The publish was a setTimeout debounce that got canceled on page-hide / logout / feature stop, so coins created by another session were never synced; a fresh device then loaded only the old (now-spent) coins, and reconcile dressed the spent inputs as a definitive "Sent" row and zeroed the balance. No funds ever moved — verified on the ASP that no such payment existed and the change coin was still spendable. Fix (codex): - src/sync-outbox.js (new): a durable outbox. Snapshots are signed synchronously at save time under the current identity (no cross-account mixing) and persisted already-encrypted to localStorage (no keys/plaintext in storage). Delivery retries with backoff and wakes on online/pageshow/visibilitychange/storage, surviving suspension, logout and relay outages. The last copy doubles as a local recovery source if an account cache is cleared. - sync.js: replace the cancel-on-stop debounced publish with the outbox; fetchAllStates also merges the outbox's own (possibly-unpublished) events so a rejected publish is still recoverable locally; forgetAll clears it. - nostr.js: factor out stateEvent() (synchronous signing) and decodeStateEvents(), shared by fetch and the outbox. - ark.js: adoptArkSnapshot() applies a synced snapshot to a LIVE connected manager without restarting payment ceremonies (a snapshot arriving while connected previously stayed invisible until reload); the pre-handshake hold-key merges instead of overwrites; and the reconcile row is reworded from a red "Sent -N" to a neutral "History incomplete / Payment details not synced" (↻) with an explainer, since a spent input proves neither an amount nor a date. - app.js: clearAll also clears the outbox (forgetAll). Tests (codex): sync-outbox-test, sync-browser-test (real Chrome: relay rejects a snapshot, browser closes, reopens and retries; a second browser recovers the balance + history without reload), sync-split-test, plus build and morph smoke — all green. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* On-chain tx history now UNIONS across every device's core snapshot (+ local ↵Adam Soltys2026-09-041-0/+19
| | | | | | | state) at sync time, newest-first per txid, instead of adopting the newest core's list wholesale. The wholesale apply lost history: a device whose history fetch keeps failing (mobile 429 storms — the balance scan is cheap per-address calls, history is the heavy /txs pulls) publishes a THIN core with txs:[] and a fine balance, and every fresh device then adopted the newest=thin core and showed a balance with no transactions — adam's phone today, with three ~2KB thin cores on the relay shadowing the fat 7KB core from Aug 18 that still holds the history. Union is safe because history is append-mostly and every SUCCESSFUL local scan still prunes rows its addresses no longer report; a wiped device also self-heals the fleet, since its republished core carries the union'd txs back to the relay Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Wallets go flat: every Spending and Savings is a top-level wallet in the ↵Adam Soltys2026-08-151-1/+1
| | | | | | | switcher, named for its purpose, renamable apart — and a new wallet can carry its own seed or live at the next BIP84 account of one you already back up Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
* Profiles wear their latest notes, each one zappable; a zap with no ark ↵Adam Soltys2026-08-081-0/+3
| | | | | | address to land on becomes a gift only their key can open Co-Authored-By: Claude Fable 5 <[email protected]>
* Sync just works: NIP-65 relays, no card; gift-locking leftovers goneAdam Soltys2026-08-021-50/+39
| | | | | | | | | | | | | | | The Device sync card and toggle are gone — sync is always on, riding the user's own NIP-65 (kind 10002) write relays: the login npub's first, else the wallet key's, coinos relay as the fallback and always included. A fresh wallet identity with no NIP-65 publishes one (default relay) so other clients can find its events. The npub-locked-gift remnants go with it: the per-wallet nostr address card that pitched gift-locking, the how-it-works sentence, the dead NostrSync.sendDM/sendNostrDM path, and the orphaned claim-code field. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Settings: surface wallet settings, add Nostr section, tuck rarities into ↵Adam Soltys2026-08-021-1/+1
| | | | | | | | | | | | | | | | | | Advanced The main Settings tab now inlines the active wallet's own cards (recovery phrase, name, public key) instead of linking to a sub-page, followed by payment address, Ark status, network, and data source. All nostr cards — address, account link, wallet connect, device sync — sit under one Nostr heading via a new nostrSettingsCards seam. Offline transfer, address rescan, and auto-logout move to an Advanced sub-page. The Ark provider picker is gone: mainnet always rides coinos (a stale stored choice no longer wins); dev networks keep their per-network defaults. The per-account screen (Accounts → ⚙) reuses the same card builders unchanged. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* Split device sync into per-domain slots with bounded, dirty-only publishesAdam Soltys2026-08-011-11/+54
| | | | | | | | | | | | | | | | One monolithic snapshot per device outgrew a vanilla relay's 64KB event cap the moment real payment traffic arrived, and sync silently died. Snapshots now fragment into per-domain replaceable events (core wallet, ark, nwc — an extension claims a domain by naming it), published under a new ':x:' d-tag marker that pre-split clients don't match, so they can never mistake a fragment for a full snapshot. Only domains whose content changed republish, a refused publish retries on the next save instead of being marked done, and readers apply full-snapshot semantics only to core-carrying events. The ark projection also caps its history: spent stubs at 200, done actions at 50 — the event size is bounded for life, no special relay rules required. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add Nostr Wallet Connect (NIP-47) — hal as a wallet serviceAdam Soltys2026-08-011-0/+7
| | | | | | | | | | | | | | | | | | | | | | | | | Other apps can connect over nostr and pay from this wallet without touching its keys, within a per-payment and daily budget the user sets, revocable at any time. hal holds no native Lightning balance, so every operation runs over Ark: pay_invoice takes the swap bridge when configured and falls back to the ASP's lightning-send, make_invoice mints on the ASP. NWC availability therefore tracks Ark availability. - kinds 13194/23194/23195, nip44_v2 and nip04, replying in whichever scheme the request used; every request gets a reply so clients never hang - each connection gets its own service keypair, so revoking one can't affect the others and none is tied to the user's nostr identity - budgets are checked BEFORE spending, not reconciled after - requests are ignored unless signed by that connection's client key, and replayed/stale events (>120s) can't re-trigger a payment - Settings card to create, show (URI + QR) and revoke connections, and it says plainly that this only works while a tab is open tools/nwc-test.js drives it as a real client: both encryption schemes, budget refusal with a genuine over-limit invoice, unsupported-method errors, and a request from an unauthorized key being ignored. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add NIP-57 Lightning zaps (npub / lightning address / lnurl)Adam Soltys2026-07-301-1/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | Send a Lightning zap to a nostr npub, a lightning address (name@domain), or an lnurl. New src/lnurl.js does the resolution half — target → LNURL-pay params → signed kind-9734 zap request → callback → bolt11 — and the new zaps feature (src/features/zaps.js) drives the amount/comment UI, then hands the invoice to the existing Boltz submarine-swap flow via a new swaps.startLnPay hook, so there's one tested pay/review/confirm/success path. Routing: a bolt11 is still caught by swaps; an npub still prefers an instant free Ark zap when Ark can serve it, and only falls through to a Lightning zap otherwise — ark's "no Ark address" screen also offers an explicit "Zap over Lightning" fallback (gated on the zaps feature's canLnZap). A real zap receipt is attributed only when the server advertises allowsNostr and we know the recipient's pubkey; otherwise it degrades to a plain LNURL-pay. Nostr seam gains nostrSign (finalize without publishing), nostrProfile (kind-0 with lud16/lud06), and nostrRelays. Lightning addresses are debounced in matchSendText since their regex matches mid-typing. Verified: unit tests for parse/encode/zap-request; a live resolve→signed 9734→payable mainnet bolt11 against coinos; and an in-browser DOM smoke (zap form + LNURL params over CORS + allowsNostr hint + handoff). Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Gift cross-device sync, snapshot size cap, reliable SW updates, gift-detail ↵Adam Soltys2026-07-061-1/+19
| | | | | | | | | | | | | | | | | | | | | layout - Gifts now sync across devices: gift records + reserved/reclaimed coin sets ride the encrypted snapshot as a merge-safe extension (like ark), and every gift state change (create/reclaim/revoke) triggers a saveCache so it actually publishes. A gift made on one device now shows on the others. - Cap the published snapshot at ~40KB JSON (relay rejects >64KB events, nip44 expands ~1.5x): trim oldest on-chain txs when oversized; another device backfills via rescan. Typical wallets untouched. - Service worker: register with updateViaCache:'none' so a new build is detected immediately (not held by the 4h HTTP cache), and reload once on controllerchange so the fresh app runs without a manual second refresh. - Gift detail: View/Reclaim share a row, Revoke on its own full-width line so 'Revoke now' stops wrapping. Co-Authored-By: Claude Fable 5 <[email protected]>
* Slim ark state for sync so it fits the relay's 64KB event capAdam Soltys2026-07-061-2/+1
| | | | | | | | | | | | | | Cross-device ark sync silently failed: the full snapshot (deep-genesis vtxo bytes + done-action signing hex) was ~153KB and relay.coinos.io rejects events over 64KB, so no publish ever landed and other devices never saw the balance. The synced ark state is now slimmed — spent vtxos drop their signed bytes (kept only for spendable/pending, which a device might spend/exit), done board/offboard/exit actions keep just the fields history labels need (hex stripped), in-flight actions and movements beyond the last 300 are dropped. Local storage keeps the full state; only the published copy is slim. ~150KB -> well under 64KB. Co-Authored-By: Claude Fable 5 <[email protected]>
* tmp: instrument ark connect + sync publish (debug)Adam Soltys2026-07-061-1/+2
|
* Per-device sync slots: devices never share a replaceable eventAdam Soltys2026-07-061-32/+30
| | | | | | | | | | | | | | | | | | | The clobber was structural: all devices published to one replaceable event (same seed -> same nostr key -> one slot), so whoever saved last won and a device saving before it merged wiped the others' coins. Now each device publishes to its OWN d-tag (<base>:d:<deviceId>, deviceId random per browser in localStorage), and readers pull ALL of the identity's kind-30078 events — every device slot plus the legacy shared tag for migration — merging ark vtxos across all of them and applying the newest full snapshot for on-chain state. No device can overwrite another's slot, so anti-entropy is no longer load-bearing (kept as belt-and-suspenders). Live subscription drops its #d filter to see all slots; publish and the echo-guard use the per-device tag. Verified: two same-seed devices produce two distinct relay slots that coexist, B sees A's funds, both converge after a spend. Co-Authored-By: Claude Fable 5 <[email protected]>
* Sync ark funds across devicesAdam Soltys2026-07-061-1/+25
| | | | | | | | | | | | | | | | | | | | | | | | Ark vtxos lived only in per-device storage, so a second device with the same seed saw the right on-chain balance (that rides the snapshot) but none of its ark funds — board/change vtxos exist only where created. Ark state now rides the encrypted nostr snapshot as a merge-safe cache extension: additive union (bring in vtxos a device is missing) with each device's own server reconcile() as the authority for spent-ness, so a stale or wrong 'spent' on one device can never propagate and poison the others. A connect-time reconcile catches coins spent elsewhere while a device was away, and a live subscription to our own replaceable event merges another device's new coins within seconds. New seam: registerCacheExtension({mergeAlways:true}) extensions get fed snapshots that aren't newer than local state too (last-writer-wins is right for rescannable chain data, wrong for merge-style vtxo state), and nostr.subscribeStates for the live path. Verified two same-seed devices over a mini relay: B sees A's boarded balance with zero mailbox activity, spends a synced vtxo, A converges to a stable balance with the spent vtxo correctly marked. Co-Authored-By: Claude Fable 5 <[email protected]>
* NWC bridge funding (PoC): ark wallets behind a lightning facadeAdam Soltys2026-07-061-0/+6
| | | | | | | | | | | | | | | | | | | | The compatibility play for ark zaps: an NWC wallet service (coinos, eventually) receives pay_invoice from any nostr client, asks the hal wallet to cover the amount over ark (kind 23196 funding request), hal auto-pays within a user-set allowance and acks (23197), the bridge receives the vtxo in its mailbox and settles the bolt11 over lightning. Custody window is the seconds the sats are in flight. hal side: a live nostr subscription primitive (nostrSubscribe through the sync seam) and an allowance-gated funding handler that arms when ark connects. PoC config via localStorage 'btc-wallet-arknwc' ({bridgePk, budgetSat}); proper UI when the protocol settles. Proven end-to-end on regtest (tools/.arknwc-e2e.tmp.js): mini relay + bridge process (ArkManager in bun + CLN) + NIP-47 client — invoice on clb paid via cl, preimage returned over NWC, hal debited 1,000 sats over ark, zero page errors. Co-Authored-By: Claude Fable 5 <[email protected]>
* Ark zaps PoC — a NIP-61-shaped zap protocol over arkoorAdam Soltys2026-07-061-0/+16
| | | | | | | | | | | | | | | | | | | | | Lightning zaps are welded to LNURL+bolt11; nutzaps (NIP-61) proved the alternative shape, and ark fits it better than Cashu because delivery already exists (the mailbox). Draft protocol, kinds provisional: kind 10037 (replaceable) announces the wallet's ark address+network; the zap itself is a plain instant arkoor send; kind 9737 is the sender-signed receipt tagging recipient, amount, comment, and the delivered vtxo id — so the receipt is checkable against the actual payment, unlike a bare attestation. UX: paste an npub into Send and hal offers the zap (needs both the ark and sync features; npub decoding is inline bech32 so ark-only builds stay lean). The nostr layer gains generic publishEvent/fetchEvents, exposed through the sync seam as wallet.nostrPublish/nostrFetch. Proven on regtest against a mini NIP-01 relay: announce, resolve, zap 1,000 sats with a comment, instant receipt+delivery, vtxo match. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never apply legacy netName-less nostr snapshotsAdam Soltys2026-07-051-3/+6
| | | | | | | | | A pre-per-network-dtag event can hold ANY network's state; applying one put regtest coins into a mainnet wallet (and its board tx was then rightly rejected by the network). Unstamped snapshots are now skipped — a full scan is safer than a wrong snapshot. Co-Authored-By: Claude Fable 5 <[email protected]>
* Per-network nostr sync + honest balance loading stateAdam Soltys2026-07-041-2/+9
| | | | | | | | | | | | One shared d-tag let a network switch apply the mainnet snapshot to a signet wallet (the flash of wrong balance) and clobber mainnet state on the relays. Each network now syncs its own replaceable event (mainnet keeps the legacy bare tag), snapshots carry netName, and a foreign-net snapshot is never applied. The balance card spins until cache, Nostr, or a scan has populated this network once — 'scanning' alone missed the cache/Nostr window after wallet.load(). Co-Authored-By: Claude Fable 5 <[email protected]>
* Features phase 2b: silent payments + nostr sync leave the core walletAdam Soltys2026-07-041-0/+119
wallet.js drops from 2,374 to ~1,970 lines, holding only BIP84 on-chain logic plus generic feature seams. New seams added this pass: registerBalanceExtra extra confirmed/pending buckets registerInputProvider extra spendable inputs for buildTx registerSendPreparer destinations needing post-selection derivation registerInputSigner non-BIP84 input signing registerHistoryProvider / registerScanHook / registerRealtimeHook / registerLoadHook / registerCacheExtension / registerCacheSavedHook src/features/sp-wallet.js: the whole BIP-352 engine — key derivation, worker-accelerated indexer scanning, the live indexer WebSocket, SP outputs as spendable inputs, sp1 send preparation, and per-output-key signing — installs via those seams (installSpWallet). src/features/sync.js: nostr identity + encrypted cross-device state sync plus its Settings card, installed via load/cache-saved/realtime hooks. profileChip/nostrProfile move here-adjacent (into gifts, the only other nostr consumer), so core app.js no longer imports nostr at all. buildTx/sign are now feature-agnostic (input providers + preparers + signers); _recomputeBalance* use the balance-extra registry, so a core build with no SP no longer calls a missing spBalance() (that regression dropped the minimal build into offline mode — caught by the smoke test). New coverage: tools/sp-test.js drives two browser "devices" through an SP send, indexer discovery, and an SP-input spend on the ark regtest chain (needs tools/sp-indexer.js pointed at bitcoind:18543). Minimal smoke now also asserts no SP/nostr code ships. Minimal profile: 314KB, down from 412KB (full: 580KB). All suites green: minimal smoke, sp-test, ark UI, gift lifecycle. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy