diff --git a/lib/services/address/index.js b/lib/services/address/index.js index 85bcf2cc..8f5e65a7 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -50,13 +50,13 @@ AddressService.prototype.stop = function(callback) { 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] + //['getBalance', this, this.getBalance, 2], + //['getOutputs', this, this.getOutputs, 2], + //['getUtxos', this, this.getUtxos, 2], + //['getInputForOutput', this, this.getInputForOutput, 2], + //['getAddressHistory', this, this.getAddressHistory, 2], + //['getAddressSummary', this, this.getAddressSummary, 1], + ['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 1] ]; }; @@ -87,117 +87,104 @@ AddressService.prototype._getAddress = function(opts, item) { }; - -AddressService.prototype._getActions = function(connect) { - - var action = 'put'; - var reverseAction = 'del'; - - if (!connect) { - action = 'del'; - reverseAction = 'put'; - } - - return { action: action, reverseAction: reverseAction }; - -}; - -AddressService.prototype._processInput = function(opts, input, cb) { - - var self = this; +AddressService.prototype._processInput = function(opts, input) { var address = this._getAddress(opts, input); if (!address) { - return setImemdiate(cb); + return; } - var action = self._getAddress(opts.connect); - - var operations = []; - // address index - var addressKey = self._encoding.encodeAddressIndexKey(address, opts.block.height, opts.tx.id); + var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, opts.tx.id); - operations.push({ - type: action.action, + var operations = [{ + type: opts.action, key: addressKey - }); + }]; - var prevTxId = input.prevTxId.toString('hex'); + // prev utxo + // TODO: ensure this is a good link backward + var rec = { + type: opts.action, + key: this._encoding.encodeUtxoIndexKey(address, input.prevTxId.toString('hex'), input.outputIndex) + }; - self._txService.getTransaction(prevTxId, {}, function(err, tx) { + // In the event where we are reorg'ing, + // this is where we are putting a utxo back in, we don't know what the original height, sats, or scriptBuffer + // since this only happens on reorg and the utxo that was spent in the chain we are reorg'ing away from will likely + // be spent again sometime soon, we will not add the value back in, just the key - if (err) { - log.debug('Error saving tx inputs.'); - return self._emit('error', err); - } + operations.push(rec); - var utxo = tx.outputs[input.outputIndex]; - - // utxo index - - - // prev utxo - var oldUtxoKey = self._encoding.encodeUtxoIndexKey(address, tx.id, input.outputIndex); - - // remove the old utxo - operations.push({ - type: action.reverseAction, - key: utxaKxey, - value: inputValue - }); - - return operations; - }); + return operations; }; -AddressService.prototype._processOutput = function(txid, output) { +AddressService.prototype._processOutput = function(tx, output, index, opts) { - var address = self.getAddressString(script); + var address = this.getAddressString(output.script); if(!address) { - continue; + return; } - var addressKey = self._encoding.encodeAddressIndexKey(address, block.height, txid); - var utxoKey = self._encoding.encodeUtxoIndexKey(address, txid, outputIndex); - var utxoValue = self._encoding.encodeUtxoIndexValue(block.height, output.satoshis, output._scriptBuffer); + var txid = tx.id; + var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid); + var utxoKey = this._encoding.encodeUtxoIndexKey(address, txid, index); + var utxoValue = this._encoding.encodeUtxoIndexValue(opts.block.height, output.satoshis, output._scriptBuffer); - operations.push({ - type: action, + var operations = [{ + type: opts.action, key: addressKey - }); + }]; operations.push({ - type: action, + type: opts.action, key: utxoKey, value: utxoValue }); }; -AddressService.prototype._processTransactions = function(opts, tx) { +AddressService.prototype._processTransaction = function(opts, tx) { var self = this; - var txid = tx.id; - var _opts = { opts.block, connect: connect ? true : false }; + var action = 'put'; + var reverseAction = 'del'; - var outputOperations = tx.outputs.map(function(tx) { - return self._processOutput(tx, opts); + if (!opts.connect) { + action = 'del'; + reverseAction = 'put'; + } + + var _opts = { block: opts.block, action: action, reverseAction: reverseAction }; + + var outputOperations = tx.outputs.map(function(output, index) { + return self._processOutput(tx, output, index, _opts); }); - opts.outputOperations = outputOperations; + outputOperations = _.flatten(_.compact(outputOperations)); - // this is async because we need to look up a utxo - var inputOperations = tx.inputs.map(function(tx) { - self._processInput(tx, opts, self._onOperations.bind(self)); + var inputOperations = tx.inputs.map(function(input) { + self._processInput(tx, input, _opts); }); + inputOperations = _.flatten(_.compact(inputOperations)); + + return outputOperations.concat(inputOperations); + }; -AddressService.prototype._onOperations = function(operations) { +AddressService.prototype._onBlock = function(block, connect) { + + var self = this; + + var operations = []; + + block.transactions.forEach(function(tx) { + operations.concat(self._processTransaction(tx, { block: block, connect: connect })); + }); if (operations && operations.length > 0) { @@ -215,15 +202,6 @@ AddressService.prototype._onOperations = function(operations) { }; -AddressService.prototype._onBlock = function(block, connect) { - var self = this; - - - self._processTransactions(txs, { block: block, connect: connect }); - - -}; - AddressService.prototype.getAddressString = function(script, output) { var address = script.toAddress(this.node.network.name); if(address) { @@ -1014,7 +992,7 @@ AddressService.prototype.getAddressHistory = function(addresses, options, callba var txids = []; - async.eachLimit(addresses, self.concurrency, function(address, next) { + async.eachLimit(addresses, 4, function(address, next) { self.getAddressTxids(address, options, function(err, tmpTxids) { if(err) { return next(err); @@ -1024,7 +1002,7 @@ AddressService.prototype.getAddressHistory = function(addresses, options, callba return next(); }); }, function() { - async.mapLimit(txids, self.concurrency, function(txid, next) { + async.mapLimit(txids, 4, function(txid, next) { self.node.services.transaction.getTransaction(txid.toString('hex'), options, function(err, tx) { if(err) { return next(err); @@ -1104,22 +1082,94 @@ AddressService.prototype.getAddressTxidsWithHeights = function(address, options, }); }; -/** - * 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.getAddressUnspentOutputs = function(address, options, callback) { + + var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; + var addresses = utils._normalizeAddressArg(address); + var cacheKey = addresses.join(''); + var utxos = this.utxosCache.get(cacheKey); + + function transformUnspentOutput(delta) { + var script = bitcore.Script.fromAddress(delta.address); + return { + address: delta.address, + txid: delta.txid, + outputIndex: delta.index, + script: script.toHex(), + satoshis: delta.satoshis, + timestamp: delta.timestamp + }; + } + + function updateWithMempool(confirmedUtxos, mempoolDeltas) { + if (!mempoolDeltas || !mempoolDeltas.length) { + return confirmedUtxos; + } + var isSpentOutputs = false; + var mempoolUnspentOutputs = []; + var spentOutputs = []; + + for (var i = 0; i < mempoolDeltas.length; i++) { + var delta = mempoolDeltas[i]; + if (delta.prevtxid && delta.satoshis <= 0) { + if (!spentOutputs[delta.prevtxid]) { + spentOutputs[delta.prevtxid] = [delta.prevout]; + } else { + spentOutputs[delta.prevtxid].push(delta.prevout); + } + isSpentOutputs = true; + } else { + mempoolUnspentOutputs.push(transformUnspentOutput(delta)); + } + } + + var utxos = mempoolUnspentOutputs.reverse().concat(confirmedUtxos); + + if (isSpentOutputs) { + return utxos.filter(function(utxo) { + if (!spentOutputs[utxo.txid]) { + return true; + } else { + return (spentOutputs[utxo.txid].indexOf(utxo.outputIndex) === -1); + } + }); + } + + return utxos; + } + + function finish(mempoolDeltas) { + if (utxos) { + return setImmediate(function() { + callback(null, updateWithMempool(utxos, mempoolDeltas)); + }); + } else { + self.client.getAddressUtxos({addresses: addresses}, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var utxos = response.result.reverse(); + self.utxosCache.set(cacheKey, utxos); + callback(null, updateWithMempool(utxos, mempoolDeltas)); + }); + } + } + + if (queryMempool) { + self.client.getAddressMempool({addresses: addresses}, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + finish(response.result); + }); + } else { + finish(); + } + +}; + AddressService.prototype.getAddressSummary = function(addressArg, options, callback) { + var self = this; var startTime = new Date(); diff --git a/lib/utils.js b/lib/utils.js index 9db49dd9..f7e84f87 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -3,6 +3,8 @@ var bitcore = require('bitcore-lib'); var BufferUtil = bitcore.util.buffer; var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1 +var crypto = require('crypto'); +var _ = require('lodash'); var utils = {}; utils.isHash = function(value) { @@ -93,7 +95,15 @@ utils.toJSONL = function(obj) { var str = JSON.stringify(obj); str = str.replace(/\n/g, ''); return str + '\n'; -} +}; + +utils.normalizeTimeStamp = function(addressArg) { + var addresses = [addressArg]; + if (Array.isArray(addressArg)) { + addresses = addressArg; + } + return addresses; +}; utils.normalizeTimeStamp = function(value) { if (value > 0xffffffff) {