diff options
| author | Adam Soltys <[email protected]> | 2026-08-22 09:10:10 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-22 09:10:10 -0700 |
| commit | 9963c4c45ccd33cc8588fd3c1cf816cf09a82c2b (patch) | |
| tree | cdc5d6ca684ae7e6e521c575a386b8b7c04bce71 /tools | |
| parent | 16a9e78080554774d8fec8a4f29d19f97fa3722a (diff) | |
| download | coinosv3-9963c4c45ccd33cc8588fd3c1cf816cf09a82c2b.tar.gz coinosv3-9963c4c45ccd33cc8588fd3c1cf816cf09a82c2b.zip | |
One record answers, and failure learns patience: a rewritten mirror disarms stale sibling copies of its connections, the responder pays from the record whose network the invoice names (standing down when none fits), a ghost coin reconciles and retries instead of dying, and error replies wait out a grace period so a sibling's success reaches the client first
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017Fc8SHJRvBBXgMCntjNCri
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/nwc-sw-test.js | 70 |
1 files changed, 67 insertions, 3 deletions
diff --git a/tools/nwc-sw-test.js b/tools/nwc-sw-test.js index 5a34c4e..acf3027 100644 --- a/tools/nwc-sw-test.js +++ b/tools/nwc-sw-test.js @@ -44,21 +44,24 @@ function makeReq(method, params = {}, scheme = 'nip44_v2', ageSec = 0) { }, clientSk); } -function harness({ rec = baseRec(), answered = false } = {}) { +function harness({ rec = baseRec(), recs = null, answered = false } = {}) { const published = []; const fetchFn = async (url, opts) => { if (url.includes('/publish')) { published.push(JSON.parse(opts.body).event); return { ok: true, json: async () => ({ ok: true }) }; } - if (url.includes('/answered')) return { ok: true, json: async () => ({ answered }) }; + if (url.includes('/answered')) { + return { ok: true, json: async () => ({ answered: typeof answered === 'function' ? answered() : answered }) }; + } throw new Error('unexpected fetch ' + url); }; const deps = { notifier: 'http://notif.test', fetchFn, - recordsFn: async () => [{ walletKey: 'w1', rec }], + recordsFn: async () => recs || [{ walletKey: 'w1', rec }], saveFn: async () => {}, + errGraceMs: 5, }; const decryptReply = (ev) => { const scheme = ev.tags.find((t) => t[0] === 'encryption')?.[1]; @@ -142,5 +145,66 @@ const INV21 = 'lnbc210n1p4xuk2wpp506wkjr0xk3677nu7je9c55vq4lzlkyd0ztcq2mlvumap0z check('exhausted key window → wake the user', handled === false); } +console.log('\n[network fit]'); +{ + // a mutinynet-era mirror must not answer a mainnet invoice with a nonsense + // error — it stands down (false → wake) and publishes nothing + const rec = baseRec(); + rec.ark.network = 'mutinynet'; + const { deps, published } = harness({ rec }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + check('wrong-network record stands down', handled === false && published.length === 0); +} +{ + // ...and stays silent when a sibling device already answered + const rec = baseRec(); + rec.ark.network = 'mutinynet'; + const { deps, published } = harness({ rec, answered: true }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + check('wrong-network + answered elsewhere → silent true', handled === true && published.length === 0); +} +{ + // two copies of the connection: the fresher mutinynet one must lose to the + // older mainnet one for a mainnet invoice. The mainnet copy has an empty + // mirror, so reaching it shows as a wake (false) with nothing published — + // the mutinynet copy would instead have published a network error. + const mut = baseRec(); mut.ark.network = 'mutinynet'; mut.updated = Date.now(); + const main = baseRec(); main.updated = Date.now() - 60_000; main.mgr.vtxos = []; + const { deps, published } = harness({ recs: [{ walletKey: 'wMut', rec: mut }, { walletKey: 'wMain', rec: main }] }); + const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('pay_invoice', { invoice: INV21 })) }, deps); + check('invoice network picks the fitting record', handled === false && published.length === 0); +} +{ + // non-pay methods answer from the freshest copy + const stale = baseRec(); stale.updated = 1; stale.mgr.vtxos = []; + const fresh = baseRec(); fresh.updated = Date.now(); + const { deps, published, decryptReply } = harness({ recs: [{ walletKey: 'wOld', rec: stale }, { walletKey: 'wNew', rec: fresh }] }); + await respondFromBg({ type: 'nwc', event: ev(makeReq('get_balance')) }, deps); + check('freshest record answers get_balance', decryptReply(published[0]).result?.balance === 5000000); +} + +console.log('\n[sibling record hygiene]'); +{ + const { disarmSiblingRecords } = await import('../src/nwc-bg.js'); + const live = baseRec(); + const staleShared = baseRec(); // same connection servicePk — must be disarmed + const unrelated = baseRec(); + unrelated.connections = [{ ...conn, id: 'c2', servicePk: 'f'.repeat(64) }]; + const legacy = { v: 2, connections: [conn] }; // pre-v3 pouch: left for its sweep + const saved = {}; + const n = await disarmSiblingRecords('wLive', live, { + loadAll: async () => [ + { walletKey: 'wLive', rec: live }, + { walletKey: 'wStale', rec: staleShared }, + { walletKey: 'wOther', rec: unrelated }, + { walletKey: 'wLegacy', rec: legacy }, + ], + save: async (k, r) => { saved[k] = r; }, + }); + check('shared connection stripped from the stale record', n === 1 && saved.wStale && saved.wStale.connections.length === 0); + check('own, unrelated and legacy records untouched', !saved.wLive && !saved.wOther && !saved.wLegacy); + check('stale record keeps its coin state', saved.wStale.mgr.vtxos.length === 1); +} + console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ background responder behaves'); process.exit(fails ? 1 : 0); |
