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 /tools/nwc-sw-test.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 'tools/nwc-sw-test.js')
| -rw-r--r-- | tools/nwc-sw-test.js | 69 |
1 files changed, 39 insertions, 30 deletions
diff --git a/tools/nwc-sw-test.js b/tools/nwc-sw-test.js index b9826dd..69ed150 100644 --- a/tools/nwc-sw-test.js +++ b/tools/nwc-sw-test.js @@ -1,11 +1,11 @@ -// The service worker's pouch responder, headless: request decrypt, balance, +// The service worker's background responder, headless: request decrypt, balance, // budgets, answered-elsewhere suppression, fallbacks. The bridge/ASP pay path // itself is covered by the manager and bridge tests. Run: bun tools/nwc-sw-test.js import { hex } from '@scure/base'; import * as nip04 from 'nostr-tools/nip04'; import * as nip44 from 'nostr-tools/nip44'; import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; -import { respondFromPouch } from '../src/nwc-respond.js'; +import { respondFromBg } from '../src/nwc-respond.js'; let fails = 0; const check = (name, ok, detail = '') => { @@ -19,13 +19,15 @@ const conn = { id: 'c1', servicePk: getPublicKey(serviceSk), serviceSk: hex.encode(serviceSk), clientPk: getPublicKey(clientSk), maxSat: 1000, dailySat: 2000, }; -const basePouch = () => ({ - v: 2, +const baseRec = () => ({ + v: 3, ark: { arkUrl: 'http://ark.test', esploraUrl: 'http://esp.test', network: 'mainnet', serverPubkey: 'ab'.repeat(32) }, - bridge: { url: 'http://bridge.test', token: '' }, - vtxos: [{ id: 'v1', bytes: '00', keyIndex: 0, amountSat: 5000, expiryHeight: 1 }], - keys: { 0: hex.encode(generateSecretKey()) }, - nextKeyIndex: 20, + mgr: { + v: 1, serverPubkey: 'ab'.repeat(32), nextKeyIndex: 5, + vtxos: [{ id: 'v1', bytes: '00', keyIndex: 1, amountSat: 5000, expiryHeight: 1, state: 'spendable' }], + actions: [], movements: [], + }, + keys: { 1: hex.encode(generateSecretKey()), 5: hex.encode(generateSecretKey()) }, connections: [conn], spends: [], }); @@ -42,7 +44,7 @@ function makeReq(method, params = {}, scheme = 'nip44_v2', ageSec = 0) { }, clientSk); } -function harness({ pouch = basePouch(), answered = false } = {}) { +function harness({ rec = baseRec(), answered = false } = {}) { const published = []; const fetchFn = async (url, opts) => { if (url.includes('/publish')) { @@ -55,7 +57,7 @@ function harness({ pouch = basePouch(), answered = false } = {}) { const deps = { notifier: 'http://notif.test', fetchFn, - pouchesFn: async () => [{ walletKey: 'w1', pouch }], + recordsFn: async () => [{ walletKey: 'w1', rec }], saveFn: async () => {}, }; const decryptReply = (ev) => { @@ -72,14 +74,14 @@ const ev = (e) => JSON.parse(JSON.stringify(e)); // strip verified symbol like a console.log('[balance + info]'); { const { deps, published, decryptReply } = harness(); - const handled = await respondFromPouch({ type: 'nwc', event: ev(makeReq('get_balance')) }, deps); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('get_balance')) }, deps); check('get_balance handled', handled === true); const r = decryptReply(published[0]); check('pouch balance in msat', r.result?.balance === 5000000, JSON.stringify(r)); } { const { deps, published, decryptReply } = harness(); - await respondFromPouch({ type: 'nwc', event: ev(makeReq('get_info', {}, 'nip04')) }, deps); + await respondFromBg({ type: 'nwc', event: ev(makeReq('get_info', {}, 'nip04')) }, deps); const r = decryptReply(published[0]); check('nip04 get_info answered in nip04', r.result?.alias === 'Hal' && published[0].tags.find((t) => t[0] === 'encryption')[1] === 'nip04'); } @@ -88,7 +90,7 @@ console.log('\n[guards]'); { const { deps } = harness(); const stale = ev(makeReq('get_balance', {}, 'nip44_v2', 300)); - check('stale request refused', (await respondFromPouch({ type: 'nwc', event: stale }, deps)) === false); + check('stale request refused', (await respondFromBg({ type: 'nwc', event: stale }, deps)) === false); } { const { deps } = harness(); @@ -96,42 +98,49 @@ console.log('\n[guards]'); kind: 23194, created_at: Math.floor(Date.now() / 1000), tags: [['p', conn.servicePk]], content: 'junk', }, generateSecretKey())); - check('foreign signer refused', (await respondFromPouch({ type: 'nwc', event: foreign }, deps)) === false); + check('foreign signer refused', (await respondFromBg({ type: 'nwc', event: foreign }, deps)) === false); } { const { deps } = harness(); - check('no payload event → wake the user', (await respondFromPouch({ type: 'nwc', servicePubkey: conn.servicePk }, deps)) === false); + check('no payload event → wake the user', (await respondFromBg({ type: 'nwc', servicePubkey: conn.servicePk }, deps)) === false); } console.log('\n[pay budgets]'); // a real (long-expired) 21-sat invoice — decode only, nothing is paid const INV21 = 'lnbc210n1p4xuk2wpp506wkjr0xk3677nu7je9c55vq4lzlkyd0ztcq2mlvumap0zpe3alqhp5ppg7g8qwpdv34hgpymhw446y37duzwcn388yp3pw05n7tlulyn2scqzysxqrrssrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur0dwyqqvusqqqqqqqlgqqqq86qqjqsp5dc0jrq94ke2f4dzx8c2dwqsc6a65eu56dt2j599l7kxp7q2hs6zq9qxpqysgqdjeft8gkl0uga24e502pvcp5vgsfap3dxuutcpgfaj33fffuqs9psmnrklshp3fg3py7vlnzsea90vj9ahqq5t9xuy67u3pk0sfnheqpn95f2g'; { - const pouch = basePouch(); - pouch.connections = [{ ...conn, maxSat: 10 }]; - const { deps, published, decryptReply } = harness({ pouch }); - const handled = await respondFromPouch({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + const rec = baseRec(); + rec.connections = [{ ...conn, maxSat: 10 }]; + const { deps, published, decryptReply } = harness({ rec }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); check('over per-payment limit refused', handled && decryptReply(published[0]).error?.code === 'QUOTA_EXCEEDED'); } { - const pouch = basePouch(); - pouch.spends = [{ connId: 'c1', amountSat: 1990, feeSat: 0, ts: Date.now() }]; - const { deps, published, decryptReply } = harness({ pouch }); - const handled = await respondFromPouch({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + const rec = baseRec(); + rec.spends = [{ connId: 'c1', amountSat: 1990, feeSat: 0, ts: Date.now() }]; + const { deps, published, decryptReply } = harness({ rec }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); check('over daily budget refused', handled && decryptReply(published[0]).error?.code === 'QUOTA_EXCEEDED'); } { const { deps, published } = harness({ answered: true }); - const handled = await respondFromPouch({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); check('answered elsewhere → silent', handled === true && published.length === 0); } { - const pouch = basePouch(); - pouch.bridge = null; - const { deps } = harness({ pouch }); - const handled = await respondFromPouch({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); - check('no bridge → wake the user', handled === false); + const rec = baseRec(); + rec.mgr.vtxos = []; // nothing spendable in the mirror + const { deps } = harness({ rec }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + check('empty mirror → wake the user', handled === false); +} +{ + const rec = baseRec(); + delete rec.keys['5']; // change index outside the mirrored window + const { deps } = harness({ rec }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + check('exhausted key window → wake the user', handled === false); } -console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ pouch responder behaves'); +console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ background responder behaves'); process.exit(fails ? 1 : 0); |
