'use strict';
/**
* @module lib/blockchains/fennel/rpc
* @summary Whiteflag API Fennel / Substrate RPC module
* @description Module to connect to the Fennel network through a Fennel parachain node
*/
module.exports = {
init: initRpc,
getBalance,
getBlock,
getBlockByNumber,
getBlockByHash,
getBlockHash,
getBlockHeader,
getChainType,
getEvents,
getFullBlock,
getHighestBlock,
getNodePeerId,
getNodeRoles,
getPeerCount,
getRuntimeVersion,
getSyncState,
getSystemHealth,
getSystemName,
getSystemVersion,
getTransactionCount,
isSyncing,
sendSignal,
sendTokens,
};
/* External modules */
const { ApiPromise,
WsProvider } = require('@polkadot/api');
/* Common internal functions and classes */
const log = require('../../_common/logger');
const { ignore } = require('../../_common/processing')
const { ProcessingError } = require('../../_common/errors');
const { timeoutPromise } = require('../../_common/processing');
/* Whiteflag modules */
const wfState = require('../../protocol/state');
/* Common blockchain functions */
const { rpcCall,
getNodeURL } = require('../_common/rpc');
/* Module constants */
const MODULELOG = 'fennel';
const DEFAULTPORT = '8844';
const TOKENPRECISION = 1e12;
const STATUSINTERVAL = 60000; // Every minute
const INFOINTERVAL = 3600000; // Every hour
/* Module variables */
let _fnlChain = 'fennel';
let _fnlState;
let _fnlApi;
let _rpcInit = false;
let _rpcTimeout = 10000;
let _rpcAuthURL;
let _rpcUser;
let _rpcPass;
/**
* Initialises Fennel RPC
* @function initRpc
* @alias module:lib/blockchains/fennel/rpc.init
* @param {Object} fnlConfig the Fennel blockchain configuration
* @param {Object} fnlState the Fennel blockchain state
* @returns {Promise} resolve if succesfully initialised
*/
function initRpc(fnlConfig, fnlState) {
_fnlChain = fnlConfig.name;
_fnlState = fnlState;
// RPC timeout period
if (Object.hasOwn(fnlConfig, 'rpcTimeout') && fnlConfig.rpcTimeout > 500) {
_rpcTimeout = fnlConfig.rpcTimeout;
}
log.info(MODULELOG, `Timeout for remote calls to the ${_fnlChain} node: ${_rpcTimeout} ms`);
// Get Node URL and credentials
const rpcCleanURL = getNodeURL(fnlConfig, true, DEFAULTPORT); // no credentials
_rpcAuthURL = getNodeURL(fnlConfig, false, DEFAULTPORT); // include credentials
_fnlState.parameters.rpcURL = rpcCleanURL;
_rpcUser = fnlConfig.rpcUsername;
_rpcPass = fnlConfig.rpcPassword;
// Connect to Fennel node
_rpcInit = true;
return new Promise((resolve, reject) => {
switch (fnlConfig.rpcProtocol) {
// Connect using websocket
case 'ws':
case 'wss': {
log.trace(MODULELOG, `Setting up web socket connection with ${_fnlChain} node: ${rpcCleanURL}`);
const wsProvider = new WsProvider(_rpcAuthURL);
timeoutPromise(ApiPromise.create({ provider: wsProvider }), _rpcTimeout)
.then(api => {
_fnlApi = api;
log.info(MODULELOG, `Connected to ${_fnlApi.runtimeVersion.specName} through ${_fnlChain} node: ${rpcCleanURL}`);
// Initialise node status monitoring
updateNodeInfo(); setInterval(updateNodeInfo, INFOINTERVAL);
updateNodeStatus(); setInterval(updateNodeStatus, STATUSINTERVAL);
// Succesfully completed initialisation
return resolve(_fnlApi);
})
.catch(err => {
_rpcInit = false;
wsProvider.disconnect();
return reject(new Error(`Could not make a web socket connection with ${_fnlChain} node: ${err.message}`), _fnlChain);
});
break;
}
// Connect via http
case 'http':
case 'https': {
log.trace(MODULELOG, `Setting up web connection with ${_fnlChain} node: ${rpcCleanURL}`);
getRuntimeVersion()
.then(fnlRuntime => {
log.info(MODULELOG, `Connected to ${fnlRuntime.specName} through ${_fnlChain} node: ${rpcCleanURL}`);
// Initialise node status monitoring
updateNodeInfo(); setInterval(updateNodeInfo, INFOINTERVAL);
updateNodeStatus(); setInterval(updateNodeStatus, STATUSINTERVAL);
// Succesfully completed initialisation
return resolve();
})
.catch(err => {
_rpcInit = false;
reject(new Error(`Could not make web connection with ${_fnlChain} node: ${err.message}`), _fnlChain)}
);
break;
}
// Unknown protocol
default: {
return reject(new Error(`Unknown protocol to connect with ${_fnlChain} node: ${fnlConfig.rpcProtocol}`), _fnlChain);
}
}
});
}
/* RPC CALL WRAPPER FUNCTIONS */
/**
* Gets the balance of the specified Fennel blockchain account
* @function getBalance
* @alias module:lib/blockchains/fennel/rpc.getBalance
* @param {string} address
* @returns {Promise}
*/
function getBalance(address) {
if (_fnlApi) {
return _fnlApi.query.system.account(address)
.then(({ nonce, data: balance }) => {
ignore(nonce);
return Promise.resolve(toTokens(balance.free));
});
} else {
return Promise.reject(new ProcessingError('No web rpc method method available to get balance', null, 'WF_API_NOT_IMPLEMENTED'));
}
}
/**
* Gets a block by its hash
* @function getBlock
* @alias module:lib/blockchains/fennel/rpc.getBlock
* @param {string} blockHash
* @returns {Promise} resolves to a JOSN representation of the block
*/
function getBlock(blockHash) {
return getFullBlock(blockHash)
.then(result => {
if (result instanceof Map) return Promise.resolve(result.get('block'));
return Promise.resolve(result.block);
})
.catch(err => {
return Promise.reject(err);
});
}
/**
* Gets the hash of the block specified by its block number
* @function getBlockHash
* @alias module:lib/blockchains/fennel/rpc.getBlockHash
* @param {Object} blockNumber
* @returns {Promise} resolves to block hash
*/
function getBlockHash(blockNumber) {
if (_fnlApi) return _fnlApi.rpc.chain.getBlockHash(blockNumber);
return rpc('chain_getBlockHash', [ blockNumber ]);
}
/**
* Gets a block by its hash
* @function getBlockByHash
* @alias module:lib/blockchains/fennel/rpc.getBlockByHash
* @param {string} blockHash
* @param {boolean} [full] get the full block including extrinsics (default)
* @returns {Promise} resolves to a JSON representation of the block
*/
function getBlockByHash(blockHash, full = true) {
if (full) return getBlock(blockHash);
return getBlockHeader(blockHash);
}
/**
* Gets a block by its number including transaction data
* @function getBlockByNumber
* @alias module:lib/blockchains/fennel/rpc.getBlockByNumber
* @param {Object} blockNumber
* @param {boolean} [full] get the full block including extrinsics (default)
* @returns {Promise} resolves to block including extrinsics
*/
function getBlockByNumber(blockNumber, full = true) {
return getBlockHash(blockNumber)
.then(blockHash => {
return getBlockByHash(blockHash, full);
})
.catch(err => {
return Promise.reject(err);
});
}
/**
* Gets a block header by its hash
* @function getBlockHeader
* @alias module:lib/blockchains/fennel/rpc.getBlockHeader
* @param {string} blockHash
* @returns {Promise} resolves to a JOSN representation of the block header
*/
async function getBlockHeader(blockHash) {
if (_fnlApi) return _fnlApi.rpc.chain.getHeader(blockHash);
return rpc('chain_getHeader', [ blockHash ]);
}
/**
* Gets the chain type
* @function getChainType
* @alias module:lib/blockchains/fennel/rpc.getChainType
* @returns {Promise} resolves to chain type
*/
function getChainType() {
// WORKORUND: websocket does not return does not return correct value
// if (_fnlApi) return _fnlApi.rpc.system.chainType();
return rpc('system_chainType');
}
function getEvents(blockNumber) {
if (_fnlApi) {
return getBlockHash(blockNumber)
.then(blockHash => {
return _fnlApi.at(blockHash);
})
.then(apiAt => {
return apiAt.query.system.events();
});
} else {
return Promise.reject(new ProcessingError('No web rpc method implemented to get events' ,null , 'WF_API_NOT_IMPLEMENTED'));
}
}
/**
* Gets a full block by its hash
* @function getFullBlock
* @alias module:lib/blockchains/fennel/rpc.getFullBlock
* @param {string} blockHash
* @returns {Promise} resolves to a JOSN representation of the block
*/
function getFullBlock(blockHash) {
if (_fnlApi) return _fnlApi.rpc.chain.getBlock(blockHash);
return rpc('chain_getBlock', [ blockHash ]);
}
/**
* Gets the highest block, i.e. the current block count of the longest chains
* @function getHighestBlock
* @alias module:lib/blockchains/fennel/rpc.getHighestBlock
* @returns {Promise} resolves to highest known block number
*/
function getHighestBlock() {
return getSyncState()
.then(result => {
if (result instanceof Map) return Promise.resolve(Number(result.get('highestBlock')));
return Promise.resolve(Number(result.highestBlock));
})
.catch(err => {
return Promise.reject(err);
});
}
/**
* Gets the system peer id
* @function getNodePeerId
* @alias module:lib/blockchains/fennel/rpc.getNodePeerId
* @returns {Promise} resolves to system name
*/
function getNodePeerId() {
if (_fnlApi) return _fnlApi.rpc.system.localPeerId();
return rpc('system_localPeerId');
}
/**
* Gets the node roles
* @function getNodeRoles
* @alias module:lib/blockchains/fennel/rpc.getNodeRoles
* @returns {Promise} resolves to array with node roles
*/
function getNodeRoles() {
// BUG: websocket does not return does not return correct value
// if (_fnlApi) return _fnlApi.rpc.system.nodeRoles();
return rpc('system_nodeRoles');
}
/**
* Gets the number of peers
* @function getPeerCount
* @alias module:lib/blockchains/fennel/rpc.getPeerCount
* @returns {Promise} resolves to the number of peers
*/
function getPeerCount() {
return getSystemHealth()
.then(result => {
if (result instanceof Map) return Promise.resolve(Number(result.get('peers')));
return Promise.resolve(Number(result.peers));
})
.catch(err => {
return Promise.reject(err);
});
}
/**
* Gets the runtime version information
* @function getRuntimeVersion
* @alias module:lib/blockchains/fennel/rpc.getRuntimeVersion
* @returns {Promise} resolves to version info
*/
function getRuntimeVersion() {
if (_fnlApi) return _fnlApi.rpc.state.getRuntimeVersion();
return rpc('state_getRuntimeVersion');
}
/**
* Gets the system synchronisation state
* @function getSyncState
* @alias module:lib/blockchains/fennel/rpc.getSyncState
* @returns {Promise} resolves to connection count
*/
function getSyncState() {
if (_fnlApi) return _fnlApi.rpc.system.syncState();
return rpc('system_syncState');
}
/**
* Gets the node health
* @function getSystemHealth
* @alias module:lib/blockchains/fennel/rpc.getPeerCount
* @returns {Promise} resolves to the node's system health
*/
function getSystemHealth() {
if (_fnlApi) return _fnlApi.rpc.system.health();
return rpc('system_health');
}
/**
* Gets the system name
* @function getSystemName
* @alias module:lib/blockchains/fennel/rpc.getSystemName
* @returns {Promise} resolves to system name
*/
function getSystemName() {
if (_fnlApi) return _fnlApi.rpc.system.name();
return rpc('system_name');
}
/**
* Gets the system version
* @function getSystemVersion
* @alias module:lib/blockchains/fennel/rpc.getSystemVersion
* @returns {Promise} resolves to connection count
*/
function getSystemVersion() {
if (_fnlApi) return _fnlApi.rpc.system.version();
return rpc('system_version');
}
/**
* Gets the transaction count of the specified Fennel blockchain account
* @function getTransactionCount
* @alias module:lib/blockchains/fennel/rpc.getTransactionCount
* @param {string} address
* @returns {Promise}
*/
function getTransactionCount(address) {
if (_fnlApi) {
return _fnlApi.query.system.account(address)
.then(({ nonce, data: balance }) => {
ignore(balance);
return Promise.resolve(Number(nonce));
});
} else {
return rpc('system_accountNextIndex', [ address ]);
}
}
/**
* Checks if the node is syncing
* @function isSyncing
* @alias module:lib/blockchains/fennel/rpc.isSyncing
* @returns {Promise} resolves to syncing infomration
*/
function isSyncing() {
return getSystemHealth()
.then(result => {
if (result instanceof Map) return Promise.resolve(result.get('isSyncing'));
return Promise.resolve(result.isSyncing);
})
.catch(err => {
return Promise.reject(err);
});
}
/**
* Send a signal transaction to the Fennel blockchain
* @param {Object} keyring the Polkadot keyring to sign the transaction
* @param {string} data the data to be sent as a signal
* @returns {Promise} resolves to the transaction hash
*/
function sendSignal(keyring, data) {
if (_fnlApi) {
return _fnlApi.tx.signal
.sendSignal(data)
.signAndSend(keyring, { nonce: -1 });
} else {
return Promise.reject(new ProcessingError('No web rpc method implemented to send signals', null, 'WF_API_NOT_IMPLEMENTED'));
}
}
/**
* Sends a token transfer transaction to the Fennel blockchain
* @param {Object} keyring the Polkadot keyring to sign the transaction
* @param {string} toAddress the address to transfer the tokens to
* @param {number} amount the amount of tokens to transfer
* @returns {Promise} resolves to the transaction hash
*/
function sendTokens(keyring, toAddress, amount) {
if (_fnlApi) {
return _fnlApi.tx.balances
.transferKeepAlive(toAddress, toUnits(amount))
.signAndSend(keyring, { nonce: -1 });
} else {
return Promise.reject(new ProcessingError('No web rpc method implemented to send tokens', null, 'WF_API_NOT_IMPLEMENTED'));
}
}
/* PRIVATE BLOCKCHAIN STATUS FUNCTIONS */
/**
* Converts units to tokens
* @private
* @param {number} unit the number of units
* @returns {number} the amount of tokens
*/
function toTokens(unit) {
return (Number(unit) / TOKENPRECISION);
}
/**
* Converts tokens to units
* @private
* @param {number} token the amount of tokens
* @returns {number} the number of units
*/
function toUnits(token) {
return (Number(token) * TOKENPRECISION);
}
/**
* Requests some semi-static Fennel parachain information
* @private
*/
async function updateNodeInfo() {
// Get system name
await getSystemName()
.then(systemName => (_fnlState.parameters.systemName = systemName || ''))
.catch(err => log.warn(MODULELOG, `Could not get system name from node: ${err.message}`));
// Get system version info
await getSystemVersion()
.then(systemVersion => (_fnlState.parameters.systemVersion = systemVersion || ''))
.catch(err => log.warn(MODULELOG, `Could not get system version from node: ${err.message}`));
// Get chain runtime info
await getRuntimeVersion()
.then(fnlRuntime => {
_fnlState.parameters.specName = fnlRuntime.specName || '';
_fnlState.parameters.specVersion = Number(fnlRuntime.specVersion) || '';
_fnlState.parameters.implName = fnlRuntime.implName || '';
_fnlState.parameters.implVersion = Number(fnlRuntime.implVersion) || '';
})
.catch(err => log.warn(MODULELOG, `Could not get chain runtime information from node: ${err.message}`));
// Get node roles
await getChainType()
.then(chainType => (_fnlState.parameters.chainType = chainType || ''))
.catch(err => log.warn(MODULELOG, `Could not get chain type from node: ${err.message}`));
// Get peer id
await getNodePeerId()
.then(peerId => (_fnlState.parameters.peerId = peerId || ''))
.catch(err => log.warn(MODULELOG, `Could not get peer id from node: ${err.message}`));
// Get node roles
await getNodeRoles()
.then(nodeRoles => (_fnlState.parameters.nodeRoles = nodeRoles || ''))
.catch(err => log.warn(MODULELOG, `Could not get roles from node: ${err.message}`));
}
/**
* Requests some dynamic Fennel parachain node status information
* @private
*/
async function updateNodeStatus() {
_fnlState.status.updated = new Date().toISOString();
// Get peer status
await getPeerCount()
.then(peers => (_fnlState.status.peers = Number(peers) || ''))
.catch(err => log.warn(MODULELOG, `Could not get peer status: ${err.message}`));
// Get synchronisation status
await isSyncing()
.then(syncing => (_fnlState.status.syncing = syncing || null))
.catch(err => log.warn(MODULELOG, `Could not get synchronisation status: ${err.message}`));
// Get synchronisation status
await getSyncState()
.then(syncState => {
if (_fnlState.status.startingBlock) delete _fnlState.status.startingBlock;
if (_fnlState.status.syncing) {
_fnlState.status.syncingBlock = Number(syncState.currentBlock) || '';
} else {
delete _fnlState.status.syncingBlock;
}
_fnlState.status.highestBlock = Number(syncState.highestBlock) || '';
})
.catch(err => log.warn(MODULELOG, `Could not get synchronisation status: ${err.message}`));
// Log node status
saveNodeStatus();
}
/**
* Logs the Fennel node status information
* @private
*/
function saveNodeStatus() {
wfState.updateBlockchainData(_fnlChain, _fnlState);
log.debug(MODULELOG, `Status: {${JSON.stringify(_fnlState.parameters)}, ${JSON.stringify(_fnlState.status)}}`);
}
/* PRIVATE RPC FUNCTIONS */
/**
* Calls the common RPC function
* @function rpc
* @private
* @param {string} method the rpc method
* @param {array} params the parameters for the rpc method
* @returns {Promise}
*/
function rpc(method, params = []) {
if (!_rpcInit) return Promise.reject(notInitialized());
return rpcCall(method, params, _rpcAuthURL, _rpcUser, _rpcPass, _rpcTimeout);
}
/**
* Returns an error that the connextion has not been initializes
* @private
* @returns {ProtocolError}
*/
function notInitialized() {
return new ProcessingError('No connection to a Fennel node has been initialized', null, 'WF_API_NOT_AVAILABLE');
}