// 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, note) { 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() + (note ? '#m=' + encodeURIComponent(note) : '')); check('the gift opens: the server sees no coin behind it', await waitText(page, /aren.t visible/i)); if (note) { const q = await page.evaluate(() => document.querySelector('.gift-note')?.textContent || null); check('the sender\'s note shows on the gift', q === note, String(q)); check('...and leaves the address bar with the code', await page.evaluate(() => location.pathname === '/' && !location.hash)); } 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, 'Happy birthday! 🎂 not bold'); 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);