aboutsummaryrefslogtreecommitdiffstats
path: root/tools/nwc-sw-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 19:38:30 -0700
committerAdam Soltys <[email protected]>2026-08-01 19:38:30 -0700
commit348a57004f94dd0f58c4e70769e65ca7ea3f7619 (patch)
tree5f8cd2930abd9c1e05bb7c18e0bec239780281ef /tools/nwc-sw-test.js
parent9196f4804851a87c8356418b1c4cade6e87c54ba (diff)
downloadcoinosv3-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.js69
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);