Simple block halving countdown website
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

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