From 1edb9ac5723f2d5fda5093437b2e375f98af3777 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 25 Jun 2026 18:08:07 -0700 Subject: Settings: let users opt out of coinos realtime notifications (privacy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WS watcher is the one place the app shares addresses off-device (it subscribes your scripthashes to coinos to push instant payment updates). Add an 'Instant notifications' toggle in the explorer settings: off means the app never connects to coinos and polls your chosen explorer instead — a bit slower, more requests, but nothing is told to coinos. wsUrl returns null when disabled, so startRealtime cleanly skips the socket and relies on the polling fallback. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 23 +++++++++++++++++------ src/app.js | 21 +++++++++++++++++++-- src/i18n.js | 2 ++ 3 files changed, 38 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/api.js b/src/api.js index 1f57353..b981bc6 100644 --- a/src/api.js +++ b/src/api.js @@ -45,11 +45,10 @@ function resolveHosts(net) { return [host('https://mempool.space', 'mempool'), host('https://blockstream.info', 'esplora')]; } -// Realtime is a private Fulcrum (Electrum protocol over WebSocket-Secure). It -// reliably pushes a scripthash-status notification the moment a tx touches a -// watched address — unlike the public Esplora/blockchain.info sockets, whose -// address push doesn't fire. Independent of the REST explorer choice. Mainnet -// only; testnet falls back to polling (null). Overridable for staging/testing. +// Realtime notifications come from coinos's WebSocket watcher: it pushes the +// instant a tx touches a watched address (the app subscribes its frontier + +// pending scripthashes). Independent of the REST explorer choice. Mainnet only; +// testnet falls back to polling (null). Overridable for staging/testing. const ELECTRUM_KEY = 'btc-wallet-electrum'; export function getElectrumWsUrl() { try { @@ -60,8 +59,20 @@ export function getElectrumWsUrl() { // WebSocket quirk; the main hostname proxies WS cleanly). 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 {} +} + export function wsUrl(net) { - if (net === 'testnet') return null; + if (net === 'testnet' || !getRealtimeEnabled()) return null; return getElectrumWsUrl(); } diff --git a/src/app.js b/src/app.js index 5709934..21aea3d 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 } from './api.js'; +import { getExplorerConfig, setExplorerConfig, EXPLORER_PRESETS, getRealtimeEnabled, setRealtimeEnabled } from './api.js'; import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js'; import { fmtBtc, @@ -1298,6 +1298,13 @@ async function doRevoke(id) { // or a custom Esplora/electrs REST URL (e.g. your own node). function explorerCard() { const cfg = getExplorerConfig(); + const rt = getRealtimeEnabled(); + const setRealtime = (on) => { + if (on === getRealtimeEnabled()) return; + setRealtimeEnabled(on); + render(); + if (!wallet.offline) wallet.startRealtime(); // reconnect or drop the watcher + }; return h( 'div', { class: 'card col' }, @@ -1330,7 +1337,17 @@ function explorerCard() { }), h('div', { class: 'small faint' }, t('explorerUrlHint')) ) - : null + : null, + // Realtime via coinos — the one place addresses are shared off-device. Opt + // out to poll the explorer above instead (a bit slower, nothing told to coinos). + 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')) ); } diff --git a/src/i18n.js b/src/i18n.js index bf6e807..0004b83 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -259,6 +259,8 @@ const STR = { explorerDesc: 'The server this wallet queries for balances and history. Point it at your own node for maximum privacy.', explorerUrl: 'Server URL', explorerUrlHint: 'Esplora / electrs REST base, e.g. https://mempool.space/api', + 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