diff --git a/lib/services/address/history.js b/lib/services/address/history.js index 998dbfde..b7750068 100644 --- a/lib/services/address/history.js +++ b/lib/services/address/history.js @@ -44,6 +44,7 @@ function AddressHistory(args) { AddressHistory.prototype._mergeAndSortTxids = function(summaries) { var appearanceIds = {}; var unconfirmedAppearanceIds = {}; + for (var i = 0; i < summaries.length; i++) { var summary = summaries[i]; for (var key in summary.appearanceIds) { @@ -79,6 +80,8 @@ AddressHistory.prototype.get = function(callback) { return callback(new TypeError('Maximum number of addresses (' + this.maxAddressesQuery + ') exceeded')); } + this.options.noBalance = true; + if (this.addresses.length === 1) { var address = this.addresses[0]; self.node.services.address.getAddressSummary(address, this.options, function(err, summary) { @@ -89,10 +92,11 @@ AddressHistory.prototype.get = function(callback) { }); } else { var opts = _.clone(this.options); + opts.fullTxList = true; async.mapLimit( self.addresses, - self.maxAddressesLimit, + self.maxaddressesLimit, function(address, next) { self.node.services.address.getAddressSummary(address, opts, next); }, diff --git a/lib/services/address/index.js b/lib/services/address/index.js index 61eed5ef..f67fa8ae 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -57,6 +57,7 @@ var AddressService = function(options) { } 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); @@ -70,6 +71,7 @@ AddressService.prototype.start = function(callback) { var self = this; async.series([ + function(next) { // Flush any existing mempool index if (fs.existsSync(self.mempoolIndexPath)) { @@ -88,8 +90,7 @@ AddressService.prototype.start = function(callback) { }, function(next) { self.mempoolIndex = levelup( - self.mempoolIndexPath, - { + self.mempoolIndexPath, { db: self.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary', @@ -154,20 +155,17 @@ AddressService.prototype.getAPIMethods = function() { * Called by the Bus to get the available events for this service. */ AddressService.prototype.getPublishEvents = function() { - 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') - } - ]; + 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') + }]; }; /** @@ -305,6 +303,14 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { continue; } + var hashBufferHex = addressInfo.hashBuffer.toString('hex'); + + if (add) { + this.mempoolAddressIndex[hashBufferHex] = true; + } else { + delete this.mempoolAddressIndex[hashBufferHex]; + } + // Update output index var outputIndexBuffer = new Buffer(4); outputIndexBuffer.writeUInt32BE(outputIndex); @@ -397,6 +403,12 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { value: inputValue }); + var inputHashBufferHex = inputHashBuffer.toString('hex'); + if (add) { + this.mempoolAddressIndex[inputHashBufferHex] = true; + } else { + delete this.mempoolAddressIndex[inputHashBufferHex]; + } } if (!callback) { @@ -446,7 +458,7 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { var script = output.script; - if(!script) { + if (!script) { log.debug('Invalid script'); continue; } @@ -462,7 +474,7 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { // less than the mean of the 11 previous blocks) and not greater than 2 // hours in the future. var key = encoding.encodeOutputKey(addressInfo.hashBuffer, addressInfo.hashTypeBuffer, - height, txidBuffer, outputIndex); + height, txidBuffer, outputIndex); var value = encoding.encodeOutputValue(output.satoshis, output._scriptBuffer); operations.push({ type: action, @@ -494,11 +506,11 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { this.transactionEventHandler(txmessages[addressKey]); } - if(tx.isCoinbase()) { + if (tx.isCoinbase()) { continue; } - for(var inputIndex = 0; inputIndex < inputs.length; inputIndex++) { + for (var inputIndex = 0; inputIndex < inputs.length; inputIndex++) { var input = inputs[inputIndex]; var inputHash; @@ -560,14 +572,14 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { * @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]) { + 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++) { + for (var i = 0; i < emitters.length; i++) { emitters[i].emit('address/transaction', { rejected: obj.rejected, height: obj.height, @@ -591,7 +603,7 @@ AddressService.prototype.transactionEventHandler = function(obj) { * @param {String} obj.addressType */ AddressService.prototype.balanceEventHandler = function(block, obj) { - if(this.subscriptions['address/balance'][obj.hashHex]) { + if (this.subscriptions['address/balance'][obj.hashHex]) { var emitters = this.subscriptions['address/balance'][obj.hashHex]; var address = new Address({ hashBuffer: obj.hashBuffer, @@ -599,10 +611,10 @@ AddressService.prototype.balanceEventHandler = function(block, obj) { type: obj.addressType }); this.getBalance(address, true, function(err, balance) { - if(err) { + if (err) { return this.emit(err); } - for(var i = 0; i < emitters.length; i++) { + for (var i = 0; i < emitters.length; i++) { emitters[i].emit('address/balance', address, balance, block); } }); @@ -621,9 +633,9 @@ 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++) { + for (var i = 0; i < addresses.length; i++) { var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex'); - if(!this.subscriptions[name][hashHex]) { + if (!this.subscriptions[name][hashHex]) { this.subscriptions[name][hashHex] = []; } this.subscriptions[name][hashHex].push(emitter); @@ -641,16 +653,16 @@ 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) { + if (!addresses) { return this.unsubscribeAll(name, emitter); } - for(var i = 0; i < addresses.length; i++) { + for (var i = 0; i < addresses.length; i++) { var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex'); - if(this.subscriptions[name][hashHex]) { + if (this.subscriptions[name][hashHex]) { var emitters = this.subscriptions[name][hashHex]; var index = emitters.indexOf(emitter); - if(index > -1) { + if (index > -1) { emitters.splice(index, 1); } } @@ -665,10 +677,10 @@ AddressService.prototype.unsubscribe = function(name, emitter, addresses) { 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]) { + for (var hashHex in this.subscriptions[name]) { var emitters = this.subscriptions[name][hashHex]; var index = emitters.indexOf(emitter); - if(index > -1) { + if (index > -1) { emitters.splice(index, 1); } } @@ -683,7 +695,7 @@ AddressService.prototype.unsubscribeAll = function(name, emitter) { */ AddressService.prototype.getBalance = function(address, queryMempool, callback) { this.getUnspentOutputs(address, queryMempool, function(err, outputs) { - if(err) { + if (err) { return callback(err); } @@ -852,7 +864,7 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { 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); + 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(); } @@ -871,7 +883,7 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { return callback(error); } - if(options.queryMempool) { + if (options.queryMempool) { self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { if (err) { return callback(err); @@ -1092,7 +1104,7 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { return callback(error); } - if(options.queryMempool) { + if (options.queryMempool) { self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { if (err) { return callback(err); @@ -1176,7 +1188,7 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, h AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) { var self = this; - if(!Array.isArray(addresses)) { + if (!Array.isArray(addresses)) { addresses = [addresses]; } @@ -1184,9 +1196,9 @@ AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, c async.eachSeries(addresses, function(address, next) { self.getUnspentOutputsForAddress(address, queryMempool, function(err, unspents) { - if(err && err instanceof errors.NoOutputs) { + if (err && err instanceof errors.NoOutputs) { return next(); - } else if(err) { + } else if (err) { return next(err); } @@ -1208,10 +1220,12 @@ AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMe var self = this; - this.getOutputs(address, {queryMempool: queryMempool}, function(err, outputs) { + this.getOutputs(address, { + queryMempool: queryMempool + }, function(err, outputs) { if (err) { return callback(err); - } else if(!outputs.length) { + } else if (!outputs.length) { return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []); } @@ -1336,6 +1350,7 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb } async.waterfall([ + function(next) { self._getAddressConfirmedSummary(address, options, next); }, @@ -1357,9 +1372,7 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb var seconds = Math.round(timeDelta / 1000); log.warn('Slow (' + seconds + 's) getAddressSummary request for address: ' + address.toString()); } - callback(null, summary); - }); }; @@ -1375,6 +1388,7 @@ AddressService.prototype._getAddressConfirmedSummary = function(address, options }; async.waterfall([ + function(next) { self._getAddressConfirmedInputsSummary(address, baseResult, options, next); }, @@ -1421,8 +1435,8 @@ AddressService.prototype._getAddressConfirmedInputsSummary = function(address, r AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, result, options, callback) { $.checkArgument(address instanceof Address); $.checkArgument(!_.isUndefined(result) && - !_.isUndefined(result.appearanceIds) && - !_.isUndefined(result.unconfirmedAppearanceIds)); + !_.isUndefined(result.appearanceIds) && + !_.isUndefined(result.unconfirmedAppearanceIds)); var self = this; var count = 0; @@ -1434,25 +1448,27 @@ AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, var txid = output.txid; var outputIndex = output.outputIndex; - // Bitcoind's isSpent only works for confirmed transactions - var spentDB = self.node.services.bitcoind.isSpent(txid, 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); + result.totalReceived += output.satoshis; + result.appearanceIds[txid] = output.height; - if (!spentDB) { - result.balance += output.satoshis; - } + 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 = encoding.encodeSpentIndexSyncKey( - new Buffer(txid, 'hex'), // TODO: get buffer directly - outputIndex - ); - var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; - if (spentMempool) { - result.unconfirmedBalance -= 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 = encoding.encodeSpentIndexSyncKey( + new Buffer(txid, 'hex'), // TODO: get buffer directly + outputIndex + ); + var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; + if (spentMempool) { + result.unconfirmedBalance -= output.satoshis; + } } } @@ -1504,38 +1520,47 @@ AddressService.prototype._getAddressMempoolSummary = function(address, options, var addressStr = address.toString(); var hashBuffer = address.hashBuffer; var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; + var hashBufferHex = hashBuffer.toString('hex'); + + if (!this.mempoolAddressIndex[hashBufferHex]) { + 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++) { + for (var i = 0; i < mempoolInputs.length; i++) { var input = mempoolInputs[i]; result.unconfirmedAppearanceIds[input.txid] = input.timestamp; } next(null, result); }); - }, function(result, next) { + }, + 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]; + if (!options.noBalance) { + for (var i = 0; i < mempoolOutputs.length; i++) { + var output = mempoolOutputs[i]; - result.unconfirmedAppearanceIds[output.txid] = output.timestamp; + result.unconfirmedAppearanceIds[output.txid] = output.timestamp; - var spentIndexSyncKey = 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; + var spentIndexSyncKey = 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);