aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-18 21:30:33 -0700
committerAdam Soltys <[email protected]>2026-10-18 21:30:33 -0700
commit29c84501956226f16d63442bce2132834c065114 (patch)
treefe81979ff425cf567fb78199b86136a70624a0a2 /tools
parent4fcfbef28ee9f53425f552b69e99422096d8dfcb (diff)
downloadcoinosv3-29c84501956226f16d63442bce2132834c065114.tar.gz
coinosv3-29c84501956226f16d63442bce2132834c065114.zip
A Lightning invoice from Receive says to keep coinos open until it's paid — paid while coinos is closed, the money goes back to the sender — and that it lasts an hour (ark.coinos.io's invoices now expire in 1 h, not 48); when the hour is up the QR gives way to "expired" and a New invoice button.
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'tools')
-rw-r--r--tools/names-invoice-expiry-test.js70
1 files changed, 70 insertions, 0 deletions
diff --git a/tools/names-invoice-expiry-test.js b/tools/names-invoice-expiry-test.js
new file mode 100644
index 0000000..511d212
--- /dev/null
+++ b/tools/names-invoice-expiry-test.js
@@ -0,0 +1,70 @@
+// Receive → More → Lightning invoice: the invoice says to keep coinos open
+// until it's paid (the server only finishes a receive while the wallet is
+// there to take it) and gives way to "expired" when its hour is up.
+// The Ark side is faked. Run: bun tools/names-invoice-expiry-test.js
+import assert from 'node:assert/strict';
+import puppeteer from './puppeteer.js';
+
+const app = await Bun.file('src/app.js').text();
+const domHelper = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
+const entry = `
+import { namesFeature } from './src/features/names.js';
+${domHelper}
+const state = { names: { byDomain: { 'coinos.io': { name: 'alice', domain: 'coinos.io' } } } };
+const ui = { screen: 'wallet', namesMore: 'bolt11' };
+const wallet = { loadFeatureState: (k, d) => (k in state ? state[k] : d), saveFeatureState: (k, v) => { state[k] = v; }, nostrSign: async (e) => e, nostrPubkey: () => 'aa'.repeat(32), _cacheKey: () => 'w1' };
+const ctx = {
+ h, ui, wallet, render, toast: () => {}, copy: () => {}, copyBtn: (x, l) => h('button', { class: 'btn-sm' }, l), brandHeader: () => null, goBack: () => {}, setAccount: () => {},
+ hook(name, ...a) {
+ if (name === 'arkReady') return true;
+ if (name === 'arkMakeInvoice') return Promise.resolve({ invoice: 'lnbc10u1testinvoice', amountSat: a[0], expiresAt: Date.now() + (window.__life || 3600_000) });
+ return null;
+ },
+};
+const feature = namesFeature(ctx);
+function render() { document.querySelector('#app').replaceChildren(feature.receiveModes()[0].render(h('div', { class: 'seg' }))); }
+window.test = { render };
+render();
+`;
+const bundle = await Bun.build({
+ entrypoints: ['names-inv-entry'], target: 'browser',
+ plugins: [{ name: 'names-inv', setup(build) {
+ build.onResolve({ filter: /^names-inv-entry$/ }, () => ({ path: 'entry', namespace: 'names-inv' }));
+ build.onLoad({ filter: /.*/, namespace: 'names-inv' }, () => ({ contents: entry, loader: 'js', resolveDir: process.cwd() }));
+ } }],
+});
+assert(bundle.success, bundle.logs.join('\n'));
+const html = `<!doctype html><meta charset="utf-8"><style>${await Bun.file('src/style.css').text()}</style><div id="app"></div><script type="module">${await bundle.outputs[0].text()}</script>`;
+const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const errors = [];
+page.on('pageerror', (e) => errors.push(e.message));
+const pause = (ms) => new Promise((r) => setTimeout(r, ms));
+const check = (name, value, detail = '') => { assert(value, name + (detail ? ' — ' + detail : '')); console.log(' ✓ ' + name); };
+const text = () => page.evaluate(() => document.body.innerText.replace(/\s+/g, ' '));
+const make = async () => {
+ await page.evaluate(() => { const i = document.querySelector('input[type=number]'); i.value = '1000'; i.dispatchEvent(new Event('input', { bubbles: true })); });
+ await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /create/i.test(b.textContent)).click());
+ await pause(100);
+};
+try {
+ await page.goto(server.url.href);
+ await page.waitForSelector('input[type=number]');
+ await make();
+ check('the invoice shows', /lnbc10u1testinvoice/.test(await text()));
+ check('...saying to keep coinos open until it\'s paid, and that it lasts an hour', /Keep coinos open until this is paid/.test(await text()) && /goes back to the sender/.test(await text()) && /expires in an hour/.test(await text()));
+ await page.evaluate(() => { window.__life = 1500; document.querySelector('.btn-sm:last-child').click(); });
+ await pause(50);
+ await page.evaluate(() => test.render());
+ await make();
+ check('a fresh one shows its QR', !!(await page.$('svg')) && !/expired/.test(await text()));
+ await pause(2300);
+ check('when its time is up it says expired, the QR gone', /This invoice has expired/.test(await text()) && !(await page.$('svg')) && !/lnbc10u1testinvoice/.test(await text()));
+ check('...with a button for a new one', await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.textContent.trim() === 'New invoice')));
+ check('no page errors', !errors.length, errors.join(' | '));
+ console.log('\n✅ invoice: keep open, expires');
+} finally {
+ await browser.close();
+ server.stop(true);
+}