'use strict'; var Writable = require('stream').Writable; var bodyParser = require('body-parser'); var compression = require('compression'); var BaseService = require('./service'); var inherits = require('util').inherits; var BlockController = require('./blocks'); var TxController = require('./transactions'); var AddressController = require('./addresses'); var StatusController = require('./status'); var MessagesController = require('./messages'); var UtilsController = require('./utils'); var CurrencyController = require('./currency'); var RateLimiter = require('./ratelimiter'); var morgan = require('morgan'); var flocore = require('flocore-lib'); var _ = flocore.deps._; var $ = flocore.util.preconditions; var EventEmitter = require('events').EventEmitter; /** * A service for Flocore to enable HTTP routes to query information about the blockchain. * * @param {Object} options * @param {Boolean} options.enableCache - This will enable cache-control headers * @param {Number} options.cacheShortSeconds - The time to cache short lived cache responses. * @param {Number} options.cacheLongSeconds - The time to cache long lived cache responses. * @param {String} options.routePrefix - The URL route prefix * @param {String} options.translateAddresses - Translate request and output address to Copay's BCH address version (see https://support.bitpay.com/hc/en-us/articles/115004671663-BitPay-s-Adopted-Conventions-for-Florincoin-Cash-Addresses-URIs-and-Payment-Requests) */ var FlosightAPI = function(options) { BaseService.call(this, options); // in minutes this.currencyRefresh = options.currencyRefresh || CurrencyController.DEFAULT_CURRENCY_DELAY; this.subscriptions = { inv: [] }; if (!_.isUndefined(options.enableCache)) { $.checkArgument(_.isBoolean(options.enableCache)); this.enableCache = options.enableCache; } this.cacheShortSeconds = options.cacheShortSeconds; this.cacheLongSeconds = options.cacheLongSeconds; this.rateLimiterOptions = options.rateLimiterOptions; this.disableRateLimiter = options.disableRateLimiter; this.translateAddresses = options.translateAddresses; this.blockSummaryCacheSize = options.blockSummaryCacheSize || BlockController.DEFAULT_BLOCKSUMMARY_CACHE_SIZE; this.blockCacheSize = options.blockCacheSize || BlockController.DEFAULT_BLOCK_CACHE_SIZE; if (!_.isUndefined(options.routePrefix)) { this.routePrefix = options.routePrefix; } else { this.routePrefix = this.name; } this.txController = new TxController(this.node); }; FlosightAPI.dependencies = ['header', 'block', 'transaction', 'address', 'web', 'mempool', 'timestamp', 'fee']; inherits(FlosightAPI, BaseService); FlosightAPI.prototype.cache = function(maxAge) { var self = this; return function(req, res, next) { if (self.enableCache) { res.header('Cache-Control', 'public, max-age=' + maxAge); } next(); }; }; FlosightAPI.prototype.cacheShort = function() { var seconds = this.cacheShortSeconds || 30; // thirty seconds return this.cache(seconds); }; FlosightAPI.prototype.cacheLong = function() { var seconds = this.cacheLongSeconds || 86400; // one day return this.cache(seconds); }; FlosightAPI.prototype.getRoutePrefix = function() { return this.routePrefix; }; FlosightAPI.prototype.start = function(callback) { if (this._subscribed) { return; } this._subscribed = true; if (!this._bus) { this._bus = this.node.openBus({remoteAddress: 'localhost-flosight-api'}); } this._bus.on('mempool/transaction', this.transactionEventHandler.bind(this)); this._bus.subscribe('mempool/transaction'); this._bus.on('block/block', this.blockEventHandler.bind(this)); this._bus.subscribe('block/block'); callback(); }; FlosightAPI.prototype.createLogInfoStream = function() { var self = this; function Log(options) { Writable.call(this, options); } inherits(Log, Writable); Log.prototype._write = function (chunk, enc, callback) { self.node.log.info(chunk.slice(0, chunk.length - 1)); // remove new line and pass to logger callback(); }; var stream = new Log(); return stream; }; FlosightAPI.prototype.getRemoteAddress = function(req) { if (req.headers['cf-connecting-ip']) { return req.headers['cf-connecting-ip']; } return req.socket.remoteAddress; }; FlosightAPI.prototype._getRateLimiter = function() { var rateLimiterOptions = _.isUndefined(this.rateLimiterOptions) ? {} : _.clone(this.rateLimiterOptions); rateLimiterOptions.node = this.node; var limiter = new RateLimiter(rateLimiterOptions); return limiter; }; FlosightAPI.prototype.setupRoutes = function(app, express, express_ws) { var self = this; //Enable rate limiter if (!this.disableRateLimiter) { var limiter = this._getRateLimiter(); app.use(limiter.middleware()); } //Setup logging morgan.token('remote-forward-addr', function(req){ return self.getRemoteAddress(req); }); var logFormat = ':remote-forward-addr ":method :url" :status :res[content-length] :response-time ":user-agent" '; var logStream = this.createLogInfoStream(); app.use(morgan(logFormat, {stream: logStream})); //Enable compression app.use(compression()); //Enable urlencoded data app.use(bodyParser.urlencoded({extended: true})); //Enable CORS app.use(function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, cf-connecting-ip'); var method = req.method && req.method.toUpperCase && req.method.toUpperCase(); if (method === 'OPTIONS') { res.statusCode = 204; res.end(); } else { next(); } }); //Add ws listener to app var expressWS = express_ws(app); //Block routes var blockOptions = { node: this.node, blockSummaryCacheSize: this.blockSummaryCacheSize, blockCacheSize: this.blockCacheSize }; var blocks = new BlockController(blockOptions); app.get('/blocks', this.cacheShort(), blocks.list.bind(blocks)); app.get('/block/:blockHash', this.cacheShort(), blocks.checkBlockHash.bind(blocks), blocks.show.bind(blocks)); app.param('blockHash', blocks.block.bind(blocks)); app.get('/rawblock/:blockHash', this.cacheLong(), blocks.checkBlockHash.bind(blocks), blocks.showRaw.bind(blocks)); app.param('blockHash', blocks.rawBlock.bind(blocks)); app.get('/block-index/:height', this.cacheShort(), blocks.blockIndex.bind(blocks)); app.param('height', blocks.blockIndex.bind(blocks)); // Transaction routes var transactions = new TxController(this.node, this.translateAddresses); app.get('/tx/:txid', this.cacheShort(), transactions.show.bind(transactions)); app.param('txid', transactions.transaction.bind(transactions)); app.get('/txs', this.cacheShort(), transactions.list.bind(transactions)); app.post('/tx/send', transactions.send.bind(transactions)); // Raw Routes app.get('/rawtx/:txid', this.cacheLong(), transactions.showRaw.bind(transactions)); app.param('txid', transactions.rawTransaction.bind(transactions)); // Address routes var addresses = new AddressController(this.node, this.translateAddresses); app.get('/addr/:addr', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.show.bind(addresses)); app.ws('/addr/:addr', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.show_ws.bind(addresses)); app.get('/addr/:addr/utxo', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.utxo.bind(addresses)); app.get('/addrs/:addrs/utxo', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses)); app.post('/addrs/utxo', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses)); app.get('/addrs/:addrs/txs', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses)); app.ws('/addrs/:addrs/txs', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multitxs_ws.bind(addresses)); app.post('/addrs/txs', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses)); // Address property routes app.get('/addr/:addr/balance', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.balance.bind(addresses)); app.get('/addr/:addr/totalReceived', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.totalReceived.bind(addresses)); app.get('/addr/:addr/totalSent', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.totalSent.bind(addresses)); app.get('/addr/:addr/unconfirmedBalance', this.cacheShort(), addresses.checkAddrs.bind(addresses), addresses.unconfirmedBalance.bind(addresses)); // Status route var status = new StatusController(this.node); app.get('/status', this.cacheShort(), status.show.bind(status)); app.get('/sync', this.cacheShort(), status.sync.bind(status)); app.get('/peer', this.cacheShort(), status.peer.bind(status)); app.get('/version', this.cacheShort(), status.version.bind(status)); // Address routes var messages = new MessagesController(this.node); app.get('/messages/verify', messages.verify.bind(messages)); app.post('/messages/verify', messages.verify.bind(messages)); // Utils route var utils = new UtilsController(this.node); app.get('/utils/estimatefee', utils.estimateFee.bind(utils)); // Currency var currency = new CurrencyController({ node: this.node, currencyRefresh: this.currencyRefresh }); app.get('/currency', currency.index.bind(currency)); // Not Found app.use(function(req, res) { res.status(404).jsonp({ status: 404, url: req.originalUrl, error: 'Not found' }); }); }; FlosightAPI.prototype.getPublishEvents = function() { return [ { name: 'inv', scope: this, subscribe: this.subscribe.bind(this), unsubscribe: this.unsubscribe.bind(this), extraEvents: ['tx', 'block'] } ]; }; FlosightAPI.prototype.blockEventHandler = function(block) { for (var i = 0; i < this.subscriptions.inv.length; i++) { this.subscriptions.inv[i].emit('block', block.rhash()); } }; FlosightAPI.prototype.transactionEventHandler = function(tx) { var result = this.txController.transformInvTransaction(tx); for (var i = 0; i < this.subscriptions.inv.length; i++) { this.subscriptions.inv[i].emit('tx', result); } }; FlosightAPI.prototype.subscribe = function(emitter) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); var emitters = this.subscriptions.inv; var index = emitters.indexOf(emitter); if(index === -1) { emitters.push(emitter); } }; FlosightAPI.prototype.unsubscribe = function(emitter) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); var emitters = this.subscriptions.inv; var index = emitters.indexOf(emitter); if(index > -1) { emitters.splice(index, 1); } }; module.exports = FlosightAPI;