Simple block halving countdown website
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. /*
  2. * SPDX-License-Identifier: AGPL-3.0-or-later
  3. */
  4. const API_PREFIX = "https://blockstream.info/api";
  5. const IRON_BLOCK = "https://gamepedia.cursecdn.com/minecraft_gamepedia/7/7e/Block_of_Iron_JE4_BE3.png?version=692673bafa1e94785ab6012d7a3c8dc4";
  6. const GOLD_BLOCK = "https://gamepedia.cursecdn.com/minecraft_gamepedia/archive/7/72/20190429060628%21Block_of_Gold_JE6_BE3.png?version=448d791eb688910a0cc215181312b715";
  7. const DIAMOND_BLOCK = "https://gamepedia.cursecdn.com/minecraft_gamepedia/archive/6/6b/20190502005227%21Block_of_Diamond_JE6_BE3.png?version=721888d830050c06303696cf79695eb9";
  8. const HALVING_PERIOD = 210000;
  9. var recentBlocks = null;
  10. function getCurTipBlock() {
  11. return recentBlocks[0];
  12. }
  13. function doApiReq(endpoint, cb) {
  14. let xhr = new XMLHttpRequest();
  15. xhr.timeout = 5000;
  16. xhr.onreadystatechange = function() {
  17. if (xhr.readyState == 4) {
  18. if (xhr.status == 200) {
  19. let json = JSON.parse(xhr.responseText);
  20. cb(json);
  21. } else {
  22. // TODO Make this better?
  23. cb(null);
  24. }
  25. }
  26. };
  27. xhr.open("GET", API_PREFIX + endpoint);
  28. xhr.send()
  29. }
  30. function doGetRecentBlocks(cb) {
  31. doApiReq("/blocks/tip", cb);
  32. }
  33. function doGetBlockCoinBaseTx(blockhash, cb) {
  34. let reqSuffix = "/block/" + blockhash + "/txs";
  35. let internalCb = function(resp) {
  36. if (resp != null) {
  37. cb(resp[0]);
  38. } else {
  39. cb(null);
  40. }
  41. };
  42. doApiReq(reqSuffix, function(resp) {
  43. if (resp != null) {
  44. internalCb(resp);
  45. } else {
  46. // Sometimes the data hasn't been fully processed yet by the time we
  47. // make the call so wait a bit and then try again.
  48. setTimeout(function() {
  49. doApiReq(reqSuffix, internalCb);
  50. }, 1000)
  51. }
  52. });
  53. }
  54. function calcRenderedSatQty(sats, decimals) {
  55. let subBtc = sats % 100000000;
  56. let btc = (sats - subBtc) / 100000000;
  57. let maskModulus = 10 ** (8 - decimals);
  58. let subDecimal = (subBtc - (subBtc % maskModulus)) / maskModulus;
  59. let decimal = "0".repeat(decimals) + subDecimal.toString();
  60. if (decimal != 0) {
  61. return btc.toString() + "." + decimal.substr(decimals * -1);
  62. } else {
  63. return btc.toString();
  64. }
  65. }
  66. function calcNextHalvingHeight() {
  67. let height = recentBlocks[0].height;
  68. let epoch = Math.floor(height / HALVING_PERIOD);
  69. return (epoch + 1) * HALVING_PERIOD;
  70. }
  71. function calcRewardAtHeight(height) {
  72. let epoch = Math.floor(height / HALVING_PERIOD);
  73. return 5000000000 / (2 ** epoch);
  74. }
  75. function calcRewardBreakdown(height, sats) {
  76. let subsidy = calcRewardAtHeight(height);
  77. let fees = sats - subsidy;
  78. let rSubsidy = calcRenderedSatQty(subsidy, 2);
  79. let rFees = calcRenderedSatQty(fees, 3);
  80. return rSubsidy + " subsidy + " + rFees + " fees"
  81. }
  82. function toggleElemSelected(elem) {
  83. if (elem.classList.contains("blbselected")) {
  84. elem.classList.remove("blbselected");
  85. } else {
  86. elem.classList.add("blbselected");
  87. }
  88. }
  89. function formatDate(date) {
  90. let year = date.getUTCFullYear();
  91. let month = "0" + (1 + date.getUTCMonth());
  92. let day = "0" + date.getUTCDate();
  93. let hours = "0" + date.getUTCHours();
  94. let minutes = "0" + date.getUTCMinutes();
  95. let seconds = "0" + date.getUTCSeconds();
  96. let datePart = year + "-" + month.substr(-2) + "-" + day.substr(-2);
  97. let timePart = hours.substr(-2) + ":" + minutes.substr(-2) + ":" + seconds.substr(-2);
  98. return datePart + " " + timePart;
  99. }
  100. function calcAdjustedETA() {
  101. let sum = 0;
  102. for (let i = 0; i < recentBlocks.length - 1; i++) {
  103. let cur = recentBlocks[i];
  104. let prev = recentBlocks[i + 1];
  105. let diff = cur.timestamp - prev.timestamp;
  106. if (diff < 0) {
  107. continue;
  108. }
  109. sum += diff;
  110. }
  111. let avgBlockTimeSec = sum / (recentBlocks.length - 1);
  112. let secsLeft = (calcNextHalvingHeight() - getCurTipBlock().height) * avgBlockTimeSec;
  113. let now = new Date();
  114. let tzOff = now.getTimezoneOffset();
  115. let adjEta = new Date(now.getTime() + (secsLeft * 1000) - (tzOff * 60));
  116. return formatDate(adjEta);
  117. }
  118. function updateFancyPrediction() {
  119. let adjEta = calcAdjustedETA();
  120. let elem = document.getElementById("fancyprediction");
  121. elem.innerHTML = "very roughly " + adjEta + " local time";
  122. }
  123. function isHalvingHeight(height) {
  124. return height % HALVING_PERIOD == 0;
  125. }
  126. function makeBlockElem(block, prevBlock) {
  127. let entry = document.createElement("div");
  128. let innerElem = document.createElement("div");
  129. innerElem.classList.add("entryinner");
  130. entry.appendChild(innerElem);
  131. if (block.height % 2 == 0) {
  132. innerElem.classList.add("blocklisteven");
  133. } else {
  134. innerElem.classList.add("blocklistodd");
  135. }
  136. /* ===== Top row data ===== */
  137. let topElem = document.createElement("div");
  138. topElem.classList.add("entrytop");
  139. innerElem.appendChild(topElem);
  140. // Make the icon.
  141. let iconCtr = document.createElement("div");
  142. iconCtr.classList.add("blbiconctr");
  143. topElem.appendChild(iconCtr);
  144. let iconElem = document.createElement("img");
  145. iconElem.classList.add("blbicon");
  146. iconCtr.appendChild(iconElem);
  147. if (isHalvingHeight(block.height)) {
  148. iconElem.src = DIAMOND_BLOCK;
  149. entry.classList.add("halvingblockentry");
  150. } else {
  151. iconElem.src = GOLD_BLOCK;
  152. }
  153. // Element for all the data.
  154. let dataElem = document.createElement("div");
  155. dataElem.classList.add("blbdata");
  156. topElem.appendChild(dataElem);
  157. // Make the height element.
  158. let heightElem = document.createElement("span");
  159. heightElem.classList.add("blbheight");
  160. heightElem.innerHTML = block.height.toString();
  161. dataElem.appendChild(heightElem);
  162. // this makes it look a lot nicer
  163. dataElem.append("—");
  164. // Make the timestamp element.
  165. let timestampElem = document.createElement("span");
  166. let timestampDate = new Date(block.timestamp * 1000);
  167. timestampElem.innerHTML = formatDate(timestampDate) + " UTC";
  168. dataElem.appendChild(timestampElem);
  169. // this makes it look a lot nicer
  170. dataElem.append("—");
  171. // Make the reward breakdown element.
  172. let rewardElem = document.createElement("span");
  173. rewardElem.innerHTML = "Reward: ? sat";
  174. dataElem.appendChild(rewardElem);
  175. doGetBlockCoinBaseTx(block.id, function(resp) {
  176. let reward = resp.vout[0].value;
  177. let rewardStr = calcRewardBreakdown(block.height, reward);
  178. rewardElem.innerHTML = rewardStr;
  179. });
  180. /* ==== Detail row data ===== */
  181. // Details
  182. let detailElem = document.createElement("div");
  183. detailElem.classList.add("entrydetail");
  184. // Make the hash element.
  185. let hashElem = document.createElement("div");
  186. hashElem.innerHTML = block.id;
  187. hashElem.classList.add("blbhash");
  188. detailElem.appendChild(hashElem);
  189. // Weight and height
  190. let sizeElem = document.createElement("div");
  191. detailElem.appendChild(sizeElem);
  192. let blockKB = Math.floor(block.size / 1024);
  193. let blockkWU = Math.floor(block.weight / 1024);
  194. sizeElem.innerHTML = "Size/Weight: " + blockKB + " KiB / " + blockkWU + " kSipa";
  195. // Number of transactions
  196. let txsElem = document.createElement("div");
  197. detailElem.appendChild(txsElem);
  198. txsElem.innerHTML = "Tx count: " + block.tx_count;
  199. // Make the duration element.
  200. if (prevBlock != null) {
  201. let timeElem = document.createElement("div");
  202. let blocktime = block.timestamp - prevBlock.timestamp;
  203. if (blocktime < 0) {
  204. blocktime = 0; // ehhhhhhhh
  205. }
  206. timeElem.innerHTML = "Since last block: " + blocktime.toString() + " sec";
  207. detailElem.appendChild(timeElem);
  208. } else {
  209. let oopsElem = document.createElement("div");
  210. oopsElem.innerHTML = "<br/>(I don't feel like making the request to get the previous block to find the blocktime)";
  211. detailElem.appendChild(oopsElem);
  212. }
  213. let linksElem = document.createElement("div");
  214. linksElem.classList.add("detaillinks");
  215. detailElem.appendChild(linksElem);
  216. // Link to the block on Blockstream's explorer.
  217. let blockstreamLink = document.createElement("a");
  218. blockstreamLink.href = "https://blockstream.info/block/"+ block.id;
  219. let blockstreamLogo = document.createElement("img");
  220. blockstreamLogo.src = "https://blockstream.info/img/icons/blockstream-logo.png";
  221. blockstreamLink.appendChild(blockstreamLogo);
  222. let viewonElem = document.createElement("span");
  223. viewonElem.innerHTML = "View on blockstream.info";
  224. blockstreamLink.appendChild(viewonElem);
  225. linksElem.appendChild(blockstreamLink);
  226. // Thing to close the details.
  227. //let closeElem = document.createElement("div");
  228. //closeElem.classList.add("closebtn");
  229. //closeElem.innerHTML = "close";
  230. linksElem.onclick = function() {
  231. console.log("foobar");
  232. // This is such a hack but it works. We have to do this because the
  233. // onclick for the entry we're removing it from here *still gets run*
  234. // when we pick up the click here. This just defers removing it until
  235. // we're about to render the next frame, so we're sure we remove it.
  236. window.requestAnimationFrame(function() {
  237. entry.classList.add("blbselected");
  238. });
  239. };
  240. //detailElem.appendChild(closeElem);
  241. /* ===== Finalize ===== */
  242. // Put it together.
  243. innerElem.appendChild(detailElem);
  244. entry.appendChild(innerElem);
  245. entry.classList.add("blocklistentry");
  246. entry.onclick = function() {
  247. toggleElemSelected(entry);
  248. };
  249. return entry;
  250. }
  251. var blocksLeftElem = null;
  252. var timeLeftElem = null;
  253. var halfTimeElem = null;
  254. const TARGET_BLOCKTIME = 10 * 60;
  255. var blocktimeAvgSecs = TARGET_BLOCKTIME;
  256. function updateRemainingCount(block) {
  257. if (blocksLeftElem == null) {
  258. blocksLeftElem = document.getElementById("blocksleft");
  259. }
  260. if (timeLeftElem == null) {
  261. timeLeftElem = document.getElementById("timeleft");
  262. }
  263. if (halfTimeElem == null) {
  264. halfTimeElem = document.getElementById("halftime");
  265. }
  266. let nextHalvingHeight = calcNextHalvingHeight();
  267. let blocksLeft = nextHalvingHeight - getCurTipBlock().height;
  268. blocksLeftElem.innerHTML = blocksLeft.toString()
  269. if (blocksLeft <= 1) {
  270. timeLeftElem.innerHTML = "Halving imminent!";
  271. // Hide the expected time.
  272. let timectr = document.getElementById("halftimectr");
  273. timectr.style.display = "none";
  274. } else {
  275. let nowUnix = Date.now() / 1000; // wtf???
  276. let sinceLastBlock = nowUnix - block.timestamp;
  277. let secsLeft = ((blocksLeft * blocktimeAvgSecs) - sinceLastBlock)|0;
  278. let secsPart = secsLeft % 60;
  279. let minLeft = (secsLeft - secsPart) / 60;
  280. let minPart = minLeft % 60;
  281. let hrsLeft = (minLeft - minPart) / 60;
  282. let acc = secsPart + "s";
  283. if (minLeft > 0) {
  284. acc = (minPart + "m ") + acc;
  285. }
  286. if (hrsLeft > 0) {
  287. acc = (hrsLeft + "h ") + acc;
  288. }
  289. timeLeftElem.innerHTML = "roughly " + acc + " remaining";
  290. // Reward halving date.
  291. let localNow = new Date();
  292. let tzOff = (new Date()).getTimezoneOffset() * 60 * 1000;
  293. let etaDate = new Date(localNow.getTime() + (secsLeft * 1000) - tzOff);
  294. let formattedEtaDate = formatDate(etaDate);
  295. halfTimeElem.innerHTML = "around " + formattedEtaDate + " local time";
  296. }
  297. }
  298. function addNewBlock(block) {
  299. let prev = getCurTipBlock();
  300. recentBlocks = [block].concat(recentBlocks);
  301. //updateBlocktimeAvg();
  302. let elem = makeBlockElem(block, prev);
  303. elem.classList.add("newblock");
  304. setTimeout(function() {
  305. elem.classList.remove("newblock");
  306. }, 2000);
  307. let blocklist = document.getElementById("blocklist");
  308. blocklist.prepend(elem);
  309. }
  310. function populateRecentBlocks(blocks) {
  311. let blocklist = document.getElementById("blocklist");
  312. let loadingElem = document.getElementById("blocklistloading");
  313. updateRemainingCount(getCurTipBlock());
  314. for (let i = 0; i < blocks.length; i++) {
  315. let block = blocks[i];
  316. let prevBlock = null;
  317. if (i < blocks.length - 1) {
  318. prevBlock = blocks[i + 1];
  319. }
  320. let elem = makeBlockElem(block, prevBlock);
  321. if (i == 0) {
  322. // Adding the new block so we give it the class to make it colored,
  323. // but set a timer so it goes away eventually.
  324. elem.classList.add("newblock");
  325. setTimeout(function() {
  326. elem.classList.remove("newblock");
  327. }, 2000);
  328. }
  329. blocklist.appendChild(elem);
  330. }
  331. loadingElem.style.display = "none";
  332. //updateRemainingCount(curTipBlock);
  333. }
  334. function getPollDelayAtHeight(height) {
  335. let blocksUntilHalving = calcNextHalvingHeight() - height;
  336. if (blocksUntilHalving <= 2) {
  337. return 2500;
  338. } else {
  339. return 10000;
  340. }
  341. }
  342. function checkNewBlocks() {
  343. console.log("Polling for new blocks...");
  344. doGetRecentBlocks(function(blocks) {
  345. if (blocks == null) {
  346. alert("Error polling for new bocks, please refresh the page.");
  347. return;
  348. }
  349. let curTip = getCurTipBlock();
  350. // Find the index of the newest known block.
  351. let knownHeight = curTip.height;
  352. let newestKnownIndex = -1;
  353. for (let i = 0; i < blocks.length; i++) {
  354. if (blocks[i].height == knownHeight) {
  355. newestKnownIndex = i;
  356. break;
  357. }
  358. }
  359. // If there's no new blocks, exit.
  360. if (newestKnownIndex == 0) {
  361. console.log("No new blocks found.");
  362. } else if (newestKnownIndex == -1) {
  363. console.log("wtf");
  364. }
  365. // If there are new blocks, add them.
  366. for (let i = newestKnownIndex - 1; i >= 0; i--) {
  367. let newBlock = blocks[i];
  368. console.log("Adding new block at height " + newBlock.height + " (" + newBlock.id + ")");
  369. addNewBlock(newBlock);
  370. }
  371. // Set up to call this again.
  372. setTimeout(checkNewBlocks, getPollDelayAtHeight(recentBlocks[0].height));
  373. });
  374. }
  375. function init() {
  376. console.log("Hello!");
  377. doGetRecentBlocks(function(blocks) {
  378. if (blocks == null) {
  379. alert("Error loading blocks, please refresh the page.");
  380. return;
  381. }
  382. recentBlocks = blocks;
  383. //updateBlocktimeAvg();
  384. populateRecentBlocks(blocks);
  385. setTimeout(checkNewBlocks, 5000);
  386. setInterval(function() {
  387. updateRemainingCount(recentBlocks[0]);
  388. }, 1000);
  389. });
  390. }