'use strict'; var util = require('util'); var bitcore = require('bitcore-lib'); var zmq = require('zmq'); var async = require('async'); var BitcoinRPC = require('bitcoind-rpc'); var _ = bitcore.deps._; var index = require('../../'); var errors = index.errors; var log = index.log; var Service = require('../../service'); var LRU = require('lru-cache'); function Bitcoin(options) { if (!(this instanceof Bitcoin)) { return new Bitcoin(options); } Service.call(this, options); this.options = options; this.subscriptions = {}; this.subscriptions.rawtransaction = []; this.subscriptions.hashblock = []; this.subscriptions.rawblock = []; this.startRetryTimes = this.options.startRetryTimes || 120; this.startRetryInterval = this.options.startRetryInterval || 1000; this._initClients(); this._process = options.process || process; this.on('error', function(err) { log.error(err.stack); }); this._hashBlockCache = LRU(100); } util.inherits(Bitcoin, Service); Bitcoin.dependencies = []; Bitcoin.prototype._initClients = function() { var self = this; this.nodes = []; this.nodesIndex = 0; Object.defineProperty(this, 'client', { get: function() { var client = self.nodes[self.nodesIndex].client; self.nodesIndex = (self.nodesIndex + 1) % self.nodes.length; return client; }, enumerable: true, configurable: false }); }; Bitcoin.prototype.getAPIMethods = function() { var methods = [ ['getBlock', this, this.getBlock, 1] ]; return methods; }; Bitcoin.prototype.getPublishEvents = function() { return [ { name: 'bitcoind/rawtransaction', scope: this, subscribe: this.subscribe.bind(this, 'rawtransaction'), unsubscribe: this.unsubscribe.bind(this, 'rawtransaction') }, { name: 'bitcoind/hashblock', scope: this, subscribe: this.subscribe.bind(this, 'hashblock'), unsubscribe: this.unsubscribe.bind(this, 'hashblock') }, { name: 'bitcoind/rawblock', scope: this, subscribe: this.subscribe.bind(this, 'rawblock'), unsubscribe: this.unsubscribe.bind(this, 'rawblock') } ]; }; Bitcoin.prototype.subscribe = function(name, emitter) { this.subscriptions[name].push(emitter); log.info(emitter.remoteAddress, 'subscribe:', 'bitcoind/' + name, 'total:', this.subscriptions[name].length); }; Bitcoin.prototype.unsubscribe = function(name, emitter) { var index = this.subscriptions[name].indexOf(emitter); if (index > -1) { this.subscriptions[name].splice(index, 1); } log.info(emitter.remoteAddress, 'unsubscribe:', 'bitcoind/' + name, 'total:', this.subscriptions[name].length); }; Bitcoin.prototype._tryAllClients = function(func, callback) { var self = this; var nodesIndex = this.nodesIndex; var retry = function(done) { var client = self.nodes[nodesIndex].client; nodesIndex = (nodesIndex + 1) % self.nodes.length; func(client, done); }; async.retry({times: this.nodes.length, interval: this.tryAllInterval || 1000}, retry, callback); }; Bitcoin.prototype._wrapRPCError = function(errObj) { var err = new errors.RPCError(errObj.message); err.code = errObj.code; return err; }; Bitcoin.prototype._getGenesisBlock = function(callback) { var self = this; if (self.height === 0) { return self.getRawBlock(self.tiphash, function(err, blockBuffer) { if(err) { return callback(err); } self.genesisBuffer = blockBuffer; callback(); }); } self.client.getBlockHash(0, function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var blockhash = response.result; self.getRawBlock(blockhash, function(err, blockBuffer) { if (err) { return callback(err); } self.genesisBuffer = blockBuffer; callback(); }); }); }; Bitcoin.prototype._getNetworkTip = function(callback) { var self = this; self.client.getBestBlockHash(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } self.tiphash = response.result; self.client.getBlock(response.result, function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } self.height = response.result.height; callback(); }); }); }; Bitcoin.prototype._initChain = function(callback) { var self = this; async.series([ self._getNetworkTip.bind(self), self._getGenesisBlock.bind(self), ], function(err) { if(err) { return callback(err); } self.emit('ready'); callback(); }); }; Bitcoin.prototype._zmqRawBlockHandler = function(message) { var block = new bitcore.Block(message); this.tiphash = block.hash; this.height++; block.__height = this.height; block.height = this.height; for (var i = 0; i < this.subscriptions.rawblock.length; i++) { this.subscriptions.rawblock[i].emit('bitcoind/rawblock', block); } }; Bitcoin.prototype._zmqBlockHandler = function(message) { var self = this; var hashBlockHex = message.toString('hex'); if (!self._isSendableHashBlock(hashBlockHex)) { return; } self._hashBlockCache.set(hashBlockHex); self.tiphash = hashBlockHex; self.height++; self.emit('block', message); for (var i = 0; i < this.subscriptions.hashblock.length; i++) { this.subscriptions.hashblock[i].emit('bitcoind/hashblock', hashBlockHex); } }; Bitcoin.prototype._isSendableHashBlock = function(hashBlockHex) { return hashBlockHex.length === 64 && !this._hashBlockCache.get(hashBlockHex); }; Bitcoin.prototype._zmqTransactionHandler = function(node, message) { var self = this; self.emit('tx', message); for (var i = 0; i < this.subscriptions.rawtransaction.length; i++) { this.subscriptions.rawtransaction[i].emit('bitcoind/rawtransaction', message.toString('hex')); } }; Bitcoin.prototype._subscribeZmqEvents = function(node) { var self = this; node.zmqSubSocket.subscribe('hashblock'); node.zmqSubSocket.subscribe('rawtx'); node.zmqSubSocket.subscribe('rawblock'); node.zmqSubSocket.on('message', function(topic, message) { var topicString = topic.toString('utf8'); if (topicString === 'rawtx') { self._zmqTransactionHandler(node, message); } else if (topicString === 'hashblock') { self._zmqBlockHandler(message); } else if (topicString === 'rawblock') { self._zmqRawBlockHandler(message); } }); }; Bitcoin.prototype._initZmqSubSocket = function(node, zmqUrl) { node.zmqSubSocket = zmq.socket('sub'); node.zmqSubSocket.on('connect', function(fd, endPoint) { log.info('ZMQ connected to:', endPoint); }); node.zmqSubSocket.on('connect_delay', function(fd, endPoint) { if (this.zmqDelayWarningMultiplierCouunt++ >= this.zmqDelayWarningMultiplier) { log.warn('ZMQ connection delay:', endPoint); this.zmqDelayWarningMultiplierCouunt = 0; } }); node.zmqSubSocket.on('disconnect', function(fd, endPoint) { log.warn('ZMQ disconnect:', endPoint); }); node.zmqSubSocket.on('monitor_error', function(err) { log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err); setTimeout(function() { node.zmqSubSocket.monitor(500, 0); }, 5000); }); node.zmqSubSocket.monitor(100, 0); if (_.isString(zmqUrl)) { node.zmqSubSocket.connect(zmqUrl); } }; Bitcoin.prototype._connectProcess = function(config) { var self = this; var node = {}; node.client = new BitcoinRPC({ protocol: config.rpcprotocol || 'http', host: config.rpchost || '127.0.0.1', port: config.rpcport, user: config.rpcuser, pass: config.rpcpassword, rejectUnauthorized: _.isUndefined(config.rpcstrict) ? true : config.rpcstrict }); self._initZmqSubSocket(node, config.zmqpubrawtx); self._subscribeZmqEvents(node); return node; }; Bitcoin.prototype.start = function(callback) { var self = this; if (!self.options.connect) { log.error('A "connect" array is required in the bitcoind service configuration.'); process.exit(-1); } self.nodes = self.options.connect.map(self._connectProcess.bind(self)); if (self.nodes.length === 0) { log.error('Could not connect to any servers in connect array.'); process.exit(-1); } async.retry({ interval: 2000, times: 30 }, self._initChain.bind(this), function(err) { if(err) { log.error(err.message); process.exit(-1); } log.info('Bitcoin Daemon Ready'); callback(); }); }; Bitcoin.prototype.stop = function(callback) { callback(); }; Bitcoin.prototype._maybeGetBlockHash = function(blockArg, callback) { var self = this; if (_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) { self._tryAllClients(function(client, done) { client.getBlockHash(blockArg, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } done(null, response.result); }); }, callback); } else { callback(null, blockArg); } }; Bitcoin.prototype.getRawBlock = function(blockArg, callback) { var self = this; function queryBlock(err, blockhash) { if (err) { return callback(err); } self._tryAllClients(function(client, done) { self.client.getBlock(blockhash, false, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } var buffer = new Buffer(response.result, 'hex'); done(null, buffer); }); }, callback); } self._maybeGetBlockHash(blockArg, queryBlock); }; Bitcoin.prototype.getBlockHeader = function(blockArg, callback) { var self = this; function queryHeader(err, blockhash) { if (err) { return callback(err); } self._tryAllClients(function(client, done) { client.getBlockHeader(blockhash, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } var result = response.result; var header = { hash: result.hash, version: result.version, confirmations: result.confirmations, height: result.height, chainWork: result.chainwork, prevHash: result.previousblockhash, nextHash: result.nextblockhash, merkleRoot: result.merkleroot, time: result.time, medianTime: result.mediantime, nonce: result.nonce, bits: result.bits, difficulty: result.difficulty }; done(null, header); }); }, callback); } self._maybeGetBlockHash(blockArg, queryHeader); }; Bitcoin.prototype.getTransaction = function(txid, callback) { var self = this; self._tryAllClients(function(client, done) { //this won't work without a bitcoin node that has a tx index log.error('Txid: ' + txid + ' not found in index! Calling getRawTransaction to retrieve.'); self.client.getRawTransaction(txid.toString('hex'), 0, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } done(null, response.result); }); }, callback); }; Bitcoin.prototype.getBlock = function(blockArg, callback) { var self = this; function queryBlock(err, blockhash) { if (err) { return callback(err); } self._tryAllClients(function(client, done) { client.getBlock(blockhash, false, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } var blockObj = bitcore.Block.fromString(response.result); done(null, blockObj); }); }, callback); } self._maybeGetBlockHash(blockArg, queryBlock); }; Bitcoin.prototype.isSynced = function(callback) { this.syncPercentage(function(err, percentage) { if (err) { return callback(err); } if (Math.round(percentage) >= 100) { callback(null, true); } else { callback(null, false); } }); }; Bitcoin.prototype.syncPercentage = function(callback) { var self = this; self.client.getBlockchainInfo(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var percentSynced = response.result.verificationprogress * 100; callback(null, percentSynced); }); }; module.exports = Bitcoin;