'use strict'; var BaseService = require('../../service'); var inherits = require('util').inherits; var async = require('async'); var index = require('../../'); var log = index.log; var errors = index.errors; var bitcore = require('bitcore-lib'); var levelup = require('levelup'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; var EventEmitter = require('events').EventEmitter; var Address = bitcore.Address; var constants = require('../../constants'); var Encoding = require('./encoding'); var utils = require('../../utils'); /** * The Address Service builds upon the Database Service and the Bitcoin Service to add additional * functionality for getting information by base58check encoded addresses. This includes getting the * balance for an address, the history for a collection of addresses, and unspent outputs for * constructing transactions. This is typically the core functionality for building a wallet. * @param {Object} options * @param {Node} options.node - An instance of the node * @param {String} options.name - An optional name of the service */ var AddressService = function(options) { BaseService.call(this, options); // this.subscriptions = {}; // this.subscriptions['address/transaction'] = {}; // this.subscriptions['address/balance'] = {}; // this._bitcoindTransactionListener = this.transactionHandler.bind(this); // this._bitcoindTransactionLeaveListener = this.transactionLeaveHandler.bind(this); // this.node.services.bitcoind.on('tx', this._bitcoindTransactionListener); // this.node.services.bitcoind.on('txleave', this._bitcoindTransactionLeaveListener); this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH; this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH; this.concurrency = options.concurrency || 20; // this.mempoolIndex = null; // Used for larger mempool indexes // this.mempoolSpentIndex = {}; // Used for small quick synchronous lookups // this.mempoolAddressIndex = {}; // Used to check if an address is on the spend pool }; inherits(AddressService, BaseService); AddressService.dependencies = [ 'bitcoind', 'db', 'transaction' ]; AddressService.prototype.start = function(callback) { var self = this; this.store = this.node.services.db.store; this.node.services.db.getPrefix(this.name, function(err, prefix) { if(err) { return callback(err); } self.prefix = prefix; self._encoding = new Encoding(self.prefix); callback(); }); }; AddressService.prototype.stop = function(callback) { // TODO Keep track of ongoing db requests before shutting down // this.node.services.bitcoind.removeListener('tx', this._bitcoindTransactionListener); // this.node.services.bitcoind.removeListener('txleave', this._bitcoindTransactionLeaveListener); // this.mempoolIndex.close(callback); setImmediate(callback); }; /** * Called by the Node to get the available API methods for this service, * that can be exposed over the JSON-RPC interface. */ AddressService.prototype.getAPIMethods = function() { return [ ['getBalance', this, this.getBalance, 2], ['getOutputs', this, this.getOutputs, 2], ['getUtxos', this, this.getUtxos, 2], ['getInputForOutput', this, this.getInputForOutput, 2], ['isSpent', this, this.isSpent, 2], ['getAddressHistory', this, this.getAddressHistory, 2], ['getAddressSummary', this, this.getAddressSummary, 1] ]; }; /** * Called by the Bus to get the available events for this service. */ AddressService.prototype.getPublishEvents = function() { return []; // return [ // { // name: 'address/transaction', // scope: this, // subscribe: this.subscribe.bind(this, 'address/transaction'), // unsubscribe: this.unsubscribe.bind(this, 'address/transaction') // }, // { // name: 'address/balance', // scope: this, // subscribe: this.subscribe.bind(this, 'address/balance'), // unsubscribe: this.unsubscribe.bind(this, 'address/balance') // } // ]; }; /** * The Database Service will run this function when blocks are connected and * disconnected to the chain during syncing and reorganizations. * @param {Block} block - An instance of a Bitcore Block * @param {Boolean} addOutput - If the block is being removed or added to the chain * @param {Function} callback */ AddressService.prototype.concurrentBlockHandler = function(block, connectBlock, callback) { var self = this; var txs = block.transactions; var height = block.__height; var action = 'put'; var reverseAction = 'del'; if (!connectBlock) { action = 'del'; reverseAction = 'put'; } var operations = []; for(var i = 0; i < txs.length; i++) { var tx = txs[i]; var txid = tx.id; var inputs = tx.inputs; var outputs = tx.outputs; // Subscription messages var txmessages = {}; var outputLength = outputs.length; for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) { var output = outputs[outputIndex]; var script = output.script; if(!script) { log.debug('Invalid script'); continue; } var address = self.getAddressString(script); if(!address) { continue; } var key = self._encoding.encodeAddressIndexKey(address, height, txid); operations.push({ type: action, key: key }); // Collect data for subscribers if (txmessages[address]) { txmessages[address].outputIndexes.push(outputIndex); } else { txmessages[address] = { tx: tx, height: height, outputIndexes: [outputIndex], address: address, timestamp: block.header.timestamp }; } } if(tx.isCoinbase()) { continue; } //TODO deal with P2PK for(var inputIndex = 0; inputIndex < inputs.length; inputIndex++) { var input = inputs[inputIndex]; if(!input.script) { log.debug('Invalid script'); continue; } var inputAddress = self.getAddressString(input.script); if(!inputAddress) { continue; } var inputKey = self._encoding.encodeAddressIndexKey(inputAddress, height, txid); operations.push({ type: action, key: inputKey }); } } setImmediate(function() { callback(null, operations); }); }; AddressService.prototype.blockHandler = function(block, connectBlock, callback) { var self = this; var txs = block.transactions; var action = 'put'; var reverseAction = 'del'; if (!connectBlock) { action = 'del'; reverseAction = 'put'; } var operations = []; async.eachSeries(txs, function(tx, next) { var txid = tx.id; var inputs = tx.inputs; var outputs = tx.outputs; var outputLength = outputs.length; for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) { var output = outputs[outputIndex]; var script = output.script; if(!script) { log.debug('Invalid script'); continue; } var address = self.getAddressString(script); if(!address) { continue; } var key = self._encoding.encodeUtxoIndexKey(address, txid, outputIndex); var value = self._encoding.encodeUtxoIndexValue(block.__height, output.satoshis, output._scriptBuffer); operations.push({ type: action, key: key, value: value }); } if(tx.isCoinbase()) { return next(); } //TODO deal with P2PK async.each(inputs, function(input, next) { if(!input.script) { log.debug('Invalid script'); return next(); } var inputAddress = self.getAddressString(input.script); if(!inputAddress) { return next(); } var inputKey = self._encoding.encodeUtxoIndexKey(inputAddress, input.prevTxId, input.outputIndex); //common case is connecting blocks and deleting outputs spent by these inputs if (connectBlock) { operations.push({ type: 'del', key: inputKey }); next(); } else { // uncommon and slower, this happens during a reorg self.node.services.transaction.getTransaction(input.prevTxId, {}, function(err, tx) { var utxo = tx.outputs[input.outputIndex]; var inputValue = self._encoding.encodeUtxoIndexValue(tx.__height, utxo.satoshis, utxo._scriptBuffer); operations.push({ type: 'put', key: inputKey, value: inputValue }); next(); }); } }, function(err) { if(err) { return next(err); } next(); }); }, function(err) { //we are aync predicated on reorg sitch if(err) { return callback(err); } callback(null, operations); }); }; AddressService.prototype.getAddressString = function(script, output) { var address = script.toAddress(); if(address) { return address.toString(); } try { var pubkey = script.getPublicKey(); if(pubkey) { return pubkey.toString('hex'); } } catch(e) { } //TODO add back in P2PK, but for this we need to look up the utxo for this script if(output && output.script && output.script.isPublicKeyOut()) { return output.script.getPublicKey().toString('hex'); } return null; }; /** * This function is responsible for emitting events to any subscribers to the * `address/transaction` event. * @param {Object} obj * @param {Transaction} obj.tx - The transaction * @param {Object} obj.addressInfo * @param {String} obj.addressInfo.hashHex - The hex string of address hash for the subscription * @param {String} obj.addressInfo.hashBuffer - The address hash buffer * @param {String} obj.addressInfo.addressType - The address type * @param {Array} obj.outputIndexes - Indexes of the inputs that includes the address * @param {Array} obj.inputIndexes - Indexes of the outputs that includes the address * @param {Date} obj.timestamp - The time of the block the transaction was included * @param {Number} obj.height - The height of the block the transaction was included * @param {Boolean} obj.rejected - If the transaction was not accepted in the mempool */ AddressService.prototype.transactionEventHandler = function(obj) { if(this.subscriptions['address/transaction'][obj.addressInfo.hashHex]) { var emitters = this.subscriptions['address/transaction'][obj.addressInfo.hashHex]; var address = new Address({ hashBuffer: obj.addressInfo.hashBuffer, network: this.node.network, type: obj.addressInfo.addressType }); for(var i = 0; i < emitters.length; i++) { emitters[i].emit('address/transaction', { rejected: obj.rejected, height: obj.height, timestamp: obj.timestamp, inputIndexes: obj.inputIndexes, outputIndexes: obj.outputIndexes, address: address, tx: obj.tx }); } } }; /** * The function is responsible for emitting events to any subscribers for the * `address/balance` event. * @param {Block} block * @param {Object} obj * @param {String} obj.hashHex * @param {Buffer} obj.hashBuffer * @param {String} obj.addressType */ AddressService.prototype.balanceEventHandler = function(block, obj) { if(this.subscriptions['address/balance'][obj.hashHex]) { var emitters = this.subscriptions['address/balance'][obj.hashHex]; var address = new Address({ hashBuffer: obj.hashBuffer, network: this.node.network, type: obj.addressType }); this.getBalance(address, true, function(err, balance) { if(err) { return this.emit(err); } for(var i = 0; i < emitters.length; i++) { emitters[i].emit('address/balance', address, balance, block); } }); } }; /** * The Bus will use this function to subscribe to the available * events for this service. For information about the available events * please see `getPublishEvents`. * @param {String} name - The name of the event * @param {EventEmitter} emitter - An event emitter instance * @param {Array} addresses - An array of addresses to subscribe */ AddressService.prototype.subscribe = function(name, emitter, addresses) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); $.checkArgument(Array.isArray(addresses), 'Second argument is expected to be an Array of addresses'); for(var i = 0; i < addresses.length; i++) { var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex'); if(!this.subscriptions[name][hashHex]) { this.subscriptions[name][hashHex] = []; } this.subscriptions[name][hashHex].push(emitter); } }; /** * The Bus will use this function to unsubscribe to the available * events for this service. * @param {String} name - The name of the event * @param {EventEmitter} emitter - An event emitter instance * @param {Array} addresses - An array of addresses to subscribe */ AddressService.prototype.unsubscribe = function(name, emitter, addresses) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); $.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined'); if(!addresses) { return this.unsubscribeAll(name, emitter); } for(var i = 0; i < addresses.length; i++) { var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex'); if(this.subscriptions[name][hashHex]) { var emitters = this.subscriptions[name][hashHex]; var index = emitters.indexOf(emitter); if(index > -1) { emitters.splice(index, 1); } } } }; /** * A helper function for the `unsubscribe` method to unsubscribe from all addresses. * @param {String} name - The name of the event * @param {EventEmitter} emitter - An instance of an event emitter */ AddressService.prototype.unsubscribeAll = function(name, emitter) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); for(var hashHex in this.subscriptions[name]) { var emitters = this.subscriptions[name][hashHex]; var index = emitters.indexOf(emitter); if(index > -1) { emitters.splice(index, 1); } } }; /** * Will sum the total of all unspent outputs to calculate the balance * for an address. * @param {String} address - The base58check encoded address * @param {Boolean} queryMempool - Include mempool in the results * @param {Function} callback */ AddressService.prototype.getBalance = function(address, queryMempool, callback) { this.getUtxos(address, queryMempool, function(err, outputs) { if(err) { return callback(err); } var satoshis = outputs.map(function(output) { return output.satoshis; }); var sum = satoshis.reduce(function(a, b) { return a + b; }, 0); return callback(null, sum); }); }; /** * Will give the input that spends an output if it exists with: * inputTxId - The input txid hex string * inputIndex - A number with the spending input index * @param {String|Buffer} txid - The transaction hash with the output * @param {Number} outputIndex - The output index in the transaction * @param {Object} options * @param {Object} options.queryMempool - Include mempool in results * @param {Function} callback */ AddressService.prototype.getInputForOutput = function(txid, outputIndex, options, callback) { $.checkArgument(_.isNumber(outputIndex)); $.checkArgument(_.isObject(options)); $.checkArgument(_.isFunction(callback)); var self = this; var txidBuffer; if (Buffer.isBuffer(txid)) { txidBuffer = txid; } else { txidBuffer = new Buffer(txid, 'hex'); } if (options.queryMempool) { var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey(txidBuffer, outputIndex); if (this.mempoolSpentIndex[spentIndexSyncKey]) { return this._getSpentMempool(txidBuffer, outputIndex, callback); } } var key = self._encoding.encodeInputKeyMap(txidBuffer, outputIndex); var dbOptions = { valueEncoding: 'binary', keyEncoding: 'binary' }; this.node.services.db.store.get(key, dbOptions, function(err, buffer) { if (err instanceof levelup.errors.NotFoundError) { return callback(null, false); } else if (err) { return callback(err); } var value = self._encoding.decodeInputValueMap(buffer); callback(null, { inputTxId: value.inputTxId.toString('hex'), inputIndex: value.inputIndex }); }); }; /** * A streaming equivalent to `getInputs`, and returns a transform stream with data * emitted in the same format as `getInputs`. * * @param {String} addressStr - The relevant address * @param {Object} options - Additional options for query the outputs * @param {Number} [options.start] - The relevant start block height * @param {Number} [options.end] - The relevant end block height * @param {Function} callback */ AddressService.prototype.createInputsStream = function(addressStr, options) { var inputStream = new InputsTransformStream({ address: new Address(addressStr, this.node.network), tipHeight: this.node.services.db.tip.__height }); var stream = this.createInputsDBStream(addressStr, options) .on('error', function(err) { // Forward the error inputStream.emit('error', err); inputStream.end(); }).pipe(inputStream); return stream; }; AddressService.prototype.createInputsDBStream = function(addressStr, options) { var stream; var addrObj = this.encoding.getAddressInfo(addressStr); var hashBuffer = addrObj.hashBuffer; var hashTypeBuffer = addrObj.hashTypeBuffer; if (options.start >= 0 && options.end >= 0) { var endBuffer = new Buffer(4); endBuffer.writeUInt32BE(options.end, 0); var startBuffer = new Buffer(4); // Because the key has additional data following it, we don't have an ability // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number // to be one value larger to include it. var adjustedStart = options.start + 1; startBuffer.writeUInt32BE(adjustedStart, 0); stream = this.node.services.db.store.createReadStream({ gt: Buffer.concat([ constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, constants.SPACER_MIN, endBuffer ]), lt: Buffer.concat([ constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, constants.SPACER_MIN, startBuffer ]), valueEncoding: 'binary', keyEncoding: 'binary' }); } else { var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]); stream = this.node.services.db.store.createReadStream({ gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]), lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]), valueEncoding: 'binary', keyEncoding: 'binary' }); } return stream; }; /** * Will give inputs that spend previous outputs for an address as an object with: * address - The base58check encoded address * hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash' * txid - A string of the transaction hash * outputIndex - A number of corresponding transaction input * height - The height of the block the transaction was included, will be -1 for mempool transactions * confirmations - The number of confirmations, will equal 0 for mempool transactions * * @param {String} addressStr - The relevant address * @param {Object} options - Additional options for query the outputs * @param {Number} [options.start] - The relevant start block height * @param {Number} [options.end] - The relevant end block height * @param {Boolean} [options.queryMempool] - Include the mempool in the results * @param {Function} callback */ AddressService.prototype.getInputs = function(addressStr, options, callback) { var self = this; var inputs = []; var addrObj = self._encoding.getAddressInfo(addressStr); var hashBuffer = addrObj.hashBuffer; var hashTypeBuffer = addrObj.hashTypeBuffer; var stream = this.createInputsStream(addressStr, options); var error; stream.on('data', function(input) { inputs.push(input); if (inputs.length > self.maxInputsQueryLength) { log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for address '+ addressStr); error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached'); stream.end(); } }); stream.on('error', function(streamError) { if (streamError) { error = streamError; } }); stream.on('finish', function() { if (error) { return callback(error); } if(options.queryMempool) { self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { if (err) { return callback(err); } inputs = inputs.concat(mempoolInputs); callback(null, inputs); }); } else { callback(null, inputs); } }); return stream; }; AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { var self = this; var mempoolInputs = []; var stream = self.mempoolIndex.createReadStream({ gte: Buffer.concat([ constants.MEMPREFIXES.SPENTS, hashBuffer, hashTypeBuffer, constants.SPACER_MIN ]), lte: Buffer.concat([ constants.MEMPREFIXES.SPENTS, hashBuffer, hashTypeBuffer, constants.SPACER_MAX ]), valueEncoding: 'binary', keyEncoding: 'binary' }); stream.on('data', function(data) { var txid = data.value.slice(0, 32); var inputIndex = data.value.readUInt32BE(32); var timestamp = data.value.readDoubleBE(36); var input = { address: addressStr, hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], txid: txid.toString('hex'), //TODO use a buffer inputIndex: inputIndex, timestamp: timestamp, height: -1, confirmations: 0 }; mempoolInputs.push(input); }); var error; stream.on('error', function(streamError) { if (streamError) { error = streamError; } }); stream.on('close', function() { if (error) { return callback(error); } callback(null, mempoolInputs); }); }; AddressService.prototype._getSpentMempool = function(txidBuffer, outputIndex, callback) { var outputIndexBuffer = new Buffer(4); outputIndexBuffer.writeUInt32BE(outputIndex); var spentIndexKey = Buffer.concat([ constants.MEMPREFIXES.SPENTSMAP, txidBuffer, outputIndexBuffer ]); this.mempoolIndex.get( spentIndexKey, function(err, mempoolValue) { if (err) { return callback(err); } var inputTxId = mempoolValue.slice(0, 32); var inputIndex = mempoolValue.readUInt32BE(32); callback(null, { inputTxId: inputTxId.toString('hex'), inputIndex: inputIndex }); } ); }; AddressService.prototype.createOutputsStream = function(addressStr, options) { var outputStream = new OutputsTransformStream({ address: new Address(addressStr, this.node.network), tipHeight: this.node.services.db.tip.__height }); var stream = this.createOutputsDBStream(addressStr, options) .on('error', function(err) { // Forward the error outputStream.emit('error', err); outputStream.end(); }) .pipe(outputStream); return stream; }; AddressService.prototype.createOutputsDBStream = function(addressStr, options) { var addrObj = this.encoding.getAddressInfo(addressStr); var hashBuffer = addrObj.hashBuffer; var hashTypeBuffer = addrObj.hashTypeBuffer; var stream; if (options.start >= 0 && options.end >= 0) { var endBuffer = new Buffer(4); endBuffer.writeUInt32BE(options.end, 0); var startBuffer = new Buffer(4); // Because the key has additional data following it, we don't have an ability // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number // to be one value larger to include it. var startAdjusted = options.start + 1; startBuffer.writeUInt32BE(startAdjusted, 0); stream = this.node.services.db.store.createReadStream({ gt: Buffer.concat([ constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, constants.SPACER_MIN, endBuffer ]), lt: Buffer.concat([ constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, constants.SPACER_MIN, startBuffer ]), valueEncoding: 'binary', keyEncoding: 'binary' }); } else { var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]); stream = this.node.services.db.store.createReadStream({ gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]), lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]), valueEncoding: 'binary', keyEncoding: 'binary' }); } return stream; }; /** * Will give outputs for an address as an object with: * address - The base58check encoded address * hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash' * txid - A string of the transaction hash * outputIndex - A number of corresponding transaction output * height - The height of the block the transaction was included, will be -1 for mempool transactions * satoshis - The satoshis value of the output * script - The script of the output as a hex string * confirmations - The number of confirmations, will equal 0 for mempool transactions * * @param {String} addressStr - The relevant address * @param {Object} options - Additional options for query the outputs * @param {Number} [options.start] - The relevant start block height * @param {Number} [options.end] - The relevant end block height * @param {Boolean} [options.queryMempool] - Include the mempool in the results * @param {Function} callback */ AddressService.prototype.getOutputs = function(addressStr, options, callback) { var self = this; $.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.'); $.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.'); var addrObj = self._encoding.getAddressInfo(addressStr); var hashBuffer = addrObj.hashBuffer; var hashTypeBuffer = addrObj.hashTypeBuffer; if (!hashTypeBuffer) { return callback(new Error('Unknown address type: ' + addrObj.hashTypeReadable + ' for address: ' + addressStr)); } var outputs = []; var stream = this.createOutputsStream(addressStr, options); var error; stream.on('data', function(data) { outputs.push(data); if (outputs.length > self.maxOutputsQueryLength) { log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for address ' + addressStr); error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached'); stream.end(); } }); stream.on('error', function(streamError) { if (streamError) { error = streamError; } }); stream.on('finish', function() { if (error) { return callback(error); } if(options.queryMempool) { self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { if (err) { return callback(err); } outputs = outputs.concat(mempoolOutputs); callback(null, outputs); }); } else { callback(null, outputs); } }); return stream; }; AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { var self = this; var mempoolOutputs = []; var stream = self.mempoolIndex.createReadStream({ gte: Buffer.concat([ constants.MEMPREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, constants.SPACER_MIN ]), lte: Buffer.concat([ constants.MEMPREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, constants.SPACER_MAX ]), valueEncoding: 'binary', keyEncoding: 'binary' }); stream.on('data', function(data) { // Format of data: // prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, txid: 32, outputIndex: 4 var txid = data.key.slice(22, 54); var outputIndex = data.key.readUInt32BE(54); var value = self._encoding.decodeOutputMempoolValue(data.value); var output = { address: addressStr, hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], txid: txid.toString('hex'), //TODO use a buffer outputIndex: outputIndex, height: -1, timestamp: value.timestamp, satoshis: value.satoshis, script: value.scriptBuffer.toString('hex'), //TODO use a buffer confirmations: 0 }; mempoolOutputs.push(output); }); var error; stream.on('error', function(streamError) { if (streamError) { error = streamError; } }); stream.on('close', function() { if (error) { return callback(error); } callback(null, mempoolOutputs); }); }; /** * Will give unspent outputs for an address or an array of addresses. * @param {Array|String} addresses - An array of addresses * @param {Boolean} queryMempool - Include or exclude the mempool * @param {Function} callback */ AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) { var self = this; if(!Array.isArray(addresses)) { addresses = [addresses]; } var utxos = []; async.eachSeries(addresses, function(address, next) { self.getUtxosForAddress(address, queryMempool, function(err, unspents) { if(err && err instanceof errors.NoOutputs) { return next(); } else if(err) { return next(err); } utxos = utxos.concat(unspents); next(); }); }, function(err) { callback(err, utxos); }); }; /** * Will give unspent outputs for an address. * @param {String} address - An address in base58check encoding * @param {Boolean} queryMempool - Include or exclude the mempool * @param {Function} callback */ AddressService.prototype.getUtxosForAddress = function(address, queryMempool, callback) { var self = this; var stream = self.store.createReadStream({ gte: self._encoding.encodeUtxoIndexKey(address), lt: self._encoding.encodeUtxoIndexKey(utils.getTerminalKey(new Buffer(address))) }); var utxos = []; stream.on('data', function(data) { var key = self._encoding.decodeUtxoIndexKey(data.key); var value = self._encoding.decodeUtxoIndexValue(data.value); utxos.push({ address: key.address, txid: key.txid, outputIndex: key.outputIndex, satoshis: value.satoshis, height: value.height, script: value.script }); }); stream.on('end', function() { return callback(null, utxos); }); stream.on('error', function(err) { if(err) { return callback(err); } }); }; /** * Will give the inverse of isSpent * @param {Object} output * @param {Object} options * @param {Boolean} options.queryMempool - Include mempool in results * @param {Function} callback */ AddressService.prototype.isUnspent = function(output, options, callback) { $.checkArgument(_.isFunction(callback)); this.isSpent(output, options, function(spent) { callback(!spent); }); }; /** * Will determine if an output is spent. * @param {Object} output - An output as returned from getOutputs * @param {Object} options * @param {Boolean} options.queryMempool - Include mempool in results * @param {Function} callback */ AddressService.prototype.isSpent = function(output, options, callback) { $.checkArgument(_.isFunction(callback)); var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; var self = this; var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; var spent = self.node.services.bitcoind.isSpent(txid, output.outputIndex); if (!spent && queryMempool) { var txidBuffer = new Buffer(txid, 'hex'); var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey(txidBuffer, output.outputIndex); spent = self.mempoolSpentIndex[spentIndexSyncKey] ? true : false; } setImmediate(function() { // TODO error should be the first argument? callback(spent); }); }; /** * This will give the history for many addresses limited by a range of block heights (to limit * the database lookup times) and/or paginated to limit the results length. * * The response format will be: * { * totalCount: 12 // the total number of items there are between the two heights * items: [ * { * addresses: { * '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX': { * inputIndexes: [], * outputIndexes: [0] * } * }, * satoshis: 100, * height: 300000, * confirmations: 1, * timestamp: 1442337090 // in seconds * fees: 1000 // in satoshis * tx: * } * ] * } * @param {Array} addresses - An array of addresses * @param {Object} options - The options to limit the query * @param {Number} [options.from] - The pagination "from" index * @param {Number} [options.to] - The pagination "to" index * @param {Number} [options.start] - The beginning block height (e.g. 1500 the most recent block height). * @param {Number} [options.end] - The ending block height (e.g. 0 the older block height, results are inclusive). * @param {Boolean} [options.queryMempool] - Include the mempool in the query * @param {Function} callback */ AddressService.prototype.getAddressHistory = function(addresses, options, callback) { var self = this; var txids = []; async.eachLimit(addresses, self.concurrency, function(address, next) { self.getAddressTxids(address, options, function(err, tmpTxids) { if(err) { return next(err); } txids = _.union(txids, tmpTxids); return next(); }); }, function() { async.mapLimit(txids, self.concurrency, function(txid, next) { self.node.services.transaction.getTransaction(txid, options, function(err, tx) { if(err) { return next(err); } var txObj = tx.toObject(); for(var i = 0; i < txObj.inputs.length; i++) { txObj.inputs[i].satoshis = tx.__inputValues[i]; } next(null, txObj); }); }, callback); }); }; AddressService.prototype.getAddressTxids = function(address, options, callback) { var self = this; var txids = {}; var start = self._encoding.encodeAddressIndexKey(address, options.start); var end = self._encoding.encodeAddressIndexKey(address, options.end); var stream = self.store.createKeyStream({ gte: start, lt: end }); var streamErr = null; stream.on('data', function(buffer) { var key = self._encoding.decodeAddressIndexKey(buffer); txids[key.txid] = true; }); stream.on('end', function() { callback(streamErr, Object.keys(txids)); }); stream.on('error', function(err) { streamErr = err; }); }; AddressService.prototype.getAddressTxidsWithHeights = function(address, options, callback) { var self = this; var txids = {}; var start = self._encoding.encodeAddressIndexKey(address, options.start); var end = self._encoding.encodeAddressIndexKey(address, options.end); var stream = self.store.createKeyStream({ gte: start, lt: end }); var streamErr = null; stream.on('data', function(buffer) { var key = self._encoding.decodeAddressIndexKey(buffer); txids[key.txid] = key.height; }); stream.on('end', function() { callback(streamErr, txids); }); stream.on('error', function(err) { streamErr = err; }); }; /** * This will give an object with: * balance - confirmed balance * unconfirmedBalance - unconfirmed balance * totalReceived - satoshis received * totalSpent - satoshis spent * appearances - number of transactions * unconfirmedAppearances - number of unconfirmed transactions * txids - list of txids (unless noTxList is set) * * @param {String} address * @param {Object} options * @param {Boolean} [options.noTxList] - if set, txid array will not be included * @param {Function} callback */ AddressService.prototype.getAddressSummary = function(addressArg, options, callback) { var self = this; var startTime = new Date(); var address = new Address(addressArg); if (_.isUndefined(options.queryMempool)) { options.queryMempool = true; } async.waterfall([ function(next) { self._getAddressConfirmedSummary(address, options, next); }, function(result, next) { self._getAddressMempoolSummary(address, options, result, next); }, function(result, next) { self._setAndSortTxidsFromAppearanceIds(result, next); } ], function(err, result) { if (err) { return callback(err); } var summary = self._transformAddressSummaryFromResult(result, options); var timeDelta = new Date() - startTime; if (timeDelta > 5000) { var seconds = Math.round(timeDelta / 1000); log.warn('Slow (' + seconds + 's) getAddressSummary request for address: ' + address.toString()); } callback(null, summary); }); }; AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) { var self = this; var baseResult = { appearanceIds: {}, totalReceived: 0, balance: 0, unconfirmedAppearanceIds: {}, unconfirmedBalance: 0 }; async.waterfall([ function(next) { self._getAddressConfirmedInputsSummary(address, baseResult, options, next); }, function(result, next) { self._getAddressConfirmedOutputsSummary(address, result, options, next); } ], callback); }; AddressService.prototype._getAddressConfirmedInputsSummary = function(address, result, options, callback) { $.checkArgument(address instanceof Address); var self = this; var error = null; var count = 0; var inputsStream = self.createInputsStream(address, options); inputsStream.on('data', function(input) { var txid = input.txid; result.appearanceIds[txid] = input.height; count++; if (count > self.maxInputsQueryLength) { log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for summary of address ' + address.toString()); error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached'); inputsStream.end(); } }); inputsStream.on('error', function(err) { error = err; }); inputsStream.on('end', function() { if (error) { return callback(error); } callback(null, result); }); }; AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, result, options, callback) { $.checkArgument(address instanceof Address); $.checkArgument(!_.isUndefined(result) && !_.isUndefined(result.appearanceIds) && !_.isUndefined(result.unconfirmedAppearanceIds)); var self = this; var count = 0; var outputStream = self.createOutputsStream(address, options); var error = null; outputStream.on('data', function(output) { var txid = output.txid; var outputIndex = output.outputIndex; result.totalReceived += output.satoshis; result.appearanceIds[txid] = output.height; if(!options.noBalance) { // Bitcoind's isSpent only works for confirmed transactions var spentDB = self.node.services.bitcoind.isSpent(txid, outputIndex); if(!spentDB) { result.balance += output.satoshis; } if(options.queryMempool) { // Check to see if this output is spent in the mempool and if so // we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta) var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey( new Buffer(txid, 'hex'), // TODO: get buffer directly outputIndex ); var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; if(spentMempool) { result.unconfirmedBalance -= output.satoshis; } } } count++; if (count > self.maxOutputsQueryLength) { log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for summary of address ' + address.toString()); error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached'); outputStream.end(); } }); outputStream.on('error', function(err) { error = err; }); outputStream.on('end', function() { if (error) { return callback(error); } callback(null, result); }); }; AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(result, callback) { result.txids = Object.keys(result.appearanceIds); result.txids.sort(function(a, b) { return result.appearanceIds[a] - result.appearanceIds[b]; }); result.unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds); result.unconfirmedTxids.sort(function(a, b) { return result.unconfirmedAppearanceIds[a] - result.unconfirmedAppearanceIds[b]; }); callback(null, result); }; AddressService.prototype._getAddressMempoolSummary = function(address, options, result, callback) { var self = this; // Skip if the options do not want to include the mempool if (!options.queryMempool) { return callback(null, result); } var addressStr = address.toString(); var hashBuffer = address.hashBuffer; var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; var addressIndexKey = self._encoding.encodeMempoolAddressIndexKey(hashBuffer, hashTypeBuffer); if(!this.mempoolAddressIndex[addressIndexKey]) { return callback(null, result); } async.waterfall([ function(next) { self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { if (err) { return next(err); } for(var i = 0; i < mempoolInputs.length; i++) { var input = mempoolInputs[i]; result.unconfirmedAppearanceIds[input.txid] = input.timestamp; } next(null, result); }); }, function(result, next) { self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { if (err) { return next(err); } for(var i = 0; i < mempoolOutputs.length; i++) { var output = mempoolOutputs[i]; result.unconfirmedAppearanceIds[output.txid] = output.timestamp; if(!options.noBalance) { var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey( new Buffer(output.txid, 'hex'), // TODO: get buffer directly output.outputIndex ); var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; // Only add this to the balance if it's not spent in the mempool already if(!spentMempool) { result.unconfirmedBalance += output.satoshis; } } } next(null, result); }); } ], callback); }; AddressService.prototype._transformAddressSummaryFromResult = function(result, options) { var confirmedTxids = result.txids; var unconfirmedTxids = result.unconfirmedTxids; var summary = { totalReceived: result.totalReceived, totalSpent: result.totalReceived - result.balance, balance: result.balance, appearances: confirmedTxids.length, unconfirmedBalance: result.unconfirmedBalance, unconfirmedAppearances: unconfirmedTxids.length }; if (options.fullTxList) { summary.appearanceIds = result.appearanceIds; summary.unconfirmedAppearanceIds = result.unconfirmedAppearanceIds; } else if (!options.noTxList) { summary.txids = confirmedTxids.concat(unconfirmedTxids); } return summary; }; module.exports = AddressService;