2015-09-09 16:45:49 -07:00
|
|
|
'use strict';
|
|
|
|
|
2015-10-16 21:56:29 -07:00
|
|
|
var bitcore = require('bitcore-lib');
|
2015-09-09 16:45:49 -07:00
|
|
|
var async = require('async');
|
|
|
|
var _ = bitcore.deps._;
|
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
var constants = require('./constants');
|
|
|
|
|
2015-09-09 16:45:49 -07:00
|
|
|
/**
|
|
|
|
* 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
|
2015-09-22 08:38:14 -07:00
|
|
|
* of results (uses leveldb key streaming).
|
2015-09-09 16:45:49 -07:00
|
|
|
*/
|
|
|
|
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];
|
|
|
|
}
|
2016-01-06 18:20:10 -08:00
|
|
|
|
2016-01-11 11:34:04 -08:00
|
|
|
this.maxHistoryQueryLength = args.options.maxHistoryQueryLength || constants.MAX_HISTORY_QUERY_LENGTH;
|
|
|
|
this.maxAddressesQuery = args.options.maxAddressesQuery || constants.MAX_ADDRESSES_QUERY;
|
2016-01-06 18:20:10 -08:00
|
|
|
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-14 06:00:18 -07:00
|
|
|
this.detailedArray = [];
|
2015-09-09 16:45:49 -07:00
|
|
|
}
|
|
|
|
|
2016-01-11 11:34:04 -08:00
|
|
|
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;
|
|
|
|
};
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2015-09-22 08:38:14 -07:00
|
|
|
/**
|
|
|
|
* This function will give detailed history for the configured
|
|
|
|
* addresses. See AddressService.prototype.getAddressHistory
|
|
|
|
* for complete documentation about options and response format.
|
|
|
|
*/
|
2015-09-09 16:45:49 -07:00
|
|
|
AddressHistory.prototype.get = function(callback) {
|
|
|
|
var self = this;
|
2015-09-15 10:17:15 -07:00
|
|
|
var totalCount;
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2016-01-11 11:34:04 -08:00
|
|
|
if (this.addresses.length > this.maxAddressesQuery) {
|
|
|
|
return callback(new Error('Maximum number of addresses (' + this.maxAddressQuery + ') exceeded'));
|
2016-01-06 16:14:45 -08:00
|
|
|
}
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2016-01-11 11:34:04 -08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2016-01-11 11:34:04 -08:00
|
|
|
function finish(allTxids) {
|
|
|
|
totalCount = allTxids.length;
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2016-01-11 11:34:04 -08:00
|
|
|
// 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);
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
// 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:' +
|
2016-01-11 11:34:04 -08:00
|
|
|
self.address.join(',')
|
2016-01-06 18:20:10 -08:00
|
|
|
));
|
|
|
|
}
|
2015-09-09 16:45:49 -07:00
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
// Reverse to include most recent at the top
|
|
|
|
txids.reverse();
|
2016-01-06 16:14:45 -08:00
|
|
|
|
|
|
|
async.eachSeries(
|
2016-01-06 18:20:10 -08:00
|
|
|
txids,
|
|
|
|
function(txid, next) {
|
|
|
|
self.getDetailedInfo(txid, next);
|
2016-01-06 16:14:45 -08:00
|
|
|
},
|
|
|
|
function(err) {
|
2015-09-09 16:45:49 -07:00
|
|
|
if (err) {
|
2016-01-06 16:14:45 -08:00
|
|
|
return callback(err);
|
2015-09-09 16:45:49 -07:00
|
|
|
}
|
2016-01-06 16:14:45 -08:00
|
|
|
callback(null, {
|
|
|
|
totalCount: totalCount,
|
|
|
|
items: self.detailedArray
|
|
|
|
});
|
2015-09-14 06:00:18 -07:00
|
|
|
}
|
2016-01-06 16:14:45 -08:00
|
|
|
);
|
2016-01-06 18:20:10 -08:00
|
|
|
|
2015-09-09 16:45:49 -07:00
|
|
|
}
|
|
|
|
|
2015-09-14 06:00:18 -07:00
|
|
|
};
|
|
|
|
|
2015-09-22 08:38:14 -07:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2016-01-06 18:20:10 -08:00
|
|
|
AddressHistory.prototype.getDetailedInfo = function(txid, next) {
|
2015-09-09 16:45:49 -07:00
|
|
|
var self = this;
|
|
|
|
var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool;
|
|
|
|
|
2015-09-14 06:00:18 -07:00
|
|
|
self.node.services.db.getTransactionWithBlockInfo(
|
2016-01-06 18:20:10 -08:00
|
|
|
txid,
|
2015-09-14 06:00:18 -07:00
|
|
|
queryMempool,
|
|
|
|
function(err, transaction) {
|
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
transaction.populateInputs(self.node.services.db, [], function(err) {
|
2016-01-06 18:20:10 -08:00
|
|
|
if (err) {
|
2015-09-09 16:45:49 -07:00
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
var addressDetails = self.getAddressDetailsForTransaction(transaction);
|
|
|
|
|
2015-09-14 06:00:18 -07:00
|
|
|
self.detailedArray.push({
|
2016-01-06 18:20:10 -08:00
|
|
|
addresses: addressDetails.addresses,
|
|
|
|
satoshis: addressDetails.satoshis,
|
2015-09-14 06:00:18 -07:00
|
|
|
height: transaction.__height,
|
|
|
|
confirmations: self.getConfirmationsDetail(transaction),
|
|
|
|
timestamp: transaction.__timestamp,
|
2015-10-16 21:56:29 -07:00
|
|
|
// TODO bitcore-lib should return null instead of throwing error on coinbase
|
2015-09-14 06:00:18 -07:00
|
|
|
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
|
|
|
|
tx: transaction
|
2015-09-09 16:45:49 -07:00
|
|
|
});
|
2015-09-14 06:00:18 -07:00
|
|
|
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
2015-09-09 16:45:49 -07:00
|
|
|
};
|
|
|
|
|
2015-09-22 08:38:14 -07:00
|
|
|
/**
|
|
|
|
* A helper function for `getDetailedInfo` for getting the confirmations.
|
|
|
|
* @param {Transaction} transaction - A transaction with a populated __height value.
|
|
|
|
*/
|
2015-09-14 06:00:18 -07:00
|
|
|
AddressHistory.prototype.getConfirmationsDetail = function(transaction) {
|
|
|
|
var confirmations = 0;
|
|
|
|
if (transaction.__height >= 0) {
|
|
|
|
confirmations = this.node.services.db.tip.__height - transaction.__height + 1;
|
2015-09-09 16:45:49 -07:00
|
|
|
}
|
2015-09-14 06:00:18 -07:00
|
|
|
return confirmations;
|
2015-09-09 16:45:49 -07:00
|
|
|
};
|
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2015-09-14 06:00:18 -07:00
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
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);
|
2015-09-14 18:03:06 -07:00
|
|
|
}
|
2016-01-06 18:20:10 -08:00
|
|
|
result.satoshis += output.satoshis;
|
2015-09-09 16:45:49 -07:00
|
|
|
}
|
|
|
|
}
|
2015-09-14 18:03:06 -07:00
|
|
|
|
2016-01-06 18:20:10 -08:00
|
|
|
return result;
|
|
|
|
|
2015-09-09 16:45:49 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = AddressHistory;
|