From eeceebb09878a544b18926a3cd164e581b0c9869 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 27 Jun 2026 14:46:02 -0700 Subject: Wire SP foundations into the wallet: configurable indexer + keys/address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api.js: per-network SP indexer config (presets + custom, stored per network) with getSpIndexerConfig/spIndexerUrl/spReceiveEnabled — regtest defaults to the local indexer, others to custom; a hosted mainnet default slots in once we run one. wallet.js: silentPaymentKeys() derives BIP-352 scan/spend keys from the master seed (needs the mnemonic / depth-0 xprv), and silentPaymentAddress() returns the sp1…/tsp1… address. Verified it matches the vector-validated e2e address. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index 2095d51..5287e13 100644 --- a/src/api.js +++ b/src/api.js @@ -106,6 +106,40 @@ function netDefaults(net) { return NET_DEFAULTS[net] || NET_DEFAULTS.mainnet; } // get a ":" suffix. function nk(base, net) { return net === 'mainnet' ? base : `${base}:${net}`; } +// Silent-payment (BIP-352) tweak indexer — a separate, optional endpoint used +// only for SP receiving. It serves public tweak/UTXO data (the scan key never +// leaves the device), but a public one can observe which blocks hold your +// outputs, so self-hosting is the privacy-max option. Per network; an empty URL +// means SP receiving is unavailable on that network. +const SP_INDEXER_PRESETS_BY_NET = { + mainnet: [{ id: 'custom', label: 'Custom', url: '' }], // a hosted default goes here once we run one + testnet: [{ id: 'custom', label: 'Custom', url: '' }], + mutinynet: [{ id: 'custom', label: 'Custom', url: '' }], + regtest: [{ id: 'local', label: 'Local (localhost:8888)', url: 'http://localhost:8888' }, { id: 'custom', label: 'Custom', url: '' }], +}; +const SP_INDEXER_DEFAULT = { mainnet: 'custom', testnet: 'custom', mutinynet: 'custom', regtest: 'local' }; +const SP_INDEXER_KEY = 'btc-wallet-sp-indexer'; +export function spIndexerPresets(net = getNetwork()) { return SP_INDEXER_PRESETS_BY_NET[net] || SP_INDEXER_PRESETS_BY_NET.mainnet; } +export function getSpIndexerConfig(net = getNetwork()) { + try { + const c = JSON.parse(localStorage.getItem(nk(SP_INDEXER_KEY, net)) || 'null'); + if (c && c.server) return { server: c.server, url: c.url || '' }; + } catch {} + return { server: SP_INDEXER_DEFAULT[net] || 'custom', url: '' }; +} +export function setSpIndexerConfig({ server, url }, net = getNetwork()) { + try { localStorage.setItem(nk(SP_INDEXER_KEY, net), JSON.stringify({ server, url: url || '' })); } catch {} +} +// The resolved indexer base URL for a network, or '' if none is configured. +export function spIndexerUrl(net = getNetwork()) { + const cfg = getSpIndexerConfig(net); + if (cfg.server === 'custom') return cfg.url.trim().replace(/\/+$/, ''); + const p = spIndexerPresets(net).find((x) => x.id === cfg.server); + return p && p.url ? p.url.replace(/\/+$/, '') : ''; +} +// SP receiving is available only when an indexer is configured for this network. +export function spReceiveEnabled(net = getNetwork()) { return !!spIndexerUrl(net); } + // Resolve the configured explorer to the host list the Api tries in order. function resolveHosts(net) { const cfg = getExplorerConfig(net); -- cgit v1.2.3 From 803ccd12cded3f09086c715fffbc918a01575e09 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 27 Jun 2026 17:45:01 -0700 Subject: Default mainnet to the hosted coinos silent-payment indexer Mainnet wallets now use https://halwallet.app/sp (our indexer on cs, behind nginx /sp), enabling silent-payment receiving on mainnet. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index 5287e13..56978f8 100644 --- a/src/api.js +++ b/src/api.js @@ -112,12 +112,12 @@ function nk(base, net) { return net === 'mainnet' ? base : `${base}:${net}`; } // outputs, so self-hosting is the privacy-max option. Per network; an empty URL // means SP receiving is unavailable on that network. const SP_INDEXER_PRESETS_BY_NET = { - mainnet: [{ id: 'custom', label: 'Custom', url: '' }], // a hosted default goes here once we run one + mainnet: [{ id: 'coinos', label: 'coinos', url: 'https://halwallet.app/sp' }, { id: 'custom', label: 'Custom', url: '' }], testnet: [{ id: 'custom', label: 'Custom', url: '' }], mutinynet: [{ id: 'custom', label: 'Custom', url: '' }], regtest: [{ id: 'local', label: 'Local (localhost:8888)', url: 'http://localhost:8888' }, { id: 'custom', label: 'Custom', url: '' }], }; -const SP_INDEXER_DEFAULT = { mainnet: 'custom', testnet: 'custom', mutinynet: 'custom', regtest: 'local' }; +const SP_INDEXER_DEFAULT = { mainnet: 'coinos', testnet: 'custom', mutinynet: 'custom', regtest: 'local' }; const SP_INDEXER_KEY = 'btc-wallet-sp-indexer'; export function spIndexerPresets(net = getNetwork()) { return SP_INDEXER_PRESETS_BY_NET[net] || SP_INDEXER_PRESETS_BY_NET.mainnet; } export function getSpIndexerConfig(net = getNetwork()) { -- cgit v1.2.3 From 98b216605604c3fea85936a54b07344ddc7a08a6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 28 Jun 2026 04:38:18 -0700 Subject: Unify data sources into one list; coinos → electrum.coinos.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Combined the separate Electrum and Explorer settings into a single data-source list. Each preset knows its type (electrum vs esplora); a custom source's type is detected from its URL scheme (ws/wss → Electrum, http/https → Esplora). The settings UI is now one dropdown + a URL field for Custom, with a one-line note showing the detected/selected type. getBackend/electrumCandidates/resolveHosts derive from the selected source. Old split settings migrate automatically. - coinos now points at wss://electrum.coinos.io (was wss://halwallet.app/electrum). Set up as a cloudflared route on the bitcoin tunnel → the Fulcrum WS (:50003), with a DNS CNAME. Verified the endpoint serves Electrum from a browser (Fulcrum 2.1.1 replies; cloudflare Bot Fight Mode only challenges non-browser clients). Verified: combined list + auto-detection (unit + browser), old-settings migration, regtest data flow unchanged, and the app loads over electrum.coinos.io with no errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 142 ++++++++++++++++++++++++++++++++++++------------------------ src/app.js | 71 +++++++++--------------------- src/i18n.js | 4 ++ 3 files changed, 111 insertions(+), 106 deletions(-) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index 56978f8..8621af8 100644 --- a/src/api.js +++ b/src/api.js @@ -50,58 +50,90 @@ export function setNetwork(net) { // live (so there are no duplicate URL fields elsewhere). Each network has its own // Electrum servers and block explorers; the selection is stored per network, so // switching networks remembers each one's choice. 'custom' points at your node. -const ELECTRUM_PRESETS_BY_NET = { +// One unified list of data sources per network. Each entry knows its type — +// electrum (an Electrum-over-WS server: data + instant push) or esplora (a block +// explorer's REST API: data only, polled). The UI shows a single list; the wallet +// derives the backend from the selected source's type. A custom source's type is +// detected from its URL scheme (ws/wss → electrum, http/https → esplora). +const DATA_SOURCES_BY_NET = { mainnet: [ - { id: 'coinos', label: 'coinos', url: 'wss://halwallet.app/electrum' }, - { id: 'blockstream', label: 'blockstream.info', url: 'wss://blockstream.info/electrum-websocket/' }, - { id: 'electroncash', label: 'btc.electroncash.dk', url: 'wss://btc.electroncash.dk:60004' }, - { id: 'blackie', label: 'blackie.c3-soft.com', url: 'wss://blackie.c3-soft.com:57004' }, - { id: 'jochen', label: 'electrum.jochen-hoenicke.de', url: 'wss://electrum.jochen-hoenicke.de:50010' }, - { id: 'mempoolguide', label: 'mempool.guide', url: 'wss://mempool.guide/electrum-websocket/' }, - { id: 'custom', label: 'Custom (your node)', url: '' }, + { id: 'coinos', label: 'coinos', type: 'electrum', url: 'wss://electrum.coinos.io' }, + { id: 'mempool', label: 'mempool.space', type: 'esplora', base: 'https://mempool.space/api', web: 'https://mempool.space', kind: 'mempool' }, + { id: 'blockstream', label: 'blockstream.info', type: 'esplora', base: 'https://blockstream.info/api', web: 'https://blockstream.info', kind: 'esplora' }, + { id: 'electroncash', label: 'btc.electroncash.dk', type: 'electrum', url: 'wss://btc.electroncash.dk:60004' }, + { id: 'jochen', label: 'electrum.jochen-hoenicke.de', type: 'electrum', url: 'wss://electrum.jochen-hoenicke.de:50010' }, + { id: 'mempoolguide', label: 'mempool.guide', type: 'electrum', url: 'wss://mempool.guide/electrum-websocket/' }, + { id: 'custom', label: 'Custom', type: 'auto', url: '' }, ], testnet: [ - { id: 'blockstream', label: 'blockstream.info', url: 'wss://blockstream.info/testnet/electrum-websocket/' }, - { id: 'custom', label: 'Custom (your node)', url: '' }, + { id: 'mempool', label: 'mempool.space', type: 'esplora', base: 'https://mempool.space/testnet/api', web: 'https://mempool.space/testnet', kind: 'mempool' }, + { id: 'blockstream', label: 'blockstream.info', type: 'esplora', base: 'https://blockstream.info/testnet/api', web: 'https://blockstream.info/testnet', kind: 'esplora' }, + { id: 'blockstreamel', label: 'blockstream.info (Electrum)', type: 'electrum', url: 'wss://blockstream.info/testnet/electrum-websocket/' }, + { id: 'custom', label: 'Custom', type: 'auto', url: '' }, ], mutinynet: [ - { id: 'custom', label: 'Custom (your node)', url: '' }, + { id: 'mutinynet', label: 'mutinynet.com', type: 'esplora', base: 'https://mutinynet.com/api', web: 'https://mutinynet.com', kind: 'esplora' }, + { id: 'custom', label: 'Custom', type: 'auto', url: '' }, ], regtest: [ - { id: 'local', label: 'Local Fulcrum', url: 'ws://localhost:50003' }, - { id: 'custom', label: 'Custom', url: '' }, + { id: 'local', label: 'Local Fulcrum', type: 'electrum', url: 'ws://localhost:50003' }, + { id: 'localesplora', label: 'Local Esplora', type: 'esplora', base: 'http://localhost:3000', web: 'http://localhost:3000', kind: 'esplora' }, + { id: 'custom', label: 'Custom', type: 'auto', url: '' }, ], }; -const EXPLORER_PRESETS_BY_NET = { - mainnet: [ - { id: 'mempool', label: 'mempool.space', base: 'https://mempool.space/api', web: 'https://mempool.space', kind: 'mempool' }, - { id: 'blockstream', label: 'blockstream.info', base: 'https://blockstream.info/api', web: 'https://blockstream.info', kind: 'esplora' }, - { id: 'custom', label: 'Custom', base: '', web: '', kind: 'esplora' }, - ], - testnet: [ - { id: 'mempool', label: 'mempool.space', base: 'https://mempool.space/testnet/api', web: 'https://mempool.space/testnet', kind: 'mempool' }, - { id: 'blockstream', label: 'blockstream.info', base: 'https://blockstream.info/testnet/api', web: 'https://blockstream.info/testnet', kind: 'esplora' }, - { id: 'custom', label: 'Custom', base: '', web: '', kind: 'esplora' }, - ], - mutinynet: [ - { id: 'mutinynet', label: 'mutinynet.com', base: 'https://mutinynet.com/api', web: 'https://mutinynet.com', kind: 'esplora' }, - { id: 'custom', label: 'Custom', base: '', web: '', kind: 'esplora' }, - ], - regtest: [ - { id: 'local', label: 'Local (localhost:3000)', base: 'http://localhost:3000', web: 'http://localhost:3000', kind: 'esplora' }, - { id: 'custom', label: 'Custom', base: '', web: '', kind: 'esplora' }, - ], -}; -// Per-network defaults: the mode + server a network starts on before any choice. +const DATA_SOURCE_DEFAULT = { mainnet: 'coinos', testnet: 'mempool', mutinynet: 'mutinynet', regtest: 'local' }; +export function dataSources(net = getNetwork()) { return DATA_SOURCES_BY_NET[net] || DATA_SOURCES_BY_NET.mainnet; } +const detectType = (url) => (/^wss?:\/\//i.test((url || '').trim()) ? 'electrum' : 'esplora'); + +// Old per-network defaults (mode + server id) — kept only so the one-time +// migration from the old split electrum/explorer settings can read them. const NET_DEFAULTS = { mainnet: { mode: 'electrum', electrum: 'coinos', explorer: 'mempool' }, - testnet: { mode: 'explorer', electrum: 'blockstream', explorer: 'mempool' }, - mutinynet: { mode: 'explorer', electrum: 'custom', explorer: 'mutinynet' }, - regtest: { mode: 'electrum', electrum: 'local', explorer: 'local' }, + testnet: { mode: 'esplora', electrum: 'blockstreamel', explorer: 'mempool' }, + mutinynet: { mode: 'esplora', electrum: 'custom', explorer: 'mutinynet' }, + regtest: { mode: 'electrum', electrum: 'local', explorer: 'localesplora' }, }; -export function electrumPresets(net = getNetwork()) { return ELECTRUM_PRESETS_BY_NET[net] || ELECTRUM_PRESETS_BY_NET.mainnet; } -export function explorerPresets(net = getNetwork()) { return EXPLORER_PRESETS_BY_NET[net] || EXPLORER_PRESETS_BY_NET.mainnet; } function netDefaults(net) { return NET_DEFAULTS[net] || NET_DEFAULTS.mainnet; } +// Internal filtered views (electrum fallover list + esplora host resolution). +export function electrumPresets(net = getNetwork()) { return dataSources(net).filter((x) => x.type === 'electrum'); } +export function explorerPresets(net = getNetwork()) { return dataSources(net).filter((x) => x.type === 'esplora'); } + +// The selected data source, resolved to a full entry. Migrates from the old split +// settings the first time (until the user picks a source, which writes SOURCE_KEY). +const SOURCE_KEY = 'btc-wallet-source'; +export function getSource(net = getNetwork()) { + let sel = null; + try { sel = JSON.parse(localStorage.getItem(nk(SOURCE_KEY, net)) || 'null'); } catch {} + if (!sel || !sel.id) sel = migrateSource(net); + const list = dataSources(net); + if (sel.id === 'custom') { + const url = (sel.url || '').trim(); + const type = detectType(url); + if (type === 'electrum') return { id: 'custom', label: 'Custom', type, url }; + const base = url.replace(/\/+$/, ''); + return { id: 'custom', label: 'Custom', type, url, base, web: base.replace(/\/api$/, ''), kind: 'esplora' }; + } + return list.find((x) => x.id === sel.id) || list.find((x) => x.id === DATA_SOURCE_DEFAULT[net]) || list[0]; +} +export function setSource({ id, url }, net = getNetwork()) { + try { localStorage.setItem(nk(SOURCE_KEY, net), JSON.stringify({ id, url: url || '' })); } catch {} +} +function migrateSource(net) { + const list = dataSources(net); + const has = (id) => list.some((x) => x.id === id); + try { + if (getDataMode(net) === 'electrum') { + const ec = getElectrumServerConfig(net); + if (ec.server === 'custom' && ec.url) return { id: 'custom', url: ec.url }; + if (has(ec.server)) return { id: ec.server }; + } else { + const xc = getExplorerConfig(net); + if (xc.server === 'custom' && xc.url) return { id: 'custom', url: xc.url }; + if (has(xc.server)) return { id: xc.server }; + } + } catch {} + return { id: DATA_SOURCE_DEFAULT[net] || list[0].id }; +} // Per-network storage keys: mainnet keeps the bare key (backward compat), others // get a ":" suffix. function nk(base, net) { return net === 'mainnet' ? base : `${base}:${net}`; } @@ -142,17 +174,17 @@ export function spReceiveEnabled(net = getNetwork()) { return !!spIndexerUrl(net // Resolve the configured explorer to the host list the Api tries in order. function resolveHosts(net) { - const cfg = getExplorerConfig(net); - const presets = explorerPresets(net); - const mk = (p) => ({ base: p.base, kind: p.kind, web: p.web, cooldownUntil: 0 }); - if (cfg.server === 'custom' && cfg.url) { - const base = cfg.url.trim().replace(/\/+$/, ''); - return [{ base, kind: 'esplora', web: base.replace(/\/api$/, ''), cooldownUntil: 0 }]; + const src = getSource(net); + const mk = (p) => ({ base: p.base, kind: p.kind || 'esplora', web: p.web || (p.base || '').replace(/\/api$/, ''), cooldownUntil: 0 }); + if (src.type === 'esplora' && src.base) { + // Mainnet silently fails mempool.space over to blockstream.info. + if (net === 'mainnet' && src.id === 'mempool') { const bs = dataSources(net).find((x) => x.id === 'blockstream'); return bs ? [mk(src), mk(bs)] : [mk(src)]; } + return [mk(src)]; } - const p = presets.find((x) => x.id === cfg.server && x.base) || presets.find((x) => x.base) || presets[0]; - // Mainnet silently fails mempool.space over to blockstream.info. - if (net === 'mainnet' && p.id === 'mempool') return [mk(p), mk(presets.find((x) => x.id === 'blockstream'))]; - return [mk(p)]; + // Electrum (or an empty custom) selected: no esplora data host, but tx links + // still need a web base, so use the network's default explorer host. + const def = dataSources(net).find((x) => x.type === 'esplora'); + return [def ? mk(def) : { base: '', kind: 'esplora', web: 'https://mempool.space', cooldownUntil: 0 }]; } // The "data source" setting (stored per network) picks where chain data + @@ -176,7 +208,7 @@ export function setDataMode(m, net = getNetwork()) { } export function getBackend() { - return getDataMode() === 'electrum' ? 'electrum' : 'esplora'; + return getSource().type === 'electrum' ? 'electrum' : 'esplora'; } // The legacy coinos watcher shim is retired (coinos is a full Electrum server // now); 'explorer' mode polls for payments instead of subscribing. @@ -200,12 +232,10 @@ export function setElectrumServerConfig({ server, url }, net = getNetwork()) { // didn't pick. const PUBLIC_ELECTRUM = (net) => electrumPresets(net).filter((x) => x.url).map((x) => x.url); export function electrumCandidates(net = getNetwork()) { - const cfg = getElectrumServerConfig(net); - const presets = electrumPresets(net); - if (cfg.server === 'custom') return cfg.url.trim() ? [cfg.url.trim()] : []; - const primary = (presets.find((x) => x.id === cfg.server) || presets[0]).url; - if (!primary) return []; - return [primary, ...PUBLIC_ELECTRUM(net).filter((u) => u !== primary)]; + const src = getSource(net); + if (src.type !== 'electrum' || !src.url) return []; + if (src.id === 'custom') return [src.url]; // your node — don't fall over to public servers + return [src.url, ...PUBLIC_ELECTRUM(net).filter((u) => u !== src.url)]; } export function resolveElectrumUrl() { return electrumCandidates()[0] || null; diff --git a/src/app.js b/src/app.js index 2470ac8..c49e279 100644 --- a/src/app.js +++ b/src/app.js @@ -8,7 +8,7 @@ import { Wallet, newMnemonic, isValidMnemonic, accountXpubFor, cacheKeyFor, utxo import { qrSvg } from './qr.js'; import { scanQr } from './scan.js'; import { getSyncConfig, setSyncConfig } from './nostr.js'; -import { getExplorerConfig, setExplorerConfig, explorerPresets, getDataMode, setDataMode, electrumPresets, getElectrumServerConfig, setElectrumServerConfig, getNetwork, setNetwork, NETWORKS, getSpIndexerConfig, setSpIndexerConfig, spIndexerPresets } from './api.js'; +import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS, getSpIndexerConfig, setSpIndexerConfig, spIndexerPresets } from './api.js'; import { isSilentPaymentAddress } from './silentpay.js'; import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js'; import { @@ -1613,63 +1613,34 @@ function networkCard() { function explorerCard() { const net = getNetwork(); - const mode = getDataMode(); - const cfg = getExplorerConfig(); - const ecfg = getElectrumServerConfig(); - const setMode = (m) => { - if (m === getDataMode()) return; - setDataMode(m); + const src = getSource(); + const sources = dataSources(net); + const pick = (id) => { + setSource({ id, url: '' }); render(); - // electrum⇄explorer swaps the whole backend, so rebuild + rescan. - if (!wallet.offline) wallet.reloadExplorer(); + // Switching source can swap the whole backend (electrum⇄esplora), so rebuild + // + rescan — except an empty custom (wait for the URL). + if (id !== 'custom' && !wallet.offline) wallet.reloadExplorer(); }; + const typeDesc = t(src.type === 'electrum' ? 'detectedElectrum' : 'detectedEsplora'); return h( 'div', { class: 'card col' }, 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: 'electrum', selected: mode === 'electrum' }, t('modeElectrum')), - h('option', { value: 'explorer', selected: mode === 'explorer' }, t('modeExplorer')) - ), - mode === 'electrum' - ? h('div', { class: 'col', style: 'gap:8px' }, - h('div', { class: 'small faint' }, t('backendElectrumDesc')), - h('select', { - onChange: (e) => { setElectrumServerConfig({ server: e.target.value, url: ecfg.url }); render(); if (e.target.value !== 'custom' || ecfg.url) wallet.reloadExplorer(); }, - }, electrumPresets(net).map((o) => h('option', { value: o.id, selected: o.id === ecfg.server }, o.label))), - ecfg.server === 'custom' - ? h('label', { class: 'field' }, - h('span', { class: 'lab' }, t('electrumUrl')), - h('input', { - type: 'text', class: 'mono-input', placeholder: 'wss://your-node:50004', - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ecfg.url, - onChange: (e) => { setElectrumServerConfig({ server: 'custom', url: e.target.value.trim() }); wallet.reloadExplorer(); }, - }), - h('div', { class: 'small faint' }, t('electrumUrlHint')) - ) - : null - ) - : 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(); }, - }, explorerPresets(net).map((o) => h('option', { value: o.id, selected: o.id === cfg.server }, o.label))), - cfg.server === 'custom' - ? h('label', { class: 'field' }, - h('span', { class: 'lab' }, t('explorerUrl')), - h('input', { - type: 'text', class: 'mono-input', placeholder: 'https://mempool.space/api', - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: cfg.url, - onChange: (e) => { setExplorerConfig({ server: 'custom', url: e.target.value.trim() }); wallet.reloadExplorer(); }, - }), - h('div', { class: 'small faint' }, t('explorerUrlHint')) - ) - : null + h('select', { onChange: (e) => pick(e.target.value) }, + sources.map((o) => h('option', { value: o.id, selected: o.id === src.id }, o.label))), + src.id === 'custom' + ? h('label', { class: 'field' }, + h('span', { class: 'lab' }, t('sourceUrl')), + h('input', { + type: 'text', class: 'mono-input', placeholder: 'wss://your-node:50004 · https://your-esplora/api', + autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: src.url || '', + onChange: (e) => { const url = e.target.value.trim(); setSource({ id: 'custom', url }); render(); if (!wallet.offline) wallet.reloadExplorer(); }, + }), + h('div', { class: 'small faint' }, src.url ? typeDesc : t('sourceUrlHint')) ) - : null + : h('div', { class: 'small faint' }, typeDesc) ); } diff --git a/src/i18n.js b/src/i18n.js index adfa4e8..5b9f1e8 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -300,6 +300,10 @@ const STR = { backendElectrumDesc: 'Data and instant payments over one Electrum WebSocket. Point it at your own server for a fully self-hosted wallet.', electrumUrl: 'Electrum server (wss://)', electrumUrlHint: 'A WebSocket Electrum server, e.g. wss://your-node:50004', + sourceUrl: 'Server URL', + sourceUrlHint: 'wss:// for an Electrum server, or an Esplora REST base (https://…/api). The type is detected from the URL.', + detectedElectrum: 'Electrum — data + instant payment notifications.', + detectedEsplora: 'Block explorer (Esplora) — data only; polls for payments.', silentPaymentNote: 'Silent payment', spReceiveTitle: 'Silent payment address', spReceiveDesc: "A reusable address that can't be tracked.", -- cgit v1.2.3 From ad71e8e879b51d4af12e74d108515d9d4d64598b Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 28 Jun 2026 04:47:12 -0700 Subject: Show full URLs in the source lists; SP indexer → tweaks.coinos.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Data-source and SP-indexer dropdowns now show the endpoint URL alongside the name (e.g. 'coinos — wss://electrum.coinos.io'), so it's clear where data comes from; 'Custom' stays bare. - The tweak indexer is now served at https://tweaks.coinos.io (was halwallet.app/ sp): a cloudflared route on the bitcoin tunnel → the indexer at :8888, with a DNS CNAME. Verified from a browser: /height, /mempool (1064 items), and /ws all work (the coinos.io Bot Fight Mode challenges non-browser clients only). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 2 +- src/app.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index 8621af8..cb582f3 100644 --- a/src/api.js +++ b/src/api.js @@ -144,7 +144,7 @@ function nk(base, net) { return net === 'mainnet' ? base : `${base}:${net}`; } // outputs, so self-hosting is the privacy-max option. Per network; an empty URL // means SP receiving is unavailable on that network. const SP_INDEXER_PRESETS_BY_NET = { - mainnet: [{ id: 'coinos', label: 'coinos', url: 'https://halwallet.app/sp' }, { id: 'custom', label: 'Custom', url: '' }], + mainnet: [{ id: 'coinos', label: 'coinos', url: 'https://tweaks.coinos.io' }, { id: 'custom', label: 'Custom', url: '' }], testnet: [{ id: 'custom', label: 'Custom', url: '' }], mutinynet: [{ id: 'custom', label: 'Custom', url: '' }], regtest: [{ id: 'local', label: 'Local (localhost:8888)', url: 'http://localhost:8888' }, { id: 'custom', label: 'Custom', url: '' }], diff --git a/src/app.js b/src/app.js index c49e279..0a74589 100644 --- a/src/app.js +++ b/src/app.js @@ -1629,7 +1629,7 @@ function explorerCard() { h('h3', {}, t('dataSource')), h('p', { class: 'small muted', style: 'margin:0' }, t('dataSourceDesc')), h('select', { onChange: (e) => pick(e.target.value) }, - sources.map((o) => h('option', { value: o.id, selected: o.id === src.id }, o.label))), + sources.map((o) => h('option', { value: o.id, selected: o.id === src.id }, o.id === 'custom' ? o.label : `${o.label} — ${o.url || o.base}`))), src.id === 'custom' ? h('label', { class: 'field' }, h('span', { class: 'lab' }, t('sourceUrl')), @@ -2319,7 +2319,7 @@ function spIndexerCard() { h('h3', {}, t('spIndexerTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('spIndexerDesc')), h('select', { onChange: (e) => { setSpIndexerConfig({ server: e.target.value, url: cfg.url }, net); render(); } }, - spIndexerPresets(net).map((o) => h('option', { value: o.id, selected: o.id === cfg.server }, o.label))), + spIndexerPresets(net).map((o) => h('option', { value: o.id, selected: o.id === cfg.server }, o.id === 'custom' ? o.label : `${o.label} — ${o.url}`))), cfg.server === 'custom' ? h('label', { class: 'field' }, h('span', { class: 'lab' }, t('spIndexerUrl')), -- cgit v1.2.3 From 06376a71d4b9a4f579095933d91fe0a369b5e0fe Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 28 Jun 2026 14:12:47 -0700 Subject: SP indexer → sp.coinos.io (clearer than tweaks); tunnel on http2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed the default mainnet SP-indexer endpoint to sp.coinos.io. Also switched the cloudflared bitcoin tunnel to http2 (QUIC was intermittently dropping edge connections → transient 502s). tweaks.coinos.io still resolves to the same indexer. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index cb582f3..b7b34d9 100644 --- a/src/api.js +++ b/src/api.js @@ -144,7 +144,7 @@ function nk(base, net) { return net === 'mainnet' ? base : `${base}:${net}`; } // outputs, so self-hosting is the privacy-max option. Per network; an empty URL // means SP receiving is unavailable on that network. const SP_INDEXER_PRESETS_BY_NET = { - mainnet: [{ id: 'coinos', label: 'coinos', url: 'https://tweaks.coinos.io' }, { id: 'custom', label: 'Custom', url: '' }], + mainnet: [{ id: 'coinos', label: 'coinos', url: 'https://sp.coinos.io' }, { id: 'custom', label: 'Custom', url: '' }], testnet: [{ id: 'custom', label: 'Custom', url: '' }], mutinynet: [{ id: 'custom', label: 'Custom', url: '' }], regtest: [{ id: 'local', label: 'Local (localhost:8888)', url: 'http://localhost:8888' }, { id: 'custom', label: 'Custom', url: '' }], -- cgit v1.2.3