-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathteztracker.html
More file actions
1 lines (1 loc) · 29.3 KB
/
teztracker.html
File metadata and controls
1 lines (1 loc) · 29.3 KB
1
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>TezTracker - Track Tezos Earnings from All Your Wallets in One Place.</title><meta name="description" content="Track Tezos Earnings from All Your Wallets in One Place"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96" /><link rel="icon" type="image/svg+xml" href="favicon.svg" /><link rel="shortcut icon" href="favicon.ico" /><meta property="og:type" content="website"><meta property="og:url" content="https://from-friends.github.io/teztracker.html"><meta property="og:title" content="↗ Try out TezTracker now!"><meta property="og:description" content="Track Tezos earnings from all your wallets in one place."><meta property="og:image" content="https://from-friends.github.io/teztracker-social-meta-image.png?v=072025"><meta property="twitter:card" content="summary_large_image"><meta property="twitter:url" content="https://from-friends.github.io/teztracker.html"><meta property="twitter:title" content="↗ Try out TezTracker now!"><meta property="twitter:description" content="Track Tezos earnings from all your wallets in one place."><meta property="twitter:image" content="https://from-friends.github.io/teztracker-social-meta-image.png?v=072025"><style>:root{--bg-color:#000;--card-bg:#1a1a1a;--card-hover-bg:#2b2b2b;--text-color:#eaeaea;--accent-color:#fff;--border-color:#333;--secondary-text:#888}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background-color:var(--bg-color);color:var(--text-color);padding:20px;padding-top:70px;max-width:800px;margin:0 auto}h1,h2,h3{color:var(--accent-color);margin-top:2em;margin-bottom:1em;border-bottom:1px solid var(--border-color);padding-bottom:.5em}h1{font-size:2.2em;border-bottom:none}h2{font-size:1.7em;scroll-margin-top:80px}h3{font-size:1.3em;border-bottom-width:0;margin-top:1.5em}table{width:100%;border-collapse:collapse;margin-top:15px;margin-bottom:25px;background-color:transparent;border-radius:8px;overflow:hidden}th,td{padding:14px 16px;text-align:left;font-size:.95em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}th{background-color:var(--accent-color);font-weight:600;color:var(--bg-color)}tbody tr{transition:background-color .15s ease-in-out}tbody tr:nth-child(even){background-color:var(--card-bg)}tbody tr:hover{background-color:var(--card-hover-bg)}tfoot td{font-weight:bold;background-color:#1a1a1a}.tx-table-wrapper{background:transparent;padding:0;border-radius:0;border:none;margin-top:20px;scroll-margin-top:80px}.total-value-display{font-size:2.5em;font-weight:700;text-align:left;margin:10px 0 0;color:#4ade80;line-height:1.1}.total-value-sub{font-size:.5em;font-weight:500;color:var(--secondary-text);margin-top:5px;margin-bottom:30px}.market-data{display:flex;gap:20px;margin-bottom:30px;padding:15px 20px;background-color:var(--card-bg);border-radius:8px}.market-item{flex:1}.market-label{font-size:.8em;color:var(--secondary-text);text-transform:uppercase}.market-value{display:block;font-size:1.1em;font-weight:600;color:var(--text-color);margin-top:2px}.market-timestamp{display:block;font-size:.8em;color:var(--secondary-text);margin-top:4px}.timestamp{font-size:.85em;color:var(--secondary-text)}a{color:var(--text-color);text-decoration:none}a u{text-decoration:none;border-bottom:1px solid var(--secondary-text);transition:border-color .15s ease-in-out}a:hover u{border-bottom-color:var(--text-color)}#addressInput{margin-bottom:25px;display:flex;gap:10px;flex-direction:column}.input-wrapper{position:relative}#addressInput input{width:100%;padding:10px 30px 10px 10px;font-size:1em;background-color:var(--card-bg);border:1px solid var(--border-color);color:var(--text-color);border-radius:6px;box-sizing:border-box;transition:border-color .15s ease-in-out}#addressInput input:focus{outline:none;border-color:#fff}.clear-input{position:absolute;right:12px;top:50%;transform:translateY(-50%);cursor:pointer;color:var(--secondary-text);font-size:1.8em;line-height:1;display:none;font-weight:300}.clear-input:hover{color:var(--text-color)}#addressInput button{padding:10px 20px;background-color:var(--accent-color);border:1px solid var(--accent-color);color:#000;cursor:pointer;font-size:1em;border-radius:6px;transition:all .2s ease-in-out}#addressInput button:hover{background-color:var(--bg-color);color:var(--accent-color)}.title-wrapper{margin-bottom:30px;text-align:center}.title-wrapper h1{font-size:2.8em;font-weight:700;border-bottom:none;margin-bottom:0}.title-wrapper small{display:block;font-size:1.2em;color:var(--secondary-text);max-width:450px;margin:10px auto 0;line-height:1.5}.footer{margin-top:50px;border-top:1px solid var(--border-color);padding:40px 0;font-size:.9em}.footer-sections{display:flex;justify-content:space-between;gap:40px}.footer-section h3{font-size:1.1em;color:var(--accent-color);margin-top:0;margin-bottom:15px;padding-bottom:0;border-bottom:none}.footer-links{list-style:none;padding:0;margin:0}.footer-links li{margin-bottom:10px}.footer-links a{color:var(--secondary-text);text-decoration:none;transition:color .15s ease-in-out}.footer-links a:hover{color:var(--text-color)}.footer-logo{height:30px}.btn{padding:.75rem 1.5rem;border-radius:.5rem;border:none;cursor:pointer;font-size:.875rem;font-weight:600;transition:all .2s;text-decoration:none;display:inline-flex;align-items:center;gap:.5rem}.btn-primary{background-color:#FF5722;color:white}.btn-primary:hover:not(:disabled){background-color:#E64A19}.btn-secondary{background-color:#4b5563;color:white}.btn-secondary:hover{background-color:#374151}.btn-outline{background-color:transparent;border:1px solid #4b5563;color:var(--secondary-text)}.btn-outline:hover:not(:disabled){background-color:#4b5563;color:white}.modal-container{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:1000}.modal-content{background-color:var(--card-bg);border:1px solid var(--border-color);border-radius:1rem;padding:2rem;width:90%;max-width:380px;position:relative;text-align:center}.close-btn{position:absolute;top:1rem;right:1rem;background:transparent;border:none;color:var(--secondary-text);font-size:2rem;font-weight:300;cursor:pointer;padding:.5rem;line-height:1}.close-btn:hover{color:var(--text-color)}.profile-card{background-color:var(--bg-color);border-radius:.5rem;padding:1rem;display:flex;align-items:center;margin-top:1rem;border:1px solid var(--border-color)}.profile-avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;margin-right:1rem;border:1px solid var(--border-color)}.profile-info{text-align:left}.profile-username{font-weight:600;color:var(--text-color)}.profile-address{font-size:.7rem;color:var(--secondary-text);font-family:'Courier New',monospace;word-break:break-all}.actions-container{display:flex;gap:.75rem;margin-top:1rem}.action-button{flex:1}.toolbar{background-color:var(--bg-color);border-bottom:1px solid var(--border-color);padding:12px 0;position:fixed;top:0;left:0;width:100%;z-index:100;transform:translateZ(0)}.toolbar-content{max-width:800px;margin:0 auto;padding:0 20px;display:flex;justify-content:space-between;align-items:center}.toolbar-title{font-weight:600;font-size:1.4em;color:var(--accent-color)}.toolbar-logo{height:16px}.avatar{width:24px;height:24px;border-radius:50%;margin-right:8px;vertical-align:middle}.load-more-container{text-align:center;padding:15px 0}.load-more-container button{padding:8px 16px;background-color:var(--card-bg);border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;font-size:.9em;border-radius:6px;transition:all .2s ease-in-out}.load-more-container button:hover:not(:disabled){background-color:var(--bg-color);border-color:var(--accent-color)}.load-more-container button:disabled{cursor:not-allowed;opacity:.7}.demo-link-container{text-align:center;margin-top:10px}.demo-link-container a{color:var(--secondary-text);font-size:.9em;text-decoration:none;transition:color .15s ease-in-out}.demo-link-container a:hover{color:var(--text-color)}.view-btn{font-size:.9em;padding:4px 10px;color:var(--text-color);background-color:var(--card-bg);border:1px solid var(--border-color);border-radius:4px;text-decoration:none;transition:all .2s ease-in-out}.view-btn:hover{background-color:var(--card-hover-bg);border-color:var(--accent-color)}@media (max-width:768px){body{padding:15px;padding-top:70px}.title-wrapper h1{font-size:2.2em}.title-wrapper small{font-size:1.1em}.total-value-display{font-size:2em;margin:5px 0 0}.total-value-sub{margin-bottom:25px}.footer-sections{flex-direction:column;gap:30px}}@media (max-width:480px){.toolbar-title{font-size:1.2em}.toolbar-logo{height:14px}.toolbar-content{padding:0 15px}h1{font-size:1.8em}h2{font-size:1.5em}h3{font-size:1.2em}th,td{padding:12px 10px;font-size:.9em;white-space:normal}.title-wrapper h1{font-size:2em}.title-wrapper small{font-size:1em}#addressInput input,#addressInput button{font-size:.95em}}#balanceTable>tbody:hover{background-color:var(--card-hover-bg)}.balance-tx-details{padding:4px 16px 12px 46px;font-size:.9em}.balance-reward-item{display:flex;justify-content:space-between;padding:3px 0;color:var(--secondary-text)}.positive-amount{color:#4ade80}.balance-tx-item{display:flex;justify-content:space-between;padding:3px 0;color:var(--secondary-text)}.balance-tx-item .sender{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:15px}.address-cell{display:flex;align-items:center}.address-cell a{line-height:1}</style><script defer src="https://cloud.umami.is/script.js" data-website-id="77f26836-27dd-4d25-b9b0-dcf4cdf4b2b7"></script></head><body><header class="toolbar"><div class="toolbar-content"><div class="toolbar-title">TezTracker</div><a href="https://from-friends.github.io/" target="_blank" rel="noopener noreferrer"><img src="fromfriends.svg" alt="FromFriends Logo" class="toolbar-logo"></a></div></header><div id="landing-page"><div class="title-wrapper"><h1>Monitor All Your Tezos Wallets</h1><small>Track balances and recent transactions for multiple Tezos addresses in one simple, clean interface.</small></div></div><div id="addressInput"><div class="input-wrapper"><input type="text" id="inputAddresses" placeholder="Enter Tezos addresses, comma separated"><span id="clearInputBtn" class="clear-input">×</span></div><button onclick="saveAddresses()">Save Addresses</button><div class="demo-link-container"><a href="?addresses=tz2SoxV4W1coqzxLaPA5kvWMXACGgA5eR7Bf,tz1PoDdN2oyRyF6DA73zTWAWYhNL4UGr3Egj,tz1eZ8amacCvSQsFM8wampwWXJFWsmGVRQFd,tz1Roq6end2LFtkpGrmuyRZH82xsWfaRCat1,tz1gBXG9fg8RMDH69KfKqwoTH5sFDmzt5yzm">Show Demo</a></div></div><main id="app-interface" style="display: none;"><h2 id="balances-heading">Tezos Balances</h2><div id="totalValue" class="total-value-display"><div id="totalValueUSD"></div><div id="totalValueSub" class="total-value-sub"><span id="totalValueꜩ"></span> | <span id="totalValueEUR"></span></div></div><div class="market-data"><div class="market-item"><span class="market-label">XTZ Price</span><span class="market-value" id="xtzPriceValue">Loading...</span><span class="market-timestamp" id="xtzPriceTimestamp"></span></div><div class="market-item"><span class="market-label">EUR → USD</span><span class="market-value" id="eurUsdRate">Loading...</span><span class="market-timestamp" id="eurUsdTimestamp"></span></div></div><table id="balanceTable"><thead><tr><th>Address</th><th class="numeric">ꜩ</th><th class="numeric">USD</th></tr></thead><tfoot><tr><td>Total</td><td id="totalꜩ" class="numeric">...</td><td id="totalUSD" class="numeric">...</td></tr></tfoot></table><h2>Latest Received Transactions</h2><div id="transactionsContainer"></div></main><footer class="footer"><div class="footer-sections"><div class="footer-section"><a href="https://from-friends.github.io/" target="_blank" rel="noopener noreferrer"><img src="fromfriends.svg" alt="FromFriends Logo" class="footer-logo"></a></div><div class="footer-section"><h3>Tezos Tools</h3><ul class="footer-links"><li><a href="https://tiktoktezos.netlify.app/" target="_blank" rel="noopener noreferrer">TikTok for Tezos</a></li><li><a href="https://frametogo.netlify.app/" target="_blank" rel="noopener noreferrer">Display your Tezos-based digital art</a></li><li><a href="https://from-friends.github.io/objktz.html?search=a&offset=0" target="_blank" rel="noopener noreferrer">Digital art explorer for Objkt.com</a></li></ul></div><div class="footer-section"><h3>Contact</h3><ul class="footer-links"><li><a href="https://x.com/FromFriends__" target="_blank" rel="noopener noreferrer">Follow FromFriends on X</a></li><li><a href="mailto:heyfromfriends@gmail.com">Email FromFriends</a></li><li><a href="https://from-friends.github.io/" target="_blank" rel="noopener noreferrer">FromFriends Website</a></li></ul></div><div class="footer-section"><h3>Support Me</h3><ul class="footer-links"><li><button class="btn btn-primary donate-button">Donate</button></li></ul></div></div></footer><div id="donateModal" class="modal-container" style="display: none;"><div class="modal-content"><button class="close-btn" id="close-donate-modal-btn">×</button><h3 style="margin-bottom: 0.5rem;">Donate</h3><p style="color: #a1a1aa; font-size: 0.9rem;">Many thanks for your support! 🫶</p><div class="profile-card"><img src="teztracker/avatar-placeholder.jpg" alt="Avatar" class="profile-avatar"><div class="profile-info"><div class="profile-username">fromfriends.tez</div><div class="profile-address" id="tezos-address">tz1U49JkLGSUR7JFN5xFoFghNwMXnQtR7Pv5</div></div></div><p style="color: #a1a1aa; font-size: 0.9rem; margin-top: 1.5rem; margin-bottom: 0;">Scan the FromFriends Tezos address</p><br/><img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=tz1U49JkLGSUR7JFN5xFoFghNwMXnQtR7Pv5" alt="QR Code for Tezos address"><div class="actions-container"><button id="copy-address-btn" class="btn btn-secondary action-button">Copy Address</button><a href="https://kukai.app" target="_blank" rel="noopener noreferrer" class="btn btn-outline action-button">Open Kukai</a></div></div></div><script>const TX_PAGE_LIMIT=5;const LOAD_MORE_LIMIT=10;const accountsCache={};const animalEmojis=["🐶","🐱","🦊","🐸","🦁","🦉","🐼","🐧","🐨","🐻","🐢","🐍","🐦","🐞","🐙","🦄","🦋","🐝","🐬","🐹","🦓","🦒","🦔","🐊","🐘","🐫","🦘","🦡"];const balanceTable=document.getElementById("balanceTable");const totalꜩCell=document.getElementById("totalꜩ");const totalUSDCell=document.getElementById("totalUSD");const txContainer=document.getElementById("transactionsContainer");const inputAddresses=document.getElementById("inputAddresses");const clearInputBtn=document.getElementById("clearInputBtn");function truncateAddress(address,startLength=5,endLength=5){if(!address||address.length<=startLength+endLength+3){return address}const start=address.substring(0,startLength);const end=address.substring(address.length-endLength);return`${start}...${end}`}async function fetchDelegationRewards(address,limit=3){try{const url=`https://staging.api.tzkt.io/v1/rewards/split/${address}?sort.desc=cycle&limit=${limit}`;const res=await fetch(url);if(res.status===404){return[]}if(!res.ok){console.error(`Failed to fetch delegation rewards for ${address}, status: ${res.status}`);return[]}return await res.json()}catch(error){console.error(`Failed to fetch delegation rewards for ${address}`,error);return[]}}async function fetchStakingRewards(address,limit=3){try{const url=`https://staging.api.tzkt.io/v1/rewards/stakers/${address}?sort.desc=cycle&limit=${limit}`;const res=await fetch(url);if(res.status===404){return[]}if(!res.ok){console.error(`Failed to fetch staking rewards for ${address}, status: ${res.status}`);return[]}const rewards=await res.json();return rewards.filter(r=>(r.rewards||0)>0)}catch(error){console.error(`Failed to fetch staking rewards for ${address}`,error);return[]}}function getAddresses(){const params=new URLSearchParams(window.location.search);const raw=params.get("addresses");return raw?raw.split(",").map(a=>a.trim()).filter(Boolean):[]}function saveAddresses(){const input=document.getElementById("inputAddresses").value;const addresses=input.split(",").map(addr=>addr.trim()).filter(Boolean);const newUrl=`${window.location.pathname}?addresses=${addresses.join(",")}`;window.location.href=newUrl}clearInputBtn.addEventListener('click',()=>{inputAddresses.value='';clearInputBtn.style.display='none';inputAddresses.focus()});inputAddresses.addEventListener('input',()=>{clearInputBtn.style.display=inputAddresses.value.length>0?'block':'none'});async function fetchꜩtoUSD(){try{const res=await fetch("https://api.coingecko.com/api/v3/simple/price?ids=tezos&vs_currencies=usd");if(!res.ok)throw new Error('Network response was not ok');const data=await res.json();const rate=data.tezos.usd;const lastUpdateTime=new Date();const timeString=lastUpdateTime.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit',second:'2-digit',hour12:true});return{rate,timeString}}catch(error){console.error("Failed to fetch ꜩ to USD rate:",error);document.getElementById('xtzPriceValue').textContent='Could not load price';document.getElementById('xtzPriceTimestamp').textContent='';return null}}async function fetchAccountData(address){if(accountsCache[address]?.checkedForDomain)return accountsCache[address];try{if(!accountsCache[address]){const res=await fetch(`https://api.tzkt.io/v1/accounts/${address}`);accountsCache[address]=await res.json()}if(!accountsCache[address].alias){let foundDomain=null;try{const query={query:`{ reverseRecord(address: "${address}") { domain { name } } }`};const domainRes=await fetch('https://api.tezos.domains/graphql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(query)});if(domainRes.ok){const domainData=await domainRes.json();if(domainData.data?.reverseRecord?.domain?.name){foundDomain=domainData.data.reverseRecord.domain.name}}}catch(e){console.error(`GraphQL lookup for ${address} failed`,e)}if(!foundDomain){try{const tzktDomainRes=await fetch(`https://api.tzkt.io/v1/domains/${address}/reverse`);if(tzktDomainRes.ok){const tzktDomainText=await tzktDomainRes.text();if(tzktDomainText){foundDomain=JSON.parse(tzktDomainText).name}}}catch(e){console.error(`TzKT reverse lookup for ${address} failed`,e)}}if(foundDomain){accountsCache[address].tezDomain=foundDomain}}}catch(error){console.error(`Failed to fetch extended data for ${address}`,error);if(!accountsCache[address])accountsCache[address]={balance:0,alias:null}}accountsCache[address].checkedForDomain=true;return accountsCache[address]}async function fetchReceivedTransactions(address,offset=0,limit=TX_PAGE_LIMIT){const url=`https://api.tzkt.io/v1/operations/transactions?target=${address}&sort.desc=id&limit=${limit}&offset=${offset}`;const res=await fetch(url);return await res.json()}function formatDate(timestamp){return new Date(timestamp).toLocaleString("en-US",{month:"short",day:"2-digit",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:true})}function timeAgo(dateString){const now=new Date();const then=new Date(dateString);const diff=now-then;const msInHour=1000*60*60;const msInDay=msInHour*24;const msInMonth=msInDay*30.44;const months=Math.floor(diff/msInMonth);const days=Math.floor((diff%msInMonth)/msInDay);const hours=Math.floor((diff%msInDay)/msInHour);let result="";if(months>0)result+=`${months} month${months>1?"s":""}, `;if(days>0)result+=`${days} day${days>1?"s":""}, `;result+=`${hours} hour${hours!==1?"s":""}`;return result}async function displayBalances(addresses){const balanceTable=document.getElementById("balanceTable");balanceTable.querySelectorAll('tbody').forEach(tbody=>tbody.remove());const usdPromise=fetchꜩtoUSD();const eurPromise=fetchEurUsdRate();const[xtzData,eurData]=await Promise.all([usdPromise,eurPromise]);if(xtzData){document.getElementById('xtzPriceValue').textContent=`$${xtzData.rate.toFixed(4)}`;document.getElementById('xtzPriceTimestamp').textContent=`Last updated: ${xtzData.timeString}`}else{return}const usdRate=xtzData.rate;const eurRate=eurData?eurData.rate:null;let totalꜩ=0;for(let i=0;i<addresses.length;i++){const address=addresses[i];const accountData=await fetchAccountData(address);if(accountData.tezDomain){console.log(`Address ${address} has .tez domain: ${accountData.tezDomain}`)}const displayName=accountData.alias||accountData.tezDomain||truncateAddress(address);const xtz=accountData.balance/1_000_000;const usd=xtz*usdRate;totalꜩ+=xtz;const newTbody=document.createElement("tbody");const balanceRow=document.createElement("tr");balanceRow.innerHTML=`<td class="address-cell"><a href="https://tzkt.io/${address}/operations/" target="_blank" title="View on TzKT explorer"><img src="https://services.tzkt.io/v1/avatars/${address}" alt="avatar" class="avatar"></a><a href="#transactions-${address}" class="view-btn" style="margin-left: 10px;">${displayName}</a></td><td class="numeric"><a href="#transactions-${address}" style="text-decoration: none;">${xtz.toFixed(6)} ꜩ</a></td><td class="numeric">$${usd.toFixed(2)}</td>`;newTbody.appendChild(balanceRow);let latestTxs=await fetchReceivedTransactions(address,0,3);let latestDelegationRewards=await fetchDelegationRewards(address,3);let latestStakingRewards=await fetchStakingRewards(address,3);if(latestTxs.length>0||latestDelegationRewards.length>0||latestStakingRewards.length>0){const txDetailsRow=document.createElement('tr');const txDetailsCell=document.createElement('td');txDetailsCell.colSpan=3;const txDetailsContainer=document.createElement('div');txDetailsContainer.className='balance-tx-details';let txHtml='';if(latestTxs.length>0){latestTxs=await enrichTransactionsWithDomains(latestTxs);latestTxs.forEach(tx=>{const senderDisplayName=tx.sender?.alias||tx.sender?.tezDomain||(tx.sender?.address?truncateAddress(tx.sender.address,4,4):'Unknown');const amount=(tx.amount/1_000_000).toFixed(4);const age=timeAgo(tx.timestamp);txHtml+=`<div class="balance-tx-item"><span class="sender"><span class="positive-amount">+${amount} ꜩ</span> ${senderDisplayName}</span><span class="age">${age} ago</span></div>`})}if(latestDelegationRewards.length>0){if(txHtml!==''){txHtml+=`<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border-color);"></div>`}latestDelegationRewards.forEach(reward=>{const bakerDisplayName=reward.baker.alias||truncateAddress(reward.baker.address,4,4);const amount=(reward.amount/1_000_000).toFixed(4);txHtml+=`<div class="balance-reward-item"><span><span class="positive-amount">+${amount} ꜩ</span> delegation reward ${bakerDisplayName}</span><span class="cycle">Cycle ${reward.cycle}</span></div>`})}if(latestStakingRewards.length>0){if(txHtml!==''){txHtml+=`<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border-color);"></div>`}latestStakingRewards.forEach(reward=>{const amount=(reward.rewards/1_000_000).toFixed(4);txHtml+=`<div class="balance-reward-item"><span><span class="positive-amount">+${amount} ꜩ</span> staking reward</span><span class="cycle">Cycle ${reward.cycle}</span></div>`})}txDetailsContainer.innerHTML=txHtml;txDetailsCell.appendChild(txDetailsContainer);txDetailsRow.appendChild(txDetailsCell);newTbody.appendChild(txDetailsRow)}balanceTable.insertBefore(newTbody,balanceTable.querySelector('tfoot'))}const totalUSD=totalꜩ*usdRate;totalꜩCell.textContent=`${totalꜩ.toFixed(6)} ꜩ`;totalUSDCell.textContent=`$${totalUSD.toFixed(2)}`;document.getElementById('totalValueUSD').textContent=`${totalꜩ.toFixed(6)} ꜩ`;document.getElementById('totalValueꜩ').textContent=`$${totalUSD.toFixed(2)}`;if(eurRate){const totalEUR=totalUSD/eurRate;document.getElementById('totalValueEUR').textContent=`€${totalEUR.toFixed(2)} EUR`}else{document.getElementById('totalValueEUR').textContent=''}}async function displayTransactions(addresses){for(let i=0;i<addresses.length;i++){const address=addresses[i];const accountData=await fetchAccountData(address);const displayName=accountData.alias||accountData.tezDomain||truncateAddress(address);let txs=await fetchReceivedTransactions(address,0);txs=await enrichTransactionsWithDomains(txs);const wrapper=document.createElement("div");wrapper.id=`transactions-${address}`;wrapper.className="tx-table-wrapper";const heading=document.createElement("h3");heading.innerHTML=`<img src="https://services.tzkt.io/v1/avatars/${address}" alt="avatar" class="avatar"> <a href="https://tzkt.io/${address}/operations/" target="_blank">Transactions for: <u>${displayName}</u></a>`;const table=document.createElement("table");const thead=document.createElement("thead");thead.innerHTML=`<tr><th>From</th><th class="numeric">Amount (ꜩ)</th><th>Date</th></tr>`;const tbody=document.createElement("tbody");txs.forEach(tx=>{tbody.appendChild(createTxRow(tx))});table.appendChild(thead);table.appendChild(tbody);wrapper.appendChild(heading);wrapper.appendChild(table);if(txs.length===TX_PAGE_LIMIT){const loadMoreContainer=document.createElement('div');loadMoreContainer.className='load-more-container';const loadMoreButton=document.createElement('button');loadMoreButton.textContent='Load More';loadMoreButton.dataset.address=address;loadMoreButton.dataset.offset=TX_PAGE_LIMIT;loadMoreButton.addEventListener('click',handleLoadMoreClick);loadMoreContainer.appendChild(loadMoreButton);wrapper.appendChild(loadMoreContainer)}txContainer.appendChild(wrapper)}}function createTxRow(tx){const row=document.createElement("tr");const formattedDate=formatDate(tx.timestamp);const age=timeAgo(tx.timestamp);const senderAddress=tx.sender?.address;const senderDisplayName=tx.sender?.alias||tx.sender?.tezDomain||(senderAddress?truncateAddress(senderAddress):'Unknown');row.innerHTML=`<td>${senderAddress?`<a href="https://tzkt.io/${senderAddress}/operations/" target="_blank"><u>${senderDisplayName}</u></a>`:'Unknown'}</td><td class="numeric"><span class="positive-amount">+${(tx.amount/1_000_000).toFixed(6)} ꜩ</span></td><td><div>${formattedDate}</div><div class="timestamp">${age} ago</div></td>`;return row}async function handleLoadMoreClick(event){const button=event.target;const address=button.dataset.address;const offset=parseInt(button.dataset.offset,10);button.textContent="Loading...";button.disabled=true;let newTxs=await fetchReceivedTransactions(address,offset,LOAD_MORE_LIMIT);newTxs=await enrichTransactionsWithDomains(newTxs);if(newTxs.length>0){const tableBody=button.closest('.tx-table-wrapper').querySelector('tbody');newTxs.forEach(tx=>{tableBody.appendChild(createTxRow(tx))});button.dataset.offset=offset+newTxs.length}if(newTxs.length<LOAD_MORE_LIMIT){button.style.display='none'}else{button.textContent="Load More";button.disabled=false}}async function enrichTransactionsWithDomains(transactions){const senderAddressesToLookup=[...new Set(transactions.filter(tx=>tx.sender&&!tx.sender.alias).map(tx=>tx.sender.address))];if(senderAddressesToLookup.length===0){return transactions}try{const domainRes=await fetch(`https://api.tzkt.io/v1/domains/reverse?addresses=${senderAddressesToLookup.join(',')}`);if(!domainRes.ok)return transactions;const responseText=await domainRes.text();console.log('.tez domain check response:',responseText);if(!responseText){return transactions}const domainData=JSON.parse(responseText);const domainMap=new Map(domainData.map(d=>[d.address,d.name]));transactions.forEach(tx=>{if(tx.sender?.address){const domain=domainMap.get(tx.sender.address);if(domain){tx.sender.tezDomain=domain}}})}catch(error){console.error("Failed to fetch Tezos Domains:",error)}return transactions}async function fetchEurUsdRate(){try{const response=await fetch('https://api.exchangerate-api.com/v4/latest/EUR');if(!response.ok)throw new Error('Network response was not ok');const data=await response.json();const rate=data.rates.USD;const lastUpdateTime=new Date();const timeString=lastUpdateTime.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit',second:'2-digit',hour12:true});document.getElementById('eurUsdRate').textContent=`1 EUR = ${rate.toFixed(4)} USD`;document.getElementById('eurUsdTimestamp').textContent=`Last updated: ${timeString}`;return{rate,timeString}}catch(error){console.error("Failed to fetch EUR to USD rate:",error);document.getElementById('eurUsdRate').textContent='Could not load rate';return null}}const addresses=getAddresses();if(addresses.length>0){document.getElementById("landing-page").style.display="none";document.getElementById("app-interface").style.display="block";inputAddresses.value=addresses.join(", ");clearInputBtn.style.display='block';displayBalances(addresses);displayTransactions(addresses);const balancesHeading=document.getElementById('balances-heading');if(balancesHeading){balancesHeading.scrollIntoView({behavior:'smooth',block:'start'})}}const donateModal=document.getElementById('donateModal');const donateButtons=document.querySelectorAll('.donate-button');const closeDonateModalBtn=document.getElementById('close-donate-modal-btn');const copyAddressBtn=document.getElementById('copy-address-btn');const tezosAddress=document.getElementById('tezos-address').textContent;donateButtons.forEach(button=>{button.addEventListener('click',()=>{donateModal.style.display='flex'})});closeDonateModalBtn.addEventListener('click',()=>{donateModal.style.display='none'});copyAddressBtn.addEventListener('click',()=>{navigator.clipboard.writeText(tezosAddress).then(()=>{const originalText=copyAddressBtn.textContent;copyAddressBtn.textContent='Copied!';setTimeout(()=>{copyAddressBtn.textContent=originalText},2000)}).catch(err=>{console.error('Failed to copy address: ',err)})});window.addEventListener('click',(event)=>{if(event.target===donateModal){donateModal.style.display='none'}})</script></body></html>