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(`${item.txid.substring(0, 14)}${item.txid.substring(14, 32)}${item.txid.substring(14, 30)}`); 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(`${item.txid.substring(0, 14)}${item.txid.substring(14, 32)}${item.txid.substring(14, 30)}`); 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(`${item.txid.substring(0, 14)}${item.txid.substring(14, 32)}${item.txid.substring(14, 30)}`); row.find(".data-tx-value").html(`${parseInt(item.value).toLocaleString()} ${vByteSizes ? "v" : ""}B`); 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(`${item.type}`); 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(`${item.type}`); 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; }