Source: blockchains/bitcoin/rpc.js

'use strict';
/**
 * @module lib/blockchains/bitcoin/rpc
 * @summary Whiteflag API Bitcoin RPC module
 * @description Module to connect to the Bitcoin network through a Bitcoin node
 */
module.exports = {
    init: initRpc,
    getConnectionCount,
    getBlockChainInfo,
    getBlockCount,
    getBlockHash,
    getBlockByNumber,
    getBlockByHash,
    getFeeRate,
    getRawTransaction,
    sendRawTransaction
};

/* Common internal functions and classes */
const log = require('../../_common/logger');
const { ProcessingError } = require('../../_common/errors');

/* Whiteflag modules */
const wfState = require('../../protocol/state');

/* Common blockchain functions */
const { rpcCall,
        getNodeURL } = require('../_common/rpc');

/* Module constants */
const MODULELOG = 'bitcoin';
const DEFAULTPORT = '8332';
const STATUSINTERVAL = 1200000; // Every two minutes
const INFOINTERVAL = 3600000; // Every hour

/* Module variables */
let _btcChain;
let _btcState;
let _btcChainID;
let _rpcInit = false;
let _rpcTimeout = 10000;
let _rpcAuthURL;
let _rpcUser;
let _rpcPass;

/**
 * Intitialises Bitcoin RPC connection
 * @function initRpc
 * @alias module:lib/blockchains/bitcoin/rpc.init
 * @param {Object} btcConfig the Bitcoin blockchain configuration
 * @param {Object} btcState the Bitcoin blockchain state
 */
function initRpc(btcConfig, btcState) {
    _btcChain = btcConfig.name;
    _btcState = btcState;

    // RPC timeout period
    if (Object.hasOwn(btcConfig, 'rpcTimeout') && btcConfig.rpcTimeout > 500) {
        _rpcTimeout = btcConfig.rpcTimeout;
    }
    log.info(MODULELOG, `Timeout for remote calls to the ${_btcChain} node: ${_rpcTimeout} ms`);

    // Get Node URL and credentials
    const rpcCleanURL = getNodeURL(btcConfig, true, DEFAULTPORT);   // no credentials
    _rpcAuthURL = getNodeURL(btcConfig, false, DEFAULTPORT);        // include credentials
    _btcState.parameters.rpcURL = rpcCleanURL;
    _rpcUser = btcConfig.rpcUsername;
    _rpcPass = btcConfig.rpcPassword;

    // Connect to Bitcoin node
    log.trace(MODULELOG, `Setting up connection with ${_btcChain} node: ${rpcCleanURL}`);
    _rpcInit = true;
    return new Promise((resolve, reject) => {
        getBlockChainInfo()
        .then(btcInfo => {
            // Check for correct chain configuration
            const chain = btcInfo.chain;
            if (btcConfig.testnet && chain !== 'test') {
                return reject(new Error(`Configured to use the ${_btcChain} (test) network but the node is on the ${chain} chain`));
            }
            if (!btcConfig.testnet && chain === 'test') {
                return reject(new Error(`Configured to use the ${_btcChain} (main) network but the node is on the ${chain} chain`));
            }
            // We have a connection
            _btcChainID = chain;
            _btcState.parameters.chain = chain;
            log.info(MODULELOG, `Connected to ${_btcChain} ${chain} chain through node: ${_btcState.parameters.rpcURL}`);

            // Initialise node status monitoring
            updateNodeInfo(); setInterval(updateNodeInfo, INFOINTERVAL);
            updateNodeStatus(); setInterval(updateNodeStatus, STATUSINTERVAL);

            // Succesfully completed initialisation
            return resolve();
        })
        .catch(err => {
            _rpcInit = false;
            reject(new Error(`Could not connect to ${_btcChain} node: ${err.message}`), _btcChain)
        });
    });
}

/* RPC CALL WRAPPER FUNCTIONS */
/**
 * Get number of connections of the node to other nodes
 * @returns {Promise} resolves to connection count
 */
 function getConnectionCount() {
    return rpc('getconnectioncount');
}

/**
 * Gets various information from the node regarding blockchain processing
 * @returns {Promise} resolves to object with blockchain info
 */
 function getBlockChainInfo() {
    return rpc('getblockchaininfo');
}

/**
 * Gets the current block count of the longest chains
 * @returns {Object} resolves to block count
 */
 function getBlockCount() {
    return rpc('getblockcount');
}

/**
 * Gets the hash of the block specified by its block number
 * @param {Object} blockNumber
 * @returns {Promise} resolves to block hash
 */
 function getBlockHash(blockNumber) {
    return rpc('getblockhash', [blockNumber]);
}

/**
 * Gets a block by its number including transaction data
 * @param {Object} blockNumber
 * @returns {Promise} resolves to block including transactions
 */
function getBlockByNumber(blockNumber, full = false) {
    return getBlockHash(blockNumber)
    .then(blockHash => {
        return getBlockByHash(blockHash, full);
    })
    .catch(err => {
        return Promise.reject(err);
    });
}

/**
 * Gets a block by its hash
 * @param {string} blockHash
 * @param {boolean} transactions get the full block including all transactions
 * @returns {Promise} resolves to a JSON representation of the block
 */
function getBlockByHash(blockHash, transactions = false) {
    let verbosity = 1;
    if (transactions) verbosity = 2;
    return rpc('getblock', [blockHash, verbosity]);
}

/**
 * Gets an estimate of the fee rate in BTC/kB
 * @param {number} blocks the number of blocks
 * @returns {Promise} resolves to a JSON representation of the block
 */
function getFeeRate(blocks) {
    return rpc('estimatesmartfee', [blocks]);
}


/**
 * Gets raw transaction data
 * @param {string} transactionHash the transaction hash, aka txid
 * @returns {Object} resolves to raw transaction data
 */
 function getRawTransaction(transactionHash) {
    return rpc('getrawtransaction', [transactionHash, true]);
}

/**
 * Send a raw signed transaction
 * @param {Object} rawTransaction raw transaction
 * @returns {Promise} resolves to the transaction hash, aka txid
 */
function sendRawTransaction(rawTransaction) {
    return rpc('sendrawtransaction', [rawTransaction]);
}

/* PRIVATE NODE STATUS FUNCTIONS */
/**
 * Requests some Bitcoin node information
 * @private
 */
async function updateNodeInfo() {
    // Get basic blockchain info
    await getBlockChainInfo()
    .then(btcInfo => {
        if (btcInfo.chain !== _btcChainID) {
            log.warn(MODULELOG, `The ${_btcChain} node changed from the ${_btcChainID} chain to the ${btcInfo.chain} chain`);
        }
        _btcState.parameters.chain = btcInfo.chain;
    })
    .catch(err => log.warn(MODULELOG, `Could not get network: ${err.message}`));
}

/**
 * Requests some Bitcoin blockchain status information
 * @private
 */
async function updateNodeStatus() {
    _btcState.status.updated = new Date().toISOString();

    // Get number of peers
    await getConnectionCount()
    .then(peers => (_btcState.status.peers = peers))
    .catch(err => log.warn(MODULELOG, `Could not get peer connection count: ${err.message}`));

    // Get fee rate
    await getFeeRate(1)
    .then(estimate => (_btcState.status.feerate = estimate.feerate))
    .catch(err => log.warn(MODULELOG, `Could not get transaction fee rate estimate: ${err.message}`));

    // Save and log node status
    saveNodeStatus();
}

/**
 * Logs the Bitcoin node status information
 * @private
 */
function saveNodeStatus() {
    wfState.updateBlockchainData(_btcChain, _btcState);
    log.info(MODULELOG, `Status: {${JSON.stringify(_btcState.parameters)}, ${JSON.stringify(_btcState.status)}}`);
}

/* PRIVATE RPC FUNCTIONS */
/**
 * Calls the common RPC function
 * @function rpc
 * @private
 * @param {string} method the rpc method
 * @param {array} params the parameters for the rpc method
 * @returns {Promise}
 */
function rpc(method, params = []) {
    if (!_rpcInit) return Promise.reject(notInitialized());
    return rpcCall(method, params, _rpcAuthURL, _rpcUser, _rpcPass, _rpcTimeout);
}

/**
 * Returns an error that the connextion has not been initializes
 * @private
 * @returns {ProtocolError}
 */
function notInitialized() {
    return new ProcessingError('No connection to a Bitcoin node has been initialized', null, 'WF_API_NOT_AVAILABLE');
}