You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

670 lines
20 KiB

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)) « 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.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.mb-huge
div.card.shadow-sm.mb-3
div.card-body
h4.h6 Loading transactions:
span(id="progress-text")
div.progress.mt-2(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-xl-4
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.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.data-tx-link
td.text-right.data-tx-value
div.col-xl-4
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.data-header Transaction
th.data-header.text-right Fee
tbody(id="tbody-top-fee-tx")
tr.text-monospace.row-prototype(style="display: none;")
td.data-tx-link
td.text-right.data-tx-value
div.col-xl-4
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Top Size Transactions
hr
div.table-responsive
table.table.table-striped.mb-0
thead
tr
th.data-header Transaction
th.data-header.text-right Size
tbody(id="tbody-top-size-tx")
tr.text-monospace.row-prototype(style="display: none;")
td.data-tx-link
td.text-right.data-tx-value
div.col-lg-6.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.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
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
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
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
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
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Size
hr
canvas(id="graph-tx-size")
div.col-lg-6
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Size Distribution
hr
canvas(id="chart-tx-size-distribution")
div.col-lg-6
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
div.card.shadow-sm.mb-3
div.card-body
h3.h6.mb-0 Transaction Output Count
hr
canvas(id="graph-tx-outputs")
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};
var vByteSizes = false;
$(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);
fillTopSizeTxTable(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);
createChart("chart-tx-size-distribution", summary.sizeDistribution, summary.sizeDistributionLabels);
//$(".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-tx-link").html(`<a href='/tx/${item.txid}'>${item.txid.substring(0, 14)}<span class='d-inline d-xl-none'>${item.txid.substring(14, 32)}</span><span class='d-none d-xxl-inline'>${item.txid.substring(14, 30)}</span>…</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-tx-link").html(`<a href='/tx/${item.txid}'>${item.txid.substring(0, 14)}<span class='d-inline d-xl-none'>${item.txid.substring(14, 32)}</span><span class='d-none d-xxl-inline'>${item.txid.substring(14, 30)}</span>…</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 fillTopSizeTxTable(data) {
var count = Math.min(10, data.topSizeTxs.length);
for (var i = 0; i < count; i++) {
var item = data.topSizeTxs[i];
var row = $("#tbody-top-size-tx .row-prototype").clone();
row.removeClass("row-prototype");
row.find(".data-tx-link").html(`<a href='/tx/${item.txid}'>${item.txid.substring(0, 14)}<span class='d-inline d-xl-none'>${item.txid.substring(14, 32)}</span><span class='d-none d-xxl-inline'>${item.txid.substring(14, 30)}</span>…</a>`);
row.find(".data-tx-value").html(`<span>${parseInt(item.value).toLocaleString()} <small>${vByteSizes ? "v" : ""}B</small></span>`);
row.show();
$("#tbody-top-size-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.txSizes = [];
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]++;
}
if (txSummary.vsize) {
vByteSizes = true;
}
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.txSizes.push({txid:txSummary.txid, value:new Decimal(txSummary.vsize ? txSummary.vsize : txSummary.size)});
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.txSizes.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));
analysis.topSizeTxs = analysis.txSizes.slice(0, Math.min(100, analysis.txSizes.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 topSize = new Decimal(analysis.txSizes[parseInt(analysis.txSizes.length * 0.1)].value);
var topValueSats = parseInt(topValue);
var topFeeSats = parseInt(topFee);
var distributionBucketCount = 25;
analysis.valueDistribution = [];
analysis.valueDistributionLabels = [];
analysis.feeDistribution = [];
analysis.feeDistributionLabels = [];
analysis.sizeDistribution = [];
analysis.sizeDistributionLabels = [];
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.sizeDistribution.push(0);
analysis.sizeDistributionLabels.push(`[${new Decimal(i * topSize / distributionBucketCount).toDP(0)} - ${new Decimal((i + 1) * topSize / distributionBucketCount).toDP(0)})`);
}
analysis.valueDistributionLabels[distributionBucketCount - 1] = `${new Decimal(topValueSats).dividedBy(satsMultiplier).toDP(3)}+`;
analysis.feeDistributionLabels[distributionBucketCount - 1] = `${topFeeSats}+`;
analysis.sizeDistributionLabels[distributionBucketCount - 1] = `${topSize}+`;
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 size = new Decimal(txSummary.vsize ? txSummary.vsize : txSummary.size);
var valueBucket = parseInt(distributionBucketCount * valueSats / topValueSats);
if (valueBucket >= distributionBucketCount) {
valueBucket = distributionBucketCount - 1;
}
var feeBucket = parseInt(distributionBucketCount * feeSats / topFeeSats);
if (feeBucket >= distributionBucketCount) {
feeBucket = distributionBucketCount - 1;
}
var sizeBucket = parseInt(distributionBucketCount * size / topSize);
if (sizeBucket >= distributionBucketCount) {
sizeBucket = distributionBucketCount - 1;
}
analysis.valueDistribution[valueBucket]++;
analysis.feeDistribution[feeBucket]++;
analysis.sizeDistribution[sizeBucket]++;
}
return analysis;
}