var debug = require("debug"); var debugLog = debug("btcexp:core"); var LRU = require("lru-cache"); var fs = require('fs'); var utils = require("../utils.js"); var config = require("../config.js"); var coins = require("../coins.js"); var redisCache = require("../redisCache.js"); var Decimal = require("decimal.js"); var md5 = require("md5"); // choose one of the below: RPC to a node, or mock data while testing var rpcApi = require("./rpcApi.js"); //var rpcApi = require("./mockApi.js"); // this value should be incremented whenever data format changes, to avoid // pulling old-format data from a persistent cache var cacheKeyVersion = "v1"; const ONE_SEC = 1000; const ONE_MIN = 60 * ONE_SEC; const ONE_HR = 60 * ONE_MIN; const ONE_DAY = 24 * ONE_HR; const ONE_YR = 265 * ONE_DAY; function createMemoryLruCache(cacheObj, onCacheEvent) { return { get:function(key) { return new Promise(function(resolve, reject) { onCacheEvent("memory", "try", key); var val = cacheObj.get(key); if (val != null) { onCacheEvent("memory", "hit", key); } else { onCacheEvent("memory", "miss", key); } resolve(cacheObj.get(key)); }); }, set:function(key, obj, maxAge) { cacheObj.set(key, obj, maxAge); } } } function tryCache(cacheKey, cacheObjs, index, resolve, reject) { if (index == cacheObjs.length) { resolve(null); return; } cacheObjs[index].get(cacheKey).then(function(result) { if (result != null) { resolve(result); } else { tryCache(cacheKey, cacheObjs, index + 1, resolve, reject); } }); } function createTieredCache(cacheObjs) { return { get:function(key) { return new Promise(function(resolve, reject) { tryCache(key, cacheObjs, 0, resolve, reject); }); }, set:function(key, obj, maxAge) { for (var i = 0; i < cacheObjs.length; i++) { cacheObjs[i].set(key, obj, maxAge); } } } } var miscCaches = []; var blockCaches = []; var txCaches = []; if (!config.noInmemoryRpcCache) { global.cacheStats.memory = { try: 0, hit: 0, miss: 0 }; var onMemoryCacheEvent = function(cacheType, eventType, cacheKey) { global.cacheStats.memory[eventType]++; //debugLog(`cache.${cacheType}.${eventType}: ${cacheKey}`); } miscCaches.push(createMemoryLruCache(new LRU(2000), onMemoryCacheEvent)); blockCaches.push(createMemoryLruCache(new LRU(2000), onMemoryCacheEvent)); txCaches.push(createMemoryLruCache(new LRU(10000), onMemoryCacheEvent)); } if (redisCache.active) { global.cacheStats.redis = { try: 0, hit: 0, miss: 0, error: 0 }; var onRedisCacheEvent = function(cacheType, eventType, cacheKey) { global.cacheStats.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 var rpcHostPort = `${config.credentials.rpc.host}:${config.credentials.rpc.port}`; var rpcCredKeyComponent = md5(JSON.stringify(config.credentials.rpc)).substring(0, 8); var redisCacheObj = redisCache.createCache(`${cacheKeyVersion}-${rpcCredKeyComponent}`, onRedisCacheEvent); miscCaches.push(redisCacheObj); blockCaches.push(redisCacheObj); txCaches.push(redisCacheObj); } var miscCache = createTieredCache(miscCaches); var blockCache = createTieredCache(blockCaches); var txCache = createTieredCache(txCaches); 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) { var cacheResult = null; var 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; finallyFunc(); }).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 > 9) { return false; } return true; } function getBlockchainInfo() { return tryCacheThenRpcApi(miscCache, "getBlockchainInfo", 10 * ONE_SEC, rpcApi.getBlockchainInfo); } 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", ONE_SEC, rpcApi.getMempoolInfo); } function getMempoolTxids() { // no caching, that would be dumb return rpcApi.getMempoolTxids(); } function getMiningInfo() { return tryCacheThenRpcApi(miscCache, "getMiningInfo", 30 * ONE_SEC, rpcApi.getMiningInfo); } function getUptimeSeconds() { return tryCacheThenRpcApi(miscCache, "getUptimeSeconds", ONE_SEC, rpcApi.getUptimeSeconds); } function getChainTxStats(blockCount) { return tryCacheThenRpcApi(miscCache, "getChainTxStats-" + blockCount, 20 * ONE_MIN, function() { return rpcApi.getChainTxStats(blockCount); }); } function getNetworkHashrate(blockCount) { return tryCacheThenRpcApi(miscCache, "getNetworkHashrate-" + blockCount, 20 * ONE_MIN, function() { return rpcApi.getNetworkHashrate(blockCount); }); } function getBlockStats(hash) { return tryCacheThenRpcApi(miscCache, "getBlockStats-" + hash, ONE_YR, function() { return rpcApi.getBlockStats(hash); }); } function getBlockStatsByHeight(height) { return tryCacheThenRpcApi(miscCache, "getBlockStatsByHeight-" + height, ONE_YR, function() { return rpcApi.getBlockStatsByHeight(height); }); } function getUtxoSetSummary() { return tryCacheThenRpcApi(miscCache, "getUtxoSetSummary", 15 * ONE_MIN, rpcApi.getUtxoSetSummary); } function getTxCountStats(dataPtCount, blockStart, blockEnd) { return new Promise(function(resolve, reject) { var dataPoints = dataPtCount; getBlockchainInfo().then(function(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) { reject(`Error 37rhw0e7ufdsgf: blockStart (${blockStart}) > blockEnd (${blockEnd})`); return; } if (blockStart < 0) { blockStart += getblockchaininfo.blocks; } if (blockEnd < 0) { blockEnd += getblockchaininfo.blocks; } var chainTxStatsIntervals = []; for (var i = 0; i < dataPoints; i++) { chainTxStatsIntervals.push(parseInt(Math.max(10, getblockchaininfo.blocks - blockStart - i * (blockEnd - blockStart) / (dataPoints - 1) - 1))); } var promises = []; for (var i = 0; i < chainTxStatsIntervals.length; i++) { promises.push(getChainTxStats(chainTxStatsIntervals[i])); } Promise.all(promises).then(function(results) { if (results[0].name == "RpcError" && results[0].code == -8) { // recently started node - no meaningful data to return resolve(null); return; } var txStats = { txCounts: [], txLabels: [], txRates: [] }; for (var i = results.length - 1; i >= 0; i--) { if (results[i].window_tx_count) { txStats.txCounts.push( {x:(getblockchaininfo.blocks - results[i].window_block_count), y: (results[i].txcount - results[i].window_tx_count)} ); txStats.txRates.push( {x:(getblockchaininfo.blocks - results[i].window_block_count), y: (results[i].txrate)} ); txStats.txLabels.push(i); } } resolve({txCountStats:txStats, getblockchaininfo:getblockchaininfo, totalTxCount:results[0].txcount}); }).catch(function(err) { reject(err); }); }).catch(function(err) { reject(err); }); }); } function getSmartFeeEstimates(mode, confTargetBlockCounts) { return new Promise(function(resolve, reject) { var promises = []; for (var 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) { var result = {}; result.getpeerinfo = getpeerinfo; var versionSummaryMap = {}; for (var i = 0; i < getpeerinfo.length; i++) { var x = getpeerinfo[i]; if (versionSummaryMap[x.subver] == null) { versionSummaryMap[x.subver] = 0; } versionSummaryMap[x.subver]++; } var versionSummary = []; for (var 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]); } }); var servicesSummaryMap = {}; for (var i = 0; i < getpeerinfo.length; i++) { var x = getpeerinfo[i]; if (servicesSummaryMap[x.services] == null) { servicesSummaryMap[x.services] = 0; } servicesSummaryMap[x.services]++; } var servicesSummary = []; for (var 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]); } }); result.versionSummary = versionSummary; result.servicesSummary = servicesSummary; resolve(result); }).catch(function(err) { reject(err); }); }); } function getMempoolDetails(start, count) { return new Promise(function(resolve, reject) { tryCacheThenRpcApi(miscCache, "getMempoolTxids", ONE_SEC, rpcApi.getMempoolTxids).then(function(resultTxids) { var txids = []; for (var i = start; (i < resultTxids.length && i < (start + count)); i++) { txids.push(resultTxids[i]); } getRawTransactionsWithInputs(txids, config.site.txMaxInput).then(function(result) { resolve({ txCount:resultTxids.length, transactions:result.transactions, txInputsByTransaction:result.txInputsByTransaction }); }); }).catch(function(err) { reject(err); }); }); } function getBlockByHeight(blockHeight) { return tryCacheThenRpcApi(blockCache, "getBlockByHeight-" + blockHeight, ONE_HR, function() { return rpcApi.getBlockByHeight(blockHeight); }); } function getBlocksByHeight(blockHeights) { return new Promise(function(resolve, reject) { var promises = []; for (var 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 getBlockHeaderByHeight(blockHeight) { return tryCacheThenRpcApi(blockCache, "getBlockHeaderByHeight-" + blockHeight, ONE_HR, function() { return rpcApi.getBlockHeaderByHeight(blockHeight); }); } function getBlockHeadersByHeight(blockHeights) { return new Promise(function(resolve, reject) { var promises = []; for (var 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) { var promises = []; for (var 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, ONE_HR, function() { return rpcApi.getBlockByHash(blockHash); }); } function getBlocksByHash(blockHashes) { return new Promise(function(resolve, reject) { var promises = []; for (var i = 0; i < blockHashes.length; i++) { promises.push(getBlockByHash(blockHashes[i])); } Promise.all(promises).then(function(results) { var result = {}; results.forEach(function(item) { result[item.hash] = item; }); resolve(result); }).catch(function(err) { reject(err); }); }); } function getRawTransaction(txid) { var rpcApiFunction = function() { return rpcApi.getRawTransaction(txid); }; return tryCacheThenRpcApi(txCache, "getRawTransaction-" + txid, ONE_HR, rpcApiFunction, shouldCacheTransaction); } /* This function pulls raw tx data and then summarizes the outputs. It's used in memory-constrained situations. */ function getSummarizedTransactionOutput(txid, voutIndex) { var rpcApiFunction = function() { return new Promise(function(resolve, reject) { rpcApi.getRawTransaction(txid).then(function(rawTx) { var 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}`, ONE_HR, rpcApiFunction, function() { return true; }); } function getTxUtxos(tx) { return new Promise(function(resolve, reject) { var promises = []; for (var i = 0; i < tx.vout.length; i++) { promises.push(getUtxo(tx.txid, i)); } Promise.all(promises).then(function(results) { resolve(results); }).catch(function(err) { reject(err); }); }); } function getUtxo(txid, outputIndex) { return new Promise(function(resolve, reject) { tryCacheThenRpcApi(miscCache, "utxo-" + txid + "-" + outputIndex, ONE_HR, function() { return rpcApi.getUtxo(txid, outputIndex); }).then(function(result) { // to avoid cache misses, rpcApi.getUtxo returns "0" instead of null if (result == "0") { resolve(null); return; } resolve(result); }).catch(function(err) { reject(err); }); }); } function getMempoolTxDetails(txid, includeAncDec) { return tryCacheThenRpcApi(miscCache, "mempoolTxDetails-" + txid + "-" + includeAncDec, ONE_HR, function() { return rpcApi.getMempoolTxDetails(txid, includeAncDec); }); } function getAddress(address) { return tryCacheThenRpcApi(miscCache, "getAddress-" + address, ONE_HR, function() { return rpcApi.getAddress(address); }); } function getRawTransactions(txids) { return new Promise(function(resolve, reject) { var promises = []; for (var i = 0; i < txids.length; i++) { promises.push(getRawTransaction(txids[i])); } Promise.all(promises).then(function(results) { resolve(results); }).catch(function(err) { reject(err); }); }); } function buildBlockAnalysisData(blockHeight, txids, txIndex, results, callback) { if (txIndex >= txids.length) { callback(); return; } var txid = txids[txIndex]; getRawTransactionsWithInputs([txid]).then(function(txData) { results.push(summarizeBlockAnalysisData(blockHeight, txData.transactions[0], txData.txInputsByTransaction[txid])); buildBlockAnalysisData(blockHeight, txids, txIndex + 1, results, callback); }); } function summarizeBlockAnalysisData(blockHeight, tx, inputs) { var 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); if (txSummary.coinbase) { var subsidy = global.coinConfig.blockRewardFunction(blockHeight, global.activeBlockchain); txSummary.totalInput = txSummary.totalInput.plus(new Decimal(subsidy)); txSummary.vin.push({ coinbase: true, value: subsidy }); } else { for (var i = 0; i < tx.vin.length; i++) { var vin = tx.vin[i]; var inputVout = inputs[i]; txSummary.totalInput = txSummary.totalInput.plus(new Decimal(inputVout.value)); txSummary.vin.push({ txid: tx.vin[i].txid, vout: tx.vin[i].vout, sequence: tx.vin[i].sequence, value: inputVout.value, type: inputVout.scriptPubKey.type, reqSigs: inputVout.scriptPubKey.reqSigs, addressCount: (inputVout.scriptPubKey.addresses ? inputVout.scriptPubKey.addresses.length : 0) }); } } txSummary.vout = []; txSummary.totalOutput = new Decimal(0); for (var 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: tx.vout[i].scriptPubKey.addresses ? tx.vout[i].scriptPubKey.addresses.length : 0 }); } if (txSummary.coinbase) { txSummary.totalFee = new Decimal(0); } else { txSummary.totalFee = txSummary.totalInput.minus(txSummary.totalOutput); } return txSummary; } function getRawTransactionsWithInputs(txids, maxInputs=-1) { return new Promise(function(resolve, reject) { getRawTransactions(txids).then(function(transactions) { var maxInputsTracked = config.site.txMaxInput; if (maxInputs <= 0) { maxInputsTracked = 1000000; } else if (maxInputs > 0) { maxInputsTracked = maxInputs; } var vinIds = []; for (var i = 0; i < transactions.length; i++) { var transaction = transactions[i]; if (transaction && transaction.vin) { for (var 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}); } } } } var promises = []; for (var i = 0; i < vinIds.length; i++) { var vinId = vinIds[i]; promises.push(getSummarizedTransactionOutput(vinId.txid, vinId.voutIndex)); } Promise.all(promises).then(function(promiseResults) { var summarizedTxOutputs = {}; for (var i = 0; i < promiseResults.length; i++) { var summarizedTxOutput = promiseResults[i]; summarizedTxOutputs[`${summarizedTxOutput.txid}:${summarizedTxOutput.n}`] = summarizedTxOutput; } var txInputsByTransaction = {}; transactions.forEach(function(tx) { txInputsByTransaction[tx.txid] = {}; if (tx && tx.vin) { for (var i = 0; i < Math.min(maxInputsTracked, tx.vin.length); i++) { var summarizedTxOutput = summarizedTxOutputs[`${tx.vin[i].txid}:${tx.vin[i].vout}`]; if (summarizedTxOutput) { txInputsByTransaction[tx.txid][i] = summarizedTxOutput; } } } }); resolve({ transactions:transactions, txInputsByTransaction:txInputsByTransaction }); }); }).catch(function(err) { reject(err); }); }); } function getBlockByHashWithTransactions(blockHash, txLimit, txOffset) { return new Promise(function(resolve, reject) { getBlockByHash(blockHash).then(function(block) { var txids = []; if (txOffset > 0) { txids.push(block.tx[0]); } for (var i = txOffset; i < Math.min(txOffset + txLimit, block.tx.length); i++) { txids.push(block.tx[i]); } getRawTransactionsWithInputs(txids, config.site.txMaxInput).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.getMinerFromCoinbaseTx(block.coinbaseTx); } // if we're on page 2, we don't really want the coinbase tx in the tx list anymore if (txOffset > 0) { txsResult.transactions.shift(); } resolve({ getblock:block, transactions:txsResult.transactions, txInputsByTransaction:txsResult.txInputsByTransaction }); }); }).catch(function(err) { reject(err); }); }); } function getHelp() { return new Promise(function(resolve, reject) { tryCacheThenRpcApi(miscCache, "getHelp", ONE_DAY, rpcApi.getHelp).then(function(helpContent) { var lines = helpContent.split("\n"); var sections = []; lines.forEach(function(line) { if (line.startsWith("==")) { var sectionName = line.substring(2); sectionName = sectionName.substring(0, sectionName.length - 2).trim(); sections.push({name:sectionName, methods:[]}); } else if (line.trim().length > 0) { var methodName = line.trim(); if (methodName.includes(" ")) { methodName = methodName.substring(0, methodName.indexOf(" ")); } sections[sections.length - 1].methods.push({name:methodName, content:line.trim()}); } }); resolve(sections); }).catch(function(err) { reject(err); }); }); } function getRpcMethodHelp(methodName) { var rpcApiFunction = function() { return rpcApi.getRpcMethodHelp(methodName); }; return new Promise(function(resolve, reject) { tryCacheThenRpcApi(miscCache, "getHelp-" + methodName, ONE_DAY, rpcApiFunction).then(function(helpContent) { var output = {}; output.string = helpContent; var str = helpContent; var lines = str.split("\n"); var argumentLines = []; var catchArgs = false; lines.forEach(function(line) { if (line.trim().length == 0) { catchArgs = false; } if (catchArgs) { argumentLines.push(line); } if (line.trim() == "Arguments:" || line.trim() == "Arguments") { catchArgs = true; } }); var args = []; var argX = null; // looking for line starting with "N. " where N is an integer (1-2 digits) argumentLines.forEach(function(line) { var regex = /^([0-9]+)\.\s*"?(\w+)"?\s*\(([^,)]*),?\s*([^,)]*),?\s*([^,)]*),?\s*([^,)]*)?\s*\)\s*(.+)?$/; var match = regex.exec(line); if (match) { argX = {}; argX.name = match[2]; argX.detailsLines = []; argX.properties = []; if (match[3]) { argX.properties.push(match[3]); } if (match[4]) { argX.properties.push(match[4]); } if (match[5]) { argX.properties.push(match[5]); } if (match[6]) { argX.properties.push(match[6]); } if (match[7]) { argX.description = match[7]; } args.push(argX); } if (!match && argX) { argX.detailsLines.push(line); } }); output.args = args; resolve(output); }).catch(function(err) { reject(err); }); }); } function logCacheSizes() { var itemCounts = [ miscCache.itemCount, blockCache.itemCount, txCache.itemCount ]; var stream = fs.createWriteStream("memoryUsage.csv", {flags:'a'}); stream.write("itemCounts: " + JSON.stringify(itemCounts) + "\n"); stream.end(); } module.exports = { getGenesisBlockHash: getGenesisBlockHash, getGenesisCoinbaseTransactionId: getGenesisCoinbaseTransactionId, getBlockchainInfo: getBlockchainInfo, getNetworkInfo: getNetworkInfo, getNetTotals: getNetTotals, getMempoolInfo: getMempoolInfo, getMempoolTxids: getMempoolTxids, getMiningInfo: getMiningInfo, getBlockByHeight: getBlockByHeight, getBlocksByHeight: getBlocksByHeight, getBlockByHash: getBlockByHash, getBlocksByHash: getBlocksByHash, getBlockByHashWithTransactions: getBlockByHashWithTransactions, getRawTransaction: getRawTransaction, getRawTransactions: getRawTransactions, getRawTransactionsWithInputs: getRawTransactionsWithInputs, getTxUtxos: getTxUtxos, getMempoolTxDetails: getMempoolTxDetails, getUptimeSeconds: getUptimeSeconds, getHelp: getHelp, getRpcMethodHelp: getRpcMethodHelp, getAddress: getAddress, logCacheSizes: logCacheSizes, getPeerSummary: getPeerSummary, getChainTxStats: getChainTxStats, getMempoolDetails: getMempoolDetails, getTxCountStats: getTxCountStats, getSmartFeeEstimates: getSmartFeeEstimates, getSmartFeeEstimate: getSmartFeeEstimate, getUtxoSetSummary: getUtxoSetSummary, getNetworkHashrate: getNetworkHashrate, getBlockStats: getBlockStats, getBlockStatsByHeight: getBlockStatsByHeight, getBlocksStatsByHeight: getBlocksStatsByHeight, buildBlockAnalysisData: buildBlockAnalysisData, getBlockHeaderByHeight: getBlockHeaderByHeight, getBlockHeadersByHeight: getBlockHeadersByHeight };