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
|
// After a gift is opened into a fresh wallet and its seed written down, the
// first-run wizard asks for a username, then a picture and a cover — the gift's
// recipient should arrive as someone people can pay by name. "Not now" lands in
// the account the gift went to.
// Drives an ark gift on mutinynet whose bearer key holds nothing (a random
// secret: the staging ASP sees no coin, so the sweep finds it taken), then
// "Create wallet anyway" → the seed → on to the wizard.
// Run: bun run build && bun tools/gift-profile-setup-test.js [screenshot.png]
import puppeteer from './puppeteer.js';
import { base32nopad } from '@scure/base';
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 server = Bun.serve({ port: 0, async fetch(req) {
const p = new URL(req.url).pathname;
const f = Bun.file('dist' + (p === '/' ? '/index.html' : p));
return (await f.exists()) ? new Response(f) : new Response(Bun.file('dist/index.html'), { headers: { 'content-type': 'text/html' } });
} });
const giftCode = () => {
const b = new Uint8Array(42);
b[0] = 0x11; b[1] = 3; // ark gift, mutinynet
new DataView(b.buffer).setBigUint64(2, 1000n, true);
b.set(crypto.getRandomValues(new Uint8Array(32)), 10);
return base32nopad.encode(b);
};
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
const text = (page) => page.evaluate(() => document.body.innerText);
const click = (page, x) => page.evaluate((x) => {
const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x));
e?.click(); return !!e;
}, x);
const waitText = async (page, re, ms = 30000) => {
for (let t = 0; t < ms; t += 250) { if (re.test(await text(page))) return true; await sleep(250); }
return false;
};
async function toSeed(page) {
await page.goto(server.url.href);
await page.evaluate(() => { localStorage.clear(); localStorage.setItem('btc-wallet-network', 'mutinynet'); });
await page.goto(server.url.href + 'ag/' + giftCode());
check('the gift opens: the server sees no coin behind it', await waitText(page, /aren.t visible/i));
await click(page, 'claim');
check('...so the claim finds it taken', await waitText(page, /create a wallet anyway/i));
check('...create a wallet anyway shows the seed', await click(page, 'create a wallet anyway') && await waitText(page, /recovery phrase/i, 10000));
}
try {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(String(e)));
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
await toSeed(page);
await click(page, 'skip verification');
check('the seed done: the wizard asks for a username', await waitText(page, /now pick a username/i, 10000), (await text(page)).slice(0, 200));
check('...with the claim form (or its wait) and Not now', await page.evaluate(() => !!document.querySelector('.onb')) && /not now/i.test(await text(page)));
if (process.argv[2]) await page.screenshot({ path: process.argv[2] });
// the look step follows the name, as in any first run
await page.evaluate(() => localStorage.setItem('btc-wallet-onb-step', 'avatar'));
await page.reload();
check('the next step is the picture and cover', !!await page.waitForSelector('.onb-cover', { timeout: 15000 }).catch(() => null));
// a second gift, verified this time, then Not now
const p2 = await browser.newPage();
p2.on('pageerror', (e) => errors.push(String(e)));
await p2.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
await toSeed(p2);
check('Verify backup opens the word check', await click(p2, 'verify') && await waitText(p2, /word #?\d/i, 5000));
await sleep(500);
// the verify page doesn't show the words: read them from the account
const filled = await p2.evaluate(() => {
const accs = JSON.parse(sessionStorage.getItem('btc-wallet-accounts') || '{}');
const list = Array.isArray(accs) ? accs : accs.accounts || accs.list || [];
const m = list.map((a) => a.mnemonic).filter(Boolean).pop(); if (!m) return false;
const w = m.split(' ');
for (const lab of document.querySelectorAll('label.field')) {
const n = Number(lab.querySelector('.lab').textContent.match(/\d+/)[0]);
const inp = lab.querySelector('input');
const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
set.call(inp, w[n - 1]); inp.dispatchEvent(new Event('input', { bubbles: true }));
}
return true;
});
if (filled) {
await click(p2, 'open wallet');
check('a verified seed leads to the username step too', await waitText(p2, /now pick a username/i, 10000));
await click(p2, 'not now');
let gone = false;
for (let i = 0; i < 20 && !gone; i++) { await sleep(250); gone = !(await p2.evaluate(() => !!document.querySelector('.onb'))); }
check('Not now: into the wallet, the wizard gone', gone, (await text(p2)).slice(0, 200));
} else console.log(' (accounts not in localStorage — verify path not driven)');
check('no page errors', errors.length === 0, errors.join(' | '));
} finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ a gift\'s new wallet is offered a name, a picture and a cover' : '\n❌ failed');
process.exit(ok ? 0 : 1);
|