node.js

'use strict'

const axios = require('axios')
const {RpcError} = require('./type')
const {isUnsignedInteger} = require('./validator')

/*eslint-disable no-unused-vars*/
const {UnsignedInteger} = require('./type')
/*eslint-enable no-unused-vars*/

/**
 * @enum {number}
 */
const NodeStatus = {
    OK: 0,
    DISCONNECTED: 1,
    ERROR: 2
}

/**
 * @typedef {object} NodeStat
 * @property {NodeStatus} status
 * @property {UnsignedInteger | undefined} blockNumber
 * @property {string | undefined} message
 */

/**
 * @typedef {object} NodeConfig
 * @property {UnsignedInteger} identity
 * @property {HttpUrl} [endpoint='http://localhost:8545']
 * @property {UnsignedInteger} [weight=1] - It is use for evalution and
 * distribution requests between nodes. The greater weight, the more
 * requests is dispatch to this node.
 */

class NodeError extends Error {
    constructor(message) {
        super(message)
        this.name = 'NodeError'
    }
}

/**
 * @private
 */
class Node {
    /**
     *
     * @param {NodeConfig} config
     */
    constructor(config) {
        let validConfig = Node._standardizeConfig(config)

        this._identity = validConfig.identity
        this._weight = validConfig.weight
        this._httpClient = axios.create({
            baseURL: validConfig.endpoint
        })
    }

    /**
     * @returns {UnsignedInteger}
     */
    get identity() {
        return this._identity
    }

    /**
     * @returns {NodeStatus | undefined}
     */
    get status() {
        return this._status
    }

    /**
     * @returns {UnsignedInteger | undefined}
     */
    get blockNumber() {
        return this._blockNumber
    }

    /**
     * @returns {any}
     */
    get error() {
        return this._error
    }

    /**
     * @returns {number}
     */
    get weight() {
        return this._weight
    }

    /**
     * Update state of this node itself. This method should be call
     * before read attributes.
     *
     * @returns {Promise<undefined>}
     */
    async updateStat() {
        try {
            this._blockNumber = await this._getBlockNumber()
            this._status = NodeStatus.OK
            this._error = undefined
        }
        catch (error) {
            this._status = NodeStatus.ERROR
            this._error = error
        }
    }

    /**
     * Perform a calling to ETH node.
     *
     * @param {string} method - Method to be call, see
     * [ETH JSON RPC](https://eth.wiki/json-rpc/API).
     * @param {Array<any>} params - Positional arguments to pass to method.
     * @returns {Promise<any>}
     */
    async call(method, params) {
        return await this._call(method, params)
    }

    /**
     * Retrieve number of latest block which is mined.
     *
     * @private
     * @returns {Promise<number>}
     */
    async _getBlockNumber() {
        let result = await this._call('eth_blockNumber', [])

        return Number.parseInt(result)
    }

    /**
     * @private
     * @param {string} method - Mehod to be call.
     * @param {Array<any>} params - Positional parameters.
     * @returns {Promise<any>}
     */
    async _call(method, params) {
        let response = await this._httpClient.post('/', {
            id: 1,
            jsonrpc: '2.0',
            method: method,
            params: params
        })
        let {error, result} = response.data

        if (error) {
            throw new RpcError(error)
        }

        return result
    }

    /**
     *
     * @param {any} config
     * @returns {NodeConfig}
     */
    static _standardizeConfig(config) {
        if (!isUnsignedInteger(config.identity)) {
            throw new NodeError('invalid identity')
        }

        let defaultConfig = {
            endpoint: 'http://localhost:8545',
            weight: 1
        }

        return Object.assign(defaultConfig, config)
    }
}

module.exports = {
    Node,
    NodeStatus,
    NodeError
}