tx history

This commit is contained in:
Ivan Socolsky 2015-02-21 22:35:12 -03:00
parent 5e73aa6f2f
commit de3eddfe39
3 changed files with 412 additions and 24 deletions

View File

@ -797,7 +797,7 @@ WalletService.prototype.rejectTx = function(opts, cb) {
};
/**
* Retrieves all pending transaction proposals.
* Retrieves pending transaction proposals.
* @param {Object} opts
* @returns {TxProposal[]} Transaction proposal.
*/
@ -812,7 +812,7 @@ WalletService.prototype.getPendingTxs = function(opts, cb) {
};
/**
* Retrieves pending transaction proposals in the range (maxTs-minTs)
* Retrieves all transaction proposals in the range (maxTs-minTs)
* Times are in UNIX EPOCH
*
* @param {Object} opts.minTs (defaults to 0)
@ -830,7 +830,7 @@ WalletService.prototype.getTxs = function(opts, cb) {
/**
* Retrieves notifications in the range (maxTs-minTs).
* Retrieves notifications in the range (maxTs-minTs).
* Times are in UNIX EPOCH. Order is assured even for events with the same time
*
* @param {Object} opts.minTs (defaults to 0)
@ -848,7 +848,141 @@ WalletService.prototype.getNotifications = function(opts, cb) {
};
WalletService.prototype._normalizeTxHistory = function(txs) {
return _.map(txs, function(tx) {
var inputs = _.map(tx.vin, function(item) {
return {
address: item.addr,
amount: item.valueSat,
}
});
var outputs = _.map(tx.vout, function(item) {
var itemAddr;
// If classic multisig, ignore
if (item.scriptPubKey && item.scriptPubKey.addresses.length == 1) {
itemAddr = item.scriptPubKey.addresses[0];
}
return {
address: itemAddr,
amount: parseInt((item.value * 1e8).toFixed(0)),
}
});
return {
txid: tx.txid,
confirmations: tx.confirmations,
fees: parseInt((tx.fees * 1e8).toFixed(0)),
minedTs: !_.isNaN(tx.time) ? tx.time * 1000 : undefined,
inputs: inputs,
outputs: outputs,
};
});
};
/**
* 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
* @returns {TxProposal[]} Transaction proposals, first newer
*/
WalletService.prototype.getTxHistory = function(opts, cb) {
var self = this;
function decorate(txs, addresses, proposals) {
function sum(items, isMine, isChange) {
var filter = {};
if (_.isBoolean(isMine)) filter.isMine = isMine;
if (_.isBoolean(isChange)) filter.isChange = isChange;
return _.reduce(_.where(items, filter),
function(memo, item) {
return memo + item.amount;
}, 0);
};
var indexedAddresses = _.indexBy(addresses, 'address');
var indexedProposals = _.indexBy(proposals, 'txid');
_.each(txs, function(tx) {
_.each(tx.inputs.concat(tx.outputs), 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';
}
tx.amount = Math.abs(amount);
if (tx.action == 'sent' || tx.action == 'moved') {
tx.addressTo = tx.outputs[0].address;
};
delete tx.inputs;
delete tx.outputs;
var proposal = indexedProposals[tx.txid];
if (proposal) {
tx.message = proposal.message;
tx.actions = proposal.actions;
// tx.sentTs = proposal.sentTs;
// tx.merchant = proposal.merchant;
//tx.paymentAckMemo = proposal.paymentAckMemo;
}
});
};
// Get addresses for this wallet
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
if (addresses.length == 0) return cb(null, []);
var addressStrs = _.pluck(addresses, 'address');
var networkName = Bitcore.Address(addressStrs[0]).toObject().network;
var bc = self._getBlockExplorer('insight', networkName);
async.parallel([
function(next) {
self.storage.fetchTxs(self.walletId, opts, function(err, txps) {
if (err) return next(err);
next(null, txps);
});
},
function(next) {
bc.getTransactions(addressStrs, function(err, txs) {
if (err) return next(err);
next(null, self._normalizeTxHistory(txs));
});
},
], function(err, res) {
if (err) return cb(err);
var proposals = res[0];
var txs = res[1];
decorate(txs, addresses, proposals);
return cb(null, txs);
});
});
};
module.exports = WalletService;

View File

@ -2,12 +2,15 @@
var _ = require('lodash');
var async = require('async');
var inspect = require('util').inspect;
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var levelup = require('levelup');
var memdown = require('memdown');
var log = require('npmlog');
log.debug = log.verbose;
var Bitcore = require('bitcore');
var Utils = require('../../lib/utils');
@ -126,6 +129,9 @@ helpers.stubBroadcastFail = function() {
blockExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
};
helpers.stubHistory = function(txs) {
blockExplorer.getTransactions = sinon.stub().callsArgWith(1, null, txs);
};
helpers.clientSign = function(txp, xprivHex) {
//Derive proper key to sign, for each input
@ -175,6 +181,19 @@ helpers.createProposalOpts = function(toAddress, amount, message, signingKey) {
return opts;
};
helpers.createAddresses = function(server, wallet, main, change, cb) {
async.map(_.range(main + change), function(i, next) {
var address = wallet.createAddress(i >= main);
server.storage.storeAddressAndWallet(wallet, address, function(err) {
if (err) return next(err);
next(null, address);
});
}, function(err, addresses) {
if (err) throw new Error('Could not generate addresses');
return cb(_.take(addresses, main), _.takeRight(addresses, change));
});
};
var db, storage, blockExplorer;
@ -926,15 +945,13 @@ describe('Copay server', function() {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, address) {
helpers.stubUtxos(server, wallet, _.range(1, 9), function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
txid = tx.id;
done();
});
helpers.stubUtxos(server, wallet, _.range(1, 9), function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
txid = tx.id;
done();
});
});
});
@ -1084,10 +1101,8 @@ describe('Copay server', function() {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, address) {
helpers.stubUtxos(server, wallet, _.range(1, 9), function() {
done();
});
helpers.stubUtxos(server, wallet, _.range(1, 9), function() {
done();
});
});
});
@ -1666,14 +1681,12 @@ describe('Copay server', function() {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, address) {
helpers.stubUtxos(server, wallet, [100, 200], function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
server.getPendingTxs({}, function(err, txs) {
txp = txs[0];
done();
});
helpers.stubUtxos(server, wallet, [100, 200], function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
server.getPendingTxs({}, function(err, txs) {
txp = txs[0];
done();
});
});
});
@ -1742,4 +1755,169 @@ describe('Copay server', function() {
});
});
});
describe('#getTxHistory', function() {
var server, wallet, mainAddresses, changeAddresses;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
helpers.createAddresses(server, wallet, 3, 3, function(main, change) {
mainAddresses = main;
changeAddresses = change;
done();
});
});
});
it('should get tx history from insight', function(done) {
helpers.stubHistory(TestData.history);
server.getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(2);
done();
});
});
it('should get tx history for incoming txs', function(done) {
server._normalizeTxHistory = sinon.stub().returnsArg(0);
var txs = [{
txid: '1',
confirmations: 1,
fees: 100,
minedTs: 1,
inputs: [{
address: 'external',
amount: 500,
}],
outputs: [{
address: mainAddresses[0].address,
amount: 200,
}],
}];
helpers.stubHistory(txs);
server.getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(1);
var tx = txs[0];
tx.action.should.equal('received');
tx.amount.should.equal(200);
tx.fees.should.equal(100);
done();
});
});
it('should get tx history for outgoing txs', function(done) {
server._normalizeTxHistory = sinon.stub().returnsArg(0);
var txs = [{
txid: '1',
confirmations: 1,
fees: 100,
minedTs: 1,
inputs: [{
address: mainAddresses[0].address,
amount: 500,
}],
outputs: [{
address: 'external',
amount: 400,
}],
}];
helpers.stubHistory(txs);
server.getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(1);
var tx = txs[0];
tx.action.should.equal('sent');
tx.amount.should.equal(400);
tx.fees.should.equal(100);
done();
});
});
it('should get tx history for outgoing txs + change', function(done) {
server._normalizeTxHistory = sinon.stub().returnsArg(0);
var txs = [{
txid: '1',
confirmations: 1,
fees: 100,
minedTs: 1,
inputs: [{
address: mainAddresses[0].address,
amount: 500,
}],
outputs: [{
address: 'external',
amount: 300,
}, {
address: changeAddresses[0].address,
amount: 100,
}],
}];
helpers.stubHistory(txs);
server.getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(1);
var tx = txs[0];
tx.action.should.equal('sent');
tx.amount.should.equal(300);
tx.fees.should.equal(100);
done();
});
});
it('should get tx history for outgoing txs with proposal', function(done) {
server._normalizeTxHistory = sinon.stub().returnsArg(0);
helpers.stubUtxos(server, wallet, [100, 200], function(utxos) {
var txOpts = helpers.createProposalOpts(mainAddresses[0].address, 80, 'some message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
tx.should.exist;
helpers.stubBroadcast('1122334455');
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey);
server.signTx({
txProposalId: tx.id,
signatures: signatures,
}, function(err, tx) {
should.not.exist(err);
var txs = [{
txid: '1122334455',
confirmations: 1,
fees: 5460,
minedTs: 1,
inputs: [{
address: tx.inputs[0].address,
amount: utxos[0].satoshis,
}],
outputs: [{
address: 'external',
amount: helpers.toSatoshi(80) - 5460,
}, {
address: changeAddresses[0].address,
amount: helpers.toSatoshi(20) - 5460,
}],
}];
helpers.stubHistory(txs);
server.getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(1);
var tx = txs[0];
tx.action.should.equal('sent');
tx.amount.should.equal(helpers.toSatoshi(80));
tx.message.should.equal(tx.message);
tx.actions.length.should.equal(1);
tx.actions[0].type.should.equal('accept');
tx.actions[0].copayerName.should.equal('copayer 1');
done();
});
});
});
});
});
});
});

View File

@ -62,6 +62,82 @@ var copayers = [{
}, ];
var history = [{
txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04",
vin: [{
txid: "c8e221141e8bb60977896561b77fa59d6dacfcc10db82bf6f5f923048b11c70d",
vout: 0,
n: 0,
addr: "2N6Zutg26LEC4iYVxi7SHhopVLP1iZPU1rZ",
valueSat: 485645,
value: 0.00485645,
}, {
txid: "6e599eea3e2898b91087eb87e041c5d8dec5362447a8efba185ed593f6dc64c0",
vout: 1,
n: 1,
addr: "2MyqmcWjmVxW8i39wdk1CVPdEqKyFSY9H1S",
valueSat: 885590,
value: 0.0088559,
}],
vout: [{
value: "0.00045753",
n: 0,
scriptPubKey: {
addresses: [
"2NAVFnsHqy5JvqDJydbHPx393LFqFFBQ89V"
]
},
}, {
value: "0.01300000",
n: 1,
scriptPubKey: {
addresses: [
"mq4D3Va5mYHohMEHrgHNGzCjKhBKvuEhPE"
]
}
}],
confirmations: 2,
time: 1424471041,
blocktime: 1424471041,
valueOut: 0.01345753,
valueIn: 0.01371235,
fees: 0.00025482
}, {
txid: "fad88682ccd2ff34cac6f7355fe9ecd8addd9ef167e3788455972010e0d9d0de",
vin: [{
txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04",
vout: 0,
n: 0,
addr: "2NAVFnsHqy5JvqDJydbHPx393LFqFFBQ89V",
valueSat: 45753,
value: 0.00045753,
}],
vout: [{
value: "0.00011454",
n: 0,
scriptPubKey: {
addresses: [
"2N7GT7XaN637eBFMmeczton2aZz5rfRdZso"
]
}
}, {
value: "0.00020000",
n: 1,
scriptPubKey: {
addresses: [
"mq4D3Va5mYHohMEHrgHNGzCjKhBKvuEhPE"
]
}
}],
confirmations: 1,
time: 1424472242,
blocktime: 1424472242,
valueOut: 0.00031454,
valueIn: 0.00045753,
fees: 0.00014299
}];
module.exports.keyPair = keyPair;
module.exports.message = message;
module.exports.copayers = copayers;
module.exports.history = history;