'use strict'; var bitcore = require('bitcore'); var async = require('async'); var _ = bitcore.deps._; /** * This represents an instance that keeps track of data over a series of * asynchronous I/O calls to get the transaction history for a group of * addresses. History can be queried by start and end block heights to limit large sets * of results (uses leveldb key streaming). */ function AddressHistory(args) { this.node = args.node; this.options = args.options; if(Array.isArray(args.addresses)) { this.addresses = args.addresses; } else { this.addresses = [args.addresses]; } this.transactionInfo = []; this.combinedArray = []; this.detailedArray = []; } AddressHistory.MAX_ADDRESS_QUERIES = 20; /** * This function will give detailed history for the configured * addresses. See AddressService.prototype.getAddressHistory * for complete documentation about options and response format. */ AddressHistory.prototype.get = function(callback) { var self = this; var totalCount; async.eachLimit( self.addresses, AddressHistory.MAX_ADDRESS_QUERIES, function(address, next) { self.getTransactionInfo(address, next); }, function(err) { if (err) { return callback(err); } self.combineTransactionInfo(); totalCount = Number(self.combinedArray.length); self.sortAndPaginateCombinedArray(); async.eachSeries( self.combinedArray, function(txInfo, next) { self.getDetailedInfo(txInfo, next); }, function(err) { if (err) { return callback(err); } callback(null, { totalCount: totalCount, items: self.detailedArray }); } ); } ); }; /** * This function will retrieve input and output information for an address * and set the property `this.transactionInfo`. * @param {String} address - A base58check encoded address * @param {Function} next */ AddressHistory.prototype.getTransactionInfo = function(address, next) { var self = this; var args = { start: self.options.start, end: self.options.end, queryMempool: _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool }; var outputs; var inputs; async.parallel([ function(done) { self.node.services.address.getOutputs(address, args, function(err, result) { if (err) { return done(err); } outputs = result; done(); }); }, function(done) { self.node.services.address.getInputs(address, args, function(err, result) { if (err) { return done(err); } inputs = result; done(); }); } ], function(err) { if (err) { return next(err); } self.transactionInfo = self.transactionInfo.concat(outputs, inputs); next(); }); }; /** * This function combines results from getInputs and getOutputs at * `this.transactionInfo` to be "txid" unique at `this.combinedArray`. */ AddressHistory.prototype.combineTransactionInfo = function() { var combinedArrayMap = {}; this.combinedArray = []; var l = this.transactionInfo.length; for(var i = 0; i < l; i++) { var item = this.transactionInfo[i]; var mapKey = item.txid; if (combinedArrayMap[mapKey] >= 0) { var combined = this.combinedArray[combinedArrayMap[mapKey]]; if (!combined.addresses[item.address]) { combined.addresses[item.address] = { outputIndexes: [], inputIndexes: [] }; } if (item.outputIndex >= 0) { combined.satoshis += item.satoshis; combined.addresses[item.address].outputIndexes.push(item.outputIndex); } else if (item.inputIndex >= 0) { combined.addresses[item.address].inputIndexes.push(item.inputIndex); } } else { item.addresses = {}; item.addresses[item.address] = { outputIndexes: [], inputIndexes: [] }; if (item.outputIndex >= 0) { item.addresses[item.address].outputIndexes.push(item.outputIndex); } else if (item.inputIndex >= 0) { item.addresses[item.address].inputIndexes.push(item.inputIndex); } delete item.outputIndex; delete item.inputIndex; delete item.address; this.combinedArray.push(item); combinedArrayMap[mapKey] = this.combinedArray.length - 1; } } }; /** * A helper function to sort and slice/paginate the `combinedArray` */ AddressHistory.prototype.sortAndPaginateCombinedArray = function() { this.combinedArray.sort(AddressHistory.sortByHeight); if (!_.isUndefined(this.options.from) && !_.isUndefined(this.options.to)) { this.combinedArray = this.combinedArray.slice(this.options.from, this.options.to); } }; /** * A helper sort function to order by height and then by date * for transactions that are in the mempool. * @param {Object} a - An item from the `combinedArray` * @param {Object} b */ AddressHistory.sortByHeight = function(a, b) { if (a.height < 0 && b.height < 0) { // Both are from the mempool, compare timestamps if (a.timestamp === b.timestamp) { return 0; } else { return a.timestamp < b.timestamp ? 1 : -1; } } else if (a.height < 0 && b.height > 0) { // A is from the mempool and B is in a block return -1; } else if (a.height > 0 && b.height < 0) { // A is in a block and B is in the mempool return 1; } else if (a.height === b.height) { // The heights are equal return 0; } else { // Otherwise compare heights return a.height < b.height ? 1 : -1; } }; /** * This function will transform items from the combinedArray into * the detailedArray with the full transaction, satoshis and confirmation. * @param {Object} txInfo - An item from the `combinedArray` * @param {Function} next */ AddressHistory.prototype.getDetailedInfo = function(txInfo, next) { var self = this; var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool; self.node.services.db.getTransactionWithBlockInfo( txInfo.txid, queryMempool, function(err, transaction) { if (err) { return next(err); } transaction.populateInputs(self.node.services.db, [], function(err) { if(err) { return next(err); } self.detailedArray.push({ addresses: txInfo.addresses, satoshis: self.getSatoshisDetail(transaction, txInfo), height: transaction.__height, confirmations: self.getConfirmationsDetail(transaction), timestamp: transaction.__timestamp, // TODO bitcore should return null instead of throwing error on coinbase fees: !transaction.isCoinbase() ? transaction.getFee() : null, tx: transaction }); next(); }); } ); }; /** * A helper function for `getDetailedInfo` for getting the confirmations. * @param {Transaction} transaction - A transaction with a populated __height value. */ AddressHistory.prototype.getConfirmationsDetail = function(transaction) { var confirmations = 0; if (transaction.__height >= 0) { confirmations = this.node.services.db.tip.__height - transaction.__height + 1; } return confirmations; }; /** * A helper function for `getDetailedInfo` for getting the satoshis. * @param {Transaction} transaction - A transaction populated with previous outputs * @param {Object} txInfo - An item from `combinedArray` */ AddressHistory.prototype.getSatoshisDetail = function(transaction, txInfo) { var satoshis = txInfo.satoshis || 0; for(var address in txInfo.addresses) { if (txInfo.addresses[address].inputIndexes.length >= 0) { for(var j = 0; j < txInfo.addresses[address].inputIndexes.length; j++) { satoshis -= transaction.inputs[txInfo.addresses[address].inputIndexes[j]].output.satoshis; } } } return satoshis; }; module.exports = AddressHistory;