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');

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

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

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

/* Module constants */
const MODULELOG = 'ethereum';
const DEFAULTPORT = '8545';
const STATUSINTERVAL = 60000; // Every minute
const INFOINTERVAL = 3600000; // Every hour

/* Module variables */
let _ethChain;
let _ethState;
let _ethApi;
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) {
    _ethChain = ethConfig.name;
    _ethState = ethState;

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

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

    // Connect to the Ethereum node
    log.trace(MODULELOG, `Setting up connection with ${_ethChain} node: ${rpcCleanURL}`);
    return new Promise((resolve, reject) => {
        // Check Ethereum chain identifier
        getChainId()
        .then(chainID => {
            // Check for correct chain configuration
            if (Object.hasOwn(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(MODULELOG, `Connected to ${_ethChain} chain ${chainID} through node: ${rpcCleanURL}`);

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

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

/**
 * 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) {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.eth.getBalance(withHexPrefix(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) {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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) {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.eth.getTransaction(withHexPrefix(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) {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.eth.getTransactionCount(withHexPrefix(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) {
    if (!_ethApi) return notInitialized();
    timeoutPromise(_ethApi.eth.getTransactionReceipt(withHexPrefix(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() {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.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) {
    if (!_ethApi) return notInitialized();
    return timeoutPromise(_ethApi.eth.sendSignedTransaction(rawTransaction), (_rpcTimeout * 5));
}

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

/**
 * 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(MODULELOG, `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(MODULELOG, `Could not get protocol version from node: ${err.message}`));

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

    // Check chain ID
    await getChainId()
    .then(chainID => {
        if (chainID !== _chainID) {
            log.warn(MODULELOG, `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(MODULELOG, `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(MODULELOG, `Could not get peer status: ${err.message}`));

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

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

    // Log node status
    saveNodeStatus();
}

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