/* * SPDX-License-Identifier: AGPL-3.0-or-later */ const API_PREFIX = "https://blockstream.info/api"; const IRON_BLOCK = "https://gamepedia.cursecdn.com/minecraft_gamepedia/7/7e/Block_of_Iron_JE4_BE3.png?version=692673bafa1e94785ab6012d7a3c8dc4"; const GOLD_BLOCK = "https://gamepedia.cursecdn.com/minecraft_gamepedia/archive/7/72/20190429060628%21Block_of_Gold_JE6_BE3.png?version=448d791eb688910a0cc215181312b715"; const DIAMOND_BLOCK = "https://gamepedia.cursecdn.com/minecraft_gamepedia/archive/6/6b/20190502005227%21Block_of_Diamond_JE6_BE3.png?version=721888d830050c06303696cf79695eb9"; const HALVING_PERIOD = 210000; var recentBlocks = null; function getCurTipBlock() { return recentBlocks[0]; } function doApiReq(endpoint, cb) { let xhr = new XMLHttpRequest(); xhr.timeout = 5000; xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { let json = JSON.parse(xhr.responseText); cb(json); } else { // TODO Make this better? cb(null); } } }; xhr.open("GET", API_PREFIX + endpoint); xhr.send() } function doGetRecentBlocks(cb) { doApiReq("/blocks/tip", cb); } function doGetBlockCoinBaseTx(blockhash, cb) { let reqSuffix = "/block/" + blockhash + "/txs"; let internalCb = function(resp) { if (resp != null) { cb(resp[0]); } else { cb(null); } }; doApiReq(reqSuffix, function(resp) { if (resp != null) { internalCb(resp); } else { // Sometimes the data hasn't been fully processed yet by the time we // make the call so wait a bit and then try again. setTimeout(function() { doApiReq(reqSuffix, internalCb); }, 1000) } }); } function calcRenderedSatQty(sats, decimals) { let subBtc = sats % 100000000; let btc = (sats - subBtc) / 100000000; let maskModulus = 10 ** (8 - decimals); let subDecimal = (subBtc - (subBtc % maskModulus)) / maskModulus; let decimal = "0".repeat(decimals) + subDecimal.toString(); if (decimal != 0) { return btc.toString() + "." + decimal.substr(decimals * -1); } else { return btc.toString(); } } function calcNextHalvingHeight() { let height = recentBlocks[0].height; let epoch = Math.floor(height / HALVING_PERIOD); return (epoch + 1) * HALVING_PERIOD; } function calcRewardAtHeight(height) { let epoch = Math.floor(height / HALVING_PERIOD); return 5000000000 / (2 ** epoch); } function calcRewardBreakdown(height, sats) { let subsidy = calcRewardAtHeight(height); let fees = sats - subsidy; let rSubsidy = calcRenderedSatQty(subsidy, 2); let rFees = calcRenderedSatQty(fees, 3); return rSubsidy + " subsidy + " + rFees + " fees" } function toggleElemSelected(elem) { if (elem.classList.contains("blbselected")) { elem.classList.remove("blbselected"); } else { elem.classList.add("blbselected"); } } function formatDate(date) { let year = date.getUTCFullYear(); let month = "0" + (1 + date.getUTCMonth()); let day = "0" + date.getUTCDate(); let hours = "0" + date.getUTCHours(); let minutes = "0" + date.getUTCMinutes(); let seconds = "0" + date.getUTCSeconds(); let datePart = year + "-" + month.substr(-2) + "-" + day.substr(-2); let timePart = hours.substr(-2) + ":" + minutes.substr(-2) + ":" + seconds.substr(-2); return datePart + " " + timePart; } function calcAdjustedETA() { let sum = 0; for (let i = 0; i < recentBlocks.length - 1; i++) { let cur = recentBlocks[i]; let prev = recentBlocks[i + 1]; let diff = cur.timestamp - prev.timestamp; if (diff < 0) { continue; } sum += diff; } let avgBlockTimeSec = sum / (recentBlocks.length - 1); let secsLeft = (calcNextHalvingHeight() - getCurTipBlock().height) * avgBlockTimeSec; let now = new Date(); let tzOff = now.getTimezoneOffset(); let adjEta = new Date(now.getTime() + (secsLeft * 1000) - (tzOff * 60)); return formatDate(adjEta); } function updateFancyPrediction() { let adjEta = calcAdjustedETA(); let elem = document.getElementById("fancyprediction"); elem.innerHTML = "very roughly " + adjEta + " local time"; } function isHalvingHeight(height) { return height % HALVING_PERIOD == 0; } function makeBlockElem(block, prevBlock) { let entry = document.createElement("div"); let innerElem = document.createElement("div"); innerElem.classList.add("entryinner"); entry.appendChild(innerElem); if (block.height % 2 == 0) { innerElem.classList.add("blocklisteven"); } else { innerElem.classList.add("blocklistodd"); } /* ===== Top row data ===== */ let topElem = document.createElement("div"); topElem.classList.add("entrytop"); innerElem.appendChild(topElem); // Make the icon. let iconCtr = document.createElement("div"); iconCtr.classList.add("blbiconctr"); topElem.appendChild(iconCtr); let iconElem = document.createElement("img"); iconElem.classList.add("blbicon"); iconCtr.appendChild(iconElem); if (isHalvingHeight(block.height)) { iconElem.src = DIAMOND_BLOCK; entry.classList.add("halvingblockentry"); } else { iconElem.src = GOLD_BLOCK; } // Element for all the data. let dataElem = document.createElement("div"); dataElem.classList.add("blbdata"); topElem.appendChild(dataElem); // Make the height element. let heightElem = document.createElement("span"); heightElem.classList.add("blbheight"); heightElem.innerHTML = block.height.toString(); dataElem.appendChild(heightElem); // this makes it look a lot nicer dataElem.append("—"); // Make the timestamp element. let timestampElem = document.createElement("span"); let timestampDate = new Date(block.timestamp * 1000); timestampElem.innerHTML = formatDate(timestampDate) + " UTC"; dataElem.appendChild(timestampElem); // this makes it look a lot nicer dataElem.append("—"); // Make the reward breakdown element. let rewardElem = document.createElement("span"); rewardElem.innerHTML = "Reward: ? sat"; dataElem.appendChild(rewardElem); doGetBlockCoinBaseTx(block.id, function(resp) { let reward = resp.vout[0].value; let rewardStr = calcRewardBreakdown(block.height, reward); rewardElem.innerHTML = rewardStr; }); /* ==== Detail row data ===== */ // Details let detailElem = document.createElement("div"); detailElem.classList.add("entrydetail"); // Make the hash element. let hashElem = document.createElement("div"); hashElem.innerHTML = block.id; hashElem.classList.add("blbhash"); detailElem.appendChild(hashElem); // Weight and height let sizeElem = document.createElement("div"); detailElem.appendChild(sizeElem); let blockKB = Math.floor(block.size / 1024); let blockkWU = Math.floor(block.weight / 1024); sizeElem.innerHTML = "Size/Weight: " + blockKB + " KiB / " + blockkWU + " kSipa"; // Number of transactions let txsElem = document.createElement("div"); detailElem.appendChild(txsElem); txsElem.innerHTML = "Tx count: " + block.tx_count; // Make the duration element. if (prevBlock != null) { let timeElem = document.createElement("div"); let blocktime = block.timestamp - prevBlock.timestamp; if (blocktime < 0) { blocktime = 0; // ehhhhhhhh } timeElem.innerHTML = "Since last block: " + blocktime.toString() + " sec"; detailElem.appendChild(timeElem); } else { let oopsElem = document.createElement("div"); oopsElem.innerHTML = "
(I don't feel like making the request to get the previous block to find the blocktime)"; detailElem.appendChild(oopsElem); } let linksElem = document.createElement("div"); linksElem.classList.add("detaillinks"); detailElem.appendChild(linksElem); // Link to the block on Blockstream's explorer. let blockstreamLink = document.createElement("a"); blockstreamLink.href = "https://blockstream.info/block/"+ block.id; let blockstreamLogo = document.createElement("img"); blockstreamLogo.src = "https://blockstream.info/img/icons/blockstream-logo.png"; blockstreamLink.appendChild(blockstreamLogo); let viewonElem = document.createElement("span"); viewonElem.innerHTML = "View on blockstream.info"; blockstreamLink.appendChild(viewonElem); linksElem.appendChild(blockstreamLink); // Thing to close the details. //let closeElem = document.createElement("div"); //closeElem.classList.add("closebtn"); //closeElem.innerHTML = "close"; linksElem.onclick = function() { console.log("foobar"); // This is such a hack but it works. We have to do this because the // onclick for the entry we're removing it from here *still gets run* // when we pick up the click here. This just defers removing it until // we're about to render the next frame, so we're sure we remove it. window.requestAnimationFrame(function() { entry.classList.add("blbselected"); }); }; //detailElem.appendChild(closeElem); /* ===== Finalize ===== */ // Put it together. innerElem.appendChild(detailElem); entry.appendChild(innerElem); entry.classList.add("blocklistentry"); entry.onclick = function() { toggleElemSelected(entry); }; return entry; } var blocksLeftElem = null; var timeLeftElem = null; var halfTimeElem = null; const TARGET_BLOCKTIME = 10 * 60; var blocktimeAvgSecs = TARGET_BLOCKTIME; function updateRemainingCount(block) { if (blocksLeftElem == null) { blocksLeftElem = document.getElementById("blocksleft"); } if (timeLeftElem == null) { timeLeftElem = document.getElementById("timeleft"); } if (halfTimeElem == null) { halfTimeElem = document.getElementById("halftime"); } let nextHalvingHeight = calcNextHalvingHeight(); let blocksLeft = nextHalvingHeight - getCurTipBlock().height; blocksLeftElem.innerHTML = blocksLeft.toString() if (blocksLeft <= 1) { timeLeftElem.innerHTML = "Halving imminent!"; // Hide the expected time. let timectr = document.getElementById("halftimectr"); timectr.style.display = "none"; } else { let nowUnix = Date.now() / 1000; // wtf??? let sinceLastBlock = nowUnix - block.timestamp; let secsLeft = ((blocksLeft * blocktimeAvgSecs) - sinceLastBlock)|0; let secsPart = secsLeft % 60; let minLeft = (secsLeft - secsPart) / 60; let minPart = minLeft % 60; let hrsLeft = (minLeft - minPart) / 60; let acc = secsPart + "s"; if (minLeft > 0) { acc = (minPart + "m ") + acc; } if (hrsLeft > 0) { acc = (hrsLeft + "h ") + acc; } timeLeftElem.innerHTML = "roughly " + acc + " remaining"; // Reward halving date. let localNow = new Date(); let tzOff = (new Date()).getTimezoneOffset() * 60 * 1000; let etaDate = new Date(localNow.getTime() + (secsLeft * 1000) - tzOff); let formattedEtaDate = formatDate(etaDate); halfTimeElem.innerHTML = "around " + formattedEtaDate + " local time"; } } function addNewBlock(block) { let prev = getCurTipBlock(); recentBlocks = [block].concat(recentBlocks); //updateBlocktimeAvg(); let elem = makeBlockElem(block, prev); elem.classList.add("newblock"); setTimeout(function() { elem.classList.remove("newblock"); }, 2000); let blocklist = document.getElementById("blocklist"); blocklist.prepend(elem); } function populateRecentBlocks(blocks) { let blocklist = document.getElementById("blocklist"); let loadingElem = document.getElementById("blocklistloading"); updateRemainingCount(getCurTipBlock()); for (let i = 0; i < blocks.length; i++) { let block = blocks[i]; let prevBlock = null; if (i < blocks.length - 1) { prevBlock = blocks[i + 1]; } let elem = makeBlockElem(block, prevBlock); if (i == 0) { // Adding the new block so we give it the class to make it colored, // but set a timer so it goes away eventually. elem.classList.add("newblock"); setTimeout(function() { elem.classList.remove("newblock"); }, 2000); } blocklist.appendChild(elem); } loadingElem.style.display = "none"; //updateRemainingCount(curTipBlock); } function getPollDelayAtHeight(height) { let blocksUntilHalving = calcNextHalvingHeight() - height; if (blocksUntilHalving <= 2) { return 2500; } else { return 10000; } } function checkNewBlocks() { console.log("Polling for new blocks..."); doGetRecentBlocks(function(blocks) { if (blocks == null) { alert("Error polling for new bocks, please refresh the page."); return; } let curTip = getCurTipBlock(); // Find the index of the newest known block. let knownHeight = curTip.height; let newestKnownIndex = -1; for (let i = 0; i < blocks.length; i++) { if (blocks[i].height == knownHeight) { newestKnownIndex = i; break; } } // If there's no new blocks, exit. if (newestKnownIndex == 0) { console.log("No new blocks found."); } else if (newestKnownIndex == -1) { console.log("wtf"); } // If there are new blocks, add them. for (let i = newestKnownIndex - 1; i >= 0; i--) { let newBlock = blocks[i]; console.log("Adding new block at height " + newBlock.height + " (" + newBlock.id + ")"); addNewBlock(newBlock); } // Set up to call this again. setTimeout(checkNewBlocks, getPollDelayAtHeight(recentBlocks[0].height)); }); } function init() { console.log("Hello!"); doGetRecentBlocks(function(blocks) { if (blocks == null) { alert("Error loading blocks, please refresh the page."); return; } recentBlocks = blocks; //updateBlocktimeAvg(); populateRecentBlocks(blocks); setTimeout(checkNewBlocks, 5000); setInterval(function() { updateRemainingCount(recentBlocks[0]); }, 1000); }); }