const { startWallet, fundWallet, sweepToFunder, teardownWallet, FUNDER_PORT } = require('./lib/setup');
const { WalletClient } = require('./lib/wallet-client');
const path = require('path');
const fs = require('fs');
const args = process.argv.slice(2);
const scenarioFilter = args.includes('--scenario') ? parseInt(args[args.indexOf('--scenario') + 1]) : null;
const dryRun = args.includes('--dry-run');
const skipWoc = args.includes('--skip-woc');
const scenarioFiles = fs.readdirSync(path.join(__dirname, 'scenarios'))
.filter(f => f.endsWith('.js'))
.sort();
const scenarios = scenarioFiles.map(f => require(`./scenarios/${f}`));
async function main() {
let walletA, walletB;
const results = [];
let funderBalanceBefore;
try {
const funder = new WalletClient(FUNDER_PORT, 'funder');
try { await funder.isAuthenticated(); } catch {
console.error(`ERROR: Funder wallet not running on :${FUNDER_PORT}`);
process.exit(1);
}
funderBalanceBefore = await funder.balance();
console.log('');
console.log('================================================================');
console.log(' bsv-wallet-cli E2E Test Suite');
console.log('================================================================');
console.log(` Funder: :${FUNDER_PORT} Balance: ${funderBalanceBefore.toLocaleString()} sats`);
console.log(' Starting test wallets...');
walletA = await startWallet(3323, 'wallet-A');
walletB = await startWallet(3324, 'wallet-B');
console.log(` Wallet A: :3323 (${walletA.identityKey.slice(0, 12)}...) ${walletA.address}`);
console.log(` Wallet B: :3324 (${walletB.identityKey.slice(0, 12)}...) ${walletB.address}`);
const fundAmount = parseInt(process.env.FUND_AMOUNT || '50000');
console.log(` Funding A with ${fundAmount.toLocaleString()} sats from funder...`);
const funded = await fundWallet(walletA, fundAmount);
console.log(` Funded: txid=${funded.txid.slice(0, 16)}...`);
const aBalance = await walletA.client.balance();
console.log(` A balance: ${aBalance.toLocaleString()} sats`);
console.log('================================================================');
if (dryRun) {
console.log(' --dry-run: skipping scenarios');
console.log('================================================================');
return;
}
const toRun = scenarioFilter !== null
? scenarios.filter((_, i) => i + 1 === scenarioFilter)
: scenarios;
for (let i = 0; i < toRun.length; i++) {
const scenario = toRun[i];
const num = scenarioFilter || (i + 1);
const assertions = [];
let failed = false;
function assert(condition, message) {
if (!condition) {
failed = true;
assertions.push(`FAIL: ${message}`);
throw new Error(message);
}
assertions.push(`OK: ${message}`);
}
const aBalBefore = await walletA.client.balance();
const bBalBefore = await walletB.client.balance();
console.log(` [${num}] ${scenario.name}: ${scenario.description}`);
const start = Date.now();
try {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('TIMEOUT: scenario exceeded 120s')), 120_000));
const result = await Promise.race([scenario.run(walletA, walletB, assert), timeout]);
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
const aBalAfter = await walletA.client.balance();
const bBalAfter = await walletB.client.balance();
const aDelta = aBalAfter - aBalBefore;
const bDelta = bBalAfter - bBalBefore;
const netLeak = -(aDelta + bDelta);
console.log(` PASS (${elapsed}s) txid=${result.txid?.slice(0, 12) || 'n/a'}... woc=${result.wocConfirmed || 'n/a'}`);
console.log(` A: ${aBalAfter} (${aDelta >= 0 ? '+' : ''}${aDelta}) B: ${bBalAfter} (${bDelta >= 0 ? '+' : ''}${bDelta}) net: ${netLeak > 0 ? '-' : '+'}${Math.abs(netLeak)} sats`);
results.push({ num, name: scenario.name, status: 'PASS', elapsed, netLeak, ...result });
} catch (e) {
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(` FAIL (${elapsed}s): ${e.message}`);
results.push({ num, name: scenario.name, status: 'FAIL', elapsed, error: e.message });
}
}
} finally {
console.log('================================================================');
console.log(' Sweeping remaining funds back to funder...');
let sweptTotal = 0;
if (walletB) {
try { sweptTotal += await sweepToFunder(walletB); } catch (e) {
console.log(` WARNING: B sweep failed: ${e.message}`);
}
}
if (walletA) {
try { sweptTotal += await sweepToFunder(walletA); } catch (e) {
console.log(` WARNING: A sweep failed: ${e.message}`);
}
}
if (walletB) teardownWallet(walletB);
if (walletA) teardownWallet(walletA);
const funderClient = new WalletClient(FUNDER_PORT, 'funder');
const funderBalanceAfter = await funderClient.balance();
const passed = results.filter(r => r.status === 'PASS').length;
const total = results.length;
const netCost = funderBalanceBefore - funderBalanceAfter;
console.log('');
console.log('================================================================');
console.log(' RESULTS');
console.log('================================================================');
for (const r of results) {
const icon = r.status === 'PASS' ? 'OK' : '!!';
const leak = r.netLeak != null ? ` leak=${r.netLeak}` : '';
console.log(` [${icon}] ${r.num}. ${r.name}: ${r.status} (${r.elapsed}s)${leak}`);
}
const totalLeak = results.reduce((s, r) => s + (r.netLeak || 0), 0);
console.log('----------------------------------------------------------------');
console.log(` Passed: ${passed}/${total}`);
console.log(` Funder before: ${funderBalanceBefore.toLocaleString()} sats`);
console.log(` Funder after: ${funderBalanceAfter.toLocaleString()} sats`);
console.log(` Net cost: ${netCost.toLocaleString()} sats`);
console.log(` Scenario leak: ${totalLeak.toLocaleString()} sats (sum of per-scenario A+B deltas)`);
console.log('================================================================');
const MAX_ACCEPTABLE_COST = 5_000;
if (netCost > MAX_ACCEPTABLE_COST) {
console.log(` BUDGET EXCEEDED: net cost ${netCost} > max ${MAX_ACCEPTABLE_COST} sats`);
process.exit(1);
}
process.exit(passed === total ? 0 : 1);
}
}
main().catch(e => {
console.error('FATAL:', e);
process.exit(1);
});