tx history
This commit is contained in:
parent
5e73aa6f2f
commit
de3eddfe39
140
lib/server.js
140
lib/server.js
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue