leasehold-http-api
Version:
HTTP API module for Leasehold sidechain
415 lines (375 loc) • 10.7 kB
JavaScript
/*
* Copyright © 2019 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
;
const _ = require('lodash');
const checkIpInList = require('../helpers/check_ip_in_list');
const apiCodes = require('../api_codes');
const swaggerHelper = require('../helpers/swagger');
const { CACHE_KEYS_TRANSACTION_COUNT } = require('leasehold-lisk-framework/src/components/cache');
const { EPOCH_TIME, FEES } = global.constants;
// Private Fields
let library;
/**
* Get the forging status of a delegate.
*
* @param {string} publicKey - Public key of delegate
* @returns {Promise<object>}
* @private
*/
async function _getForgingStatus(publicKey) {
const fullList = await library.channel.invoke(
`${library.chainModuleAlias}:getForgingStatusForAllDelegates`,
);
if (publicKey && !_.find(fullList, { publicKey })) {
return [];
}
const result = _.find(fullList, { publicKey });
if (result) {
return [result];
}
return fullList;
}
/**
* Get the network height
*
* @returns Number
* @private
*/
async function _getNetworkHeight() {
const peers = await library.channel.invoke('network:getPeers', {
limit: 100,
state: 2,
});
if (!peers || !peers.length) {
return 0;
}
const networkHeightCount = peers.filter(peer => !!peer.modules).reduce((previous, { modules }) => {
const leasehold = modules ? modules.leasehold || {} : {};
const height = leasehold.height || 0;
const heightCount = previous[height] || 0;
previous[height] = heightCount + 1;
return previous;
}, {});
const heightCountPairs = Object.entries(networkHeightCount);
if (!heightCountPairs.length) {
return 0;
}
const [defaultHeight, defaultCount] = heightCountPairs[0];
const { height: networkHeight } = heightCountPairs.reduce(
(prev, [height, count]) => {
if (count > prev.count) {
return {
height,
count,
};
}
return prev;
},
{
height: defaultHeight,
count: defaultCount,
},
);
return parseInt(networkHeight, 10);
}
/**
* Get count of confirmedTransaction from cache
*
* @returns Number
* @private
*/
async function _getConfirmedTransactionCount() {
// if cache is ready, then get cache and return
if (library.components.cache.ready) {
try {
const { confirmed } = await library.components.cache.getJsonForKey(
CACHE_KEYS_TRANSACTION_COUNT,
);
if (confirmed === undefined || confirmed === null) {
throw new Error(
'Transaction count wasn cached but confirmed did not exist',
);
}
return confirmed;
} catch (error) {
library.components.logger.warn("Transaction count wasn't cached", error);
}
}
const confirmed = await library.components.storage.entities.Transaction.count();
// only update cache if ready
if (library.components.cache.ready) {
try {
await library.components.cache.setJsonForKey(
CACHE_KEYS_TRANSACTION_COUNT,
{
confirmed,
},
);
} catch (error) {
// Ignore error and just put warn
library.components.logger.warn("Transaction count wasn't cached", error);
}
}
return confirmed;
}
/**
* Parse transaction instance to raw data
*
* @returns Object
* @private
*/
function _normalizeTransactionOutput(transaction) {
return {
id: transaction.id,
type: transaction.type,
amount: transaction.amount.toString(),
fee: transaction.fee.toString(),
timestamp: transaction.timestamp,
senderPublicKey: transaction.senderPublicKey,
senderId: transaction.senderId || '',
signature: transaction.signature,
signatures: transaction.signatures,
recipientPublicKey: transaction.recipientPublicKey || '',
recipientId: transaction.recipientId || '',
asset: transaction.asset,
};
}
/**
* Description of the function.
*
* @class
* @memberof api.controllers
* @requires lodash
* @requires helpers/apiCodes.FORBIDDEN
* @requires helpers/apiCodes.NOT_FOUND
* @requires helpers/checkIpInList
* @requires helpers/swagger.generateParamsErrorObject
* @requires helpers/swagger.invalidParams
* @param {Object} scope - App instance
* @todo Add description of NodeController
*/
function NodeController(scope) {
library = {
chainModuleAlias: scope.chainModuleAlias,
components: {
storage: scope.components.storage,
cache: scope.components.cache,
logger: scope.components.logger,
},
config: scope.config,
channel: scope.channel,
applicationState: scope.applicationState,
lastCommitId: scope.lastCommitId,
buildVersion: scope.buildVersion,
};
}
/**
* Description of the function.
*
* @param {Object} context
* @param {function} next
* @todo Add description for the function and the params
*/
NodeController.getConstants = async (context, next) => {
const invalidParams = swaggerHelper.invalidParams(context.request);
if (invalidParams.length) {
return next(swaggerHelper.generateParamsErrorObject(invalidParams));
}
try {
const { lastBlock } = await library.channel.invoke(`${library.chainModuleAlias}:getNodeStatus`);
const { height } = lastBlock || {};
const milestone = await library.channel.invoke(`${library.chainModuleAlias}:calculateMilestone`, {
height,
});
const reward = await library.channel.invoke(`${library.chainModuleAlias}:calculateReward`, {
height,
});
const supply = await library.channel.invoke(`${library.chainModuleAlias}:calculateSupply`, {
height,
});
const { buildVersion: build, lastCommitId: commit } = library;
return next(null, {
build,
commit,
epoch: new Date(EPOCH_TIME),
fees: {
send: FEES.SEND.toString(),
vote: FEES.VOTE.toString(),
secondSignature: FEES.SECOND_SIGNATURE.toString(),
delegate: FEES.DELEGATE.toString(),
multisignature: FEES.MULTISIGNATURE.toString(),
dappRegistration: FEES.DAPP_REGISTRATION.toString(),
dappWithdrawal: FEES.DAPP_WITHDRAWAL.toString(),
dappDeposit: FEES.DAPP_DEPOSIT.toString(),
},
nethash: library.config.nethash,
nonce: library.config.nonce,
milestone: milestone.toString(),
reward: reward.toString(),
supply: supply.toString(),
version: library.config.version,
protocolVersion: library.config.protocolVersion,
});
} catch (error) {
return next(error);
}
};
/**
* Description of the function.
*
* @param {Object} context
* @param {function} next
* @todo Add description for the function and the params
*/
NodeController.getStatus = async (context, next) => {
try {
const {
consensus,
secondsSinceEpoch,
loaded,
syncing,
unconfirmedTransactions,
lastBlock,
} = await library.channel.invoke(`${library.chainModuleAlias}:getNodeStatus`);
// get confirmed count from cache or chain
const [confirmed, networkHeight] = await Promise.all([
_getConfirmedTransactionCount(),
_getNetworkHeight(),
]);
const total =
confirmed +
Object.values(unconfirmedTransactions).reduce(
(prev, current) => prev + current,
0,
);
const modules = library.applicationState.modules || {};
const moduleInfo = modules[library.chainModuleAlias] || {};
const broadhash = moduleInfo.broadhash || library.applicationState.broadhash;
const data = {
broadhash,
consensus: consensus || 0,
currentTime: Date.now(),
secondsSinceEpoch,
height: (lastBlock || {}).height || 0,
loaded,
networkHeight,
syncing,
transactions: {
confirmed,
...unconfirmedTransactions,
total,
},
};
return next(null, data);
} catch (err) {
return next(err);
}
};
/**
* Description of the function.
*
* @param {Object} context
* @param {function} next
* @todo Add description for the function and the params
*/
NodeController.getForgingStatus = async (context, next) => {
if (
!checkIpInList(library.config.forging.access.whiteList, context.request.ip)
) {
context.statusCode = apiCodes.FORBIDDEN;
return next(new Error('Access Denied'));
}
const publicKey = context.request.swagger.params.publicKey.value;
try {
const forgingStatus = await _getForgingStatus(publicKey);
return next(null, forgingStatus);
} catch (err) {
return next(err);
}
};
/**
* Description of the function.
*
* @param {Object} context
* @param {function} next
* @todo Add description for the function and the params
*/
NodeController.updateForgingStatus = async (context, next) => {
if (
!checkIpInList(library.config.forging.access.whiteList, context.request.ip)
) {
context.statusCode = apiCodes.FORBIDDEN;
return next(new Error('Access Denied'));
}
const { publicKey } = context.request.swagger.params.data.value;
const { password } = context.request.swagger.params.data.value;
const { forging } = context.request.swagger.params.data.value;
try {
const data = await library.channel.invoke(`${library.chainModuleAlias}:updateForgingStatus`, {
publicKey,
password,
forging,
});
return next(null, [data]);
} catch (err) {
context.statusCode = apiCodes.NOT_FOUND;
return next(err);
}
};
/**
* Description of the function.
*
* @param {Object} context
* @param {function} next
* @todo Add description for the function and the params
*/
NodeController.getPooledTransactions = async function(context, next) {
const invalidParams = swaggerHelper.invalidParams(context.request);
if (invalidParams.length) {
return next(swaggerHelper.generateParamsErrorObject(invalidParams));
}
const { params } = context.request.swagger;
const state = context.request.swagger.params.state.value;
let filters = {
id: params.id.value,
recipientId: params.recipientId.value,
recipientPublicKey: params.recipientPublicKey.value,
senderId: params.senderId.value,
senderPublicKey: params.senderPublicKey.value,
type: params.type.value,
sort: params.sort.value,
limit: params.limit.value,
offset: params.offset.value,
};
// Remove filters with null values
filters = _.pickBy(filters, v => !(v === undefined || v === null));
try {
const data = await library.channel.invoke(`${library.chainModuleAlias}:getTransactionsFromPool`, {
type: state,
filters: _.clone(filters),
});
const transactions = data.transactions.map(_normalizeTransactionOutput);
return next(null, {
data: transactions,
meta: {
offset: filters.offset,
limit: filters.limit,
count: parseInt(data.count, 10),
},
});
} catch (err) {
return next(err);
}
};
module.exports = NodeController;