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
};

// Node.js core and external modules //
const request = require('request');

// Whiteflag common functions and classes //
const log = require('../../common/logger');
const { timeoutPromise, ignore } = require('../../common/processing');

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

// Module constants //
const STATUSINTERVAL = 1200000; // Every two minutes
const INFOINTERVAL = 3600000; // Every hour

// Module variables //
let _blockchainName;
let _bcState;
let _chain;
let _rpcTimeout = 10000;
let _rpcURL;
let _rpcUser;
let _rpcPass;

/**
 * Intitialises Bitcoin RPC connection
 * @function initRpc
 * @alias module:lib/blockchains/bitcoin/rpc.init
 * @param {Object} bcConfig the Bitcoin blockchain configuration
 * @param {Object} bcState the Bitcoin blockchain state
 */
function initRpc(bcConfig, bcState) {
    _blockchainName = bcConfig.name;
    _bcState = bcState;
    log.trace(_blockchainName, 'Initialising Bitcoin RPC connection...');

    // Get Node URL and credentials
    _rpcURL = getNodeURL(bcConfig);
    _bcState.parameters.rpcURL = _rpcURL;
    _rpcUser = bcConfig.username;
    _rpcPass = bcConfig.password;

    // Connect to Bitcoin node
    return new Promise((resolve, reject) => {
        // Check Bitcoin chain
        getBlockChainInfo()
        .then(bcInfo => {
            // Check for correct chain configuration
            const chain = bcInfo.chain;
            if (bcConfig.testnet && chain !== 'test') {
                return reject(new Error(`Configured to use the Bitcoin test network but the node is on the ${chain} chain`));
            }
            if (!bcConfig.testnet && chain === 'test') {
                return reject(new Error(`Configured to use the Bitcoin main network but the node is on the ${chain} chain`));
            }
            // We have a connection
            _chain = chain;
            _bcState.parameters.chain = chain;
            log.info(_blockchainName, `Connected to Bitcoin ${chain} chain through node: ${_rpcURL}`);

            // RPC timeout period
            if (bcConfig.rpcTimeout && bcConfig.rpcTimeout > 500) {
                _rpcTimeout = bcConfig.rpcTimeout;
            }
            log.info(_blockchainName, `Timeout for remote calls to the Bitcoin node: ${_rpcTimeout} ms`);

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

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

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

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

/**
 * Gets the current block count of the longest chains
 * @returns {Object} resolves to block count
 */
 function getBlockCount() {
    return rpcCall('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 rpcCall('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} full get the full block including all transactions
 * @returns {Promise} resolves to a JOSN representation of the block
 */
function getBlockByHash(blockHash, full = false) {
    let verbosity = 1;
    if (full) verbosity = 2;
    return rpcCall('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 JOSN representation of the block
 */
function getFeeRate(blocks) {
    return rpcCall('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 rpcCall('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 rpcCall('sendrawtransaction', [rawTransaction]);
}

// PRIVATE NODE STATUS FUNCTIONS //
/**
 * Requests some Bitcoin node information
 * @private
 */
async function updateNodeInfo() {
    // Get basic blockchain info
    await getBlockChainInfo()
    .then(bcInfo => {
        if (bcInfo.chain !== _chain) {
            log.warn(_blockchainName, `The node's chain has changed to ${bcInfo.chain} and does not correspond with the configured chain ${_chain}`);
        }
        _bcState.parameters.chain = bcInfo.chain;
    })
    .catch(err => log.warn(_blockchainName, `Could not get network: ${err.message}`));
}

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

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

    // Get fee rate
    await getFeeRate(1)
    .then(estimate => (_bcState.status.feerate = estimate.feerate))
    .catch(err => log.warn(_blockchainName, `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(_blockchainName, _bcState);
    log.info(_blockchainName, `Status: {${JSON.stringify(_bcState.parameters)}, ${JSON.stringify(_bcState.status)}}`);
}

// PRIVATE RPC FUNCTIONS //
/**
 * Makes a connection with a node
 * @function rpcCall
 * @private
 * @param {string} method the rpc method
 * @param {string} params the parameters for the rpc method
 * @returns {Promise}
 */
function rpcCall(method, params) {
    // Prepare request
    let rpcOptions = {
        url: _rpcURL,
        auth: {
            user: _rpcUser,
            password: _rpcPass
        },
        method: 'post',
        headers:
        {
            'content-type': 'text/plain'
        },
        body: JSON.stringify({
            'jsonrpc': '2.0',
            'method': method,
            'params': params
        })
    };
    // Send request with a timeout
    // log.trace(_blockchainName, `Making remote procedure call: ${JSON.stringify(rpcOptions.body)}`);
    return timeoutPromise(
        new Promise((resolve, reject) => {
            request(rpcOptions, function rpcRequestCb(err, response, body) {
                if (err) return reject(err);
                ignore(response);
                try {
                    if (JSON.parse(body).error !== null) {
                        return reject(new Error(JSON.stringify(JSON.parse(body).error)));
                    }
                    resolve(JSON.parse(body).result);
                } catch(jsonErr) {
                    reject(jsonErr);
                }
            });
        }),
        _rpcTimeout
    );
}

/**
 * Gets URL of the Bitcoin blockchain node from the configuration
 * @private
 * @param {Object} bcConfig blockchain configuration parameters
 * @returns {string} the url of the Bitcoin node
 */
 function getNodeURL(bcConfig) {
    const rpcProtocol = (bcConfig.rpcProtocol || 'http') + '://';
    const rpcHost = bcConfig.rpcHost || 'localhost';
    const rpcPort = ':' + (bcConfig.rpcPort || '8545');
    const rpcPath = bcConfig.rpcPath || '';
    return (rpcProtocol + rpcHost + rpcPort + rpcPath);
}