473 lines
12 KiB
JavaScript
473 lines
12 KiB
JavaScript
'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;
|