From b73bb9ee51b9be33bc1859fd56d3e11905c3d456 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 19:02:25 -0700 Subject: Background NWC: the worker now answers — and pays — from the pouch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The no-tap half of background wallet-connect. The push payload carries the (encrypted, client-signed) request event; a bundled service-worker responder (src/sw-nwc.js -> nwc-respond.js) finds the matching pouch in IndexedDB, decrypts with the connection's service key, and answers get_info/get_balance/list_transactions from pouch state. pay_invoice pays through the swap bridge with an ArkManager running over the pouch's chain-6 key window (one HTLC lock, one HTTP swap), enforcing per-payment and daily budgets from the pouch's own spend log, checking the notifier's /answered endpoint so it never double-pays or shouts over an open device, and persisting the swap before handing it off so the app can refund a crashed flight. Anything it can't handle falls back to the wake-the-user notification. The notifier grows /publish (verified 23195s only, rate-limited) as the worker's reply pipe and /answered for race avoidance. The app gains pouch funding (mailbox-less arkoor self-send whose recipient bytes we keep), emptying, spend/budget reconciliation on open, and refund recovery with the pouch's own keys. Co-Authored-By: Claude Fable 5 --- src/nwc-pouch.js | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) (limited to 'src/nwc-pouch.js') diff --git a/src/nwc-pouch.js b/src/nwc-pouch.js index 0d4fcbc..187ab7a 100644 --- a/src/nwc-pouch.js +++ b/src/nwc-pouch.js @@ -73,6 +73,23 @@ const get = (s, k) => new Promise((res, rej) => { const r = s.get(k); r.onsucces export async function loadPouch(walletKey) { try { return (await tx('readonly', (s) => get(s, walletKey))) || null; } catch { return null; } } +// The service worker doesn't know wallet cache keys — it finds the right +// pouch by the connection's service pubkey across all of them. +export async function allPouches() { + try { + return await tx('readonly', (s) => new Promise((res, rej) => { + const out = []; + const cur = s.openCursor(); + cur.onsuccess = () => { + const c = cur.result; + if (!c) return res(out); + out.push({ walletKey: c.key, pouch: c.value }); + c.continue(); + }; + cur.onerror = () => rej(cur.error); + })); + } catch { return []; } +} export async function savePouch(walletKey, pouch) { return tx('readwrite', (s) => { s.put(pouch, walletKey); }); } @@ -80,25 +97,32 @@ export async function clearPouch(walletKey) { return tx('readwrite', (s) => { s.delete(walletKey); }); } +// The worker can't derive keys (no seed), so the pouch carries a fixed window +// of pre-derived chain-6 keys: funding uses low indices, the worker's change +// and HTLC-refund outputs allocate upward from POUCH_CHANGE_BASE. +export const POUCH_KEY_WINDOW = 50; +export const POUCH_CHANGE_BASE = 20; + // Everything the service worker needs to spend, and nothing more: the ASP -// endpoints, the pouch vtxos with their bytes, and the private keys for -// exactly those vtxos. -export function buildPouch({ arkUrl, esploraUrl, network, serverPubkey, vtxos, account, connections }) { +// endpoints, the bridge, the pouch vtxos with their bytes, and the private +// keys for exactly the pouch's key window. +export function buildPouch({ arkUrl, esploraUrl, network, serverPubkey, bridge, vtxos, account, connections }) { const keys = {}; - for (const v of vtxos) { - const k = pouchKey(account, v.keyIndex); - keys[String(v.keyIndex)] = hex.encode(k.privkey); + for (let i = 0; i < POUCH_KEY_WINDOW; i++) { + keys[String(i)] = hex.encode(pouchKey(account, i).privkey); } return { - v: 1, + v: 2, updated: Date.now(), ark: { arkUrl, esploraUrl, network, serverPubkey }, + bridge: bridge || null, // { url, token } — the worker pays through this // [{ id, bytes, keyIndex, amountSat, expiryHeight }] vtxos: vtxos.map((v) => ({ id: v.id, bytes: v.bytes, keyIndex: v.keyIndex, amountSat: v.amountSat, expiryHeight: v.expiryHeight, })), keys, + nextKeyIndex: POUCH_CHANGE_BASE, // service keys for the connections the SW may answer for, so it can // decrypt requests and sign replies without the wallet connections: (connections || []).map((c) => ({ -- cgit v1.2.3