flosight-api/lib/blocks.js
2018-05-22 13:50:46 -07:00

398 lines
10 KiB
JavaScript

'use strict';
var Stream = require('stream');
var util = require('util');
var flocore = require('flocore-lib');
var _ = flocore.deps._;
var pools = require('../pools.json');
var LRU = require('lru-cache');
var Common = require('./common');
var bcoin = require('fcoin');
var JsonStream = require('JSONStream');
function BlockController(options) {
var self = this;
this.node = options.node;
this.blockSummaryCache = LRU(options.blockSummaryCacheSize || BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE);
this.blockCacheConfirmations = 6;
this.blockCache = 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});
this._block = this.node.services.block;
this._header = this.node.services.header;
this._timestamp = this.node.services.timestamp;
}
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)) {
return self.common.handleErrors(null, res);
}
next();
};
/**
* Find block by hash ...
*/
BlockController.prototype.block = function(req, res, next) {
var self = this;
var hash = req.params.blockHash;
// Searching Genesis block crashes server
// Tmp handle by returning no block in index
if (hash === "0" || hash === 0)
return self.common.handleErrors(new Error('block not in index'), res);
var blockCached = self.blockCache.get(hash);
if (blockCached) {
var height = self._block.getTip().height;
blockCached.confirmations = height - blockCached.height + 1;
req.block = blockCached;
next();
} else {
self._block.getBlock(hash, function(err, block) {
if (err) {
return self.common.handleErrors(err, res);
}
if (!block) {
return self.common.handleErrors(new Error('block not in index'), res);
}
self._header.getBlockHeader(hash, function(err, info) {
if (err) {
return self.common.handleErrors(err, res);
}
if (!info) {
return self.common.handleErrors(new Error('block header info undefined'), res)
}
var blockResult = self.transformBlock(block, info);
if (!blockResult) {
return self.common.handleErrors(new Error('blockResult from transformBlock undefined'), res)
}
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);
}
if (!blockBuffer) {
return next();
}
req.rawBlock = {
rawblock: blockBuffer.toString('hex')
};
next();
});
};
BlockController.prototype._normalizePrevHash = function(hash) {
// TODO fix flocore to give back null instead of null hash
if (hash !== '0000000000000000000000000000000000000000000000000000000000000000') {
return hash;
} else {
return null;
}
};
BlockController.prototype.transformBlock = function(block, info) {
if (!block || !info){
return undefined;
}
var transactionIds = block.txs.map(function(tx) {
return tx.txid();
});
// MerkleRoot is backwards hex data in the block object, flip before returning
var merkleRoot = block.merkleRoot;
var strArray = merkleRoot.match(/.{1,2}/g);
var reversedArray = strArray.reverse();
var builtStr = "";
for (var str of reversedArray){
builtStr += str;
}
merkleRoot = builtStr
return {
hash: block.rhash(),
size: block.getSize(),
height: info.height,
version: block.version,
merkleroot: merkleRoot,
tx: transactionIds,
time: block.time,
nonce: block.nonce,
bits: block.bits,
difficulty: this._header.getCurrentDifficulty(),
chainwork: info.chainwork,
confirmations: this._block.getTip().height - info.height + 1,
previousblockhash: info.prevHash,
nextblockhash: info.nextHash,
reward: this.getBlockReward(block.txs[0]),
isMainChain: true,
poolInfo: this.getPoolInfo(block.txs[0])
};
};
/**
* 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;
self._header.getBlockHeader(parseInt(height), function(err, info) {
if (err || !info) {
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._block.getRawBlock(hash, function(err, blockBuffer) {
if (err) {
return next(err);
}
if (!blockBuffer) {
return next();
}
var block = bcoin.block.fromRaw(blockBuffer, 'hex');
// if we don't we a block header back, this is highly unusual,
// but possible if there was a very recent reorg and the header
// was removed but the block was not yet removed from the index.
// It is best to not return a result.
self._header.getBlockHeader(hash, function(err, header) {
if (err) {
return next(err);
}
if (!header) {
return next();
}
var height = header.height;
var summary = {
height: header.height,
size: block.getSize(),
virtualSize: block.getVirtualSize(),
hash: hash,
time: header.timestamp,
txlength: block.txs.length,
poolInfo: self.getPoolInfo(block.txs[0])
};
var _height = self._block.getTip().height;
var confirmations = _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._timestamp.getBlockHashesByTimestamp(lte, gte, function(err, hashes) {
if(err) {
return self.common.handleErrors(err, res);
}
function BlockBuilder() {
Stream.Transform.call(this, {objectMode: true});
}
util.inherits(BlockBuilder, Stream.Transform);
BlockBuilder.prototype._transform = function transformObject(hash, encoding, done) {
self._getBlockSummary(hash, moreTimestamp, done);
};
hashes.reverse();
if(hashes.length > limit) {
more = true;
hashes = hashes.slice(0, limit);
}
var data = JSON.stringify({
length: hashes.length,
pagination: {
next: next,
prev: prev,
currentTs: lte - 1,
current: dateStr,
isToday: isToday,
more: more,
moreTs: more ? moreTimestamp : undefined
}
});
var readableStream = new Stream.Readable({objectMode: true});
var blockBuilder = new BlockBuilder();
readableStream
.pipe(blockBuilder)
.pipe(JsonStream.stringify('{"blocks":[', ',', '],' + data.substr(1)))
.pipe(res);
hashes.forEach(function(hash) {
readableStream.push(hash);
});
readableStream.push(null);
});
};
BlockController.prototype.getBlockReward = function(tx) {
var amt = 0;
tx.outputs.forEach(function(output) {
amt += output.value;
});
return flocore.Unit.fromSatoshis(amt).toFLO();
};
BlockController.prototype.getPoolInfo = function(tx) {
if (!tx) {
return {};
}
var strFloData = "";
if (tx.strFloData)
strFloData = tx.strFloData;
for(var k in this.poolStrings) {
if (strFloData.includes(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
};
module.exports = BlockController;