aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/nwc.js11
-rw-r--r--tools/nwc-test.js14
2 files changed, 25 insertions, 0 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js
index f561a8a..20b0dd3 100644
--- a/src/features/nwc.js
+++ b/src/features/nwc.js
@@ -78,6 +78,10 @@ export function nwcFeature(ctx) {
function recordSpend(c, sat) {
const { today, spent } = spendRoom(c);
updateConn(c.id, { spentDate: today, spentToday: spent + sat, lastUsed: Date.now() });
+ // Publish the snapshot: budgets only bind across devices if the spend
+ // travels — and the payment just changed the ark state (vtxos, movement)
+ // that the user's other devices display.
+ try { wallet.saveCache(); } catch {}
}
// The relays advertised in the URI MUST be the ones we listen on, or a
@@ -226,6 +230,13 @@ export function nwcFeature(ctx) {
if (ev.pubkey !== c.clientPk) {
return console.log(`nwc: guard drop — pubkey mismatch got=${ev.pubkey.slice(0, 8)} want=${c.clientPk.slice(0, 8)}`);
}
+ // Re-resolve the connection from stored state: the closure's `c` is a
+ // snapshot from listen() time, so budget math computed from it restarts
+ // from a stale value on every request (spends never accumulate), and a
+ // connection revoked on another device would keep being served here.
+ const cur = (load().conns || []).find((x) => x.id === c.id);
+ if (!cur || cur.revoked) return console.log('nwc: guard drop — connection gone or revoked', c.id);
+ c = cur;
const scheme = ev.tags?.find((x) => x[0] === 'encryption')?.[1] === 'nip44_v2'
? 'nip44_v2' : 'nip04';
diff --git a/tools/nwc-test.js b/tools/nwc-test.js
index 64d1cd6..7b7f8c3 100644
--- a/tools/nwc-test.js
+++ b/tools/nwc-test.js
@@ -92,6 +92,20 @@ r = await request('pay_invoice', { invoice: 'lnbc50u1p4xu8whsp5jjnp3jzhjrgp3992d
check('over-limit payment refused', r?.error?.code === 'QUOTA_EXCEEDED', JSON.stringify(r?.error));
check('nothing was paid', paid.length === 0);
+console.log('\n[cumulative spend]');
+// a real 21-sat invoice (long expired — the mock seam never pays anything);
+// two payments must ACCUMULATE: recordSpend once restarted from a stale
+// snapshot of the connection, so only the last payment ever counted
+const INV21 = 'lnbc210n1p4xuk2wpp506wkjr0xk3677nu7je9c55vq4lzlkyd0ztcq2mlvumap0zpe3alqhp5ppg7g8qwpdv34hgpymhw446y37duzwcn388yp3pw05n7tlulyn2scqzysxqrrssrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur0dwyqqvusqqqqqqqlgqqqq86qqjqsp5dc0jrq94ke2f4dzx8c2dwqsc6a65eu56dt2j599l7kxp7q2hs6zq9qxpqysgqdjeft8gkl0uga24e502pvcp5vgsfap3dxuutcpgfaj33fffuqs9psmnrklshp3fg3py7vlnzsea90vj9ahqq5t9xuy67u3pk0sfnheqpn95f2g';
+r = await request('pay_invoice', { invoice: INV21 });
+check('first payment succeeds', !!r?.result?.preimage, JSON.stringify(r?.error || r));
+r = await request('pay_invoice', { invoice: INV21 });
+check('second payment succeeds', !!r?.result?.preimage, JSON.stringify(r?.error || r));
+{
+ const spent = JSON.parse(store['fs:nwc']).conns.find((x) => x.id === 'test1').spentToday;
+ check('spend accumulates across payments', spent === 52, String(spent)); // 2 × (25 + 1 fee) from the mock seam
+}
+
console.log('\n[nip04 fallback]');
r = await request('get_balance', {}, 'nip04');
check('nip04 request answered', r?.result?.balance === 42000*1000);