Brings the whole project into git (previously only the on-ramp feature and a few parallel files were tracked). Includes: wallet core (src/), CashScript contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text). Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB), transcripts/ (session logs), and machine-local config (.claude/*.local, *.lock). Secret-scanned before commit — no credentials in the tree. Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are legacy (OP_RETURN era) but still imported; committed as-is, flagged for a separate removal once de-referenced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
184 lines
8 KiB
JavaScript
184 lines
8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* dashboard-serve.mjs — tiny HTTP dashboard for all chipnet wallets.
|
|
*
|
|
* Discovers every `.chipnet-keys-*.json` at the repo root, queries the chipnet
|
|
* electrum for each, and serves:
|
|
* GET / → an HTML page that auto-polls /api/balances every N seconds
|
|
* GET /api/balances → live JSON snapshot
|
|
*
|
|
* Run: rtk node scripts/dashboard-serve.mjs (default port 5180, 15s poll)
|
|
* rtk node scripts/dashboard-serve.mjs --port=8080 --interval=10
|
|
* Then: open http://localhost:5180/
|
|
*
|
|
* The Node side does the electrum work (the same code path as chipnet-balances.mjs);
|
|
* the browser just renders. Ctrl+C to stop.
|
|
*/
|
|
import http from 'node:http';
|
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TestNetWallet } from 'mainnet-js';
|
|
import { ElectrumNetworkProvider } from 'cashscript';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
|
|
const args = process.argv.slice(2);
|
|
const portArg = args.find((a) => a.startsWith('--port='));
|
|
const intervalArg = args.find((a) => a.startsWith('--interval='));
|
|
const PORT = portArg ? Number(portArg.split('=')[1]) : 5180;
|
|
const INTERVAL_SEC = intervalArg ? Number(intervalArg.split('=')[1]) : 15;
|
|
|
|
function discoverWallets() {
|
|
return readdirSync(ROOT)
|
|
.filter((f) => /^\.chipnet-keys(-[a-z0-9]+)?\.json$/.test(f))
|
|
.map((f) => {
|
|
const m = /^\.chipnet-keys(?:-([a-z0-9]+))?\.json$/.exec(f);
|
|
const label = m?.[1] ? m[1].toUpperCase() : 'A';
|
|
return { label, keys: join(ROOT, f) };
|
|
})
|
|
.sort((a, b) => a.label.localeCompare(b.label));
|
|
}
|
|
|
|
async function fetchAll(provider) {
|
|
const wallets = discoverWallets();
|
|
const rows = [];
|
|
for (const { label, keys } of wallets) {
|
|
if (!existsSync(keys)) continue;
|
|
try {
|
|
const { wif } = JSON.parse(readFileSync(keys, 'utf8'));
|
|
const wallet = await TestNetWallet.fromWIF(wif);
|
|
const utxos = await provider.getUtxos(wallet.cashaddr);
|
|
const bch = utxos.filter((u) => !u.token);
|
|
const tok = utxos.filter((u) => u.token);
|
|
const bchSats = bch.reduce((s, u) => s + BigInt(u.satoshis), 0n);
|
|
const lockedSats = tok.reduce((s, u) => s + BigInt(u.satoshis), 0n);
|
|
const cats = new Set(tok.map((u) => u.token.category));
|
|
rows.push({
|
|
label,
|
|
cashaddr: wallet.cashaddr,
|
|
bchSats: bchSats.toString(),
|
|
bchUtxos: bch.length,
|
|
tokUtxos: tok.length,
|
|
lockedSats: lockedSats.toString(),
|
|
cats: cats.size,
|
|
});
|
|
} catch (e) {
|
|
rows.push({ label, error: e?.message ?? String(e) });
|
|
}
|
|
}
|
|
return { ts: new Date().toISOString(), rows };
|
|
}
|
|
|
|
const PAGE = (interval) => `<!doctype html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8" /><title>Deviant · chipnet dashboard</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<style>
|
|
:root { color-scheme: dark; --bg:#0c0e12; --fg:#dfe7ee; --muted:#7d8a98; --acid:#a5f050; --row:#161a22; --row2:#1c2230; --bord:#2a3346; }
|
|
body { margin:0; padding:24px; background:var(--bg); color:var(--fg); font:14px/1.4 ui-sans-serif,system-ui,sans-serif; }
|
|
h1 { font-size:18px; font-weight:600; margin:0 0 4px; }
|
|
.sub { color:var(--muted); font-size:12px; margin-bottom:18px; }
|
|
.sub .acid { color:var(--acid); }
|
|
table { border-collapse:collapse; width:100%; max-width:980px; background:var(--row); border:1px solid var(--bord); }
|
|
th, td { padding:9px 12px; border-bottom:1px solid var(--bord); text-align:left; }
|
|
tr:last-child td { border-bottom:none; }
|
|
tr:nth-child(even) td { background:var(--row2); }
|
|
th { font-weight:600; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.06em; }
|
|
td.num { font-variant-numeric:tabular-nums; text-align:right; font-family:ui-monospace,monospace; }
|
|
td.addr { font-family:ui-monospace,monospace; font-size:12px; color:#bcc4cf; }
|
|
.lbl { display:inline-block; padding:2px 8px; border-radius:11px; background:rgba(165,240,80,.12); border:1px solid rgba(165,240,80,.35); color:var(--acid); font-weight:600; font-size:11px; }
|
|
.err { color:#ff8079; font-style:italic; }
|
|
.total { background:#10141a !important; font-weight:600; }
|
|
.footer { color:var(--muted); font-size:11px; margin-top:14px; }
|
|
</style></head>
|
|
<body>
|
|
<h1>Deviant · chipnet wallet dashboard</h1>
|
|
<div class="sub">Auto-refreshing every <span class="acid">${interval}s</span> · last update <span id="ts">—</span></div>
|
|
<table>
|
|
<thead><tr>
|
|
<th>Wallet</th><th>Address</th>
|
|
<th style="text-align:right">Plain BCH (sats)</th>
|
|
<th style="text-align:right">Plain BCH</th>
|
|
<th style="text-align:right">BCH UTXOs</th>
|
|
<th style="text-align:right">Tok UTXOs</th>
|
|
<th style="text-align:right">Locked (sats)</th>
|
|
<th style="text-align:right">Cats</th>
|
|
</tr></thead>
|
|
<tbody id="rows"><tr><td colspan="8" style="color:#7d8a98;text-align:center">Loading…</td></tr></tbody>
|
|
</table>
|
|
<div class="footer">Served by <code>scripts/dashboard-serve.mjs</code>. Source of truth: chipnet electrum (<code>ElectrumNetworkProvider</code>).</div>
|
|
|
|
<script>
|
|
const fmtBch = (sats) => (Number(sats) / 1e8).toFixed(8);
|
|
const shortAddr = (a) => a ? a.slice(0, 14) + '…' + a.slice(-8) : '';
|
|
async function tick() {
|
|
try {
|
|
const r = await fetch('/api/balances', { cache: 'no-store' });
|
|
const { ts, rows } = await r.json();
|
|
document.getElementById('ts').textContent = new Date(ts).toLocaleString();
|
|
let totalBch = 0n, totalLocked = 0n;
|
|
let html = '';
|
|
for (const row of rows) {
|
|
if (row.error) {
|
|
html += '<tr><td><span class="lbl">' + row.label + '</span></td><td colspan="7" class="err">' + row.error + '</td></tr>';
|
|
continue;
|
|
}
|
|
totalBch += BigInt(row.bchSats);
|
|
totalLocked += BigInt(row.lockedSats);
|
|
html += '<tr>'
|
|
+ '<td><span class="lbl">' + row.label + '</span></td>'
|
|
+ '<td class="addr" title="' + row.cashaddr + '">' + shortAddr(row.cashaddr) + '</td>'
|
|
+ '<td class="num">' + Number(row.bchSats).toLocaleString() + '</td>'
|
|
+ '<td class="num">' + fmtBch(row.bchSats) + '</td>'
|
|
+ '<td class="num">' + row.bchUtxos + '</td>'
|
|
+ '<td class="num">' + row.tokUtxos + '</td>'
|
|
+ '<td class="num">' + Number(row.lockedSats).toLocaleString() + '</td>'
|
|
+ '<td class="num">' + row.cats + '</td>'
|
|
+ '</tr>';
|
|
}
|
|
html += '<tr class="total"><td colspan="2">Σ all wallets</td>'
|
|
+ '<td class="num">' + Number(totalBch).toLocaleString() + '</td>'
|
|
+ '<td class="num">' + fmtBch(totalBch) + '</td>'
|
|
+ '<td class="num">—</td><td class="num">—</td>'
|
|
+ '<td class="num">' + Number(totalLocked).toLocaleString() + '</td>'
|
|
+ '<td class="num">—</td></tr>';
|
|
document.getElementById('rows').innerHTML = html;
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
tick();
|
|
setInterval(tick, ${interval} * 1000);
|
|
</script>
|
|
</body></html>`;
|
|
|
|
async function main() {
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
try {
|
|
if (req.url === '/' || req.url === '/index.html') {
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(PAGE(INTERVAL_SEC));
|
|
return;
|
|
}
|
|
if (req.url === '/api/balances') {
|
|
const snapshot = await fetchAll(provider);
|
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
res.end(JSON.stringify(snapshot));
|
|
return;
|
|
}
|
|
res.writeHead(404); res.end('not found');
|
|
} catch (e) {
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
res.end(`error: ${e?.message ?? e}`);
|
|
}
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
const n = discoverWallets().length;
|
|
console.log(`Deviant chipnet dashboard — ${n} wallets discovered`);
|
|
console.log(` http://localhost:${PORT}/ (HTML, refreshes every ${INTERVAL_SEC}s)`);
|
|
console.log(` http://localhost:${PORT}/api/balances (live JSON)`);
|
|
console.log(`Ctrl+C to stop.`);
|
|
});
|
|
}
|
|
main().catch((e) => { console.error('FAILED:', e?.message ?? e); process.exit(1); });
|