Simple block halving countdown website
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

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. }