@axerunners/insight-api
Version:
An AXE blockchain REST and WebSocket API Service
468 lines (403 loc) • 13.4 kB
JavaScript
;
var async = require('async');
var axecore = require('@axerunners/axecore-lib');
var _ = axecore.deps._;
var pools = require('../pools.json');
var BN = axecore.crypto.BN;
var LRU = require('lru-cache');
var Common = require('./common');
function BlockController(options) {
var self = this;
this.node = options.node;
this.blockSummaryCache = new LRU(options.blockSummaryCacheSize || BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE);
this.blockCacheConfirmations = 6;
this.blockCache = new LRU(options.blockCacheSize || BlockController.DEFAULT_BLOCK_CACHE_SIZE);
this.poolStrings = {};
pools.forEach(function(pool) {
pool.searchStrings.forEach(function(s) {
self.poolStrings[s] = {
poolName: pool.poolName,
url: pool.url
};
});
});
this.common = new Common({log: this.node.log});
}
var BLOCK_LIMIT = 200;
BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE = 1000000;
BlockController.DEFAULT_BLOCK_CACHE_SIZE = 1000;
function isHexadecimal(hash) {
if (!_.isString(hash)) {
return false;
}
return /^[0-9a-fA-F]+$/.test(hash);
}
BlockController.prototype.checkBlockHash = function(req, res, next) {
var self = this;
var hash = req.params.blockHash;
if (hash.length < 64 || !isHexadecimal(hash)) {
this.node.services.axed.getBlockHeader(parseInt(hash), function(err, info) {
if (err) {
return self.common.handleErrors(err, res);
}
req.params.blockHash = info.hash;
next();
});
} else {
next();
}
};
/**
* Find block header by hash ...
*/
BlockController.prototype.blockHeader = function(req, res, next) {
var self = this;
var hash = req.params.blockHash;
self.node.services.axed.getBlockHeader(hash, function(err, info) {
if (err) {
return self.common.handleErrors(err, res);
}
req.blockHeader = info;
next();
});
};
/**
* Retrieve an array of blockHeaders from a starting hash point
* By default (i.e no nbOfBlock specified) it will return only 25 blocks
*/
BlockController.prototype.blockHeaders = function(req, res, next) {
var self = this;
var blockIdentifier = req.params.blockIdentifier;
var nbOfBlockToFetch = ((req.params.hasOwnProperty('nbOfBlock')) ? (parseInt(req.params.nbOfBlock)>0 && parseInt(req.params.nbOfBlock) || false) : false) || 25;
var cb = function(err, headers) {
if (err) {
return self.common.handleErrors(err, res);
}
else{
res.jsonp({headers:headers});
}
};
if(blockIdentifier.length===64){
var hash = blockIdentifier;
var result = self.node.services.axed.getBlockHeaders(hash, cb, nbOfBlockToFetch);
}else{
var height = blockIdentifier;
var result = self.node.services.axed.getBlockHeaders(height, cb, nbOfBlockToFetch);
}
};
/**
* Find block by hash ...
*/
BlockController.prototype.block = function(req, res, next) {
var self = this;
var hash = req.params.blockHash;
var blockCached = self.blockCache.get(hash);
if (blockCached) {
blockCached.confirmations = self.node.services.axed.height - blockCached.height + 1;
req.block = blockCached;
next();
} else {
self.node.getBlock(hash, function(err, block) {
if((err && err.code === -5) || (err && err.code === -8)) {
return self.common.handleErrors(null, res);
} else if(err) {
return self.common.handleErrors(err, res);
}
self.node.services.axed.getBlockHeader(hash, function(err, info) {
if (err) {
return self.common.handleErrors(err, res);
}
var blockResult = self.transformBlock(block, info);
self.getBlockReward(blockResult.previousblockhash, function(err,reward) {
if (err) {
return self.common.handleErrors(err, res);
}
blockResult.reward = reward;
if (blockResult.confirmations >= self.blockCacheConfirmations) {
self.blockCache.set(hash, blockResult);
}
req.block = blockResult;
next();
});
});
});
}
};
/**
* Find rawblock by hash and height...
*/
BlockController.prototype.rawBlock = function(req, res, next) {
var self = this;
var blockHash = req.params.blockHash;
self.node.getRawBlock(blockHash, function(err, blockBuffer) {
if((err && err.code === -5) || (err && err.code === -8)) {
return self.common.handleErrors(null, res);
} else if(err) {
return self.common.handleErrors(err, res);
}
req.rawBlock = {
rawblock: blockBuffer.toString('hex')
};
next();
});
};
BlockController.prototype._normalizePrevHash = function(hash) {
// TODO fix axecore to give back null instead of null hash
if (hash !== '0000000000000000000000000000000000000000000000000000000000000000') {
return hash;
} else {
return null;
}
};
BlockController.prototype.transformBlock = function(block, info) {
var blockObj = block.toObject();
var transactionIds = blockObj.transactions.map(function(tx) {
return tx.hash;
});
return {
hash: block.hash,
size: block.toBuffer().length,
height: info.height,
version: blockObj.header.version,
merkleroot: blockObj.header.merkleRoot,
tx: transactionIds,
time: blockObj.header.time,
nonce: blockObj.header.nonce,
bits: blockObj.header.bits.toString(16),
difficulty: parseFloat(info.difficulty),
chainwork: info.chainWork,
confirmations: info.confirmations,
previousblockhash: this._normalizePrevHash(blockObj.header.prevHash),
nextblockhash: info.nextHash,
reward: null,
isMainChain: (info.confirmations !== -1),
poolInfo: this.getPoolInfo(block)
};
};
/**
* Show block
*/
BlockController.prototype.show = function(req, res) {
if (req.block) {
res.jsonp(req.block);
}
};
BlockController.prototype.showRaw = function(req, res) {
if (req.rawBlock) {
res.jsonp(req.rawBlock);
}
};
BlockController.prototype.blockIndex = function(req, res) {
var self = this;
var height = req.params.height;
this.node.services.axed.getBlockHeader(parseInt(height), function(err, info) {
if (err) {
return self.common.handleErrors(err, res);
}
res.jsonp({
blockHash: info.hash
});
});
};
BlockController.prototype._getBlockSummary = function(hash, moreTimestamp, next) {
var self = this;
function finish(result) {
if (moreTimestamp > result.time) {
moreTimestamp = result.time;
}
return next(null, result);
}
var summaryCache = self.blockSummaryCache.get(hash);
if (summaryCache) {
finish(summaryCache);
} else {
self.node.services.axed.getRawBlock(hash, function(err, blockBuffer) {
if (err) {
return next(err);
}
var br = new axecore.encoding.BufferReader(blockBuffer);
// take a shortcut to get number of transactions and the blocksize.
// Also reads the coinbase transaction and only that.
// Old code parsed all transactions in every block _and_ then encoded
// them all back together to get the binary size of the block.
// FIXME: This code might still read the whole block. Fixing that
// would require changes in axecore-node.
var header = axecore.BlockHeader.fromBufferReader(br);
var info = {};
var txlength = br.readVarintNum();
info.transactions = [axecore.Transaction().fromBufferReader(br)];
self.node.services.axed.getBlockHeader(hash, function(err, blockHeader) {
if (err) {
return next(err);
}
var height = blockHeader.height;
var summary = {
height: height,
size: blockBuffer.length,
hash: hash,
time: header.time,
txlength: txlength,
poolInfo: self.getPoolInfo(info)
};
var confirmations = self.node.services.axed.height - height + 1;
if (confirmations >= self.blockCacheConfirmations) {
self.blockSummaryCache.set(hash, summary);
}
finish(summary);
});
});
}
};
// List blocks by date
BlockController.prototype.list = function(req, res) {
var self = this;
var dateStr;
var todayStr = this.formatTimestamp(new Date());
var isToday;
if (req.query.blockDate) {
dateStr = req.query.blockDate;
var datePattern = /\d{4}-\d{2}-\d{2}/;
if(!datePattern.test(dateStr)) {
return self.common.handleErrors(new Error('Please use yyyy-mm-dd format'), res);
}
isToday = dateStr === todayStr;
} else {
dateStr = todayStr;
isToday = true;
}
var gte = Math.round((new Date(dateStr)).getTime() / 1000);
//pagination
var lte = parseInt(req.query.startTimestamp) || gte + 86400;
var prev = this.formatTimestamp(new Date((gte - 86400) * 1000));
var next = lte ? this.formatTimestamp(new Date(lte * 1000)) : null;
var limit = parseInt(req.query.limit || BLOCK_LIMIT);
var more = false;
var moreTimestamp = lte;
self.node.services.axed.getBlockHashesByTimestamp(lte, gte, function(err, hashes) {
if(err) {
return self.common.handleErrors(err, res);
}
hashes.reverse();
if(hashes.length > limit) {
more = true;
hashes = hashes.slice(0, limit);
}
async.mapSeries(
hashes,
function(hash, next) {
self._getBlockSummary(hash, moreTimestamp, next);
},
function(err, blocks) {
if(err) {
return self.common.handleErrors(err, res);
}
blocks.sort(function(a, b) {
return b.height - a.height;
});
var data = {
blocks: blocks,
length: blocks.length,
pagination: {
next: next,
prev: prev,
currentTs: lte - 1,
current: dateStr,
isToday: isToday,
more: more
}
};
if(more) {
data.pagination.moreTs = moreTimestamp;
}
res.jsonp(data);
}
);
});
};
BlockController.prototype.getPoolInfo = function(block) {
var coinbaseBuffer = block.transactions[0].inputs[0]._scriptBuffer;
for(var k in this.poolStrings) {
if (coinbaseBuffer.toString('utf-8').match(k)) {
return this.poolStrings[k];
}
}
return {};
};
//helper to convert timestamps to yyyy-mm-dd format
BlockController.prototype.formatTimestamp = function(date) {
var yyyy = date.getUTCFullYear().toString();
var mm = (date.getUTCMonth() + 1).toString(); // getMonth() is zero-based
var dd = date.getUTCDate().toString();
return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]); //padding
};
/**
* Previous blockHeader is used to determine block reward
*/
BlockController.prototype.getPreviousBlock = function(prevHash, cb) {
var self = this;
//On genesis block, the previousHash will be null.
if(prevHash===null) cb(null,null);
else {
self.node.getBlock(prevHash, function (err, block) {
if ((err && err.code === -5) || (err && err.code === -8)) {
return self.common.handleErrors(null, block);
} else if (err) {
return self.common.handleErrors(err, block);
}
self.node.services.axed.getBlockHeader(prevHash, function (err, info) {
if (err) {
return self.common.handleErrors(err, info);
}
cb(null, info); // return blockHeader
});
});
}
};
BlockController.prototype.getBlockReward = function(prevHash, cb) {
var self = this;
var nSubsidyHalvingInterval = 210240;
var nBudgetPaymentsStartBlock = 271350;
var nSubsidyBase;
// block reward is based on the previous block diff / height
self.getPreviousBlock(prevHash, function(err, info) {
if(err) cb(err, null);
//if we seek for prevHash of genesis, previousBlock return an info being null.
if(info===null){
//Genesis reward is 50.
cb(null, parseFloat("50".toString(10)).toFixed(8));
}
else{
var dDiff = info.difficulty;
var nPrevHeight = info.height;
if (nPrevHeight < 5200) {
// Swap + budget
// 1111/((x+1)^2)
nSubsidyBase = (1111.0 / (Math.pow((dDiff+1.0),2.0)));
if(nSubsidyBase > 500) nSubsidyBase = 500;
else if(nSubsidyBase < 1) nSubsidyBase = 1;
} else if (nPrevHeight < 5620 || (dDiff <= 75 && nPrevHeight < 24000)) {
// CPU mining era
// 11111/(((x+51)/6)^2)
nSubsidyBase = (11111.0 / (Math.pow((dDiff+51.0)/6.0,2.0)));
if(nSubsidyBase > 500) nSubsidyBase = 500;
else if(nSubsidyBase < 25) nSubsidyBase = 25;
} else {
// GPU/ASIC mining era
// 2222222/(((x+2600)/9)^2)
nSubsidyBase = (2222222.0 / (Math.pow((dDiff+2600.0)/9.0,2.0)));
if(nSubsidyBase > 25) nSubsidyBase = 25;
else if(nSubsidyBase < 5) nSubsidyBase = 5;
}
var nSubsidy = nSubsidyBase;
// yearly decline of production by ~7.1% per year, projected ~18M coins max by year 2050+.
for (var i = nSubsidyHalvingInterval; i <= nPrevHeight; i += nSubsidyHalvingInterval) {
nSubsidy -= nSubsidy/14;
}
// Hard fork to reduce the block reward by 10 extra percent (allowing budget/superblocks)
var nSuperblockPart = (nPrevHeight > nBudgetPaymentsStartBlock) ? nSubsidy/10 : 0;
var reward = nSubsidy - nSuperblockPart;
cb(null, parseFloat(reward.toString(10)).toFixed(8));
}
});
};
module.exports = BlockController;