aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-07 22:34:18 -0700
committerAdam Soltys <[email protected]>2026-11-07 22:34:18 -0700
commitd6b9298e2dfde43542a516b3b0cac4770592a794 (patch)
tree047d5ec76ea1e659fa186f043f632a4f18a6ea7f
parenta1cb2db1907313530b50798dea0e37278cbbb0ef (diff)
downloadcoinosv3-d6b9298e2dfde43542a516b3b0cac4770592a794.tar.gz
coinosv3-d6b9298e2dfde43542a516b3b0cac4770592a794.zip
A scanned LNURL opens as what it is even while Spending is still connecting. "I refreshed and scanned it again and it put the lnurl in the recipient field and gave me a zap button" (Adam, 2026-11-08): zaps' matchSendText turned everything down until canLnPay, which is false for the first moments after a refresh, so his own withdraw link fell through to the send form's "Zap LNURL…" affordance. A withdraw pays us and needs nothing spendable, and only the GET says which kind an lnurl is, so a scanned or pasted lnurl is taken first and resolved; a pay one meets the pay checks where it's paid. lnurl-withdraw-test covers the scan with nothing spendableHEADmaster
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
-rw-r--r--src/features/zaps.js10
-rw-r--r--tools/lnurl-withdraw-test.js9
2 files changed, 17 insertions, 2 deletions
diff --git a/src/features/zaps.js b/src/features/zaps.js
index 8ac78fb..8e29409 100644
--- a/src/features/zaps.js
+++ b/src/features/zaps.js
@@ -533,7 +533,15 @@ export function zapsFeature(ctx) {
// a login challenge needs no balance, only the wallet's key
const auth = parseLnurlAuth(text);
if (auth) { beginLogin(auth); return true; }
- if (!canPay() || !ui.send || ui.send.recipients.length !== 1) return false;
+ if (!ui.send || ui.send.recipients.length !== 1) return false;
+ // An LNURL may be a withdraw — it pays US, so it needs no spendable
+ // balance, and right after a refresh (Spending still connecting) the
+ // check below turned a scanned withdraw link into a "Zap LNURL…"
+ // recipient (Adam, 2026-11-08). Only the GET can tell: take it now,
+ // and a pay one meets the pay checks where it's paid.
+ const early = parseZapTarget(text);
+ if (early && early.kind === 'lnurl' && !typed) { begin(early, zapDisplay(early, text), true); return true; }
+ if (!canPay()) return false;
const tap = !typed; // scanned/pasted (a tap), not a keystroke
const nof = maybeNoffer(text);
if (nof) {
diff --git a/tools/lnurl-withdraw-test.js b/tools/lnurl-withdraw-test.js
index cc1b152..0241f31 100644
--- a/tools/lnurl-withdraw-test.js
+++ b/tools/lnurl-withdraw-test.js
@@ -22,7 +22,7 @@ const ctx = {
parseAmount: (v) => { const n = Number(String(v).replace(/,/g, '')); return isFinite(n) ? Math.round(n) : null; }, getUnit: () => 'sats',
amountField: (sat) => String(sat),
hook(name, ...a) {
- if (name === 'canLnPay') return true;
+ if (name === 'canLnPay') return !window.noPay;
if (name === 'arkLnInvoice') { invoices++; return Promise.resolve({ id: 'ln' + invoices, invoice: 'lnbc' + a[0] + 'n1testinvoice' + invoices, amountSat: a[0] }); }
if (name === 'arkLnWatch') { watch = { id: a[0], cb: a[1] }; return true; }
if (name === 'arkLnUnwatch') { if (watch && watch.id === a[0]) watch = null; return true; }
@@ -94,6 +94,13 @@ try {
await click('Back'); await pause(50);
check('Back before it is paid stops watching', (await page.evaluate(() => test.watch)) === null && !!(await page.$('#closed')));
+ console.log('[scanned while Spending is still connecting]');
+ await page.evaluate(() => { window.noPay = true; });
+ check('a withdraw lnurl is still taken (it pays us; nothing to spend)', await page.evaluate((s) => test.paste(s), lnurl('/withdraw')));
+ check('…and opens as a withdraw, not a zap', await waitText(/Withdraw over Lightning/) && !/Zap/.test(await text()), await text());
+ await click('Back'); await pause(50);
+ await page.evaluate(() => { window.noPay = false; });
+
console.log('[a site that refuses]');
await page.evaluate((s) => test.paste(s), lnurl('/withdraw-spent'));
await waitText(/will pay you 1 sats/); await click('Withdraw');