aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-02 07:19:29 -0700
committerAdam Soltys <[email protected]>2026-10-02 07:19:29 -0700
commite8d82b8b23c271aa57134d762e14fd6dcbed9c7a (patch)
tree75ce06480f1e65552e43073647b5110ec7e76a63 /tools
parentbaef720df9e34be38fc2e5b4e4837674121e5c59 (diff)
downloadcoinosv3-e8d82b8b23c271aa57134d762e14fd6dcbed9c7a.tar.gz
coinosv3-e8d82b8b23c271aa57134d762e14fd6dcbed9c7a.zip
NWC: lookup_invoice, so BTCPay Server can connect
BTCPay's NWC connector requires get_info, make_invoice, lookup_invoice and list_transactions; we never offered lookup_invoice, so it refused the connection string. - ArkManager.lnLookup answers lookup_invoice from wallet state (by hash or bolt11, either direction; pending/settled/expired/failed, preimage once claimed). A retry's several actions on one hash defer to the furthest. - The open app pushes a pending receive along before answering, so a merchant polling sees it settle. - The service worker now answers make_invoice and lookup_invoice too, and claims a paid invoice's HTLCs during the lookup: a BTCPay sale settles with the wallet closed. Its list_transactions reports receives as well. - get_info and the 13194 info event list the new method. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
Diffstat (limited to 'tools')
-rw-r--r--tools/ln-lookup-test.js38
-rw-r--r--tools/nwc-sw-test.js30
-rw-r--r--tools/nwc-test.js11
3 files changed, 79 insertions, 0 deletions
diff --git a/tools/ln-lookup-test.js b/tools/ln-lookup-test.js
new file mode 100644
index 0000000..d825d4b
--- /dev/null
+++ b/tools/ln-lookup-test.js
@@ -0,0 +1,38 @@
+// ArkManager.lnLookup — the NIP-47 lookup_invoice answer, from wallet state.
+// bun tools/ln-lookup-test.js
+import { hex } from '@scure/base';
+import { ArkManager } from '../src/ark/manager.js';
+import { decodeBolt11 } from '../src/ark/lightning.js';
+
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+const INV = 'lnbc210n1p4xuk2wpp506wkjr0xk3677nu7je9c55vq4lzlkyd0ztcq2mlvumap0zpe3alqhp5ppg7g8qwpdv34hgpymhw446y37duzwcn388yp3pw05n7tlulyn2scqzysxqrrssrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur0dwyqqvusqqqqqqqlgqqqq86qqjqsp5dc0jrq94ke2f4dzx8c2dwqsc6a65eu56dt2j599l7kxp7q2hs6zq9qxpqysgqdjeft8gkl0uga24e502pvcp5vgsfap3dxuutcpgfaj33fffuqs9psmnrklshp3fg3py7vlnzsea90vj9ahqq5t9xuy67u3pk0sfnheqpn95f2g';
+const dec = decodeBolt11(INV);
+const H = dec.paymentHash;
+const pre = new Uint8Array(32).fill(7);
+const mgr = (state) => {
+ const m = Object.create(ArkManager.prototype);
+ m.state = { actions: [], movements: [], ...state };
+ m._lnPreimage = () => pre;
+ return m;
+};
+const recv = (step) => ({ id: `lnrecv-${Date.now()}`, type: 'ln-recv', step, paymentHash: H, preimageIndex: 3, amountSat: 21, invoice: INV, expiresAt: dec.expiresAt });
+
+check('unknown hash → null', mgr({}).lnLookup({ paymentHash: 'ab'.repeat(32) }) === null);
+check('no hash or invoice → null', mgr({}).lnLookup({}) === null);
+let t = mgr({ actions: [recv('awaiting')] }).lnLookup({ paymentHash: H });
+check('an awaiting receive past its expiry reads expired', t.type === 'incoming' && t.state === 'expired' && !t.preimage, JSON.stringify(t));
+t = mgr({ actions: [{ ...recv('awaiting'), invoice: INV }] }).lnLookup({ invoice: INV });
+check('found by bolt11 as well as hash', t && t.payment_hash === H);
+t = mgr({ actions: [recv('htlcsReady')] }).lnLookup({ paymentHash: H });
+check('HTLCs in, not yet claimed → pending, no preimage', t.state === 'pending' && !t.preimage, t.state);
+t = mgr({ actions: [recv('done')], movements: [{ type: 'ln-receive', status: 'complete', invoice: INV, amountSat: 21, ts: 1700000000000 }] }).lnLookup({ paymentHash: H.toUpperCase() });
+check('claimed → settled with the preimage', t.state === 'settled' && t.preimage === hex.encode(pre) && t.amount === 21000 && t.settled_at === 1700000000, JSON.stringify(t));
+t = mgr({ actions: [{ id: 'lnpay-1700000000000', type: 'ln-pay', step: 'done', paymentHash: H, invoice: INV, amountSat: 21, feeSat: 1, preimage: 'ab'.repeat(32) }] }).lnLookup({ paymentHash: H });
+check('a paid invoice reads outgoing, settled, with fees', t.type === 'outgoing' && t.state === 'settled' && t.preimage === 'ab'.repeat(32) && t.fees_paid === 1000, JSON.stringify(t));
+t = mgr({ actions: [{ id: 'lnpay-1', type: 'ln-pay', step: 'failed', paymentHash: H, invoice: INV, amountSat: 21 }] }).lnLookup({ paymentHash: H });
+check('a failed send reads failed', t.state === 'failed');
+t = mgr({ movements: [{ type: 'ln-send', status: 'complete', invoice: INV, preimage: 'cd'.repeat(32), paymentHash: H, amountSat: 21, ts: 1 }] }).lnLookup({ paymentHash: H });
+check('history alone (action pruned) still answers', t && t.type === 'outgoing' && t.state === 'settled' && t.preimage === 'cd'.repeat(32));
+console.log(ok ? '\n✅ lnLookup answers lookup_invoice' : '\n❌ failures');
+process.exit(ok ? 0 : 1);
diff --git a/tools/nwc-sw-test.js b/tools/nwc-sw-test.js
index ae79e18..62248a4 100644
--- a/tools/nwc-sw-test.js
+++ b/tools/nwc-sw-test.js
@@ -92,6 +92,36 @@ console.log('[balance + info]');
check('nip04 get_info answered in nip04', r.result?.alias === 'Coinos' && published[0].tags.find((t) => t[0] === 'encryption')[1] === 'nip04');
}
+console.log('\n[merchant methods (BTCPay)]');
+{
+ const { deps, published, decryptReply } = harness();
+ await respondFromBg({ type: 'nwc', event: ev(makeReq('get_info')) }, deps);
+ const m = decryptReply(published[0]).result?.methods || [];
+ check('get_info has every method BTCPay requires', ['get_info', 'make_invoice', 'lookup_invoice', 'list_transactions'].every((x) => m.includes(x)), JSON.stringify(m));
+}
+{
+ const INV = 'lnbc210n1p4xuk2wpp506wkjr0xk3677nu7je9c55vq4lzlkyd0ztcq2mlvumap0zpe3alqhp5ppg7g8qwpdv34hgpymhw446y37duzwcn388yp3pw05n7tlulyn2scqzysxqrrssrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur0dwyqqvusqqqqqqqlgqqqq86qqjqsp5dc0jrq94ke2f4dzx8c2dwqsc6a65eu56dt2j599l7kxp7q2hs6zq9qxpqysgqdjeft8gkl0uga24e502pvcp5vgsfap3dxuutcpgfaj33fffuqs9psmnrklshp3fg3py7vlnzsea90vj9ahqq5t9xuy67u3pk0sfnheqpn95f2g';
+ const H = '7e9d690de6b475ef4f9e964b8a5180afc5fb11af12f0056fece6fa1788398f7e';
+ const rec = baseRec();
+ const k5 = generateSecretKey();
+ rec.keys5 = { 0: hex.encode(k5) };
+ rec.mgr.actions.push({ id: 'lnrecv-1700000000000', type: 'ln-recv', step: 'done', paymentHash: H, preimageIndex: 0, amountSat: 21, invoice: INV });
+ rec.mgr.movements.push({ type: 'ln-receive', status: 'complete', amountSat: 21, invoice: INV, ts: 1700000000000 });
+ const { deps, published, decryptReply } = harness({ rec });
+ const handled = await respondFromBg({ type: 'nwc', event: ev(makeReq('lookup_invoice', { payment_hash: H })) }, deps);
+ const r = decryptReply(published[0]);
+ check('lookup_invoice of a claimed receive → settled, with the preimage, no ASP needed', handled === true && r.result?.state === 'settled' && r.result?.preimage?.length === 64, JSON.stringify(r.result || r.error));
+ await respondFromBg({ type: 'nwc', event: ev(makeReq('lookup_invoice', { payment_hash: 'ab'.repeat(32) })) }, deps);
+ check('lookup_invoice of an unknown hash → NOT_FOUND', decryptReply(published[1]).error?.code === 'NOT_FOUND');
+ await respondFromBg({ type: 'nwc', event: ev(makeReq('list_transactions')) }, deps);
+ const txs = decryptReply(published[2]).result?.transactions || [];
+ check('list_transactions includes receives', txs.some((t) => t.type === 'incoming' && t.amount === 21000), JSON.stringify(txs));
+}
+{
+ const { deps } = harness(); // a mirror without receive keys
+ check('make_invoice on an old mirror → wake the user', (await respondFromBg({ type: 'nwc', event: ev(makeReq('make_invoice', { amount: 21000 })) }, deps)) === false);
+}
+
console.log('\n[guards]');
{
const { deps } = harness();
diff --git a/tools/nwc-test.js b/tools/nwc-test.js
index 9189d52..848b2c6 100644
--- a/tools/nwc-test.js
+++ b/tools/nwc-test.js
@@ -40,6 +40,7 @@ const hooks = {
arkSpendableSat: () => 42000,
arkPayInvoice: async (inv, opts) => { paid.push({ inv, opts }); return { preimage: 'ab'.repeat(32), feeSat: 1, amountSat: 25 }; },
arkMakeInvoice: async (sat) => ({ invoice: 'lnbc'+sat, paymentHash: 'cd'.repeat(32), amountSat: sat }),
+ arkLnLookup: async (q) => (q.paymentHash === 'cd'.repeat(32) ? { type: 'incoming', state: 'settled', payment_hash: q.paymentHash, preimage: '12'.repeat(32), amount: 21000 } : null),
arkMovements: () => ([{ type:'ln-send', status:'complete', amountSat: 25, ts: Date.now(), invoice:'lnbc250n1', preimage:'ef'.repeat(32) }]),
};
const ctx = { h:()=>null, ui:{}, render:()=>{}, wallet, hook:(n,...a)=>hooks[n]?hooks[n](...a):null,
@@ -162,6 +163,16 @@ check('nip04 request answered', r?.result?.balance === 42000*1000);
console.log('\n[other methods]');
r = await request('make_invoice', { amount: 21000 });
check('make_invoice returns a bolt11', !!r?.result?.invoice, r?.result?.invoice);
+r = await request('lookup_invoice', { payment_hash: 'cd'.repeat(32) });
+check('lookup_invoice reports a paid invoice settled', r?.result?.state === 'settled' && r?.result?.preimage === '12'.repeat(32), JSON.stringify(r?.result));
+r = await request('lookup_invoice', { payment_hash: 'ee'.repeat(32) });
+check('lookup_invoice of an unknown invoice is NOT_FOUND', r?.error?.code === 'NOT_FOUND');
+r = await request('get_info', {});
+// BTCPay Server's NWC connector refuses a wallet missing any of these
+const btcpay = ['get_info', 'make_invoice', 'lookup_invoice', 'list_transactions'];
+check('get_info has every method BTCPay requires', btcpay.every((m) => r?.result?.methods?.includes(m)), JSON.stringify(r?.result?.methods));
+const info = published.find((e) => e.kind === 13194);
+check('the 13194 info event lists them too', info && btcpay.every((m) => info.content.split(' ').includes(m)), info?.content);
r = await request('list_transactions', {});
check('list_transactions returns history', Array.isArray(r?.result?.transactions) && r.result.transactions.length === 1);
r = await request('pay_keysend', {});