aboutsummaryrefslogtreecommitdiffstats
path: root/src/nwc-pouch.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 19:02:25 -0700
committerAdam Soltys <[email protected]>2026-08-01 19:02:25 -0700
commitb73bb9ee51b9be33bc1859fd56d3e11905c3d456 (patch)
tree8a008d57e450961d1ab203a663e9d8fa233a2686 /src/nwc-pouch.js
parent2a7a825ce5850a2a836effae820adbae0b66d093 (diff)
downloadcoinosv3-b73bb9ee51b9be33bc1859fd56d3e11905c3d456.tar.gz
coinosv3-b73bb9ee51b9be33bc1859fd56d3e11905c3d456.zip
Background NWC: the worker now answers — and pays — from the pouch
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 <[email protected]>
Diffstat (limited to 'src/nwc-pouch.js')
-rw-r--r--src/nwc-pouch.js38
1 files changed, 31 insertions, 7 deletions
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) => ({