flocore-node/lib/services/address/history.js

247 lines
7.2 KiB
JavaScript

'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 = constants.MAX_HISTORY_QUERY_LENGTH;
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.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;
// TODO: handle multiple addresses (restore previous functionality)
if (self.addresses.length > 1) {
return callback('Only single address queries supported currently');
}
var address = self.addresses[0];
this.node.services.address.getAddressSummary(address, this.options, function(err, summary) {
if (err) {
return callback(err);
}
totalCount = summary.txids.length;
// TODO: Make sure txids are sorted by height and time
var fromOffset = summary.txids.length - self.options.from;
var toOffset = summary.txids.length - self.options.to;
var txids = summary.txids.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:' +
this.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
});
}
);
});
};
/**
* A helper function to sort and slice/paginate the `combinedArray`
*/
// TODO: Remove once txids summary results are verified to be sorted
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
*/
// TODO: Remove once txids summary results are verified to be sorted
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(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;