From 1043ca529e859b41f27d80c51b80ac897773a5df Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 6 Jul 2026 06:19:06 -0700 Subject: Sync ark funds across devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/nostr.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'src/nostr.js') diff --git a/src/nostr.js b/src/nostr.js index 431bb1e..91005ec 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -186,6 +186,20 @@ export class NostrSync { } catch { return () => {}; } } + // Live subscription to our own (replaceable) state event, decrypted. + // Another device saving state delivers here within seconds. + subscribeStates(onState) { + if (!this.pk) return () => {}; + return this.subscribeEvents( + { kinds: [30078], authors: [this.pk], '#d': [this.dtag] }, + (ev) => { + try { + const v = JSON.parse(nip44.decrypt(ev.content, this.ck)); + if (v) onState(v); + } catch {} + }); + } + // Fetch the newest decrypted state across relays, or null. async fetch() { if (!this.sk) return null; -- cgit v1.2.3