Merge pull request #146 from isocolsky/feat/history_pagination

Feat/history pagination
This commit is contained in:
Matias Alejo Garcia 2015-03-18 00:56:44 -03:00
commit fb81a4f551
4 changed files with 132 additions and 37 deletions

View File

@ -261,7 +261,12 @@ ExpressApp.start = function(opts) {
router.get('/v1/txhistory/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getTxHistory({}, function(err, txs) {
var opts = {};
if (req.query.minTs) opts.minTs = +req.query.minTs;
if (req.query.maxTs) opts.maxTs = +req.query.maxTs;
if (req.query.limit) opts.limit = +req.query.limit;
server.getTxHistory(opts, function(err, txs) {
if (err) return returnError(err, res, req);
res.json(txs);
res.end();

View File

@ -916,15 +916,20 @@ WalletService.prototype._normalizeTxHistory = function(txs) {
* Retrieves all transactions (incoming & outgoing) in the range (maxTs-minTs)
* Times are in UNIX EPOCH
*
* @param {Object} opts.minTs (defaults to 0)
* @param {Object} opts.maxTs (defaults to now)
* @param {Object} opts.limit
* @param {Object} opts
* @param {Number} opts.minTs (defaults to 0)
* @param {Number} opts.maxTs (defaults to now)
* @param {Number} opts.limit
* @returns {TxProposal[]} Transaction proposals, first newer
*/
WalletService.prototype.getTxHistory = function(opts, cb) {
var self = this;
function decorate(txs, addresses, proposals) {
var indexedAddresses = _.indexBy(addresses, 'address');
var indexedProposals = _.indexBy(proposals, 'txid');
function sum(items, isMine, isChange) {
var filter = {};
if (_.isBoolean(isMine)) filter.isMine = isMine;
@ -935,53 +940,78 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
}, 0);
};
var indexedAddresses = _.indexBy(addresses, 'address');
var indexedProposals = _.indexBy(proposals, 'txid');
_.each(txs, function(tx) {
_.each(tx.inputs.concat(tx.outputs), function(item) {
function classify(items) {
return _.map(items, function(item) {
var address = indexedAddresses[item.address];
item.isMine = !!address;
item.isChange = address ? address.isChange : false;
});
var amountIn = sum(tx.inputs, true);
var amountOut = sum(tx.outputs, true, false);
var amountOutChange = sum(tx.outputs, true, true);
var amount;
if (amountIn == (amountOut + amountOutChange + (amountIn > 0 ? tx.fees : 0))) {
tx.action = 'moved';
amount = amountOut;
} else {
amount = amountIn - amountOut - amountOutChange - (amountIn > 0 ? tx.fees : 0);
tx.action = amount > 0 ? 'sent' : 'received';
return {
address: item.address,
amount: item.amount,
isMine: !!address,
isChange: address ? address.isChange : false,
}
tx.amount = Math.abs(amount);
if (tx.action == 'sent' || tx.action == 'moved') {
tx.addressTo = tx.outputs[0].address;
});
};
delete tx.inputs;
delete tx.outputs;
return _.map(txs, function(tx) {
var inputs = classify(tx.inputs);
var outputs = classify(tx.outputs);
var amountIn = sum(inputs, true);
var amountOut = sum(outputs, true, false);
var amountOutChange = sum(outputs, true, true);
var amount, action, addressTo;
if (amountIn == (amountOut + amountOutChange + (amountIn > 0 ? tx.fees : 0))) {
amount = amountOut;
action = 'moved';
} else {
amount = amountIn - amountOut - amountOutChange - (amountIn > 0 ? tx.fees : 0);
action = amount > 0 ? 'sent' : 'received';
}
amount = Math.abs(amount);
if (action == 'sent' || action == 'moved') {
addressTo = outputs[0].address;
};
var newTx = {
txid: tx.txid,
action: action,
amount: amount,
fees: tx.fees,
time: tx.time,
addressTo: addressTo,
confirmations: tx.confirmations,
};
var proposal = indexedProposals[tx.txid];
if (proposal) {
tx.proposalId = proposal.id;
tx.creatorName = proposal.creatorName;
tx.message = proposal.message;
tx.actions = _.map(proposal.actions, function(action) {
newTx.proposalId = proposal.id;
newTx.creatorName = proposal.creatorName;
newTx.message = proposal.message;
newTx.actions = _.map(proposal.actions, function(action) {
return _.pick(action, ['createdOn', 'type', 'copayerId', 'copayerName', 'comment']);
});
// tx.sentTs = proposal.sentTs;
// tx.merchant = proposal.merchant;
//tx.paymentAckMemo = proposal.paymentAckMemo;
// newTx.sentTs = proposal.sentTs;
// newTx.merchant = proposal.merchant;
//newTx.paymentAckMemo = proposal.paymentAckMemo;
}
return newTx;
});
};
function paginate(txs) {
// TODO
var limited = opts.limit && opts.limit != -1;
if (!opts.minTs && !opts.maxTs && !limited) return;
var minTs = opts.minTs || 0;
var maxTs = opts.maxTs || Math.ceil(Date.now() / 1000);
var filtered = _.sortBy(_.filter(txs, function(tx) {
return tx.time >= minTs && tx.time <= maxTs;
}), 'time');
return limited ? _.take(filtered, opts.limit) : filtered;
};
// Get addresses for this wallet
@ -1014,8 +1044,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
var proposals = res[0];
var txs = res[1];
decorate(txs, addresses, proposals);
paginate(txs);
txs = paginate(decorate(txs, addresses, proposals));
return cb(null, txs);
});

View File

@ -2,7 +2,7 @@
"name": "bitcore-wallet-service",
"description": "A service for Mutisig HD Bitcoin Wallets",
"author": "BitPay Inc",
"version": "0.0.8",
"version": "0.0.9",
"keywords": [
"bitcoin",
"copay",

View File

@ -2206,7 +2206,7 @@ describe('Copay server', function() {
txid: '1',
confirmations: 1,
fees: 100,
minedTs: 1,
time: 1,
inputs: [{
address: 'external',
amount: 500,
@ -2234,7 +2234,7 @@ describe('Copay server', function() {
txid: '1',
confirmations: 1,
fees: 100,
minedTs: 1,
time: 1,
inputs: [{
address: mainAddresses[0].address,
amount: 500,
@ -2262,7 +2262,7 @@ describe('Copay server', function() {
txid: '1',
confirmations: 1,
fees: 100,
minedTs: 1,
time: 1,
inputs: [{
address: mainAddresses[0].address,
amount: 500,
@ -2312,7 +2312,7 @@ describe('Copay server', function() {
txid: '1122334455',
confirmations: 1,
fees: 5460,
minedTs: 1,
time: 1,
inputs: [{
address: tx.inputs[0].address,
amount: utxos[0].satoshis,
@ -2345,5 +2345,66 @@ describe('Copay server', function() {
});
});
});
it('should get various paginated tx history', function(done) {
var testCases = [{
opts: {
minTs: 15,
maxTs: 45,
},
expected: [20, 30, 40],
}, {
opts: {
minTs: 15,
maxTs: 45,
limit: 2,
},
expected: [20, 30],
}, {
opts: {
maxTs: 35,
},
expected: [10, 20, 30],
}, {
opts: {
minTs: 15,
},
expected: [20, 30, 40, 50],
}, {
opts: {
minTs: 15,
limit: 3,
},
expected: [20, 30, 40],
}];
server._normalizeTxHistory = sinon.stub().returnsArg(0);
var timestamps = [10, 50, 30, 40, 20];
var txs = _.map(timestamps, function(ts, idx) {
return {
txid: (idx + 1).toString(),
confirmations: ts / 10,
fees: 100,
time: ts,
inputs: [{
address: 'external',
amount: 500,
}],
outputs: [{
address: mainAddresses[0].address,
amount: 200,
}],
};
});
helpers.stubHistory(txs);
async.each(testCases, function(testCase, next) {
server.getTxHistory(testCase.opts, function(err, txs) {
should.not.exist(err);
should.exist(txs);
_.pluck(txs, 'time').should.deep.equal(testCase.expected);
next();
});
}, done);
});
});
});