bitcore-node-zcash/lib/services/bitcoind.js

1498 lines
42 KiB
JavaScript
Raw Normal View History

2015-08-31 06:00:00 -07:00
'use strict';
2015-09-16 10:35:54 -07:00
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
2015-08-31 06:00:00 -07:00
var util = require('util');
var mkdirp = require('mkdirp');
2015-10-16 21:56:29 -07:00
var bitcore = require('bitcore-lib');
var zmq = require('zmq');
var async = require('async');
2016-03-25 11:17:22 -07:00
var LRU = require('lru-cache');
var BitcoinRPC = require('bitcoind-rpc');
2015-08-31 06:00:00 -07:00
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
2015-08-31 06:00:00 -07:00
var index = require('../');
var errors = index.errors;
2015-08-31 06:00:00 -07:00
var log = index.log;
var utils = require('../utils');
2015-08-31 06:00:00 -07:00
var Service = require('../service');
var Transaction = require('../transaction');
2015-08-31 06:00:00 -07:00
/**
2016-04-08 08:58:59 -07:00
* Provides a friendly event driven API to bitcoind in Node.js. Manages starting and
* stopping bitcoind as a child process for application support, as well as connecting
* to multiple bitcoind processes for server infrastructure. Results are cached in an
* LRU cache for improved performance and methods added for common queries.
*
2015-08-31 06:00:00 -07:00
* @param {Object} options
* @param {Node} options.node - A reference to the node
*/
function Bitcoin(options) {
if (!(this instanceof Bitcoin)) {
return new Bitcoin(options);
}
2015-08-31 06:00:00 -07:00
Service.call(this, options);
2016-04-08 08:58:59 -07:00
this.options = options;
this._initCaches();
// bitcoind child process
this.spawn = false;
2016-03-25 11:17:22 -07:00
2016-04-08 11:44:24 -07:00
// event subscribers
this.subscriptions = {};
this.subscriptions.rawtransaction = [];
this.subscriptions.hashblock = [];
2016-04-08 11:44:24 -07:00
// limits
this.maxAddressesQuery = options.maxAddressesQuery || Bitcoin.DEFAULT_MAX_ADDRESSES_QUERY;
// try all interval
this.tryAllInterval = options.tryAllInterval || Bitcoin.DEFAULT_TRY_ALL_INTERVAL;
this.startRetryInterval = options.startRetryInterval || Bitcoin.DEFAULT_START_RETRY_INTERVAL;
2016-04-08 08:58:59 -07:00
// available bitcoind nodes
this._initClients();
}
util.inherits(Bitcoin, Service);
Bitcoin.dependencies = [];
Bitcoin.DEFAULT_MAX_ADDRESSES_QUERY = 10000;
Bitcoin.DEFAULT_TRY_ALL_INTERVAL = 1000;
Bitcoin.DEFAULT_START_RETRY_INTERVAL = 5000;
Bitcoin.DEFAULT_CONFIG_SETTINGS = {
server: 1,
whitelist: '127.0.0.1',
txindex: 1,
addressindex: 1,
timestampindex: 1,
spentindex: 1,
zmqpubrawtx: 'tcp://127.0.0.1:28332',
zmqpubhashblock: 'tcp://127.0.0.1:28332',
rpcallowip: '127.0.0.1',
rpcuser: 'bitcoin',
rpcpassword: 'local321'
};
2016-04-08 08:58:59 -07:00
Bitcoin.prototype._initCaches = function() {
2016-03-25 11:17:22 -07:00
// caches valid until there is a new block
2016-04-05 07:32:07 -07:00
this.utxosCache = LRU(50000);
2016-03-25 11:17:22 -07:00
this.txidsCache = LRU(50000);
this.balanceCache = LRU(50000);
this.summaryCache = LRU(50000);
2016-04-12 12:02:37 -07:00
this.transactionInfoCache = LRU(100000);
2016-03-25 11:17:22 -07:00
// caches valid indefinitely
2016-03-25 11:17:22 -07:00
this.transactionCache = LRU(100000);
this.rawTransactionCache = LRU(50000);
2016-03-25 11:17:22 -07:00
this.blockCache = LRU(144);
this.rawBlockCache = LRU(72);
2016-03-25 11:17:22 -07:00
this.blockHeaderCache = LRU(288);
this.zmqKnownTransactions = LRU(50);
2016-04-11 07:15:20 -07:00
this.zmqKnownBlocks = LRU(50);
this.lastTip = 0;
this.lastTipTimeout = false;
2016-04-08 08:58:59 -07:00
};
2016-04-08 08:58:59 -07:00
Bitcoin.prototype._initClients = function() {
var self = this;
this.nodes = [];
this.nodesIndex = 0;
Object.defineProperty(this, 'client', {
get: function() {
2016-04-07 11:46:19 -07:00
var client = self.nodes[self.nodesIndex].client;
self.nodesIndex = (self.nodesIndex + 1) % self.nodes.length;
return client;
},
enumerable: true,
configurable: false
});
2016-04-08 08:58:59 -07:00
};
/**
* Called by Node to determine the available API methods.
*/
Bitcoin.prototype.getAPIMethods = function() {
var methods = [
['getBlock', this, this.getBlock, 1],
['getRawBlock', this, this.getRawBlock, 1],
['getBlockHeader', this, this.getBlockHeader, 1],
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2],
['getBestBlockHash', this, this.getBestBlockHash, 0],
2016-04-12 12:02:37 -07:00
['getSpentInfo', this, this.getSpentInfo, 1],
['getInfo', this, this.getInfo, 0],
['syncPercentage', this, this.syncPercentage, 0],
['isSynced', this, this.isSynced, 0],
['getRawTransaction', this, this.getRawTransaction, 1],
['getTransaction', this, this.getTransaction, 2],
['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2],
['sendTransaction', this, this.sendTransaction, 1],
['estimateFee', this, this.estimateFee, 1],
['getAddressTxids', this, this.getAddressTxids, 2],
['getAddressBalance', this, this.getAddressBalance, 2],
['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 2],
['getAddressHistory', this, this.getAddressHistory, 2],
['getAddressSummary', this, this.getAddressSummary, 1],
['generateBlock', this, this.generateBlock, 1]
];
return methods;
};
2015-08-31 06:00:00 -07:00
2016-04-08 11:44:24 -07:00
/**
* Called by the Bus to determine the available events.
*/
Bitcoin.prototype.getPublishEvents = function() {
return [
{
name: 'bitcoind/rawtransaction',
2016-04-08 11:44:24 -07:00
scope: this,
subscribe: this.subscribe.bind(this, 'rawtransaction'),
unsubscribe: this.unsubscribe.bind(this, 'rawtransaction')
2016-04-08 11:44:24 -07:00
},
{
name: 'bitcoind/hashblock',
2016-04-08 11:44:24 -07:00
scope: this,
subscribe: this.subscribe.bind(this, 'hashblock'),
unsubscribe: this.unsubscribe.bind(this, 'hashblock')
2016-04-08 11:44:24 -07:00
}
];
};
Bitcoin.prototype.subscribe = function(name, emitter) {
this.subscriptions[name].push(emitter);
};
Bitcoin.prototype.unsubscribe = function(name, emitter) {
var index = this.subscriptions[name].indexOf(emitter);
if (index > -1) {
this.subscriptions[name].splice(index, 1);
}
};
Bitcoin.prototype._getDefaultConfig = function() {
var config = '';
var defaults = Bitcoin.DEFAULT_CONFIG_SETTINGS;
for(var key in defaults) {
config += key + '=' + defaults[key] + '\n';
}
return config;
};
Bitcoin.prototype._loadSpawnConfiguration = function(node) {
2015-08-31 06:00:00 -07:00
/* jshint maxstatements: 25 */
$.checkArgument(this.options.spawn, 'Please specify "spawn" in bitcoind config options');
$.checkArgument(this.options.spawn.datadir, 'Please specify "spawn.datadir" in bitcoind config options');
$.checkArgument(this.options.spawn.exec, 'Please specify "spawn.exec" in bitcoind config options');
var spawnOptions = this.options.spawn;
var configPath = spawnOptions.datadir + '/bitcoin.conf';
2015-08-31 06:00:00 -07:00
this.spawn = {};
this.spawn.datadir = this.options.spawn.datadir;
this.spawn.exec = this.options.spawn.exec;
this.spawn.configPath = configPath;
this.spawn.config = {};
if (!fs.existsSync(spawnOptions.datadir)) {
mkdirp.sync(spawnOptions.datadir);
2015-08-31 06:00:00 -07:00
}
if (!fs.existsSync(configPath)) {
var defaultConfig = this._getDefaultConfig();
fs.writeFileSync(configPath, defaultConfig);
}
2015-08-31 06:00:00 -07:00
var file = fs.readFileSync(configPath);
var unparsed = file.toString().split('\n');
for(var i = 0; i < unparsed.length; i++) {
var line = unparsed[i];
if (!line.match(/^\#/) && line.match(/\=/)) {
var option = line.split('=');
var value;
if (!Number.isNaN(Number(option[1]))) {
value = Number(option[1]);
} else {
value = option[1];
}
this.spawn.config[option[0]] = value;
2015-08-31 06:00:00 -07:00
}
}
var spawnConfig = this.spawn.config;
this._checkConfigIndexes(spawnConfig, node);
};
Bitcoin.prototype._checkConfigIndexes = function(spawnConfig, node) {
2015-08-31 06:00:00 -07:00
$.checkState(
spawnConfig.txindex && spawnConfig.txindex === 1,
'"txindex" option is required in order to use transaction query features of bitcore-node. ' +
2015-08-31 06:00:00 -07:00
'Please add "txindex=1" to your configuration and reindex an existing database if ' +
2015-08-31 06:00:00 -07:00
'necessary with reindex=1'
);
$.checkState(
spawnConfig.addressindex && spawnConfig.addressindex === 1,
'"addressindex" option is required in order to use address query features of bitcore-node. ' +
'Please add "addressindex=1" to your configuration and reindex an existing database if ' +
'necessary with reindex=1'
);
$.checkState(
spawnConfig.spentindex && spawnConfig.spentindex === 1,
'"spentindex" option is required in order to use spent info query features of bitcore-node. ' +
'Please add "spentindex=1" to your configuration and reindex an existing database if ' +
'necessary with reindex=1'
);
$.checkState(
spawnConfig.server && spawnConfig.server === 1,
'"server" option is required to communicate to bitcoind from bitcore. ' +
'Please add "server=1" to your configuration and restart'
);
$.checkState(
spawnConfig.zmqpubrawtx,
'"zmqpubrawtx" option is required to get event updates from bitcoind. ' +
'Please add "zmqpubrawtx=tcp://127.0.0.1:<port>" to your configuration and restart'
);
$.checkState(
spawnConfig.zmqpubhashblock,
'"zmqpubhashblock" option is required to get event updates from bitcoind. ' +
'Please add "zmqpubhashblock=tcp://127.0.0.1:<port>" to your configuration and restart'
);
if (spawnConfig.reindex && spawnConfig.reindex === 1) {
log.warn('Reindex option is currently enabled. This means that bitcoind is undergoing a reindex. ' +
'The reindex flag will start the index from beginning every time the node is started, so it ' +
'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' +
'of bitcore-node services will start.');
node._reindex = true;
}
2015-08-31 06:00:00 -07:00
};
2016-03-25 11:17:22 -07:00
Bitcoin.prototype._resetCaches = function() {
2016-04-12 12:02:37 -07:00
this.transactionInfoCache.reset();
2016-04-05 07:32:07 -07:00
this.utxosCache.reset();
2016-03-25 11:17:22 -07:00
this.txidsCache.reset();
this.balanceCache.reset();
this.summaryCache.reset();
};
Bitcoin.prototype._tryAll = function(func, callback) {
async.retry({times: this.nodes.length, interval: this.tryAllInterval || 1000}, func, callback);
};
Bitcoin.prototype._wrapRPCError = function(errObj) {
var err = new errors.RPCError(errObj.message);
err.code = errObj.code;
return err;
};
Bitcoin.prototype._initChain = function(callback) {
2015-08-31 08:09:24 -07:00
var self = this;
self.client.getBestBlockHash(function(err, response) {
2015-08-31 08:09:24 -07:00
if (err) {
return callback(self._wrapRPCError(err));
2015-08-31 08:09:24 -07:00
}
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();
});
});
});
2015-08-31 08:09:24 -07:00
});
};
2015-08-31 08:09:24 -07:00
Bitcoin.prototype._getNetworkOption = function() {
var networkOption;
if (this.node.network === bitcore.Networks.testnet) {
2016-04-07 11:46:19 -07:00
networkOption = '--testnet';
if (this.node.network.regtestEnabled) {
networkOption = '--regtest';
}
}
return networkOption;
2015-08-31 08:09:24 -07:00
};
Bitcoin.prototype._zmqBlockHandler = function(node, message) {
var self = this;
// Update the current chain tip
self._rapidProtectedUpdateTip(node, message);
2016-04-07 11:46:19 -07:00
// Notify block subscribers
var id = message.toString('binary');
if (!self.zmqKnownBlocks.get(id)) {
self.zmqKnownBlocks.set(id, true);
self.emit('block', message);
for (var i = 0; i < this.subscriptions.hashblock.length; i++) {
this.subscriptions.hashblock[i].emit('bitcoind/hashblock', message.toString('hex'));
}
}
2016-04-07 11:46:19 -07:00
};
Bitcoin.prototype._rapidProtectedUpdateTip = function(node, message) {
var self = this;
2016-04-11 07:15:20 -07:00
// Prevent a rapid succession of tip updates
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);
2016-04-07 11:46:19 -07:00
}
};
2016-04-07 11:46:19 -07:00
Bitcoin.prototype._updateTip = function(node, message) {
var self = this;
2016-04-11 07:15:20 -07:00
var hex = message.toString('hex');
if (hex !== self.tiphash) {
self.tiphash = message.toString('hex');
// reset block valid caches
self._resetCaches();
2016-04-11 07:15:20 -07:00
node.client.getBlock(self.tiphash, function(err, response) {
if (err) {
var error = self._wrapRPCError(err);
log.error(error);
self.emit('error', error);
} else {
self.height = response.result.height;
$.checkState(self.height >= 0);
self.emit('tip', self.height);
}
});
if(!self.node.stopping) {
self.syncPercentage(function(err, percentage) {
if (err) {
log.error(err);
self.emit('error', err);
} else {
if (Math.round(percentage) >= 100) {
self.emit('synced', self.height);
}
log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2));
}
});
}
2016-04-08 11:44:24 -07:00
}
};
Bitcoin.prototype._zmqTransactionHandler = function(node, message) {
var self = this;
var id = message.toString('binary');
if (!self.zmqKnownTransactions.get(id)) {
self.zmqKnownTransactions.set(id, true);
self.emit('tx', message);
2016-04-08 11:44:24 -07:00
// Notify transaction subscribers
for (var i = 0; i < this.subscriptions.rawtransaction.length; i++) {
this.subscriptions.rawtransaction[i].emit('bitcoind/rawtransaction', message.toString('hex'));
}
2016-04-08 11:44:24 -07:00
}
};
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) {
var self = this;
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) {
log.warn('ZMQ connection delay:', endPoint);
});
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() {
self.zmqSubSocket.monitor(500, 0);
}, 5000);
});
node.zmqSubSocket.monitor(500, 0);
node.zmqSubSocket.connect(zmqUrl);
};
Bitcoin.prototype._checkReindex = function(node, callback) {
2015-08-31 06:00:00 -07:00
var self = this;
var interval;
function finish(err) {
clearInterval(interval);
callback(err);
}
if (node._reindex) {
interval = setInterval(function() {
node.client.syncPercentage(function(err, percentSynced) {
if (err) {
return finish(self._wrapRPCError(err));
}
log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2));
if (Math.round(percentSynced) >= 100) {
node._reindex = false;
finish();
}
});
}, self._reindexWait);
} else {
callback();
}
};
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._spawnChildProcess = function(callback) {
var self = this;
var node = {};
node._reindex = false;
node._reindexWait = 10000;
try {
self._loadSpawnConfiguration(node);
} catch(e) {
return callback(e);
}
2015-08-31 06:00:00 -07:00
var options = [
'--conf=' + this.spawn.configPath,
'--datadir=' + this.spawn.datadir,
];
if (self._getNetworkOption()) {
options.push(self._getNetworkOption());
2016-02-03 15:29:33 -08:00
}
self.spawn.process = spawn(this.spawn.exec, options, {stdio: 'inherit'});
2016-02-03 15:29:33 -08:00
self.spawn.process.on('error', function(err) {
log.error(err);
});
async.retry({times: 60, interval: self.startRetryInterval}, function(done) {
if (self.node.stopping) {
return done(new Error('Stopping while trying to connect to bitcoind.'));
2015-08-31 06:00:00 -07:00
}
node.client = new BitcoinRPC({
protocol: 'http',
host: '127.0.0.1',
port: self.spawn.config.rpcport,
user: self.spawn.config.rpcuser,
pass: self.spawn.config.rpcpassword
});
self._loadTipFromNode(node, done);
}, function(err) {
if (err) {
return callback(err);
}
self._initZmqSubSocket(node, self.spawn.config.zmqpubrawtx);
self._checkReindex(node, function(err) {
2015-08-31 08:09:24 -07:00
if (err) {
return callback(err);
2015-08-31 06:00:00 -07:00
}
self._subscribeZmqEvents(node);
callback(null, node);
});
});
};
Bitcoin.prototype._connectProcess = function(config, callback) {
var self = this;
var node = {};
async.retry({times: 60, interval: self.startRetryInterval}, function(done) {
if (self.node.stopping) {
return done(new Error('Stopping while trying to connect to bitcoind.'));
}
node.client = new BitcoinRPC({
protocol: config.rpcprotocol || 'http',
host: config.rpchost || '127.0.0.1',
port: config.rpcport,
user: config.rpcuser,
pass: config.rpcpassword
});
self._loadTipFromNode(node, done);
}, function(err) {
if (err) {
return callback(err);
}
self._initZmqSubSocket(node, config.zmqpubrawtx);
self._subscribeZmqEvents(node);
callback(null, node);
});
};
/**
* Called by Node to start the service
* @param {Function} callback
*/
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);
}
2016-04-07 11:46:19 -07:00
if (self.nodes.length === 0) {
return callback(new Error('Bitcoin configuration options "spawn" or "connect" are expected'));
}
self._initChain(callback);
2015-08-31 06:00:00 -07:00
});
2015-08-31 06:00:00 -07:00
};
2015-09-22 08:38:14 -07:00
/**
* Helper to determine the state of the database.
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
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);
}
});
2015-08-31 06:00:00 -07:00
};
2015-09-22 08:38:14 -07:00
/**
* Helper to determine the progress of the database.
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
Bitcoin.prototype.syncPercentage = function(callback) {
var self = this;
this.client.getBlockchainInfo(function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
var percentSynced = response.result.verificationprogress * 100;
callback(null, percentSynced);
});
};
2016-04-08 13:04:27 -07:00
Bitcoin.prototype._normalizeAddressArg = function(addressArg) {
var addresses = [addressArg];
if (Array.isArray(addressArg)) {
addresses = addressArg;
}
return addresses;
};
2016-04-08 08:58:59 -07:00
/**
* Will get the balance for an address or multiple addresses
* @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses
* @param {Object} options
* @param {Function} callback
*/
Bitcoin.prototype.getAddressBalance = function(addressArg, options, callback) {
2016-03-25 11:17:22 -07:00
var self = this;
2016-04-08 13:04:27 -07:00
var addresses = self._normalizeAddressArg(addressArg);
2016-03-25 11:17:22 -07:00
var cacheKey = addresses.join('');
var balance = self.balanceCache.get(cacheKey);
if (balance) {
return setImmediate(function() {
callback(null, balance);
});
} else {
this.client.getAddressBalance({addresses: addresses}, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
2016-03-25 11:17:22 -07:00
}
self.balanceCache.set(cacheKey, response.result);
callback(null, response.result);
});
}
};
2016-04-08 08:58:59 -07:00
/**
* Will get the unspent outputs for an address or multiple addresses
* @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses
* @param {Object} options
* @param {Function} callback
*/
2016-04-05 07:32:07 -07:00
Bitcoin.prototype.getAddressUnspentOutputs = function(addressArg, options, callback) {
var self = this;
2016-04-08 13:04:27 -07:00
var addresses = self._normalizeAddressArg(addressArg);
2016-04-05 07:32:07 -07:00
var cacheKey = addresses.join('');
var utxos = self.utxosCache.get(cacheKey);
if (utxos) {
return setImmediate(function() {
callback(null, utxos);
});
} else {
self.client.getAddressUtxos({addresses: addresses}, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
2016-04-05 07:32:07 -07:00
}
self.utxosCache.set(cacheKey, response.result);
callback(null, response.result);
});
}
};
Bitcoin.prototype._getBalanceFromMempool = function(deltas) {
var satoshis = 0;
for (var i = 0; i < deltas.length; i++) {
satoshis += deltas[i].satoshis;
}
return satoshis;
};
Bitcoin.prototype._getTxidsFromMempool = function(deltas) {
var mempoolTxids = [];
var mempoolTxidsKnown = {};
for (var i = 0; i < deltas.length; i++) {
var txid = deltas[i].txid;
if (!mempoolTxidsKnown[txid]) {
mempoolTxids.push(txid);
mempoolTxidsKnown[txid] = true;
}
}
return mempoolTxids;
};
Bitcoin.prototype._getHeightRangeQuery = function(options, clone) {
2016-04-18 10:37:37 -07:00
if (options.start >= 0 && options.end >= 0) {
if (options.end > options.start) {
throw new TypeError('"end" is expected to be less than or equal to "start"');
}
if (clone) {
// reverse start and end as the order in bitcore is most recent to less recent
clone.start = options.end;
clone.end = options.start;
}
return true;
}
return false;
};
2016-04-08 08:58:59 -07:00
/**
* Will get the txids for an address or multiple addresses
* @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses
* @param {Object} options
* @param {Function} callback
*/
Bitcoin.prototype.getAddressTxids = function(addressArg, options, callback) {
2016-03-25 11:17:22 -07:00
var self = this;
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
var rangeQuery = false;
try {
rangeQuery = self._getHeightRangeQuery(options);
} catch(err) {
return callback(err);
}
if (rangeQuery) {
queryMempool = false;
}
2016-04-08 13:04:27 -07:00
var addresses = self._normalizeAddressArg(addressArg);
2016-03-25 11:17:22 -07:00
var cacheKey = addresses.join('');
var mempoolTxids = [];
2016-03-25 11:17:22 -07:00
var txids = self.txidsCache.get(cacheKey);
function finish() {
if (txids && !rangeQuery) {
var allTxids = mempoolTxids.reverse().concat(txids);
return setImmediate(function() {
callback(null, allTxids);
});
} else {
var txidOpts = {
addresses: addresses
};
if (rangeQuery) {
self._getHeightRangeQuery(options, txidOpts);
}
self.client.getAddressTxids(txidOpts, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
response.result.reverse();
if (!rangeQuery) {
self.txidsCache.set(cacheKey, response.result);
}
var allTxids = mempoolTxids.reverse().concat(response.result);
return callback(null, allTxids);
});
}
}
if (queryMempool) {
self.client.getAddressMempool({addresses: addresses}, function(err, response) {
2016-03-25 11:17:22 -07:00
if (err) {
return callback(self._wrapRPCError(err));
2016-03-25 11:17:22 -07:00
}
mempoolTxids = self._getTxidsFromMempool(response.result);
finish();
2016-03-25 11:17:22 -07:00
});
} else {
finish();
2016-03-25 11:17:22 -07:00
}
};
Bitcoin.prototype._getConfirmationsDetail = function(transaction) {
$.checkState(this.height > 0, 'current height is unknown');
var confirmations = 0;
if (transaction.__height >= 0) {
confirmations = this.height - transaction.__height + 1;
}
if (confirmations < 0) {
log.warn('Negative confirmations calculated for transaction:', transaction.hash);
}
return Math.max(0, confirmations);
};
Bitcoin.prototype._getAddressDetailsForTransaction = function(transaction, addressStrings) {
var result = {
addresses: {},
satoshis: 0
};
for (var inputIndex = 0; inputIndex < transaction.inputs.length; inputIndex++) {
var input = transaction.inputs[inputIndex];
if (!input.script) {
continue;
}
var inputAddress = input.script.toAddress(this.node.network);
if (inputAddress) {
var inputAddressString = inputAddress.toString();
if (addressStrings.indexOf(inputAddressString) >= 0) {
if (!result.addresses[inputAddressString]) {
result.addresses[inputAddressString] = {
inputIndexes: [inputIndex],
outputIndexes: []
};
} else {
result.addresses[inputAddressString].inputIndexes.push(inputIndex);
}
result.satoshis -= input.output.satoshis;
}
}
}
for (var outputIndex = 0; outputIndex < transaction.outputs.length; outputIndex++) {
var output = transaction.outputs[outputIndex];
if (!output.script) {
continue;
}
var outputAddress = output.script.toAddress(this.node.network);
if (outputAddress) {
var outputAddressString = outputAddress.toString();
if (addressStrings.indexOf(outputAddressString) >= 0) {
if (!result.addresses[outputAddressString]) {
result.addresses[outputAddressString] = {
inputIndexes: [],
outputIndexes: [outputIndex]
};
} else {
result.addresses[outputAddressString].outputIndexes.push(outputIndex);
}
result.satoshis += output.satoshis;
}
}
}
return result;
};
/**
* Will expand into a detailed transaction from a txid
* @param {Object} txid - A bitcoin transaction id
* @param {Function} callback
*/
Bitcoin.prototype._getDetailedTransaction = function(txid, options, next) {
var self = this;
self.getTransactionWithBlockInfo(
txid,
function(err, transaction) {
if (err) {
return next(err);
}
transaction.populateInputs(self, [], function(err) {
if (err) {
return next(err);
}
var addressDetails = self._getAddressDetailsForTransaction(transaction, options.addressStrings);
var details = {
addresses: addressDetails.addresses,
satoshis: addressDetails.satoshis,
height: transaction.__height,
confirmations: self._getConfirmationsDetail(transaction),
timestamp: transaction.__timestamp,
// TODO bitcore-lib should return null instead of throwing error on coinbase
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
tx: transaction
};
next(null, details);
});
}
);
};
Bitcoin.prototype._getAddressStrings = function(addresses) {
var addressStrings = [];
for (var i = 0; i < addresses.length; i++) {
var address = addresses[i];
if (address instanceof bitcore.Address) {
addressStrings.push(address.toString());
} else if (_.isString(address)) {
addressStrings.push(address);
} else {
throw new TypeError('Addresses are expected to be strings');
}
}
return addressStrings;
};
2016-03-25 11:17:22 -07:00
Bitcoin.prototype._paginateTxids = function(fullTxids, from, to) {
var txids;
if (from >= 0 && to >= 0) {
$.checkState(from < to, '"from" is expected to be less than "to"');
2016-03-25 12:09:44 -07:00
txids = fullTxids.slice(from, to);
2016-03-25 11:17:22 -07:00
} else {
txids = fullTxids;
}
return txids;
};
2016-04-08 08:58:59 -07:00
/**
* Will detailed transaction history for an address or multiple addresses
* @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses
* @param {Object} options
* @param {Function} callback
*/
Bitcoin.prototype.getAddressHistory = function(addressArg, options, callback) {
var self = this;
2016-04-08 13:04:27 -07:00
var addresses = self._normalizeAddressArg(addressArg);
if (addresses.length > this.maxAddressesQuery) {
return callback(new TypeError('Maximum number of addresses (' + this.maxAddressesQuery + ') exceeded'));
}
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
var addressStrings = this._getAddressStrings(addresses);
self.getAddressTxids(addresses, options, function(err, txids) {
if (err) {
return callback(err);
}
2016-03-25 11:17:22 -07:00
var totalCount = txids.length;
try {
txids = self._paginateTxids(txids, options.from, options.to);
} catch(e) {
return callback(e);
}
2016-03-25 11:17:22 -07:00
async.mapSeries(
txids,
function(txid, next) {
self._getDetailedTransaction(txid, {
queryMempool: queryMempool,
addressStrings: addressStrings
}, next);
},
function(err, transactions) {
if (err) {
return callback(err);
}
callback(null, {
2016-03-25 11:17:22 -07:00
totalCount: totalCount,
items: transactions
});
}
);
});
};
2016-04-08 08:58:59 -07:00
/**
* Will get the summary including txids and balance for an address or multiple addresses
* @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses
* @param {Object} options
* @param {Function} callback
*/
Bitcoin.prototype.getAddressSummary = function(addressArg, options, callback) {
var self = this;
var summary = {};
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
2016-03-25 11:17:22 -07:00
var summaryTxids = [];
var mempoolTxids = [];
2016-04-08 13:04:27 -07:00
var addresses = self._normalizeAddressArg(addressArg);
2016-03-25 11:17:22 -07:00
var cacheKey = addresses.join('');
2016-03-25 11:17:22 -07:00
function querySummary() {
async.parallel([
function getTxList(done) {
2016-04-08 13:04:27 -07:00
self.getAddressTxids(addresses, {queryMempool: false}, function(err, txids) {
2016-03-25 11:17:22 -07:00
if (err) {
return done(err);
}
summaryTxids = txids;
summary.appearances = txids.length;
done();
});
},
function getBalance(done) {
2016-04-08 13:04:27 -07:00
self.getAddressBalance(addresses, options, function(err, data) {
2016-03-25 11:17:22 -07:00
if (err) {
return done(err);
}
summary.totalReceived = data.received;
summary.totalSpent = data.received - data.balance;
summary.balance = data.balance;
done();
});
},
function getMempool(done) {
if (!queryMempool) {
return done();
}
2016-04-08 13:04:27 -07:00
self.client.getAddressMempool({'addresses': addresses}, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
mempoolTxids = self._getTxidsFromMempool(response.result);
summary.unconfirmedAppearances = mempoolTxids.length;
summary.unconfirmedBalance = self._getBalanceFromMempool(response.result);
done();
});
},
2016-03-25 11:17:22 -07:00
], function(err) {
if (err) {
2016-03-25 11:17:22 -07:00
return callback(err);
}
2016-03-25 11:17:22 -07:00
self.summaryCache.set(cacheKey, summary);
if (!options.noTxList) {
var allTxids = mempoolTxids.reverse().concat(summaryTxids);
summary.txids = allTxids;
2016-03-25 11:17:22 -07:00
}
callback(null, summary);
});
}
2016-03-25 11:17:22 -07:00
if (options.noTxList) {
var summaryCache = self.summaryCache.get(cacheKey);
if (summaryCache) {
callback(null, summaryCache);
} else {
querySummary();
}
} else {
querySummary();
}
2015-08-31 06:00:00 -07:00
};
/**
* Will retrieve a block as a Node.js Buffer
* @param {String|Number} block - A block hash or block height number
* @param {Function} callback
*/
Bitcoin.prototype.getRawBlock = function(blockArg, callback) {
// TODO apply performance patch to the RPC method for raw data
var self = this;
function queryBlock(blockhash) {
self._tryAll(function(done) {
self.client.getBlock(blockhash, false, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var buffer = new Buffer(response.result, 'hex');
self.rawBlockCache.set(blockhash, buffer);
done(null, buffer);
});
}, callback);
}
var cachedBlock = self.rawBlockCache.get(blockArg);
if (cachedBlock) {
return setImmediate(function() {
callback(null, cachedBlock);
});
} else {
if (_.isNumber(blockArg)) {
self._tryAll(function(done) {
self.client.getBlockHash(blockArg, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
done(null, response.result);
});
}, function(err, blockhash) {
if (err) {
return callback(err);
}
queryBlock(blockhash);
});
} else {
queryBlock(blockArg);
}
}
};
2015-09-22 08:38:14 -07:00
/**
2016-04-08 08:58:59 -07:00
* Will retrieve a block as a Bitcore object
2015-09-22 08:38:14 -07:00
* @param {String|Number} block - A block hash or block height number
2016-04-08 08:58:59 -07:00
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
2016-03-31 07:15:29 -07:00
Bitcoin.prototype.getBlock = function(blockArg, callback) {
// TODO apply performance patch to the RPC method for raw data
var self = this;
2016-03-25 11:17:22 -07:00
function queryBlock(blockhash) {
2016-04-18 07:37:33 -07:00
var cachedBlock = self.blockCache.get(blockhash);
if (cachedBlock) {
return setImmediate(function() {
callback(null, cachedBlock);
});
2016-04-18 07:37:33 -07:00
} else {
self._tryAll(function(done) {
2016-04-18 07:37:33 -07:00
self.client.getBlock(blockhash, false, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
2016-04-18 07:37:33 -07:00
var blockObj = bitcore.Block.fromString(response.result);
self.blockCache.set(blockhash, blockObj);
done(null, blockObj);
});
2016-04-18 07:37:33 -07:00
}, callback);
}
}
if (_.isNumber(blockArg)) {
self._tryAll(function(done) {
self.client.getBlockHash(blockArg, function(err, response) {
2016-03-25 11:17:22 -07:00
if (err) {
2016-04-18 07:37:33 -07:00
return done(self._wrapRPCError(err));
2016-03-25 11:17:22 -07:00
}
2016-04-18 07:37:33 -07:00
done(null, response.result);
2016-03-25 11:17:22 -07:00
});
2016-04-18 07:37:33 -07:00
}, function(err, blockhash) {
if (err) {
return callback(err);
}
queryBlock(blockhash);
});
} else {
queryBlock(blockArg);
}
2015-08-31 06:00:00 -07:00
};
2016-04-08 08:58:59 -07:00
/**
* Will retrieve an array of block hashes within a range of timestamps
* @param {Number} high - The more recent timestamp in seconds
* @param {Number} low - The older timestamp in seconds
* @param {Function} callback
*/
Bitcoin.prototype.getBlockHashesByTimestamp = function(high, low, callback) {
var self = this;
self.client.getBlockHashes(high, low, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
callback(null, response.result);
});
2015-08-31 06:00:00 -07:00
};
2015-09-22 08:38:14 -07:00
/**
* Will return the block index information, the output will have the format:
* {
* prevHash: '000000004956cc2edd1a8caa05eacfa3c69f4c490bfc9ace820257834115ab35',
* nextHash: '0000000000629d100db387f37d0f37c51118f250fb0946310a8c37316cbc4028'
* hash: ' 00000000009e2958c15ff9290d571bf9459e93b19765c6801ddeccadbb160a1e',
2015-09-22 08:38:14 -07:00
* chainWork: '0000000000000000000000000000000000000000000000000000000000000016',
* height: 10
* }
* @param {String|Number} block - A block hash or block height
2016-04-08 08:58:59 -07:00
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
Bitcoin.prototype.getBlockHeader = function(block, callback) {
var self = this;
2015-08-31 06:00:00 -07:00
function queryHeader(blockhash) {
self._tryAll(function(done) {
self.client.getBlockHeader(blockhash, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
// TODO format response prevHash instead of previousblockhash, etc.
done(null, response.result);
});
}, callback);
}
if (_.isNumber(block)) {
self._tryAll(function(done) {
self.client.getBlockHash(block, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
done(null, response.result);
});
}, function(err, blockhash) {
if (err) {
return callback(err);
}
queryHeader(blockhash);
});
} else {
queryHeader(block);
}
2015-09-08 11:04:14 -07:00
};
2015-09-22 08:38:14 -07:00
/**
* Will estimate the fee per kilobyte.
* @param {Number} blocks - The number of blocks for the transaction to be confirmed.
2016-04-08 08:58:59 -07:00
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
Bitcoin.prototype.estimateFee = function(blocks, callback) {
var self = this;
this.client.estimateFee(blocks, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
callback(null, response.result);
});
2015-08-31 06:00:00 -07:00
};
2015-09-22 08:38:14 -07:00
/**
2016-04-08 08:58:59 -07:00
* Will add a transaction to the mempool and relay to connected peers
* @param {String|Transaction} transaction - The hex string of the transaction
* @param {Object=} options
* @param {Boolean=} options.allowAbsurdFees - Enable large fees
2016-04-08 08:58:59 -07:00
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
Bitcoin.prototype.sendTransaction = function(tx, options, callback) {
var self = this;
var allowAbsurdFees = false;
var txString;
if (tx instanceof Transaction) {
txString = tx.serialize();
} else {
txString = tx;
}
if (_.isFunction(options) && _.isUndefined(callback)) {
callback = options;
} else if (_.isObject(options)) {
allowAbsurdFees = options.allowAbsurdFees;
2016-04-07 15:59:28 -07:00
}
2016-04-07 15:59:28 -07:00
this.client.sendRawTransaction(txString, allowAbsurdFees, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
callback(null, response.result);
});
2015-08-31 06:00:00 -07:00
};
/**
* Will get a transaction as a Node.js Buffer. Results include the mempool.
* @param {String} txid - The transaction hash
* @param {Function} callback
*/
Bitcoin.prototype.getRawTransaction = function(txid, callback) {
var self = this;
var tx = self.rawTransactionCache.get(txid);
if (tx) {
return setImmediate(function() {
callback(null, tx);
});
} else {
self._tryAll(function(done) {
self.client.getRawTransaction(txid, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var buffer = new Buffer(response.result, 'hex');
self.rawTransactionCache.set(txid, buffer);
done(null, buffer);
});
}, callback);
}
};
2015-09-22 08:38:14 -07:00
/**
2016-04-08 08:58:59 -07:00
* Will get a transaction as a Bitcore Transaction. Results include the mempool.
2015-09-22 08:38:14 -07:00
* @param {String} txid - The transaction hash
* @param {Boolean} queryMempool - Include the mempool
* @param {Function} callback
*/
Bitcoin.prototype.getTransaction = function(txid, callback) {
2016-03-25 11:17:22 -07:00
var self = this;
var tx = self.transactionCache.get(txid);
if (tx) {
return setImmediate(function() {
callback(null, tx);
});
} else {
self._tryAll(function(done) {
self.client.getRawTransaction(txid, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var tx = Transaction();
tx.fromString(response.result);
self.transactionCache.set(txid, tx);
done(null, tx);
});
}, callback);
2016-03-25 11:17:22 -07:00
}
2015-08-31 06:00:00 -07:00
};
2015-09-22 08:38:14 -07:00
/**
2016-04-08 08:58:59 -07:00
* Will get a transaction as Bitcore Transaction with additional fields:
2015-09-22 08:38:14 -07:00
* {
2016-04-08 08:58:59 -07:00
* __blockHash: '2725743288feae6bdaa976590af7cb12d7b535b5a242787de6d2789c73682ed1',
* __height: 48,
* __timestamp: 1442951110, // in seconds
2015-09-22 08:38:14 -07:00
* }
* @param {String} txid - The transaction hash
* @param {Function} callback
*/
Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, callback) {
// TODO give response back as standard js object with bitcore tx
2016-03-25 11:17:22 -07:00
var self = this;
var tx = self.transactionInfoCache.get(txid);
if (tx) {
return setImmediate(function() {
callback(null, tx);
});
} else {
self._tryAll(function(done) {
self.client.getRawTransaction(txid, 1, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var tx = Transaction();
tx.fromString(response.result.hex);
tx.__blockHash = response.result.blockhash;
tx.__height = response.result.height ? response.result.height : -1;
tx.__timestamp = response.result.time;
2016-04-12 12:02:37 -07:00
for (var i = 0; i < response.result.vout.length; i++) {
tx.outputs[i].__spentTxId = response.result.vout[i].spentTxId;
tx.outputs[i].__spentIndex = response.result.vout[i].spentIndex;
2016-04-12 12:30:17 -07:00
tx.outputs[i].__spentHeight = response.result.vout[i].spentHeight;
}
2016-04-12 12:02:37 -07:00
self.transactionInfoCache.set(txid, tx);
done(null, tx);
});
}, callback);
2016-03-25 11:17:22 -07:00
}
2015-08-31 06:00:00 -07:00
};
2015-09-22 08:38:14 -07:00
/**
* Will get the best block hash for the chain.
2016-04-08 08:58:59 -07:00
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
Bitcoin.prototype.getBestBlockHash = function(callback) {
var self = this;
this.client.getBestBlockHash(function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
callback(null, response.result);
});
};
2016-04-12 12:02:37 -07:00
/**
* Will give the txid and inputIndex that spent an output
* @param {Function} callback
*/
Bitcoin.prototype.getSpentInfo = function(options, callback) {
var self = this;
this.client.getSpentInfo(options, function(err, response) {
if (err && err.code === -5) {
return callback(null, {});
} else if (err) {
return callback(self._wrapRPCError(err));
}
callback(null, response.result);
});
};
2015-09-22 08:38:14 -07:00
/**
* This will return information about the database in the format:
* {
* version: 110000,
* protocolversion: 70002,
* blocks: 151,
* timeoffset: 0,
* connections: 0,
* difficulty: 4.6565423739069247e-10,
* testnet: false,
2016-04-08 08:58:59 -07:00
* network: 'testnet'
2015-09-22 08:38:14 -07:00
* relayfee: 1000,
* errors: ''
* }
2016-04-08 08:58:59 -07:00
* @param {Function} callback
2015-09-22 08:38:14 -07:00
*/
Bitcoin.prototype.getInfo = function(callback) {
2016-04-07 15:59:28 -07:00
var self = this;
this.client.getInfo(function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
2016-04-07 15:59:28 -07:00
var result = response.result;
2016-04-08 13:00:33 -07:00
result.network = self.node.getNetworkName();
2016-04-07 15:59:28 -07:00
callback(null, result);
});
2015-08-31 06:00:00 -07:00
};
Bitcoin.prototype.generateBlock = function(num, callback) {
var self = this;
this.client.generate(num, function(err, response) {
if (err) {
return callback(self._wrapRPCError(err));
}
callback(null, response.result);
});
};
2015-09-22 08:38:14 -07:00
/**
* Called by Node to stop the service.
* @param {Function} callback
*/
2015-08-31 06:00:00 -07:00
Bitcoin.prototype.stop = function(callback) {
if (this.spawn && this.spawn.process) {
this.spawn.process.once('exit', function(code) {
if (code !== 0) {
var error = new Error('bitcoind spawned process exited with status code: ' + code);
error.code = code;
return callback(error);
} else {
return callback();
}
});
2016-04-07 11:46:19 -07:00
this.spawn.process.kill('SIGINT');
} else {
callback();
}
2015-08-31 06:00:00 -07:00
};
module.exports = Bitcoin;