diff options
| author | Adam Soltys <[email protected]> | 2026-08-01 19:38:30 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-01 19:38:30 -0700 |
| commit | 348a57004f94dd0f58c4e70769e65ca7ea3f7619 (patch) | |
| tree | 5f8cd2930abd9c1e05bb7c18e0bec239780281ef /src/features/nwc.js | |
| parent | 9196f4804851a87c8356418b1c4cade6e87c54ba (diff) | |
| download | coinosv3-348a57004f94dd0f58c4e70769e65ca7ea3f7619.tar.gz coinosv3-348a57004f94dd0f58c4e70769e65ca7ea3f7619.zip | |
Simplify: one Lightning path, one balance, fewer features
Dropped: silent payments (feature, wallet chains, scan worker, indexer
config), Boltz submarine/reverse swaps (feature + swap.js + tools), the
third-party ark swap bridge (config, client flows, settings card — the
ASP's own Lightning send is the one path now), and the Consolidate
coins settings action.
The NWC pouch is gone with them: background answering now spends the
WHOLE ark balance. The IndexedDB record mirrors the real manager state
plus the chain-3 keys for its live coins and a change-index window; the
worker pays through the ASP exactly like the open app, and the app
folds the worker's spends, change, and key allocations back in on
connect (additive merge + reconcile against the server). A leftover
chain-6 pouch from the short-lived pouch era is swept home
automatically using the keys stored in its own record. The stated
tradeoff: same-origin script access now reaches the ark balance, not a
carve-out — the ark balance is walking-around money, the on-chain
wallet's keys are never mirrored, and per-connection NWC budgets remain
the enforced ceiling.
Bundle: 726KB -> 602KB.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/features/nwc.js')
| -rw-r--r-- | src/features/nwc.js | 110 |
1 files changed, 34 insertions, 76 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js index a91768d..f7e42c8 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -7,10 +7,8 @@ // at any time. // // hal holds no native Lightning balance, so every Lightning operation here -// runs over Ark: paying goes through the swap bridge when one is configured -// and falls back to the ASP's own lightning-send, and receiving mints an -// invoice on the ASP. That means NWC availability tracks Ark availability — -// no Ark server, no wallet service. +// runs over Ark: the ASP pays and mints invoices. That means NWC +// availability tracks Ark availability — no Ark server, no wallet service. // // Shape of the protocol (mirrors coinos-server/lib/nwc.ts): // kind 13194 info event, content = space-separated supported methods @@ -18,8 +16,9 @@ // kind 23195 response (wallet -> client), same encryption scheme back // Encryption is nip04 unless the request carries `["encryption","nip44_v2"]`. // -// IMPORTANT limitation, surfaced in the UI: hal is a web wallet, so it only -// answers while a tab is open. It is not an always-on wallet service. +// While a tab is open it answers directly; with background answering on, +// the service worker answers from a mirrored state (see nwc-bg.js) so the +// wallet keeps working closed. import { hex } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; @@ -30,7 +29,7 @@ import { import { t } from '../i18n.js'; import { qrSvg } from '../qr.js'; import { getNetwork } from '../api.js'; -import { buildPouch, loadPouch, savePouch, clearPouch, pouchKey } from '../nwc-pouch.js'; +import { loadBg, saveBg, clearBg } from '../nwc-bg.js'; import { maybeBolt11 } from '../ark/lightning.js'; const REQ_KIND = 23194; @@ -82,6 +81,7 @@ export function nwcFeature(ctx) { // travels — and the payment just changed the ark state (vtxos, movement) // that the user's other devices display. try { wallet.saveCache(); } catch {} + writeBg(); // keep the worker's mirror as fresh as the state it spends } // The relays advertised in the URI MUST be the ones we listen on, or a @@ -449,7 +449,7 @@ export function nwcFeature(ctx) { }); if (!r.ok) throw new Error(`notifier refused: ${r.status}`); const st = load(); st.background = true; save(st); - await writePouch(); + writeBg(); return true; } @@ -493,56 +493,45 @@ export function nwcFeature(ctx) { const st = load(); st.background = false; save(st); // no background answering means no reason to keep spendable keys where a // worker can read them - try { await clearPouch(wallet._cacheKey()); } catch {} + try { await clearBg(wallet._cacheKey()); } catch {} } - // (Re)write the pouch record the worker answers from: connections' service - // keys, ASP + bridge endpoints, the chain-6 key window — while PRESERVING - // anything the pouch already holds (its coins, the worker's spend log and - // manager state), which a rebuild must never wipe. - async function writePouch() { + // (Re)write the background record the worker answers from: the full ark + // state mirror plus this feature's connections. The ark feature owns the + // heavy lifting; this decides WHEN (background answering on). + let bgTimer = null; + function writeBg() { if (!load().background) return; - const cfg = hook('arkPouchContext'); - if (!cfg) return; - try { - const prev = await loadPouch(wallet._cacheKey()); - const fresh = buildPouch({ - ...cfg, vtxos: prev?.vtxos || [], account: wallet.account(), connections: conns(), - }); - if (prev) { - fresh.mgr = prev.mgr; - fresh.spends = prev.spends; - fresh.swaps = prev.swaps; - fresh.fundSeq = prev.fundSeq; - if (prev.mgr?.nextKeyIndex) fresh.nextKeyIndex = prev.mgr.nextKeyIndex; - } - await savePouch(wallet._cacheKey(), fresh); - pouchSat = await hook('arkPouchBalance'); - } catch (e) { console.warn('nwc: could not write pouch', e.message); } + clearTimeout(bgTimer); + bgTimer = setTimeout(async () => { + try { + if (!hook('arkBgReady')) return; + await hook('arkBgWrite', conns()); + } catch (e) { console.warn('nwc: could not write background state', e.message); } + }, 1500); } // When the app opens after the worker has been answering: absorb its spends - // into the budgets and history, and rescue any swap it left hanging. - let pouchSat = null; // cached for the (synchronous) settings card - async function reconcilePouch() { + // into the budgets and history. (The ark feature separately merges the + // worker's coin state on connect.) + async function reconcileBg() { try { if (!load().background) return; - await hook('arkPouchRecover'); - const pouch = await loadPouch(wallet._cacheKey()); - if (!pouch) return; - const fresh = (pouch.spends || []).filter((s) => !s.absorbed); + const rec = await loadBg(wallet._cacheKey()); + if (!rec) return; + const fresh = (rec.spends || []).filter((s) => !s.absorbed); if (fresh.length) { for (const s of fresh) { const c = conns().find((x) => x.id === s.connId); if (c) recordSpend(c, (s.amountSat || 0) + (s.feeSat || 0)); s.absorbed = true; } - await hook('arkPouchNoteSpends', fresh); - await savePouch(wallet._cacheKey(), pouch); + await hook('arkBgNoteSpends', fresh); + await saveBg(wallet._cacheKey(), rec); } - pouchSat = await hook('arkPouchBalance'); + writeBg(); // refresh the mirror after the merge settled render(); - } catch (e) { console.warn('nwc: pouch reconcile failed', e.message); } + } catch (e) { console.warn('nwc: background reconcile failed', e.message); } } // An open window handles the request itself; the worker just nudges it. @@ -561,7 +550,7 @@ export function nwcFeature(ctx) { let watchdog = null; function startWatchdog() { if (watchdog) return; - watchdog = setInterval(() => { if (conns().length) listen(); }, 90 * 1000); + watchdog = setInterval(() => { if (conns().length) { listen(); writeBg(); } }, 90 * 1000); if (typeof window !== 'undefined') { window.addEventListener('online', () => listen()); document.addEventListener('visibilitychange', () => { @@ -615,42 +604,11 @@ export function nwcFeature(ctx) { h('button', { class: 'btn-sm', onClick: async () => { try { if (load().background) { await disableBackground(); toast(t('nwcBackgroundOff')); } - else { await enableBackground(); await reconcilePouch(); toast(t('nwcBackgroundOn')); } + else { await enableBackground(); await reconcileBg(); toast(t('nwcBackgroundOn')); } } catch (e) { toast(e.message); } render(); } }, load().background ? t('nwcOn') : t('nwcOff'))) : null, - list.length && load().background - ? h('div', { class: 'col', style: 'gap:6px' }, - h('div', { class: 'row between' }, - h('span', { class: 'small' }, t('nwcPouch')), - h('span', { class: 'small' }, pouchSat == null ? '…' : fmtAmount(pouchSat))), - h('p', { class: 'small faint', style: 'margin:0' }, t('nwcPouchDesc')), - h('div', { class: 'row gap6' }, - h('input', { type: 'number', min: '1', placeholder: t('nwcPouchAmount'), value: ui.nwcPouchAmt || '', - style: 'flex:1', onInput: (e) => { ui.nwcPouchAmt = e.target.value; } }), - h('button', { class: 'btn-sm', onClick: async () => { - const sat = parseInt(ui.nwcPouchAmt, 10); - if (!sat) return; - try { - const moved = await hook('arkPouchFund', sat); - ui.nwcPouchAmt = ''; - pouchSat = await hook('arkPouchBalance'); - toast(t('nwcPouchFunded', { sats: fmtAmount(moved) })); - } catch (e) { toast(e.message); } - render(); - } }, t('nwcPouchFund')), - pouchSat > 0 - ? h('button', { class: 'btn-ghost btn-sm', onClick: async () => { - try { - const moved = await hook('arkPouchEmpty'); - pouchSat = await hook('arkPouchBalance'); - toast(t('nwcPouchEmptied', { sats: fmtAmount(moved) })); - } catch (e) { toast(e.message); } - render(); - } }, t('nwcPouchEmpty')) - : null)) - : null, st ? h('div', { class: 'col', style: 'gap:6px;border-top:1px solid var(--border,rgba(128,128,128,.2));padding-top:8px' }, h('input', { type: 'text', placeholder: t('nwcAppName'), value: st.name, @@ -677,7 +635,7 @@ export function nwcFeature(ctx) { return { id: 'nwc', - init() { listen(); startWatchdog(); refreshRegistration(); reconcilePouch(); }, + init() { listen(); startWatchdog(); refreshRegistration(); reconcileBg(); }, stop() { stop(); if (watchdog) { clearInterval(watchdog); watchdog = null; } }, settingsCards() { return [nwcCard()]; }, }; |
