git / moonbite-network / templates/balance.html · raw
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><title>Check your MoonBite balance</title><meta name="description" content="Paste the address you mined to and see its balance, read live from the MoonBite chain. No account, no wallet connection."><link rel="icon" type="image/svg+xml" href="/static/moonbite-logo.svg"><link rel="stylesheet" href="/static/fonts/moonbite-fonts.css"><style nonce="{{ csp_nonce }}">/* Brand Reserve only: Void / Slate / Moongold / Bone / Ash. Matches the home page so the walk from "you mined a block" to "here is what you have" is one continuous world. Nothing loads from a third party. */:root{ --void:#0B0D12; --slate:#161A22; --gold:#D9A441; --gold-2:#EBCB86; --bone:#F5F2EA; --ash:#8A8580; --ok:#2E9E6B; --display:'Archivo Black','Helvetica Neue','Arial Black',sans-serif; --ui:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; --mono:ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace; --ease:cubic-bezier(0.22,1,0.36,1);}*{margin:0;padding:0;box-sizing:border-box}html{-webkit-text-size-adjust:100%}body{background:var(--void);color:var(--bone);font-family:var(--ui);line-height:1.5; min-height:100svh;-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration:none}::selection{background:var(--gold);color:var(--void)}:focus-visible{outline:2px solid var(--gold);outline-offset:3px;border-radius:3px}.skip{position:absolute;left:-9999px;z-index:50;background:var(--gold);color:var(--void);padding:12px 18px;font-weight:700}.skip:focus{left:0;top:0}main{max-width:720px;margin:0 auto;padding:clamp(28px,7vh,72px) clamp(20px,5vw,28px) 80px}.tag{font-family:var(--mono);font-size:.66rem;letter-spacing:.28em;text-transform:uppercase;color:var(--gold);margin-bottom:16px}h1{font-family:var(--display);font-weight:400;letter-spacing:-.03em;line-height:1; font-size:clamp(2rem,6vw,3.4rem)}.lede{margin-top:16px;max-width:46ch;color:rgba(245,242,234,.76);font-size:clamp(1rem,1.8vw,1.12rem)}form{margin-top:32px;display:flex;gap:10px;flex-wrap:wrap}.field{flex:1 1 320px;display:flex;flex-direction:column;gap:6px}label{font-family:var(--mono);font-size:.62rem;letter-spacing:.16em;text-transform:uppercase;color:var(--ash)}input{width:100%;background:var(--slate);border:1px solid rgba(245,242,234,.16);color:var(--bone); border-radius:6px;padding:14px 16px;font-family:var(--mono);font-size:.92rem;min-height:52px}input::placeholder{color:rgba(245,242,234,.34)}input:focus{border-color:var(--gold);outline:none}button{min-height:52px;align-self:flex-end;padding:0 30px;border:0;border-radius:6px;cursor:pointer; background:var(--gold);color:var(--void);font-weight:700;font-size:1rem; transition:transform .12s var(--ease),background .2s var(--ease)}button:hover{transform:translateY(-2px);background:var(--gold-2)}button:disabled{opacity:.5;cursor:default;transform:none}.hint{margin-top:14px;font-size:.86rem;color:var(--ash);max-width:52ch;line-height:1.6}/* result card, one shape for every state */.card{margin-top:30px;border:1px solid rgba(245,242,234,.14);border-radius:12px; background:linear-gradient(180deg,rgba(22,26,34,.7),rgba(11,13,18,.7)); padding:26px clamp(20px,4vw,30px)}.card[hidden]{display:none}.card .addr{font-family:var(--mono);font-size:.8rem;color:var(--ash);word-break:break-all;margin-bottom:16px}.amount{font-family:var(--display);font-weight:400;letter-spacing:-.02em;line-height:1; font-size:clamp(2.4rem,8vw,4rem)}.amount .u{font-family:var(--ui);font-weight:600;font-size:.34em;color:var(--gold);margin-left:.35em;letter-spacing:0}.meta{margin-top:16px;display:flex;flex-wrap:wrap;gap:8px 22px;font-family:var(--mono); font-size:.74rem;color:rgba(245,242,234,.6)}.meta b{color:var(--bone);font-weight:400}.maturing{margin-top:14px;font-size:.9rem;color:var(--bone);line-height:1.55;max-width:54ch; border-left:2px solid var(--gold);padding-left:12px}.maturing b{color:var(--gold)}.note{margin-top:18px;font-size:.82rem;color:var(--ash);line-height:1.6;max-width:54ch}.zero .amount{color:var(--ash)}.spin{display:inline-block;width:15px;height:15px;border:2px solid rgba(217,164,65,.3); border-top-color:var(--gold);border-radius:50%;animation:sp .8s linear infinite;vertical-align:-2px;margin-right:8px}@keyframes sp{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spin{animation:none}button:hover{transform:none}}.err{border-color:rgba(200,64,47,.45)}.err .amount{font-size:clamp(1.1rem,3vw,1.5rem);color:var(--bone)}.note-foot{max-width:720px;margin:0 auto;padding:0 clamp(20px,5vw,28px) 72px; font-family:var(--mono);font-size:.7rem;color:var(--ash);line-height:1.8}.note-foot a{color:var(--ash);text-decoration:underline;text-underline-offset:3px}</style></head><body><a class="skip" href="#check">Skip to the address box</a>{% include "partials/site_header.html" %}<main id="check"> <p class="tag">Read live from the chain</p> <h1>See what you've mined.</h1> <p class="lede">Paste the address you mined to. No wallet to connect, no account, nothing to sign — this only reads the public chain.</p> <form id="form" autocomplete="off"> <div class="field"> <label for="addr">Your MoonBite address</label> <input id="addr" name="addr" inputmode="text" spellcheck="false" placeholder="moon1…" aria-describedby="hint"> </div> <button type="submit" id="go">Check balance</button> </form> <p class="hint" id="hint">A mainnet address begins with <b>moon1</b>. Your seed never leaves your machine — you are only sharing a public address.</p> <section class="card" id="card" hidden aria-live="polite"></section> <noscript> <p class="hint">This lookup needs JavaScript to read the chain live. You can also look up any address on the <a href="/explorer" style="color:var(--gold)">block explorer</a>.</p> </noscript></main><p class="note-foot"> This is the current <b>unspent</b> balance, scanned live from the MoonBite chain. Freshly mined coins become spendable 100 blocks after they are found (standard coinbase maturity); until then they are listed as maturing.<br> Prefer the raw data? <a href="/explorer">Open the explorer</a>.</p>{% include "partials/site_footer.html" %}<script nonce="{{ csp_nonce }}">(function(){"use strict";var $=function(id){return document.getElementById(id)};var form=$('form'), input=$('addr'), go=$('go'), card=$('card');var UNITS=100000000;function fmt(coins){ // whole MBITE with thousands separators, up to 8 dp trimmed of trailing zeros var s=coins.toLocaleString('en-US',{maximumFractionDigits:8}); return s;}function esc(s){return String(s).replace(/[<&>]/g,function(c){return {'<':'<','&':'&','>':'>'}[c]})}function show(html, cls){ card.className='card'+(cls?' '+cls:''); card.innerHTML=html; card.hidden=false;}function looksLikeAddress(a){ // permissive gate only; the node is the authority. bech32 charset, moon1 HRP. return /^moon1[0-9ac-hj-np-z]{10,90}$/.test(a);}function render(addr, data){ var units=(data.balance_units!=null)?data.balance_units:0; var coins=units/UNITS; var n=(data.utxos&&data.utxos.length!=null)?data.utxos.length:null; var height=data.height!=null?data.height:null; var maturing=Number(data.immature_units)||0; var firstMature=null; (data.immature_utxos||[]).forEach(function(u){ var h=Number(u.matures_at_height); if(isFinite(h)&&(firstMature===null||h<firstMature)) firstMature=h; }); var maturingNote=''; if(maturing>0){ var left=(firstMature!==null&&height!==null)?firstMature-height:null; maturingNote='<p class="maturing"><b>+ '+fmt(maturing/UNITS)+' MBITE maturing.</b> '+ 'Mining rewards unlock 100 blocks after they are found'+ (left!==null&&left>0?': the first unlocks at block <b>#'+firstMature.toLocaleString('en-US')+ '</b>, '+left+' block'+(left===1?'':'s')+' from now (about '+(left*2)+' min at 2 min per block).' :', and the first is unlocking now.')+'</p>'; } if(units===0&&maturing>0){ show( '<p class="addr">'+esc(addr)+'</p>'+ '<div class="amount">0<span class="u">MBITE spendable</span></div>'+ maturingNote+ (height!=null?'<div class="meta"><span>as of block <b>#'+height.toLocaleString('en-US')+'</b></span></div>':'')+ '<p class="note">Your mining is working: these rewards are on the chain and '+ 'belong to this address. They become spendable on their own; nothing to do.</p>'); return; } if(units===0){ show( '<p class="addr">'+esc(addr)+'</p>'+ '<div class="amount">0<span class="u">MBITE</span></div>'+ '<p class="note">Nothing has landed on this address yet. If you just started '+ 'mining, give it a few blocks — and remember new rewards need 100 confirmations '+ 'before they show as spendable.</p>', 'zero'); return; } var meta=''; if(n!=null) meta+='<span><b>'+n+'</b> unspent output'+(n===1?'':'s')+'</span>'; if(height!=null) meta+='<span>as of block <b>#'+height.toLocaleString('en-US')+'</b></span>'; show( '<p class="addr">'+esc(addr)+'</p>'+ '<div class="amount">'+fmt(coins)+'<span class="u">MBITE</span></div>'+ maturingNote+ (meta?'<div class="meta">'+meta+'</div>':'')+ '<p class="note">Confirmed, unspent, and yours. This is read straight from the '+ 'chain — nobody was asked, and nothing was signed.</p>');}function check(addr){ go.disabled=true; show('<div class="amount" style="font-size:1.2rem"><span class="spin"></span>'+ 'Scanning the chain…</div>'+ '<p class="note">Reading every unspent output for this address. '+ 'This can take a few seconds.</p>'); var ctrl=new AbortController(); var t=setTimeout(function(){ctrl.abort()},28000); fetch('/api/chain/address/'+encodeURIComponent(addr),{signal:ctrl.signal}) .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j}})}) .then(function(res){ clearTimeout(t); if(res.ok && res.j && res.j.status==='success'){ render(addr,res.j); } else if(res.j && /not configured/i.test(res.j.message||'')){ show('<div class="amount">Live lookup is off</div><p class="note">This '+ 'server is not connected to the node right now. Try the '+ '<a href="/explorer" style="color:var(--gold)">explorer</a>.</p>','err'); } else { show('<div class="amount">Could not read that address</div><p class="note">'+ esc((res.j&&res.j.message)||'The scan did not complete.')+ ' Check the address and try again.</p>','err'); } }) .catch(function(e){ clearTimeout(t); var msg=(e&&e.name==='AbortError') ? 'The node is busy scanning the chain. Give it a moment and try again.' : 'Something went wrong reaching the node. Try again in a moment.'; show('<div class="amount">Try again</div><p class="note">'+msg+'</p>','err'); }) .then(function(){ go.disabled=false; });}form.addEventListener('submit',function(e){ e.preventDefault(); var addr=(input.value||'').trim(); if(!addr){ input.focus(); return; } if(!looksLikeAddress(addr.toLowerCase())){ show('<div class="amount">That is not a MoonBite address</div><p class="note">'+ 'A mainnet address begins with <b>moon1</b>. Paste the receiving address '+ 'you mined to.</p>','err'); return; } // reflect the address in the URL so the result is shareable / reloadable try{ history.replaceState(null,'','/balance?address='+encodeURIComponent(addr)); }catch(_e){} check(addr);});// deep-link: /balance?address=moon1... checks immediately(function(){ var m=/[?&]address=([^&]+)/.exec(location.search); if(m){ try{ input.value=decodeURIComponent(m[1]); }catch(_e){ input.value=m[1]; } if(input.value.trim()) form.dispatchEvent(new Event('submit')); } else { input.focus(); }})();})();</script></body></html>