Browse Source

Moving toward release, now deemed v2.0.0:

- new /block-analysis tool for summarizing data for all tx within a given block
- include subsidy and tx fees in volume displays (on homepage network summary and on block details pages)
- misc minor styling and frontend ux tweaks
master
Dan Janosik 5 years ago
parent
commit
9fa86189da
No known key found for this signature in database GPG Key ID: C6F8CE9FFDB2CED2
  1. 3
      CHANGELOG.md
  2. 4
      app.js
  3. 95
      app/api/coreApi.js
  4. 7
      app/coins/btc.js
  5. 42
      app/utils.js
  6. 8
      public/css/styling.css
  7. 18
      public/js/site.js
  8. 3
      public/robots.txt
  9. 45
      routes/apiRouter.js
  10. 35
      routes/baseActionsRouter.js
  11. 604
      views/block-analysis.pug
  12. 3
      views/block-stats.pug
  13. 80
      views/fun.pug
  14. 17
      views/includes/block-content.pug
  15. 2
      views/includes/index-network-summary.pug
  16. 32
      views/includes/transaction-io-details.pug
  17. 16
      views/index.pug
  18. 2
      views/layout.pug
  19. 29
      views/mempool-summary.pug
  20. 14
      views/mining-summary.pug
  21. 2
      views/transaction.pug
  22. 16
      views/tx-stats.pug

3
CHANGELOG.md

@ -1,4 +1,4 @@
#### v1.2.0
#### v2.0.0
##### 2020-02-23
* Optional querying of UTXO set summary
@ -28,6 +28,7 @@
* Min / Max tx sizes
* New tool `/block-stats` for viewing summarized block data from recent blocks
* New tool `/mining-summary` for viewing summarized mining data from recent blocks
* New tool `/block-analysis` for analyzing the details of transactions in a block
* Change `/mempool-summary` to load data via ajax (UX improvement to give feedback while loading large data sets)
* Zero-indexing for tx inputs/outputs (#173)
* Labels for transaction output types

4
app.js

@ -324,6 +324,8 @@ function refreshNetworkVolumes() {
for (var i = 0; i < results.length; i++) {
if (results[i].time * 1000 > cutoff1d) {
volume1d = volume1d.plus(new Decimal(results[i].total_out));
volume1d = volume1d.plus(new Decimal(results[i].subsidy));
volume1d = volume1d.plus(new Decimal(results[i].totalfee));
blocks1d++;
endBlock1d = results[i].height;
@ -332,6 +334,8 @@ function refreshNetworkVolumes() {
if (results[i].time * 1000 > cutoff7d) {
volume7d = volume7d.plus(new Decimal(results[i].total_out));
volume7d = volume7d.plus(new Decimal(results[i].subsidy));
volume7d = volume7d.plus(new Decimal(results[i].totalfee));
blocks7d++;
endBlock7d = results[i].height;

95
app/api/coreApi.js

@ -557,7 +557,7 @@ function getMempoolStats() {
var fee = txMempoolInfo.modifiedfee;
var size = txMempoolInfo.vsize ? txMempoolInfo.vsize : txMempoolInfo.size;
var feePerByte = txMempoolInfo.modifiedfee / size;
var satoshiPerByte = feePerByte * 100000000; // TODO: magic number - replace with coinConfig.baseCurrencyUnit.multiplier
var satoshiPerByte = feePerByte * global.coinConfig.baseCurrencyUnit.multiplier;
var age = Date.now() / 1000 - txMempoolInfo.time;
var addedToBucket = false;
@ -755,6 +755,98 @@ function getRawTransactions(txids) {
});
}
function buildBlockAnalysisData(blockHeight, txids, txIndex, results, callback) {
if (txIndex >= txids.length) {
callback();
return;
}
var txid = txids[txIndex];
getRawTransactionsWithInputs([txid]).then(function(txData) {
results.push(summarizeBlockAnalysisData(blockHeight, txData.transactions[0], txData.txInputsByTransaction[txid]));
buildBlockAnalysisData(blockHeight, txids, txIndex + 1, results, callback);
});
}
function summarizeBlockAnalysisData(blockHeight, tx, inputs) {
var txSummary = {};
txSummary.txid = tx.txid;
txSummary.version = tx.version;
txSummary.size = tx.size;
if (tx.vsize) {
txSummary.vsize = tx.vsize;
}
if (tx.weight) {
txSummary.weight = tx.weight;
}
if (tx.vin[0].coinbase) {
txSummary.coinbase = true;
}
txSummary.vin = [];
txSummary.totalInput = new Decimal(0);
if (txSummary.coinbase) {
var subsidy = global.coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain);
txSummary.totalInput = txSummary.totalInput.plus(new Decimal(subsidy));
txSummary.vin.push({
coinbase: true,
value: subsidy
});
} else {
for (var i = 0; i < tx.vin.length; i++) {
var vin = tx.vin[i];
var inputVout = inputs[i].vout[vin.vout];
txSummary.totalInput = txSummary.totalInput.plus(new Decimal(inputVout.value));
txSummary.vin.push({
txid: tx.vin[i].txid,
vout: tx.vin[i].vout,
sequence: tx.vin[i].sequence,
value: inputVout.value,
type: inputVout.scriptPubKey.type,
reqSigs: inputVout.scriptPubKey.reqSigs,
addressCount: (inputVout.scriptPubKey.addresses ? inputVout.scriptPubKey.addresses.length : 0)
});
}
}
txSummary.vout = [];
txSummary.totalOutput = new Decimal(0);
for (var i = 0; i < tx.vout.length; i++) {
txSummary.totalOutput = txSummary.totalOutput.plus(new Decimal(tx.vout[i].value));
txSummary.vout.push({
value: tx.vout[i].value,
type: tx.vout[i].scriptPubKey.type,
reqSigs: tx.vout[i].scriptPubKey.reqSigs,
addressCount: tx.vout[i].scriptPubKey.addresses ? tx.vout[i].scriptPubKey.addresses.length : 0
});
}
if (txSummary.coinbase) {
txSummary.totalFee = new Decimal(0);
} else {
txSummary.totalFee = txSummary.totalInput.minus(txSummary.totalOutput);
}
return txSummary;
}
function getRawTransactionsWithInputs(txids, maxInputs=-1) {
return new Promise(function(resolve, reject) {
getRawTransactions(txids).then(function(transactions) {
@ -1030,4 +1122,5 @@ module.exports = {
getBlockStats: getBlockStats,
getBlockStatsByHeight: getBlockStatsByHeight,
getBlocksStatsByHeight: getBlocksStatsByHeight,
buildBlockAnalysisData: buildBlockAnalysisData
};

7
app/coins/btc.js

@ -401,6 +401,13 @@ module.exports = {
referenceUrl: "https://bitcoin.stackexchange.com/questions/38994/will-there-be-21-million-bitcoins-eventually/38998#38998",
alertBodyHtml: "This is one of 2 'duplicate coinbase' transactions. An early bitcoin bug (fixed by <a href='https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki'>BIP30</a>) allowed identical coinbase transactions - a newer duplicate would overwrite older copies. This transaction was the coinbase transaction for <a href='/block-height/91812'>Block #91,812</a> and, ~3 hours later, <a href='/block-height/91842'>Block #91,842</a>. The 50 BTC claimed as the coinbase for block 91,812 were also overwritten and lost."
},
{
type: "tx",
date: "2020-03-11",
chain: "main",
txid: "eeea72f5c9fe07178013eac84c3705443321d5453befd7591f52d22ac39b3963",
summary: "500+ million USD transferred for < 1 USD fee (2020 prices)."
},
// testnet

42
app/utils.js

@ -675,6 +675,44 @@ function buildQrCodeUrl(str, results) {
});
}
function outputTypeAbbreviation(outputType) {
var map = {
"pubkey": "p2pk",
"pubkeyhash": "p2pkh",
"scripthash": "p2sh",
"witness_v0_keyhash": "v0_p2wpkh",
"witness_v0_scripthash": "v0_p2wsh",
"nonstandard": "nonstandard",
"nulldata": "nulldata"
};
if (map[outputType]) {
return map[outputType];
} else {
return "???";
}
}
function outputTypeName(outputType) {
var map = {
"pubkey": "Pay to Public Key",
"pubkeyhash": "Pay to Public Key Hash",
"scripthash": "Pay to Script Hash",
"witness_v0_keyhash": "Witness, v0 Key Hash",
"witness_v0_scripthash": "Witness, v0 Script Hash",
"nonstandard": "Non-Standard",
"nulldata": "Null Data"
};
if (map[outputType]) {
return map[outputType];
} else {
return "???";
}
}
module.exports = {
reflectPromise: reflectPromise,
@ -706,5 +744,7 @@ module.exports = {
logError: logError,
buildQrCodeUrls: buildQrCodeUrls,
ellipsize: ellipsize,
shortenTimeDiff: shortenTimeDiff
shortenTimeDiff: shortenTimeDiff,
outputTypeAbbreviation: outputTypeAbbreviation,
outputTypeName: outputTypeName
};

8
public/css/styling.css

@ -54,6 +54,14 @@ code, .text-monospace {
margin-bottom: 3px;
}
.col-lg-left {
padding-right: 8px;
}
.col-lg-right {
padding-left: 8px;
}
.nav-tabs .nav-link.active {
background-color: transparent;

18
public/js/site.js

@ -0,0 +1,18 @@
function updateCurrencyValue(element, val) {
$.ajax({
url: `/snippet/formatCurrencyAmount/${val}`
}).done(function(result) {
element.html(result);
$('[data-toggle="tooltip"]').tooltip();
});
}
function updateFeeRateValue(element, val, digits) {
$.ajax({
url: `/api/utils/formatCurrencyAmountInSmallestUnits/${val},${digits}`
}).done(function(result) {
element.html(`<span>${result.val} <small>${result.currencyUnit}/vB</small></span>`);
});
}

3
public/robots.txt

@ -3,4 +3,5 @@ Disallow: /address/
Disallow: /rpc*
Disallow: /tx-stats
Disallow: /peers
Disallow: /unconfirmed-tx
Disallow: /unconfirmed-tx
Disallow: /block-analysis/

45
routes/apiRouter.js

@ -77,6 +77,49 @@ router.get("/mempool-txs/:txids", function(req, res, next) {
});
});
router.get("/raw-tx-with-inputs/:txid", function(req, res, next) {
var txid = req.params.txid;
var promises = [];
promises.push(coreApi.getRawTransactionsWithInputs([txid]));
Promise.all(promises).then(function(results) {
res.json(results);
next();
}).catch(function(err) {
res.json({success:false, error:err});
next();
});
});
router.get("/block-tx-summaries/:blockHeight/:txids", function(req, res, next) {
var blockHeight = parseInt(req.params.blockHeight);
var txids = req.params.txids.split(",");
var promises = [];
var results = [];
promises.push(new Promise(function(resolve, reject) {
coreApi.buildBlockAnalysisData(blockHeight, txids, 0, results, resolve);
}));
Promise.all(promises).then(function() {
res.json(results);
next();
}).catch(function(err) {
res.json({success:false, error:err});
next();
});
});
router.get("/utils/:func/:params", function(req, res, next) {
var func = req.params.func;
var params = req.params.params;
@ -97,8 +140,6 @@ router.get("/utils/:func/:params", function(req, res, next) {
data = utils.formatCurrencyAmountInSmallestUnits(new Decimal(parts[0]), parseInt(parts[1]));
console.log("ABC: " + JSON.stringify(data));
} else {
data = {success:false, error:`Unknown function: ${func}`};
}

35
routes/baseActionsRouter.js

@ -730,6 +730,41 @@ router.get("/block/:blockHash", function(req, res, next) {
});
});
router.get("/block-analysis/:blockHash", function(req, res, next) {
var blockHash = req.params.blockHash;
res.locals.blockHash = blockHash;
res.locals.result = {};
var txResults = [];
var promises = [];
res.locals.result = {};
coreApi.getBlockByHash(blockHash).then(function(block) {
res.locals.result.getblock = block;
res.render("block-analysis");
next();
}).catch(function(err) {
res.locals.pageErrors.push(utils.logError("943h84ehedr", err));
res.render("block-analysis");
next();
});
});
router.get("/block-analysis", function(req, res, next) {
res.render("block-analysis");
next();
});
router.get("/tx/:transactionId", function(req, res, next) {
var txid = req.params.transactionId;

604
views/block-analysis.pug

@ -0,0 +1,604 @@
extends layout
block headContent
title Block Analysis ##{result.getblock.height.toLocaleString()}, #{result.getblock.hash}
block content
h1.h3 Block Analysis
small.text-monospace(style="width: 100%;") ##{result.getblock.height.toLocaleString()}
br
small.text-monospace.word-wrap(style="width: 100%;") #{result.getblock.hash}
hr
div.mb-2
if (result.getblock)
a.btn.btn-sm.btn-primary.mb-1(href=("/block/" + result.getblock.hash)) &laquo; Back to Block Details:
span.text-monospace ##{result.getblock.height.toLocaleString()}
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.mb-3.col
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-table-label Date
div.summary-table-content.text-monospace
- var timestampHuman = result.getblock.time;
include includes/timestamp-human.pug
small.ml-1 utc
- var timeAgoTime = result.getblock.time;
span.text-muted.ml-2 (
include includes/time-ago-text.pug
span ago)
if (result.getblock.weight)
div.row
div.summary-table-label Weight
div.summary-table-content.text-monospace
span #{result.getblock.weight.toLocaleString()}
small wu
span.text-muted (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full)
else
div.row
div.summary-table-label Size
div.summary-table-content.text-monospace #{result.getblock.size.toLocaleString()}
small B
div.row
div.summary-table-label Transactions
div.summary-table-content.text-monospace #{result.getblock.tx.length.toLocaleString()}
div.row
div.summary-table-label Confirmations
div.summary-table-content.text-monospace
if (result.getblock.confirmations < 6)
span.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.font-weight-bold.text-success #{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
div#progress-wrapper
div.card.shadow-sm.mb-3
div.card-body
h4.h6 Loading transactions:
span(id="progress-text")
div.progress(id="progress-bar", style="height: 7px;")
div.progress-bar(id="data-progress", role="progressbar", aria-valuenow="0", aria-valuemin="0" ,aria-valuemax="100")
div#main-content(style="display: none;")
div.row
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Top Value Transactions
hr
div.table-responsive
table.table.table-striped.mb-0
thead
tr
th
th.data-header Transaction
th.data-header.text-right Output Value
tbody(id="tbody-top-value-tx")
tr.text-monospace.row-prototype(style="display: none;")
td
small.text-muted.data-index
td.data-tx-link
td.text-right.data-tx-value
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Top Fee Transactions
hr
div.table-responsive
table.table.table-striped.mb-0
thead
tr
th
th.data-header Transaction
th.data-header.text-right Fee
tbody(id="tbody-top-fee-tx")
tr.text-monospace.row-prototype(style="display: none;")
td
small.text-muted.data-index
td.data-tx-link
td.text-right.data-tx-value
div.col-lg-6.col-lg-left.mb-3
div.card.shadow-sm(style="height: 100%;")
div.card-body
h3.h6.mb-0 Input Types
hr
table.table.table-striped.mb-0
tbody(id="tbody-input-types")
tr.text-monospace.row-prototype(style="display: none;")
td
small.data-tag.bg-dark.data-type
td.data-count
div.col-lg-6.col-lg-right.mb-3
div.card.shadow-sm(style="height: 100%;")
div.card-body
h3.h6.mb-0 Output Types
hr
table.table.table-striped.mb-0
tbody(id="tbody-output-types")
tr.text-monospace.row-prototype(style="display: none;")
td
small.data-tag.bg-dark.data-type
td.data-count
div.row
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Value
hr
canvas(id="graph-tx-value")
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Value Distribution
hr
canvas(id="chart-tx-value-distribution")
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Fee
hr
canvas(id="graph-tx-fee")
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Fee Distribution
hr
canvas(id="chart-tx-fee-distribution")
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Input Count
hr
canvas(id="graph-tx-inputs")
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Output Count
hr
canvas(id="graph-tx-outputs")
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Size
hr
canvas(id="graph-tx-size")
block endOfBody
- var txidChunkSize = 10;
script(src="/js/chart.bundle.min.js", integrity="sha384-qgOtiGNaHh9fVWUnRjyHlV39rfbDcvPPkEzL1RHvsHKbuqUqM6uybNuVnghY2z4/")
script(src='/js/decimal.js')
script.
var txidChunkSize = !{txidChunkSize};
var txidChunks = !{JSON.stringify(utils.splitArrayIntoChunks(result.getblock.tx, txidChunkSize))};
var blockHeight = !{result.getblock.height};
var satsMultiplier = !{coinConfig.baseCurrencyUnit.multiplier};
$(document).ready(function() {
loadTransactions(txidChunks, txidChunkSize, txidChunks.length * txidChunkSize);
});
function loadTransactions(txidChunks, chunkSize, count) {
var chunkStrs = [];
for (var i = 0; i < txidChunks.length; i++) {
var txidChunk = txidChunks[i];
var chunkStr = "";
for (var j = 0; j < txidChunk.length; j++) {
if (j > 0) {
chunkStr += ",";
}
chunkStr += txidChunk[j];
}
chunkStrs.push(chunkStr);
}
//alert(JSON.stringify(chunks));
var results = [];
var statusCallback = function(chunkIndexDone, chunkCount) {
//console.log("Done: " + Math.min(((chunkIndexDone + 1) * chunkSize), count) + " of " + count);
var wPercent = `${parseInt(100 * (chunkIndexDone + 1) / parseFloat(chunkCount))}%`;
$("#data-progress").css("width", wPercent);
$("#progress-text").text(`${Math.min(((chunkIndexDone + 1) * chunkSize), count).toLocaleString()} of ${count.toLocaleString()} (${wPercent})`);
};
var finishedCallback = function() {
var summary = summarizeData(results);
fillTopValueTxTable(summary);
fillTopFeeTxTable(summary);
fillInputOutputTypesTable(summary);
createGraph("graph-tx-value", summary.txValueGraphData, "Value");
createGraph("graph-tx-fee", summary.txFeeGraphData, "Fee");
createGraph("graph-tx-inputs", summary.txInputCountGraphData, "Input Count");
createGraph("graph-tx-outputs", summary.txOutputCountGraphData, "Output Count");
createGraph("graph-tx-size", summary.txSizeGraphData, "Size");
createChart("chart-tx-value-distribution", summary.valueDistribution, summary.valueDistributionLabels);
createChart("chart-tx-fee-distribution", summary.feeDistribution, summary.feeDistributionLabels);
//$(".abc").text(JSON.stringify(summary));
$("#main-content").show();
$("#progress-wrapper").hide();
};
getTxData(results, chunkStrs, 0, statusCallback, finishedCallback);
}
function fillTopValueTxTable(data) {
var count = Math.min(10, data.topValueTxs.length);
for (var i = 0; i < count; i++) {
var item = data.topValueTxs[i];
var row = $("#tbody-top-value-tx .row-prototype").clone();
row.removeClass("row-prototype");
row.find(".data-index").text((i + 1).toLocaleString());
row.find(".data-tx-link").html(`<a href='/tx/${item.txid}'>${item.txid}</a>`);
row.find(".data-tx-value").text(item.value);
updateCurrencyValue(row.find(".data-tx-value"), item.value);
row.show();
$("#tbody-top-value-tx").append(row);
}
}
function fillTopFeeTxTable(data) {
var count = Math.min(10, data.topFeeTxs.length);
for (var i = 0; i < count; i++) {
var item = data.topFeeTxs[i];
var row = $("#tbody-top-fee-tx .row-prototype").clone();
row.removeClass("row-prototype");
row.find(".data-index").text((i + 1).toLocaleString());
row.find(".data-tx-link").html(`<a href='/tx/${item.txid}'>${item.txid}</a>`);
row.find(".data-tx-value").text(item.value);
updateCurrencyValue(row.find(".data-tx-value"), item.value);
row.show();
$("#tbody-top-fee-tx").append(row);
}
}
function fillInputOutputTypesTable(data) {
var sortedInputs = [];
for (var key in data.inputTypeCounts) {
if (data.inputTypeCounts.hasOwnProperty(key)) {
sortedInputs.push({type:key, count:data.inputTypeCounts[key]});
}
}
var sortedOutputs = [];
for (var key in data.outputTypeCounts) {
if (data.outputTypeCounts.hasOwnProperty(key)) {
sortedOutputs.push({type:key, count:data.outputTypeCounts[key]});
}
}
sortedInputs.sort(function(a, b) {
return b.count - a.count;
});
sortedOutputs.sort(function(a, b) {
return b.count - a.count;
});
for (var i = 0; i < sortedInputs.length; i++) {
var item = sortedInputs[i];
var row = $("#tbody-input-types .row-prototype").clone();
row.removeClass("row-prototype");
if (i == 0) {
row.addClass("table-borderless");
}
// span(title=`Output Type: ${utils.outputTypeName(inputTypeKey)}`, data-toggle="tooltip") #{utils.outputTypeAbbreviation(inputTypeKey)}
row.find(".data-type").html(`<span title='Type: ${item.type}' data-toggle='tooltip'>${item.type}</span>`);
row.find(".data-count").text(item.count.toLocaleString());
row.show();
$("#tbody-input-types").append(row);
}
for (var i = 0; i < sortedOutputs.length; i++) {
var item = sortedOutputs[i];
var row = $("#tbody-output-types .row-prototype").clone();
row.removeClass("row-prototype");
if (i == 0) {
row.addClass("table-borderless");
}
// span(title=`Output Type: ${utils.outputTypeName(inputTypeKey)}`, data-toggle="tooltip") #{utils.outputTypeAbbreviation(inputTypeKey)}
row.find(".data-type").html(`<span title='Type: ${item.type}' data-toggle='tooltip'>${item.type}</span>`);
row.find(".data-count").text(item.count.toLocaleString());
row.show();
$("#tbody-output-types").append(row);
}
}
function createGraph(graphId, data, yLabelStr) {
var ctx = document.getElementById(graphId).getContext('2d');
var graph = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
borderColor: '#007bff',
borderWidth: 2,
backgroundColor: 'rgba(0,0,0,0)',
data: data,
pointRadius: 1
}]
},
options: {
legend: { display: false },
scales: {
xAxes: [{
type: 'linear',
position: 'bottom',
scaleLabel: {
display: true,
labelString: 'Index in Block'
},
//ticks: {
// stepSize: 100,
//}
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: yLabelStr
}
}]
}
}
});
}
function createChart(chartId, data, labels) {
var bgColors = [];
for (var i = 0; i < labels.length; i++) {
bgColors.push(`hsl(${(333 * i / labels.length)}, 100%, 50%)`);
}
var ctx1 = document.getElementById(chartId).getContext('2d');
var chart = new Chart(ctx1, {
type: 'bar',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: bgColors
}]
},
options: {
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
}
function getTxData(results, chunks, chunkIndex, statusCallback, finishedCallback) {
if (chunkIndex > chunks.length - 1) {
finishedCallback();
return;
}
var url = `/api/block-tx-summaries/${blockHeight}/${chunks[chunkIndex]}`;
//console.log(url);
$.ajax({
url: url
}).done(function(result) {
for (var i = 0; i < result.length; i++) {
results.push(result[i]);
}
statusCallback(chunkIndex, chunks.length);
getTxData(results, chunks, chunkIndex + 1, statusCallback, finishedCallback);
});
}
function summarizeData(txResults) {
var analysis = {};
analysis.inputTypeCounts = {};
analysis.outputTypeCounts = {};
analysis.txValues = [];
analysis.txValueGraphData = [];
analysis.txFees = [];
analysis.txFeeGraphData = [];
analysis.txSizeGraphData = [];
analysis.txInputCountGraphData = [];
analysis.txOutputCountGraphData = [];
for (var i = 0; i < txResults.length; i++) {
var txSummary = txResults[i];
//console.log(JSON.stringify(txSummary));
for (var j = 0; j < txSummary.vout.length; j++) {
var vout = txSummary.vout[j];
var outputType = vout.type;
if (!analysis.outputTypeCounts[outputType]) {
analysis.outputTypeCounts[outputType] = 0;
}
analysis.outputTypeCounts[outputType]++;
}
analysis.txValues.push({txid:txSummary.txid, value:new Decimal(txSummary.totalOutput)});
analysis.txValueGraphData.push({x:i, y:new Decimal(txSummary.totalOutput).toNumber()});
analysis.txFees.push({txid:txSummary.txid, value:new Decimal(txSummary.totalFee)});
analysis.txFeeGraphData.push({x:i, y:new Decimal(txSummary.totalFee).toNumber()});
analysis.txSizeGraphData.push({x:i, y:(txSummary.vsize ? txSummary.vsize : txSummary.size)});
analysis.txInputCountGraphData.push({x:i, y:txSummary.vin.length});
analysis.txOutputCountGraphData.push({x:i, y:txSummary.vout.length});
if (!txSummary.coinbase) {
for (var j = 0; j < txSummary.vin.length; j++) {
var vin = txSummary.vin[j];
var inputType = vin.type;
if (!analysis.inputTypeCounts[inputType]) {
analysis.inputTypeCounts[inputType] = 0;
}
analysis.inputTypeCounts[inputType]++;
}
}
}
analysis.txValues.sort(function(a, b) {
return b.value.cmp(a.value);
});
analysis.txFees.sort(function(a, b) {
return b.value.cmp(a.value);
});
analysis.topValueTxs = analysis.txValues.slice(0, Math.min(100, analysis.txValues.length));
analysis.topFeeTxs = analysis.txFees.slice(0, Math.min(100, analysis.txFees.length));
var topValue = new Decimal(analysis.txValues[parseInt(analysis.txValues.length * 0.1)].value).times(satsMultiplier);
var topFee = new Decimal(analysis.txFees[parseInt(analysis.txFees.length * 0.1)].value).times(satsMultiplier);
var topValueSats = parseInt(topValue);
var topFeeSats = parseInt(topFee);
var distributionBucketCount = 25;
analysis.valueDistribution = [];
analysis.valueDistributionLabels = [];
analysis.feeDistribution = [];
analysis.feeDistributionLabels = [];
for (var i = 0; i < distributionBucketCount; i++) {
analysis.valueDistribution.push(0);
analysis.valueDistributionLabels.push(`[${new Decimal(i * topValueSats / distributionBucketCount).dividedBy(satsMultiplier).toDP(3)} - ${new Decimal((i + 1) * topValueSats / distributionBucketCount).dividedBy(satsMultiplier).toDP(3)})`);
analysis.feeDistribution.push(0);
analysis.feeDistributionLabels.push(`[${new Decimal(i * topFeeSats / distributionBucketCount).toDP(0)} - ${new Decimal((i + 1) * topFeeSats / distributionBucketCount).toDP(0)})`);
}
analysis.valueDistributionLabels[distributionBucketCount - 1] = `${new Decimal(topValueSats).dividedBy(satsMultiplier).toDP(3)}+`;
analysis.feeDistributionLabels[distributionBucketCount - 1] = `${topFeeSats}+`;
for (var i = 0; i < txResults.length; i++) {
var txSummary = txResults[i];
var valueSats = new Decimal(txSummary.totalOutput).times(satsMultiplier);
var feeSats = new Decimal(txSummary.totalFee).times(satsMultiplier);
var valueBucket = parseInt(distributionBucketCount * valueSats / topValueSats);
if (valueBucket >= distributionBucketCount) {
valueBucket = distributionBucketCount - 1;
}
var feeBucket = parseInt(distributionBucketCount * feeSats / topFeeSats);
if (feeBucket >= distributionBucketCount) {
feeBucket = distributionBucketCount - 1;
}
analysis.valueDistribution[valueBucket]++;
analysis.feeDistribution[feeBucket]++;
}
return analysis;
}

3
views/block-stats.pug

@ -88,7 +88,7 @@ block content
div.row.clearfix
each graphId, graphIndex in graphIds
div.col-lg-6.float-left
div.col-lg-6.float-left(class=(graphIndex % 2 == 0 ? "col-lg-left" : "col-lg-right"))
div.card.shadow-sm.mb-3.graph-wrapper(id=`graph-wrapper-${graphId}`)
div.card-body
h3.h6.mb-0 #{graphTitles[graphIndex]}
@ -381,6 +381,7 @@ block endOfBody
borderWidth: 2,
borderColor: colors[i],
backgroundColor: 'rgba(0, 0, 0, 0)',
pointRadius: 1
});
yaxes.push({

80
views/fun.pug

@ -7,47 +7,49 @@ block content
h1.h3 #{coinConfig.name} Fun
hr
p Below is a list of fun/interesting things in the #{coinConfig.name} blockchain. Some are historical firsts, others are just fun or cool.
div(class="table-responsive")
table(class="table table-striped mt-4")
thead
tr
th(class="data-header") Date
th(class="data-header") Description
th(class="data-header") Link
th(class="data-header") Reference
tbody
each item, index in coinConfig.historicalData
if (item.chain == global.activeBlockchain)
div.card.shadow-sm.mb-3
div.card-body
p Below is a list of fun/interesting things in the #{coinConfig.name} blockchain. Some are historical firsts, others are just fun or cool.
div(class="table-responsive")
table(class="table table-striped mt-4")
thead
tr
td(class="data-cell") #{item.date}
th(class="data-header") Date
th(class="data-header") Description
th(class="data-header") Link
th(class="data-header") Reference
tbody
each item, index in coinConfig.historicalData
if (item.chain == global.activeBlockchain)
tr
td(class="data-cell") #{item.date}
td(class="data-cell") #{item.summary}
td(class="data-cell text-monospace")
if (item.type == "tx")
a(href=("/tx/" + item.txid), title=item.txid, data-toggle="tooltip") Tx #{item.txid.substring(0, 23)}...
else if (item.type == "block")
a(href=("/block/" + item.blockHash), title="Block #{item.blockHash}", data-toggle="tooltip") Block #{item.blockHash.substring(0, 20)}...
else if (item.type == "blockheight")
a(href=("/block/" + item.blockHash)) Block ##{item.blockHeight}
else if (item.type == "address")
a(href=("/address/" + item.address), title=item.address, data-toggle="tooltip") Address #{item.address.substring(0, 18)}...
else if (item.type == "link")
a(href=item.url) #{item.url.substring(0, 20)}...
td(class="data-cell") #{item.summary}
td(class="data-cell text-monospace")
if (item.type == "tx")
a(href=("/tx/" + item.txid), title=item.txid, data-toggle="tooltip") Tx #{item.txid.substring(0, 23)}...
else if (item.type == "block")
a(href=("/block/" + item.blockHash), title="Block #{item.blockHash}", data-toggle="tooltip") Block #{item.blockHash.substring(0, 20)}...
else if (item.type == "blockheight")
a(href=("/block/" + item.blockHash)) Block ##{item.blockHeight}
else if (item.type == "address")
a(href=("/address/" + item.address), title=item.address, data-toggle="tooltip") Address #{item.address.substring(0, 18)}...
else if (item.type == "link")
a(href=item.url) #{item.url.substring(0, 20)}...
td(class="data-cell")
if (item.referenceUrl && item.referenceUrl.trim().length > 0)
- var matches = item.referenceUrl.match(/^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)/i);
td(class="data-cell")
if (item.referenceUrl && item.referenceUrl.trim().length > 0)
- var matches = item.referenceUrl.match(/^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)/i);
- var domain = null;
- var domain = matches && matches[1];
- var domain = null;
- var domain = matches && matches[1];
if (domain)
a(href=item.referenceUrl, rel="nofollow") #{domain}
i(class="fas fa-external-link-alt")
else
a(href=item.referenceUrl, rel="nofollow") Reference
else
span -
if (domain)
a(href=item.referenceUrl, rel="nofollow") #{domain}
i(class="fas fa-external-link-alt")
else
a(href=item.referenceUrl, rel="nofollow") Reference
else
span -

17
views/includes/block-content.pug

@ -25,7 +25,7 @@ ul.nav.nav-tabs.mb-3
- var txCount = result.getblock.tx.length;
div.tab-content
div(id="tab-details", class="tab-pane active", role="tabpanel")
div.tab-pane.active(id="tab-details", role="tabpanel")
if (global.specialBlocks && global.specialBlocks[result.getblock.hash])
div(class="alert alert-primary shadow-sm", style="padding-bottom: 0;")
div(class="float-left", style="width: 55px; height: 55px; font-size: 18px;")
@ -76,11 +76,12 @@ div.tab-content
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.border-dotted(title="Total value of all transaction outputs", data-toggle="tooltip")
span Total Output
div.text-monospace(class=sumTableValueClass)
if (result.blockstats.total_out)
- var currencyValue = new Decimal(result.blockstats.total_out).dividedBy(coinConfig.baseCurrencyUnit.multiplier);
- var currencyValue = new Decimal(result.blockstats.total_out).plus(new Decimal(result.blockstats.totalfee)).plus(new Decimal(result.blockstats.subsidy));
- currencyValue = currencyValue.dividedBy(coinConfig.baseCurrencyUnit.multiplier);
include ./value-display.pug
else
span 0
@ -228,7 +229,7 @@ div.tab-content
div.summary-split-table-content.text-monospace
if (result.blockstats.minfeerate)
span #{result.blockstats.minfeerate}
span #{result.blockstats.minfeerate.toLocaleString()}
small.ml-1 sat/vB
else
span ?
@ -236,7 +237,7 @@ div.tab-content
br
if (result.blockstats.avgfeerate)
span #{result.blockstats.avgfeerate}
span #{result.blockstats.avgfeerate.toLocaleString()}
small.ml-1 sat/vB
else
@ -246,7 +247,7 @@ div.tab-content
br
if (result.blockstats.maxfeerate)
span #{result.blockstats.maxfeerate}
span #{result.blockstats.maxfeerate.toLocaleString()}
small.ml-1 sat/vB
else
span ?
@ -357,12 +358,14 @@ div.tab-content
div.card-body.px-2.px-md-3
div.row
div.col-md-4
h2.h6.mb-0 #{txCount.toLocaleString()}
h2.h6.mb-0.d-inline-block #{txCount.toLocaleString()}
if (txCount == 1)
span Transaction
else
span Transactions
a.ml-2(href=`/block-analysis/${result.getblock.hash}`) See Transaction Analysis &raquo;
if (false || (!config.demoSite && !crawlerBot && txCount > 20))
div(class="col-md-8 text-right")
small.mr-1.text-muted Show

2
views/includes/index-network-summary.pug

@ -205,7 +205,7 @@ div.row.index-summary
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
span.border-dotted(title=`Total output of all 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);

32
views/includes/transaction-io-details.pug

@ -43,7 +43,7 @@ div.row.text-monospace
span Newly minted coins
else
div.word-wrap
small.data-tag.bg-dark.mr-2 txo
small.data-tag.bg-dark.mr-2 #{utils.outputTypeAbbreviation(vout.scriptPubKey.type)}
a(href=("/tx/" + txInput.txid + "#output-" + txVin.vout)) #{utils.ellipsize(txInput.txid, 26)}
span ##{txVin.vout}
@ -155,20 +155,7 @@ div.row.text-monospace
if (true)
div.mb-tiny
small.data-tag.bg-dark
if (vout.scriptPubKey.type == "pubkey")
span(title="Output Type: Pay to Public Key", data-toggle="tooltip") p2pk
else if (vout.scriptPubKey.type == "pubkeyhash")
span(title="Output Type: Pay to Public Key Hash", data-toggle="tooltip") p2pkh
else if (vout.scriptPubKey.type == "scripthash")
span(title="Output Type: Pay to Script Hash", data-toggle="tooltip") p2sh
else if (vout.scriptPubKey.type == "witness_v0_keyhash")
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 ???
span(title=`Output Type: ${utils.outputTypeName(vout.scriptPubKey.type)}`, data-toggle="tooltip") #{utils.outputTypeAbbreviation(vout.scriptPubKey.type)}
each addr in vout.scriptPubKey.addresses
a(id=("output-" + voutIndex), href=("/address/" + addr))
@ -200,20 +187,7 @@ div.row.text-monospace
else
div.mb-tiny
small.data-tag.bg-dark
if (vout.scriptPubKey.type == "pubkey")
span(title="Output Type: Pay to Public Key", data-toggle="tooltip") p2pk
else if (vout.scriptPubKey.type == "pubkeyhash")
span(title="Output Type: Pay to Public Key Hash", data-toggle="tooltip") p2pkh
else if (vout.scriptPubKey.type == "scripthash")
span(title="Output Type: Pay to Script Hash", data-toggle="tooltip") p2sh
else if (vout.scriptPubKey.type == "witness_v0_keyhash")
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 ???
span(title=`Output Type: ${utils.outputTypeName(vout.scriptPubKey.type)}`, data-toggle="tooltip") #{utils.outputTypeAbbreviation(vout.scriptPubKey.type)}
span
small.font-weight-bold asm:

16
views/index.pug

@ -54,22 +54,6 @@ block content
span(aria-hidden="true") &times;
- var networkSummaryItemCount = 4;
if (getblockchaininfo.size_on_disk)
- networkSummaryItemCount++;
if (exchangeRates)
- networkSummaryItemCount++;
if (txStats)
- networkSummaryItemCount++;
- var networkSummaryColumnClass = "col-md-4";
if (networkSummaryItemCount > 6)
- networkSummaryColumnClass = "col-md-3";
div.row
- var summaryColCount = 8;
if (exchangeRates)

2
views/layout.pug

@ -212,6 +212,8 @@ html(lang="en")
script(defer, src="/js/fontawesome.min.js", integrity="sha384-eVEQC9zshBn0rFj4+TU78eNA19HMNigMviK/PU/FFjLXqa/GKPgX58rvt5Z8PLs7")
script(src="/js/highlight.min.js", integrity="sha384-xLrpH5gNLD6HMLgeDH1/p4FXigQ8T9mgNm+EKtCSXL0OJ5i1bnSi57dnwFuUMM9/")
script(src="/js/site.js", integrity="sha384-4/UxV25z5d3QFVgqmQ1Aez9CEzXwGz5MbBctDnsoCg6twESX4Jn0wgkazFXUJFqR")
script.
$(document).ready(function() {

29
views/mempool-summary.pug

@ -11,7 +11,7 @@ block content
div.card.shadow-sm.mb-3
div.card-body
h4.h6 Loading mempool transactions:
span(id="block-progress-text")
span(id="progress-text")
div.progress(id="progress-bar", style="height: 7px;")
div.progress-bar(id="data-progress", role="progressbar", aria-valuenow="0", aria-valuemin="0" ,aria-valuemax="100")
@ -122,7 +122,7 @@ block endOfBody
var wPercent = `${parseInt(100 * (chunkIndexDone + 1) / parseFloat(chunkCount))}%`;
$("#data-progress").css("width", wPercent);
$("#block-progress-text").text(`${Math.min(((chunkIndexDone + 1) * chunkSize), count).toLocaleString()} of ${count.toLocaleString()} (${wPercent})`);
$("#progress-text").text(`${Math.min(((chunkIndexDone + 1) * chunkSize), count).toLocaleString()} of ${count.toLocaleString()} (${wPercent})`);
};
var finishedCallback = function() {
@ -263,29 +263,10 @@ block endOfBody
$("#progress-wrapper").hide();
};
getBlockData(results, chunkStrs, 0, statusCallback, finishedCallback);
getTxData(results, chunkStrs, 0, statusCallback, finishedCallback);
}
function updateCurrencyValue(element, val) {
$.ajax({
url: `/snippet/formatCurrencyAmount/${val}`
}).done(function(result) {
element.html(result);
$('[data-toggle="tooltip"]').tooltip();
});
}
function updateFeeRateValue(element, val, digits) {
$.ajax({
url: `/api/utils/formatCurrencyAmountInSmallestUnits/${val},${digits}`
}).done(function(result) {
element.html(`<span>${result.val} <small>${result.currencyUnit}/vB</small></span>`);
});
}
function getBlockData(results, chunkStrs, chunkIndex, statusCallback, finishedCallback) {
function getTxData(results, chunkStrs, chunkIndex, statusCallback, finishedCallback) {
if (chunkIndex > chunkStrs.length - 1) {
finishedCallback();
@ -306,7 +287,7 @@ block endOfBody
statusCallback(chunkIndex, chunkStrs.length);
getBlockData(results, chunkStrs, chunkIndex + 1, statusCallback, finishedCallback);
getTxData(results, chunkStrs, chunkIndex + 1, statusCallback, finishedCallback);
});
}

14
views/mining-summary.pug

@ -214,7 +214,7 @@ block endOfBody
$("#time-range-buttons .block-count-btn").removeClass("disabled");
$("#block-selections-buttons .dropdown-item").removeClass("disabled");
console.log(JSON.stringify(results));
//console.log(JSON.stringify(results));
var summary = summarizeBlockData(results);
@ -269,8 +269,6 @@ block endOfBody
$("#summary-table").show();
$("#progress-wrapper").hide();
//$("#xyz").html(JSON.stringify(summary, null, 4));
};
getBlockData(results, chunks, 0, statusCallback, finishedCallback);
@ -301,16 +299,6 @@ block endOfBody
});
}
function updateCurrencyValue(element, val) {
$.ajax({
url: `/snippet/formatCurrencyAmount/${val}`
}).done(function(result) {
element.html(result);
$('[data-toggle="tooltip"]').tooltip();
});
}
function summarizeBlockData(blocks) {
var summariesByMiner = {};
var minerNamesSortedByBlockCount = [];

2
views/transaction.pug

@ -146,7 +146,7 @@ block content
div.summary-table-content.text-monospace
if (result.getrawtransaction.locktime < 500000000)
span Spendable in block
a(href=("/block-height/" + result.getrawtransaction.locktime)) #{result.getrawtransaction.locktime.toLocaleString()}
a(href=("/block-height/" + result.getrawtransaction.locktime)) ##{result.getrawtransaction.locktime.toLocaleString()}
span or later
a(href="https://bitcoin.org/en/developer-guide#locktime-and-sequence-number", data-toggle="tooltip", title="More info about locktime", target="_blank")
i.fas.fa-info-circle

16
views/tx-stats.pug

@ -17,7 +17,7 @@ block content
if (true)
div.row
div.col-lg-6
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transactions, 24hr
@ -30,7 +30,7 @@ block content
- var graphData = {id:"graphDay", dataVar:"txCountDataDay", labels:txStatsDay.txLabels, title:"Transactions, 24hr", xaxisTitle:"Block", xaxisStep:5, yaxisTitle:"Tx Count"};
include ./includes/line-graph.pug
div.col-lg-6
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transactions, 7day
@ -43,7 +43,7 @@ block content
- var graphData = {id:"graphWeek", dataVar:"txCountDataWeek", labels:txStatsWeek.txLabels, title:"Transactions, 7day", xaxisTitle:"Block", xaxisStep:100, yaxisTitle:"Tx Count"};
include ./includes/line-graph.pug
div.col-lg-6
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transactions, 30day
@ -56,7 +56,7 @@ block content
- var graphData = {id:"graphMonth", dataVar:"txCountDataMonth", labels:txStatsMonth.txLabels, title:"Transactions, 30day", xaxisTitle:"Block", xaxisStep:500, yaxisTitle:"Tx Count"};
include ./includes/line-graph.pug
div.col-lg-6
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transactions, All time
@ -71,7 +71,7 @@ block content
div.row
div.col-lg-6
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Tx Rate, 24hr
@ -84,7 +84,7 @@ block content
- var graphData = {id:"graphRateDay", dataVar:"txRateDataDay", labels:txStatsDay.txLabels, title:"Tx Rate, 24hr", xaxisTitle:"Block", xaxisStep:5, yaxisTitle:"Tx Per Sec"};
include ./includes/line-graph.pug
div.col-lg-6
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Tx Rate, 7day
@ -97,7 +97,7 @@ block content
- var graphData = {id:"graphRateWeek", dataVar:"txRateDataWeek", labels:txStatsWeek.txLabels, title:"Tx Rate, 7day", xaxisTitle:"Block", xaxisStep:100, yaxisTitle:"Tx Per Sec"};
include ./includes/line-graph.pug
div.col-lg-6
div.col-lg-6.col-lg-left
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Tx Rate, 30day
@ -110,7 +110,7 @@ block content
- var graphData = {id:"graphRateMonth", dataVar:"txRateDataMonth", labels:txStatsMonth.txLabels, title:"Tx Rate, 30day", xaxisTitle:"Block", xaxisStep:500, yaxisTitle:"Tx Per Sec"};
include ./includes/line-graph.pug
div.col-lg-6
div.col-lg-6.col-lg-right
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Tx Rate, All time

Loading…
Cancel
Save