aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/lnurl-withdraw-links-test.js27
1 files changed, 24 insertions, 3 deletions
diff --git a/tools/lnurl-withdraw-links-test.js b/tools/lnurl-withdraw-links-test.js
index b349e17..0e0b340 100644
--- a/tools/lnurl-withdraw-links-test.js
+++ b/tools/lnurl-withdraw-links-test.js
@@ -66,13 +66,13 @@ const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content
// ---- the stand-in registrar: the real service, a fake node ----
const OUR = '02' + 'aa'.repeat(32), THEIRS = '03' + 'bb'.repeat(32);
const invoices = {}, decoded = {}, outbound = {};
-let seq = 0, circular = 0;
+let seq = 0, circular = 0, routeless = false;
const mk = (sat, payee) => { const hash = (++seq).toString(16).padStart(64, '0'); const b = 'lnbc' + sat + 'n1fake' + seq; decoded[b] = { valid: true, payment_hash: hash, amount_msat: sat * 1000, payee, created_at: Math.floor(Date.now() / 1000), expiry: 3600 }; return { b, hash }; };
const ln = { async call(m, p) {
if (m === 'invoice') { const { b, hash } = mk(p.amount_msat / 1000, OUR); invoices[hash] = { status: 'unpaid', bolt11: b }; return { bolt11: b, payment_hash: hash }; }
if (m === 'listinvoices') return { invoices: invoices[p.payment_hash] ? [invoices[p.payment_hash]] : [] };
if (m === 'decode') { if (!decoded[p.string]) throw new Error('bad'); return decoded[p.string]; }
- if (m === 'pay') { const d = decoded[p.bolt11]; outbound[d.payment_hash] = 'complete'; return { status: 'complete', amount_msat: d.amount_msat, amount_sent_msat: d.amount_msat }; }
+ if (m === 'pay') { const d = decoded[p.bolt11]; if (routeless) { outbound[d.payment_hash] = 'failed'; throw new Error('Failed: We could not find a usable set of paths'); } outbound[d.payment_hash] = 'complete'; return { status: 'complete', amount_msat: d.amount_msat, amount_sent_msat: d.amount_msat }; }
if (m === 'waitsendpay') { outbound[p.payment_hash] = 'complete'; return { status: 'complete' }; }
if (m === 'listsendpays') return { payments: outbound[p.payment_hash] ? [{ status: outbound[p.payment_hash] }] : [] };
throw new Error('unexpected ' + m);
@@ -105,7 +105,7 @@ const server = Bun.serve({ port: 0, async fetch(req) {
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
svc = withdrawService({ state, persist: () => {}, lnFor: () => ln, ourNodeId: async () => OUR,
- circularPick: async () => ({ cap: 1e9 }), payCircular: async () => { circular++; return { feeMsat: 0 }; }, publicBase: server.url.origin.replace(/\/$/, '') });
+ circularPick: async () => ({ cap: 1e9 }), payCircular: async () => { circular++; return { feeMsat: 0 }; }, wait: async () => {}, publicBase: server.url.origin.replace(/\/$/, '') });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
const page = await browser.newPage();
@@ -161,6 +161,27 @@ try {
check('…and the wallet watched for that payment', (await page.evaluate(() => test.watched.length)) >= 1);
check('…the toast says so', (await page.evaluate(() => window.toasts || [])).some((m) => /1,000 sats is back in Spending/.test(m)));
check('a closed link refuses claims', svc.params(link.id).status === 'ERROR');
+
+ console.log('\n[a claim with no route to the wallet that asked]');
+ await click('← Back');
+ if (!(await page.$('#wl-amount'))) await page.evaluate(() => document.querySelector('#wl-open')?.click());
+ await page.waitForSelector('#wl-amount', { timeout: 3000 }).catch(async () => { throw new Error('no form: ' + await text()); });
+ await type('#wl-amount', '700'); await type('#wl-uses', '1'); await type('textarea', '');
+ await page.click('#wl-create');
+ check('a second link is made', await waitText(/Your withdraw link is ready/));
+ const link2 = Object.values(state.withdraws).find((x) => x.id !== link.id);
+ routeless = true;
+ const theirs = mk(700, THEIRS).b;
+ check('the claimer is told OK (LUD-03 pays after)', (await svc.callback(link2.id, link2.k1, theirs)).status === 'OK');
+ for (let i = 0; i < 50 && link2.claims[0]?.status !== 'failed'; i++) await pause(20);
+ check('it was tried again before giving up, and the sats went back', link2.claims[0].status === 'failed' && link2.claims[0].tries > 1 && link2.spentSat === 0, JSON.stringify(link2));
+ await page.evaluate(() => { test.ui.giftMode = false; test.render(); document.querySelector('#wl-open').click(); });
+ check('the list says the claim failed', await waitText(/claim failed · 700 sats left/), await text());
+ await page.evaluate(() => [...document.querySelectorAll('.wl-row')].find((b) => /claim failed/.test(b.textContent)).click());
+ check('the link page says nothing was sent and the sats are still here', await waitText(/A claim of 700 sats couldn't be delivered.*sats are still in this link/), await text());
+ check('…and still offers the QR and take back', !!(await page.$('svg')) && /Take back 700 sats/.test(await text()));
+ routeless = false;
+ check('the same link pays a claim once a route is there', (await svc.callback(link2.id, link2.k1, mk(700, THEIRS).b)).status === 'OK' && (await (async () => { await pause(50); return link2.claims[1]?.status === 'paid'; })()), JSON.stringify(link2.claims));
check('no page errors', errors.length === 0, errors.join('; '));
console.log('\n✅ prepaid withdraw links');
} catch (e) { console.error('\n❌', e.message); process.exitCode = 1; } finally { await browser.close(); server.stop(true); }