123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- /*
- * 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 = "<br/>(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);
- });
- }
|