diff options
| author | Adam Soltys <[email protected]> | 2026-06-27 09:09:31 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-27 09:09:31 -0700 |
| commit | 33bbdd3c1a2c92e8d39d461f3b198f034283633b (patch) | |
| tree | 052b1c4a76bac8531d18b97b612b496e09c23d1e | |
| parent | 2b739d818acd045905bcd5e9c9c7c8b0cab98cc9 (diff) | |
| parent | 1d0c1c9bfb9ccfb8a28e0cf147af12c37655cdcc (diff) | |
| download | coinosv3-33bbdd3c1a2c92e8d39d461f3b198f034283633b.tar.gz coinosv3-33bbdd3c1a2c92e8d39d461f3b198f034283633b.zip | |
Merge branch 'master' into boltz-swaps
# Conflicts:
# src/app.js
| -rw-r--r-- | src/api.js | 14 | ||||
| -rw-r--r-- | src/app.js | 28 | ||||
| -rw-r--r-- | tools/electrum-ws.test.js | 44 |
3 files changed, 75 insertions, 11 deletions
@@ -204,8 +204,20 @@ export function resolveElectrumUrl() { return electrumCandidates()[0] || null; } +// Regtest realtime push: a local electrs reached over a WS<->TCP bridge (electrs +// is TCP-only). Same Electrum protocol the watcher already speaks, so the wallet +// subscribes scripthashes and reconciles via esplora on each notification. +const REGTEST_ELECTRUM_KEY = 'btc-wallet-regtest-electrum'; +export function getRegtestElectrumWs() { + try { return (localStorage.getItem(REGTEST_ELECTRUM_KEY) || '').trim() || 'ws://localhost:50003'; } + catch { return 'ws://localhost:50003'; } +} +export function setRegtestElectrumWs(url) { try { localStorage.setItem(REGTEST_ELECTRUM_KEY, (url || '').trim()); } catch {} } + export function wsUrl(net) { - if (net !== 'mainnet') return null; // coinos watcher is mainnet-only + // Regtest gets realtime via the local electrs WS bridge (data still via esplora). + if (net === 'regtest') return getRealtimeEnabled() ? getRegtestElectrumWs() : null; + if (net !== 'mainnet') return null; // testnet: poll-only // Electrum backend selects/rotates candidates in the wallet; here we only // resolve the coinos watcher (esplora 'coinos' mode) or null (esplora 'explorer'). if (getBackend() === 'electrum') return null; @@ -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, BOLTZ_PRESETS, getBoltzProviderId, setBoltzProviderId, getBoltzCustom, setBoltzCustom } from './api.js'; +import { getExplorerConfig, setExplorerConfig, EXPLORER_PRESETS, getDataMode, setDataMode, ELECTRUM_PRESETS, getElectrumServerConfig, setElectrumServerConfig, getNetwork, setNetwork, getRegtestEsplora, setRegtestEsplora, getRegtestElectrumWs, setRegtestElectrumWs, 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'; @@ -1620,15 +1620,23 @@ function networkCard() { h('option', { value: 'regtest', selected: net === 'regtest' }, 'Regtest') ), net === 'regtest' - ? h('label', { class: 'field' }, - h('span', { class: 'lab' }, 'Regtest Esplora URL'), - h('input', { - type: 'text', class: 'mono-input', placeholder: 'http://localhost:3000', - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: getRegtestEsplora(), - onChange: (e) => { setRegtestEsplora(e.target.value.trim()); wallet.reloadExplorer(); }, - }), - h('div', { class: 'small faint' }, 'Esplora-compatible REST endpoint (coinos chopsticks/electrs).') - ) + ? h('div', { class: 'col', style: 'gap:10px' }, + h('label', { class: 'field' }, + h('span', { class: 'lab' }, 'Regtest Esplora URL'), + h('input', { + type: 'text', class: 'mono-input', placeholder: 'http://localhost:3000', + autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: getRegtestEsplora(), + onChange: (e) => { setRegtestEsplora(e.target.value.trim()); wallet.reloadExplorer(); }, + }), + h('div', { class: 'small faint' }, 'Esplora-compatible REST endpoint (coinos chopsticks/electrs).')), + h('label', { class: 'field' }, + h('span', { class: 'lab' }, 'Regtest realtime (Electrum WS)'), + h('input', { + type: 'text', class: 'mono-input', placeholder: 'ws://localhost:50003', + autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: getRegtestElectrumWs(), + onChange: (e) => { setRegtestElectrumWs(e.target.value.trim()); wallet.reloadExplorer(); }, + }), + h('div', { class: 'small faint' }, 'Electrum-over-WebSocket bridge (ews) for instant updates; falls back to polling if unreachable.'))) : null ); } diff --git a/tools/electrum-ws.test.js b/tools/electrum-ws.test.js new file mode 100644 index 0000000..1d5b5d0 --- /dev/null +++ b/tools/electrum-ws.test.js @@ -0,0 +1,44 @@ +// Verify the regtest electrum-ws bridge gives realtime push: subscribe to an +// address scripthash over ws://localhost:50003, fund it, expect a notification. +// Run (stack + ews up): bun tools/electrum-ws.test.js +import { $ } from 'bun'; +import { p2wpkh } from '@scure/btc-signer/payment'; +import * as btc from '@scure/btc-signer'; +import { secp256k1 } from '@noble/curves/secp256k1'; +import { sha256 } from '@noble/hashes/sha2'; +import { bytesToHex } from '@noble/hashes/utils'; + +const REGTEST = { ...btc.TEST_NETWORK, bech32: 'bcrt' }; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const bcli = (...a) => $`docker exec bc bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 ${a}`.text(); + +const pub = secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey(), true); +const pay = p2wpkh(pub, REGTEST); +const sh = sha256(pay.script).reverse(); // electrum scripthash (reversed) +const shHex = bytesToHex(sh); +console.log('address', pay.address, '\nscripthash', shHex); + +const ws = new WebSocket('ws://localhost:50003'); +let gotInitial = false, gotPush = false; +ws.onmessage = (ev) => { + for (const line of String(ev.data).split('\n')) { + if (!line.trim()) continue; + let m; try { m = JSON.parse(line); } catch { continue; } + if (m.id === 2) { gotInitial = true; console.log('subscribe ok (initial status:', m.result, ')'); } + if (m.method === 'blockchain.scripthash.subscribe') { gotPush = true; console.log('PUSH notification received:', JSON.stringify(m.params)); } + } +}; +await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); +ws.send(JSON.stringify({ id: 1, method: 'server.version', params: ['test', '1.4'] }) + '\n'); +ws.send(JSON.stringify({ id: 2, method: 'blockchain.scripthash.subscribe', params: [shHex] }) + '\n'); + +await sleep(1500); +if (!gotInitial) { console.log('FAIL — no subscribe response'); process.exit(1); } + +console.log('funding the address…'); +await bcli('-rpcwallet=coinos', 'sendtoaddress', pay.address, '0.005'); +await bcli('generatetoaddress', '1', (await bcli('-rpcwallet=coinos', 'getnewaddress')).trim()); + +for (let i = 0; i < 20 && !gotPush; i++) await sleep(500); +console.log(gotPush ? '\nPASS — realtime push delivered over the WS bridge' : '\nFAIL — no push within timeout'); +process.exit(gotPush ? 0 : 1); |
