From ae632abed58f30a9d066e003d933786a01af6766 Mon Sep 17 00:00:00 2001 From: Dan Janosik Date: Tue, 17 Mar 2020 16:41:43 -0400 Subject: [PATCH] More data on block pages and rpc api versioning: * New data in "Summary" on Block pages (supported for bitcoind v0.17.0+) * Fee percentiles * Min / Max fees * Input / Output counts * Outputs total value * UTXO count change * Min / Max tx sizes * Start of RPC API versioning support --- CHANGELOG.md | 9 + app.js | 56 +++- app/api/coreApi.js | 9 +- app/api/rpcApi.js | 16 +- npm-shrinkwrap.json | 28 +- package.json | 1 + roadmap.md | 6 + routes/baseActionsRouter.js | 80 +++++- views/includes/block-content.pug | 420 ++++++++++++++++++----------- views/includes/debug-overrides.pug | 5 +- 10 files changed, 457 insertions(+), 173 deletions(-) create mode 100644 roadmap.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 653c093..3a84667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,20 @@ * Add total fees display * Demote display of "block size" value to hover * Show weight in kWu instead of Wu +* New data in "Summary" on Block pages (supported for bitcoind v0.17.0+) + * Fee percentiles + * Min / Max fees + * Input / Output counts + * Outputs total value + * UTXO count change + * Min / Max tx sizes * New tool `/mining-summary` for viewing summarized mining data from recent blocks * Zero-indexing for tx inputs/outputs (#173) * Labels for transaction output types * Configurable UI "sub-header" links +* Start of RPC API versioning support * Tweaked styling +* Remove "Bitcoin Explorer" H1 from homepage (it's redundant) * Updated miner configs #### v1.1.9 diff --git a/app.js b/app.js index 378d46f..c1e1be2 100755 --- a/app.js +++ b/app.js @@ -19,6 +19,7 @@ var debug = require("debug"); debug.enable(process.env.DEBUG || "btcexp:app,btcexp:error"); var debugLog = debug("btcexp:app"); +var debugErrorLog = debug("btcexp:error"); var debugPerfLog = debug("btcexp:actionPerformace"); var express = require('express'); @@ -203,7 +204,48 @@ function onRpcConnectionVerified(getnetworkinfo, getblockchaininfo) { global.getnetworkinfo = getnetworkinfo; - debugLog(`RPC Connected: version=${getnetworkinfo.version} (${getnetworkinfo.subversion}), protocolversion=${getnetworkinfo.protocolversion}, chain=${getblockchaininfo.chain}, services=${services}`); + var bitcoinCoreVersionRegex = /^.*\/Satoshi\:(.*)\/.*$/; + + var match = bitcoinCoreVersionRegex.exec(getnetworkinfo.subversion); + if (match) { + global.btcNodeVersion = match[1]; + + var semver4PartRegex = /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/; + + var semver4PartMatch = semver4PartRegex.exec(global.btcNodeVersion); + if (semver4PartMatch) { + var p0 = semver4PartMatch[1]; + var p1 = semver4PartMatch[2]; + var p2 = semver4PartMatch[3]; + var p3 = semver4PartMatch[4]; + + // drop last segment, which usually indicates a bug fix release which is (hopefully) irrelevant for RPC API versioning concerns + global.btcNodeSemver = `${p0}.${p1}.${p2}`; + + } else { + var semver3PartRegex = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/; + + var semver3PartMatch = semver3PartRegex.exec(global.btcNodeVersion); + if (semver3PartMatch) { + var p0 = semver3PartMatch[1]; + var p1 = semver3PartMatch[2]; + var p2 = semver3PartMatch[3]; + + global.btcNodeSemver = `${p0}.${p1}.${p2}`; + + } else { + // short-circuit: force all RPC calls to pass their version checks - this will likely lead to errors / instability / unexpected results + global.btcNodeSemver = "1000.1000.0" + } + } + } else { + // short-circuit: force all RPC calls to pass their version checks - this will likely lead to errors / instability / unexpected results + global.btcNodeSemver = "1000.1000.0" + + debugErrorLog(`Unable to parse node version string: ${getnetworkinfo.subversion} - RPC versioning will likely be unreliable. Is your node a version of Bitcoin Core?`); + } + + debugLog(`RPC Connected: version=${getnetworkinfo.version} subversion=${getnetworkinfo.subversion}, parsedVersion(used for RPC versioning)=${global.btcNodeSemver}, protocolversion=${getnetworkinfo.protocolversion}, chain=${getblockchaininfo.chain}, services=${services}`); // load historical/fun items for this chain loadHistoricalDataForChain(global.activeBlockchain); @@ -408,6 +450,18 @@ app.use(function(req, res, next) { } } + // blockPage.showTechSummary + if (!req.session.blockPageShowTechSummary) { + var cookieValue = req.cookies['user-setting-blockPageShowTechSummary']; + + if (cookieValue) { + req.session.blockPageShowTechSummary = cookieValue; + + } else { + req.session.blockPageShowTechSummary = "true"; + } + } + // homepage banner if (!req.session.hideHomepageBanner) { var cookieValue = req.cookies['user-setting-hideHomepageBanner']; diff --git a/app/api/coreApi.js b/app/api/coreApi.js index 5a74f5e..9472bb9 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -179,6 +179,12 @@ function getNetworkHashrate(blockCount) { }); } +function getBlockStats(hash) { + return tryCacheThenRpcApi(miscCache, "getBlockStats-" + hash, 1000 * 60 * 1000, function() { + return rpcApi.getBlockStats(hash); + }); +} + function getUtxoSetSummary() { return tryCacheThenRpcApi(miscCache, "getUtxoSetSummary", 15 * 60 * 1000, rpcApi.getUtxoSetSummary); } @@ -992,5 +998,6 @@ module.exports = { getSmartFeeEstimates: getSmartFeeEstimates, getSmartFeeEstimate: getSmartFeeEstimate, getUtxoSetSummary: getUtxoSetSummary, - getNetworkHashrate: getNetworkHashrate + getNetworkHashrate: getNetworkHashrate, + getBlockStats: getBlockStats, }; \ No newline at end of file diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index 787e0dd..41fb3d0 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -3,6 +3,7 @@ var debug = require('debug'); var debugLog = debug("btcexp:rpc"); var async = require("async"); +var semver = require("semver"); var utils = require("../utils.js"); var config = require("../config.js"); @@ -66,7 +67,13 @@ function getNetworkHashrate(blockCount=144) { } function getBlockStats(hash) { - return getRpcDataWithParams({method:"getblockstats", parameters:[hash]}); + if (semver.gte(global.btcNodeSemver, "0.17.0")) { + return getRpcDataWithParams({method:"getblockstats", parameters:[hash]}); + + } else { + // unsupported + return nullPromise(); + } } function getUtxoSetSummary() { @@ -341,6 +348,12 @@ function getRpcDataWithParams(request) { }); } +function nullPromise() { + return new Promise(function(resolve, reject) { + resolve(null); + }); +} + module.exports = { getBlockchainInfo: getBlockchainInfo, @@ -364,4 +377,5 @@ module.exports = { getSmartFeeEstimate: getSmartFeeEstimate, getUtxoSetSummary: getUtxoSetSummary, getNetworkHashrate: getNetworkHashrate, + getBlockStats: getBlockStats }; \ No newline at end of file diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index fb24b4c..d23a805 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -304,6 +304,13 @@ "request": "^2.53.0", "semver": "^5.1.0", "standard-error": "^1.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "bitcoin-ops": { @@ -1885,6 +1892,13 @@ "is-builtin-module": "^1.0.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "npm-run-all": { @@ -1915,6 +1929,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "load-json-file": { @@ -2501,9 +2523,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==" }, "send": { "version": "0.17.1", diff --git a/package.json b/package.json index 71b93fd..a5d31d2 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "qrcode": "^1.4.4", "redis": "^2.8.0", "request": "^2.88.0", + "semver": "^7.1.3", "serve-favicon": "^2.5.0", "simple-git": "^1.129.0" }, diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..f202206 --- /dev/null +++ b/roadmap.md @@ -0,0 +1,6 @@ +* Tool for analyzing block details + * fee graphs + * value graphs +* Homepage: + * Summary table item for 24hr volume (pulled via blockstats.total_output) +* New page for 24hr volume for last ~30days \ No newline at end of file diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index 55a007f..be5bf41 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -525,15 +525,49 @@ router.get("/block-height/:blockHeight", function(req, res, next) { coreApi.getBlockByHeight(blockHeight).then(function(result) { res.locals.result.getblockbyheight = result; - coreApi.getBlockByHashWithTransactions(result.hash, limit, offset).then(function(result) { - res.locals.result.getblock = result.getblock; - res.locals.result.transactions = result.transactions; - res.locals.result.txInputsByTransaction = result.txInputsByTransaction; + var promises = []; + + promises.push(new Promise(function(resolve, reject) { + coreApi.getBlockByHashWithTransactions(result.hash, limit, offset).then(function(result) { + res.locals.result.getblock = result.getblock; + res.locals.result.transactions = result.transactions; + res.locals.result.txInputsByTransaction = result.txInputsByTransaction; + + resolve(); + }); + })); + + promises.push(new Promise(function(resolve, reject) { + coreApi.getBlockStats(result.hash).then(function(result) { + res.locals.result.blockstats = result; + + resolve(); + + }).catch(function(err) { + res.locals.userMessage = "Error getting block stats"; + + reject(err); + }); + })); + + Promise.all(promises).then(function() { + res.render("block"); + + next(); + + }).catch(function(err) { + res.locals.pageErrors.push(utils.logError("3249y2ewgfee", err)); res.render("block"); next(); }); + }).catch(function(err) { + res.locals.pageErrors.push(utils.logError("389wer07eghdd", err)); + + res.render("block"); + + next(); }); }); @@ -565,18 +599,44 @@ router.get("/block/:blockHash", function(req, res, next) { res.locals.limit = limit; res.locals.offset = offset; res.locals.paginationBaseUrl = "/block/" + blockHash; - - coreApi.getBlockByHashWithTransactions(blockHash, limit, offset).then(function(result) { - res.locals.result.getblock = result.getblock; - res.locals.result.transactions = result.transactions; - res.locals.result.txInputsByTransaction = result.txInputsByTransaction; + var promises = []; + + promises.push(new Promise(function(resolve, reject) { + coreApi.getBlockByHashWithTransactions(blockHash, limit, offset).then(function(result) { + res.locals.result.getblock = result.getblock; + res.locals.result.transactions = result.transactions; + res.locals.result.txInputsByTransaction = result.txInputsByTransaction; + + resolve(); + + }).catch(function(err) { + res.locals.userMessage = "Error getting block data"; + + reject(err); + }); + })); + + promises.push(new Promise(function(resolve, reject) { + coreApi.getBlockStats(blockHash).then(function(result) { + res.locals.result.blockstats = result; + + resolve(); + + }).catch(function(err) { + res.locals.userMessage = "Error getting block stats"; + + reject(err); + }); + })); + + Promise.all(promises).then(function() { res.render("block"); next(); }).catch(function(err) { - res.locals.userMessage = "Error getting block data"; + res.locals.pageErrors.push(utils.logError("3217wfeghy9sdgs", err)); res.render("block"); diff --git a/views/includes/block-content.pug b/views/includes/block-content.pug index 17ef98e..c73cf08 100644 --- a/views/includes/block-content.pug +++ b/views/includes/block-content.pug @@ -1,3 +1,20 @@ +div.mb-2 + if (result.getblock.previousblockhash) + a.btn.btn-sm.btn-primary.mb-1(href=("/block/" + result.getblock.previousblockhash)) « Prev Block: + span.text-monospace ##{(result.getblock.height - 1).toLocaleString()} + + else if (result.getblock.hash == genesisBlockHash) + span.btn.btn-sm.btn-secondary.disabled.mb-1 « Prev Block: none (genesis block) + + span.mx-2 + + if (result.getblock.nextblockhash) + a.btn.btn-sm.btn-primary.mb-1(href=("/block/" + result.getblock.nextblockhash)) Next Block: + span.text-monospace ##{(result.getblock.height + 1).toLocaleString()} » + else + a.btn.btn-sm.btn-secondary.disabled.mb-1 Next Block: N/A + small (latest block) + ul.nav.nav-tabs.mb-3 li.nav-item a.nav-link.active(data-toggle="tab", href="#tab-details", role="tab") Details @@ -32,170 +49,252 @@ div.tab-content span a(href=sbInfo.referenceUrl) Read more - div.card.shadow-sm.mb-3 - div.card-body.px-2.px-md-3 - h3.h6 Summary - hr + div.row + - var sumTableLabelClass = (result.blockstats != null ? "summary-split-table-label" : "summary-table-label"); + - var sumTableValueClass = (result.blockstats != null ? "summary-split-table-content" : "summary-table-content"); - div.row - div(class="col-md-6") - div.row - div.summary-split-table-label Previous Block - div.summary-split-table-content.text-monospace - if (result.getblock.previousblockhash) - a(class="word-wrap", href=("/block/" + result.getblock.previousblockhash)) #{result.getblock.previousblockhash} - br - span (##{(result.getblock.height - 1).toLocaleString()}) - - else if (result.getblock.hash == genesisBlockHash) - span None (genesis block) - - div.row - div.summary-split-table-label Date - div.summary-split-table-content.text-monospace - - var timestampHuman = result.getblock.time; - include ./timestamp-human.pug - small.ml-1 utc - - br - - - var timeAgoTime = result.getblock.time; - span.text-muted ( - include ./time-ago-text.pug - span ) - - div.row - div.summary-split-table-label Transactions - div.summary-split-table-content.text-monospace #{result.getblock.tx.length.toLocaleString()} - - div.row - div.summary-split-table-label Total Fees - div.summary-split-table-content.text-monospace - - var currencyValue = new Decimal(result.getblock.totalFees); - include ./value-display.pug - - if (result.getblock.totalFees > 0) + div.mb-3(class=(result.blockstats != null ? "col-md-6 pr-0" : "col-md-12")) + div.card.shadow-sm(style="height: 100%;") + div.card-body.px-2.px-md-3 + h3.h6.mb-0 Summary + hr + + div.clearfix div.row - div.summary-split-table-label Average Fee - div.summary-split-table-content.text-monospace - - var currencyValue = new Decimal(result.getblock.totalFees).dividedBy(result.getblock.strippedsize).times(coinConfig.baseCurrencyUnit.multiplier).toDecimalPlaces(1); - span #{currencyValue} sat/vB - br - span.text-muted ( - - var currencyValue = new Decimal(result.getblock.totalFees).dividedBy(result.getblock.tx.length); - include ./value-display.pug + div(class=sumTableLabelClass) Date + div.text-monospace(class=sumTableValueClass) + - var timestampHuman = result.getblock.time; + include ./timestamp-human.pug + small.ml-1 utc + + - var timeAgoTime = result.getblock.time; + span.text-muted.ml-2 ( + include ./time-ago-text.pug span ) - - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height, global.activeBlockchain); - - var coinbaseTxTotalOutputValue = new Decimal(0); - each vout in result.getblock.coinbaseTx.vout - - coinbaseTxTotalOutputValue = coinbaseTxTotalOutputValue.plus(new Decimal(vout.value)); + if (result.blockstats) + div.row + div(class=sumTableLabelClass) + span.border-dotted(title="Total value of all transaction outputs (excluding the coinbase transaction)", data-toggle="tooltip") + span Total Output + div.text-monospace(class=sumTableValueClass) + - var currencyValue = new Decimal(result.blockstats.total_out).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + include ./value-display.pug + + if (result.blockstats) + div.row + div(class=sumTableLabelClass) + span.border-dotted(title="Total number of inputs and outputs", data-toggle="tooltip") + span In # + span.text-muted.font-weight-normal.mx-1 / + span Out # + div.text-monospace(class=sumTableValueClass) + span #{result.blockstats.ins.toLocaleString()} + span.text-muted.mx-1 / + span #{result.blockstats.outs.toLocaleString()} + + if (result.blockstats) + div.row + div(class=sumTableLabelClass) + span.border-dotted(title="Change in number (and size) of UTXO set.", data-toggle="tooltip") + span UTXO Δ + div.text-monospace(class=sumTableValueClass) + - var sizePlusMinus = (result.blockstats.utxo_size_inc > 0) ? "+" : "-"; + - var sizeDeltaData = utils.formatLargeNumber(Math.abs(result.blockstats.utxo_size_inc), 1); + - var plusMinus = (result.blockstats.utxo_increase > 0) ? "+" : ""; + span #{plusMinus}#{result.blockstats.utxo_increase.toLocaleString()} + span.text-muted (#{sizePlusMinus}#{sizeDeltaData[0]} #{sizeDeltaData[1].abbreviation}B) + + if (result.blockstats) + div.row + div(class=sumTableLabelClass) Min - Max Tx Size + div.text-monospace(class=sumTableValueClass) + span #{result.blockstats.mintxsize.toLocaleString()} + span.text-muted.mx-1 - + span #{result.blockstats.maxtxsize.toLocaleString()} B + + - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height, global.activeBlockchain); + - var coinbaseTxTotalOutputValue = new Decimal(0); + each vout in result.getblock.coinbaseTx.vout + - coinbaseTxTotalOutputValue = coinbaseTxTotalOutputValue.plus(new Decimal(vout.value)); + + if (parseFloat(coinbaseTxTotalOutputValue) < blockRewardMax) + div.row + div(class=sumTableLabelClass) + span.border-dotted(data-toggle="tooltip" title="The miner of this block failed to collect this value. As a result, it is lost.") Fees Destroyed + div.text-monospace.text-danger(class=sumTableValueClass) + - var currencyValue = new Decimal(blockRewardMax).minus(coinbaseTxTotalOutputValue); + include ./value-display.pug + + if (result.getblock.weight) + div.row + div(class=sumTableLabelClass) Weight + div.text-monospace(class=sumTableValueClass) + span(style="") #{result.getblock.weight.toLocaleString()} wu + + span.text-muted (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full) + - if (parseFloat(coinbaseTxTotalOutputValue) < blockRewardMax) div.row - div.summary-split-table-label - span.border-dotted(data-toggle="tooltip" title="The miner of this block failed to collect this value. As a result, it is lost.") Fees Destroyed - div(class="summary-split-table-content text-monospace text-danger") - - var currencyValue = new Decimal(blockRewardMax).minus(coinbaseTxTotalOutputValue); - include ./value-display.pug + div(class=sumTableLabelClass) Size + div.text-monospace(class=sumTableValueClass) #{result.getblock.size.toLocaleString()} bytes - if (result.getblock.weight) div.row - div.summary-split-table-label Weight - div.summary-split-table-content.text-monospace - span(style="") #{result.getblock.weight.toLocaleString()} wu + div(class=sumTableLabelClass) Confirmations + div.text-monospace(class=sumTableValueClass) + if (result.getblock.confirmations < 6) + span(class="font-weight-bold text-warning") #{result.getblock.confirmations.toLocaleString()} + a(data-toggle="tooltip", title="Fewer than 6 confirmations is generally considered 'unsettled' for high-value transactions. The applicability of this guidance may vary.") + i(class="fas fa-unlock-alt") + else + span(class="font-weight-bold text-success font-weight-bold") #{result.getblock.confirmations.toLocaleString()} + a(data-toggle="tooltip", title="6 confirmations is generally considered 'settled'. High-value transactions may require more; low-value transactions may require less.") + i.fas.fa-lock + + if (result.blockstats) + div.col-md-6.mb-3 + div.card.shadow-sm(style="height: 100%;") + div.card-body.px-2.px-md-3 + h3.h6.mb-0 Fees Summary + hr + + div.clearfix + div.row + div.summary-split-table-label Total + div.summary-split-table-content.text-monospace + - var currencyValue = new Decimal(result.getblock.totalFees); + include ./value-display.pug + + if (result.blockstats) + div.row + div.summary-split-table-label Percentiles + br + small.font-weight-normal (sat/vB) + div.summary-split-table-content.text-monospace + div.clearfix + each item, itemIndex in [10, 25, 50, 75, 90] + div.float-left.mr-3 + span.font-weight-bold #{item}% + br + span #{JSON.stringify(result.blockstats.feerate_percentiles[itemIndex])} + + if (result.getblock.totalFees > 0) + div.row + div.summary-split-table-label Average Rate + div.summary-split-table-content.text-monospace + - var currencyValue = new Decimal(result.getblock.totalFees).dividedBy(result.getblock.strippedsize).times(coinConfig.baseCurrencyUnit.multiplier).toDecimalPlaces(1); + span #{currencyValue} sat/vB + br + span.text-muted ( + - var currencyValue = new Decimal(result.getblock.totalFees).dividedBy(result.getblock.tx.length); + include ./value-display.pug + span ) + + if (result.blockstats) + div.row + div.summary-split-table-label Median Rate + div.summary-split-table-content.text-monospace + - var currencyValue = new Decimal(result.blockstats.medianfee).dividedBy(1000).toDecimalPlaces(1); + span #{currencyValue} sat/vB + + if (result.blockstats) + div.row + div.summary-split-table-label Min, Max Rate + div.summary-split-table-content.text-monospace + - var currencyValue = new Decimal(result.blockstats.minfeerate).toDecimalPlaces(1); + span #{currencyValue} + + span.text-muted.mx-1 - - span.text-muted (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full) + - var currencyValue = new Decimal(result.blockstats.maxfeerate).toDecimalPlaces(1); + span #{currencyValue} - - var fullPercent = new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(0); + small.ml-1 sat/vB + if (result.blockstats) div.row - div(class="col-md-10 col-lg-8 col-12") - div(class="progress my-1 mr-2", style="height: 4px;") - div(class="progress-bar", role="progressbar", style=("width: " + fullPercent + "%;"), aria-valuenow=parseInt(100 * result.getblock.weight / coinConfig.maxBlockWeight), aria-valuemin="0" ,aria-valuemax="100") + div.summary-split-table-label + span.border-dotted(title="These are the min and max fees for individual transactions included in this block.", data-toggle="tooltip") Min, Max Total + div.summary-split-table-content.text-monospace + - var currencyValue = new Decimal(result.blockstats.minfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + include ./value-display.pug + + br + + - var currencyValue = new Decimal(result.blockstats.maxfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + include ./value-display.pug + + div.card.shadow-sm.mb-3 + div.card-body.px-2.px-md-3 + div.clearfix + div.float-left.mr-2 + h3.h6.mb-0 Technical Details + + div.float-right + if (!session.blockPageShowTechSummary || session.blockPageShowTechSummary == "true") + a(href="/changeSetting?name=blockPageShowTechSummary&value=false") hide + else + a(href="/changeSetting?name=blockPageShowTechSummary&value=true") show + + div(id="tech-details-wrapper", class=(session.blockPageShowTechSummary == "true" ? "" : "d-none")) + hr + + div.row + div.col-md-6 + div.row + div.summary-split-table-label Difficulty + div.summary-split-table-content.text-monospace + - var difficultyData = utils.formatLargeNumber(result.getblock.difficulty, 3); + span(title=parseFloat(result.getblock.difficulty).toLocaleString(), data-toggle="tooltip") + span #{difficultyData[0]} + span x 10 + sup #{difficultyData[1].exponent} - div.row - div.summary-split-table-label Size - div.summary-split-table-content.text-monospace #{result.getblock.size.toLocaleString()} bytes - - div.row - div.summary-split-table-label Confirmations - div.summary-split-table-content.text-monospace - if (result.getblock.confirmations < 6) - span(class="font-weight-bold text-warning") #{result.getblock.confirmations.toLocaleString()} - a(data-toggle="tooltip", title="Fewer than 6 confirmations is generally considered 'unsettled' for high-value transactions. The applicability of this guidance may vary.") - i(class="fas fa-unlock-alt") - else - span(class="font-weight-bold text-success font-weight-bold") #{result.getblock.confirmations.toLocaleString()} - a(data-toggle="tooltip", title="6 confirmations is generally considered 'settled'. High-value transactions may require more; low-value transactions may require less.") - i(class="fas fa-lock") - - - div(class="col-md-6") - div.row - div.summary-split-table-label Next Block - div.summary-split-table-content.text-monospace - if (result.getblock.nextblockhash) - a(href=("/block/" + result.getblock.nextblockhash)) #{result.getblock.nextblockhash} - br - span (##{(result.getblock.height + 1).toLocaleString()}) - else - span None (latest block) - - div.row - div.summary-split-table-label Difficulty - div.summary-split-table-content.text-monospace - - var difficultyData = utils.formatLargeNumber(result.getblock.difficulty, 3); - - span(title=parseFloat(result.getblock.difficulty).toLocaleString(), data-toggle="tooltip") - span #{difficultyData[0]} - span x 10 - sup #{difficultyData[1].exponent} - - div.row - div.summary-split-table-label Version - div.summary-split-table-content.text-monospace 0x#{result.getblock.versionHex} - span.text-muted (decimal: #{result.getblock.version}) - - div.row - div.summary-split-table-label Nonce - div.summary-split-table-content.text-monospace #{result.getblock.nonce} - - div.row - div.summary-split-table-label Bits - div.summary-split-table-content.text-monospace #{result.getblock.bits} - - div.row - div.summary-split-table-label Merkle Root - div.summary-split-table-content.text-monospace #{result.getblock.merkleroot} - - div.row - div.summary-split-table-label Chainwork - div.summary-split-table-content.text-monospace - - var chainworkData = utils.formatLargeNumber(parseInt("0x" + result.getblock.chainwork), 2); - - span #{chainworkData[0]} - span x 10 - sup #{chainworkData[1].exponent} - span hashes - - span.text-muted (#{result.getblock.chainwork.replace(/^0+/, '')}) + div.row + div.summary-split-table-label Version + div.summary-split-table-content.text-monospace 0x#{result.getblock.versionHex} + span.text-muted (decimal: #{result.getblock.version}) - if (result.getblock.miner) div.row - div.summary-split-table-label Miner - div.summary-split-table-content.text-monospace.mb-0 - if (result.getblock.miner) - if (result.getblock.miner.identifiedBy) - small.data-tag.bg-primary(data-toggle="tooltip", title=("Identified by: " + result.getblock.miner.identifiedBy)) #{result.getblock.miner.name} + div.summary-split-table-label Nonce + div.summary-split-table-content.text-monospace #{result.getblock.nonce} + div.row + div.summary-split-table-label Bits + div.summary-split-table-content.text-monospace #{result.getblock.bits} + + div.row + div.summary-split-table-label Merkle Root + div.summary-split-table-content.text-monospace #{result.getblock.merkleroot} + + div.row + div.summary-split-table-label Chainwork + div.summary-split-table-content.text-monospace + - var chainworkData = utils.formatLargeNumber(parseInt("0x" + result.getblock.chainwork), 2); + + span #{chainworkData[0]} + span x 10 + sup #{chainworkData[1].exponent} + span hashes + + span.text-muted (#{result.getblock.chainwork.replace(/^0+/, '')}) + + if (result.getblock.miner) + div.row + div.summary-split-table-label Miner + div.summary-split-table-content.text-monospace.mb-0 + if (result.getblock.miner) + if (result.getblock.miner.identifiedBy) + small.data-tag.bg-primary(data-toggle="tooltip", title=("Identified by: " + result.getblock.miner.identifiedBy)) #{result.getblock.miner.name} + + else + small.data-tag.bg-primary #{result.getblock.miner.name} else - small.data-tag.bg-primary #{result.getblock.miner.name} - else - span ? - span(data-toggle="tooltip", title="Unable to identify miner") - i(class="fas fa-info-circle") + span ? + span(data-toggle="tooltip", title="Unable to identify miner") + i.fas.fa-info-circle + div.card.shadow-sm.mb-3 @@ -230,7 +329,7 @@ div.tab-content div each tx, txIndex in result.transactions //pre - // code(class="json bg-light") #{JSON.stringify(tx, null, 4)} + // code.json.bg-light #{JSON.stringify(tx, null, 4)} div.card.shadow-sm(class=(" " + ((txIndex < (result.transactions.length - 1) || txCount > limit) ? "mb-3" : ""))) div.card-header.text-monospace if (tx && tx.txid) @@ -245,7 +344,7 @@ div.tab-content div.card-body.px-2.px-md-3 //pre - // code(class="json bg-light") #{JSON.stringify(result.txInputsByTransaction[tx.txid], null, 4)} + // code.json.bg-light #{JSON.stringify(result.txInputsByTransaction[tx.txid], null, 4)} if (true) - var txInputs = result.txInputsByTransaction[tx.txid]; - var blockHeight = result.getblock.height; @@ -253,7 +352,7 @@ div.tab-content include ./transaction-io-details.pug //pre - // code(class="json bg-light") #{JSON.stringify(tx, null, 4)} + // code.json.bg-light #{JSON.stringify(tx, null, 4)} if (!crawlerBot && txCount > limit) - var pageNumber = offset / limit + 1; @@ -273,18 +372,27 @@ div.tab-content - var blockDetails = JSON.parse(JSON.stringify(result.getblock)); - blockDetails.tx = "See 'Transaction IDs'"; - ul(class='nav nav-pills mb-3') + ul.nav.nav-pills.mb-3 li.nav-item - a(data-toggle="tab", href="#tab-json-block-summary", class="nav-link active", role="tab") Block Summary + a.nav-link.active(data-toggle="tab", href="#tab-json-block-summary", role="tab") Block Summary li.nav-item - a(data-toggle="tab", href="#tab-json-tx-ids", class="nav-link", role="tab") Transaction IDs + a.nav-link(data-toggle="tab", href="#tab-json-tx-ids", role="tab") Transaction IDs + + if (result.blockstats) + li.nav-item + a.nav-link(data-toggle="tab", href="#tab-json-blockstats", role="tab") Block Stats div.tab-content - div(id="tab-json-block-summary", class="tab-pane active", role="tabpanel") + div.tab-pane.active(id="tab-json-block-summary", role="tabpanel") pre - code(class="json bg-light") #{JSON.stringify(blockDetails, null, 4)} + code.json.bg-light #{JSON.stringify(blockDetails, null, 4)} - div(id="tab-json-tx-ids", class="tab-pane", role="tabpanel") + div.tab-pane(id="tab-json-tx-ids", role="tabpanel") pre - code(class="json bg-light") #{JSON.stringify(result.getblock.tx, null, 4)} + code.json.bg-light #{JSON.stringify(result.getblock.tx, null, 4)} + + if (result.blockstats) + div.tab-pane(id="tab-json-blockstats", role="tabpanel") + pre + code.json.bg-light #{JSON.stringify(result.blockstats, null, 4)} diff --git a/views/includes/debug-overrides.pug b/views/includes/debug-overrides.pug index 2a463bc..e8bee85 100644 --- a/views/includes/debug-overrides.pug +++ b/views/includes/debug-overrides.pug @@ -3,4 +3,7 @@ // debug as if we're in performance protection mode (which means we don't calculate UTXO set details) //- utxoSetSummary = null; -//- utxoSetSummaryPending = false; \ No newline at end of file +//- utxoSetSummaryPending = false; + +// debug as if we don't have result.blockstats (applies to block pages when node version < 0.17.0) +//- result.blockstats = null; \ No newline at end of file