btc-rpc-explorer
Version:
Open-source, self-hosted Bitcoin explorer
644 lines (492 loc) • 17.8 kB
JavaScript
;
const debug = require('debug');
const debugLog = debug("btcexp:rpc");
const async = require("async");
const semver = require("semver");
const utils = require("../utils.js");
const config = require("../config.js");
const coins = require("../coins.js");
const statTracker = require("../statTracker.js");
let activeQueueTasks = 0;
const rpcQueue = async.queue(function(task, callback) {
activeQueueTasks++;
//debugLog("activeQueueTasks: " + activeQueueTasks);
task.rpcCall(function() {
callback();
activeQueueTasks--;
//debugLog("activeQueueTasks: " + activeQueueTasks);
});
}, config.rpcConcurrency);
const minRpcVersions = {
getblockstats: "0.17.0",
getindexinfo: "0.21.0",
getdeploymentinfo: "23.0.0"
};
global.rpcStats = {};
function getBlockchainInfo() {
return new Promise((resolve, reject) => {
getRpcData("getblockchaininfo").then((getblockchaininfo) => {
// keep global.pruneHeight updated
if (getblockchaininfo.pruned) {
global.pruneHeight = getblockchaininfo.pruneheight;
}
resolve(getblockchaininfo);
}).catch(reject);
});
}
function getBlockCount() {
return getRpcData("getblockcount");
}
function getNetworkInfo() {
return getRpcData("getnetworkinfo");
}
function getNetTotals() {
return getRpcData("getnettotals");
}
function getMempoolInfo() {
return getRpcData("getmempoolinfo");
}
function getMiningInfo() {
return getRpcData("getmininginfo");
}
function getIndexInfo() {
if (semver.gte(global.btcNodeSemver, minRpcVersions.getindexinfo)) {
return getRpcData("getindexinfo");
} else {
// unsupported
return unsupportedPromise(minRpcVersions.getindexinfo);
}
}
function getDeploymentInfo() {
if (semver.gte(global.btcNodeSemver, minRpcVersions.getdeploymentinfo)) {
return getRpcData("getdeploymentinfo");
} else {
// unsupported
return unsupportedPromise(minRpcVersions.getdeploymentinfo);
}
}
function getUptimeSeconds() {
return getRpcData("uptime");
}
function getPeerInfo() {
return getRpcData("getpeerinfo");
}
function getBlockTemplate() {
return getRpcDataWithParams({method:"getblocktemplate", parameters:[{"rules": ["segwit"]}]});
}
function getAllMempoolTxids() {
return getRpcDataWithParams({method:"getrawmempool", parameters:[false]});
}
function getSmartFeeEstimate(mode="CONSERVATIVE", confTargetBlockCount) {
return getRpcDataWithParams({method:"estimatesmartfee", parameters:[confTargetBlockCount, mode]});
}
function getNetworkHashrate(blockCount=144) {
return getRpcDataWithParams({method:"getnetworkhashps", parameters:[blockCount]});
}
function getBlockStats(hash) {
if (semver.gte(global.btcNodeSemver, minRpcVersions.getblockstats)) {
if (hash == coinConfig.genesisBlockHashesByNetwork[global.activeBlockchain] && coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]) {
return new Promise(function(resolve, reject) {
resolve(coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]);
});
} else {
return getRpcDataWithParams({method:"getblockstats", parameters:[hash]});
}
} else {
// unsupported
return unsupportedPromise(minRpcVersions.getblockstats);
}
}
function getBlockStatsByHeight(height) {
if (semver.gte(global.btcNodeSemver, minRpcVersions.getblockstats)) {
if (height == 0 && coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]) {
return new Promise(function(resolve, reject) {
resolve(coinConfig.genesisBlockStatsByNetwork[global.activeBlockchain]);
});
} else {
return getRpcDataWithParams({method:"getblockstats", parameters:[height]});
}
} else {
// unsupported
return unsupportedPromise(minRpcVersions.getblockstats);
}
}
function getUtxoSetSummary(useCoinStatsIndexIfAvailable=true) {
if (useCoinStatsIndexIfAvailable && global.getindexinfo && global.getindexinfo.coinstatsindex) {
return getRpcDataWithParams({method:"gettxoutsetinfo", parameters:["muhash"]});
} else {
return getRpcData("gettxoutsetinfo");
}
}
function getRawMempool() {
return new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getrawmempool", parameters:[false]}).then(function(txids) {
let promises = [];
for (let i = 0; i < txids.length; i++) {
let txid = txids[i];
promises.push(getRawMempoolEntry(txid));
}
Promise.all(promises).then(function(results) {
let finalResult = {};
for (let i = 0; i < results.length; i++) {
if (results[i] != null) {
finalResult[results[i].txid] = results[i];
}
}
resolve(finalResult);
}).catch(function(err) {
reject(err);
});
}).catch(function(err) {
reject(err);
});
});
}
function getRawMempoolEntry(txid) {
return new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getmempoolentry", parameters:[txid]}).then(function(result) {
result.txid = txid;
resolve(result);
}).catch(function(err) {
resolve(null);
});
});
}
function getChainTxStats(blockCount, blockhashEnd=null) {
let params = [blockCount];
if (blockhashEnd) {
params.push(blockhashEnd);
}
return getRpcDataWithParams({method:"getchaintxstats", parameters:params});
}
function getBlockByHeight(blockHeight) {
return new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getblockhash", parameters:[blockHeight]}).then(function(blockhash) {
getBlockByHash(blockhash).then(function(block) {
resolve(block);
}).catch(function(err) {
reject(err);
});
}).catch(function(err) {
reject(err);
});
});
}
function getBlockHeaderByHash(blockhash) {
return getRpcDataWithParams({method:"getblockheader", parameters:[blockhash]});
}
function getBlockHeaderByHeight(blockHeight) {
return new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getblockhash", parameters:[blockHeight]}).then(function(blockhash) {
getBlockHeaderByHash(blockhash).then(function(blockHeader) {
resolve(blockHeader);
}).catch(function(err) {
reject(err);
});
}).catch(function(err) {
reject(err);
});
});
}
function getBlockHashByHeight(blockHeight) {
return getRpcDataWithParams({method:"getblockhash", parameters:[blockHeight]});
}
function getBlockByHash(blockHash) {
return getRpcDataWithParams({method:"getblock", parameters:[blockHash]})
.then(function(block) {
return getRawTransaction(block.tx[0], blockHash).then(function(tx) {
block.coinbaseTx = tx;
block.totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight(tx, block.height);
block.miner = utils.identifyMiner(tx, block.height);
return block;
})
}).catch(function(err) {
// the block is pruned, use `getblockheader` instead
debugLog('getblock failed, falling back to getblockheader', blockHash, err);
return getRpcDataWithParams({method:"getblockheader", parameters:[blockHash]})
.then(function(block) { block.tx = []; return block });
}).then(function(block) {
block.subsidy = coinConfig.blockRewardFunction(block.height, global.activeBlockchain);
return block;
})
}
function getAddress(address) {
return getRpcDataWithParams({method:"validateaddress", parameters:[address]});
}
function getRawTransaction(txid, blockhash) {
return new Promise(function(resolve, reject) {
if (coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain] && txid == coins[config.coin].genesisCoinbaseTransactionIdsByNetwork[global.activeBlockchain]) {
// copy the "confirmations" field from genesis block to the genesis-coinbase tx
getBlockchainInfo().then(function(blockchainInfoResult) {
let result = coins[config.coin].genesisCoinbaseTransactionsByNetwork[global.activeBlockchain];
result.confirmations = blockchainInfoResult.blocks;
// hack: default regtest node returns "0" for number of blocks, despite including a genesis block;
// to display this block without errors, tag it with 1 confirmation
if (global.activeBlockchain == "regtest" && result.confirmations == 0) {
result.confirmations = 1;
}
resolve(result);
}).catch(function(err) {
reject(err);
});
} else {
let extra_params = blockhash ? [ blockhash ] : [];
getRpcDataWithParams({method:"getrawtransaction", parameters:[txid, 1, ...extra_params]}).then(function(result) {
if (result == null || result.code && result.code < 0) {
return Promise.reject(result);
}
resolve(result);
}).catch(function(err) {
if (!global.txindexAvailable) {
noTxIndexTransactionLookup(txid, !!blockhash).then(resolve, reject);
} else {
reject(err);
}
});
}
});
}
async function noTxIndexTransactionLookup(txid, walletOnly) {
// Try looking up with an external Electrum server, using 'get_confirmed_blockhash'.
// This is only available in Electrs and requires enabling BTCEXP_ELECTRUM_TXINDEX.
if (!walletOnly && (config.addressApi == "electrum" || config.addressApi == "electrumx") && config.electrumTxIndex) {
try {
let blockhash = await electrumAddressApi.lookupTxBlockHash(txid);
return await getRawTransaction(txid, blockhash);
} catch (err) {
debugLog(`Electrs blockhash lookup failed for ${txid}:`, err);
}
}
// Try looking up in wallet transactions
for (let wallet of await listWallets()) {
try { return await getWalletTransaction(wallet, txid); }
catch (_) {}
}
// Try looking up in recent blocks
if (!walletOnly) {
let tip_height = await getRpcDataWithParams({method:"getblockcount", parameters:[]});
for (let height=tip_height; height>Math.max(tip_height - config.noTxIndexSearchDepth, 0); height--) {
let blockhash = await getRpcDataWithParams({method:"getblockhash", parameters:[height]});
try { return await getRawTransaction(txid, blockhash); }
catch (_) {}
}
}
throw new Error(`The requested tx ${txid} cannot be found in wallet transactions, mempool transactions, or recently confirmed transactions`)
}
function listWallets() {
return getRpcDataWithParams({method:"listwallets", parameters:[]})
}
async function getWalletTransaction(wallet, txid) {
global.rpcClient.wallet = wallet;
try {
return await getRpcDataWithParams({method:"gettransaction", parameters:[ txid, true, true ]})
.then(wtx => ({ ...wtx, ...wtx.decoded, decoded: null }))
} finally {
global.rpcClient.wallet = null;
}
}
function getUtxo(txid, outputIndex) {
return new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"gettxout", parameters:[txid, outputIndex]}).then(function(result) {
if (result == null) {
resolve("0");
return;
}
if (result.code && result.code < 0) {
reject(result);
return;
}
resolve(result);
}).catch(function(err) {
reject(err);
});
});
}
function getMempoolTxDetails(txid, includeAncDec=true) {
let promises = [];
let mempoolDetails = {};
promises.push(new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getmempoolentry", parameters:[txid]}).then(function(result) {
mempoolDetails.entry = result;
resolve();
}).catch(function(err) {
reject(err);
});
}));
if (includeAncDec) {
promises.push(new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getmempoolancestors", parameters:[txid]}).then(function(result) {
mempoolDetails.ancestors = result;
resolve();
}).catch(function(err) {
reject(err);
});
}));
promises.push(new Promise(function(resolve, reject) {
getRpcDataWithParams({method:"getmempooldescendants", parameters:[txid]}).then(function(result) {
mempoolDetails.descendants = result;
resolve();
}).catch(function(err) {
reject(err);
});
}));
}
return new Promise(function(resolve, reject) {
Promise.all(promises).then(function() {
resolve(mempoolDetails);
}).catch(function(err) {
reject(err);
});
});
}
function getTxOut(txid, vout) {
return getRpcDataWithParams({method:"gettxout", parameters:[txid, vout]});
}
function getHelp() {
return getRpcData("help");
}
function getRpcMethodHelp(methodName) {
return getRpcDataWithParams({method:"help", parameters:[methodName]});
}
function getRpcData(cmd, verifyingConnection=false) {
let startTime = new Date().getTime();
if (!verifyingConnection && !global.rpcConnected) {
return Promise.reject(new Error("No RPC connection available. Check your connection/authentication parameters."));
}
return new Promise(function(resolve, reject) {
debugLog(`RPC: ${cmd}`);
let rpcCall = async function(callback) {
let client = (cmd == "gettxoutsetinfo" ? global.rpcClientNoTimeout : global.rpcClient);
try {
const rpcResult = await client.request(cmd, []);
const result = rpcResult.result;
//console.log(`RPC: request=${cmd}, result=${JSON.stringify(result)}`);
if (Array.isArray(result) && result.length == 1) {
let result0 = result[0];
if (result0 && result0.name && result0.name == "RpcError") {
logStats(cmd, false, new Date().getTime() - startTime, false);
debugLog("RpcErrorResult-01: " + JSON.stringify(result0));
throw new Error(`RpcError: type=errorResponse-01`);
}
}
if (result && result.name && result.name == "RpcError") {
logStats(cmd, false, new Date().getTime() - startTime, false);
debugLog("RpcErrorResult-02: " + JSON.stringify(result));
throw new Error(`RpcError: type=errorResponse-02`);
}
resolve(result);
logStats(cmd, false, new Date().getTime() - startTime, true);
callback();
} catch (err) {
err.userData = {request:cmd};
utils.logError("RpcError-001", err, {request:cmd});
logStats(cmd, false, new Date().getTime() - startTime, false);
reject(err);
callback();
}
};
rpcQueue.push({rpcCall:rpcCall});
});
}
function getRpcDataWithParams(request, verifyingConnection=false) {
let startTime = new Date().getTime();
if (!verifyingConnection && !global.rpcConnected) {
return Promise.reject(new Error("No RPC connection available. Check your connection/authentication parameters."));
}
return new Promise(function(resolve, reject) {
debugLog(`RPC: ${JSON.stringify(request)}`);
let rpcCall = async function(callback) {
let client = (request.method == "gettxoutsetinfo" ? global.rpcClientNoTimeout : global.rpcClient);
try {
const rpcResult = await client.request(request.method, request.parameters);
const result = rpcResult.result;
//console.log(`RPC: request=${request}, result=${JSON.stringify(result)}`);
if (Array.isArray(result) && result.length == 1) {
let result0 = result[0];
if (result0 && result0.name && result0.name == "RpcError") {
logStats(request.method, true, new Date().getTime() - startTime, false);
debugLog("RpcErrorResult-03: request=" + JSON.stringify(request) + ", result=" + JSON.stringify(result0));
throw new Error(`RpcError: type=errorResponse-03`);
}
}
if (result && result.name && result.name == "RpcError") {
logStats(request.method, true, new Date().getTime() - startTime, false);
debugLog("RpcErrorResult-04: " + JSON.stringify(result));
throw new Error(`RpcError: type=errorResponse-04`);
}
resolve(result);
logStats(request.method, true, new Date().getTime() - startTime, true);
callback();
} catch (err) {
err.userData = {request:request};
utils.logError("RpcError-002", err, {request:`${request.method}${request.parameters ? ("(" + JSON.stringify(request.parameters) + ")") : ""}`});
logStats(request.method, true, new Date().getTime() - startTime, false);
reject(err);
callback();
}
};
rpcQueue.push({rpcCall:rpcCall});
});
}
function unsupportedPromise(minRpcVersionNeeded) {
return new Promise(function(resolve, reject) {
resolve({success:false, error:"Unsupported", minRpcVersionNeeded:minRpcVersionNeeded});
});
}
function logStats(cmd, hasParams, dt, success) {
if (!global.rpcStats[cmd]) {
global.rpcStats[cmd] = {count:0, withParams:0, time:0, successes:0, failures:0};
}
global.rpcStats[cmd].count++;
global.rpcStats[cmd].time += dt;
statTracker.trackPerformance(`rpc.${cmd}`, dt);
statTracker.trackPerformance(`rpc.*`, dt);
if (hasParams) {
global.rpcStats[cmd].withParams++;
}
if (success) {
global.rpcStats[cmd].successes++;
statTracker.trackEvent(`rpc-result.${cmd}.success`);
statTracker.trackEvent(`rpc-result.*.success`);
} else {
global.rpcStats[cmd].failures++;
statTracker.trackEvent(`rpc-result.${cmd}.failure`);
statTracker.trackEvent(`rpc-result.*.failure`);
}
}
module.exports = {
getRpcData: getRpcData,
getRpcDataWithParams: getRpcDataWithParams,
getBlockchainInfo: getBlockchainInfo,
getDeploymentInfo: getDeploymentInfo,
getBlockCount: getBlockCount,
getNetworkInfo: getNetworkInfo,
getNetTotals: getNetTotals,
getMempoolInfo: getMempoolInfo,
getAllMempoolTxids: getAllMempoolTxids,
getMiningInfo: getMiningInfo,
getIndexInfo: getIndexInfo,
getBlockByHeight: getBlockByHeight,
getBlockByHash: getBlockByHash,
getRawTransaction: getRawTransaction,
getUtxo: getUtxo,
getMempoolTxDetails: getMempoolTxDetails,
getRawMempool: getRawMempool,
getUptimeSeconds: getUptimeSeconds,
getHelp: getHelp,
getRpcMethodHelp: getRpcMethodHelp,
getAddress: getAddress,
getPeerInfo: getPeerInfo,
getChainTxStats: getChainTxStats,
getSmartFeeEstimate: getSmartFeeEstimate,
getUtxoSetSummary: getUtxoSetSummary,
getNetworkHashrate: getNetworkHashrate,
getBlockStats: getBlockStats,
getBlockStatsByHeight: getBlockStatsByHeight,
getBlockHeaderByHash: getBlockHeaderByHash,
getBlockHeaderByHeight: getBlockHeaderByHeight,
getBlockHashByHeight: getBlockHashByHeight,
getTxOut: getTxOut,
getBlockTemplate: getBlockTemplate,
minRpcVersions: minRpcVersions
};