aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-27 08:56:25 -0700
committerAdam Soltys <[email protected]>2026-06-27 08:56:25 -0700
commit2b739d818acd045905bcd5e9c9c7c8b0cab98cc9 (patch)
treea0c08508f1d69322a937ac55080c0474f9c4973c /src
parent10f9ebc0d047f1599f4665fda0b073c449bb8f64 (diff)
downloadcoinosv3-2b739d818acd045905bcd5e9c9c7c8b0cab98cc9.tar.gz
coinosv3-2b739d818acd045905bcd5e9c9c7c8b0cab98cc9.zip
Let users choose a Boltz swap provider in Settings
Replaces the single hardcoded Boltz endpoint with a provider selector. Presets are independent Boltz-compatible instances from SwapMarket's list (Boltz Exchange, Middle Way, ZEUS Swaps, Eldamar) plus Local (regtest) and a Custom option; default is Local on regtest, Boltz Exchange otherwise. The swap-status WebSocket is derived from the API URL (http->ws + /v2/ws) unless overridden. Swaps remain non-custodial regardless of provider — a provider can fail a swap but never take funds (hal claims/refunds on-chain). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/api.js47
-rw-r--r--src/app.js36
2 files changed, 70 insertions, 13 deletions
diff --git a/src/api.js b/src/api.js
index d6fa2ee..e5aa076 100644
--- a/src/api.js
+++ b/src/api.js
@@ -57,20 +57,43 @@ export function setRegtestEsplora(url) {
try { localStorage.setItem(REGTEST_ESPLORA_KEY, (url || '').trim()); } catch {}
}
-// Boltz swap backend endpoints (regtest defaults; the api-v2 REST + the
-// sidecar swap-status WebSocket). Overridable in Settings for staging/mainnet.
-const BOLTZ_API_KEY = 'btc-wallet-boltz-api';
-const BOLTZ_WS_KEY = 'btc-wallet-boltz-ws';
-export function getBoltzApi() {
- try { return (localStorage.getItem(BOLTZ_API_KEY) || '').trim() || 'http://localhost:9001'; }
- catch { return 'http://localhost:9001'; }
+// Boltz swap provider — the api-v2 REST endpoint the SwapManager talks to; the
+// swap-status WebSocket is derived from it (http->ws + /v2/ws) unless given.
+// Presets are independent Boltz-compatible instances from SwapMarket's list
+// (swapmarket.github.io); `local` is the regtest stack. Non-custodial either
+// way: a provider can fail a swap but never steal (we claim/refund on-chain).
+export const BOLTZ_PRESETS = [
+ { id: 'local', label: 'Local (regtest)', api: 'http://localhost:9001', ws: 'ws://localhost:9004/v2/ws' },
+ { id: 'boltz', label: 'Boltz Exchange', api: 'https://api.boltz.exchange' },
+ { id: 'middleway', label: 'Middle Way', api: 'https://api.middle-way.space' },
+ { id: 'zeus', label: 'ZEUS Swaps', api: 'https://swaps.zeuslsp.com/api' },
+ { id: 'eldamar', label: 'Eldamar', api: 'https://boltz-api.eldamar.icu' },
+ { id: 'custom', label: 'Custom…', api: '', ws: '' },
+];
+const BOLTZ_PROVIDER_KEY = 'btc-wallet-boltz-provider'; // selected preset id
+const BOLTZ_CUSTOM_KEY = 'btc-wallet-boltz-custom'; // { api, ws } for custom
+
+export function getBoltzCustom() {
+ try { const c = JSON.parse(localStorage.getItem(BOLTZ_CUSTOM_KEY) || 'null'); if (c) return { api: c.api || '', ws: c.ws || '' }; } catch {}
+ return { api: '', ws: '' };
+}
+export function setBoltzCustom({ api, ws }) {
+ try { localStorage.setItem(BOLTZ_CUSTOM_KEY, JSON.stringify({ api: (api || '').trim(), ws: (ws || '').trim() })); } catch {}
}
-export function getBoltzWs() {
- try { return (localStorage.getItem(BOLTZ_WS_KEY) || '').trim() || 'ws://localhost:9004/v2/ws'; }
- catch { return 'ws://localhost:9004/v2/ws'; }
+export function getBoltzProviderId() {
+ try { const id = localStorage.getItem(BOLTZ_PROVIDER_KEY); if (id && BOLTZ_PRESETS.some((p) => p.id === id)) return id; } catch {}
+ return getNetwork() === 'regtest' ? 'local' : 'boltz'; // default per network
+}
+export function setBoltzProviderId(id) { try { localStorage.setItem(BOLTZ_PROVIDER_KEY, id); } catch {} }
+
+export function getBoltzProvider() {
+ const p = BOLTZ_PRESETS.find((x) => x.id === getBoltzProviderId()) || BOLTZ_PRESETS[1];
+ if (p.id === 'custom') { const c = getBoltzCustom(); return { id: 'custom', api: c.api, ws: c.ws }; }
+ return { id: p.id, api: p.api, ws: p.ws || '' };
}
-export function setBoltzApi(url) { try { localStorage.setItem(BOLTZ_API_KEY, (url || '').trim()); } catch {} }
-export function setBoltzWs(url) { try { localStorage.setItem(BOLTZ_WS_KEY, (url || '').trim()); } catch {} }
+const deriveBoltzWs = (api) => api ? api.replace(/^http/, 'ws').replace(/\/+$/, '') + '/v2/ws' : '';
+export function getBoltzApi() { return getBoltzProvider().api; }
+export function getBoltzWs() { const p = getBoltzProvider(); return p.ws || deriveBoltzWs(p.api); }
// Resolve the configured explorer to the host list the Api tries in order.
function resolveHosts(net) {
diff --git a/src/app.js b/src/app.js
index 04d87cc..bdc0b31 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, EXPLORER_PRESETS, getDataMode, setDataMode, ELECTRUM_PRESETS, getElectrumServerConfig, setElectrumServerConfig, getNetwork, setNetwork, getRegtestEsplora, setRegtestEsplora, getBoltzApi, setBoltzApi } from './api.js';
+import { getExplorerConfig, setExplorerConfig, EXPLORER_PRESETS, getDataMode, setDataMode, ELECTRUM_PRESETS, getElectrumServerConfig, setElectrumServerConfig, getNetwork, setNetwork, getRegtestEsplora, setRegtestEsplora, getBoltzApi, BOLTZ_PRESETS, getBoltzProviderId, setBoltzProviderId, getBoltzCustom, setBoltzCustom } from './api.js';
import { SwapManager } from './swap.js';
import { isSilentPaymentAddress } from './silentpay.js';
import { t, LANGS, getLang, setLang, isRTL, loadLocale } from './i18n.js';
@@ -1330,6 +1330,7 @@ function settingsTab() {
),
autoLockCard(),
networkCard(),
+ boltzProviderCard(),
explorerCard(),
wallet.watchOnly || !wallet.mnemonic ? null : syncCard()
);
@@ -1571,6 +1572,39 @@ function changeNetwork(net) {
if (acc) activateAccount(acc); else render();
}
+// Swap provider selector. Boltz-compatible providers (from SwapMarket) plus a
+// custom option. Swaps stay non-custodial regardless of provider.
+function boltzProviderCard() {
+ const id = getBoltzProviderId();
+ const custom = getBoltzCustom();
+ return h(
+ 'div',
+ { class: 'card col' },
+ h('h3', {}, '⚡ Swap provider'),
+ h('p', { class: 'small muted', style: 'margin:0' }, 'Boltz-compatible provider for Lightning swaps. Non-custodial: a provider can fail a swap but never take your funds.'),
+ h('select', { onChange: (e) => { setBoltzProviderId(e.target.value); render(); } },
+ BOLTZ_PRESETS.map((p) => h('option', { value: p.id, selected: p.id === id }, p.label))),
+ id === 'custom'
+ ? h('div', { class: 'col', style: 'gap:8px' },
+ h('label', { class: 'field' },
+ h('span', { class: 'lab' }, 'API URL'),
+ h('input', {
+ type: 'text', class: 'mono-input', placeholder: 'https://api.example.com',
+ autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: custom.api,
+ onChange: (e) => { setBoltzCustom({ api: e.target.value.trim(), ws: custom.ws }); render(); },
+ })),
+ h('label', { class: 'field' },
+ h('span', { class: 'lab' }, 'WebSocket URL (optional)'),
+ h('input', {
+ type: 'text', class: 'mono-input', placeholder: 'wss://api.example.com/v2/ws',
+ autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: custom.ws,
+ onChange: (e) => { setBoltzCustom({ api: custom.api, ws: e.target.value.trim() }); render(); },
+ }),
+ h('div', { class: 'small faint' }, 'Left blank, the WS is derived from the API URL.')))
+ : h('div', { class: 'small faint', style: 'word-break:break-all' }, getBoltzApi())
+ );
+}
+
// Network selector (mainnet / testnet / regtest). Regtest also exposes the
// Esplora base URL (default coinos chopsticks at http://localhost:3000).
function networkCard() {