diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a84667..eec12aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,14 @@ * UTXO set size * Total coins in circulation * Market cap + * 24-hour network volume (sum of outputs) * Tweaks to data in blocks lists: * Simpler timestamp formatting for easy reading * Include "Time-to-Mine" (TTM) for each block (with green/red highlighting for "fast"/"slow" (<5min/>15min) blocks) * Display average fee in sat/vB - * Add total fees display - * Demote display of "block size" value to hover - * Show weight in kWu instead of Wu + * Add total fees + * Add output volume (if `getblockstats` rpc call is supported, i.e. 0.17.0+) + * Show %Full instead of weight/size * New data in "Summary" on Block pages (supported for bitcoind v0.17.0+) * Fee percentiles * Min / Max fees @@ -31,7 +32,9 @@ * Configurable UI "sub-header" links * Start of RPC API versioning support * Tweaked styling -* Remove "Bitcoin Explorer" H1 from homepage (it's redundant) +* Homepage tweaks + * Remove "Bitcoin Explorer" H1 (it's redundant) + * Hide the "Date" (timestamp) column for recent blocks (the Age+TTM is more valuable) * Updated miner configs #### v1.1.9 diff --git a/app.js b/app.js index c1e1be2..ad2ab10 100755 --- a/app.js +++ b/app.js @@ -258,6 +258,15 @@ function onRpcConnectionVerified(getnetworkinfo, getblockchaininfo) { // refresh exchange rate periodically setInterval(utils.refreshExchangeRates, 1800000); } + + // UTXO pull + refreshUtxoSetSummary(); + setInterval(refreshUtxoSetSummary, 30 * 60 * 1000); + + + // 1d / 7d volume + refreshNetworkVolumes(); + setInterval(refreshNetworkVolumes, 30 * 60 * 1000); } function refreshUtxoSetSummary() { @@ -282,6 +291,67 @@ function refreshUtxoSetSummary() { }); } +function refreshNetworkVolumes() { + var cutoff1d = new Date().getTime() - (60 * 60 * 24 * 1000); + var cutoff7d = new Date().getTime() - (60 * 60 * 24 * 7 * 1000); + + coreApi.getBlockchainInfo().then(function(result) { + var promises = []; + + var blocksPerDay = 144 + 20; // 20 block padding + + for (var i = 0; i < (blocksPerDay * 1); i++) { + promises.push(coreApi.getBlockStatsByHeight(result.blocks - i)); + } + + var startBlock = result.blocks; + + var endBlock1d = result.blocks; + var endBlock7d = result.blocks; + + var endBlockTime1d = 0; + var endBlockTime7d = 0; + + Promise.all(promises).then(function(results) { + var volume1d = new Decimal(0); + var volume7d = new Decimal(0); + + var blocks1d = 0; + var blocks7d = 0; + + if (results && results.length > 0 && results[0] != null) { + for (var i = 0; i < results.length; i++) { + if (results[i].time * 1000 > cutoff1d) { + volume1d = volume1d.plus(new Decimal(results[i].total_out)); + blocks1d++; + + endBlock1d = results[i].height; + endBlockTime1d = results[i].time; + } + + if (results[i].time * 1000 > cutoff7d) { + volume7d = volume7d.plus(new Decimal(results[i].total_out)); + blocks7d++; + + endBlock7d = results[i].height; + endBlockTime7d = results[i].time; + } + } + + volume1d = volume1d.dividedBy(coinConfig.baseCurrencyUnit.multiplier); + volume7d = volume7d.dividedBy(coinConfig.baseCurrencyUnit.multiplier); + + global.networkVolume = {d1:{amt:volume1d, blocks:blocks1d, startBlock:startBlock, endBlock:endBlock1d, startTime:results[0].time, endTime:endBlockTime1d}}; + + debugLog(`Network volume: ${JSON.stringify(global.networkVolume)}`); + + } else { + debugLog("Unable to load network volume, likely due to bitcoind version older than 0.17.0 (the first version to support getblockstats)."); + } + }); + }); +} + app.onStartup = function() { global.config = config; @@ -381,10 +451,6 @@ app.continueStartup = function() { utils.logMemoryUsage(); setInterval(utils.logMemoryUsage, 5000); - - - refreshUtxoSetSummary(); - setInterval(refreshUtxoSetSummary, 30 * 60 * 1000); }; app.use(function(req, res, next) { @@ -416,6 +482,7 @@ app.use(function(req, res, next) { res.locals.exchangeRates = global.exchangeRates; res.locals.utxoSetSummary = global.utxoSetSummary; res.locals.utxoSetSummaryPending = global.utxoSetSummaryPending; + res.locals.networkVolume = global.networkVolume; res.locals.host = req.session.host; res.locals.port = req.session.port; diff --git a/app/api/coreApi.js b/app/api/coreApi.js index 9472bb9..8b5a9c7 100644 --- a/app/api/coreApi.js +++ b/app/api/coreApi.js @@ -185,6 +185,12 @@ function getBlockStats(hash) { }); } +function getBlockStatsByHeight(height) { + return tryCacheThenRpcApi(miscCache, "getBlockStatsByHeight-" + height, 1000 * 60 * 1000, function() { + return rpcApi.getBlockStatsByHeight(height); + }); +} + function getUtxoSetSummary() { return tryCacheThenRpcApi(miscCache, "getUtxoSetSummary", 15 * 60 * 1000, rpcApi.getUtxoSetSummary); } @@ -626,6 +632,22 @@ function getBlocksByHeight(blockHeights) { }); } +function getBlocksStatsByHeight(blockHeights) { + return new Promise(function(resolve, reject) { + var promises = []; + for (var i = 0; i < blockHeights.length; i++) { + promises.push(getBlockStatsByHeight(blockHeights[i])); + } + + Promise.all(promises).then(function(results) { + resolve(results); + + }).catch(function(err) { + reject(err); + }); + }); +} + function getBlockByHash(blockHash) { return tryCacheThenRpcApi(blockCache, "getBlockByHash-" + blockHash, 3600000, function() { return rpcApi.getBlockByHash(blockHash); @@ -1000,4 +1022,6 @@ module.exports = { getUtxoSetSummary: getUtxoSetSummary, getNetworkHashrate: getNetworkHashrate, getBlockStats: getBlockStats, + getBlockStatsByHeight: getBlockStatsByHeight, + getBlocksStatsByHeight: getBlocksStatsByHeight, }; \ No newline at end of file diff --git a/app/api/rpcApi.js b/app/api/rpcApi.js index 41fb3d0..0dfa6e9 100644 --- a/app/api/rpcApi.js +++ b/app/api/rpcApi.js @@ -68,8 +68,30 @@ function getNetworkHashrate(blockCount=144) { function getBlockStats(hash) { if (semver.gte(global.btcNodeSemver, "0.17.0")) { - return getRpcDataWithParams({method:"getblockstats", parameters:[hash]}); + if (hash == coinConfig.genesisBlockHashesByNetwork[global.activeBlockchain] && coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]) { + return new Promise(function(resolve, reject) { + resolve(coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]); + }); + } else { + return getRpcDataWithParams({method:"getblockstats", parameters:[hash]}); + } + } else { + // unsupported + return nullPromise(); + } +} + +function getBlockStatsByHeight(height) { + if (semver.gte(global.btcNodeSemver, "0.17.0")) { + if (height == 0 && coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]) { + return new Promise(function(resolve, reject) { + resolve(coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]); + }); + + } else { + return getRpcDataWithParams({method:"getblockstats", parameters:[height]}); + } } else { // unsupported return nullPromise(); @@ -377,5 +399,6 @@ module.exports = { getSmartFeeEstimate: getSmartFeeEstimate, getUtxoSetSummary: getUtxoSetSummary, getNetworkHashrate: getNetworkHashrate, - getBlockStats: getBlockStats + getBlockStats: getBlockStats, + getBlockStatsByHeight: getBlockStatsByHeight, }; \ No newline at end of file diff --git a/app/coins/btc.js b/app/coins/btc.js index a03d07c..e92e3d2 100644 --- a/app/coins/btc.js +++ b/app/coins/btc.js @@ -180,6 +180,45 @@ module.exports = { "blocktime": 1296688602 } }, + genesisBlockStatsByNetwork:{ + "main": { + "avgfee": 0, + "avgfeerate": 0, + "avgtxsize": 0, + "blockhash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "feerate_percentiles": [ + 0, + 0, + 0, + 0, + 0 + ], + "height": 0, + "ins": 0, + "maxfee": 0, + "maxfeerate": 0, + "maxtxsize": 0, + "medianfee": 0, + "mediantime": 1231006505, + "mediantxsize": 0, + "minfee": 0, + "minfeerate": 0, + "mintxsize": 0, + "outs": 1, + "subsidy": 5000000000, + "swtotal_size": 0, + "swtotal_weight": 0, + "swtxs": 0, + "time": 1231006505, + "total_out": 0, + "total_size": 0, + "total_weight": 0, + "totalfee": 0, + "txs": 1, + "utxo_increase": 1, + "utxo_size_inc": 117 + } + }, genesisCoinbaseOutputAddressScripthash:"8b01df4e368ea28f8dc0423bcf7a4923e3a12d307c875e47a0cfbf90b5c39161", historicalData: [ { diff --git a/app/utils.js b/app/utils.js index eed0a0b..8e4bb43 100644 --- a/app/utils.js +++ b/app/utils.js @@ -322,8 +322,8 @@ function shortenTimeDiff(str) { str = str.replace(" years", "y"); str = str.replace(" year", "y"); - str = str.replace(" months", "m"); - str = str.replace(" month", "m"); + str = str.replace(" months", "mo"); + str = str.replace(" month", "mo"); str = str.replace(" weeks", "w"); str = str.replace(" week", "w"); diff --git a/public/css/styling.css b/public/css/styling.css index bd9221d..7b8606d 100755 --- a/public/css/styling.css +++ b/public/css/styling.css @@ -219,20 +219,20 @@ strong { } .summary-table-label { - max-width: 11%; + max-width: 13%; text-align: right; } .summary-table-content { - max-width: 89%; + max-width: 87%; margin-bottom: 5px; } .summary-split-table-label { - max-width: 22%; + max-width: 24%; text-align: right; } .summary-split-table-content { - max-width: 78%; + max-width: 76%; margin-bottom: 5px; } diff --git a/public/img/block-fullness-0.png b/public/img/block-fullness-0.png new file mode 100644 index 0000000..b458aea Binary files /dev/null and b/public/img/block-fullness-0.png differ diff --git a/public/img/block-fullness-1.png b/public/img/block-fullness-1.png new file mode 100644 index 0000000..0f0940b Binary files /dev/null and b/public/img/block-fullness-1.png differ diff --git a/public/img/block-fullness-2.png b/public/img/block-fullness-2.png new file mode 100644 index 0000000..89d8868 Binary files /dev/null and b/public/img/block-fullness-2.png differ diff --git a/public/img/block-fullness-3.png b/public/img/block-fullness-3.png new file mode 100644 index 0000000..eedcd16 Binary files /dev/null and b/public/img/block-fullness-3.png differ diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index be5bf41..7bd4c21 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -43,6 +43,10 @@ router.get("/", function(req, res, next) { } res.locals.homepage = true; + + // don't need timestamp on homepage "blocks-list", this flag disables + res.locals.hideTimestampColumn = true; + // variables used by blocks-list.pug res.locals.offset = 0; @@ -71,10 +75,25 @@ router.get("/", function(req, res, next) { coreApi.getBlockchainInfo().then(function(getblockchaininfo) { res.locals.getblockchaininfo = getblockchaininfo; + var blockHeights = []; + if (getblockchaininfo.blocks) { + // +1 to page size here so we have the next block to calculate T.T.M. + for (var i = 0; i < (config.site.homepage.recentBlocksCount + 1); i++) { + blockHeights.push(getblockchaininfo.blocks - i); + } + } else if (global.activeBlockchain == "regtest") { + // hack: default regtest node returns getblockchaininfo.blocks=0, despite having a genesis block + // hack this to display the genesis block + blockHeights.push(0); + } + + // promiseResults[5] + promises.push(coreApi.getBlocksStatsByHeight(blockHeights)); + if (getblockchaininfo.chain !== 'regtest') { var targetBlocksPerDay = 24 * 60 * 60 / global.coinConfig.targetBlockTimeSeconds; - // promiseResults[4] (if not regtest) + // promiseResults[6] (if not regtest) promises.push(coreApi.getTxCountStats(targetBlocksPerDay / 4, -targetBlocksPerDay, "latest")); var chainTxStatsIntervals = [ targetBlocksPerDay, targetBlocksPerDay * 7, targetBlocksPerDay * 30, targetBlocksPerDay * 365 ] @@ -84,23 +103,12 @@ router.get("/", function(req, res, next) { .slice(0, chainTxStatsIntervals.length) .concat("All time"); + // promiseResults[7-X] (if not regtest) for (var i = 0; i < chainTxStatsIntervals.length; i++) { promises.push(coreApi.getChainTxStats(chainTxStatsIntervals[i])); } } - var blockHeights = []; - if (getblockchaininfo.blocks) { - // +1 to page size here so we have the next block to calculate T.T.M. - for (var i = 0; i < (config.site.homepage.recentBlocksCount + 1); i++) { - blockHeights.push(getblockchaininfo.blocks - i); - } - } else if (global.activeBlockchain == "regtest") { - // hack: default regtest node returns getblockchaininfo.blocks=0, despite having a genesis block - // hack this to display the genesis block - blockHeights.push(0); - } - if (getblockchaininfo.chain !== 'regtest') { promises.push(coreApi.getChainTxStats(getblockchaininfo.blocks - 1)); } @@ -113,6 +121,7 @@ router.get("/", function(req, res, next) { res.locals.miningInfo = promiseResults[1]; var rawSmartFeeEsimates = promiseResults[2]; + var smartFeeEsimates = {}; for (var i = 0; i < feeConfTargets.length; i++) { @@ -125,12 +134,25 @@ router.get("/", function(req, res, next) { res.locals.hashrate1d = promiseResults[3]; res.locals.hashrate7d = promiseResults[4]; + + var rawblockstats = promiseResults[5]; + if (rawblockstats && rawblockstats.length > 0 && rawblockstats[0] != null) { + res.locals.blockstatsByHeight = {}; + + for (var i = 0; i < rawblockstats.length; i++) { + var blockstats = rawblockstats[i]; + + res.locals.blockstatsByHeight[blockstats.height] = blockstats; + } + } + + if (getblockchaininfo.chain !== 'regtest') { - res.locals.txStats = promiseResults[5]; + res.locals.txStats = promiseResults[6]; var chainTxStats = []; for (var i = 0; i < res.locals.chainTxStatsLabels.length; i++) { - chainTxStats.push(promiseResults[i + 6]); + chainTxStats.push(promiseResults[i + 7]); } res.locals.chainTxStats = chainTxStats; @@ -355,14 +377,39 @@ router.get("/blocks", function(req, res, next) { } } } - - coreApi.getBlocksByHeight(blockHeights).then(function(blocks) { - res.locals.blocks = blocks; + + var promises = []; + + promises.push(coreApi.getBlocksByHeight(blockHeights)); + + promises.push(coreApi.getBlocksStatsByHeight(blockHeights)); + + Promise.all(promises).then(function(promiseResults) { + res.locals.blocks = promiseResults[0]; + var rawblockstats = promiseResults[1]; + + if (rawblockstats != null && rawblockstats.length > 0 && rawblockstats[0] != null) { + res.locals.blockstatsByHeight = {}; + + for (var i = 0; i < rawblockstats.length; i++) { + var blockstats = rawblockstats[i]; + + res.locals.blockstatsByHeight[blockstats.height] = blockstats; + } + } + + res.render("blocks"); + + next(); + + }).catch(function(err) { + res.locals.pageErrors.push(utils.logError("32974hrbfbvc", err)); res.render("blocks"); next(); }); + }).catch(function(err) { res.locals.userMessage = "Error: " + err; diff --git a/views/address.pug b/views/address.pug index 0891ccb..2551bb1 100644 --- a/views/address.pug +++ b/views/address.pug @@ -232,7 +232,7 @@ block content div.card-body div.clearfix div.float-left - h3.h6 + h3.h6.mb-0 if (addressDetails && addressDetails.txCount) if (addressDetails.txCount == 1) span 1 Transaction @@ -354,7 +354,7 @@ block content - var timeAgoTime = tx.time; small.text-muted ( include includes/time-ago-text.pug - span ) + span ago) else span.text-danger Unconfirmed diff --git a/views/includes/block-content.pug b/views/includes/block-content.pug index d0c8ef6..05bf448 100644 --- a/views/includes/block-content.pug +++ b/views/includes/block-content.pug @@ -10,7 +10,8 @@ div.mb-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()} » + span.text-monospace ##{(result.getblock.height + 1).toLocaleString()} + span » else a.btn.btn-sm.btn-secondary.disabled.mb-1 Next Block: N/A small (latest block) @@ -70,7 +71,7 @@ div.tab-content - var timeAgoTime = result.getblock.time; span.text-muted.ml-2 ( include ./time-ago-text.pug - span ) + span ago) if (result.blockstats) div.row @@ -78,8 +79,11 @@ div.tab-content 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.total_out) + - var currencyValue = new Decimal(result.blockstats.total_out).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + include ./value-display.pug + else + span 0 if (result.blockstats) div.row @@ -89,9 +93,17 @@ div.tab-content span.text-muted.font-weight-normal.mx-1 / span Out # div.text-monospace(class=sumTableValueClass) - span #{result.blockstats.ins.toLocaleString()} + if (result.blockstats.ins) + span #{result.blockstats.ins.toLocaleString()} + else + span 0 + span.text-muted.mx-1 / - span #{result.blockstats.outs.toLocaleString()} + + if (result.blockstats.outs) + span #{result.blockstats.outs.toLocaleString()} + else + span 1 if (result.blockstats) div.row @@ -102,16 +114,30 @@ div.tab-content - 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.utxo_increase) + span #{plusMinus}#{result.blockstats.utxo_increase.toLocaleString()} + span.text-muted (#{sizePlusMinus}#{sizeDeltaData[0]} + small #{sizeDeltaData[1].abbreviation}B + span ) + else + span 0 if (result.blockstats) div.row - div(class=sumTableLabelClass) Min - Max Tx Size + div(class=sumTableLabelClass) Min, Max Tx Size div.text-monospace(class=sumTableValueClass) - span #{result.blockstats.mintxsize.toLocaleString()} + if (result.blockstats.mintxsize) + span #{result.blockstats.mintxsize.toLocaleString()} + else + span 0 + span.text-muted.mx-1 - - span #{result.blockstats.maxtxsize.toLocaleString()} B + + if (result.blockstats.maxtxsize) + span #{result.blockstats.maxtxsize.toLocaleString()} + small B + else + span 0 - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height, global.activeBlockchain); - var coinbaseTxTotalOutputValue = new Decimal(0); @@ -121,7 +147,7 @@ div.tab-content 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 + span.border-dotted(data-toggle="tooltip" title="The miner of this block failed to collect this value. As a result, it is permanently lost.") Fees Destroyed div.text-monospace.text-danger(class=sumTableValueClass) - var currencyValue = new Decimal(blockRewardMax).minus(coinbaseTxTotalOutputValue); include ./value-display.pug @@ -130,14 +156,16 @@ div.tab-content div.row div(class=sumTableLabelClass) Weight div.text-monospace(class=sumTableValueClass) - span(style="") #{result.getblock.weight.toLocaleString()} wu + span #{result.getblock.weight.toLocaleString()} + small wu span.text-muted (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full) div.row div(class=sumTableLabelClass) Size - div.text-monospace(class=sumTableValueClass) #{result.getblock.size.toLocaleString()} bytes + div.text-monospace(class=sumTableValueClass) #{result.getblock.size.toLocaleString()} + small B div.row div(class=sumTableLabelClass) Confirmations @@ -159,70 +187,104 @@ div.tab-content 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.ins == 0 || !result.blockstats.ins) + span.text-monospace N/A (no inputs) - 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) + else div.row - div.summary-split-table-label Average Rate + div.summary-split-table-label Total 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); + if (result.blockstats.totalfee) + - var currencyValue = new Decimal(result.blockstats.totalfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); 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 - - - - var currencyValue = new Decimal(result.blockstats.maxfeerate).toDecimalPlaces(1); - span #{currencyValue} - small.ml-1 sat/vB - - if (result.blockstats) - div.row - 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 Fee - div.summary-split-table-content.text-monospace - - var currencyValue = new Decimal(result.blockstats.minfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); - include ./value-display.pug - - br + else + - var currencyValue = new Decimal(result.getblock.totalFees); + include ./value-display.pug - - var currencyValue = new Decimal(result.blockstats.maxfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); - 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 + + if (result.blockstats.feerate_percentiles) + span #{JSON.stringify(result.blockstats.feerate_percentiles[itemIndex])} + else + span - + + if (result.getblock.totalFees > 0) + div.row + div.summary-split-table-label Average Rate + div.summary-split-table-content.text-monospace + if (result.blockstats) + span #{result.blockstats.avgfeerate} + + else + - var currencyValue = new Decimal(result.getblock.totalFees).dividedBy(result.getblock.strippedsize).times(coinConfig.baseCurrencyUnit.multiplier).toDecimalPlaces(1); + span #{currencyValue} + + small.ml-1 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 + if (result.blockstats.medianfee) + - var currencyValue = new Decimal(result.blockstats.medianfee).dividedBy(1000).toDecimalPlaces(1); + span #{currencyValue} + small sat/vB + else + span 0 + + if (result.blockstats) + div.row + div.summary-split-table-label Min, Max Rate + div.summary-split-table-content.text-monospace + if (result.blockstats.minfeerate) + span #{result.blockstats.minfeerate.toLocaleString()} + else + span 0 + + span.text-muted.mx-1 - + + if (result.blockstats.maxfeerate) + span #{result.blockstats.maxfeerate.toLocaleString()} + else + span 0 + + small.ml-1 sat/vB + + if (result.blockstats) + div.row + 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 Fee + div.summary-split-table-content.text-monospace + if (result.blockstats.minfee) + - var currencyValue = new Decimal(result.blockstats.minfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + include ./value-display.pug + else + span 0 + + br + + if (result.blockstats.maxfee) + - var currencyValue = new Decimal(result.blockstats.maxfee).dividedBy(coinConfig.baseCurrencyUnit.multiplier); + include ./value-display.pug + else + span 0 div.card.shadow-sm.mb-3 div.card-body.px-2.px-md-3 @@ -276,7 +338,7 @@ div.tab-content span #{chainworkData[0]} span x 10 sup #{chainworkData[1].exponent} - span hashes + small hashes span.text-muted (#{result.getblock.chainwork.replace(/^0+/, '')}) diff --git a/views/includes/blocks-list.pug b/views/includes/blocks-list.pug index 73a79f4..87af08b 100644 --- a/views/includes/blocks-list.pug +++ b/views/includes/blocks-list.pug @@ -5,22 +5,31 @@ div.table-responsive //th th th.data-header.text-right Height - th.data-header.text-right Date - small (utc) + + if (!hideTimestampColumn) + th.data-header.text-right + span.border-dotted(title="UTC timestamp of the block.", data-toggle="tooltip") Date + th.data-header.text-right Age th.data-header.text-right span.border-dotted(title="Time To Mine - The time it took to mine this block after the previous block. 'Fast' blocks (mined in < 5min) are shown in green; 'slow' blocks (mined in > 15min) are shown in red.", data-toggle="tooltip") T.T.M. th.data-header.text-right Miner - th.data-header.text-right Transactions - th.data-header.text-right Avg Fee - small (sat/vB) - th.data-header.text-right Total Fees + th.data-header.text-right + span.border-dotted(title="The number of transactions included in each block.", data-toggle="tooltip") N(tx) + + if (blockstatsByHeight) + th.data-header.text-right + span.border-dotted(title="The total output of all transactions in each block (excluding coinbase transactions).", data-toggle="tooltip") Volume + + th.data-header.text-right + span.border-dotted(title="The average fee (sat/vB) for all block transactions.", data-toggle="tooltip") Avg Fee + th.data-header.text-right Σ Fees //th.data-header.text-right Size (kB) if (blocks && blocks.length > 0 && blocks[0].weight) - th.data-header.text-right Weight - small (kWu) + th.data-header.text-right % Full + //span.border-dotted(title="Block weight, in kWu.", data-toggle="tooltip") Weight else th.data-header.text-right Size @@ -56,16 +65,20 @@ div.table-responsive if (blockIndex < blocks.length - 1) - var timeDiff = moment.duration(moment.utc(new Date(parseInt(block.time) * 1000)).diff(moment.utc(new Date(parseInt(blocks[blockIndex + 1].time) * 1000)))); - td.data-cell.text-monospace.text-right - - var timestampHuman = block.time; - include timestamp-human.pug + if (!hideTimestampColumn) + td.data-cell.text-monospace.text-right + - var timestampHuman = block.time; + include timestamp-human.pug td.data-cell.text-monospace.text-right if (sort != "asc" && blockIndex == 0 && offset == 0 && timeAgoTime > (15 * 60 * 1000)) - span.text-danger.border-dotted(title="It's been > 15 min since this latest block.", data-toggle="tooltip") #{utils.shortenTimeDiff(timeAgo.format())} + - var timeAgoTime = block.time; + span.text-danger.border-dotted(title="It's been > 15 min since this latest block.", data-toggle="tooltip") + include ./time-ago-text.pug else - span #{utils.shortenTimeDiff(timeAgo.format())} + - var timeAgoTime = block.time; + include ./time-ago-text.pug td.data-cell.text-monospace.text-right if (timeDiff) @@ -75,7 +88,14 @@ div.table-responsive if (timeDiff > 900000) - var colorClass = "text-danger"; - span.font-weight-light(class=colorClass) #{utils.shortenTimeDiff(timeDiff.format())} + span.font-weight-light(class=colorClass) + span #{timeDiff.format()} + + if (false) + if (timeDiff.asMinutes() < 1) + span #{parseInt(timeDiff.asSeconds())}s + else + span #{parseInt(timeDiff.asMinutes())}m #{parseInt(timeDiff.asSeconds())}s else if (block.height == 0) @@ -92,6 +112,16 @@ div.table-responsive td.data-cell.text-monospace.text-right #{block.tx.length.toLocaleString()} + if (blockstatsByHeight) + td.data-cell.text-monospace.text-right + if (blockstatsByHeight[block.height]) + - var currencyValue = parseInt(new Decimal(blockstatsByHeight[block.height].total_out).dividedBy(coinConfig.baseCurrencyUnit.multiplier)); + - var currencyValueDecimals = 0; + include ./value-display.pug + + else + span 0 + td.data-cell.text-monospace.text-right - var currencyValue = new Decimal(block.totalFees).dividedBy(block.strippedsize).times(coinConfig.baseCurrencyUnit.multiplier).toDecimalPlaces(1); span #{currencyValue} @@ -115,18 +145,33 @@ div.table-responsive if (blocks && blocks.length > 0 && blocks[0].weight) td.data-cell.text-monospace.text-right - - var bWeightK = parseInt(block.weight / 1000); - - var fullPercent = new Decimal(100 * block.weight / coinConfig.maxBlockWeight).toDecimalPlaces(1); + if (true) + - var full = new Decimal(block.weight).dividedBy(coinConfig.maxBlockWeight).times(100); + - var full2 = full.toDP(2); + + span #{full2} + if (full > 90) + img(src="/img/block-fullness-3.png", style="width: 18px; height: 18px;") + else if (full > 50) + img(src="/img/block-fullness-2.png", style="width: 18px; height: 18px;") + else if (full > 25) + img(src="/img/block-fullness-1.png", style="width: 18px; height: 18px;") + else + img(src="/img/block-fullness-0.png", style="width: 18px; height: 18px;") - span #{bWeightK.toLocaleString()} - small.font-weight-light.text-muted (#{fullPercent}%) + else + - var bWeightK = parseInt(block.weight / 1000); + - var fullPercent = new Decimal(100 * block.weight / coinConfig.maxBlockWeight).toDecimalPlaces(1); - - var bSizeK = parseInt(block.size / 1000); - span.ml-1(data-toggle="tooltip", title=`Size: ${bSizeK.toLocaleString()} kB`) - i.fas.fa-ellipsis-h.text-muted + span #{bWeightK.toLocaleString()} + small.font-weight-light.text-muted (#{fullPercent}%) - div(class="progress", style="height: 4px;") - div(class="progress-bar", role="progressbar", style=("width: " + fullPercent + "%;"), aria-valuenow=parseInt(100 * block.weight / coinConfig.maxBlockWeight), aria-valuemin="0" ,aria-valuemax="100") + - var bSizeK = parseInt(block.size / 1000); + span.ml-1(data-toggle="tooltip", title=`Size: ${bSizeK.toLocaleString()} kB`) + i.fas.fa-ellipsis-h.text-muted + + div(class="progress", style="height: 4px;") + div(class="progress-bar", role="progressbar", style=("width: " + fullPercent + "%;"), aria-valuenow=parseInt(100 * block.weight / coinConfig.maxBlockWeight), aria-valuemin="0" ,aria-valuemax="100") else td.data-cell.text-monospace.text-right diff --git a/views/includes/debug-overrides.pug b/views/includes/debug-overrides.pug index e8bee85..11dc0a4 100644 --- a/views/includes/debug-overrides.pug +++ b/views/includes/debug-overrides.pug @@ -6,4 +6,7 @@ //- 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 +//- result.blockstats = null; + +// no networkVolume +//- networkVolume = null; \ No newline at end of file diff --git a/views/includes/index-network-summary.pug b/views/includes/index-network-summary.pug index 096bc64..a5bcb46 100644 --- a/views/includes/index-network-summary.pug +++ b/views/includes/index-network-summary.pug @@ -150,52 +150,65 @@ div.row.index-summary else small.text-muted.border-dotted(title=utxoCalculatingDesc, data-toggle="tooltip") calculating... - - if (exchangeRates) + if (networkVolume || exchangeRates) div(class=colClass) h5.h6 Financials table.table.table-borderless.table-sm.table-hover tbody - tr - th.px-2.px-lg-0.px-xl-2 - i.fas.fa-money-bill-wave-alt.mr-1.summary-icon - span.border-dotted(data-toggle="tooltip", title=("Exchange-rate data from: " + coinConfig.exchangeRateData.jsonUrl)) Exchange Rate - td.text-right.text-monospace - span #{utils.formatValueInActiveCurrency(1.0)} - - tr - th.px-2.px-lg-0.px-xl-2 - i.fab.fa-btc.mr-1.summary-icon - span Sats Rate - td.text-right.text-monospace - - var satsRateData = utils.satoshisPerUnitOfActiveCurrency(); - span #{satsRateData.amt} - small #{satsRateData.unit} - - if (utxoSetSummary || utxoSetSummaryPending) + if (exchangeRates) tr th.px-2.px-lg-0.px-xl-2 - i.fas.fa-globe.mr-1.summary-icon - span Market Cap + i.fas.fa-money-bill-wave-alt.mr-1.summary-icon + span.border-dotted(data-toggle="tooltip", title=("Exchange-rate data from: " + coinConfig.exchangeRateData.jsonUrl)) Exchange Rate td.text-right.text-monospace - if (utxoSetSummary) - - var activeCurrency = global.currencyFormatType.length > 0 ? global.currencyFormatType : "usd"; - - var xxx = utils.formatLargeNumber(parseFloat(utxoSetSummary.total_amount) * exchangeRates[activeCurrency], 1); + span #{utils.formatValueInActiveCurrency(1.0)} - if (activeCurrency == "eur") - span € - else - span $ + if (exchangeRates) + tr + th.px-2.px-lg-0.px-xl-2 + i.fab.fa-btc.mr-1.summary-icon + span Sats Rate + td.text-right.text-monospace + - var satsRateData = utils.satoshisPerUnitOfActiveCurrency(); + span #{satsRateData.amt} + small #{satsRateData.unit} + + if (exchangeRates) + if (utxoSetSummary || utxoSetSummaryPending) + tr + th.px-2.px-lg-0.px-xl-2 + i.fas.fa-globe.mr-1.summary-icon + span Market Cap + td.text-right.text-monospace + if (utxoSetSummary) + - var activeCurrency = global.currencyFormatType.length > 0 ? global.currencyFormatType : "usd"; + - var xxx = utils.formatLargeNumber(parseFloat(utxoSetSummary.total_amount) * exchangeRates[activeCurrency], 1); + + if (activeCurrency == "eur") + span € + else + span $ + + span #{xxx[0]} + if (xxx[1].textDesc) + span #{xxx[1].textDesc} + else + span x 10 + sup #{xxx[1].exponent} + // ["154.9",{"val":1000000000,"name":"giga","abbreviation":"G","exponent":"9"}] - span #{xxx[0]} - if (xxx[1].textDesc) - span #{xxx[1].textDesc} else - span x 10 - sup #{xxx[1].exponent} - // ["154.9",{"val":1000000000,"name":"giga","abbreviation":"G","exponent":"9"}] + small.text-muted.border-dotted(title=utxoCalculatingDesc, data-toggle="tooltip") calculating... - else - small.text-muted.border-dotted(title=utxoCalculatingDesc, data-toggle="tooltip") calculating... - \ No newline at end of file + if (networkVolume) + tr + th.px-2.px-lg-0.px-xl-2 + i.fas.fa-history.mr-1.summary-icon + span.border-dotted(title=`Total output of all transactions (excluding coinbase transactions) over the last 24 hrs (blocks: [#${networkVolume.d1.endBlock.toLocaleString()} - #${networkVolume.d1.startBlock.toLocaleString()}]).`, data-toggle="tooltip") Volume + small.ml-1 (24h) + td.text-right.text-monospace + - var currencyValue = parseInt(networkVolume.d1.amt); + - var currencyValueDecimals = 0; + include ./value-display.pug + \ No newline at end of file diff --git a/views/includes/time-ago-text.pug b/views/includes/time-ago-text.pug index 29ae5c3..7462866 100644 --- a/views/includes/time-ago-text.pug +++ b/views/includes/time-ago-text.pug @@ -1,3 +1,17 @@ - var timeAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(timeAgoTime) * 1000)))); -span #{utils.shortenTimeDiff(timeAgo.format())} ago \ No newline at end of file +if (timeAgo.asHours() < 1) + if (timeAgo.asMinutes() < 1) + span <1m + else + span #{timeAgo.minutes()}m + +else + if (timeAgo.asHours() >= 1 && timeAgo.asHours() < 24) + span #{timeAgo.hours()}h + + if (timeAgo.minutes() > 0) + span #{timeAgo.minutes()}m + + else + span #{utils.shortenTimeDiff(timeAgo.format())} \ No newline at end of file diff --git a/views/includes/timestamp-human.pug b/views/includes/timestamp-human.pug index be881db..1e64a5b 100644 --- a/views/includes/timestamp-human.pug +++ b/views/includes/timestamp-human.pug @@ -1,10 +1,14 @@ -span #{moment.utc(new Date(parseInt(timestampHuman) * 1000)).format("M/D")} - - var yearStr = moment.utc(new Date(parseInt(timestampHuman) * 1000)).format("Y"); - var nowYearStr = moment.utc(new Date()).format("Y"); -if (yearStr != nowYearStr) - span , #{yearStr} +- var dateStr = moment.utc(new Date(parseInt(timestampHuman) * 1000)).format("Y-M-D"); +- var nowDateStr = moment.utc(new Date()).format("Y-M-D"); + +if (dateStr == nowDateStr) +else + span #{moment.utc(new Date(parseInt(timestampHuman) * 1000)).format("M/D")} + if (yearStr != nowYearStr) + span , #{yearStr} span span.border-dotted(title=`${moment.utc(new Date(parseInt(timestampHuman) * 1000)).format("Y-MM-DD HH:mm:ss")}`, data-toggle="tooltip") #{moment.utc(new Date(parseInt(timestampHuman) * 1000)).format("HH:mm")} \ No newline at end of file diff --git a/views/includes/transaction-io-details.pug b/views/includes/transaction-io-details.pug index 2389dc7..5f48791 100644 --- a/views/includes/transaction-io-details.pug +++ b/views/includes/transaction-io-details.pug @@ -165,6 +165,8 @@ div.row.text-monospace span(title="Output Type: Witness, v0 Key Hash", data-toggle="tooltip") v0_p2wpkh else if (vout.scriptPubKey.type == "witness_v0_scripthash") span(title="Output Type: Witness, v0 Script Hash", data-toggle="tooltip") v0_p2wsh + else if (vout.scriptPubKey.type == "nonstandard") + span(title="Output Type: Non-Standard", data-toggle="tooltip") nonstandard else span ??? @@ -208,6 +210,8 @@ div.row.text-monospace span(title="Output Type: Witness, v0 Key Hash", data-toggle="tooltip") v0_p2wpkh else if (vout.scriptPubKey.type == "witness_v0_scripthash") span(title="Output Type: Witness, v0 Script Hash", data-toggle="tooltip") v0_p2wsh + else if (vout.scriptPubKey.type == "nonstandard") + span(title="Output Type: Non-Standard", data-toggle="tooltip") nonstandard else span ??? diff --git a/views/transaction.pug b/views/transaction.pug index 32f17dd..c2b657b 100644 --- a/views/transaction.pug +++ b/views/transaction.pug @@ -116,12 +116,10 @@ block content include includes/timestamp-human.pug small.ml-1 utc - br - - var timeAgoTime = result.getrawtransaction.time; - span.text-muted ( + span.text-muted.ml-2 ( include includes/time-ago-text.pug - span ) + span ago) div.row div.summary-table-label Version @@ -132,12 +130,15 @@ block content div.summary-table-content.text-monospace if (result.getrawtransaction.vsize != result.getrawtransaction.size) span #{result.getrawtransaction.vsize.toLocaleString()} - span.border-dotted(title="Virtual bytes", data-toggle="tooltip") vB + small vB br - span.text-muted (#{result.getrawtransaction.size.toLocaleString()} B) + span.text-muted (#{result.getrawtransaction.size.toLocaleString()} + small B + span ) else - span #{result.getrawtransaction.size.toLocaleString()} B + span #{result.getrawtransaction.size.toLocaleString()} + small B if (result.getrawtransaction.locktime > 0) div.row @@ -179,7 +180,7 @@ block content if (parseFloat(totalOutputValue) < parseFloat(blockRewardMax)) div.row div.summary-table-label - span.border-dotted(data-toggle="tooltip" title="The miner of this transaction's block failed to collect this value. As a result, it is lost.") Fees Destroyed + span.border-dotted(data-toggle="tooltip" title="The miner of this transaction's block failed to collect this value. As a result, it is permanently lost.") Fees Destroyed div.summary-table-content.text-monospace.text-danger - var currencyValue = new Decimal(blockRewardMax).minus(totalOutputValue); include includes/value-display.pug @@ -220,11 +221,17 @@ block content div.summary-table-label Fee Rate div.summary-table-content.text-monospace if (result.getrawtransaction.vsize != result.getrawtransaction.size) - span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.vsize).times(100000000))} sat/ - span.border-dotted(title="Virtual bytes" data-toggle="tooltip") vB + span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.vsize).times(100000000))} + small sat/vB br - span.text-muted (#{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.size).times(100000000))} sat/B) + span.text-muted (#{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.size).times(100000000))} + small sat/B + span ) + + else + span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.size).times(100000000))} + small sat/B if (result.getrawtransaction.vin[0].coinbase)