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 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 isHalvingHeight(height) {
  101. return height % HALVING_PERIOD == 0;
  102. }
  103. function makeBlockElem(block, prevBlock) {
  104. let entry = document.createElement("div");
  105. let innerElem = document.createElement("div");
  106. innerElem.classList.add("entryinner");
  107. entry.appendChild(innerElem);
  108. if (block.height % 2 == 0) {
  109. innerElem.classList.add("blocklisteven");
  110. } else {
  111. innerElem.classList.add("blocklistodd");
  112. }
  113. /* ===== Top row data ===== */
  114. let topElem = document.createElement("div");
  115. topElem.classList.add("entrytop");
  116. innerElem.appendChild(topElem);
  117. // Make the icon.
  118. let iconCtr = document.createElement("div");
  119. iconCtr.classList.add("blbiconctr");
  120. topElem.appendChild(iconCtr);
  121. let iconElem = document.createElement("img");
  122. iconElem.classList.add("blbicon");
  123. iconCtr.appendChild(iconElem);
  124. if (isHalvingHeight(block.height)) {
  125. iconElem.src = DIAMOND_BLOCK;
  126. entry.classList.add("halvingblockentry");
  127. } else {
  128. iconElem.src = GOLD_BLOCK;
  129. }
  130. // Element for all the data.
  131. let dataElem = document.createElement("div");
  132. dataElem.classList.add("blbdata");
  133. topElem.appendChild(dataElem);
  134. // Make the height element.
  135. let heightElem = document.createElement("span");
  136. heightElem.classList.add("blbheight");
  137. heightElem.innerHTML = block.height.toString();
  138. dataElem.appendChild(heightElem);
  139. // this makes it look a lot nicer
  140. dataElem.append("—");
  141. // Make the timestamp element.
  142. let timestampElem = document.createElement("span");
  143. let timestampDate = new Date(block.timestamp * 1000);
  144. timestampElem.innerHTML = formatDate(timestampDate) + " UTC";
  145. dataElem.appendChild(timestampElem);
  146. // this makes it look a lot nicer
  147. dataElem.append("—");
  148. // Make the reward breakdown element.
  149. let rewardElem = document.createElement("span");
  150. rewardElem.innerHTML = "Reward: ? sat";
  151. dataElem.appendChild(rewardElem);
  152. doGetBlockCoinBaseTx(block.id, function(resp) {
  153. let reward = resp.vout[0].value;
  154. let rewardStr = calcRewardBreakdown(block.height, reward);
  155. rewardElem.innerHTML = rewardStr;
  156. });
  157. /* ==== Detail row data ===== */
  158. // Details
  159. let detailElem = document.createElement("div");
  160. detailElem.classList.add("entrydetail");
  161. // Make the hash element.
  162. let hashElem = document.createElement("div");
  163. hashElem.innerHTML = block.id;
  164. hashElem.classList.add("blbhash");
  165. detailElem.appendChild(hashElem);
  166. // Weight and height
  167. let sizeElem = document.createElement("div");
  168. detailElem.appendChild(sizeElem);
  169. let blockKB = Math.floor(block.size / 1024);
  170. let blockkWU = Math.floor(block.weight / 1024);
  171. sizeElem.innerHTML = "Size/Weight: " + blockKB + " KiB / " + blockkWU + " kSipa";
  172. // Number of transactions
  173. let txsElem = document.createElement("div");
  174. detailElem.appendChild(txsElem);
  175. txsElem.innerHTML = "Tx count: " + block.tx_count;
  176. // Make the duration element.
  177. if (prevBlock != null) {
  178. let timeElem = document.createElement("div");
  179. let blocktime = block.timestamp - prevBlock.timestamp;
  180. if (blocktime < 0) {
  181. blocktime = 0; // ehhhhhhhh
  182. }
  183. timeElem.innerHTML = "Since last block: " + blocktime.toString() + " sec";
  184. detailElem.appendChild(timeElem);
  185. } else {
  186. let oopsElem = document.createElement("div");
  187. oopsElem.innerHTML = "<br/>(I don't feel like making the request to get the previous block to find the blocktime)";
  188. detailElem.appendChild(oopsElem);
  189. }
  190. let linksElem = document.createElement("div");
  191. linksElem.classList.add("detaillinks");
  192. detailElem.appendChild(linksElem);
  193. // Link to the block on Blockstream's explorer.
  194. let blockstreamLink = document.createElement("a");
  195. blockstreamLink.href = "https://blockstream.info/block/"+ block.id;
  196. let blockstreamLogo = document.createElement("img");
  197. blockstreamLogo.src = "https://blockstream.info/img/icons/blockstream-logo.png";
  198. blockstreamLink.appendChild(blockstreamLogo);
  199. let viewonElem = document.createElement("span");
  200. viewonElem.innerHTML = "View on blockstream.info";
  201. blockstreamLink.appendChild(viewonElem);
  202. linksElem.appendChild(blockstreamLink);
  203. // Thing to close the details.
  204. //let closeElem = document.createElement("div");
  205. //closeElem.classList.add("closebtn");
  206. //closeElem.innerHTML = "close";
  207. linksElem.onclick = function() {
  208. console.log("foobar");
  209. // This is such a hack but it works. We have to do this because the
  210. // onclick for the entry we're removing it from here *still gets run*
  211. // when we pick up the click here. This just defers removing it until
  212. // we're about to render the next frame, so we're sure we remove it.
  213. window.requestAnimationFrame(function() {
  214. entry.classList.add("blbselected");
  215. });
  216. };
  217. //detailElem.appendChild(closeElem);
  218. /* ===== Finalize ===== */
  219. // Put it together.
  220. innerElem.appendChild(detailElem);
  221. entry.appendChild(innerElem);
  222. entry.classList.add("blocklistentry");
  223. entry.onclick = function() {
  224. toggleElemSelected(entry);
  225. };
  226. return entry;
  227. }
  228. var blocksLeftElem = null;
  229. var timeLeftElem = null;
  230. var halfTimeElem = null;
  231. const TARGET_BLOCKTIME = 10 * 60;
  232. var blocktimeAvgSecs = TARGET_BLOCKTIME;
  233. function updateRemainingCount(block) {
  234. if (blocksLeftElem == null) {
  235. blocksLeftElem = document.getElementById("blocksleft");
  236. }
  237. if (timeLeftElem == null) {
  238. timeLeftElem = document.getElementById("timeleft");
  239. }
  240. if (halfTimeElem == null) {
  241. halfTimeElem = document.getElementById("halftime");
  242. }
  243. let nextHalvingHeight = calcNextHalvingHeight();
  244. let blocksLeft = nextHalvingHeight - getCurTipBlock().height;
  245. blocksLeftElem.innerHTML = blocksLeft.toString()
  246. if (blocksLeft <= 1) {
  247. timeLeftElem.innerHTML = "Halving imminent!";
  248. // Hide the expected time.
  249. let timectr = document.getElementById("halftimectr");
  250. timectr.style.display = "none";
  251. } else {
  252. let nowUnix = Date.now() / 1000; // wtf???
  253. let sinceLastBlock = nowUnix - block.timestamp;
  254. let secsLeft = ((blocksLeft * blocktimeAvgSecs) - sinceLastBlock)|0;
  255. let secsPart = secsLeft % 60;
  256. let minLeft = (secsLeft - secsPart) / 60;
  257. let minPart = minLeft % 60;
  258. let hrsLeft = (minLeft - minPart) / 60;
  259. let acc = secsPart + "s";
  260. if (minLeft > 0) {
  261. acc = (minPart + "m ") + acc;
  262. }
  263. if (hrsLeft > 0) {
  264. acc = (hrsLeft + "h ") + acc;
  265. }
  266. timeLeftElem.innerHTML = "roughly " + acc + " remaining";
  267. // Reward halving date.
  268. let localNow = new Date();
  269. let tzOff = (new Date()).getTimezoneOffset() * 60 * 1000;
  270. let etaDate = new Date(localNow.getTime() + (secsLeft * 1000) - tzOff);
  271. let formattedEtaDate = formatDate(etaDate);
  272. halfTimeElem.innerHTML = "around " + formattedEtaDate + " local time";
  273. }
  274. }
  275. function addNewBlock(block) {
  276. let prev = getCurTipBlock();
  277. recentBlocks = [block].concat(recentBlocks);
  278. //updateBlocktimeAvg();
  279. let elem = makeBlockElem(block, prev);
  280. elem.classList.add("newblock");
  281. setTimeout(function() {
  282. elem.classList.remove("newblock");
  283. }, 2000);
  284. let blocklist = document.getElementById("blocklist");
  285. blocklist.prepend(elem);
  286. }
  287. function populateRecentBlocks(blocks) {
  288. let blocklist = document.getElementById("blocklist");
  289. let loadingElem = document.getElementById("blocklistloading");
  290. updateRemainingCount(getCurTipBlock());
  291. for (let i = 0; i < blocks.length; i++) {
  292. let block = blocks[i];
  293. let prevBlock = null;
  294. if (i < blocks.length - 1) {
  295. prevBlock = blocks[i + 1];
  296. }
  297. let elem = makeBlockElem(block, prevBlock);
  298. if (i == 0) {
  299. // Adding the new block so we give it the class to make it colored,
  300. // but set a timer so it goes away eventually.
  301. elem.classList.add("newblock");
  302. setTimeout(function() {
  303. elem.classList.remove("newblock");
  304. }, 2000);
  305. }
  306. blocklist.appendChild(elem);
  307. }
  308. loadingElem.style.display = "none";
  309. //updateRemainingCount(curTipBlock);
  310. }
  311. function getPollDelayAtHeight(height) {
  312. let blocksUntilHalving = HALVING_PERIOD - (height % HALVING_PERIOD);
  313. if (blocksUntilHalving <= 1) {
  314. return 1000;
  315. } else if (blocksUntilHalving == 2) {
  316. return 2500;
  317. } else {
  318. return 10000;
  319. }
  320. }
  321. function checkNewBlocks() {
  322. console.log("Polling for new blocks...");
  323. doGetRecentBlocks(function(blocks) {
  324. if (blocks == null) {
  325. alert("Error polling for new bocks, please refresh the page.");
  326. return;
  327. }
  328. let curTip = getCurTipBlock();
  329. // Find the index of the newest known block.
  330. let knownHeight = curTip.height;
  331. let newestKnownIndex = -1;
  332. for (let i = 0; i < blocks.length; i++) {
  333. if (blocks[i].height == knownHeight) {
  334. newestKnownIndex = i;
  335. break;
  336. }
  337. }
  338. // If there's no new blocks, exit.
  339. if (newestKnownIndex == 0) {
  340. console.log("No new blocks found.");
  341. } else if (newestKnownIndex == -1) {
  342. console.log("wtf");
  343. }
  344. // If there are new blocks, add them.
  345. for (let i = newestKnownIndex - 1; i >= 0; i--) {
  346. let newBlock = blocks[i];
  347. console.log("Adding new block at height " + newBlock.height + " (" + newBlock.id + ")");
  348. addNewBlock(newBlock);
  349. }
  350. // Set up to call this again.
  351. setTimeout(checkNewBlocks, getPollDelayAtHeight(recentBlocks[0].height));
  352. });
  353. }
  354. function init() {
  355. console.log("Hello!");
  356. doGetRecentBlocks(function(blocks) {
  357. if (blocks == null) {
  358. alert("Error loading blocks, please refresh the page.");
  359. return;
  360. }
  361. recentBlocks = blocks;
  362. //updateBlocktimeAvg();
  363. populateRecentBlocks(blocks);
  364. setTimeout(checkNewBlocks, 5000);
  365. setInterval(function() {
  366. updateRemainingCount(recentBlocks[0]);
  367. }, 1000);
  368. });
  369. }