'use strict'
const {Node} = require('./node')
const {Gateway} = require('./gateway')
const {Diary} = require('./diary')
const {heximalToNumber, numberToHeximal} = require('./formatter')
const {
isUnsignedInteger,
isPositiveInteger,
isValidEndpoint,
isTransactionHashHeximal
} = require('./validator')
/*eslint-disable no-unused-vars*/
const {
Endpoint,
LogFilter,
Transaction,
TransactionHashHeximal,
PositiveInteger,
UnsignedInteger,
Block
} = require('./type')
/*eslint-enable no-unused-vars*/
/**
* @type Endpoint
*/
const DEFAULT_ENDPOINT = {
url: 'http://localhost:8545',
weight: 1
}
/**
* @typedef {object} ClientConfig
* @property {Array<Endpoint>} [endpoints=[DEFAULT_ENDPOINT]] - List of
* ETH RPC endpoints.
* @property {UnsignedInteger} [reorganisationBlocks=6] - There is a block
* `n = latest - reorganisationBlocks`. Where `latest` is newest mined block
* number. The client will not process related things that has block number
* greater than block number `n`.
* @property {PositiveInteger} [healthCheckInterval=3000] - For each time
* period, check health of nodes, in miliseconds. Default is 3000.
*/
class ClientError extends Error {
constructor(message) {
super(message)
this.name = 'ClientError'
}
}
/**
* * Interact with ETH node as accurate, durable and stable as possible.
* * Work with more than an ETH node, do load balancing and healthcheck.
* * WARN: It does not work with endpoints that does load balancing to
* other nodes.
*/
class Client {
/**
* @param {ClientConfig} config
*/
constructor(config) {
let validConfig = Client._standardizeConfig(config)
let nodes = validConfig.endpoints.map((endpoint, index) => {
return new Node({
identity: index + 1,
endpoint: endpoint.url,
weight: endpoint.weight
})
})
this._gateway = new Gateway(nodes, {
healthcheckInterval: validConfig.healthCheckInterval
})
this._reorganisationblocks = validConfig.reorganisationBlocks
}
async open() {
await this._gateway.open()
this._diary = new Diary(this._gateway)
}
async close() {
if (this._gateway) {
await this._gateway.close()
this._gateway = undefined
}
}
/**
* Safe block number `latest - reorganisationBlocks`, where
* `latest` is newest block number from nodes.
*
* @returns {number}
*/
get blockNumber() {
return this._gateway.safeBlockNumber
}
/**
*
* @param {UnsignedInteger} blockNumber
* @param {boolean} [includeTransaction=false]
* @returns {Promise<Block>}
*/
async getBlockByNumber(blockNumber, includeTransaction=false) {
if (!isUnsignedInteger(blockNumber)) {
throw new TypeError('not a unsigned integer')
}
let node = this._pickNode()
let blockNumberHeximal = numberToHeximal(blockNumber)
if (blockNumberHeximal === undefined) {
throw new ClientError('invalid block number')
}
let block = await node.call(
'eth_getBlockByNumber',
[blockNumberHeximal, includeTransaction]
)
return Client._standardizeBlock(block)
}
/**
* Retrieve log records.
* It is equivalent to RPC `eth_getLogs`.
*
* @param {LogFilter} filter - Matching conditions.
* @returns {Promise<Array<Log>>}
* @throws {DiaryError} - Call to unsafe block.
*/
async getLogs(filter) {
return await this._diary.getLogs(filter)
}
/**
* Retrieve a transaction by it's hash.
* It is equivalent to RPC `eth_getTransactionByHash`.
*
* @param {TransactionHashHeximal} txHash
* @returns {Transaction | undefined}
* @throws {ClientError}
*/
async getTransaction(txHash) {
if (!isTransactionHashHeximal(txHash)) {
throw new TypeError('not a transaction hash heximal')
}
let node = this._pickNode()
let transaction = await node.call(
'eth_getTransactionByHash',
[txHash]
)
Client._formatTransaction(transaction)
if (transaction.blockNumber > this._gateway.blockNumber) {
throw new ClientError('unsafe block number calling')
}
return transaction
}
/**
* Executes a new message call immediately without creating a transaction
* on the block chain.
* It is equivalent to RPC `eth_call`.
*
* @private
* @param {string} method - Method to be call.
* @param {Array} data - Positional arguments.
* @returns {Promise<any>}
*/
async _call(method, data) {
let node = this._pickNode()
return await node.call(method, data)
}
/**
* @private
* @returns {Node}
*/
_pickNode() {
let node = this._gateway.pickNode()
if (!node) {
throw new ClientError('no avaiable node')
}
return node
}
/**
* @private
* @param {ClientConfig} config
* @returns {InternalClientConfig}
* @throws {ClientError}
*/
static _standardizeConfig(config={}) {
let invalidName = Client._getInvalidConfigAttributeName(config)
if (invalidName) {
throw new ClientError('not accepted config.' + invalidName)
}
let validConfig = {
endpoints: [DEFAULT_ENDPOINT],
reorganisationBlocks: 6,
healthCheckInterval: 3000
}
Object.assign(validConfig, config)
Client._validateEndpoints(validConfig.endpoints)
if (!isUnsignedInteger(validConfig.reorganisationBlocks)) {
throw new ClientError('invalid config.reorganisationBlocks')
}
if (!isPositiveInteger(validConfig.healthCheckInterval)) {
throw new ClientError('invalid config.healthCheckInterval')
}
return validConfig
}
/**
* @private
* @param {any} config
* @returns {string}
*/
static _getInvalidConfigAttributeName(config) {
let acceptedNames = [
'endpoints',
'reorganisationBlocks',
'healthCheckInterval'
]
let names = Object.keys(config)
for (let name of names) {
if (!acceptedNames.includes(name)) {
return name
}
}
return undefined
}
/**
* Convert data type of attributes inplace.
*
* @private
* @param {any} tx
*/
static _formatTransaction(tx) {
tx.blockNumber = heximalToNumber(tx.blockNumber)
tx.transactionIndex = heximalToNumber(tx.transactionIndex)
tx.type = heximalToNumber(tx.type)
tx.nonce = heximalToNumber(tx.nonce)
tx.gas = BigInt(tx.gas)
tx.gasPrice = BigInt(tx.gasPrice)
}
/**
* @private
* @param {Array<Endpoint>} endpoints
* @throws {ClientError}
*/
static _validateEndpoints(endpoints) {
if (!Array.isArray(endpoints) || endpoints.length === 0) {
throw new ClientError('invalid config.endpoints')
}
for (let i = 0; i < endpoints.length; ++i) {
if (!isValidEndpoint(endpoints[i])) {
throw new ClientError(`invalid config.endpoints[${i}]`)
}
}
}
/**
* @private
* @param {Block} block
* @returns {Block}
*/
static _standardizeBlock(block) {
block.number = heximalToNumber(block.number)
block.timestamp = heximalToNumber(block.timestamp)
return block
}
}
module.exports = {
Client,
ClientError
}