Snapshot of the shared working tree's outstanding changes (multiple concurrent workstreams), excluding generated runtime state files (.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html). Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens, currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters, a telegram-orderbook subproject, design mockups, and docs. See shells/app/WALLET-COORDINATION.md for the per-file ownership split. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
243 lines
12 KiB
HTML
243 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Deviant · chipnet address explorer</title>
|
|
<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); }
|
|
.sub .err { color:#ff8079; }
|
|
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; line-height:1.6; }
|
|
.footer code { color:#bcc4cf; }
|
|
.dot { display:inline-block; width:7px; height:7px; border-radius:50%; vertical-align:middle; margin-right:6px; background:var(--muted); }
|
|
.dot.on { background:var(--acid); box-shadow:0 0 5px var(--acid); }
|
|
.dot.err { background:#ff8079; }
|
|
|
|
/* Token-family switcher: single-select chips above the table */
|
|
.filter { display:flex; gap:7px; max-width:980px; margin: 0 0 12px; }
|
|
.chip { font: 11px/1 ui-monospace,monospace; letter-spacing:.08em; text-transform:uppercase; padding:8px 14px; border-radius:18px; border:1px solid var(--bord); background:transparent; color:var(--muted); cursor:pointer; transition: color .15s, border-color .15s, background .15s; }
|
|
.chip:hover { color:#bcc4cf; }
|
|
.chip.on { color:var(--acid); border-color:rgba(165,240,80,.45); background:rgba(165,240,80,.07); text-shadow:0 0 5px rgba(165,240,80,.5); }
|
|
|
|
/* Show only the active family's columns. data-col="bch"|"mba"|"nba". */
|
|
table[data-fam="mba"] [data-col="nba"] { display:none; }
|
|
table[data-fam="nba"] [data-col="mba"] { display:none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Deviant · chipnet address explorer</h1>
|
|
<div class="sub">
|
|
<span class="dot" id="conn-dot"></span><span id="conn-status">connecting…</span>
|
|
· queries <span class="acid">chipnet.imaginary.cash</span> directly from your browser
|
|
· last update <span id="ts">—</span>
|
|
</div>
|
|
<div class="filter">
|
|
<button class="chip on" data-fam="mba">MBA</button>
|
|
<button class="chip" data-fam="nba">NBA</button>
|
|
</div>
|
|
<table id="tbl" data-fam="mba">
|
|
<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" data-col="mba" title="Sum of token_data.amount across MBA-class UTXOs (per-UTXO sats > 1,000 threshold)">MBA units</th>
|
|
<th style="text-align:right" data-col="mba" title="Σ sats locked inside MBA UTXOs — the PNV (redeemable BCH floor)">MBA (sats)</th>
|
|
<th style="text-align:right" data-col="mba" title="MBA (sats) ÷ 1e8 — the PNV in BCH units">MBA (BCH)</th>
|
|
<th style="text-align:right" data-col="nba" title="Sum of token_data.amount across NBA-class UTXOs (dust-only sat values)">NBA units</th>
|
|
<th style="text-align:right">Cats</th>
|
|
</tr></thead>
|
|
<tbody id="rows"><tr><td colspan="10" style="color:#7d8a98;text-align:center">Loading addresses…</td></tr></tbody>
|
|
</table>
|
|
<div class="footer">
|
|
Address list: inlined below. To update, edit the <code>WALLETS</code> constant in this file or regenerate via <code>rtk node scripts/derive-chipnet-addresses.mjs</code> (copies into <code>design/explorer/addresses.json</code> as a backup snapshot).<br>
|
|
Electrum: <code>wss://chipnet.imaginary.cash:50004</code> · public chipnet ElectrumX (CashTokens-aware). No private keys in this page.
|
|
</div>
|
|
|
|
<script type="module">
|
|
const ENDPOINT = 'wss://chipnet.imaginary.cash:50004';
|
|
const POLL_MS = 15000;
|
|
const VERSION_CLIENT = 'deviant-explorer/0.1';
|
|
const VERSION_PROTOCOL = '1.5.3'; // CashTokens-aware (Fulcrum gates token_data behind ≥1.5)
|
|
|
|
// --- tiny electrum JSON-RPC over WSS client ----------------------------------
|
|
let ws = null;
|
|
let nextId = 1;
|
|
const pending = new Map();
|
|
|
|
function connect() {
|
|
return new Promise((resolve, reject) => {
|
|
ws = new WebSocket(ENDPOINT);
|
|
ws.onopen = async () => {
|
|
try {
|
|
await call('server.version', [VERSION_CLIENT, VERSION_PROTOCOL]);
|
|
setConn('on', 'connected');
|
|
resolve();
|
|
} catch (e) { reject(e); }
|
|
};
|
|
ws.onmessage = (evt) => {
|
|
for (const line of evt.data.split('\n')) {
|
|
if (!line.trim()) continue;
|
|
let msg;
|
|
try { msg = JSON.parse(line); } catch { continue; }
|
|
if (msg.id != null && pending.has(msg.id)) {
|
|
const { resolve: r, reject: j } = pending.get(msg.id);
|
|
pending.delete(msg.id);
|
|
msg.error ? j(new Error(msg.error?.message || JSON.stringify(msg.error))) : r(msg.result);
|
|
}
|
|
}
|
|
};
|
|
ws.onclose = () => { setConn('err', 'disconnected — retrying in 5s'); setTimeout(() => connect().then(tick).catch(() => {}), 5000); };
|
|
ws.onerror = () => { setConn('err', 'connection error'); };
|
|
});
|
|
}
|
|
function call(method, params = []) {
|
|
const id = nextId++;
|
|
return new Promise((resolve, reject) => {
|
|
pending.set(id, { resolve, reject });
|
|
ws.send(JSON.stringify({ id, method, params }) + '\n');
|
|
setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error('timeout: ' + method)); } }, 20000);
|
|
});
|
|
}
|
|
|
|
// --- UI helpers --------------------------------------------------------------
|
|
const fmtBch = (sats) => (Number(sats) / 1e8).toFixed(8);
|
|
const shortAddr = (a) => a ? a.slice(0, 14) + '…' + a.slice(-8) : '';
|
|
function setConn(state, text) {
|
|
const dot = document.getElementById('conn-dot');
|
|
dot.className = 'dot ' + state;
|
|
document.getElementById('conn-status').textContent = text;
|
|
}
|
|
|
|
// --- balance fetch -----------------------------------------------------------
|
|
// Heuristic split: a token UTXO with per-UTXO sats > MBA_MIN_SATS is treated
|
|
// as MBA (has BCH backing inside it — that's its PNV). Token UTXOs at or near
|
|
// dust (NBA / NFTs) hold only ~546 sats. Replace with a registry lookup if you
|
|
// need strict classification.
|
|
const MBA_MIN_SATS = 1000n;
|
|
async function balanceFor(cashaddr) {
|
|
const utxos = await call('blockchain.address.listunspent', [cashaddr]);
|
|
let bchSats = 0n, mbaSats = 0n, mbaUnits = 0n, nbaUnits = 0n, bchUtxos = 0;
|
|
const cats = new Set();
|
|
for (const u of utxos) {
|
|
const v = BigInt(u.value);
|
|
if (u.token_data) {
|
|
const amt = BigInt(u.token_data.amount || 0);
|
|
cats.add(u.token_data.category);
|
|
if (v > MBA_MIN_SATS) { mbaSats += v; mbaUnits += amt; }
|
|
else { nbaUnits += amt; }
|
|
} else {
|
|
bchSats += v; bchUtxos++;
|
|
}
|
|
}
|
|
return {
|
|
cashaddr,
|
|
bchSats: bchSats.toString(),
|
|
bchUtxos,
|
|
mbaUnits: mbaUnits.toString(),
|
|
mbaSats: mbaSats.toString(),
|
|
nbaUnits: nbaUnits.toString(),
|
|
cats: cats.size,
|
|
};
|
|
}
|
|
|
|
// --- inlined wallet list -----------------------------------------------------
|
|
// No JSON fetch, no server dependency — the page works from any origin (incl.
|
|
// file://). Edit this list directly or regenerate with
|
|
// rtk node scripts/derive-chipnet-addresses.mjs
|
|
// then paste the result here.
|
|
const WALLETS = [
|
|
{ label: 'A', cashaddr: 'bchtest:qrd5g74f64gp704n4ggspcwn08e0yalz3u2ltaxuwh' },
|
|
{ label: 'B', cashaddr: 'bchtest:qzd2dw4tk2amr5v5pe8vs2ukgzn6kc4cmsv9t3eyc6' },
|
|
{ label: 'C', cashaddr: 'bchtest:qzgda5hsk96trkfk4qxa0t808kjwt8gzt5aqnt38dm' },
|
|
{ label: 'D', cashaddr: 'bchtest:qzjqnzgjz7w9tdflpz6ufx0rnrnfu4hsps9rznw4dl' },
|
|
{ label: 'E', cashaddr: 'bchtest:qpyns8t44guzvcstqgs7wvueq6n6hqgu7cpqzsfhha' },
|
|
{ label: 'F', cashaddr: 'bchtest:qr4f5sh8lhs50u7xq5568rcqxwk209dz4ynanfnyhh' },
|
|
{ label: 'G', cashaddr: 'bchtest:qp20fvzqy43q7u43erhjxg52w39mqn7cccyqnlkaqu' },
|
|
];
|
|
async function tick() {
|
|
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
const rows = [];
|
|
for (const w of WALLETS) {
|
|
try {
|
|
const bal = await balanceFor(w.cashaddr);
|
|
rows.push({ label: w.label, ...bal });
|
|
} catch (e) {
|
|
rows.push({ label: w.label, error: e.message });
|
|
}
|
|
}
|
|
render(rows);
|
|
document.getElementById('ts').textContent = new Date().toLocaleString();
|
|
}
|
|
function render(rows) {
|
|
let totalBch = 0n, totalMbaSats = 0n, totalMbaUnits = 0n, totalNbaUnits = 0n, html = '';
|
|
for (const row of rows) {
|
|
if (row.error) {
|
|
html += '<tr><td><span class="lbl">' + row.label + '</span></td><td colspan="9" class="err">' + row.error + '</td></tr>';
|
|
continue;
|
|
}
|
|
totalBch += BigInt(row.bchSats);
|
|
totalMbaSats += BigInt(row.mbaSats);
|
|
totalMbaUnits += BigInt(row.mbaUnits);
|
|
totalNbaUnits += BigInt(row.nbaUnits);
|
|
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" data-col="mba">' + Number(row.mbaUnits).toLocaleString() + '</td>'
|
|
+ '<td class="num" data-col="mba">' + Number(row.mbaSats).toLocaleString() + '</td>'
|
|
+ '<td class="num" data-col="mba">' + fmtBch(row.mbaSats) + '</td>'
|
|
+ '<td class="num" data-col="nba">' + Number(row.nbaUnits).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" data-col="mba">' + Number(totalMbaUnits).toLocaleString() + '</td>'
|
|
+ '<td class="num" data-col="mba">' + Number(totalMbaSats).toLocaleString() + '</td>'
|
|
+ '<td class="num" data-col="mba">' + fmtBch(totalMbaSats) + '</td>'
|
|
+ '<td class="num" data-col="nba">' + Number(totalNbaUnits).toLocaleString() + '</td>'
|
|
+ '<td class="num">—</td></tr>';
|
|
document.getElementById('rows').innerHTML = html;
|
|
}
|
|
|
|
// Family switcher — single-select MBA / NBA. Default MBA.
|
|
document.querySelectorAll('.chip[data-fam]').forEach((chip) => {
|
|
chip.addEventListener('click', () => {
|
|
document.querySelectorAll('.chip[data-fam]').forEach((c) => c.classList.remove('on'));
|
|
chip.classList.add('on');
|
|
document.getElementById('tbl').setAttribute('data-fam', chip.getAttribute('data-fam'));
|
|
});
|
|
});
|
|
|
|
// --- boot --------------------------------------------------------------------
|
|
(async () => {
|
|
try { await connect(); }
|
|
catch (e) {
|
|
document.getElementById('rows').innerHTML = '<tr><td colspan="10" class="err">Could not reach the chipnet electrum server — check connectivity.</td></tr>';
|
|
return;
|
|
}
|
|
await tick();
|
|
setInterval(tick, POLL_MS);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|