1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
// 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.js';
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'),
};
});
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);
// The welcome card is just the title and the buttons now (42c8d4e): the
// way in itself says what the wallet is — tapping it puts the twelve
// words on screen at once, with what they are.
await page.evaluate(() => document.getElementById('onb-create')?.click());
await sleep(600);
const made = await page.evaluate(() => ({
words: document.querySelectorAll('.words .w .t').length,
says: /12 words|twelve words/i.test(document.body.innerText),
}));
check('the way in shows what the wallet actually is: its twelve words', made.words === 12 && made.says,
made.words + ' words' + (made.says ? ', explained' : ', unexplained'));
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);
|