diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/lnurl-withdraw-links-test.js | 27 |
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); } |
