aboutsummaryrefslogtreecommitdiffstats
path: root/tools/lnurl-auth-unit-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-15 15:24:25 -0700
committerAdam Soltys <[email protected]>2026-10-15 15:24:25 -0700
commit38ce0b441505d0bdd048d38c82ac95afe4d3b407 (patch)
tree111711030a02cd8eec42c8e75c5de8c8ed1bacc5 /tools/lnurl-auth-unit-test.js
parent46829a7a54bf21138d8bb068966f7f083ac8df4c (diff)
downloadcoinosv3-master.tar.gz
coinosv3-master.zip
Lightning withdraw (LNURL-withdraw): a site's withdraw lnurl in Send gets paid to you; the media pager says when a picture can't be loaded. (1) LUD-03 (Adam, 2026-10-16: "add lnurlw support"): an lnurl whose terms are a withdrawRequest — found when the pay fetch meets one (fetchPayParams throws with the terms attached, resolve() takes them up) — shows the site, their range (or the fixed amount) and description, the amount field at their max; Withdraw makes an invoice over Ark for the amount (arkLnInvoice), hands it to their callback with k1 (submitWithdraw), and watches it (arkLnWatch): "has your invoice for…", then "Received …" on settlement; their refusal in their words, with Try again; Back before it is paid stops watching. An amount outside their range is told the range, nothing sent; a watch-only device says it can't receive. LUD-17 schemes lnurlw:// and lnurlp:// (and lightning: before them) are the URL over https, http on an onion (parseZapTarget). (2) A pager slide whose picture can't load — the server copy and then the original both failing (isolabellart's relay answering 502 that afternoon: the broken-picture glyph sat in the corner of a black slide) — hides the <img> and says "This picture couldn't be loaded" with the host, caption and controls kept (mp-gone / .mp-missing). tools/lnurl-auth-unit-test.js (lnurlw:// and lnurlp://, an onion, the terms, a pay fetch that finds a withdraw, the callback's k1 and pr, a refusal); tools/lnurl-withdraw-test.js (the zaps feature against a faked Ark and a stand-in site: the terms, max prefilled, out of range refused unsent, the invoice at the callback and watched, Received on settlement, a fixed amount with no field, Back unwatches, a refusal's reason); tools/pager-gone-picture-test.js; lnurl-auth and feed-picture-pager tests pass.HEADmaster
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
Diffstat (limited to 'tools/lnurl-auth-unit-test.js')
-rw-r--r--tools/lnurl-auth-unit-test.js21
1 files changed, 21 insertions, 0 deletions
diff --git a/tools/lnurl-auth-unit-test.js b/tools/lnurl-auth-unit-test.js
index 0707a8e..c9989be 100644
--- a/tools/lnurl-auth-unit-test.js
+++ b/tools/lnurl-auth-unit-test.js
@@ -40,3 +40,24 @@ assert.match(got.sig, /^30[0-9a-f]+$/, 'a DER signature');
globalThis.fetch = async () => new Response(JSON.stringify({ status: 'ERROR', reason: 'no such user' }), { headers: { 'content-type': 'application/json' } });
await assert.rejects(lnurlAuthLogin(a, k), /no such user/, "the server's reason is the error");
console.log('✓ lnurl-auth: a login is told from a payment, one key per site per wallet, the signature verifies, the server has the last word');
+
+// ---- LNURL-withdraw (LUD-03) ----
+const { parseZapTarget, withdrawParamsOf, fetchWithdrawParams, submitWithdraw, fetchPayParams } = await import('../src/lnurl.js');
+const w = parseZapTarget('lnurlw://pay.example.com/withdraw?k=1');
+assert.equal(w.url, 'https://pay.example.com/withdraw?k=1', 'lnurlw:// is the URL over https');
+assert.equal(parseZapTarget('LIGHTNING:lnurlp://pay.example.com/.well-known/lnurlp/bob').url, 'https://pay.example.com/.well-known/lnurlp/bob');
+assert.equal(parseZapTarget('lnurlw://abc.onion/w').url, 'http://abc.onion/w', 'an onion over http');
+const terms = { tag: 'withdrawRequest', callback: 'https://pay.example.com/cb', k1: 'k1k1', minWithdrawable: 1000, maxWithdrawable: 50000, defaultDescription: 'faucet' };
+assert.deepEqual(withdrawParamsOf(terms), { callback: 'https://pay.example.com/cb', k1: 'k1k1', minMsat: 1000, maxMsat: 50000, description: 'faucet', domain: 'pay.example.com' });
+assert.equal(withdrawParamsOf({ tag: 'payRequest', callback: 'x' }), null);
+assert.equal(withdrawParamsOf({ ...terms, maxWithdrawable: 0 }), null, 'nothing to withdraw is no withdraw');
+globalThis.fetch = async () => Response.json(terms);
+assert.deepEqual(await fetchWithdrawParams('https://pay.example.com/w'), withdrawParamsOf(terms));
+await assert.rejects(fetchPayParams('https://pay.example.com/w'), (e) => e.withdraw && e.withdraw.k1 === 'k1k1', 'a pay fetch that finds a withdraw says so');
+let cb = null;
+globalThis.fetch = async (url) => { const u = new URL(url); cb = { path: u.pathname, k1: u.searchParams.get('k1'), pr: u.searchParams.get('pr') }; return Response.json({ status: 'OK' }); };
+assert.equal(await submitWithdraw(withdrawParamsOf(terms), 'lnbc1invoice'), true);
+assert.deepEqual(cb, { path: '/cb', k1: 'k1k1', pr: 'lnbc1invoice' }, 'the callback gets k1 and the invoice');
+globalThis.fetch = async () => Response.json({ status: 'ERROR', reason: 'already claimed' });
+await assert.rejects(submitWithdraw(withdrawParamsOf(terms), 'lnbc1invoice'), /already claimed/);
+console.log('✓ lnurl-withdraw: lnurlw:// and lnurlp:// schemes, the terms, a pay fetch that finds a withdraw, the callback, the refusal');