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.
 
 
 

533 lines
15 KiB

extends layout
block headContent
title Mempool Summary
block content
h1.h3 Mempool Summary
hr
div#progress-wrapper.mb-huge
div.card.shadow-sm.mb-3
div.card-body
h4.h6 Loading mempool 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(id="main-content", style="display: none;")
div.card.shadow-sm.mb-3
div.card-body.px-2.px-md-3
h3.h6.mb-0 Summary
hr
div.clearfix
div.row
div.summary-table-label Tx Count
div.summary-table-content.text-monospace(id="tx-count")
div.row
div.summary-table-label Memory Usage
div.summary-table-content.text-monospace(id="mem-usage")
div.row
div.summary-table-label Total Fees
div.summary-table-content.text-monospace(id="total-fees")
div.row
div.summary-table-label Avg Fee
div.summary-table-content.text-monospace(id="avg-fee")
div.row
div.summary-table-label Avg Fee Rate
div.summary-table-content.text-monospace(id="avg-fee-rate")
div.card.shadow-sm.mb-3
div.card-body.px-2.px-md-3
h3.h6.mb-0 Transactions by fee rate
hr
canvas.mb-3(id="mempoolBarChart", height="100")
div.table-responsive
table.table.table-striped.mb-3
thead
tr
th Fee Rate
th.text-right Tx Count
th.text-right Total Fees
th.text-right Avg Fee
th.text-right Avg Fee Rate
tbody(id="fee-rate-table-body")
tr(id="fee-rate-table-row-prototype", style="display: none;")
td.text-monospace.data-label
td.text-monospace.text-right.data-count
td.text-monospace.text-right.data-total-fees
td.text-monospace.text-right.data-avg-fee
td.text-monospace.text-right.data-fee-rate
div.card.shadow-sm.mb-3
div.card-body.px-2.px-md-3
h3.h6.mb-0 Transactions by size
hr
canvas.mb-3(id="txSizesBarChart", height="100")
div.card.shadow-sm.mb-3
div.card-body.px-2.px-md-3
h3.h6.mb-0 Transactions by age
hr
canvas.mb-3(id="txAgesBarChart", height="100")
block endOfBody
script(src="/js/chart.bundle.min.js", integrity="sha384-qgOtiGNaHh9fVWUnRjyHlV39rfbDcvPPkEzL1RHvsHKbuqUqM6uybNuVnghY2z4/")
script(src='/js/decimal.js')
script.
var txidChunks = !{JSON.stringify(mempooltxidChunks)};
var satoshiPerByteBucketMaxima = !{JSON.stringify(satoshiPerByteBucketMaxima)};
$(document).ready(function() {
loadMempool(txidChunks, 25, txidChunks.length * 25);
});
function loadMempool(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);
var feeRateGraphData = buildFeeRateGraphData(summary);
var txSizeGraphData = buildTxSizeGraphData(summary);
var txAgeGraphData = buildTxAgeGraphData(summary);
//console.log(JSON.stringify(summary));
$("#tx-count").text(summary.count.toLocaleString());
$("#mem-usage").text(summary.totalBytes.toLocaleString());
$("#total-fees").text(summary.totalFees);
$("#avg-fee").text(summary.averageFee);
$("#avg-fee-rate").text(summary.averageFeePerByte);
$.ajax({
url: `/api/utils/formatLargeNumber/${summary.totalBytes},2`
}).done(function(result) {
$("#mem-usage").html(`<span>${result[0]} <small>${result[1].abbreviation}B</small></span>`);
});
updateCurrencyValue($("#total-fees"), summary.totalFees);
updateCurrencyValue($("#avg-fee"), summary.averageFee);
updateFeeRateValue($("#avg-fee-rate"), summary.averageFeePerByte, 2);
//$("#summary-json").text(JSON.stringify(summary, null, 4));
// fee rate chart
var ctx1 = document.getElementById("mempoolBarChart").getContext('2d');
var mempoolBarChart = new Chart(ctx1, {
type: 'bar',
data: {
labels: feeRateGraphData.feeBucketLabels,
datasets: [{
data: feeRateGraphData.feeBucketTxCounts,
backgroundColor: feeRateGraphData.bgColors
}]
},
options: {
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
// tx size chart
var ctx2 = document.getElementById("txSizesBarChart").getContext('2d');
var txSizesBarChart = new Chart(ctx2, {
type: 'bar',
data: {
labels: txSizeGraphData.sizeBucketLabels,
datasets: [{
data: txSizeGraphData.sizeBucketTxCounts,
backgroundColor: txSizeGraphData.bgColors
}]
},
options: {
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
// tx age chart
var ctx3 = document.getElementById("txAgesBarChart").getContext('2d');
var txSizesBarChart = new Chart(ctx3, {
type: 'bar',
data: {
labels: txAgeGraphData.ageBucketLabels,
datasets: [{
data: txAgeGraphData.ageBucketTxCounts,
backgroundColor: txAgeGraphData.bgColors
}]
},
options: {
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
// fee rate table
for (var i = 0; i < summary.satoshiPerByteBuckets.length; i++) {
var item = summary.satoshiPerByteBuckets[i];
var row = $("#fee-rate-table-row-prototype").clone();
row.attr("id", null);
row.addClass("fee-rate-table-row");
row.find(".data-label").text(summary.satoshiPerByteBucketLabels[i]);
row.find(".data-count").text(item.count.toLocaleString());
row.find(".data-total-fees").text(item.count > 0 ? item.totalFees : "-");
row.find(".data-avg-fee").text(item.count > 0 ? item.totalFees / item.count : "-");
row.find(".data-fee-rate").text("-");
if (item.count > 0) {
updateCurrencyValue(row.find(".data-total-fees"), item.totalFees);
updateCurrencyValue(row.find(".data-avg-fee"), item.totalFees / item.count);
updateFeeRateValue(row.find(".data-fee-rate"), item.totalFees / item.totalBytes, 2);
}
row.show();
$("#fee-rate-table-body").append(row);
}
$("#main-content").show();
$("#progress-wrapper").hide();
};
getTxData(results, chunkStrs, 0, statusCallback, finishedCallback);
}
function getTxData(results, chunkStrs, chunkIndex, statusCallback, finishedCallback) {
if (chunkIndex > chunkStrs.length - 1) {
finishedCallback();
return;
}
var url = `/api/mempool-txs/${chunkStrs[chunkIndex]}`;
//console.log(url);
$.ajax({
url: url
}).done(function(result) {
for (var i = 0; i < result.length; i++) {
results.push(result[i]);
}
statusCallback(chunkIndex, chunkStrs.length);
getTxData(results, chunkStrs, chunkIndex + 1, statusCallback, finishedCallback);
});
}
function buildFeeRateGraphData(summary) {
var feeBucketLabels = [("[0 - " + summary["satoshiPerByteBucketMaxima"][0] + ")")];
for (var i = 0; i < summary["satoshiPerByteBuckets"].length; i++) {
var item = summary["satoshiPerByteBuckets"][i];
if (i > 0 && i < summary["satoshiPerByteBuckets"].length - 1) {
feeBucketLabels.push(("[" + summary["satoshiPerByteBucketMaxima"][i - 1] + " - " + summary["satoshiPerByteBucketMaxima"][i] + ")"));
}
}
feeBucketLabels.push((summary.satoshiPerByteBucketMaxima[summary.satoshiPerByteBucketMaxima.length - 1] + "+"));
var feeBucketTxCounts = summary["satoshiPerByteBucketCounts"];
var totalfeeBuckets = summary["satoshiPerByteBucketTotalFees"];
var graphData = {feeBucketLabels:[], bgColors:[], feeBucketTxCounts:feeBucketTxCounts};
for (var i = 0; i < feeBucketLabels.length; i++) {
var feeBucketLabel = feeBucketLabels[i];
var percentTx = Math.round(100 * feeBucketTxCounts[i] / summary.count).toLocaleString();
graphData.feeBucketLabels.push([feeBucketLabel, `${feeBucketTxCounts[i]} tx (${percentTx}%)`]);
graphData.bgColors.push(`hsl(${(333 * i / feeBucketLabels.length)}, 100%, 50%)`);
}
return graphData;
}
function buildTxSizeGraphData(summary) {
var sizeBucketLabels = [];
var bgColors = [];
for (var i = 0; i < summary.sizeBucketLabels.length; i++) {
var sizeBucketLabel = summary.sizeBucketLabels[i];
var percentTx = Math.round(100 * summary.sizeBucketTxCounts[i] / summary.count).toLocaleString();
sizeBucketLabels.push([`${sizeBucketLabel} bytes`, `${summary.sizeBucketTxCounts[i]} tx (${percentTx}%)`]);
bgColors.push(`hsl(${(333 * i / summary.sizeBucketLabels.length)}, 100%, 50%)`);
}
return {
sizeBucketLabels: sizeBucketLabels,
bgColors: bgColors,
sizeBucketTxCounts: summary.sizeBucketTxCounts
};
}
function buildTxAgeGraphData(summary) {
var ageBucketLabels = [];
var bgColors = [];
for (var i = 0; i < summary.ageBucketLabels.length; i++) {
var ageBucketLabel = summary.ageBucketLabels[i];
var percentTx = Math.round(100 * summary.ageBucketTxCounts[i] / summary.count).toLocaleString();
ageBucketLabels.push([`${ageBucketLabel}`, `${summary.ageBucketTxCounts[i]} tx (${percentTx}%)`]);
bgColors.push(`hsl(${(333 * i / summary.ageBucketLabels.length)}, 100%, 50%)`);
}
return {
ageBucketLabels: ageBucketLabels,
bgColors: bgColors,
ageBucketTxCounts: summary.ageBucketTxCounts
};
}
function summarizeData(rawdata) {
var summary = [];
var maxFee = 0;
var maxFeePerByte = 0;
var maxAge = 0;
var maxSize = 0;
var ages = [];
var sizes = [];
for (var i = 0; i < rawdata.length; i++) {
var txMempoolInfo = rawdata[i].entry;
var fee = txMempoolInfo.modifiedfee;
var size = txMempoolInfo.vsize ? txMempoolInfo.vsize : txMempoolInfo.size;
var feePerByte = txMempoolInfo.modifiedfee / size;
var age = Date.now() / 1000 - txMempoolInfo.time;
if (fee > maxFee) {
maxFee = txMempoolInfo.modifiedfee;
}
if (feePerByte > maxFeePerByte) {
maxFeePerByte = txMempoolInfo.modifiedfee / size;
}
ages.push({age:age, txid:"abc"});
sizes.push({size:size, txid:"abc"});
if (age > maxAge) {
maxAge = age;
}
if (size > maxSize) {
maxSize = size;
}
}
ages.sort(function(a, b) {
if (a.age != b.age) {
return b.age - a.age;
} else {
return a.txid.localeCompare(b.txid);
}
});
sizes.sort(function(a, b) {
if (a.size != b.size) {
return b.size - a.size;
} else {
return a.txid.localeCompare(b.txid);
}
});
maxSize = 2000;
var bucketCount = satoshiPerByteBucketMaxima.length + 1;
var satoshiPerByteBuckets = [];
var satoshiPerByteBucketLabels = [];
satoshiPerByteBucketLabels[0] = ("[0 - " + satoshiPerByteBucketMaxima[0] + ")");
for (var i = 0; i < bucketCount; i++) {
satoshiPerByteBuckets[i] = {"count":0, "totalFees":0, "totalBytes":0};
if (i > 0 && i < bucketCount - 1) {
satoshiPerByteBucketLabels[i] = ("[" + satoshiPerByteBucketMaxima[i - 1] + " - " + satoshiPerByteBucketMaxima[i] + ")");
}
}
var ageBucketCount = 150;
var ageBucketTxCounts = [];
var ageBucketLabels = [];
var sizeBucketCount = 150;
var sizeBucketTxCounts = [];
var sizeBucketLabels = [];
for (var i = 0; i < ageBucketCount; i++) {
var rangeMin = i * maxAge / ageBucketCount;
var rangeMax = (i + 1) * maxAge / ageBucketCount;
ageBucketTxCounts.push(0);
if (maxAge > 600) {
var rangeMinutesMin = new Decimal(rangeMin / 60).toFixed(1);
var rangeMinutesMax = new Decimal(rangeMax / 60).toFixed(1);
ageBucketLabels.push(rangeMinutesMin + " - " + rangeMinutesMax + " min");
} else {
ageBucketLabels.push(parseInt(rangeMin) + " - " + parseInt(rangeMax) + " sec");
}
}
for (var i = 0; i < sizeBucketCount; i++) {
sizeBucketTxCounts.push(0);
if (i == sizeBucketCount - 1) {
sizeBucketLabels.push(parseInt(i * maxSize / sizeBucketCount) + "+");
} else {
sizeBucketLabels.push(parseInt(i * maxSize / sizeBucketCount) + " - " + parseInt((i + 1) * maxSize / sizeBucketCount));
}
}
satoshiPerByteBucketLabels[bucketCount - 1] = (satoshiPerByteBucketMaxima[satoshiPerByteBucketMaxima.length - 1] + "+");
var summary = {
"count":0,
"totalFees":0,
"totalBytes":0,
"satoshiPerByteBuckets":satoshiPerByteBuckets,
"satoshiPerByteBucketLabels":satoshiPerByteBucketLabels,
"ageBucketTxCounts":ageBucketTxCounts,
"ageBucketLabels":ageBucketLabels,
"sizeBucketTxCounts":sizeBucketTxCounts,
"sizeBucketLabels":sizeBucketLabels
};
for (var x = 0; x < rawdata.length; x++) {
var txMempoolInfo = rawdata[x].entry;
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 age = Date.now() / 1000 - txMempoolInfo.time;
var addedToBucket = false;
for (var i = 0; i < satoshiPerByteBucketMaxima.length; i++) {
if (satoshiPerByteBucketMaxima[i] > satoshiPerByte) {
satoshiPerByteBuckets[i]["count"]++;
satoshiPerByteBuckets[i]["totalFees"] += fee;
satoshiPerByteBuckets[i]["totalBytes"] += size;
addedToBucket = true;
break;
}
}
if (!addedToBucket) {
satoshiPerByteBuckets[bucketCount - 1]["count"]++;
satoshiPerByteBuckets[bucketCount - 1]["totalFees"] += fee;
satoshiPerByteBuckets[bucketCount - 1]["totalBytes"] += size;
}
summary["count"]++;
summary["totalFees"] += txMempoolInfo.modifiedfee;
summary["totalBytes"] += size;
var ageBucketIndex = Math.min(ageBucketCount - 1, parseInt(age / (maxAge / ageBucketCount)));
var sizeBucketIndex = Math.min(sizeBucketCount - 1, parseInt(size / (maxSize / sizeBucketCount)));
ageBucketTxCounts[ageBucketIndex]++;
sizeBucketTxCounts[sizeBucketIndex]++;
}
summary["averageFee"] = summary["totalFees"] / summary["count"];
summary["averageFeePerByte"] = summary["totalFees"] / summary["totalBytes"];
summary["satoshiPerByteBucketMaxima"] = satoshiPerByteBucketMaxima;
summary["satoshiPerByteBucketCounts"] = [];
summary["satoshiPerByteBucketTotalFees"] = [];
for (var i = 0; i < bucketCount; i++) {
summary["satoshiPerByteBucketCounts"].push(summary["satoshiPerByteBuckets"][i]["count"]);
summary["satoshiPerByteBucketTotalFees"].push(summary["satoshiPerByteBuckets"][i]["totalFees"]);
}
return summary;
}