Source: blockchains/ethereum/rpc.js

'use strict';
/**
 * @module lib/blockchains/ethereum/rpc
 * @summary Whiteflag API Ethereum RPC module
 * @description Module to connect to the Ethereum network through a Ethereum node
 */
module.exports = {
    init: initRpc,
    getBalance,
    getBlockByNumber,
    getChainId,
    getGasPrice,
    getHighestBlock,
    getNetworkId,
    getNodeInfo,
    getPeerCount,
    getProtocolVersion,
    getRawTransaction,
    getTransactionCount,
    getTransactionReceipt,
    isSyncing,
    sendSignedTransaction
};

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

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

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

// Ethereum sub-modules //
const { formatHexEthereum } = require('./common');

// Module constants //
const STATUSINTERVAL = 60000; // Every minute
const INFOINTERVAL = 3600000; // Every hour

// Module variables //
let _blockchainName;
let _ethState;
let _web3;
let _chainID = 1;
let _rpcTimeout = 10000;

/**
 * Initialises Ethereum RPC
 * @function initRpc
 * @alias module:lib/blockchains/ethereum/rpc.init
 * @param {Object} ethConfig the Ethereum blockchain configuration
 * @param {Object} ethState the Ethereum blockchain state
 * @returns {Promise} resolve if succesfully initialised
 */
function initRpc(ethConfig, ethState) {
    _blockchainName = ethConfig.name;
    _ethState = ethState;

    // Get Node URL and credentials
    const rpcAuthURL = getNodeURL(ethConfig, false);
    const rpcCleanURL = getNodeURL(ethConfig, true); // no credentials
    _ethState.parameters.rpcURL = rpcCleanURL;
    _web3 = new Web3(rpcAuthURL);
    log.info(_blockchainName, `Using web3 version: ${_web3.version}`);

    // Connect to the Ethereum node
    log.trace(_blockchainName, `Setting up connection with Ethereum node: ${rpcCleanURL}`);
    return new Promise((resolve, reject) => {
        // Check Ethereum chain identifier
        getChainId()
        .then(chainID => {
            // Check for correct chain configuration
            if (ethConfig.chainID) _chainID = ethConfig.chainID;
            if (chainID !== _chainID) {
                return reject(new Error(`The node's Chain ID ${chainID} does not correspond with configured Chain ID ${_chainID}`));
            }
            // We have a connection
            _ethState.parameters.chainID = chainID;
            log.info(_blockchainName, `Connected to Ethereum chain ${chainID} through node: ${rpcCleanURL}`);

            // Set configured timeout
            if (ethConfig.rpcTimeout && ethConfig.rpcTimeout > 500) {
                _rpcTimeout = ethConfig.rpcTimeout;
            }
            log.info(_blockchainName, `Timeout for remote calls to the Ethereum node: ${_rpcTimeout} ms`);

            // Periodically update node parameters and status information
            updateNodeInfo(); setInterval(updateNodeInfo, INFOINTERVAL);
            updateNodeStatus(); setInterval(updateNodeStatus, STATUSINTERVAL);

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

/**
 * Gets the balance of the specified Ethereum blockchain account
 * @function getBalance
 * @alias module:lib/blockchains/ethereum/rpc.getBalance
 * @param {string} address
 * @returns {Promise}
 */
function getBalance(address) {
    return timeoutPromise(_web3.eth.getBalance(formatHexEthereum(address)), _rpcTimeout);
}

/**
 * Gets a block including transaction data by its block number
 * @function getBlockByNumber
 * @alias module:lib/blockchains/ethereum/rpc.getBlockByNumber
 * @param {Object} blockNumber
 * @returns {Promise} resolves to block with transactions
 */
function getBlockByNumber(blockNumber) {
    return timeoutPromise(_web3.eth.getBlock(blockNumber), _rpcTimeout);
}

/**
 * Gets the chain identifier of the node
 * @function getChainId
 * @alias module:lib/blockchains/ethereum/rpc.getChainId
 * @returns {Promise} resolves to the chain identifier
 */
function getChainId() {
    return timeoutPromise(_web3.eth.getChainId(), _rpcTimeout);
}

/**
 * Gets the current gas price
 * @function getGasPrice
 * @alias module:lib/blockchains/ethereum/rpc.getGasPrice
 * @returns {Promise} resolves to gas price
 */
 function getGasPrice() {
    return timeoutPromise(_web3.eth.getGasPrice(), _rpcTimeout);
}

/**
 * Gets the highest block, i.e. the current block count of the longest chains
 * @function getHighestBlock
 * @alias module:lib/blockchains/ethereum/rpc.getHighestBlock
 * @returns {Promise} resolves to highest known block number
 */
function getHighestBlock() {
    return timeoutPromise(_web3.eth.getBlockNumber(), (_rpcTimeout));
}

/**
 * Gets the software and version of the node
 * @function getNodeInfo
 * @alias module:lib/blockchains/ethereum/rpc.getNodeInfo
 * @returns {Promise} resolves to the node information
 */
 function getNodeInfo() {
    return timeoutPromise(_web3.eth.getNodeInfo(), _rpcTimeout);
}

/**
 * Gets the identifier of the network the node is connected to
 * @function getNetworkId
 * @alias module:lib/blockchains/ethereum/rpc.getNetworkId
 * @returns {Promise} resolves to the network identifier
 */
function getNetworkId() {
    return timeoutPromise(_web3.eth.net.getId(), _rpcTimeout);
}

/**
 * Gets the number of peers the node is connected to
 * @function getPeerCount
 * @alias module:lib/blockchains/ethereum/rpc.getPeerCount
 * @returns {Promise} resolves to the number of peers
 */
function getPeerCount() {
    return timeoutPromise(_web3.eth.net.getPeerCount(), _rpcTimeout);
}

/**
 * Gets the Ethereum protocol version of the node
 * @function getProtocolVersion
 * @alias module:lib/blockchains/ethereum/rpc.getProtocolVersion
 * @returns {Promise} resolves to the protocol version
 */
function getProtocolVersion() {
    return timeoutPromise(_web3.eth.getProtocolVersion(), _rpcTimeout);
}

/**
 * Gets a single transaction from the Ethereum blockchain under a timeout
 * @function getRawTransaction
 * @alias module:lib/blockchains/ethereum/rpc.getRawTransaction
 * @param {string} transactionHash
 * @returns {Promise} resolved to the transaction
 */
function getRawTransaction(transactionHash) {
    return timeoutPromise(_web3.eth.getTransaction(formatHexEthereum(transactionHash)), _rpcTimeout);
}

/**
 * Gets the transaction count of the specified Ethereum blockchain account
 * @function getTransactionCount
 * @alias module:lib/blockchains/ethereum/rpc.getTransactionCount
 * @param {string} address
 * @returns {Promise} resolves to the numer of transactions
 */
function getTransactionCount(address) {
    return timeoutPromise(_web3.eth.getTransactionCount(formatHexEthereum(address)), _rpcTimeout);
}

/**
 * Gets the receipt for the specified transaction
 * @function getTransactionReceipt
 * @alias module:lib/blockchains/ethereum/rpc.getTransactionReceipt
 * @param {string} transactionHash
 * @returns {Promise} resolves to a tranasction receipt
 */
function getTransactionReceipt(transactionHash) {
    timeoutPromise(_web3.eth.getTransactionReceipt(formatHexEthereum(transactionHash)), _rpcTimeout);
}

/**
 * Checks if the node is syncing
 * @function isSyncing
 * @alias module:lib/blockchains/ethereum/rpc.isSyncing
 * @returns {Promise} resolves to syncing infomration
 */
function isSyncing() {
    return timeoutPromise(_web3.eth.isSyncing(), _rpcTimeout);
}

/**
 * Sends a raw signed transaction
 * @function getTransactionReceipt
 * @alias module:lib/blockchains/ethereum/rpc.getTransactionReceipt
 * @param {string} rawTransaction the raw signed transaction to be sent
 * @returns {Promise} resolves to a tranasction receipt
 */
function sendSignedTransaction(rawTransaction) {
    return timeoutPromise(_web3.eth.sendSignedTransaction(rawTransaction), (_rpcTimeout * 5));
}

// PRIVATE RPC FUNCTIONS //
/**
 * Gets URL of the Ethereum blockchain node from the configuration
 * @private
 * @param {Object} ethConfig blockchain configuration parameters
 * @param {boolean} hideCredentials whether to inlude username and password in url
 * @returns {string} the url of the Ethereum node
 */
 function getNodeURL(ethConfig, hideCredentials = false) {
    const rpcProtocol = (ethConfig.rpcProtocol || 'http') + '://';
    const rpcHost = ethConfig.rpcHost || 'localhost';
    const rpcPort = ':' + (ethConfig.rpcPort || '8545');
    const rpcPath = ethConfig.rpcPath || '';
    let rpcAuth = '';
    if (ethConfig.username && ethConfig.password && !hideCredentials) {
        rpcAuth = ethConfig.username + ':' + ethConfig.password + '@';
    }
    return (rpcProtocol + rpcAuth + rpcHost + rpcPort + rpcPath);
}

// PRIVATE BLOCKCHAIN STATUS FUNCTIONS //
/**
 * Requests some semi-static Ethereum blockchain information
 * @private
 */
async function updateNodeInfo() {
    // Get node information
    await getNodeInfo()
    .then(nodeInfo => (_ethState.parameters.nodeInfo = nodeInfo))
    .catch(err => log.warn(_blockchainName, `Could not get node software and version information: ${err.message}`));

    // Get protocol version
    await getProtocolVersion()
    .then(protocolVersion => (_ethState.parameters.protocolVersion = protocolVersion))
    .catch(err => log.warn(_blockchainName, `Could not get protocol version from node: ${err.message}`));

    // Get network ID
    await getNetworkId()
    .then(networkID => (_ethState.parameters.networkID = networkID))
    .catch(err => log.warn(_blockchainName, `Could not get Network ID from node: ${err.message}`));

    // Check chain ID
    await getChainId()
    .then(chainID => {
        if (chainID !== _chainID) {
            log.warn(_blockchainName, `The node's Chain ID has changed to ${chainID} and does not correspond with the configured Chain ID ${_chainID}`);
        }
        _ethState.parameters.chainID = chainID;
    })
    .catch(err => log.warn(_blockchainName, `Could not get Chain ID from node: ${err.message}`));
}

/**
 * Requests some dynamic Ethereum blockchain node status information
 * @private
 */
async function updateNodeStatus() {
    _ethState.status.updated = new Date().toISOString();

    // Get peer status
    await getPeerCount()
    .then(peers => (_ethState.status.peers = peers))
    .catch(err => log.warn(_blockchainName, `Could not get peer status: ${err.message}`));

    // Get synchronisation status
    await isSyncing()
    .then(syncing => (_ethState.status.syncing = syncing))
    .catch(err => log.warn(_blockchainName, `Could not get synchronisation status: ${err.message}`));

    // Get gas price
    await getGasPrice()
    .then(gasPrice => (_ethState.status.gasPrice = gasPrice))
    .catch(err => log.warn(_blockchainName, `Could not get gas price: ${err.message}`));

    // Log node status
    saveNodeStatus();
}

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