From eef3193729ccea2c3d712cda9381c24325fac260 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 26 Jun 2026 09:03:49 -0700 Subject: Fold the coinos opt-out into a single Data source picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Esplora|Electrum backend toggle plus the separate 'Instant notifications' on/off with one 'Data source' selector: • coinos (instant) — default; explorer data + coinos push • Block explorer — REST + polling, nothing sent to coinos (the opt-out) • Electrum server — one WS server for data + push (your node / public) Picking a non-coinos source IS the privacy opt-out, so the standalone toggle is gone. A single 'mode' setting drives getBackend()/getRealtimeEnabled(), so the wallet/wsUrl logic is unchanged; existing per-feature settings migrate in (realtime=off → explorer, backend=electrum → electrum). coinos⇄explorer just flips the watcher (no rescan); to/from electrum rebuilds the backend. Sets up cleanly for a future coinos Fulcrum: 'coinos' mode upgrades from REST+watcher to a full Electrum backend transparently. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 41 ++++++++++++++++++++++------------------ src/app.js | 62 +++++++++++++++++++++++++++---------------------------------- src/i18n.js | 12 +++++++----- 3 files changed, 57 insertions(+), 58 deletions(-) (limited to 'src') diff --git a/src/api.js b/src/api.js index 4d4212f..03db6b9 100644 --- a/src/api.js +++ b/src/api.js @@ -60,26 +60,31 @@ export function getElectrumWsUrl() { return 'wss://coinos.io/electrum'; } -// Privacy opt-out: turning this off means the app never connects to coinos's -// watcher (so it can't see your addresses) and relies on polling your explorer -// instead. On by default. -const REALTIME_KEY = 'btc-wallet-realtime'; -export function getRealtimeEnabled() { - try { return localStorage.getItem(REALTIME_KEY) !== 'off'; } catch { return true; } -} -export function setRealtimeEnabled(on) { - try { localStorage.setItem(REALTIME_KEY, on ? 'on' : 'off'); } catch {} -} - -// Data backend: the default Esplora REST (`Api`) or a full Electrum-over-WS -// server (`ElectrumApi`) that serves data AND notifications over one connection. -const BACKEND_KEY = 'btc-wallet-backend'; -export function getBackend() { - try { return localStorage.getItem(BACKEND_KEY) === 'electrum' ? 'electrum' : 'esplora'; } catch { return 'esplora'; } +// One "data source" setting drives both where chain data comes from and whether +// coinos sees your addresses for instant push: +// coinos — block-explorer data + coinos instant-payment notifications (default) +// explorer — block-explorer data only; polls for payments, nothing sent to coinos +// electrum — a single Electrum-over-WS server for both data and push (your node) +// getBackend()/getRealtimeEnabled() derive from it, so the rest of the code (and +// the wsUrl logic) is unchanged. Existing per-feature settings migrate in. +const DATA_MODE_KEY = 'btc-wallet-mode'; +const BACKEND_KEY = 'btc-wallet-backend'; // legacy (migration only) +const REALTIME_KEY = 'btc-wallet-realtime'; // legacy (migration only) +export function getDataMode() { + try { + const m = localStorage.getItem(DATA_MODE_KEY); + if (m === 'coinos' || m === 'explorer' || m === 'electrum') return m; + if (localStorage.getItem(BACKEND_KEY) === 'electrum') return 'electrum'; + if (localStorage.getItem(REALTIME_KEY) === 'off') return 'explorer'; + } catch {} + return 'coinos'; } -export function setBackend(b) { - try { localStorage.setItem(BACKEND_KEY, b === 'electrum' ? 'electrum' : 'esplora'); } catch {} +export function setDataMode(m) { + try { localStorage.setItem(DATA_MODE_KEY, m === 'electrum' || m === 'explorer' ? m : 'coinos'); } catch {} } +export function getBackend() { return getDataMode() === 'electrum' ? 'electrum' : 'esplora'; } +// The coinos watcher is used only in 'coinos' mode; 'explorer' polls instead. +export function getRealtimeEnabled() { return getDataMode() === 'coinos'; } // Electrum servers that accept WebSocket connections. blockstream.info is // electrs (validated); the point, though, is pointing at your own Fulcrum/electrs. diff --git a/src/app.js b/src/app.js index 6a994d8..8524528 100644 --- a/src/app.js +++ b/src/app.js @@ -8,7 +8,7 @@ import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, giftOutpoint import { qrSvg } from './qr.js'; import { scanQr } from './scan.js'; import { getSyncConfig, setSyncConfig } from './nostr.js'; -import { getExplorerConfig, setExplorerConfig, EXPLORER_PRESETS, getRealtimeEnabled, setRealtimeEnabled, getBackend, setBackend, ELECTRUM_PRESETS, getElectrumServerConfig, setElectrumServerConfig } from './api.js'; +import { getExplorerConfig, setExplorerConfig, EXPLORER_PRESETS, getDataMode, setDataMode, ELECTRUM_PRESETS, getElectrumServerConfig, setElectrumServerConfig } from './api.js'; import { isSilentPaymentAddress } from './silentpay.js'; import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js'; import { @@ -1298,36 +1298,34 @@ async function doRevoke(id) { // Block explorer / server selection: a preset (mempool.space, blockstream.info) // or a custom Esplora/electrs REST URL (e.g. your own node). function explorerCard() { - const backend = getBackend(); + const mode = getDataMode(); const cfg = getExplorerConfig(); const ecfg = getElectrumServerConfig(); - const rt = getRealtimeEnabled(); - const setRealtime = (on) => { - if (on === getRealtimeEnabled()) return; - setRealtimeEnabled(on); + const setMode = (m) => { + const prev = getDataMode(); + if (m === prev) return; + setDataMode(m); render(); - if (!wallet.offline) wallet.startRealtime(); // reconnect or drop the watcher - }; - const setBackendTo = (b) => { - if (b === getBackend()) return; - setBackend(b); - render(); - if (!wallet.offline) wallet.reloadExplorer(); // rebuild api + reconnect + if (wallet.offline) return; + // coinos⇄explorer only flips the watcher (same REST data); to/from electrum + // swaps the whole backend, so rebuild + rescan there. + if ((m === 'electrum') !== (prev === 'electrum')) wallet.reloadExplorer(); + else wallet.startRealtime(); }; return h( 'div', { class: 'card col' }, - h('h3', {}, t('explorer')), - h('p', { class: 'small muted', style: 'margin:0' }, t('explorerDesc')), - // Data source: Esplora REST, or a full Electrum-over-WS server (data + push). - h('div', { class: 'row between', style: 'margin-top:2px' }, - h('span', { class: 'lab', style: 'margin:0' }, t('dataSource')), - h('div', { class: 'seg' }, - h('button', { type: 'button', class: backend === 'esplora' ? 'active' : '', onClick: () => setBackendTo('esplora') }, t('backendEsplora')), - h('button', { type: 'button', class: backend === 'electrum' ? 'active' : '', onClick: () => setBackendTo('electrum') }, t('backendElectrum')) - ) + h('h3', {}, t('dataSource')), + h('p', { class: 'small muted', style: 'margin:0' }, t('dataSourceDesc')), + h('select', { onChange: (e) => setMode(e.target.value) }, + h('option', { value: 'coinos', selected: mode === 'coinos' }, t('modeCoinos')), + h('option', { value: 'explorer', selected: mode === 'explorer' }, t('modeExplorer')), + h('option', { value: 'electrum', selected: mode === 'electrum' }, t('modeElectrum')) ), - backend === 'electrum' + mode === 'coinos' + ? h('div', { class: 'small faint' }, t('modeCoinosDesc')) + : null, + mode === 'electrum' ? h('div', { class: 'col', style: 'gap:8px' }, h('div', { class: 'small faint' }, t('backendElectrumDesc')), h('select', { @@ -1345,7 +1343,10 @@ function explorerCard() { ) : null ) - : h('div', { class: 'col', style: 'gap:8px' }, + : null, + mode === 'explorer' + ? h('div', { class: 'col', style: 'gap:8px' }, + h('div', { class: 'small faint' }, t('modeExplorerDesc')), h('select', { onChange: (e) => { setExplorerConfig({ server: e.target.value, url: cfg.url }); render(); if (e.target.value !== 'custom' || cfg.url) wallet.reloadExplorer(); }, }, EXPLORER_PRESETS.map((o) => h('option', { value: o.id, selected: o.id === cfg.server }, o.label))), @@ -1359,18 +1360,9 @@ function explorerCard() { }), h('div', { class: 'small faint' }, t('explorerUrlHint')) ) - : null, - // Realtime via coinos — the one place addresses are shared off-device. - // (Electrum already serves its own push, so this only applies to REST.) - h('div', { class: 'row between', style: 'margin-top:8px' }, - h('span', { class: 'lab', style: 'margin:0' }, t('instantNotif')), - h('div', { class: 'seg' }, - h('button', { type: 'button', class: rt ? 'active' : '', onClick: () => setRealtime(true) }, t('syncOn')), - h('button', { type: 'button', class: !rt ? 'active' : '', onClick: () => setRealtime(false) }, t('syncOff')) - ) - ), - h('div', { class: 'small faint' }, t('instantNotifDesc')) + : null ) + : null ); } diff --git a/src/i18n.js b/src/i18n.js index aa8b499..e3341a7 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -260,14 +260,16 @@ const STR = { explorerUrl: 'Server URL', explorerUrlHint: 'Esplora / electrs REST base, e.g. https://mempool.space/api', dataSource: 'Data source', - backendEsplora: 'Explorer', - backendElectrum: 'Electrum', - backendElectrumDesc: 'Fetch data and get instant payments over one Electrum WebSocket. Point it at your own Fulcrum/electrs for a fully self-hosted wallet — no coinos, no REST.', + dataSourceDesc: 'Where Hal reads the chain and how it learns about incoming payments.', + modeCoinos: 'coinos (instant)', + modeCoinosDesc: 'The default. Instant payment alerts via coinos, with chain data from a public block explorer.', + modeExplorer: 'Block explorer', + modeExplorerDesc: 'Reads from your chosen block explorer and polls for payments — nothing is sent to coinos. A bit slower.', + modeElectrum: 'Electrum server', + backendElectrumDesc: 'Data and instant payments over one Electrum WebSocket. Point it at your own Fulcrum/electrs for a fully self-hosted wallet — no coinos, no REST.', electrumUrl: 'Electrum server (wss://)', electrumUrlHint: 'A WebSocket Electrum server, e.g. wss://your-node:50004', silentPaymentNote: 'Silent payment', - instantNotif: 'Instant notifications', - instantNotifDesc: 'Uses coinos to push payments the instant they arrive. Turn off to poll the explorer above instead — a bit slower, but your addresses are never sent to coinos.', deviceSync: 'Device sync', deviceSyncDesc: 'Keep this wallet in sync across your devices by storing an encrypted copy on a Nostr relay — only a device with your seed can read it. Turn it off to keep everything on this device.', syncAcross: 'Sync across devices', -- cgit v1.2.3