aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark/board-fee-preview-test.js
blob: 0ff96cdc86616734b7bf8de40572ffbe7bfc68ea (plain) (blame)
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
// Fee previews use the real wallet transaction builder, without signing or
// broadcasting anything. Run: bun tools/ark/board-fee-preview-test.js
import assert from 'node:assert/strict';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { Wallet } from '../../src/wallet.js';
import { previewBoardFunding } from '../../src/features/ark.js';
import { p2trAddress } from '../../src/ark/board.js';

const wallet = new Wallet();
wallet.load({ mnemonic: generateMnemonic(wordlist), netName: 'mainnet', offline: true });
wallet.utxos = [{ txid: 'a'.repeat(64), vout: 0, value: 300000, chain: 0, index: 0, confirmed: true }];
wallet.feeRates = { halfHourFee: 1 };
const ark = { info: { network: 'bitcoin', boardFees: {} }, _key: () => ({ pubkey: wallet.derive(0, 0).pubkey }) };
// A different P2TR program stands in for the final funding address. Its
// output size matches the preview while its key is unrelated to the probe.
const fundingAddress = p2trAddress(wallet.derive(0, 1).pubkey.slice(1), 'bc');
const actualDraft = sats => wallet.buildTx({ recipients: [{ address: fundingAddress, amount: sats }],
  feeRate: wallet.feeRates.halfHourFee, noSort: true });

const max = previewBoardFunding(wallet, ark, 0, true);
assert.deepEqual(max, { fundingSat: 299878, chainFeeSat: 122, serviceFeeSat: 0, feeSat: 122, netSat: 299878 });
const maxPreview = previewBoardFunding(wallet, ark, max.fundingSat);
assert.deepEqual(maxPreview, max);
assert.equal(maxPreview.chainFeeSat, actualDraft(max.fundingSat).fee);
assert.equal(maxPreview.netSat + maxPreview.feeSat, 300000);
console.log('✓ 300,000-sat Max shows a 122-sat fee and 299,878 sats arriving');

const partial = previewBoardFunding(wallet, ark, 100000);
assert.equal(partial.netSat, 100000, 'the mining fee is paid from Savings, not deducted again from the deposit');
assert.equal(partial.chainFeeSat, actualDraft(100000).fee);
assert.ok(partial.chainFeeSat > max.chainFeeSat, 'partial deposits include the change output fee');
console.log('✓ partial deposits include the change output and preserve the correct received amount');

ark.info.boardFees = { baseFeeSat: 25, ppm: 1000 };
const service = previewBoardFunding(wallet, ark, max.fundingSat);
assert.equal(service.serviceFeeSat, 325);
assert.equal(service.feeSat, 447);
assert.equal(service.netSat, 299553);
assert.equal(service.netSat + service.feeSat, 300000);
console.log('✓ nonzero service fees are included in the total and deducted exactly once');

ark.info.boardFees = {};
wallet.utxos = [
  { txid: 'a'.repeat(64), vout: 0, value: 200000, chain: 0, index: 0, confirmed: true },
  { txid: 'b'.repeat(64), vout: 0, value: 100000, chain: 0, index: 1, confirmed: true },
];
wallet.feeRates.halfHourFee = 1.6;
const multi = previewBoardFunding(wallet, ark, 0, true);
assert.equal(multi.chainFeeSat, actualDraft(multi.fundingSat).fee);
assert.equal(multi.netSat + multi.feeSat, 300000);
assert.ok(multi.feeSat > max.feeSat);
assert.throws(() => previewBoardFunding(wallet, ark, 300000), /Insufficient funds/);
console.log('✓ multiple inputs, fee-rate rounding, and insufficient funds follow the actual funding transaction');
process.exit(0);
fallback: document.querySelectorAll('.chat-avatar.fallback').length, header: !!document.querySelector('.header-ava'), headerIsImg: !!document.querySelector('.header-ava.ava-img'), offlineBanner: document.body.innerText.includes('offline') || document.body.innerText.includes('Offline'), })); await page.setViewport({ width: 420, height: 860 }); await page.goto('http://localhost:5234/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); await click('button', 'Create a new wallet'); await sleep(300); await click('button', 'Have an existing seed'); await sleep(300); await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('button', 'Open wallet'); await waitText('receive', 15000); await openMessages(); await sleep(800); await openCoinos(); console.log('online room:', await waitText('GM Coinos community', 30000)); await sleep(6000); // thumbnails, profiles, caches settle console.log('online snapshot:', JSON.stringify(await snap())); const sw = await page.evaluate(async () => { const r = await navigator.serviceWorker.getRegistration(); const ks = await caches.keys(); const c = ks.length ? await (await caches.open(ks[0])).keys() : []; return { reg: !!r, active: !!(r && r.active), caches: ks, cached: c.length }; }); console.log('sw:', JSON.stringify(sw)); const cdp = await page.target().createCDPSession(); await cdp.send('Network.enable'); await cdp.send('Network.emulateNetworkConditions', { offline: true, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }); await sleep(500); try { await page.reload({ waitUntil: 'domcontentloaded', timeout: 20000 }); } catch (e) { console.log('reload:', e.message.slice(0, 100)); } await sleep(5000); console.log('offline boot:', JSON.stringify(await snap())); await openMessages(); await sleep(1500); console.log('offline messages home:', JSON.stringify(await snap())); await openCoinos(); await sleep(1500); const room = await snap(); console.log('offline room:', JSON.stringify(room).slice(0, 300)); let ok = true; const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; check('the room shows cached messages offline', room.bubbles >= 20, `${room.bubbles} bubbles`); check('most faces paint from local thumbnails', room.avaImgData >= room.avaImg * 0.6, `${room.avaImgData} of ${room.avaImg}`); check('names resolve from the profile cache', !/npub1[a-z0-9]{8}/.test(room.text.slice(0, 120)) || room.text.includes('Adam'), room.text.slice(40, 120)); await page.screenshot({ path: process.argv[2] || '/tmp/offline.png' }); await browser.close(); server.stop(true); console.log(ok ? '\nALL GOOD' : '\nFAILURES'); process.exit(ok ? 0 : 1);