diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/minimal-smoke.js | 7 | ||||
| -rw-r--r-- | tools/sp-test.js | 143 |
2 files changed, 149 insertions, 1 deletions
diff --git a/tools/minimal-smoke.js b/tools/minimal-smoke.js index ab31619..1e80d67 100644 --- a/tools/minimal-smoke.js +++ b/tools/minimal-smoke.js @@ -28,6 +28,8 @@ check('meaningfully smaller than full', html.length < full.length * 0.9); check('no ark protocol code', !html.includes('Ark VTXO mailbox authorization')); check('no swap protocol code', !html.includes('swap/reverse')); check('no musig code', !html.includes('MuSig/noncecoef')); +check('no silent-payment engine', !html.includes('BIP0352') && !html.includes('__SP_WORKER__')); +check('no nostr sync code', !html.includes('30078') && !html.includes('relay.coinos.io')); const server = Bun.serve({ port: 5198, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); @@ -79,6 +81,7 @@ try { check('no swap provider card', !settings.includes('Swap provider')); check('no ark card', !/⚔ Ark/.test(settings)); check('no sp indexer card', !settings.includes('Silent payment indexer')); + check('no sync card', !settings.toLowerCase().includes('sync')); console.log('\n[3] core receive still works'); await clickText('.tabs button', 'Receive'); @@ -87,7 +90,9 @@ try { check('onchain address shown', /^bcrt1/.test(addr || ''), (addr || '').slice(0, 16) + '…'); sh(`${BCLI} sendtoaddress ${addr} 0.0004`); sh(`${BCLI} generatetoaddress 1 $(${BCLI} getnewaddress) >/dev/null`); - check('payment lands', await waitText('40,000', 30000)); + const landed = await waitText('40,000', 30000); + if (!landed) console.log(' [balance area]', (await bodyText()).slice(0, 200).replace(/\n+/g, ' | ')); + check('payment lands', landed); check('no page errors', errs.length === 0, errs.join(' | ')); console.log(ok ? '\n✅ SUCCESS: minimal profile is a working on-chain-only wallet' diff --git a/tools/sp-test.js b/tools/sp-test.js new file mode 100644 index 0000000..d602d0f --- /dev/null +++ b/tools/sp-test.js @@ -0,0 +1,143 @@ +// Silent-payments e2e on regtest, through the real UI (two isolated browser +// contexts = two devices): +// +// wallet B shows its sp1 address -> wallet A (funded on-chain) pays it -> +// B's indexer scan finds the payment -> B SPENDS the SP coin (exercises the +// BIP-352 per-output key signing path) -> miner address receives it. +// +// Needs the ark-regtest stack + the SP indexer: +// COOKIE=$(cat ~/ark-regtest/bitcoind/regtest/.cookie); \ +// CORE_RPC=http://127.0.0.1:18543 RPC_USER=${COOKIE%%:*} RPC_PASS=${COOKIE#*:} \ +// SP_PORT=8899 START_HEIGHT=<tip> bun tools/sp-indexer.js +// +// Usage: bun tools/sp-test.js + +import { spawn, execSync } from 'node:child_process'; +import puppeteer from 'puppeteer-core'; +import { generateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; + +const PORT = 5199; +const APP = `http://localhost:${PORT}`; +const BCLI = `bitcoin-cli -regtest -datadir=${process.env.HOME}/ark-regtest/bitcoind -rpcport=18543 -rpcwallet=miner`; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const sh = (cmd) => execSync(cmd, { shell: '/bin/bash' }).toString().trim(); +const mine = (n = 1) => sh(`${BCLI} generatetoaddress ${n} $(${BCLI} getnewaddress) >/dev/null`); + +let ok = true; +const check = (name, cond, detail = '') => { + console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ' — ' + detail : ''}`); + if (!cond) ok = false; +}; + +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'] }); + +async function newWalletPage() { + const ctx = await browser.createBrowserContext(); // isolated storage = its own device + const page = await ctx.newPage(); + page.on('pageerror', (e) => console.log(' [pageerror]', e.message)); + 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-sp-indexer:regtest', JSON.stringify({ server: 'custom', url: 'http://localhost:8899' })); + }); + await page.reload({ waitUntil: 'domcontentloaded' }); + await sleep(400); + const clickText = (sel, text) => page.evaluate((s, x) => { + const el = [...document.querySelectorAll(s)].find((e) => e.textContent.trim().toLowerCase().includes(x.toLowerCase())); + if (el) { el.click(); return true; } + return false; + }, sel, text); + const bodyText = () => page.evaluate(() => document.body.innerText); + const waitText = async (text, ms = 20000) => { + for (let i = 0; i < ms / 300; i++) { + if ((await bodyText()).toLowerCase().includes(text.toLowerCase())) return true; + await sleep(300); + } + return false; + }; + await clickText('button', 'Import existing'); + await sleep(200); + await page.type('textarea', generateMnemonic(wordlist)); + await clickText('button', 'Open wallet'); + await waitText('receive'); + return { page, clickText, bodyText, waitText }; +} + +try { + console.log('\n[1] two wallets'); + const A = await newWalletPage(); + const B = await newWalletPage(); + + // B: silent-payment address + await B.page.select('select', 'sp'); + await sleep(400); + const spAddr = await B.page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim()); + check('B shows sp1 address', /^t?sp1[a-z0-9]+$/.test(spAddr || ''), (spAddr || '').slice(0, 20) + '…'); + + // A: fund on-chain + const aAddr = await A.page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim()); + sh(`${BCLI} sendtoaddress ${aAddr} 0.001`); + mine(1); + check('A funded', await A.waitText('100,000', 30000)); + if (await A.page.evaluate(() => document.body.innerText.includes('Payment received'))) { + await A.clickText('.card', 'Payment received'); + await sleep(300); + } + + console.log('\n[2] A pays B\'s silent-payment address'); + await A.clickText('.tabs button', 'Send'); + await sleep(300); + await A.page.type('.mono-input', spAddr); + await sleep(500); // destReady render + await A.page.type('input[type="number"]', '30000'); + await A.clickText('button.btn-primary', 'Review transaction'); + check('review shows SP note', await A.waitText('Silent payment', 8000)); + await A.clickText('button.btn-primary', 'Sign & broadcast'); + const bok = await A.waitText('Transaction ID', 15000); + if (!bok) console.log(' [A after send]', (await A.bodyText()).slice(0, 320).replace(/\n+/g, ' | ')); + check('broadcast ok', bok); + mine(1); + + console.log('\n[3] B discovers the payment'); + const found = await (async () => { + for (let i = 0; i < 25; i++) { + if ((await B.bodyText()).includes('30,000')) return true; + await sleep(2000); + } + return false; + })(); + check('B sees 30,000 sats via SP scan', found, (await B.bodyText()).match(/([\d,]+)\s*\n?\s*sats/)?.[1]); + + console.log('\n[4] B spends the SP coin (BIP-352 per-output key signing)'); + if (await B.page.evaluate(() => document.body.innerText.includes('Payment received'))) { + await B.clickText('.card', 'Payment received'); + await sleep(300); + } + mine(1); // confirm the SP receive; unconfirmed SP coins aren't spendable + await B.waitText('30,000', 15000); + await sleep(2000); + const minerAddr = sh(`${BCLI} getnewaddress`); + await B.clickText('.tabs button', 'Send'); + await sleep(300); + await B.page.type('.mono-input', minerAddr); + await sleep(500); + await B.page.type('input[type="number"]', '20000'); + await B.clickText('button.btn-primary', 'Review transaction'); + await sleep(800); + await B.clickText('button.btn-primary', 'Sign & broadcast'); + const spent = await B.waitText('Transaction ID', 15000); + check('SP coin spend broadcast', spent, spent ? '' : (await B.bodyText()).slice(0, 200)); + mine(1); + + console.log(ok ? '\n✅ SUCCESS: SP send, indexer discovery, and SP-input spend all work' + : '\n❌ some checks failed'); +} finally { + await browser.close(); + dev.kill(); +} +process.exit(ok ? 0 : 1); |
