diff options
| author | Adam Soltys <[email protected]> | 2026-07-03 15:08:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-03 15:08:26 -0700 |
| commit | 190c13a338462a7a76ee65e9486e42c0165a65f4 (patch) | |
| tree | 0006e713e91e39b5b90d458a35b6c4a33cef34cc /tools | |
| parent | 5b7f8ba40cbf5c8189147b6a37aadc1b230ac4e9 (diff) | |
| download | coinosv3-190c13a338462a7a76ee65e9486e42c0165a65f4.tar.gz coinosv3-190c13a338462a7a76ee65e9486e42c0165a65f4.zip | |
Ark: wire into the wallet UI
- Settings: Ark server card (off by default, per-network presets incl.
Second mainnet + local regtest, custom URLs) with balance, coin count,
board form (on-chain -> Ark; funding output pinned to vout 0 via the
new buildTx noSort option), refresh/consolidate, and recent activity.
- Receive: Ark option in the address-type dropdown (QR + copy).
- Send: t?ark1 addresses are recognized in the normal send form and
route to an Ark confirmation (no mining fee) with a success view.
- Balance card: Ark balance line under the on-chain figure.
- ArkManager lifecycle: per-wallet instance created on account
activation, background sync every 5s (regtest) / 30s, state persisted
under the wallet cache key.
Verified end-to-end in a real browser (tools/ark/ui-test.js, puppeteer
against the regtest stack): receive, send, board, and refresh all green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/ark/ui-shots.js | 61 | ||||
| -rw-r--r-- | tools/ark/ui-test.js | 172 |
2 files changed, 233 insertions, 0 deletions
diff --git a/tools/ark/ui-shots.js b/tools/ark/ui-shots.js new file mode 100644 index 0000000..5331ab8 --- /dev/null +++ b/tools/ark/ui-shots.js @@ -0,0 +1,61 @@ +// Screenshot run: import a wallet, receive over Ark, show the balance card + +// Ark settings card. Writes PNGs to /tmp/ark-ui-*.png. +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 BARK = `${process.env.HOME}/bark/target/debug/bark`; +const ALICE = `${BARK} --datadir ${process.env.HOME}/ark-regtest/alice`; +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(); +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(); +}, sel, text); + +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 sleep(4000); + +await page.select('select', 'ark'); +await sleep(400); +const addr = await page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim()); +await page.screenshot({ path: '/tmp/ark-ui-receive.png' }); + +sh(`${ALICE} -q send ${addr} "25000 sat"`); +for (let i = 0; i < 30; i++) { await sleep(1000); if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes('ark balance')) break; } +await sleep(500); +await page.screenshot({ path: '/tmp/ark-ui-balance.png' }); + +await clickText('.tabs button', 'Settings'); +await sleep(500); +await page.evaluate(() => { + const el = [...document.querySelectorAll('h3')].find((e) => e.textContent.includes('Ark')); + if (el) el.scrollIntoView({ block: 'start' }); +}); +await sleep(300); +await page.screenshot({ path: '/tmp/ark-ui-settings.png' }); + +await browser.close(); +dev.kill(); +console.log('done'); diff --git a/tools/ark/ui-test.js b/tools/ark/ui-test.js new file mode 100644 index 0000000..f8288e2 --- /dev/null +++ b/tools/ark/ui-test.js @@ -0,0 +1,172 @@ +// Browser-level e2e of the Ark UI wiring, driven with puppeteer against the +// real dev server + the ark regtest stack: +// +// import wallet -> ark connects -> receive over Ark (alice pays the address +// shown in the Receive tab) -> send over Ark from the Send form -> board +// from the Settings card -> refresh/consolidate -> balances line up. +// +// Usage: bun tools/ark/ui-test.js (regtest stack up + nothing on :5199) + +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 BARK = `${process.env.HOME}/bark/target/debug/bark`; +const ALICE = `${BARK} --datadir ${process.env.HOME}/ark-regtest/alice`; +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`); +const aliceBalance = () => JSON.parse(sh(`${ALICE} -q balance`)).spendable_sat; + +let ok = true; +const check = (name, cond, detail = '') => { + console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ' — ' + detail : ''}`); + if (!cond) ok = false; +}; + +// --- dev server --- +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)); + +// helpers — NB innerText respects CSS text-transform, so match case-insensitively +const bodyText = () => page.evaluate(() => document.body.innerText); +const clickText = async (sel, text) => { + const found = await 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); + if (!found) throw new Error(`clickText: no ${sel} with "${text}"`); +}; +const waitText = async (text, ms = 20000) => { + for (let i = 0; i < ms / 250; i++) { + if ((await bodyText()).toLowerCase().includes(text.toLowerCase())) return true; + await sleep(250); + } + return false; +}; +// The number shown next to a label like "ark balance" (sats mode, comma groups) +const labeledNumber = async (label) => { + const txt = await bodyText(); + const esc = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const m = txt.match(new RegExp(esc + String.raw`[\s\n]*([\d,]+)`, 'i')); + return m ? Number(m[1].replaceAll(',', '')) : null; +}; +const waitLabeledNumber = async (label, want, ms = 30000) => { + for (let i = 0; i < ms / 500; i++) { + if (await labeledNumber(label) === want) return true; + await sleep(500); + } + return false; +}; + +try { + // --- configure: regtest, esplora on the ark chain, ark local --- + 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); + + // --- import a fresh wallet --- + console.log('\n[1] import wallet + ark connects'); + const mnemonic = generateMnemonic(wordlist); + await clickText('button', 'Import existing'); + await sleep(200); + await page.type('textarea', mnemonic); + await clickText('button', 'Open wallet'); + check('wallet opened', await waitText('receive', 15000)); + + check('ark connected (receive option present)', await (async () => { + for (let i = 0; i < 40; i++) { + const has = await page.evaluate(() => [...document.querySelectorAll('option')].some((o) => o.value === 'ark')); + if (has) return true; + await sleep(500); + } + return false; + })()); + + // --- receive over ark --- + console.log('\n[2] receive over Ark'); + await page.select('select', 'ark'); + await sleep(300); + const arkAddr = await page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim() || ''); + check('ark address shown', /^tark1[a-z0-9]+$/.test(arkAddr), arkAddr.slice(0, 24) + '…'); + sh(`${ALICE} -q send ${arkAddr} "25000 sat"`); + check('ark balance shows 25,000', await waitLabeledNumber('ark balance', 25000, 30000), String(await labeledNumber('ark balance'))); + + // --- send over ark --- + console.log('\n[3] send over Ark'); + const aliceAddr = sh(`${ALICE} -q address`); + const before = aliceBalance(); + await clickText('.tabs button', 'Send'); + await sleep(300); + await page.type('.mono-input', aliceAddr); + await sleep(400); // destReady re-render reveals the amount input + await page.type('input[type="number"]', '7000'); + await clickText('button.btn-primary', 'Review transaction'); + check('ark review shown', await waitText('Send over Ark', 5000)); + await clickText('button.btn-primary', 'Send'); + check('sent view', await waitText('Sent!', 15000)); + check('alice +7000', aliceBalance() === before + 7000, String(aliceBalance() - before)); + + // --- board from settings --- + console.log('\n[4] board'); + await clickText('.tabs button', 'Receive'); + await sleep(300); + await page.select('select', 'address'); + await sleep(300); + const onchainAddr = await page.evaluate(() => document.querySelector('.addr-box').textContent.trim()); + sh(`${BCLI} sendtoaddress ${onchainAddr} 0.001`); + mine(1); + check('onchain funds arrive', await waitText('100,000', 30000)); + + await clickText('.tabs button', 'Settings'); + await sleep(300); + const preBoard = await labeledNumber('ark balance'); + const boardInput = await page.evaluateHandle(() => + [...document.querySelectorAll('input')].find((i) => (i.placeholder || '').toLowerCase().startsWith('board amount'))); + await boardInput.asElement().type('30000'); + await clickText('button', 'Board'); + check('board started', await waitText('Boarding', 20000)); + mine(2); + check('board vtxo lands (+29,670)', await waitLabeledNumber('ark balance', preBoard + 29670, 40000), + String(await labeledNumber('ark balance'))); + + // --- refresh / consolidate --- + console.log('\n[5] refresh'); + await clickText('button', 'Refresh / consolidate'); + check('refresh submitted', await waitText('Refresh submitted', 10000)); + const done = await (async () => { + for (let i = 0; i < 45; i++) { + mine(1); + await sleep(2000); + if (await labeledNumber('coins (vtxos)') === 1 && !(await bodyText()).toLowerCase().includes('in progress')) return true; + } + return false; + })(); + check('refresh consolidated to 1 coin', done, String(await labeledNumber('coins (vtxos)'))); + console.log(` final ark balance: ${await labeledNumber('ark balance')} sats`); + + console.log(ok ? '\n✅ SUCCESS: Ark UI — receive, send, board, refresh all work in the browser' + : '\n❌ some checks failed'); +} finally { + await browser.close(); + dev.kill(); +} +process.exit(ok ? 0 : 1); |
