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
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(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
|
|
|
|
table.table.details-table.mb-0
|
|
tr
|
|
td.properties-header Transaction Count
|
|
td.text-monospace(id="tx-count")
|
|
|
|
tr
|
|
td.properties-header Memory Usage
|
|
td.text-monospace(id="mem-usage")
|
|
|
|
tr
|
|
td.properties-header Total Fees
|
|
td.text-monospace(id="total-fees")
|
|
|
|
tr
|
|
td.properties-header Avg Fee
|
|
td.text-monospace(id="avg-fee")
|
|
|
|
tr
|
|
td.properties-header Avg Fee Rate
|
|
td.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;
|
|
}
|