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.

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