-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathobserver.html
More file actions
65 lines (65 loc) · 18.3 KB
/
Copy pathobserver.html
File metadata and controls
65 lines (65 loc) · 18.3 KB
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
56
57
58
59
60
61
62
63
64
65
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover">
<title>Whirlpool.Observer</title>
<meta name="application-name" content="Whirlpool.Observer">
<meta name="apple-mobile-web-app-title" content="Whirlpool.Observer">
<meta name="theme-color" content="#000000">
<meta name="description" content="Whirlpool.Observer is a minimal dashboard for exploring Ashigaru Whirlpool CoinJoin Cycles and TX0 activity.">
<meta name="robots" content="index,follow">
<link rel="canonical" href="__BASE_URL__/">
<link rel="icon" type="image/png" href="/assets/Ashigaru_Whirlpool_Logo_White.png">
<link rel="shortcut icon" type="image/png" href="/assets/Ashigaru_Whirlpool_Logo_White.png">
<link rel="apple-touch-icon" href="/assets/Ashigaru_Whirlpool_Logo_White.png">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Whirlpool.Observer">
<meta property="og:title" content="Whirlpool.Observer">
<meta property="og:description" content="Explore Ashigaru Whirlpool CoinJoin cycle history and TX0 activity.">
<meta property="og:url" content="__BASE_URL__/">
<meta property="og:logo" content="__BASE_URL__/assets/Ashigaru_Whirlpool_Logo_White.png">
<meta property="og:image" content="__BASE_URL__/assets/social.png">
<meta property="og:image:secure_url" content="__BASE_URL__/assets/social.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:alt" content="Whirlpool.Observer social preview image">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Whirlpool.Observer">
<meta name="twitter:description" content="Explore Ashigaru Whirlpool CoinJoin cycle history and TX0 activity.">
<meta name="twitter:image" content="__BASE_URL__/assets/social.png">
<meta name="twitter:image:alt" content="Whirlpool.Observer social preview image">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root{color-scheme:dark;--bg:#000;--panel:#090909;--line:#262626;--text:#f5f5f7;--muted:#8e8e93;--white:#f5f5f7;--grey:#8e8e93}*{box-sizing:border-box}html,body{margin:0;background:#000;color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",system-ui,sans-serif}body{min-height:100vh;overflow-x:hidden}.wrap{width:min(1480px,100%);margin:0 auto;padding:28px}.hero{margin:10px 0 24px;text-align:center}.brand{display:inline-flex;align-items:center;justify-content:center;gap:14px}.brand-logo{width:52px;height:52px;object-fit:contain;flex:0 0 auto}.title{margin:0;font-size:clamp(38px,7vw,58px);line-height:.96;letter-spacing:-.055em;font-weight:820}.grid{display:grid;gap:14px}.cards{grid-template-columns:repeat(4,minmax(0,1fr))}.poolcards{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.charts{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.card,.chart,.table,.reference,.footer{background:var(--panel);border:1px solid var(--line);border-radius:22px;padding:20px;box-shadow:none}.card,.metric{text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center}.label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.11em}.value{font-size:32px;font-weight:760;letter-spacing:-.04em;margin-top:8px;overflow-wrap:anywhere}.small{font-size:13px;color:var(--muted);margin-top:8px}.progress{height:10px;background:#1a1a1a;border:1px solid var(--line);border-radius:999px;overflow:hidden;margin-top:14px;width:100%}.bar{height:100%;background:var(--white);border-radius:999px}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px;width:100%}.metric{background:#000;border:1px solid var(--line);border-radius:16px;padding:12px}.metric .n{font-weight:740;font-size:20px;margin-top:4px}.chart{min-width:0}.chart h2,.table h2,.reference h2{margin:0 0 14px;font-size:20px;letter-spacing:-.03em;text-align:center}.chart-box{position:relative;width:100%;height:340px;max-width:100%;overflow:hidden}.chart-box canvas{width:100%!important;height:100%!important;display:block}.cycle-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.tabs{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.tab,.pager button{border:1px solid var(--line);background:#000;color:var(--text);border-radius:999px;padding:9px 12px;cursor:pointer}.tab.active{background:var(--white);color:#000;border-color:var(--white)}.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}table{width:100%;border-collapse:collapse}td,th{border-bottom:1px solid var(--line);padding:12px 8px;text-align:left;color:#e5e5ea;vertical-align:top}th{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}.txlink{color:var(--text);font-family:"SF Mono",ui-monospace,monospace;font-size:13px;text-decoration:none;word-break:break-all}.txlink:hover{text-decoration:underline}.tx0-list,.tx0-list a{font-family:"SF Mono",ui-monospace,monospace;font-size:13px;color:var(--text);line-height:1.55}.tx0-list a{text-decoration:none}.tx0-list a:hover{text-decoration:underline}.pooldot{display:inline-block;width:10px;height:10px;border-radius:999px;margin-right:8px;border:1px solid var(--line)}.pager{display:flex;justify-content:center;align-items:center;gap:12px;margin-top:14px;color:var(--muted)}.pager button:disabled{opacity:.35;cursor:not-allowed}.mobile-cards{display:none}.mobile-cycle-card{border:1px solid var(--line);border-radius:16px;padding:12px;margin-top:10px;background:#000;color:var(--muted)}.empty{color:var(--muted)}details{border-top:1px solid var(--line);padding:14px 0}details:first-of-type{border-top:0}summary{cursor:pointer;font-weight:700;color:var(--text)}details p{color:var(--muted);line-height:1.55;margin:10px 0 0}.reference{margin-top:14px}.reference a,.footer a{color:var(--text);text-decoration:none}.reference a:hover,.footer a:hover{text-decoration:underline}.footer{margin-top:14px;display:flex;justify-content:center;align-items:center;gap:10px;flex-wrap:wrap;color:var(--muted);font-size:14px}.github-icon{width:16px;height:16px;vertical-align:-3px;fill:currentColor;margin-right:6px}.sep{color:#444}@media(max-width:980px){.wrap{padding:16px}.cards,.poolcards,.charts{grid-template-columns:1fr}.card,.chart,.table,.reference,.footer{border-radius:18px;padding:16px}.metrics{grid-template-columns:repeat(2,1fr)}.cycle-head{display:block}.tabs{justify-content:flex-start;margin-top:12px}.desktop-table{display:none}.mobile-cards{display:block}.chart-box{height:260px}.value{font-size:28px}.footer{align-items:flex-start}}@media(max-width:520px){.wrap{padding:12px}.metrics{grid-template-columns:1fr}.chart-box{height:230px}.title{font-size:36px}.txlink{font-size:12px}.cards{gap:10px}.poolcards,.charts{gap:10px;margin-top:10px}}
</style>
</head>
<body>
<div class="wrap">
<div class="hero"><div class="brand"><img class="brand-logo" src="/assets/Ashigaru_Whirlpool_Logo_White.png" alt="Ashigaru Whirlpool logo"><h1 class="title">Whirlpool.Observer</h1></div></div>
<div class="grid cards">
<div class="card"><div class="label" id="syncLabel">Sync progress</div><div class="value" id="progressValue">0%</div><div class="progress" id="progressWrap"><div class="bar" id="progressBar" style="width:0%"></div></div><div class="small" id="heightText"></div></div>
<div class="card"><div class="label">Total unspent</div><div class="value" id="totalUnspent">0 BTC</div></div>
<div class="card"><div class="label">Total entered</div><div class="value" id="totalEntered">0 BTC</div></div>
<div class="card"><div class="label">Total cycles</div><div class="value" id="totalCycles">0</div></div>
</div>
<div class="grid poolcards" id="poolCards"></div>
<div class="grid charts"><div class="chart"><h2>Total Entered Capacity by Pool</h2><div class="chart-box"><canvas id="enteredChart"></canvas></div></div><div class="chart"><h2>Live Unspent Capacity by Pool</h2><div class="chart-box"><canvas id="lineChart"></canvas></div></div></div>
<div class="grid charts"><div class="chart"><h2>Total UTXOs Entered</h2><div class="chart-box"><canvas id="enteredUtxoChart"></canvas></div></div><div class="chart"><h2>Total Unspent UTXOs</h2><div class="chart-box"><canvas id="utxoChart"></canvas></div></div></div>
<div class="table" style="margin-top:14px"><div class="cycle-head"><h2>Whirlpool Cycles</h2><div class="tabs" id="poolTabs"></div></div><div class="table-scroll desktop-table"><table><thead><tr><th>Height</th><th>Pool</th><th>TXID</th><th class="desktop-only">Input TX0s / Fee efficiency</th></tr></thead><tbody id="txRows"></tbody></table></div><div class="mobile-cards" id="txCards"></div><div class="pager"><button id="prevPage">Previous</button><span id="pageText">Page 1 / 1</span><button id="nextPage">Next</button></div></div>
<div class="reference"><h2>Reference</h2><details><summary>Whirlpool</summary><p>A CoinJoin implementation by Samourai Wallet that creates transactions with exactly 5 equal outputs at fixed denominations, now run by the <a href="https://ashigaru.rs" target="_blank" rel="noopener">Ashigaru Open Source Project</a>. Whirlpool.Observer tracks the 0.25 BTC and 0.025 BTC pools.</p></details><details><summary>Tx0</summary><p>The preparation transaction in Whirlpool. It takes a user's deposit UTXO(s) and splits them into equal-sized premix outputs matching the pool denomination. Any leftover bitcoin becomes doxxic change. The Tx0 also pays the coordinator a one-time pool entry fee.</p></details><details><summary>Fee efficiency</summary><p>Whirlpool.Observer estimates fee efficiency as coordinator fee divided by detected entered capacity, expressed as a percentage. Entered capacity is counted as detected premix output count multiplied by the pool denomination, ignoring the tiny extra sats attached for miner fees. Larger Tx0s are more efficient because the fixed coordinator fee is spread across more premix outputs.</p></details><details><summary>Entered capacity</summary><p>The cumulative BTC that entered a pool through detected Tx0 premix outputs. For example, 20 detected 0.025 BTC premix outputs count as 0.5 BTC entered capacity.</p></details><details><summary>Unspent capacity</summary><p>The value of tracked Whirlpool outputs that remain unspent and still in their fixed pool denominations.</p></details><details><summary>Cycle</summary><p>One Whirlpool CoinJoin transaction, following the 5 inputs and 5 outputs structure with the fixed pool denominations.</p></details><details><summary>Input TX0s</summary><p>The Tx0 transactions that directly funded a Whirlpool cycle input. All mature Whirlpool cycles have 3 remix inputs and 2 Tx0 inputs; early cycles (genesis adjecent) can have more Tx0 inputs because no prior whirlpool outputs existed.</p></details><details><summary>Doxxic change</summary><p>Leftover change from a Tx0 that does not enter the equal-output Whirlpool pool.</p></details></div>
<div class="footer"><a href="https://github.com/vibrant-btc/whirlpool-observer" target="_blank" rel="noopener"><svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.62 7.62 0 0 1 8 3.87c.68 0 1.36.09 2 .26 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>GitHub repo</a><span class="sep">•</span><a href="https://ashigaru.rs" target="_blank" rel="noopener">Ashigaru Open Source Project</a></div>
</div>
<script>
let charts={},summaryCache=null,selectedPool='all',currentPage=1,totalPages=1,lastChartFingerprint='';const fmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:4});const intfmt=n=>Number(n||0).toLocaleString();const hms=s=>{if(s==null)return '';s=Math.max(0,Number(s)||0);let h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=Math.floor(s%60);return h?`${h}h ${m}m`:m?`${m}m ${sec}s`:`${sec}s`};
function colorForPool(pool){return pool.includes('0.25_')?'#f5f5f7':'#8e8e93'}
function labelFor(pool){let p=(summaryCache?.pools||[]).find(x=>x.pool===pool);return p?p.label:pool}
function makeChart(id,type,data,options={}){if(charts[id])charts[id].destroy();charts[id]=new Chart(document.getElementById(id),{type,data,options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#f5f5f7',usePointStyle:true}},tooltip:{mode:'index',intersect:false}},scales:{x:{ticks:{color:'#8e8e93',maxTicksLimit:7},grid:{color:'#1c1c1e'}},y:{ticks:{color:'#8e8e93'},grid:{color:'#1c1c1e'}}},...options}})}
function updateTabs(pools){const tabs=document.getElementById('poolTabs');tabs.innerHTML='<button class="tab '+(selectedPool==='all'?'active':'')+'" data-pool="all">All Pools</button>'+pools.map(p=>`<button class="tab ${selectedPool===p.pool?'active':''}" data-pool="${p.pool}">${p.label}</button>`).join('');tabs.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{selectedPool=b.dataset.pool;currentPage=1;refreshTxs();updateTabs(summaryCache.pools)})}
function updateSummary(s){summaryCache=s;let tu=0,te=0,cy=0;s.pools.forEach(p=>{tu+=p.unspent_btc;te+=p.entered_btc;cy+=p.cycles});if(s.is_synced){document.getElementById('syncLabel').textContent='Synced';document.getElementById('progressValue').textContent=`Blockheight ${intfmt(s.last_processed_block)}`;document.getElementById('progressWrap').style.display='none';document.getElementById('heightText').textContent=s.next_update_seconds==null?'Waiting for next update':`Next update in ${hms(s.next_update_seconds)}`}else{document.getElementById('syncLabel').textContent='Sync progress';document.getElementById('progressValue').textContent=s.progress_pct.toFixed(2)+'%';document.getElementById('progressWrap').style.display='block';document.getElementById('progressBar').style.width=s.progress_pct+'%';document.getElementById('heightText').textContent=`${intfmt(s.last_processed_block)} / ${intfmt(s.tip_height)}`}document.getElementById('totalUnspent').textContent=fmt(tu)+' BTC';document.getElementById('totalEntered').textContent=fmt(te)+' BTC';document.getElementById('totalCycles').textContent=intfmt(cy);document.getElementById('poolCards').innerHTML=s.pools.map(p=>`<div class="card"><div class="label">${p.label}</div><div class="value">${fmt(p.entered_btc)} BTC</div><div class="metrics"><div class="metric"><div class="label">Unspent</div><div class="n">${fmt(p.unspent_btc)} BTC</div></div><div class="metric"><div class="label">Cycles</div><div class="n">${intfmt(p.cycles)}</div></div><div class="metric"><div class="label">Tx0 count</div><div class="n">${intfmt(p.tx0_count)}</div></div><div class="metric"><div class="label">UTXOs</div><div class="n">${intfmt(p.unspent_utxos)}</div></div><div class="metric"><div class="label">Avg fee %</div><div class="n">${fmt(p.avg_fee_efficiency_pct)}%</div></div><div class="metric"><div class="label">Pool</div><div class="n"><span class="pooldot" style="background:${p.color}"></span></div></div></div></div>`).join('');updateTabs(s.pools)}
function updateCharts(c){const entered=c.entered||{blocks:[],series:{}};makeChart('enteredChart','line',{labels:entered.blocks,datasets:Object.keys(entered.series||{}).map(k=>({label:labelFor(k),data:entered.series[k],borderColor:colorForPool(k),backgroundColor:'transparent',stepped:true,tension:0,borderWidth:2,pointRadius:0}))});const cap=c.capacity||{blocks:[],series:{}};makeChart('lineChart','line',{labels:cap.blocks,datasets:Object.keys(cap.series||{}).map(k=>({label:labelFor(k),data:cap.series[k],borderColor:colorForPool(k),backgroundColor:'transparent',stepped:true,tension:0,borderWidth:2,pointRadius:0}))});const u=c.utxos||{blocks:[],total_utxos:[]};makeChart('utxoChart','line',{labels:u.blocks,datasets:[{label:'Unspent UTXOs',data:u.total_utxos,borderColor:'#f5f5f7',backgroundColor:'transparent',stepped:true,pointRadius:0,borderWidth:2}]});const eu=c.entered_utxos||{blocks:[],total_utxos:[]};makeChart('enteredUtxoChart','line',{labels:eu.blocks,datasets:[{label:'Entered UTXOs',data:eu.total_utxos,borderColor:'#8e8e93',backgroundColor:'transparent',stepped:true,pointRadius:0,borderWidth:2}]})}
function tx0Text(t){return(t.tx0_inputs||[]).length?t.tx0_inputs.map(x=>`<div><a target="_blank" href="http://am-i.exposed/#tx=${x.txid}">${x.txid.slice(0,12)}…</a> · ${x.fee_efficiency_pct}%</div>`).join(''):'<span class="empty">None recorded</span>'}
async function refreshTxs(){const poolParam=selectedPool==='all'?'':`&pool=${encodeURIComponent(selectedPool)}`;const data=await fetch(`/api/txs?page=${currentPage}&per_page=25${poolParam}`).then(r=>r.json());totalPages=data.total_pages||1;document.getElementById('pageText').textContent=`Page ${data.page} / ${totalPages} · ${data.total} cycles`;document.getElementById('prevPage').disabled=data.page<=1;document.getElementById('nextPage').disabled=data.page>=totalPages;let rows=data.items||[];document.getElementById('txRows').innerHTML=rows.length?rows.map(t=>`<tr><td>${intfmt(t.block_height)}</td><td><span class="pooldot" style="background:${t.pool_color}"></span>${t.pool_label}</td><td><a class="txlink" target="_blank" href="${t.am_i_exposed_url}">${t.txid}</a></td><td class="desktop-only tx0-list">${tx0Text(t)}</td></tr>`).join(''):'<tr><td colspan="4" class="empty">No cycles found yet.</td></tr>';document.getElementById('txCards').innerHTML=rows.length?rows.map(t=>`<div class="mobile-cycle-card"><div>${intfmt(t.block_height)} · <span class="pooldot" style="background:${t.pool_color}"></span>${t.pool_label}</div><a class="txlink" target="_blank" href="${t.am_i_exposed_url}">${t.txid}</a></div>`).join(''):'<div class="mobile-cycle-card empty">No cycles found yet.</div>'}
async function refresh(){const[s,c]=await Promise.all([fetch('/api/summary').then(r=>r.json()),fetch('/api/charts').then(r=>r.json())]);updateSummary(s);const fp=JSON.stringify(c);if(fp!==lastChartFingerprint){lastChartFingerprint=fp;updateCharts(c)}await refreshTxs()}
document.getElementById('prevPage').onclick=()=>{if(currentPage>1){currentPage--;refreshTxs()}};document.getElementById('nextPage').onclick=()=>{if(currentPage<totalPages){currentPage++;refreshTxs()}};refresh();setInterval(refresh,15000);
</script></body></html>
"""