'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.util.preconditions; var _ = bitcore.deps._; var index = require('../../'); var errors = index.errors; var log = index.log; var Service = require('../../service'); 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._initClients(); this._process = options.process || process; this.on('error', function(err) { log.error(err.stack); }); } 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') } ]; }; 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._initChain = 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; 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; self.emit('ready'); log.info('Bitcoin Daemon Ready'); callback(); }); }); }); }); }; Bitcoin.prototype._zmqBlockHandler = function(node, message) { var self = this; self._rapidProtectedUpdateTip(node, message); self.emit('block', message); for (var i = 0; i < this.subscriptions.hashblock.length; i++) { this.subscriptions.hashblock[i].emit('bitcoind/hashblock', message.toString('hex')); } }; Bitcoin.prototype._rapidProtectedUpdateTip = function(node, message) { var self = this; if (new Date() - self.lastTip > 1000) { self.lastTip = new Date(); self._updateTip(node, message); } else { clearTimeout(self.lastTipTimeout); self.lastTipTimeout = setTimeout(function() { self._updateTip(node, message); }, 1000); } }; Bitcoin.prototype._updateTip = function(node, message) { var self = this; var hex = message.toString('hex'); if (hex !== self.tiphash) { self.tiphash = message.toString('hex'); node.client.getBlock(self.tiphash, function(err, response) { if (err) { var error = self._wrapRPCError(err); self.emit('error', error); } else { self.height = response.result.height; $.checkState(self.height >= 0); self.emit('tip', self.height); } }); } }; 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._checkSyncedAndSubscribeZmqEvents = function(node) { var self = this; var interval; function checkAndSubscribe(callback) { node.client.getBestBlockHash(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var blockhash = new Buffer(response.result, 'hex'); self.emit('block', blockhash); self._updateTip(node, blockhash); node.client.getBlockchainInfo(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var progress = response.result.verificationprogress; if (progress >= self.zmqSubscribeProgress) { self._subscribeZmqEvents(node); clearInterval(interval); callback(null, true); } else { callback(null, false); } }); }); } checkAndSubscribe(function(err, synced) { if (err) { log.error(err); } if (!synced) { interval = setInterval(function() { if (self.node.stopping) { return clearInterval(interval); } checkAndSubscribe(function(err) { if (err) { log.error(err); } }); }, node._tipUpdateInterval || Bitcoin.DEFAULT_TIP_UPDATE_INTERVAL); } }); }; Bitcoin.prototype._subscribeZmqEvents = function(node) { var self = this; node.zmqSubSocket.subscribe('hashblock'); node.zmqSubSocket.subscribe('rawtx'); 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(node, 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); node.zmqSubSocket.connect(zmqUrl); }; Bitcoin.prototype._loadTipFromNode = function(node, callback) { var self = this; node.client.getBestBlockHash(function(err, response) { if (err && err.code === -28) { log.warn(err.message); return callback(self._wrapRPCError(err)); } else if (err) { return callback(self._wrapRPCError(err)); } node.client.getBlock(response.result, function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } self.height = response.result.height; $.checkState(self.height >= 0); self.emit('tip', self.height); callback(); }); }); }; Bitcoin.prototype._connectProcess = function(config, callback) { var self = this; var node = {}; var exitShutdown = false; async.retry({times: 60, interval: self.startRetryInterval}, function(done) { if (self.node.stopping) { exitShutdown = true; return done(); } 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._loadTipFromNode(node, done); }, function(err) { if (err) { return callback(err); } if (exitShutdown) { return callback(new Error('Stopping while trying to connect to bitcoind.')); } self._initZmqSubSocket(node, config.zmqpubrawtx); self._subscribeZmqEvents(node); callback(null, node); }); }; Bitcoin.prototype.start = function(callback) { var self = this; async.series([ function(next) { if (self.options.spawn) { self._spawnChildProcess(function(err, node) { if (err) { return next(err); } self.nodes.push(node); next(); }); } else { next(); } }, function(next) { if (self.options.connect) { async.map(self.options.connect, self._connectProcess.bind(self), function(err, nodes) { if (err) { return callback(err); } for(var i = 0; i < nodes.length; i++) { self.nodes.push(nodes[i]); } next(); }); } else { next(); } } ], function(err) { if (err) { return callback(err); } if (self.nodes.length === 0) { return callback(new Error('Bitcoin configuration options "spawn" or "connect" are expected')); } self._initChain(callback); }); }; Bitcoin.prototype._getAddressDetailsForOutput = function(output, outputIndex, result, addressStrings) { if (!output.address) { return; } var address = output.address; if (addressStrings.indexOf(address) >= 0) { if (!result.addresses[address]) { result.addresses[address] = { inputIndexes: [], outputIndexes: [outputIndex] }; } else { result.addresses[address].outputIndexes.push(outputIndex); } result.satoshis += output.satoshis; } }; 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.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.stop = function(callback) { callback(); }; module.exports = Bitcoin;