- Reindexing the bitcore-node database is required with this change. - Address indexes are updated to include address and height in spent index so that both inputs and outputs can be queried by address and height using "start" and "stop" to limit the range of the query. - Address history also now supports paginated results using "from" and "to" values that indicate an index in the array.
189 lines
5.3 KiB
JavaScript
189 lines
5.3 KiB
JavaScript
'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). See AddressService.prototype.getAddressHistory
|
|
* for complete documentation about options.
|
|
*/
|
|
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.transactions = {};
|
|
this.sortedArray = [];
|
|
}
|
|
|
|
AddressHistory.MAX_ADDRESS_QUERIES = 20;
|
|
AddressHistory.MAX_TX_QUERIES = 10;
|
|
|
|
AddressHistory.prototype.get = function(callback) {
|
|
var self = this;
|
|
|
|
// TODO check for mempool inputs and outputs by a group of addresses, currently
|
|
// each address individually loops through the mempool and does not check input scripts.
|
|
|
|
async.eachLimit(
|
|
self.addresses,
|
|
AddressHistory.MAX_ADDRESS_QUERIES,
|
|
function(address, next) {
|
|
self.getTransactionInfo(address, next);
|
|
},
|
|
function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
async.eachSeries(
|
|
self.transactionInfo,
|
|
function(txInfo, next) {
|
|
self.getDetailedInfo(txInfo, next);
|
|
},
|
|
function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
self.sortTransactionsIntoArray();
|
|
self.paginateSortedArray();
|
|
callback(null, self.sortedArray);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
};
|
|
|
|
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();
|
|
});
|
|
};
|
|
|
|
AddressHistory.sortByHeight = function(a, b) {
|
|
// TODO consider timestamp for mempool transactions
|
|
return a.height < b.height;
|
|
};
|
|
|
|
AddressHistory.prototype.paginateSortedArray = function() {
|
|
if (!_.isUndefined(this.options.from) && !_.isUndefined(this.options.to)) {
|
|
this.sortedArray = this.sortedArray.slice(this.options.from, this.options.to);
|
|
}
|
|
};
|
|
|
|
AddressHistory.prototype.getDetailedInfo = function(txInfo, next) {
|
|
var self = this;
|
|
var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool;
|
|
|
|
if (self.transactions[txInfo.address] && self.transactions[txInfo.address][txInfo.txid]) {
|
|
self.amendDetailedInfoWithSatoshis(txInfo);
|
|
setImmediate(next);
|
|
} else {
|
|
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);
|
|
}
|
|
var confirmations = 0;
|
|
if (transaction.__height >= 0) {
|
|
confirmations = self.node.services.db.tip.__height - transaction.__height + 1;
|
|
}
|
|
|
|
if (!self.transactions[txInfo.address]) {
|
|
self.transactions[txInfo.address] = {};
|
|
}
|
|
|
|
self.transactions[txInfo.address][txInfo.txid] = {
|
|
address: txInfo.address,
|
|
satoshis: 0,
|
|
height: transaction.__height,
|
|
confirmations: confirmations,
|
|
timestamp: transaction.__timestamp,
|
|
// TODO bitcore should return null instead of throwing error on coinbase
|
|
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
|
|
outputIndexes: [],
|
|
inputIndexes: [],
|
|
tx: transaction
|
|
};
|
|
|
|
self.amendDetailedInfoWithSatoshis(txInfo);
|
|
next();
|
|
});
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
AddressHistory.prototype.amendDetailedInfoWithSatoshis = function(txInfo) {
|
|
var historyItem = this.transactions[txInfo.address][txInfo.txid];
|
|
if (txInfo.outputIndex >= 0) {
|
|
historyItem.outputIndexes.push(txInfo.outputIndex);
|
|
historyItem.satoshis += txInfo.satoshis;
|
|
} else if (txInfo.inputIndex >= 0){
|
|
historyItem.inputIndexes.push(txInfo.inputIndex);
|
|
historyItem.satoshis -= historyItem.tx.inputs[txInfo.inputIndex].output.satoshis;
|
|
}
|
|
};
|
|
|
|
AddressHistory.prototype.sortTransactionsIntoArray = function() {
|
|
this.sortedArray = [];
|
|
for(var address in this.transactions) {
|
|
for(var txid in this.transactions[address]) {
|
|
this.sortedArray.push(this.transactions[address][txid]);
|
|
}
|
|
}
|
|
this.sortedArray.sort(AddressHistory.sortByHeight);
|
|
};
|
|
|
|
module.exports = AddressHistory;
|