Created
May 10, 2025 19:30
-
-
Save mikedotexe/2e1f8707cd93c419026ff1fb4f7f70f4 to your computer and use it in GitHub Desktop.
npx serve .
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>NEAR Validator Balances</title> | |
| <style> | |
| :root { | |
| --bg-gradient: linear-gradient(135deg, #131f37 0%, #1c2942 100%); | |
| --text: #ffffff; | |
| --accent: #64ffda; | |
| --card-bg: rgba(13, 25, 48, 0.98); | |
| font-size: 19px; | |
| } | |
| *{box-sizing:border-box} | |
| body{ | |
| margin: 0; | |
| font-family: "Inter", system-ui, sans-serif; | |
| background: var(--bg-gradient); | |
| color: var(--text); | |
| min-height: 100vh; | |
| } | |
| body p { | |
| line-height: 1.9rem; | |
| } | |
| @keyframes ripple { | |
| 0% { opacity: 1; } | |
| 100% { opacity: 0; } | |
| } | |
| #bar-wrap{position:fixed;top:0;left:0;width:100%;height:3px;background:rgba(255,255,255,.08);z-index:1000;border-bottom: 1px solid rgba(100, 255, 218, 0.1)} | |
| #bar{width:0%;height:100%;background:var(--accent);transition:width .2s ease;box-shadow: 0 0 15px rgba(100, 255, 218, 0.15), 0 0 30px rgba(100, 255, 218, 0.07)} | |
| main{padding:5rem 1rem 2rem;max-width:46rem;margin:0 auto;position:relative} | |
| input,button{font:inherit;padding:.6rem 1rem;border-radius:4px;border:1px solid rgba(100, 255, 218, 0.1);background:var(--card-bg);color:var(--text)} | |
| input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px rgba(100, 255, 218, 0.2)} | |
| button{ | |
| cursor:pointer; | |
| border-color:var(--accent); | |
| background:rgba(0, 0, 0, 0.2); | |
| color:var(--accent); | |
| transition:all 0.2s ease; | |
| position:relative; | |
| overflow:hidden; | |
| } | |
| button:hover:not(:disabled){ | |
| background:rgba(100, 255, 218, 0.1); | |
| border-color:var(--accent); | |
| } | |
| button:focus{ | |
| outline:none; | |
| box-shadow:0 0 15px rgba(100, 255, 218, 0.15); | |
| } | |
| button:active:not(:disabled){ | |
| transform:scale(0.98); | |
| } | |
| button:disabled{ | |
| opacity:0.6; | |
| cursor:not-allowed; | |
| background:rgba(0, 0, 0, 0.3); | |
| } | |
| button.active{ | |
| background:var(--accent); | |
| color:var(--card-bg); | |
| transform:scale(0.98); | |
| } | |
| button::after{ | |
| content:''; | |
| position:absolute; | |
| top:50%; | |
| left:50%; | |
| width:5px; | |
| height:5px; | |
| background:rgba(255,255,255,0.4); | |
| border-radius:100%; | |
| transform:scale(0); | |
| opacity:0; | |
| transition:all 0.5s; | |
| } | |
| button.ripple::after{ | |
| animation:ripple 0.6s linear; | |
| transform:scale(40); | |
| opacity:0; | |
| } | |
| pre{ | |
| overflow-x:auto; | |
| background:rgba(0, 0, 0, 0.2); | |
| border:1px solid rgba(100, 255, 218, 0.15); | |
| padding:1rem; | |
| border-radius:0.5rem; | |
| white-space:pre-wrap; | |
| word-break:break-word; | |
| box-shadow:0 0 15px rgba(100, 255, 218, 0.15), 0 0 30px rgba(100, 255, 218, 0.07), 0 0 60px rgba(100, 255, 218, 0.04); | |
| } | |
| .warn{color:#ff7373} | |
| h1 { | |
| color: var(--accent); | |
| margin-bottom: 0.5rem; | |
| } | |
| .neon-text { | |
| text-shadow: 0 0 1px #00ffa2, 0 0 0 #00ffa2, 0 0 1px #00a2ff; | |
| letter-spacing: 0.2rem; | |
| } | |
| </style> | |
| <script src="https://unpkg.com/@fastnear/api/dist/umd/browser.global.js"></script> | |
| <script type="module"> | |
| /* global near */ | |
| await import('https://unpkg.com/@fastnear/api/dist/umd/browser.global.js'); | |
| const { sendRpc, utils } = near; | |
| if(!sendRpc) throw new Error('sendRpc missing'); | |
| near.config({ | |
| networkId:'mainnet', | |
| nodeUrl:'https://rpc.mainnet.fastnear.com', | |
| }); | |
| const td = new TextDecoder(); | |
| const encodeArgs = (o)=>utils.toBase64(JSON.stringify(o)); | |
| const decodeBytes = (bytes)=>td.decode(Uint8Array.from(bytes)); | |
| const parseAmt = (b)=>decodeBytes(b).replace(/"/g,''); | |
| // simple concurrency limiter | |
| const limit = async (iter, concurrency, fn) => { | |
| const ret=[]; const exe=[]; | |
| for(const it of iter){ | |
| const p=Promise.resolve().then(()=>fn(it)); | |
| ret.push(p); | |
| if(concurrency<=iter.length){ | |
| const e=p.then(()=>exe.splice(exe.indexOf(e),1)); | |
| exe.push(e); | |
| if(exe.length>=concurrency) await Promise.race(exe); | |
| } | |
| } | |
| return Promise.all(ret); | |
| }; | |
| const bar = document.getElementById('bar'); | |
| const input = document.getElementById('acct'); | |
| const run = document.getElementById('run'); | |
| const count = document.getElementById('count'); | |
| const out = document.getElementById('out'); | |
| const setBar=(d,t)=>bar.style.width=`${(d/t*100).toFixed(1)}%`; | |
| const viewCall = (v,m,a)=>sendRpc('query',{ | |
| request_type:'call_function',account_id:v,method_name:m,args_base64:encodeArgs(a),finality:'optimistic' | |
| }); | |
| async function fetchBalances(acct){ | |
| run.disabled=true; | |
| run.classList.add('active'); | |
| out.textContent='Fetching…'; | |
| count.textContent='0'; | |
| try{ | |
| const { result:{current_validators} } = await sendRpc('validators',[null]); | |
| const validators=current_validators.map(v=>v.account_id); | |
| const totalCalls=validators.length*2; let done=0; setBar(0,totalCalls); | |
| let processed=0; | |
| const balances=[]; const errors=[]; | |
| await limit(validators, 120, async v => { | |
| let staked='0',unstaked='0',err=null; | |
| const rpc=async m=>{ | |
| for(let i=0;i<3;i++){ | |
| try{const r=await viewCall(v,m,{account_id:acct});return parseAmt(r.result.result);}catch(e){if(/429|Too Many/i.test(e.message)){await new Promise(r=>setTimeout(r,500*(i+1)));continue;}throw e;}} | |
| throw new Error('429 Too Many Requests'); | |
| }; | |
| try{staked=await rpc('get_account_staked_balance');}catch(e){err=e.message;}finally{setBar(++done,totalCalls);} | |
| try{unstaked=await rpc('get_account_unstaked_balance');}catch(e){err=err||e.message;}finally{setBar(++done,totalCalls);} | |
| processed++;count.textContent=processed.toString(); | |
| if(err){errors.push({validator:v,error:err});return;} | |
| if(staked!=='0'||unstaked!=='0') balances.push({validator:v,staked,unstaked}); | |
| }); | |
| out.innerHTML=`${balances.length?JSON.stringify({balances},null,2):'<span class="warn">No non-zero balances</span>'}\n\n${errors.length?JSON.stringify({errors},null,2):''}`; | |
| setBar(totalCalls,totalCalls); | |
| }catch(e){ out.innerHTML=`<span class="warn">Fatal: ${e.message}</span>`; } | |
| finally{ | |
| run.disabled=false; | |
| run.classList.remove('active'); | |
| } | |
| } | |
| const trigger=()=>{ const a=input.value.trim(); if(!a){alert('Enter account_id');return;} fetchBalances(a);} ; | |
| // Visual feedback for button interactions | |
| run.addEventListener('mousedown', () => { | |
| if (!run.disabled) { | |
| run.classList.add('ripple'); | |
| setTimeout(() => run.classList.remove('ripple'), 600); | |
| } | |
| }); | |
| run.addEventListener('keydown', (e) => { | |
| if ((e.key === 'Enter' || e.key === ' ') && !run.disabled) { | |
| run.classList.add('ripple'); | |
| setTimeout(() => run.classList.remove('ripple'), 600); | |
| } | |
| }); | |
| // Focus handling | |
| run.addEventListener('blur', () => { | |
| run.classList.remove('ripple'); | |
| }); | |
| run.addEventListener('click', trigger); | |
| input.addEventListener('keydown', e => { if(e.key === 'Enter') trigger(); }); | |
| </script> | |
| </head> | |
| <body> | |
| <div id="bar-wrap"><div id="bar"></div></div> | |
| <main> | |
| <div class="hero-section"> | |
| <h1 class="neon-text">Validator Balances</h1> | |
| <p>Check staking balances across all NEAR validators</p> | |
| <p>Validators queried: <strong id="count" class="accent">0</strong></p> | |
| <div style="display:flex;gap:.5rem;margin:1.5rem 0;position:relative;"> | |
| <input id="acct" value="root.near" style="flex:1 1 16rem" placeholder="Enter NEAR account ID" /> | |
| <button id="run">Check staking</button> | |
| </div> | |
| <pre id="out">Hit that button to see your staking balances across all validators.</pre> | |
| </div> | |
| </main> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment