btc-rpc-explorer
Version:
Open-source, self-hosted Bitcoin explorer
1,861 lines (1,382 loc) • 61.4 kB
JavaScript
"use strict";
const debug = require("debug");
const debugLog = debug("btcexp:core");
const fs = require('fs');
const utils = require("../utils.js");
const redisCache = require("../redisCache.js");
const cacheUtils = require("../cacheUtils.js");
const config = require("../config.js");
const coins = require("../coins.js");
const Decimal = require("decimal.js");
const md5 = require("md5");
const statTracker = require("../statTracker.js");
const async = require("async");
// choose one of the below: RPC to a node, or mock data while testing
const rpcApi = require("./rpcApi.js");
//const rpcApi = require("./mockApi.js");
// this value should be incremented whenever data format changes, to avoid
// pulling old-format data from a persistent cache
const cacheKeyVersion = "v1";
const ONE_SEC = 1000;
const ONE_MIN = 60 * ONE_SEC;
const ONE_HR = 60 * ONE_MIN;
const FIFTEEN_MIN = 15 * ONE_MIN;
const ONE_DAY = 24 * ONE_HR;
const ONE_YR = 365 * ONE_DAY;
const SECONDS_PER_MIN = 60;
const SECONDS_PER_HOUR = SECONDS_PER_MIN * 60;
const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
const miscCaches = [];
const blockCaches = [];
const txCaches = [];
const miningSummaryCaches = [];
global.miscLruCache = cacheUtils.lruCache(config.slowDeviceMode ? 200 : 1000);
global.blockLruCache = cacheUtils.lruCache(config.slowDeviceMode ? 200 : 1000);
global.txLruCache = cacheUtils.lruCache(config.slowDeviceMode ? 200 : 1000);
global.miningSummaryLruCache = cacheUtils.lruCache(config.slowDeviceMode ? 500 : 4500);
global.lruCaches = [ global.miscLruCache, global.blockLruCache, global.txLruCache, global.miningSummaryLruCache ];
(function () {
const pruneCaches = () => {
let totalSizeBefore = 0;
global.lruCaches.forEach(x => (totalSizeBefore += x.size));
global.lruCaches.forEach(x => x.purgeStale());
let totalSizeAfter = 0;
global.lruCaches.forEach(x => (totalSizeAfter += x.size));
statTracker.trackEvent("caches.pruned-items", (totalSizeBefore - totalSizeAfter));
statTracker.trackValue("caches.misc.size", global.miscLruCache.size);
statTracker.trackValue("caches.misc.itemCount", global.miscLruCache.itemCount);
statTracker.trackValue("caches.block.size", global.blockLruCache.size);
statTracker.trackValue("caches.block.itemCount", global.blockLruCache.itemCount);
statTracker.trackValue("caches.tx.size", global.txLruCache.size);
statTracker.trackValue("caches.tx.itemCount", global.txLruCache.itemCount);
statTracker.trackValue("caches.mining.size", global.miningSummaryLruCache.size);
statTracker.trackValue("caches.mining.itemCount", global.miningSummaryLruCache.itemCount);
debugLog(`Pruned caches: ${totalSizeBefore.toLocaleString()} -> ${totalSizeAfter.toLocaleString()}`);
};
setInterval(pruneCaches, 60000);
})();
if (!config.noInmemoryRpcCache) {
global.cacheStats.memory = {
try: 0,
hit: 0,
miss: 0,
error: 0
};
const onMemoryCacheEvent = function(cacheType, eventType, cacheKey) {
global.cacheStats.memory[eventType]++;
statTracker.trackEvent(`caches.memory.${eventType}`);
//debugLog(`cache.${cacheType}.${eventType}: ${cacheKey}`);
}
miscCaches.push(cacheUtils.createMemoryLruCache("misc", global.miscLruCache, onMemoryCacheEvent));
blockCaches.push(cacheUtils.createMemoryLruCache("block", global.blockLruCache, onMemoryCacheEvent));
txCaches.push(cacheUtils.createMemoryLruCache("tx", global.txLruCache, onMemoryCacheEvent));
miningSummaryCaches.push(cacheUtils.createMemoryLruCache("mining", global.miningSummaryLruCache, onMemoryCacheEvent));
}
if (redisCache.active) {
global.cacheStats.redis = {
try: 0,
hit: 0,
miss: 0,
error: 0
};
const onRedisCacheEvent = function(cacheType, eventType, cacheKey) {
global.cacheStats.redis[eventType]++;
statTracker.trackEvent(`caches.redis.${eventType}`);
//debugLog(`cache.${cacheType}.${eventType}: ${cacheKey}`);
}
// md5 of the active RPC credentials serves as part of the key; this enables
// multiple instances of btc-rpc-explorer (eg mainnet + testnet) to share
// a single redis instance peacefully
const rpcHostPort = `${config.credentials.rpc.host}:${config.credentials.rpc.port}`;
const rpcCredKeyComponent = md5(JSON.stringify(config.credentials.rpc)).substring(0, 8);
const redisCacheObj = redisCache.createCache(`${cacheKeyVersion}-${rpcCredKeyComponent}`, onRedisCacheEvent);
miscCaches.push(redisCacheObj);
blockCaches.push(redisCacheObj);
txCaches.push(redisCacheObj);
miningSummaryCaches.push(redisCacheObj);
}
const miscCache = cacheUtils.createTieredCache(miscCaches);
const blockCache = cacheUtils.createTieredCache(blockCaches);
const txCache = cacheUtils.createTieredCache(txCaches);
const miningSummaryCache = cacheUtils.createTieredCache(miningSummaryCaches);
function getGenesisBlockHash() {
return coins[config.coin].genesisBlockHashesByNetwork[global.activeBlockchain];
}
function getGenesisCoinbaseTransactionId() {
return coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain];
}
function tryCacheThenRpcApi(cache, cacheKey, cacheMaxAge, rpcApiFunction, cacheConditionFunction) {
//debugLog("tryCache: " + versionedCacheKey + ", " + cacheMaxAge);
if (cacheConditionFunction == null) {
cacheConditionFunction = function(obj) {
return true;
};
}
return new Promise(function(resolve, reject) {
let cacheResult = null;
let finallyFunc = function() {
if (cacheResult != null) {
resolve(cacheResult);
} else {
rpcApiFunction().then(function(rpcResult) {
if (rpcResult != null && cacheConditionFunction(rpcResult)) {
cache.set(cacheKey, rpcResult, cacheMaxAge);
}
resolve(rpcResult);
}).catch(function(err) {
reject(err);
});
}
};
cache.get(cacheKey).then(function(result) {
cacheResult = result;
try {
finallyFunc();
} catch (e) {
utils.logError("823hredhee", e);
reject(e);
}
}).catch(function(err) {
utils.logError("nds9fc2eg621tf3", err, {cacheKey:cacheKey});
finallyFunc();
});
});
}
function shouldCacheTransaction(tx) {
if (!tx.confirmations) {
return false;
}
if (tx.confirmations < 1) {
return false;
}
if (tx.vin != null && tx.vin.length > 5) {
return false;
}
if (tx.vout != null && tx.vout.length > 5) {
return false;
}
return true;
}
function getBlockchainInfo() {
return tryCacheThenRpcApi(miscCache, "getBlockchainInfo", 10 * ONE_SEC, rpcApi.getBlockchainInfo);
}
function getDeploymentInfo() {
return tryCacheThenRpcApi(miscCache, "getDeploymentInfo", 10 * ONE_SEC, rpcApi.getDeploymentInfo);
}
function getNetworkInfo() {
return tryCacheThenRpcApi(miscCache, "getNetworkInfo", 10 * ONE_SEC, rpcApi.getNetworkInfo);
}
function getNetTotals() {
return tryCacheThenRpcApi(miscCache, "getNetTotals", 10 * ONE_SEC, rpcApi.getNetTotals);
}
function getMempoolInfo() {
return tryCacheThenRpcApi(miscCache, "getMempoolInfo", 5 * ONE_SEC, rpcApi.getMempoolInfo);
}
function getIndexInfo() {
return tryCacheThenRpcApi(miscCache, "getIndexInfo", 10 * ONE_SEC, rpcApi.getIndexInfo);
}
function getAllMempoolTxids() {
// no caching, that would be dumb
return rpcApi.getAllMempoolTxids();
}
function getMiningInfo() {
return tryCacheThenRpcApi(miscCache, "getMiningInfo", 30 * ONE_SEC, rpcApi.getMiningInfo);
}
function getUptimeSeconds() {
return tryCacheThenRpcApi(miscCache, "getUptimeSeconds", ONE_SEC, rpcApi.getUptimeSeconds);
}
function getChainTxStats(blockCount, blockhashEnd) {
return tryCacheThenRpcApi(miscCache, "getChainTxStats-" + blockCount + "-" + blockhashEnd, FIFTEEN_MIN, function() {
return rpcApi.getChainTxStats(blockCount, blockhashEnd);
});
}
function getNetworkHashrate(blockCount) {
return tryCacheThenRpcApi(miscCache, "getNetworkHashrate-" + blockCount, FIFTEEN_MIN, function() {
return rpcApi.getNetworkHashrate(blockCount);
});
}
function getBlockStats(hash) {
return tryCacheThenRpcApi(miscCache, "getBlockStats-" + hash, FIFTEEN_MIN, function() {
return rpcApi.getBlockStats(hash);
});
}
function getBlockStatsByHeight(height) {
return tryCacheThenRpcApi(miscCache, "getBlockStatsByHeight-" + height, FIFTEEN_MIN, function() {
return rpcApi.getBlockStatsByHeight(height);
});
}
const utxoSetFileCache = utils.fileCache(config.filesystemCacheDir, `utxo-set`);
function getUtxoSetSummary(useCoinStatsIndexIfAvailable=true, useCacheIfAvailable=true) {
return tryCacheThenRpcApi(miscCache, "getUtxoSetSummary", FIFTEEN_MIN, async () => {
let utxoSetSummary = utxoSetFileCache.tryLoadJson();
if (utxoSetSummary && useCacheIfAvailable) {
return utxoSetSummary;
} else {
utxoSetSummary = await rpcApi.getUtxoSetSummary(useCoinStatsIndexIfAvailable);
if (utxoSetSummary && utxoSetSummary.total_amount) {
if (useCoinStatsIndexIfAvailable && global.getindexinfo && global.getindexinfo.coinstatsindex) {
utxoSetSummary.usingCoinStatsIndex = true;
} else {
utxoSetSummary.usingCoinStatsIndex = false;
}
utxoSetSummary.lastUpdated = Date.now();
try {
utxoSetFileCache.writeJson(utxoSetSummary);
} catch (e) {
utils.logError("h32uheifehues", e);
}
return utxoSetSummary;
} else {
return null;
}
}
});
}
async function getNextBlockEstimate() {
const blockTemplate = await getBlockTemplate();
let minFeeRate = 1000000;
let maxFeeRate = 0;
let minFeeTxid = null;
let maxFeeTxid = null;
let medianFeeRate = 0;
let parentTxIndexes = new Set();
let templateWeight = 0;
blockTemplate.transactions.forEach(tx => {
templateWeight += tx.weight;
if (tx.depends && tx.depends.length > 0) {
tx.depends.forEach(index => {
parentTxIndexes.add(index);
});
}
});
let txIndex = 1;
let feeRates = [];
blockTemplate.transactions.forEach(tx => {
let feeRate = tx.fee / tx.weight * 4;
if (tx.depends && tx.depends.length > 0) {
let totalFee = tx.fee;
let totalWeight = tx.weight;
tx.depends.forEach(index => {
totalFee += blockTemplate.transactions[index - 1].fee;
totalWeight += blockTemplate.transactions[index - 1].weight;
});
tx.avgFeeRate = totalFee / totalWeight * 4;
}
// txs that are ancestors should not be included in min/max
// calculations since their native fee rate is different than
// their effective fee rate (which takes descendant fee rates
// into account)
if (!parentTxIndexes.has(txIndex) && (!tx.depends || tx.depends.length == 0)) {
feeRates.push(feeRate);
if (feeRate < minFeeRate) {
minFeeRate = feeRate;
minFeeTxid = tx.txid;
}
if (feeRate > maxFeeRate) {
maxFeeRate = feeRate;
maxFeeTxid = tx.txid;
}
}
txIndex++;
});
if (feeRates.length > 0) {
medianFeeRate = feeRates[Math.floor(feeRates.length / 2)];
}
const feeRateGroups = [];
let groupCount = 10;
for (let i = 0; i < groupCount; i++) {
feeRateGroups.push({
minFeeRate: minFeeRate + i * (maxFeeRate - minFeeRate) / groupCount,
maxFeeRate: minFeeRate + (i + 1) * (maxFeeRate - minFeeRate) / groupCount,
totalWeight: 0,
txidCount: 0,
//txids: []
});
}
let txIncluded = 0;
blockTemplate.transactions.forEach(tx => {
let feeRate = tx.avgFeeRate ? tx.avgFeeRate : (tx.fee / tx.weight * 4);
for (let i = 0; i < feeRateGroups.length; i++) {
if (feeRate >= feeRateGroups[i].minFeeRate) {
if (feeRate < feeRateGroups[i].maxFeeRate) {
feeRateGroups[i].totalWeight += tx.weight;
feeRateGroups[i].txidCount++;
//res.locals.nextBlockFeeRateGroups[i].txids.push(tx.txid);
txIncluded++;
break;
}
}
}
});
feeRateGroups.forEach(group => {
group.weightRatio = group.totalWeight / blockTemplate.weightlimit;
});
const subsidy = coinConfig.blockRewardFunction(blockTemplate.height, global.activeBlockchain);
const totalFees = new Decimal(blockTemplate.coinbasevalue).dividedBy(SATS_PER_BTC).minus(new Decimal(subsidy));
return {
blockTemplate: blockTemplate,
weight: templateWeight,
feeRateGroups: feeRateGroups,
totalFees: totalFees,
minFeeRate: minFeeRate,
maxFeeRate: maxFeeRate,
medianFeeRate: medianFeeRate,
minFeeTxid: minFeeTxid,
maxFeeTxid: maxFeeTxid
};
}
function getBlockTemplate() {
return tryCacheThenRpcApi(miscCache, "getblocktemplate", 5 * ONE_SEC, rpcApi.getBlockTemplate);
}
const difficultyFileCache = utils.fileCache(config.filesystemCacheDir, `difficulty-by-blockheight`, 2);
global.difficultyByBlockheightCache = difficultyFileCache.tryLoadJson() || {};
global.difficultyByBlockheightCacheDirty = false;
(function () {
const writeDifficultyCache = () => {
if (global.difficultyByBlockheightCacheDirty) {
difficultyFileCache.writeJson(global.difficultyByBlockheightCache);
}
};
setInterval(writeDifficultyCache, 60000);
})();
async function getDifficultyByBlockHeights(blockHeights) {
const results = {};
const neededBlockHeights = [];
for (let i = 0; i < blockHeights.length; i++) {
let blockHeight = blockHeights[i];
let blockHeightStr = `${blockHeight}`;
if (global.difficultyByBlockheightCache[blockHeightStr]) {
results[blockHeight] = global.difficultyByBlockheightCache[blockHeightStr];
} else {
neededBlockHeights.push(blockHeight);
}
}
const blockHeaders = await getBlockHeadersByHeight(neededBlockHeights);
blockHeaders.forEach(header => {
global.difficultyByBlockheightCache[`${header.height}`] = {
difficulty: header.difficulty,
time: header.time
};
global.difficultyByBlockheightCacheDirty = true;
results[header.height] = {
difficulty: header.difficulty,
time: header.time
};
});
return results;
}
async function getTxStats(dataPtCount, blockStart, blockEnd) {
let cacheKey = `txStats-${dataPtCount}-${blockStart}-${blockEnd}`;
let cacheResult = await miscCache.get(cacheKey);
if (cacheResult) {
return cacheResult;
}
let getblockchaininfo = await getBlockchainInfo();
if (typeof blockStart === "string") {
if (["genesis", "first", "zero"].includes(blockStart)) {
blockStart = 0;
}
}
if (typeof blockEnd === "string") {
if (["latest", "tip", "newest"].includes(blockEnd)) {
blockEnd = getblockchaininfo.blocks;
}
}
if (blockStart > blockEnd) {
throw new Error(`Error 37rhw0e7ufdsgf: blockStart (${blockStart}) > blockEnd (${blockEnd})`);
}
if (blockStart < 0) {
blockStart += getblockchaininfo.blocks;
}
if (blockEnd < 0) {
blockEnd += getblockchaininfo.blocks;
}
const promises = [];
const blockCount = Math.floor((blockEnd - blockStart) / dataPtCount) || 1;
if (dataPtCount > (blockEnd - blockStart)) {
dataPtCount = (blockEnd - blockStart);
}
for (let i = 0; i < dataPtCount; i++) {
let blockHeightEnd = blockStart + (i + 1) * blockCount;
promises.push((async () => {
let blockhashEnd = await getBlockHashByHeight(blockHeightEnd);
// Math.min below is to handle the edge case where we're starting from genesis block, which doesn't behave the same as others
return await rpcApi.getChainTxStats(Math.min(blockCount, blockHeightEnd - 1), blockhashEnd);
})());
}
const results = await Promise.all(promises);
//console.log(results);
if (results.length == 0 || (results[0].name == "RpcError" && results[0].code == -8)) {
// recently started node - no meaningful data to return
return null;
}
let summary = {
txCounts: [],
txLabels: [],
txRates: [],
timespans: [],
blocksPerPoint: blockCount
};
let totalTimespan = 0;
for (let i = results.length - 1; i >= 0; i--) {
if (results[i].window_tx_count) {
summary.txCounts.push( {x:(blockStart + i * blockCount), y: results[i].window_tx_count} );
summary.txRates.push( {x:(blockStart + i * blockCount), y: results[i].txrate} );
summary.timespans.push( {x:(blockStart + i * blockCount), y: results[i].window_interval});
summary.txLabels.push(i);
totalTimespan += results[i].window_interval;
}
}
summary.avgTimespan = (totalTimespan / results.length);
miscCache.set(cacheKey, summary, 60 * ONE_MIN);
//console.log(summary);
return summary;
}
function getSmartFeeEstimates(mode, confTargetBlockCounts) {
return new Promise(function(resolve, reject) {
let promises = [];
for (let i = 0; i < confTargetBlockCounts.length; i++) {
promises.push(getSmartFeeEstimate(mode, confTargetBlockCounts[i]));
}
Promise.all(promises).then(function(results) {
resolve(results);
}).catch(function(err) {
reject(err);
});
});
}
function getSmartFeeEstimate(mode, confTargetBlockCount) {
return tryCacheThenRpcApi(miscCache, "getSmartFeeEstimate-" + mode + "-" + confTargetBlockCount, 5 * ONE_MIN, function() {
return rpcApi.getSmartFeeEstimate(mode, confTargetBlockCount);
});
}
function getPeerSummary() {
return new Promise(function(resolve, reject) {
tryCacheThenRpcApi(miscCache, "getpeerinfo", ONE_SEC, rpcApi.getPeerInfo).then(function(getpeerinfo) {
let result = {};
result.getpeerinfo = getpeerinfo;
let versionSummaryMap = {};
for (let i = 0; i < getpeerinfo.length; i++) {
let x = getpeerinfo[i];
if (versionSummaryMap[x.subver] == null) {
versionSummaryMap[x.subver] = 0;
}
versionSummaryMap[x.subver]++;
}
let versionSummary = [];
for (let prop in versionSummaryMap) {
if (versionSummaryMap.hasOwnProperty(prop)) {
versionSummary.push([prop, versionSummaryMap[prop]]);
}
}
versionSummary.sort(function(a, b) {
if (b[1] > a[1]) {
return 1;
} else if (b[1] < a[1]) {
return -1;
} else {
return a[0].localeCompare(b[0]);
}
});
let serviceNamesAvailable = false;
let servicesSummaryMap = {};
for (let i = 0; i < getpeerinfo.length; i++) {
let x = getpeerinfo[i];
if (x.servicesnames) {
serviceNamesAvailable = true;
x.servicesnames.forEach(name => {
if (servicesSummaryMap[name] == null) {
servicesSummaryMap[name] = 0;
}
servicesSummaryMap[name]++;
});
} else {
if (servicesSummaryMap[x.services] == null) {
servicesSummaryMap[x.services] = 0;
}
servicesSummaryMap[x.services]++;
}
}
let servicesSummary = [];
for (let prop in servicesSummaryMap) {
if (servicesSummaryMap.hasOwnProperty(prop)) {
servicesSummary.push([prop, servicesSummaryMap[prop]]);
}
}
servicesSummary.sort(function(a, b) {
if (b[1] > a[1]) {
return 1;
} else if (b[1] < a[1]) {
return -1;
} else {
return a[0].localeCompare(b[0]);
}
});
if (getpeerinfo.length > 0 && getpeerinfo[0].connection_type) {
let connectionTypeSummaryMap = {};
for (let i = 0; i < getpeerinfo.length; i++) {
let x = getpeerinfo[i];
if (connectionTypeSummaryMap[x.connection_type] == null) {
connectionTypeSummaryMap[x.connection_type] = 0;
}
connectionTypeSummaryMap[x.connection_type]++;
}
let connectionTypeSummary = [];
for (let prop in connectionTypeSummaryMap) {
if (connectionTypeSummaryMap.hasOwnProperty(prop)) {
connectionTypeSummary.push([prop, connectionTypeSummaryMap[prop]]);
}
}
connectionTypeSummary.sort(function(a, b) {
if (b[1] > a[1]) {
return 1;
} else if (b[1] < a[1]) {
return -1;
} else {
return a[0].localeCompare(b[0]);
}
});
result.connectionTypeSummary = connectionTypeSummary;
}
if (getpeerinfo.length > 0 && getpeerinfo[0].network) {
let networkTypeSummaryMap = {};
for (let i = 0; i < getpeerinfo.length; i++) {
let x = getpeerinfo[i];
if (networkTypeSummaryMap[x.network] == null) {
networkTypeSummaryMap[x.network] = 0;
}
networkTypeSummaryMap[x.network]++;
}
let networkTypeSummary = [];
for (let prop in networkTypeSummaryMap) {
if (networkTypeSummaryMap.hasOwnProperty(prop)) {
networkTypeSummary.push([prop, networkTypeSummaryMap[prop]]);
}
}
networkTypeSummary.sort(function(a, b) {
if (b[1] > a[1]) {
return 1;
} else if (b[1] < a[1]) {
return -1;
} else {
return a[0].localeCompare(b[0]);
}
});
result.networkTypeSummary = networkTypeSummary;
}
result.versionSummary = versionSummary;
result.servicesSummary = servicesSummary;
result.serviceNamesAvailable = serviceNamesAvailable;
resolve(result);
}).catch(function(err) {
reject(err);
});
});
}
function getMempoolTxids(limit, offset) {
return new Promise(function(resolve, reject) {
tryCacheThenRpcApi(miscCache, "getMempoolTxids", ONE_SEC, rpcApi.getAllMempoolTxids).then(function(resultTxids) {
let txids = [];
for (let i = offset; (i < resultTxids.length && i < (offset + limit)); i++) {
txids.push(resultTxids[i]);
}
resolve({ txCount:resultTxids.length, txids:txids });
}).catch(function(err) {
reject(err);
});
});
}
function getBlockByHeight(blockHeight) {
return tryCacheThenRpcApi(blockCache, "getBlockByHeight-" + blockHeight, FIFTEEN_MIN, function() {
return rpcApi.getBlockByHeight(blockHeight);
});
}
function getBlockHashByHeight(blockHeight) {
return tryCacheThenRpcApi(blockCache, "getBlockHashByHeight-" + blockHeight, ONE_HR, function() {
return rpcApi.getBlockHashByHeight(blockHeight);
});
}
function getBlocksByHeight(blockHeights) {
return new Promise(function(resolve, reject) {
let promises = [];
for (let i = 0; i < blockHeights.length; i++) {
promises.push(getBlockByHeight(blockHeights[i]));
}
Promise.all(promises).then(function(results) {
resolve(results);
}).catch(function(err) {
reject(err);
});
});
}
function getBlockHeaderByHash(hash) {
return tryCacheThenRpcApi(blockCache, "getBlockHeaderByHash-" + hash, FIFTEEN_MIN, function() {
return rpcApi.getBlockHeaderByHash(hash);
});
}
function getBlockHeaderByHeight(blockHeight) {
return tryCacheThenRpcApi(blockCache, "getBlockHeaderByHeight-" + blockHeight, FIFTEEN_MIN, function() {
return rpcApi.getBlockHeaderByHeight(blockHeight);
});
}
function getBlockHeadersByHeight(blockHeights) {
return new Promise(function(resolve, reject) {
let promises = [];
for (let i = 0; i < blockHeights.length; i++) {
promises.push(getBlockHeaderByHeight(blockHeights[i]));
}
Promise.all(promises).then(function(results) {
resolve(results);
}).catch(function(err) {
reject(err);
});
});
}
function getBlocksStatsByHeight(blockHeights) {
return new Promise(function(resolve, reject) {
let promises = [];
for (let i = 0; i < blockHeights.length; i++) {
promises.push(getBlockStatsByHeight(blockHeights[i]));
}
Promise.all(promises).then(function(results) {
resolve(results);
}).catch(function(err) {
reject(err);
});
});
}
function getBlockByHash(blockHash) {
return tryCacheThenRpcApi(blockCache, "getBlockByHash-" + blockHash, FIFTEEN_MIN, function() {
return rpcApi.getBlockByHash(blockHash);
});
}
function getBlocksByHash(blockHashes) {
return new Promise(function(resolve, reject) {
let promises = [];
for (let i = 0; i < blockHashes.length; i++) {
promises.push(getBlockByHash(blockHashes[i]));
}
Promise.all(promises).then(function(results) {
let result = {};
results.forEach(function(item) {
result[item.hash] = item;
});
resolve(result);
}).catch(function(err) {
reject(err);
});
});
}
function getRawTransaction(txid, blockhash) {
let rpcApiFunction = function() {
return rpcApi.getRawTransaction(txid, blockhash);
};
return tryCacheThenRpcApi(txCache, "getRawTransaction-" + txid, FIFTEEN_MIN, rpcApiFunction, shouldCacheTransaction);
}
/*
This function pulls raw tx data and then summarizes the outputs. It's used in memory-constrained situations.
*/
function getSummarizedTransactionOutput(txid, voutIndex) {
let rpcApiFunction = function() {
return new Promise(function(resolve, reject) {
rpcApi.getRawTransaction(txid).then(function(rawTx) {
let vout = rawTx.vout[voutIndex];
if (vout.scriptPubKey) {
if (vout.scriptPubKey.asm) {
delete vout.scriptPubKey.asm;
}
if (vout.scriptPubKey.hex) {
delete vout.scriptPubKey.hex;
}
}
vout.txid = txid;
vout.utxoTime = rawTx.time;
if (rawTx.vin.length == 1 && rawTx.vin[0].coinbase) {
vout.coinbaseSpend = true;
}
resolve(vout);
}).catch(function(err) {
reject(err);
});
});
};
return tryCacheThenRpcApi(txCache, `txoSummary-${txid}-${voutIndex}`, FIFTEEN_MIN, rpcApiFunction, function() { return true; });
}
async function getTxUtxos(tx) {
const promises = [];
for (let i = 0; i < tx.vout.length; i++) {
promises.push(getUtxo(tx.txid, i));
}
return Promise.all(promises);
}
function getUtxo(txid, outputIndex) {
return new Promise(function(resolve, reject) {
tryCacheThenRpcApi(miscCache, "utxo-" + txid + "-" + outputIndex, FIFTEEN_MIN, function() {
return rpcApi.getUtxo(txid, outputIndex);
}).then(function(result) {
// to avoid cache misses, rpcApi.getUtxo returns "0" instead of null
if (typeof result == "string" && result == "0") {
resolve(null);
return;
}
resolve(result);
}).catch(function(err) {
reject(err);
});
});
}
function getMempoolTxDetails(txid, includeAncDec) {
return tryCacheThenRpcApi(miscCache, "mempoolTxDetails-" + txid + "-" + includeAncDec, FIFTEEN_MIN, function() {
return rpcApi.getMempoolTxDetails(txid, includeAncDec);
});
}
function getAddress(address) {
return tryCacheThenRpcApi(miscCache, "getAddress-" + address, FIFTEEN_MIN, function() {
return rpcApi.getAddress(address);
});
}
function getRawTransactions(txids, blockhash) {
return new Promise(function(resolve, reject) {
let promises = [];
for (let i = 0; i < txids.length; i++) {
promises.push(getRawTransaction(txids[i], blockhash));
}
Promise.all(promises).then(function(results) {
resolve(results);
}).catch(function(err) {
reject(err);
});
});
}
async function getRawTransactionsByHeights(txids, blockHeightsByTxid) {
return Promise.all(txids.map(async txid => {
let blockheight = blockHeightsByTxid[txid];
let blockhash = blockheight ? await getBlockByHeight(blockheight) : null;
return getRawTransaction(txid, blockhash);
}))
}
function buildBlockAnalysisData(blockHeight, blockHash, txids, txIndex, results, callback) {
if (txIndex >= txids.length) {
callback();
return;
}
let txid = txids[txIndex];
getRawTransactionsWithInputs([txid], -1, blockHash).then(function(txData) {
results.push(summarizeBlockAnalysisData(blockHeight, txData.transactions[0], txData.txInputsByTransaction[txid]));
buildBlockAnalysisData(blockHeight, blockHash, txids, txIndex + 1, results, callback);
});
}
function summarizeBlockAnalysisData(blockHeight, tx, inputs) {
let 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);
txSummary.totalDaysDestroyed = new Decimal(0);
if (txSummary.coinbase) {
let subsidy = global.coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain);
txSummary.totalInput = txSummary.totalInput.plus(new Decimal(subsidy));
txSummary.vin.push({
coinbase: true,
value: subsidy
});
} else {
for (let i = 0; i < tx.vin.length; i++) {
let vin = tx.vin[i];
let txSummaryVin = {
txid: tx.vin[i].txid,
vout: tx.vin[i].vout,
sequence: tx.vin[i].sequence
};
if (inputs) {
let inputVout = inputs[i];
txSummary.totalInput = txSummary.totalInput.plus(new Decimal(inputVout.value));
let timeDestroyed = tx.time - inputVout.utxoTime;
let daysDestroyed = timeDestroyed / SECONDS_PER_DAY;
txSummary.totalDaysDestroyed = txSummary.totalDaysDestroyed.plus(new Decimal(inputVout.value).times(daysDestroyed));
//console.log(`tx:id=${tx.txid}, tx.time=${tx.time}, inputVout.time=${inputVout.time}, input=${i}, TD=${timeDestroyed}, DD=${daysDestroyed}`);
//console.log(`inputVout: ${JSON.stringify(inputVout)}`);
txSummaryVin.value = inputVout.value;
txSummaryVin.type = inputVout.scriptPubKey.type;
txSummaryVin.reqSigs = inputVout.scriptPubKey.reqSigs;
txSummaryVin.addressCount = utils.getVoutAddresses(inputVout).length;
}
txSummary.vin.push(txSummaryVin);
}
}
txSummary.vout = [];
txSummary.totalOutput = new Decimal(0);
for (let 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: utils.getVoutAddresses(tx.vout[i]).length
});
}
if (txSummary.coinbase) {
txSummary.totalFee = new Decimal(0);
} else {
txSummary.totalFee = txSummary.totalInput.minus(txSummary.totalOutput);
}
return txSummary;
}
function getRawTransactionsWithInputs(txids, maxInputs=-1, blockhash) {
// Get just the transactions without their prevouts when txindex is disabled
if (!global.txindexAvailable) {
return getRawTransactions(txids, blockhash)
.then(transactions => ({ transactions, txInputsByTransaction: {} }))
}
return new Promise(function(resolve, reject) {
getRawTransactions(txids, blockhash).then(function(transactions) {
let maxInputsTracked = config.site.txMaxInput;
if (maxInputs <= 0) {
maxInputsTracked = 1000000;
} else if (maxInputs > 0) {
maxInputsTracked = maxInputs;
}
let vinIds = [];
for (let i = 0; i < transactions.length; i++) {
let transaction = transactions[i];
if (transaction && transaction.vin) {
for (let j = 0; j < Math.min(maxInputsTracked, transaction.vin.length); j++) {
if (transaction.vin[j].txid) {
vinIds.push({txid:transaction.vin[j].txid, voutIndex:transaction.vin[j].vout});
}
}
}
}
let promises = [];
for (let i = 0; i < vinIds.length; i++) {
let vinId = vinIds[i];
promises.push(getSummarizedTransactionOutput(vinId.txid, vinId.voutIndex));
}
Promise.all(promises).then(function(promiseResults) {
let summarizedTxOutputs = {};
for (let i = 0; i < promiseResults.length; i++) {
let summarizedTxOutput = promiseResults[i];
summarizedTxOutputs[`${summarizedTxOutput.txid}:${summarizedTxOutput.n}`] = summarizedTxOutput;
}
let txInputsByTransaction = {};
transactions.forEach(function(tx) {
txInputsByTransaction[tx.txid] = {};
if (tx && tx.vin) {
for (let i = 0; i < Math.min(maxInputsTracked, tx.vin.length); i++) {
let summarizedTxOutput = summarizedTxOutputs[`${tx.vin[i].txid}:${tx.vin[i].vout}`];
if (summarizedTxOutput) {
txInputsByTransaction[tx.txid][i] = summarizedTxOutput;
}
}
}
});
resolve({ transactions:transactions, txInputsByTransaction:txInputsByTransaction });
}).catch(reject);
}).catch(reject);
});
}
function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) {
return new Promise(function(resolve, reject) {
getBlockByHash(blockHash).then(function(block) {
let txids = [];
// to get miner info, always include the coinbase tx in the list
if (txOffset > 0) {
txids.push(block.tx[0]);
}
for (let i = txOffset; i < Math.min(txOffset + txLimit, block.tx.length); i++) {
txids.push(block.tx[i]);
}
getRawTransactionsWithInputs(txids, config.site.txMaxInput, blockHash).then(function(txsResult) {
if (txsResult.transactions && txsResult.transactions.length > 0) {
block.coinbaseTx = txsResult.transactions[0];
block.totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight(block.coinbaseTx, block.height);
block.miner = utils.identifyMiner(block.coinbaseTx, block.height);
}
// if we're on page 2+, drop the coinbase tx that was added in order to get miner info
if (txOffset > 0) {
txsResult.transactions.shift();
}
resolve({ getblock:block, transactions:txsResult.transactions, txInputsByTransaction:txsResult.txInputsByTransaction });
}).catch(function(err) {
if (!global.txindexAvailable || global.prunedBlockchain) {
// likely due to pruning or no txindex, report the error but continue with an empty transaction list
resolve({ getblock:block, transactions:[], txInputsByTransaction:{} });
} else {
reject(err);
}
});
}).catch(reject);
});
}
let activeMiningQueueTasks = 0;
const miningPromiseQueue = async.queue((task, callback) => {
activeMiningQueueTasks++;
task.run(() => {
callback();
activeMiningQueueTasks--;
});
}, 30);
function buildMiningSummary(statusId, startBlock, endBlock, statusFunc) {
return new Promise(async (resolve, reject) => {
try {
const blockCount = (endBlock - startBlock + 1);
let doneCount = 0;
const markItemsDone = (count) => {
doneCount += count;
if (statusFunc) {
statusFunc({count: 3 * blockCount + 1, done: doneCount});
}
};
const summariesByHeight = {};
const minerInfoByName = {};
for (let i = startBlock; i <= endBlock; i++) {
const height = i;
const cacheKey = `${height}`;
let cachedSummary = await miningSummaryCache.get(cacheKey);
if (cachedSummary) {
summariesByHeight[height] = cachedSummary;
markItemsDone(3);
} else {
miningPromiseQueue.push({run:async (callback) => {
let itemsDone = 0;
try {
const blockHash = await getBlockHashByHeight(height);
itemsDone++;
markItemsDone(1);
const block = await getBlockByHash(blockHash);
itemsDone++;
markItemsDone(1);
const coinbaseTx = await getRawTransaction(block.tx[0]);
const minerInfo = utils.identifyMiner(coinbaseTx, height);
const totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight(coinbaseTx, height);
const subsidy = coinConfig.blockRewardFunction(height, global.activeBlockchain);
let minerName = "Unknown";
if (minerInfo) {
if (minerInfo.type == "address-only") {
minerName = "address-only:" + minerInfo.name;
} else {
minerName = minerInfo.name;
}
}
minerInfoByName[minerName] = minerInfo;
let heightSummary = {
mn: minerName,
tx: block.tx.length,
f: totalFees,
s: subsidy,
w: block.weight
};
miningSummaryCache.set(cacheKey, heightSummary);
summariesByHeight[height] = heightSummary;
itemsDone++;
markItemsDone(1);
callback();
} catch (e) {
utils.logError("430835hre", e);
markItemsDone(3 - itemsDone);
// resolve anyway
callback();
}
}});
}
}
if (!miningPromiseQueue.idle()) {
await miningPromiseQueue.drain();
}
let summary = {
miners:{},
minerNamesSortedByBlockCount: [],
overall:{
blockCount: 0, totalFees: new Decimal(0), totalSubsidy: new Decimal(0), totalTransactions: 0, totalWeight: 0, subsidyCount: 0
}
};
for (let height = startBlock; height <= endBlock; height++) {
const blockSummary = summariesByHeight[height];
const miner = blockSummary.mn;
if (!summary.miners[miner]) {
summary.minerNamesSortedByBlockCount.push(miner);
summary.miners[miner] = {
name: miner, details: minerInfoByName[miner], blocks: [], totalFees: new Decimal(0), totalSubsidy: new Decimal(0), totalTransactions: 0, totalWeight: 0, subsidyCount: 0
};
}
summary.miners[miner].blocks.push(height);
summary.miners[miner].totalFees = summary.miners[miner].totalFees.plus(blockSummary.f);
summary.miners[miner].totalSubsidy = summary.miners[miner].totalSubsidy.plus(blockSummary.s);
summary.miners[miner].totalTransactions += blockSummary.tx;
summary.miners[miner].totalWeight += blockSummary.w;
summary.miners[miner].subsidyCount++;
summary.overall.blockCount++;
summary.overall.totalFees = summary.overall.totalFees.plus(blockSummary.f);
summary.overall.totalSubsidy = summary.overall.totalSubsidy.plus(blockSummary.s);
summary.overall.totalTransactions += blockSummary.tx;
summary.overall.totalWeight += blockSummary.w;
summary.overall.subsidyCount++;
}
summary.minerNamesSortedByBlockCount.sort(function(a, b) {
return ((summary.miners[a].blocks.length > summary.miners[b].blocks.length) ? -1 : 1);
});
// we're done, send final status update
if (statusFunc) {
statusFunc({count: 3 * blockCount + 1, done: 3 * blockCount + 1});
}
resolve(summary);
} catch (err) {
utils.logError("208yrwregud9e3", err);
reject(err);
}
});
}
let mempoolTxSummaryCache = {};
let mempoolCacheKeyForTxid = (txid) => {
return txid.substring(0, 10);
};
function getCachedMempoolTxSummaries() {
return new Promise(async (resolve, reject) => {
try {
const allTxids = await utils.timePromise("coreApi_mempool_summary_getAllMempoolTxids", getAllMempoolTxids);
//const txids = allTxids.slice(0, 50); // for debugging
const txids = allTxids;
const txidCount = txids.length;
const results = [];
const txidKeysForCachePurge = {};
for (let i = 0; i < txids.length; i++) {
const txid = txids[i];
const key = mempoolCacheKeyForTxid(txid);
txidKeysForCachePurge[key] = 1;
if (mempoolTxSummaryCache[key]) {
const itemSummary = Object.assign({}, mempoolTxSummaryCache[key]);
itemSummary.key = key;
results.push(itemSummary);
} else {
// nothing
}
}
// cleanup cache, but we don't need to wait for it to finish before resolving
new Promise((resolve, reject) => {
// purge items from cache that are no longer present in mempool
let keysToDelete = [];
for (let key in mempoolTxSummaryCache) {
if (!txidKeysForCachePurge[key]) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(x => { delete mempoolTxSummaryCache[x] });
});
resolve(results);
} catch (err) {
utils.logError("asodfuhou33", err);
reject(err);
}
});
}
const mempoolTxFileCache = utils.fileCache(config.filesystemCacheDir, `mempool-tx-summaries`, 2);
function getMempoolTxSummaries(allTxids, statusId, statusFunc) {
return new Promise(async (resolve, reject) => {
try {
mempoolTxSummaryCache = (mempoolTxFileCache.tryLoadJson() || {});
//const txids = allTxids.slice(0, 50); // for debugging
const txids = allTxids;
const txidCount = txids.length;
let doneCount = 0;
const statusUpdate = () => { statusFunc({count: txidCount, done: doneCount}); };
const promises = [];
const results = [];
const txidKeysForCachePurge = {};
const btcToSat = (btcFloat) => {
return parseInt(new Decimal(btcFloat).times(SATS_PER_BTC).toDP(0));
};
for (let i = 0; i < txids.length; i++) {
const txid = txids[i];
const key = mempoolCacheKeyForTxid(txid);
txidKeysForCachePurge[key] = 1;
if (mempoolTxSummaryCache[key]) {
const itemSummary = Object.assign({}, mempoolTxSummaryCache[key]);
itemSummary.key = key;
results.push(itemSummary);
doneCount++;
statusUpdate();
} else {
promises.push(new Promise(async (resolve, reject) => {
try {
const item = await getMempoolTxDetails(txid, false);
const itemSummary = {
f: btcToSat(item.entry.fees.modified),
af: btcToSat(item.entry.fees.ancestor),
asz: item.entry.ancestorsize,
a: item.entry.depends.map(x => mempoolCacheKeyForTxid(x)),
t: item.entry.time,
w: item.entry.weight ? item.entry.weight : item.entry.size * 4,
};
mempoolTxSummaryCache[key] = itemSummary;
const itemSummaryWithKey = Object.assign({}, itemSummary);
itemSummaryWithKey.key = key;
results.push(itemSummaryWithKey);
doneCount++;
statusUpdate();
resolve();
} catch (e) {
utils.logError("31297rg34edwe", e);
doneCount++;
statusUpdate();
// resolve anyway
resolve();
}
}));
}
}
await Promise.all(promises);
// purge items from cache that are no longer present in mempool
let keysToDelete = [];
for (let key in mempoolTxSummaryCache) {
if (!txidKeysForCachePurge[key]) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(x => { delete mempoolTxSummaryCache[x] });
mempoolTxSummaryCache.lastUpdated = new Date();
try {
mempoolTxFileCache.writeJson(mempoolTxSummaryCache);
} catch (e) {
utils.logError("h32uheifehues", e);
}
resolve(results);
} catch (err) {
utils.logError("asodfuhou33", err);
reject(err);
}
});
}
function buildMempoolSummary(statusId, ageBuckets, sizeBuckets, statusFunc) {
return new Promise(async (resolve, reject) => {
try {
const allTxids = await utils.timePromise("coreApi_mempool_summary_getAllMempoolTxids", getAllMempoolTxids);
const txSummaries = await getMempoolTxSummaries(allTxids, statusId, statusFunc);
const txids = allTxids;
let maxFee = 0;
let maxFeePerByte = 0;
let maxAge = 0;
let maxSize = 0;
let ages = [];
let sizes = [];
let topfees = [];
for (let i = 0; i < txSummaries.length; i++) {
let summary = txSummaries[i];
let fee = summary.f;
let size = summary.w / 4; // TOOD: hack
let feePerByte = summary.f / summary.w;
let age = Date.now() / 1000 - summary.t;
if (fee > maxFee) {
maxFee = fee;
}
if (feePerByte > maxFeePerByte) {
maxFeePerByte = feePerByte;
}
if (age > maxAge) {
maxAge = age;
}
if (size > maxSize) {
maxSize = size;
}
ages.push({age:age, txidKey:summary.key});
sizes.push({size:size, txidKey:summary.key});
topfees.push({feePerByte:feePerByte, txidKey:summary.key});
}
ages.sort(function(a, b) {
if (a.age != b.age) {
return b.age - a.age;
} else {
return a.txidKey.localeCompare(b.txidKey);
}
});
sizes.sort(function(a, b) {
if (a.size != b.size) {
return b.size - a.size;
} else {
return a.txidKey.localeCompare(b.txidKey);
}
});
topfees.sort(function(a, b) {
if (a.feePerByte != b.feePerByte) {
return b.feePerByte - a.feePerByte;
} else {
return a.txidKey.localeCompare(b.txidKey);
}
});
//maxSize = 2000;
const feeBucketMaxCount = 250;
const feeSatoshiBuckets = [];
for (let i = 0; i < feeBucketMaxCount; i++) {
feeSatoshiBuckets.push(i);
}
let satoshiPerByteBucketMaxima = feeSatoshiBuckets;
let bucketCount = satoshiPerByteBucketMaxima.length + 1;
let satoshiPerByteBuckets = [];
let satoshiPerByteBucketLabels = [];
//satoshiPerByteBucketLabels[0] = ("[0 - " + satoshiPerByteBucketMaxima[0] + ")");
for (let i = 1; i < bucketCount; i++) {
satoshiPerByteBuckets.push({
count: 0,
totalFees: new Decimal(0),
totalBytes: 0,
totalWeight: 0,
minFeeRate: satoshiPerByteBucketMaxima[i - 1],
maxFeeRate: satoshiPerByteBucketMaxima[i]
});
if (i > 0 && i < bucketCount - 1) {
satoshiPerByteBucketLabels.push("[" + satoshiPerByteBucketMaxima[i - 1] + " - " + satoshiPerByteBucketMaxima[i] + ")");
}
}
let ageBucketCount = sizeBuckets;
let ageBucketTxCounts = [];
let ageBucketLabels = [];
let sizeBucketCount = sizeBuckets;
let sizeBucketTxCounts = [];
let sizeBucketLabels = [];
let topfeeBucketCount = sizeBuckets;
let topfeeBucketTxCounts = [];
let topfeeBucketLabels = [];
for (let i = 0; i < ageBucketCount; i++) {
let rangeMin = i * maxAge / ageBucketCount;
let rangeMax = (i + 1) * maxAge / ageBucketCount;
ageBucketTxCounts.push(0);
if (maxAge > 60 * 60 * 24) {
let rangeMinutesMin = new Decimal(rangeMin / 60 / 60 / 24).toFixed(1);
let rangeMinutesMax = new Decimal(rangeMax / 60 / 60 / 24).toFixed(1);
ageBucketLabels.push(rangeMinutesMax + "d");
} else if (maxAge > 60 * 60) {
let rangeMinutesMin = new Decimal(rangeMin / 60 / 60).toFixed(1);
let rangeMinutesMax = new Decimal(rangeMax / 60 / 60).toFixed(1);
ageBucketLabels.push(rangeMinutesMax + "m");
} else if (maxAge > 60 * 10) {
let rangeMinutesMin = new Decimal(rangeMin / 60).toFixed(1);
let rangeMinutesMax = new Decimal(rangeMax / 60).toFixed(1);
ageBucketLabels.push(rangeMinutesMax + "m");
} else {
ageBucketLabels.push(parseInt(rangeMax) + "s");
}
}
for (let i = 0; i < sizeBucketCount; i++) {
sizeBucketTxCounts.push(0);
if (i == sizeBucketCount - 1) {
sizeBucketLabels.push(parseInt(i * maxSize / sizeBucketCount) + "+");
} else if (i == 0) {
sizeBucketLabels.push(parseInt(i * maxSize / sizeBucketCount) + " - " + parseInt((i + 1) * maxSize / sizeBucketCount));
} else {
sizeBucketLabels.push(parseInt((i + 1) * maxSize / sizeBucketCount));
}
}
satoshiPerByteBucketLabels[bucketCount - 1] = (satoshiPerByteBucketMaxima[satoshiPerByteBucketMaxima.length - 1] + "+");
const oldestLargestCount = 20;
let summary = {
"count": 0,
"totalFees": new Decimal(0),
"totalBytes": 0,
"totalWeight": 0,
"satoshiPerByteBuckets": satoshiPerByteBuckets,
"satoshiPerByteBucketLabels": satoshiPerByteBucketLabels,
"ageBucketTxCounts": ageBucketTxCounts,
"ageBucketLabels": ageBucketLabels,
"sizeBucketTxCounts": sizeBucketTxCounts,
"sizeBucketLabels": sizeBucketLabels,
"oldestTxs": ages.slice(0, oldestLargestCount),
"largestTxs": sizes.slice(0, oldestLargestCount),
"highestFeeTxs": topfees.slice(0, oldestLargestCount)
};
for (let i = 0; i < oldestLargestCount; i++) {
let oldTx = summary.oldestTxs[i];
let largeTx = summary.largestTxs[i];
let topfeeTx = summary.highestFeeTxs[i];
for (let j = 0; j < txSummaries.length; j++) {
if (oldTx && txids[j].startsWith(oldTx.txidKey)) {
oldTx.txid = txids[j];
break;
}
}
for (let j = 0; j < txids.length; j++) {
if (largeTx && txids[j].startsWith(largeTx.txidKey)) {
largeTx.txid = txids[j];
break;
}
}
for (let j = 0; j < txSummaries.length; j++) {
if (topfeeTx && txids[j].startsWith(topfeeTx.txidKey)) {
topfeeTx.txid = txids[j];
break;
}
}
}
for (let x = 0; x < txSummaries.length; x++) {
let txMempoolInfo = txSummaries[x];
let fee = txMempoolInfo.f;
let size = txMempoolInfo.w / 4;
let weight = txMempoolInfo.w;
let feePerByte = new Decimal(txMempoolInfo.f).dividedBy(SATS_PER_BTC).toNumber() / weight;
let satoshiPerByte = feePerByte * SATS_PER_BTC;
let age = Date.now() / 1000 - txMempoolInfo.t;
let addedToBucket = false;
for (let i = 0; i < satoshiPerByteBuckets.length; i++) {
if (satoshiPerByteBuckets[i].maxFeeRate > satoshiPerByte) {
satoshiPerByteBuckets[i]["count"]++;
satoshiPerByteBuckets[i]["totalFees"] = satoshiPerByteBuckets[i]["totalFees"].plus(new Decimal(fee).dividedBy(SATS_PER_BTC));
satoshiPerByteBuckets[i]["totalBytes"] += size;
satoshiPerByteBuckets[i]["totalWeight"] += weight;
addedToBucket = true;
break;
}
}
if (!addedToBucket) {
satoshiPerByteBuckets[bucketCount - 2]["count"]++;
satoshiPerByteBuckets[bucketCount - 2]["totalFees"] = satoshiPerByteBuckets[bucketCount - 2]["totalFees"].plus(new Decimal(fee).dividedBy(SATS_PER_BTC));
satoshiPerByteBuckets[bucketCount - 2]["totalBytes"] += size;
satoshiPerByteBuckets[bucketCount - 2]["totalWeight"] += weight;
}
summary["count"]++;
summary["totalFees"] = summary.totalFees.plus(new Decimal(fee).dividedBy(SATS_PER_BTC));
summary["totalBytes"] += size;
summary["totalWeight"] += weight;
let ageBucketIndex = Math.min(ageBucketCount - 1, parseInt(age / (maxAge / ageBucketCount)));
let sizeBucketIndex = Math.min(sizeBucketCount - 1, parseInt(size / (maxSize / sizeBucketCount)));
ageBucketTxCounts[ageBucketIndex]++;
sizeBucketTxCounts[sizeBucketIndex]++;
}
let topTargetPercent = 0.25;
let totWeight = 0;
let topIndex = -1;
for (let i = satoshiPerByteBuckets.length - 1; i >= 0; i--) {
totWeight += satoshiPerByteBuckets[i].totalWeight;
if (totWeight / summary.totalWeight * 100 > topTargetPercent) {
topIndex = i;
break;
}
}
summary.satoshiPerByteBucketLabels = summary.satoshiPerByteBucketLabels.slice(0, topIndex);
if (topIndex < feeBucketMaxCount) {
summary.satoshiPerByteBucketLabels.push(topIndex + "+");
}
if (topIndex < satoshiPerByteBuckets.length) {
satoshiPerByteBuckets[topIndex].buckets = 0;
// merge the top buckets into one
for (let i = topIndex + 1; i < satoshiPerByteBuckets.length; i++) {
satoshiPerByteBuckets[topIndex].count += satoshiPerByteBuckets[i].count;
satoshiPerByteBuckets[topIndex].totalFees = satoshiPerByteBuckets[topIndex].totalFees.plus(satoshiPerByteBuckets[i].totalFees);
satoshiPerByteBuckets[topIndex].totalBytes += satoshiPerByteBuckets[i].totalBytes;
satoshiPerByteBuckets[topIndex].totalWeight += satoshiPerByteBuckets[i].totalWeight;
satoshiPerByteBuckets[topIndex].buckets++;
}
satoshiPerByteBuckets = satoshiPerByteBuckets.slice(0, topIndex + 1);
satoshiPerByteBucketMaxima = satoshiPerByteBucketMaxima.slice(0, topIndex + 1);
}
summary["averageFee"] = summary["totalFees"] / summary["count"];
summary["averageFeePerByte"] = summary["totalFees"] / summary["totalBytes"];
summary["satoshiPerByteBucketMaxima"] = satoshiPerByteBucketMaxima;
summary.satoshiPerByteBuckets = satoshiPerByteBuc