'use strict'; var bitcore = require('bitcore-lib'); var async = require('async'); var _ = bitcore.deps._; var constants = require('./constants'); /** * 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.maxHistoryQueryLength = args.options.maxHistoryQueryLength || constants.MAX_HISTORY_QUERY_LENGTH; this.maxAddressesQuery = args.options.maxAddressesQuery || constants.MAX_ADDRESSES_QUERY; this.addressStrings = []; for (var i = 0; i < this.addresses.length; i++) { var address = this.addresses[i]; if (address instanceof bitcore.Address) { this.addressStrings.push(address.toString()); } else if (_.isString(address)) { this.addressStrings.push(address); } else { throw new TypeError('Addresses are expected to be strings'); } } this.detailedArray = []; } 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) { appearanceIds[key] = summary.appearanceIds[key]; delete summary.appearanceIds[key]; } for (var unconfirmedKey in summary.unconfirmedAppearanceIds) { unconfirmedAppearanceIds[unconfirmedKey] = summary.unconfirmedAppearanceIds[key]; delete summary.unconfirmedAppearanceIds[key]; } } var confirmedTxids = Object.keys(appearanceIds); confirmedTxids.sort(function(a, b) { // Confirmed are sorted by height return appearanceIds[a] - appearanceIds[b]; }); var unconfirmedTxids = Object.keys(unconfirmedAppearanceIds); unconfirmedTxids.sort(function(a, b) { // Unconfirmed are sorted by timestamp return unconfirmedAppearanceIds[a] - unconfirmedAppearanceIds[b]; }); var txids = confirmedTxids.concat(unconfirmedTxids); return txids; }; /** * 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; if (this.addresses.length > this.maxAddressesQuery) { return callback(new Error('Maximum number of addresses (' + this.maxAddressQuery + ') exceeded')); } if (this.addresses.length === 0) { var address = this.addresses[0]; self.node.services.address.getAddressSummary(address, this.options, function(err, summary) { if (err) { return callback(err); } return finish(summary.txids); }); } else { var opts = _.clone(this.options); opts.fullTxList = true; async.map( self.addresses, function(address, next) { self.node.services.address.getAddressSummary(address, opts, next); }, function(err, summaries) { if (err) { return callback(err); } var txids = self._mergeAndSortTxids(summaries); return finish(txids); } ); } function finish(allTxids) { totalCount = allTxids.length; // Slice the page starting with the most recent var fromOffset = totalCount - self.options.from; var toOffset = totalCount - self.options.to; var txids = allTxids.slice(toOffset, fromOffset); // Verify that this query isn't too long if (txids.length > self.maxHistoryQueryLength) { return callback(new Error( 'Maximum length query (' + self.maxAddressQueryLength + ') exceeded for addresses:' + self.address.join(',') )); } // Reverse to include most recent at the top txids.reverse(); async.eachSeries( txids, function(txid, next) { self.getDetailedInfo(txid, next); }, function(err) { if (err) { return callback(err); } callback(null, { totalCount: totalCount, items: self.detailedArray }); } ); } }; /** * 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(txid, next) { var self = this; var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool; self.node.services.db.getTransactionWithBlockInfo( txid, queryMempool, function(err, transaction) { if (err) { return next(err); } transaction.populateInputs(self.node.services.db, [], function(err) { if (err) { return next(err); } var addressDetails = self.getAddressDetailsForTransaction(transaction); self.detailedArray.push({ 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(); }); } ); }; /** * 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; }; AddressHistory.prototype.getAddressDetailsForTransaction = function(transaction) { 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 && this.addressStrings.indexOf(inputAddress.toString()) > 0) { if (!result.addresses[inputAddress]) { result.addresses[inputAddress] = { inputIndexes: [], outputIndexes: [] }; } else { result.addresses[inputAddress].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 && this.addressStrings.indexOf(outputAddress.toString()) > 0) { if (!result.addresses[outputAddress]) { result.addresses[outputAddress] = { inputIndexes: [], outputIndexes: [] }; } else { result.addresses[outputAddress].inputIndexes.push(outputIndex); } result.satoshis += output.satoshis; } } return result; }; module.exports = AddressHistory;