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
|
// Screenshot the gift-claim wallet chooser: mint a real gift from a funded
// wallet, open its link in the same browser (wallet 1 still in session),
// capture the "choose where to receive" screen.
import { spawn, execSync } from 'node:child_process';
import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
const PORT = 5199;
const APP = `http://localhost:${PORT}`;
const BCLI = `docker exec bc bitcoin-cli -rpcwallet=coinos`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const sh = (cmd) => execSync(cmd, { shell: '/bin/bash' }).toString().trim();
const dev = spawn('bun', ['run', 'dev'], { cwd: process.env.HOME + '/halwallet', env: { ...process.env, PORT: String(PORT) }, stdio: 'ignore' });
for (let i = 0; i < 30; i++) { try { if ((await fetch(APP)).ok) break; } catch {} await sleep(500); }
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
page.on('pageerror', (e) => console.log('[pageerror]', e.message));
await page.setViewport({ width: 420, height: 860, deviceScaleFactor: 2 });
const clickText = (sel, text) => page.evaluate((sel, text) => {
const el = [...document.querySelectorAll(sel)].find((e) => e.textContent.trim().toLowerCase().includes(text.toLowerCase()));
if (el) { el.click(); return true; } return false;
}, sel, text);
const waitText = async (text, ms = 20000) => {
for (let i = 0; i < ms / 250; i++) {
if (await page.evaluate((t) => document.body.innerText.toLowerCase().includes(t.toLowerCase()), text)) return true;
await sleep(250);
}
return false;
};
await page.goto(APP, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => {
localStorage.setItem('btc-wallet-network', 'regtest');
localStorage.setItem('btc-wallet-source:regtest', JSON.stringify({ id: 'custom', url: 'http://localhost:30002' }));
localStorage.setItem('btc-wallet-ark-provider:regtest', 'local');
});
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(500);
await clickText('button', 'Import existing');
await sleep(200);
await page.type('textarea', generateMnemonic(wordlist));
await clickText('button', 'Open wallet');
await waitText('receive');
// fund on-chain, confirmed
const addr = await page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim());
sh(`${BCLI} sendtoaddress ${addr} 0.0005`);
sh(`${BCLI} generatetoaddress 1 $(${BCLI} getnewaddress) >/dev/null`);
await waitText('50,000', 30000);
if (await page.evaluate(() => document.body.innerText.includes('Payment received'))) {
await clickText('.card', 'Payment received');
await sleep(300);
}
// make a 2000-sat gift
await clickText('.tabs button', 'Send');
await sleep(300);
await clickText('button', 'Gift some Bitcoin');
await sleep(300);
const amtInput = await page.evaluateHandle(() => [...document.querySelectorAll('input[type="number"]')][0]);
await amtInput.asElement().type('2000');
await clickText('button', 'Create gift link');
await sleep(500);
if (await page.evaluate(() => document.body.innerText.includes('Lock the whole coin'))) {
await clickText('button', 'Lock the whole coin');
}
await waitText('/g/', 20000);
const link = await page.evaluate(() =>
[...document.querySelectorAll('.addr-box')].map((e) => e.textContent.trim()).find((x) => x.includes('/g/')));
console.log('gift link:', (link || '').slice(0, 50) + '…');
// open the gift link — wallet 1 exists, so the chooser should appear
await page.goto(link, { waitUntil: 'domcontentloaded' });
const shown = await waitText('Choose where', 15000);
console.log('chooser shown:', shown);
await sleep(400);
await page.screenshot({ path: '/tmp/gift-choose.png' });
// pick the existing wallet and capture the claim screen copy
await clickText('button', 'Wallet 1');
await waitText('Claim it', 15000);
await sleep(400);
await page.screenshot({ path: '/tmp/gift-claim-existing.png' });
console.log('claim copy:', await page.evaluate(() => [...document.querySelectorAll('p')].map((e) => e.textContent).find((x) => x.includes('Claim it'))));
// claim it — the celebration should be instant (no scan wait)
await clickText('button', 'Claim it');
const t0 = Date.now();
const celebrated = await waitText('Payment received', 10000);
console.log('celebration:', celebrated, 'after', Date.now() - t0, 'ms');
await page.screenshot({ path: '/tmp/gift-claimed.png' });
// tap through, mine so the claim settles, then check History
await clickText('.card', 'Payment received');
await sleep(400);
sh(`${BCLI} generatetoaddress 1 $(${BCLI} getnewaddress) >/dev/null`);
await sleep(4000); // scan + outspend resolution
await clickText('.tabs button', 'History');
await sleep(2500);
const rows = await page.evaluate(() => [...document.querySelectorAll('.item')].map((e) => e.textContent.replace(/\s+/g, ' ').trim()));
console.log('history rows:', JSON.stringify(rows.slice(0, 5), null, 1));
const giftRows = rows.filter((r) => r.toLowerCase().includes('gift link'));
console.log('gift rows in timeline:', giftRows.length);
await page.evaluate(() => { const el = [...document.querySelectorAll('.item')].find((e) => e.textContent.toLowerCase().includes('gift link')); if (el) el.click(); });
await sleep(800);
const detail = await page.evaluate(() => document.body.innerText);
console.log('gift detail page:', /🎁 Gift link/.test(detail), '| link+cancel actions:', detail.includes('View link') || detail.includes('Cancel'));
await page.screenshot({ path: '/tmp/gift-history.png' });
console.log('done');
await browser.close();
dev.kill();
|