diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 22:03:50 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 22:03:50 -0700 |
| commit | 5a16f87c3ba44c9c5231b37cf626db06b63ce351 (patch) | |
| tree | 80330f929ce5108c388cc5dbbea4c0b9cddd0cb1 /src/features/sync.js | |
| parent | c68a29852ee90d5955e84371ad28d8258a5c8b47 (diff) | |
| download | coinosv3-5a16f87c3ba44c9c5231b37cf626db06b63ce351.tar.gz coinosv3-5a16f87c3ba44c9c5231b37cf626db06b63ce351.zip | |
Sync just works: NIP-65 relays, no card; gift-locking leftovers gone
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
Diffstat (limited to 'src/features/sync.js')
| -rw-r--r-- | src/features/sync.js | 89 |
1 files changed, 39 insertions, 50 deletions
diff --git a/src/features/sync.js b/src/features/sync.js index 96616c9..481dda9 100644 --- a/src/features/sync.js +++ b/src/features/sync.js @@ -6,7 +6,8 @@ import { NostrSync, getSyncConfig, setSyncConfig, npubOf, syncDtag, deviceDtag, domainDtag, isOurDtag, isCoreDtag, isOwnDeviceDtag, - fetchNostrProfile, PROFILE_RELAYS, + fetchNostrProfile, PROFILE_RELAYS, DEFAULT_SYNC_RELAYS, queryOn, publishOn, + finalizeEvent, } from '../nostr.js'; import { t } from '../i18n.js'; @@ -62,9 +63,6 @@ export function installSyncWallet(wallet) { nostrNpub() { const pk = this.nostrPubkey(); return pk ? npubOf(pk) : null; }, // Send a nostr DM (e.g. a locked gift's claim code) to a recipient pubkey. - async sendNostrDM(recipientPkHex, text) { - return this.nostr && this.nostr.sk ? this.nostr.sendDM(recipientPkHex, text) : false; - }, // Generic event publish/fetch on the configured sync relays — the seam // other features (ark zaps) use to speak their own kinds. No-ops when @@ -154,7 +152,44 @@ export function installSyncWallet(wallet) { if (wallet.mnemonic) wallet.nostr.load(wallet.mnemonic, wallet.passphrase); else wallet.nostr.unload(); wallet._syncPubSeen = {}; // a new identity has published nothing yet + adoptSyncRelays().catch(() => {}); }); + + // Sync rides the user's own relays: their NIP-65 (kind 10002) write set — + // the login npub's first, else the wallet key's — falling back to the + // coinos relay. A wallet key with no NIP-65 gets one published (default + // relay, signed silently) so other clients can find this identity's events. + async function adoptSyncRelays() { + if (wallet.offline || !wallet.nostr.pk) return; + const pks = []; + try { + const login = wallet.loadFeatureState('nostrlogin', {}); + if (login && /^[0-9a-f]{64}$/.test(login.pubkey || '')) pks.push(login.pubkey); + } catch {} + pks.push(wallet.nostr.pk); + const evs = await queryOn([...new Set([...PROFILE_RELAYS, ...DEFAULT_SYNC_RELAYS])], { kinds: [10002], authors: pks }, 3500); + const newestFor = (pk) => evs.filter((e) => e.pubkey === pk).sort((a, b) => b.created_at - a.created_at)[0]; + const writeRelays = (e) => e.tags + .filter((x) => x[0] === 'r' && x[1] && (!x[2] || x[2] === 'write')) + .map((x) => x[1].trim()).filter((r) => /^wss?:\/\//.test(r)).slice(0, 4); + for (const pk of pks) { + const e = newestFor(pk); + const relays = e && writeRelays(e); + if (relays && relays.length) { + setSyncConfig({ enabled: true, relays: [...new Set([...relays, ...DEFAULT_SYNC_RELAYS])].slice(0, 5) }); + break; + } + } + // a brand-new wallet identity: announce its relays (NIP-65) + if (wallet.nostr.sk && !newestFor(wallet.nostr.pk)) { + const evt = finalizeEvent({ + kind: 10002, content: '', + tags: DEFAULT_SYNC_RELAYS.map((r) => ['r', r]), + created_at: Math.floor(Date.now() / 1000), + }, wallet.nostr.sk); + publishOn([...new Set([...PROFILE_RELAYS, ...DEFAULT_SYNC_RELAYS])], evt); + } + } // Live cross-device state: subscribe to ALL our devices' slots so a save on // another device merges here within seconds. Merge-safe extension state (ark // vtxos union) applies live; full snapshots stay a load-time affair. With @@ -207,53 +242,7 @@ export function syncFeature(ctx) { const { h, ui, render, wallet } = ctx; installSyncWallet(wallet); - // Cross-device sync settings: toggle + editable relay list (default coinos). - function syncCard() { - const cfg = getSyncConfig(); - const setEnabled = (enabled) => { - if (enabled === cfg.enabled) return; - setSyncConfig({ enabled, relays: cfg.relays }); - render(); - // On enable, pull anything newer from the relays, then push our copy up. - if (enabled && !wallet.offline) { - wallet.syncFromNostr().catch(() => {}).finally(() => wallet.saveCache()); - } - }; - return h( - 'div', - { class: 'card col' }, - h('h3', {}, t('deviceSync')), - h('p', { class: 'small muted', style: 'margin:0' }, t('deviceSyncDesc')), - h('div', { class: 'row between' }, - h('span', { class: 'lab', style: 'margin:0' }, t('syncAcross')), - h('div', { class: 'seg' }, - h('button', { type: 'button', class: cfg.enabled ? 'active' : '', onClick: () => setEnabled(true) }, t('syncOn')), - h('button', { type: 'button', class: !cfg.enabled ? 'active' : '', onClick: () => setEnabled(false) }, t('syncOff')) - ) - ), - cfg.enabled - ? h('label', { class: 'field' }, - h('span', { class: 'lab' }, t('relaysLabel')), - h('textarea', { - placeholder: 'wss://relay.example.com', - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', - style: 'min-height:64px', - value: cfg.relays.join('\n'), - onInput: (e) => { - const relays = e.target.value.split('\n').map((s) => s.trim()).filter(Boolean); - setSyncConfig({ enabled: true, relays }); - }, - }), - h('div', { class: 'small faint' }, t('relaysHint')) - ) - : null - ); - } - return { id: 'sync', - nostrSettingsCards() { - return wallet.watchOnly || !wallet.mnemonic ? [] : [syncCard()]; - }, }; } |
