'use strict'; var fs = require('fs'); var path = require('path'); var spawn = require('child_process').spawn; var util = require('util'); var mkdirp = require('mkdirp'); var bitcore = require('bitcore-lib'); var zmq = require('zmq'); var async = require('async'); var LRU = require('lru-cache'); var BitcoinRPC = require('bitcoind-rpc'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; var index = require('../'); var errors = index.errors; var log = index.log; var utils = require('../utils'); var Service = require('../service'); var Transaction = require('../transaction'); /** * 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. * * @param {Object} options * @param {Node} options.node - A reference to the node */ function Bitcoin(options) { if (!(this instanceof Bitcoin)) { return new Bitcoin(options); } Service.call(this, options); this.options = options; this._initCaches(); // bitcoind child process this.spawn = false; // event subscribers this.subscriptions = {}; this.subscriptions.rawtransaction = []; this.subscriptions.hashblock = []; // 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; // 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_REINDEX_INTERVAL = 10000; Bitcoin.DEFAULT_START_RETRY_INTERVAL = 5000; Bitcoin.DEFAULT_TIP_UPDATE_INTERVAL = 15000; 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', uacomment: 'bitcore' }; Bitcoin.prototype._initCaches = function() { // caches valid until there is a new block this.utxosCache = LRU(50000); this.txidsCache = LRU(50000); this.balanceCache = LRU(50000); this.summaryCache = LRU(50000); this.transactionInfoCache = LRU(100000); // caches valid indefinitely this.transactionCache = LRU(100000); this.rawTransactionCache = LRU(50000); this.blockCache = LRU(144); this.rawBlockCache = LRU(72); this.blockHeaderCache = LRU(288); this.zmqKnownTransactions = LRU(50); this.zmqKnownBlocks = LRU(50); this.lastTip = 0; this.lastTipTimeout = false; }; 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 }); }; /** * 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], ['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; }; /** * Called by the Bus to determine the available events. */ 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); }; 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) { /* 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'; 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); } if (!fs.existsSync(configPath)) { var defaultConfig = this._getDefaultConfig(); fs.writeFileSync(configPath, defaultConfig); } 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; } } var spawnConfig = this.spawn.config; this._checkConfigIndexes(spawnConfig, node); }; Bitcoin.prototype._checkConfigIndexes = function(spawnConfig, node) { $.checkState( spawnConfig.txindex && spawnConfig.txindex === 1, '"txindex" option is required in order to use transaction query features of bitcore-node. ' + 'Please add "txindex=1" to your configuration and reindex an existing database if ' + '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:" 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:" 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; } }; Bitcoin.prototype._resetCaches = function() { this.transactionInfoCache.reset(); this.utxosCache.reset(); 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) { var self = this; self.client.getBestBlockHash(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } 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._getNetworkOption = function() { var networkOption; if (this.node.network === bitcore.Networks.testnet) { networkOption = '--testnet'; if (this.node.network.regtestEnabled) { networkOption = '--regtest'; } } return networkOption; }; Bitcoin.prototype._zmqBlockHandler = function(node, message) { var self = this; // Update the current chain tip self._rapidProtectedUpdateTip(node, message); // 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')); } } }; Bitcoin.prototype._rapidProtectedUpdateTip = function(node, message) { var self = this; // 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); } }; Bitcoin.prototype._updateTip = function(node, message) { var self = this; var hex = message.toString('hex'); if (hex !== self.tiphash) { self.tiphash = message.toString('hex'); // reset block valid caches self._resetCaches(); 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)); } }); } } }; 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); // Notify transaction subscribers 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) { // update tip node.client.getBestBlockHash(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var blockhash = new Buffer(response.result, 'hex'); self._updateTip(node, blockhash); // check if synced node.client.getBlockchainInfo(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var percentSynced = response.result.verificationprogress * 100; if (Math.round(percentSynced) >= 99) { // subscribe to events for further updates 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() { checkAndSubscribe(function(err) { if (err) { log.error(err); } }); }, node._tipUpdateInterval || Bitcoin.DEFAULT_TIP_UPDATE_INTERVAL).unref(); } }); }; 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) { var self = this; var interval; function finish(err) { clearInterval(interval); callback(err); } if (node._reindex) { interval = setInterval(function() { node.client.getBlockchainInfo(function(err, response) { if (err) { return finish(self._wrapRPCError(err)); } var percentSynced = response.result.verificationprogress * 100; log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2)); if (Math.round(percentSynced) >= 100) { node._reindex = false; finish(); } }); }, node._reindexWait || Bitcoin.DEFAULT_REINDEX_INTERVAL); } 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); } var options = [ '--conf=' + this.spawn.configPath, '--datadir=' + this.spawn.datadir, ]; if (self._getNetworkOption()) { options.push(self._getNetworkOption()); } self.spawn.process = spawn(this.spawn.exec, options, {stdio: 'inherit'}); 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.')); } 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) { if (err) { return callback(err); } self._checkSyncedAndSubscribeZmqEvents(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._checkSyncedAndSubscribeZmqEvents(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); } if (self.nodes.length === 0) { return callback(new Error('Bitcoin configuration options "spawn" or "connect" are expected')); } self._initChain(callback); }); }; /** * Helper to determine the state of the database. * @param {Function} callback */ 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); } }); }; /** * Helper to determine the progress of the database. * @param {Function} callback */ 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); }); }; Bitcoin.prototype._normalizeAddressArg = function(addressArg) { var addresses = [addressArg]; if (Array.isArray(addressArg)) { addresses = addressArg; } return addresses; }; /** * 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) { var self = this; var addresses = self._normalizeAddressArg(addressArg); 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)); } self.balanceCache.set(cacheKey, response.result); callback(null, response.result); }); } }; /** * 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 */ Bitcoin.prototype.getAddressUnspentOutputs = function(addressArg, options, callback) { var self = this; var addresses = self._normalizeAddressArg(addressArg); 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)); } 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) { 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; }; /** * 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) { 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; } var addresses = self._normalizeAddressArg(addressArg); var cacheKey = addresses.join(''); var mempoolTxids = []; 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) { if (err) { return callback(self._wrapRPCError(err)); } mempoolTxids = self._getTxidsFromMempool(response.result); finish(); }); } else { finish(); } }; 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; }; 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"'); txids = fullTxids.slice(from, to); } else { txids = fullTxids; } return txids; }; /** * 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; 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); } var totalCount = txids.length; try { txids = self._paginateTxids(txids, options.from, options.to); } catch(e) { return callback(e); } async.mapSeries( txids, function(txid, next) { self._getDetailedTransaction(txid, { queryMempool: queryMempool, addressStrings: addressStrings }, next); }, function(err, transactions) { if (err) { return callback(err); } callback(null, { totalCount: totalCount, items: transactions }); } ); }); }; /** * 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; var summaryTxids = []; var mempoolTxids = []; var addresses = self._normalizeAddressArg(addressArg); var cacheKey = addresses.join(''); function querySummary() { async.parallel([ function getTxList(done) { self.getAddressTxids(addresses, {queryMempool: false}, function(err, txids) { if (err) { return done(err); } summaryTxids = txids; summary.appearances = txids.length; done(); }); }, function getBalance(done) { self.getAddressBalance(addresses, options, function(err, data) { 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(); } 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(); }); }, ], function(err) { if (err) { return callback(err); } self.summaryCache.set(cacheKey, summary); if (!options.noTxList) { var allTxids = mempoolTxids.reverse().concat(summaryTxids); summary.txids = allTxids; } callback(null, summary); }); } if (options.noTxList) { var summaryCache = self.summaryCache.get(cacheKey); if (summaryCache) { callback(null, summaryCache); } else { querySummary(); } } else { querySummary(); } }; /** * 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); } } }; /** * Will retrieve a block as a Bitcore object * @param {String|Number} block - A block hash or block height number * @param {Function} callback */ Bitcoin.prototype.getBlock = function(blockArg, callback) { // TODO apply performance patch to the RPC method for raw data var self = this; function queryBlock(blockhash) { var cachedBlock = self.blockCache.get(blockhash); if (cachedBlock) { return setImmediate(function() { callback(null, cachedBlock); }); } else { self._tryAll(function(done) { self.client.getBlock(blockhash, false, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } var blockObj = bitcore.Block.fromString(response.result); self.blockCache.set(blockhash, blockObj); done(null, blockObj); }); }, callback); } } if (_.isNumber(blockArg)) { self._tryAll(function(done) { self.client.getBlockHash(blockArg, function(err, response) { if (err) { return done(self._wrapRPCError(err)); } done(null, response.result); }); }, function(err, blockhash) { if (err) { return callback(err); } queryBlock(blockhash); }); } else { queryBlock(blockArg); } }; /** * 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); }); }; /** * Will return the block index information, the output will have the format: * { * prevHash: '000000004956cc2edd1a8caa05eacfa3c69f4c490bfc9ace820257834115ab35', * nextHash: '0000000000629d100db387f37d0f37c51118f250fb0946310a8c37316cbc4028' * hash: ' 00000000009e2958c15ff9290d571bf9459e93b19765c6801ddeccadbb160a1e', * chainWork: '0000000000000000000000000000000000000000000000000000000000000016', * height: 10 * } * @param {String|Number} block - A block hash or block height * @param {Function} callback */ Bitcoin.prototype.getBlockHeader = function(block, callback) { var self = this; 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); } }; /** * Will estimate the fee per kilobyte. * @param {Number} blocks - The number of blocks for the transaction to be confirmed. * @param {Function} callback */ 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); }); }; /** * 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 * @param {Function} callback */ 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; } this.client.sendRawTransaction(txString, allowAbsurdFees, function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } callback(null, response.result); }); }; /** * 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); } }; /** * Will get a transaction as a Bitcore Transaction. Results include the mempool. * @param {String} txid - The transaction hash * @param {Boolean} queryMempool - Include the mempool * @param {Function} callback */ Bitcoin.prototype.getTransaction = function(txid, callback) { 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); } }; /** * Will get a transaction as Bitcore Transaction with additional fields: * { * __blockHash: '2725743288feae6bdaa976590af7cb12d7b535b5a242787de6d2789c73682ed1', * __height: 48, * __timestamp: 1442951110, // in seconds * } * @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 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; 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; tx.outputs[i].__spentHeight = response.result.vout[i].spentHeight; } self.transactionInfoCache.set(txid, tx); done(null, tx); }); }, callback); } }; /** * Will get the best block hash for the chain. * @param {Function} callback */ 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); }); }; /** * 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); }); }; /** * 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, * network: 'testnet' * relayfee: 1000, * errors: '' * } * @param {Function} callback */ Bitcoin.prototype.getInfo = function(callback) { var self = this; this.client.getInfo(function(err, response) { if (err) { return callback(self._wrapRPCError(err)); } var result = response.result; result.network = self.node.getNetworkName(); callback(null, result); }); }; 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); }); }; /** * Called by Node to stop the service. * @param {Function} callback */ 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(); } }); this.spawn.process.kill('SIGINT'); } else { callback(); } }; module.exports = Bitcoin;