git / moonbite-network / templates/start.html · raw
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="theme-color" content="#0B0D12"> <title>Mine your first block — MoonBite</title> <meta name="description" content="No signup, no purchase, no wallet to install. An address is already waiting. Press start and a real block reward of {{ consensus.current_reward_coins|int }} MBITE is on the line."> <link rel="icon" type="image/svg+xml" href="/static/moonbite-logo.svg"> <link rel="stylesheet" href="/static/fonts/moonbite-fonts.css"> <style> /* Doc 04: three roles. A heavy grotesque for display, a quiet sans for reading, a monospace for anything that is a number. Archivo Black and Inter are self-hosted from /static/fonts (latin subset, OFL-licensed). They are deliberately not loaded from Google Fonts: /privacy states the site loads nothing that phones home, and a webfont CDN sends every visitor's IP to Google on every page load. Every figure that is money, a hash, an address or a block height is monospace with tabular figures — amounts that shift width as they update look untrustworthy. */ :root{ --font-display:'Archivo Black','Helvetica Neue','Arial Black',sans-serif; --font-body:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; --font-data:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; } h1,h2,.big,.reveal .big,.balance-amount,.elapsed,.mort-big{ font-family:var(--font-display); font-weight:400; letter-spacing:-.02em; } .num,.mono,.addr,.cert .amt,.cl .v,.tot .v,.mcell .v,.odds .v, .loss .v,.count .v,.zero-band .v,.total .v,.vault-v,td.n,th.n{ font-family:var(--font-data); font-variant-numeric:tabular-nums; } * { margin:0; padding:0; box-sizing:border-box; } body { background:#0B0D12; color:#F5F2EA; font-family:var(--font-body); overflow-x:hidden; -webkit-font-smoothing:antialiased; min-height:100svh; } ::selection { background:#D9A441; color:#0B0D12; } a,button { -webkit-tap-highlight-color:transparent; font-family:inherit; } #sky { position:fixed; inset:0; z-index:0; } .wrap { position:relative; z-index:1; max-width:520px; margin:0 auto; padding:0 18px 60px; } .kicker { letter-spacing:.3em; font-size:.64rem; color:#D9A441; text-transform:uppercase; font-weight:800; } .num { font-variant-numeric:tabular-nums; } .hidden { display:none !important; } h1 { font-size:clamp(2rem,8vw,3rem); font-weight:800; line-height:1.05; letter-spacing:-.02em; margin:12px 0 10px; } .sub { color:#8A8580; font-size:.98rem; line-height:1.6; max-width:420px; margin:0 auto; } .step { text-align:center; padding-top:5vh; } .btn { display:inline-flex; align-items:center; justify-content:center; gap:9px; border:none; border-radius:10px; padding:17px 30px; min-height:52px; font-weight:800; font-size:1rem; cursor:pointer; text-decoration:none; transition:transform .15s ease; } .btn:active { transform:scale(.97); } .btn-gold { background:#D9A441; color:#0B0D12; box-shadow:0 12px 38px rgba(217,164,65,.3); } .btn-gold:disabled { opacity:.45; box-shadow:none; } .btn-ghost { background:transparent; color:#F5F2EA; border:1px solid rgba(245,242,234,.22); } .actions { display:flex; gap:11px; justify-content:center; flex-wrap:wrap; margin-top:22px; } /* ---------------------------------------------------------- ADDRESS card */ .acard { background:#161A22; border:1px solid rgba(217,164,65,.22); border-radius:20px; padding:20px 18px; margin-top:26px; text-align:left; box-shadow:0 20px 60px rgba(0,0,0,.45); } .acard .lbl { font-size:.6rem; letter-spacing:.22em; color:#8A8580; text-transform:uppercase; font-weight:800; display:flex; align-items:center; gap:8px; } .acard .lbl .dot { width:6px; height:6px; border-radius:50%; background:#2E9E6B; } .addr { font-family:ui-monospace,Menlo,Consolas,monospace; font-size:.82rem; color:#F5F2EA; word-break:break-all; line-height:1.65; margin-top:11px; } .acopy { margin-top:13px; background:transparent; border:1px solid rgba(217,164,65,.4); color:#D9A441; border-radius:10px; padding:11px 16px; min-height:44px; font-weight:800; font-size:.75rem; letter-spacing:.1em; cursor:pointer; width:100%; } .anote { color:#8A8580; font-size:.78rem; line-height:1.6; margin-top:12px; } /* prize strip */ .prize { display:flex; align-items:center; justify-content:center; gap:14px; margin-top:24px; padding:16px 18px; border-radius:20px; background:linear-gradient(135deg, rgba(217,164,65,.14), rgba(184,134,59,.06)); border:1px solid rgba(217,164,65,.3); } .prize .v { font-size:clamp(1.7rem,7vw,2.3rem); font-weight:800; color:#D9A441; line-height:1; } .prize .k { font-size:.62rem; letter-spacing:.18em; color:#8A8580; text-transform:uppercase; font-weight:800; margin-top:5px; text-align:left; } .prize .sep { width:1px; align-self:stretch; background:rgba(217,164,65,.25); } /* -------------------------------------------------------- MINING screen */ .hunt-wrap { width:min(56vw,220px); aspect-ratio:1; margin:2vh auto 0; position:relative; } #hunt { width:100%; height:100%; display:block; } .elapsed { font-size:clamp(2.6rem,13vw,4rem); font-weight:800; color:#F5F2EA; line-height:1; letter-spacing:-.02em; margin-top:6px; } .elapsed-k { font-size:.6rem; letter-spacing:.26em; color:#8A8580; text-transform:uppercase; font-weight:800; margin-top:9px; } .odds { background:#161A22; border:1px solid rgba(200,64,47,.28); border-radius:20px; padding:18px 16px; margin-top:22px; } .odds .k { font-size:.6rem; letter-spacing:.2em; color:#8A8580; text-transform:uppercase; font-weight:800; } .odds .v { font-size:clamp(1.15rem,5.2vw,1.6rem); font-weight:800; color:#C8402F; margin-top:10px; line-height:1.1; letter-spacing:-.01em; } .odds .vsub { font-size:.8rem; color:#F5F2EA; margin-top:6px; font-weight:600; } .odds .n { font-size:.76rem; color:#8A8580; margin-top:9px; line-height:1.55; } .mgrid { display:grid; grid-template-columns:1fr 1fr 1fr; gap:9px; margin-top:14px; } .mcell { background:#161A22; border:1px solid rgba(245,242,234,.08); border-radius:10px; padding:13px 8px; } .mcell .v { font-size:clamp(.95rem,3.8vw,1.2rem); font-weight:800; color:#D9A441; line-height:1.1; } .mcell .k { font-size:.52rem; letter-spacing:.14em; color:#8A8580; text-transform:uppercase; margin-top:5px; font-weight:700; } .whisper { color:#8A8580; font-size:.86rem; line-height:1.65; margin-top:20px; min-height:3.3em; transition:opacity .45s; padding:0 4px; } .stopwrap { margin-top:18px; } .stopbtn { background:none; border:none; color:#8A8580; font-size:.76rem; letter-spacing:.14em; text-transform:uppercase; font-weight:700; cursor:pointer; padding:12px 18px; min-height:44px; } /* ---------------------------------------------------------- DONE screen */ .dcard { background:linear-gradient(160deg,#161A22,#0B0D12); border:1px solid rgba(217,164,65,.35); border-radius:20px; padding:26px 20px; margin-top:24px; box-shadow:0 24px 70px rgba(0,0,0,.5); } .dcard .amt { font-size:clamp(2.4rem,11vw,3.4rem); font-weight:800; color:#D9A441; line-height:1; } .dcard .meta { color:#8A8580; font-size:.86rem; margin-top:10px; } .badgerow { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-top:16px; } .bpill { border:1px solid rgba(217,164,65,.5); background:rgba(217,164,65,.1); color:#D9A441; border-radius:999px; padding:7px 13px; font-size:.75rem; font-weight:800; } .nextrow { display:flex; flex-direction:column; gap:10px; margin-top:22px; } .nextcard { display:flex; align-items:center; gap:14px; text-decoration:none; text-align:left; background:#161A22; border:1px solid rgba(217,164,65,.2); border-radius:10px; padding:16px 16px; } .nextcard .ic { font-size:1.5rem; } .nextcard b { color:#F5F2EA; font-size:.95rem; display:block; } .nextcard span { color:#8A8580; font-size:.79rem; } .nextcard .ar { margin-left:auto; color:#D9A441; font-weight:800; } .toast { position:fixed; left:50%; bottom:26px; transform:translateX(-50%) translateY(16px); background:#2E9E6B; color:#04160E; font-weight:800; padding:12px 22px; border-radius:10px; opacity:0; transition:all .3s; z-index:60; pointer-events:none; font-size:.88rem; } .toast.show { opacity:1; transform:translateX(-50%) translateY(0); } /* first-block ceremony (self-contained mirror of site.css) */ .mb-first-block-overlay { position:fixed; inset:0; z-index:999; display:flex; align-items:center; justify-content:center; background:rgba(5,8,20,.93); } .mb-confetti { position:absolute; inset:0; pointer-events:none; } .mb-first-block-card { position:relative; max-width:400px; width:calc(100% - 36px); text-align:center; padding:34px 26px; border-radius:20px; background:linear-gradient(180deg,#161A22,#0B0D12); border:1px solid rgba(217,164,65,.42); box-shadow:0 0 70px rgba(217,164,65,.16); animation:mbFbPop .55s cubic-bezier(.2,1.6,.4,1); } @keyframes mbFbPop { from{transform:scale(.72);opacity:0} to{transform:scale(1);opacity:1} } .mb-fb-kicker { letter-spacing:.25em; font-size:.72rem; color:#2E9E6B; font-weight:800; } .mb-fb-amount { font-size:2.9rem; font-weight:800; margin:10px 0 4px; color:#D9A441; } .mb-fb-line { color:rgba(245,242,234,.75); font-size:.93rem; margin-top:4px; } .mb-fb-addr { font-family:monospace; font-size:.82rem; opacity:.6; } .mb-fb-badges { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin:16px 0 6px; } .mb-fb-badge { padding:6px 12px; border-radius:999px; font-size:.78rem; font-weight:800; border:1px solid rgba(217,164,65,.5); background:rgba(217,164,65,.1); color:#D9A441; } .mb-fb-quote { font-style:italic; color:rgba(245,242,234,.6); font-size:.89rem; margin:12px 0 18px; } .mb-fb-actions { display:flex; gap:10px; justify-content:center; flex-wrap:wrap; } .mb-fb-btn { padding:13px 20px; min-height:44px; border-radius:10px; cursor:pointer; font-weight:800; border:1px solid rgba(217,164,65,.6); color:#0B0D12; background:#D9A441; font-family:inherit; } .mb-fb-btn.ghost { background:transparent; color:#F5F2EA; border-color:rgba(245,242,234,.3); } .mb-badge-toast { position:fixed; left:50%; bottom:40px; transform:translateX(-50%); background:#D9A441; color:#0B0D12; font-weight:800; padding:12px 22px; border-radius:10px; z-index:1000; } @media (prefers-reduced-motion: reduce){ .mb-first-block-card { animation:none; } } /* Doc 17: visible keyboard focus on every interactive element. */ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, [tabindex]:focus-visible { outline:2px solid #D9A441; outline-offset:3px; border-radius:6px; } </style> </head> <body> <canvas id="sky"></canvas> {% include 'partials/nav.html' %} <div class="wrap"> <!-- ========================================================= 1. READY --> <section class="step" id="sReady"> <div class="kicker">STEP ONE OF ONE</div> <h1>Mine your<br>first block.</h1> <p class="sub">No signup, no purchase, nothing to install. An address is already waiting for you below. Press start and it goes to work.</p> <div class="prize"> <div> <div class="v num">{{ consensus.current_reward_coins|int }}</div> <div class="k">MBITE at stake</div> </div> <div class="sep"></div> <div> <div class="v num">~{{ consensus.block_time_min }}<span style="font-size:.55em;">min</span></div> <div class="k">Average hunt</div> </div> </div> <div class="acard"> <div class="lbl"><span class="dot"></span>YOUR MINING ADDRESS</div> <div class="addr" id="addr">generating…</div> <button class="acopy" id="btnCopy">COPY ADDRESS</button> <p class="anote">Stored in this browser only — we never see it. Any block you find pays here, and this address is the only claim to it.</p> </div> <div class="actions"> <button class="btn btn-gold" id="btnStart" disabled>START MINING</button> </div> <p class="sub" style="margin-top:14px; font-size:.78rem;"> RandomX is genuine proof-of-work. The difficulty is the point.</p> </section> <!-- ======================================================== 2. MINING --> <section class="step hidden" id="sMining"> <div class="hunt-wrap"><canvas id="hunt"></canvas></div> <div class="kicker" style="color:#2E9E6B;">● HUNTING FOR A BLOCK</div> <div class="elapsed num" id="mElapsed">0:00</div> <div class="elapsed-k">TIME SPENT SEARCHING</div> <div class="odds"> <div class="k">WHAT YOUR NODE IS UP AGAINST</div> <div class="v num" id="oOdds">—</div> <div class="vsub">hashes, on average, for one winning block</div> <div class="n">There is no progress bar for mining. Every attempt is independent — the next one is as likely as the first. That is what makes it fair.</div> </div> <div class="mgrid"> <div class="mcell"><div class="v num" id="mReward">—</div><div class="k">Reward</div></div> <div class="mcell"><div class="v num" id="mDiff">—</div><div class="k">Difficulty</div></div> <div class="mcell"><div class="v num" id="mHeight">—</div><div class="k">Height</div></div> </div> <p class="whisper" id="whisper">Your node is filling memory with a randomly generated program and running it. That is what makes an ASIC pointless here.</p> <div class="stopwrap"><button class="stopbtn" id="btnStop">Stop mining</button></div> <p class="sub" style="margin-top:8px; font-size:.75rem;">Keep this tab open — the search continues whether or not you watch.</p> </section> <!-- ========================================================== 3. DONE --> <section class="step hidden" id="sDone"> <div class="kicker">YOU ARE A MINER NOW</div> <h1>Block found.</h1> <div class="dcard"> <div class="amt num" id="dAmt">+{{ consensus.current_reward_coins|int }} MBITE</div> <div class="meta" id="dMeta">—</div> <div class="badgerow" id="dBadges"></div> </div> <div class="nextrow"> <a class="nextcard" href="/world-cup"> <div class="ic"></div> <div><b>Claim your country</b><span>Put your flag on the world board</span></div> <div class="ar">→</div> </a> <a class="nextcard" href="/calculator"> <div class="ic"></div> <div><b>What it's worth being early</b><span>Run the numbers on your machine</span></div> <div class="ar">→</div> </a> <a class="nextcard" href="#" id="againCard"> <div class="ic"></div> <div><b>Mine another block</b><span>Same address, same odds</span></div> <div class="ar">→</div> </a> </div> </section> </div> {% include 'partials/footer.html' %} <div class="toast" id="toast"></div> <script src="/static/moonbite-badges.js"></script> <script nonce="{{ csp_nonce }}"> (function () { 'use strict'; var CONS = { reward: {{ consensus.current_reward_coins }}, blockMin: {{ consensus.block_time_min }} }; var addr = null, poller = null, ticker = null, started = 0, busy = false, baseHeight = null; /* A freshly generated address has provably mined nothing, so its baseline is zero. Sampling it on the first poll would race a fast node that already found the block and swallow the ceremony. */ var baseBlocks = 0, mining = false; function $(id){ return document.getElementById(id); } function shortAddr(a){ /* Doc 18: first and last six characters are always visible, so a miner can verify an address by eye. Never shorten past that. */ if (!a) return ''; return a.length > 14 ? a.slice(0,6) + '…' + a.slice(-6) : a; } function fmt(n){ return Math.round(n).toLocaleString('en-US'); } /* Doc 11: block heights are never thousands-separated — only amounts are. */ function ht(n){ return String(Math.round(n||0)); } /* clipboard.writeText rejects on insecure contexts, denied permission and some browsers; an uncaught rejection surfaces as a console error. */ function copyText(s){ try { if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(s).catch(function(){}); } catch(e){} return null; } function show(id){ ['sReady','sMining','sDone'].forEach(function(s){ $(s).classList.toggle('hidden', s !== id); }); } function toast(m){ var t=$('toast'); t.textContent=m; t.classList.add('show'); setTimeout(function(){ t.classList.remove('show'); }, 2400); } function clock(sec){ sec = Math.max(0, Math.round(sec)); var m = Math.floor(sec/60), s = sec%60; if (m < 60) return m + ':' + (s<10?'0':'') + s; return Math.floor(m/60) + ':' + (m%60<10?'0':'') + (m%60) + ':' + (s<10?'0':'') + s; } /* ------------------------------------------------------------- sky */ var sky=$('sky'), sg=sky.getContext('2d'), W,H,stars=[]; var DPR=Math.min(window.devicePixelRatio||1,2); var reduced=window.matchMedia('(prefers-reduced-motion: reduce)').matches; function resize(){ W=innerWidth; H=innerHeight; sky.width=W*DPR; sky.height=H*DPR; sky.style.width=W+'px'; sky.style.height=H+'px'; sg.setTransform(DPR,0,0,DPR,0,0); stars=[]; for(var i=0,n=Math.min(150,(W*H/9500)|0); i<n; i++) stars.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.1+.25,tw:Math.random()*6.28}); } addEventListener('resize',resize); resize(); var st=0; (function loop(){ st += reduced?0:(mining?.05:.008); sg.fillStyle='#0B0D12'; sg.fillRect(0,0,W,H); for(var i=0;i<stars.length;i++){ var s=stars[i]; sg.globalAlpha=.16+.34*Math.abs(Math.sin(st+s.tw)); sg.fillStyle = mining ? '#D9A441' : '#F5F2EA'; sg.beginPath(); sg.arc(s.x,s.y,s.r,0,6.2832); sg.fill(); } sg.globalAlpha=1; requestAnimationFrame(loop); })(); /* ------------------------------------------------- the hunting mark */ /* Renamed from ht: it shadowed the ht() height formatter declared above, so by the time the miner polled, ht was a number and ht(height) threw — which aborted the poll handler before it could ever detect a found block, leaving the page hunting forever. */ var hunt=$('hunt'), hg=hunt.getContext('2d'), huntT=0; if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){ r=Math.min(r,w/2,h/2); this.moveTo(x+r,y); this.arcTo(x+w,y,x+w,y+h,r); this.arcTo(x+w,y+h,x,y+h,r); this.arcTo(x,y+h,x,y,r); this.arcTo(x,y,x+w,y,r); this.closePath(); return this; }; } (function drawHunt(){ huntT += reduced?0:.03; var S=hunt.clientWidth; if(S>0 && hunt.width!==S*DPR){ hunt.width=S*DPR; hunt.height=S*DPR; } hg.setTransform(DPR,0,0,DPR,0,0); hg.clearRect(0,0,S,S); var c=S/2, r=S*.29; /* searching sweep — a radar, not a progress bar: it never fills */ var sweepA = huntT % 6.2832; var grad = hg.createConicGradient ? null : null; hg.save(); hg.strokeStyle='rgba(217,164,65,.75)'; hg.lineWidth=Math.max(2,S*.014); hg.lineCap='round'; hg.beginPath(); hg.arc(c,c,S*.44, sweepA, sweepA+1.0); hg.stroke(); hg.strokeStyle='rgba(245,242,234,.07)'; hg.lineWidth=Math.max(2,S*.014); hg.beginPath(); hg.arc(c,c,S*.44,0,6.2832); hg.stroke(); hg.restore(); /* glow pulse */ var pulse = .5+.5*Math.sin(huntT*1.6); var gr=hg.createRadialGradient(c,c,r*.5,c,c,r*1.9); gr.addColorStop(0,'rgba(217,164,65,'+(.10+.10*pulse)+')'); gr.addColorStop(1,'rgba(217,164,65,0)'); hg.fillStyle=gr; hg.beginPath(); hg.arc(c,c,r*1.9,0,6.2832); hg.fill(); /* the mark */ var bg=hg.createRadialGradient(c-r*.35,c-r*.35,r*.1,c,c,r); bg.addColorStop(0,'#FFFFFF'); bg.addColorStop(.6,'#F5F2EA'); bg.addColorStop(1,'#CFC9BB'); hg.fillStyle=bg; hg.beginPath(); hg.arc(c,c,r,0,6.2832); hg.fill(); hg.fillStyle='#0B0D12'; hg.beginPath(); hg.arc(c+r*.55,c-r*.75,r*.78,0,6.2832); hg.fill(); var bh=Math.max(3,r*.13); hg.fillStyle='#D9A441'; hg.beginPath(); hg.roundRect(c-r*2.9/2,c-r*.33-bh/2,r*2.9,bh,bh/2); hg.fill(); hg.beginPath(); hg.roundRect(c-r*2.7/2,c+r*.22-bh/2,r*2.7,bh,bh/2); hg.fill(); requestAnimationFrame(drawHunt); })(); /* ------------------------------------------------------- whispers */ var WHISPERS = [ 'Your node is filling memory with a randomly generated program and running it. That is what makes an ASIC pointless here.', 'Nobody can pay to skip this queue. The only way past it is electricity and time.', 'Every hash is an independent lottery ticket. Two minutes in, the next one is still as likely as the first.', 'A block found now pays ' + CONS.reward + ' MBITE. After the next halving it pays half that — forever.', 'This is the same work securing the chain. You are not simulating mining; you are mining.' ]; var wi=0; setInterval(function(){ if(!mining) return; var el=$('whisper'); el.style.opacity=0; setTimeout(function(){ wi=(wi+1)%WHISPERS.length; el.textContent=WHISPERS[wi]; el.style.opacity=1; },450); }, 9000); /* -------------------------------------------------------- address */ fetch('/api/wallet/new').then(function(r){return r.json();}).then(function(j){ if(!j.address) throw 0; addr=j.address; $('addr').textContent=addr; $('btnStart').disabled=false; try { localStorage.setItem('mb_start_address', addr); var saved=JSON.parse(sessionStorage.getItem('addresses')||'[]'); saved.unshift({address:addr}); sessionStorage.setItem('addresses',JSON.stringify(saved)); } catch(e){} }).catch(function(){ $('addr').textContent='Could not reach the node — reload to retry.'; }); $('btnCopy').addEventListener('click', function(){ if(addr) copyText(addr); toast('Address copied'); }); /* --------------------------------------------------------- mining */ /* Attribution comes from the chain-derived leaderboard, not the server's aggregate counter: several people may mine this node at once, and the aggregate would credit everyone with everyone else's blocks. */ function myBlocks(d){ var lb=d.leaderboard||[]; for(var i=0;i<lb.length;i++) if(lb[i].address===addr) return lb[i].blocks||0; return 0; } function poll(){ if(busy) return; busy=true; fetch('/api/mining/status?include=leaderboard').then(function(r){return r.json();}) .then(function(d){ busy=false; if(!d || d.status==='error') return; /* Each field is written independently. A single throw here used to abort the whole handler before the completion check ever ran, and the bare catch below hid it — the page then hunted forever while the node had already mined the block. */ try { if(d.difficulty){ $('mDiff').textContent = fmt(d.difficulty); $('oOdds').textContent = fmt(d.difficulty*4294967296); } if(d.current_height!=null) $('mHeight').textContent = ht(d.current_height); $('mReward').textContent = (d.next_block_reward_coins||CONS.reward); } catch(e){ if(window.console) console.warn('stat render failed', e); } var mine = 0; try { mine = myBlocks(d); } catch(e){ mine = 0; } if(mine>baseBlocks){ finish(mine-baseBlocks, d.current_height); return; } /* The leaderboard is the primary signal but needs the coinbase indexed, so it can lag behind the block. Only this address's own balance is checked as a backstop — a global signal like the chain height or the node's blocks_mined would credit this miner for somebody else's block. */ if(d.status!=='mining' && !d.mining){ confirmEarned(d.current_height); } }).catch(function(e){ busy=false; if(window.console) console.warn('mining poll failed', e); }); } /* The job has stopped. Decide between "found one" and "nothing yet" by asking what this address actually holds, rather than trusting a signal that could belong to another miner. */ function confirmEarned(height){ if(!addr){ stop(false); return; } fetch('/api/address/'+encodeURIComponent(addr)+'/balance') .then(function(r){ return r.json(); }) .then(function(b){ if(b && b.status==='success' && (b.balance_units||0) > 0){ finish(1, height); } else { stop(false); } }) .catch(function(){ stop(false); }); } function finish(delta, height){ mining=false; clearInterval(poller); clearInterval(ticker); fetch('/api/mining/stop').catch(function(){}); var reward = CONS.reward * delta; $('dAmt').textContent = '+' + reward + ' MBITE'; $('dMeta').textContent = 'Block #' + ht(height) + ' · paid to ' + shortAddr(addr); if(window.MoonBiteBadges){ try { MoonBiteBadges.recordBlocks(delta, height, addr); } catch(e){} try { var prof=JSON.parse(localStorage.getItem('mb_miner_profiles')||'{}')[addr]; if(prof && prof.badges){ var NAMES={first_bite:' First Bite',genesis:' Genesis Miner',pioneer:'Pioneer', pre_halving:'Pre-Halving',ten:'Ten Bites',hundred:' Hundred Bites', week_streak:' Seven Moons'}; $('dBadges').innerHTML = prof.badges.map(function(b){ return '<span class="bpill">'+(NAMES[b]||b)+'</span>'; }).join(''); } } catch(e){} } show('sDone'); } function stop(found){ mining=false; if(poller){ clearInterval(poller); poller=null; } if(ticker){ clearInterval(ticker); ticker=null; } fetch('/api/mining/stop').catch(function(){}); if(!found){ show('sReady'); $('btnStart').disabled=false; $('btnStart').innerHTML='START MINING'; } } function begin(){ if(!addr) return; var b=$('btnStart'); b.disabled=true; b.textContent='STARTING…'; fetch('/api/mining/start',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({blocks:1,address:addr})}) .then(function(r){return r.json();}).then(function(d){ if(d.status!=='mining') throw new Error(d.message||'start failed'); mining=true; started=Date.now(); baseBlocks=0; baseHeight=null; show('sMining'); poll(); poller=setInterval(poll,2000); /* elapsed ticks locally every second — the one number measured here */ ticker=setInterval(function(){ if(mining) $('mElapsed').textContent=clock((Date.now()-started)/1000); },1000); }).catch(function(){ b.disabled=false; b.innerHTML='START MINING'; toast('Could not start mining. Try again.'); }); } $('btnStart').addEventListener('click', begin); $('btnStop').addEventListener('click', function(){ stop(false); }); $('againCard').addEventListener('click', function(e){ e.preventDefault(); begin(); }); })(); </script> </body> </html>