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.
507 lines
12 KiB
507 lines
12 KiB
var debug = require('debug');
|
|
|
|
var debugLog = debug("btcexp:rpc");
|
|
|
|
var async = require("async");
|
|
var semver = require("semver");
|
|
|
|
var utils = require("../utils.js");
|
|
var config = require("../config.js");
|
|
var coins = require("../coins.js");
|
|
|
|
var activeQueueTasks = 0;
|
|
|
|
var rpcQueue = async.queue(function(task, callback) {
|
|
activeQueueTasks++;
|
|
//debugLog("activeQueueTasks: " + activeQueueTasks);
|
|
|
|
task.rpcCall(function() {
|
|
callback();
|
|
|
|
activeQueueTasks--;
|
|
//debugLog("activeQueueTasks: " + activeQueueTasks);
|
|
});
|
|
|
|
}, config.rpcConcurrency);
|
|
|
|
var minRpcVersions = {getblockstats:"0.17.0"};
|
|
|
|
global.rpcStats = {};
|
|
|
|
|
|
|
|
function getBlockchainInfo() {
|
|
return getRpcData("getblockchaininfo");
|
|
}
|
|
|
|
function getNetworkInfo() {
|
|
return getRpcData("getnetworkinfo");
|
|
}
|
|
|
|
function getNetTotals() {
|
|
return getRpcData("getnettotals");
|
|
}
|
|
|
|
function getMempoolInfo() {
|
|
return getRpcData("getmempoolinfo");
|
|
}
|
|
|
|
function getMiningInfo() {
|
|
return getRpcData("getmininginfo");
|
|
}
|
|
|
|
function getUptimeSeconds() {
|
|
return getRpcData("uptime");
|
|
}
|
|
|
|
function getPeerInfo() {
|
|
return getRpcData("getpeerinfo");
|
|
}
|
|
|
|
function getMempoolTxids() {
|
|
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() {
|
|
return getRpcData("gettxoutsetinfo");
|
|
}
|
|
|
|
function getRawMempool() {
|
|
return new Promise(function(resolve, reject) {
|
|
getRpcDataWithParams({method:"getrawmempool", parameters:[false]}).then(function(txids) {
|
|
var promises = [];
|
|
|
|
for (var i = 0; i < txids.length; i++) {
|
|
var txid = txids[i];
|
|
|
|
promises.push(getRawMempoolEntry(txid));
|
|
}
|
|
|
|
Promise.all(promises).then(function(results) {
|
|
var finalResult = {};
|
|
|
|
for (var 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) {
|
|
return getRpcDataWithParams({method:"getchaintxstats", parameters:[blockCount]});
|
|
}
|
|
|
|
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 getBlockByHash(blockHash) {
|
|
debugLog("getBlockByHash: %s", blockHash);
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
getRpcDataWithParams({method:"getblock", parameters:[blockHash]}).then(function(block) {
|
|
getRawTransaction(block.tx[0]).then(function(tx) {
|
|
block.coinbaseTx = tx;
|
|
block.totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight(tx, block.height);
|
|
block.subsidy = coinConfig.blockRewardFunction(block.height, global.activeBlockchain);
|
|
block.miner = utils.getMinerFromCoinbaseTx(tx);
|
|
|
|
resolve(block);
|
|
|
|
}).catch(function(err) {
|
|
reject(err);
|
|
});
|
|
}).catch(function(err) {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getAddress(address) {
|
|
return getRpcDataWithParams({method:"validateaddress", parameters:[address]});
|
|
}
|
|
|
|
function getRawTransaction(txid) {
|
|
debugLog("getRawTransaction: %s", txid);
|
|
|
|
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) {
|
|
var 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 {
|
|
getRpcDataWithParams({method:"getrawtransaction", parameters:[txid, 1]}).then(function(result) {
|
|
if (result == null || result.code && result.code < 0) {
|
|
reject(result);
|
|
|
|
return;
|
|
}
|
|
|
|
resolve(result);
|
|
|
|
}).catch(function(err) {
|
|
reject(err);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function getUtxo(txid, outputIndex) {
|
|
debugLog("getUtxo: %s (%d)", 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) {
|
|
debugLog("getMempoolTxDetails: %s", txid);
|
|
|
|
var promises = [];
|
|
|
|
var 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 getHelp() {
|
|
return getRpcData("help");
|
|
}
|
|
|
|
function getRpcMethodHelp(methodName) {
|
|
return getRpcDataWithParams({method:"help", parameters:[methodName]});
|
|
}
|
|
|
|
|
|
|
|
function getRpcData(cmd) {
|
|
var startTime = new Date().getTime();
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
debugLog(`RPC: ${cmd}`);
|
|
|
|
rpcCall = function(callback) {
|
|
var client = (cmd == "gettxoutsetinfo" ? global.rpcClientNoTimeout : global.rpcClient);
|
|
|
|
client.command(cmd, function(err, result, resHeaders) {
|
|
try {
|
|
if (err) {
|
|
logStats(cmd, false, new Date().getTime() - startTime, false);
|
|
|
|
throw new Error(`RpcError: type=failure-01`);
|
|
}
|
|
|
|
if (Array.isArray(result) && result.length == 1) {
|
|
var result0 = result[0];
|
|
|
|
if (result0 && result0.name && result0.name == "RpcError") {
|
|
logStats(cmd, false, new Date().getTime() - startTime, false);
|
|
|
|
throw new Error(`RpcError: type=errorResponse-01`);
|
|
}
|
|
}
|
|
|
|
if (result.name && result.name == "RpcError") {
|
|
logStats(cmd, false, new Date().getTime() - startTime, false);
|
|
|
|
throw new Error(`RpcError: type=errorResponse-02`);
|
|
}
|
|
|
|
resolve(result);
|
|
|
|
logStats(cmd, false, new Date().getTime() - startTime, true);
|
|
|
|
callback();
|
|
|
|
} catch (e) {
|
|
e.userData = {error:err, request:cmd, result:result};
|
|
|
|
utils.logError("9u4278t5h7rfhgf", e, {error:err, request:cmd, result:result});
|
|
|
|
reject(e);
|
|
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
|
|
rpcQueue.push({rpcCall:rpcCall});
|
|
});
|
|
}
|
|
|
|
function getRpcDataWithParams(request) {
|
|
var startTime = new Date().getTime();
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
debugLog(`RPC: ${JSON.stringify(request)}`);
|
|
|
|
rpcCall = function(callback) {
|
|
global.rpcClient.command([request], function(err, result, resHeaders) {
|
|
try {
|
|
if (err != null) {
|
|
logStats(request.method, true, new Date().getTime() - startTime, false);
|
|
|
|
throw new Error(`RpcError: type=failure-02`);
|
|
}
|
|
|
|
if (Array.isArray(result) && result.length == 1) {
|
|
var result0 = result[0];
|
|
|
|
if (result0 && result0.name && result0.name == "RpcError") {
|
|
logStats(request.method, true, new Date().getTime() - startTime, false);
|
|
|
|
throw new Error(`RpcError: type=errorResponse-03`);
|
|
}
|
|
}
|
|
|
|
if (result.name && result.name == "RpcError") {
|
|
logStats(request.method, true, new Date().getTime() - startTime, false);
|
|
|
|
throw new Error(`RpcError: type=errorResponse-04`);
|
|
}
|
|
|
|
resolve(result[0]);
|
|
|
|
logStats(request.method, true, new Date().getTime() - startTime, true);
|
|
|
|
callback();
|
|
|
|
} catch (e) {
|
|
e.userData = {error:err, request:request, result:result};
|
|
|
|
utils.logError("283h7ewsede", e, {error:err, request:request, result:result});
|
|
|
|
reject(e);
|
|
|
|
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;
|
|
|
|
if (hasParams) {
|
|
global.rpcStats[cmd].withParams++;
|
|
}
|
|
|
|
if (success) {
|
|
global.rpcStats[cmd].successes++;
|
|
|
|
} else {
|
|
global.rpcStats[cmd].failures++;
|
|
}
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
getBlockchainInfo: getBlockchainInfo,
|
|
getNetworkInfo: getNetworkInfo,
|
|
getNetTotals: getNetTotals,
|
|
getMempoolInfo: getMempoolInfo,
|
|
getMempoolTxids: getMempoolTxids,
|
|
getMiningInfo: getMiningInfo,
|
|
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,
|
|
|
|
minRpcVersions: minRpcVersions
|
|
};
|