aboutsummaryrefslogtreecommitdiffstats
path: root/tools/onboard-backup-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/onboard-backup-test.js')
-rw-r--r--tools/onboard-backup-test.js121
1 files changed, 121 insertions, 0 deletions
diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js
new file mode 100644
index 0000000..3b242dd
--- /dev/null
+++ b/tools/onboard-backup-test.js
@@ -0,0 +1,121 @@
+// Two things about the front door.
+//
+// One: making your own keys has to look like the way in. Four equally
+// weighted buttons, where only the sign-in ones said what they did, sent
+// people straight past the seed phrase — which IS the wallet — into a login
+// that is only a convenience wrapped around one.
+//
+// Two: a wallet that arrived by SIGN-IN has a recovery phrase its owner has
+// never seen, and nobody can reissue it. So it gets shown once, during
+// setup, and if they put it off the wallet keeps one quiet line about it.
+//
+// Run: bun tools/onboard-backup-test.js
+import puppeteer from 'puppeteer-core';
+import { bech32 } from '@scure/base';
+import { generateSecretKey } from 'nostr-tools/pure';
+import { seedFromNostrKey } from '../src/nostr-login.js';
+import { buildHtml } from '../build.js';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+
+const html = await buildHtml({ minify: true, pwa: false });
+const server = Bun.serve({ port: 5291, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+
+const click = (page, t, sel = 'button') => page.evaluate((s, x) => {
+ const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x));
+ if (e) { e.click(); return true; } return false;
+}, sel, t.toLowerCase());
+const text = (page) => page.evaluate(() => document.body.innerText);
+const waitText = async (page, x, ms = 30000) => {
+ for (let i = 0; i < ms / 250; i++) { if ((await text(page)).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); }
+ return false;
+};
+
+// Sign in with a pasted nsec and stop at whatever the wizard shows next.
+// Its own browser context, so each run meets a genuinely fresh front door
+// rather than the wallet the previous one left in localStorage.
+async function signIn() {
+ const ctx = await browser.createBrowserContext();
+ const page = await ctx.newPage();
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ // wait for the front door to actually be painted before poking it
+ await waitText(page, 'sign in with nostr');
+ const sk = generateSecretKey();
+ const nsec = bech32.encode('nsec', bech32.toWords(sk), 1000);
+ await click(page, 'sign in with nostr');
+ await waitText(page, 'log in with nostr');
+ await page.waitForSelector('input[placeholder*="nsec" i], textarea[placeholder*="nsec" i]', { timeout: 15000 });
+ await page.type('input[placeholder*="nsec" i], textarea[placeholder*="nsec" i]', nsec);
+ await click(page, 'log in');
+ return { page, ctx, seed: seedFromNostrKey(sk) };
+}
+
+try {
+ console.log('[the start page]');
+ {
+ const page = await browser.newPage();
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' });
+ await sleep(1500);
+ const front = await page.evaluate(() => {
+ const btns = [...document.querySelectorAll('button')].filter((b) => b.offsetParent);
+ const primary = document.getElementById('onb-create');
+ const idx = btns.indexOf(primary);
+ const signins = btns.filter((b) => /sign in with/i.test(b.textContent));
+ return {
+ label: primary ? primary.textContent.trim() : null,
+ first: idx === 0 || (idx >= 0 && idx < btns.findIndex((b) => /sign in with/i.test(b.textContent))),
+ primaryTop: primary ? primary.getBoundingClientRect().top : -1,
+ signinTop: signins.length ? signins[0].getBoundingClientRect().top : -1,
+ divider: !!document.querySelector('.onb-or'),
+ body: (document.querySelector('.onb p') || {}).textContent || '',
+ };
+ });
+ check('the way in says what it makes', /wallet/i.test(front.label || ''), front.label || 'missing');
+ check('...and comes before the sign-in buttons', front.first && front.primaryTop < front.signinTop,
+ `create at ${Math.round(front.primaryTop)}, first sign-in at ${Math.round(front.signinTop)}`);
+ check('...with a line between them, so they read as the alternative', front.divider);
+ check('the page says what the wallet actually is', /twelve words|seed|phrase/i.test(front.body),
+ front.body.slice(0, 60) + '…');
+ await page.close();
+ }
+
+ console.log('\n[signing in, then writing it down]');
+ {
+ const { page, ctx, seed } = await signIn();
+ check('a sign-in is asked to write the phrase down', await waitText(page, 'write down your recovery phrase'));
+ const shown = await page.evaluate(() => [...document.querySelectorAll('.words .w .t')].map((e) => e.textContent.trim()));
+ check('...and the real twelve words are on screen', shown.length === 12 && shown.join(' ') === seed,
+ shown.length + ' words' + (shown.join(' ') === seed ? ', matching the derived seed' : ', WRONG'));
+ await click(page, "i've written it down");
+ check('confirming lands in the wallet', await waitText(page, 'receive'));
+ const nag = await text(page);
+ check('...and there is no reminder afterwards', !/still unwritten/i.test(nag));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ check('...not after a reload either', await waitText(page, 'receive') && !/still unwritten/i.test(await text(page)));
+ await ctx.close();
+ }
+
+ console.log('\n[signing in, then putting it off]');
+ {
+ const { page, ctx } = await signIn();
+ check('the ask appears', await waitText(page, 'write down your recovery phrase'));
+ await click(page, 'later');
+ check('later lands in the wallet too', await waitText(page, 'receive'));
+ check('...but the wallet keeps a quiet reminder', /still unwritten/i.test(await text(page)));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitText(page, 'receive');
+ check('...which survives a reload', /still unwritten/i.test(await text(page)));
+ // and it is a way IN, not just a scold
+ await click(page, 'write it down');
+ check('tapping it opens the recovery phrase', await waitText(page, 'recovery phrase'));
+ await ctx.close();
+ }
+} finally { await browser.close(); server.stop(true); }
+console.log(ok ? '\n✅ the front door points at your own keys' : '\n❌ failed');
+process.exit(ok ? 0 : 1);