From 5e73aa6f2f07a142ded17871060eafe93fd968f1 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sat, 21 Feb 2015 19:24:41 -0300 Subject: [PATCH 01/10] do not include change addresses in #getAddresses by default --- lib/server.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/server.js b/lib/server.js index f114052..7a8d1d9 100644 --- a/lib/server.js +++ b/lib/server.js @@ -289,11 +289,11 @@ WalletService.prototype.getMainAddresses = function(opts, cb) { self.storage.fetchAddresses(self.walletId, function(err, addresses) { if (err) return cb(err); - var mainAddresses = _.filter(addresses, { - isChange: false - }); - return cb(null, mainAddresses); + var onlyMain = _.reject(addresses, { + isChange: true + }); + return cb(null, onlyMain); }); }; From de3eddfe392e8a33c20f71df325646c4bc3706e1 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sat, 21 Feb 2015 22:35:12 -0300 Subject: [PATCH 02/10] tx history --- lib/server.js | 140 ++++++++++++++++++++++- test/integration/server.js | 220 +++++++++++++++++++++++++++++++++---- test/testdata.js | 76 +++++++++++++ 3 files changed, 412 insertions(+), 24 deletions(-) diff --git a/lib/server.js b/lib/server.js index 7a8d1d9..af8c9fb 100644 --- a/lib/server.js +++ b/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; diff --git a/test/integration/server.js b/test/integration/server.js index d090af4..a60bd5e 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -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(); + }); + }); + }); + }); + }); + }); }); diff --git a/test/testdata.js b/test/testdata.js index fac1b1b..76e4ecc 100644 --- a/test/testdata.js +++ b/test/testdata.js @@ -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; From d2a1c668a437f9c676b45647d555837456fcaa44 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sat, 21 Feb 2015 23:42:14 -0300 Subject: [PATCH 03/10] tests --- test/integration/server.js | 148 +++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 82 deletions(-) diff --git a/test/integration/server.js b/test/integration/server.js index a60bd5e..a48a4f7 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -94,7 +94,7 @@ helpers.toSatoshi = function(btc) { helpers.stubUtxos = function(server, wallet, amounts, cb) { var amounts = [].concat(amounts); - async.map(_.range(Math.ceil(amounts.length / 2)), function(i, next) { + async.map(_.range(1, Math.ceil(amounts.length / 2) + 1), function(i, next) { server.createAddress({}, function(err, address) { next(err, address); }); @@ -669,9 +669,7 @@ describe('Copay server', function() { helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; - server.createAddress({}, function(err, address) { - done(); - }); + done(); }); }); @@ -680,7 +678,7 @@ describe('Copay server', function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', TestData.copayers[0].privKey); server.createTx(txOpts, function(err, tx) { should.not.exist(err); - tx.should.exist; + should.exist(tx); tx.message.should.equal('some message'); tx.isAccepted().should.equal.false; tx.isRejected().should.equal.false; @@ -810,11 +808,11 @@ describe('Copay server', function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, null, TestData.copayers[0].privKey); server.createTx(txOpts, function(err, tx) { should.not.exist(err); - tx.should.exist; + should.exist(tx); var txOpts2 = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 8, null, TestData.copayers[0].privKey); server.createTx(txOpts2, function(err, tx) { should.not.exist(err); - tx.should.exist; + should.exist(tx); server.getPendingTxs({}, function(err, txs) { should.not.exist(err); txs.length.should.equal(2); @@ -835,7 +833,7 @@ describe('Copay server', function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, null, TestData.copayers[0].privKey); server.createTx(txOpts, function(err, tx) { should.not.exist(err); - tx.should.exist; + should.exist(tx); var txOpts2 = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 24, null, TestData.copayers[0].privKey); server.createTx(txOpts2, function(err, tx) { err.code.should.equal('INSUFFICIENTFUNDS'); @@ -858,9 +856,7 @@ describe('Copay server', function() { it('should create tx using different UTXOs for simultaneous requests', function(done) { var N = 5; - helpers.stubUtxos(server, wallet, _.times(N, function() { - return 100; - }), function(utxos) { + helpers.stubUtxos(server, wallet, _.range(100, 100 + N, 0), function(utxos) { server.getBalance({}, function(err, balance) { should.not.exist(err); balance.totalAmount.should.equal(helpers.toSatoshi(N * 100)); @@ -896,15 +892,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); - tx.should.exist; - 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(); }); }); }); @@ -1178,11 +1172,9 @@ 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() { - helpers.stubBroadcast('999'); - done(); - }); + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + helpers.stubBroadcast('999'); + done(); }); }); }); @@ -1378,17 +1370,15 @@ 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(10), function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, null, TestData.copayers[0].privKey); - async.eachSeries(_.range(10), function(i, next) { - clock.tick(10000); - server.createTx(txOpts, function(err, tx) { - next(); - }); - }, function(err) { - return done(err); + helpers.stubUtxos(server, wallet, _.range(10), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, null, TestData.copayers[0].privKey); + async.eachSeries(_.range(10), function(i, next) { + clock.tick(10000); + server.createTx(txOpts, function(err, tx) { + next(); }); + }, function(err) { + return done(err); }); }); }); @@ -1464,17 +1454,15 @@ describe('Copay server', function() { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; - server.createAddress({}, function(err, address) { - helpers.stubUtxos(server, wallet, helpers.toSatoshi(_.range(4)), function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, null, TestData.copayers[0].privKey); - async.eachSeries(_.range(3), function(i, next) { - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - next(); - }); - }, function(err) { - return done(err); + helpers.stubUtxos(server, wallet, helpers.toSatoshi(_.range(4)), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, null, TestData.copayers[0].privKey); + async.eachSeries(_.range(3), function(i, next) { + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + next(); }); + }, function(err) { + return done(err); }); }); }); @@ -1598,18 +1586,16 @@ describe('Copay server', function() { server = s; wallet = w; - server.createAddress({}, function(err, address) { - helpers.stubUtxos(server, wallet, _.range(2), function() { - var txOpts = { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: helpers.toSatoshi(0.1), - }; - async.eachSeries(_.range(2), function(i, next) { - server.createTx(txOpts, function(err, tx) { - next(); - }); - }, done); - }); + helpers.stubUtxos(server, wallet, _.range(2), function() { + var txOpts = { + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(0.1), + }; + async.eachSeries(_.range(2), function(i, next) { + server.createTx(txOpts, function(err, tx) { + next(); + }); + }, done); }); }); }); @@ -1647,27 +1633,25 @@ describe('Copay server', function() { server = s; wallet = w; - server.createAddress({}, function(err, address) { - helpers.stubUtxos(server, wallet, _.range(2), function() { - var txOpts = { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: helpers.toSatoshi(0.1), - }; - async.eachSeries(_.range(2), function(i, next) { - server.createTx(txOpts, function(err, tx) { - next(); - }); - }, function() { - server.removeWallet({}, function(err) { - db = []; - server.storage._dump(function() { - var after = _.clone(db); - after.should.deep.equal(before); - done(); - }, cat); - }); - }, cat); - }); + helpers.stubUtxos(server, wallet, _.range(2), function() { + var txOpts = { + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(0.1), + }; + async.eachSeries(_.range(2), function(i, next) { + server.createTx(txOpts, function(err, tx) { + next(); + }); + }, function() { + server.removeWallet({}, function(err) { + db = []; + server.storage._dump(function() { + var after = _.clone(db); + after.should.deep.equal(before); + done(); + }, cat); + }); + }, cat); }); }); }, cat); @@ -1763,7 +1747,7 @@ describe('Copay server', function() { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; - helpers.createAddresses(server, wallet, 3, 3, function(main, change) { + helpers.createAddresses(server, wallet, 1, 1, function(main, change) { mainAddresses = main; changeAddresses = change; done(); @@ -1867,14 +1851,14 @@ describe('Copay server', function() { done(); }); }); - it('should get tx history for outgoing txs with proposal', function(done) { + it('should get tx history with accepted 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; + should.exist(tx); helpers.stubBroadcast('1122334455'); var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); @@ -1909,7 +1893,7 @@ describe('Copay server', function() { var tx = txs[0]; tx.action.should.equal('sent'); tx.amount.should.equal(helpers.toSatoshi(80)); - tx.message.should.equal(tx.message); + tx.message.should.equal('some message'); tx.actions.length.should.equal(1); tx.actions[0].type.should.equal('accept'); tx.actions[0].copayerName.should.equal('copayer 1'); From 036cc88ba8617a6ec7bc849ea9bfe7c6b9aefb2a Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 22 Feb 2015 00:12:05 -0300 Subject: [PATCH 04/10] add client api for history --- lib/client/api.js | 16 ++++++++++++++++ lib/expressapp.js | 11 +++++++++++ lib/server.js | 5 ++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/client/api.js b/lib/client/api.js index db61edb..5d1b14a 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -768,4 +768,20 @@ API.prototype.removeTxProposal = function(txp, cb) { }); }; +API.prototype.getTxHistory = function(opts, cb) { + var self = this; + + this._loadAndCheck(function(err, data) { + if (err) return cb(err); + var url = '/v1/txhistory/'; + self._doGetRequest(url, data, function(err, txs) { + if (err) return cb(err); + + _processTxps(txs, data.sharedEncryptingKey); + + return cb(null, txs); + }); + }); +}; + module.exports = API; diff --git a/lib/expressapp.js b/lib/expressapp.js index 083ab5b..6e88da0 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -263,6 +263,17 @@ ExpressApp.start = function(opts) { }); }); + router.get('/v1/txhistory/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.getTxHistory({}, function(err, txs) { + if (err) return returnError(err, res, req); + res.json(txs); + }); + }); + }); + + + // TODO: DEBUG only! router.get('/v1/dump', function(req, res) { var server = WalletService.getInstance(); diff --git a/lib/server.js b/lib/server.js index af8c9fb..e211872 100644 --- a/lib/server.js +++ b/lib/server.js @@ -945,7 +945,9 @@ WalletService.prototype.getTxHistory = function(opts, cb) { }); }; - + function paginate(txs) { + // TODO + }; // Get addresses for this wallet self.storage.fetchAddresses(self.walletId, function(err, addresses) { @@ -978,6 +980,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { var txs = res[1]; decorate(txs, addresses, proposals); + paginate(txs); return cb(null, txs); }); From 3b83dc095f5efdeb7504f500c3355034fcb72f8b Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 22 Feb 2015 23:26:21 -0300 Subject: [PATCH 05/10] bit-history --- bit-wallet/bit | 1 + bit-wallet/bit-history | 38 ++++++++++++++++++++++++++++++++++++++ lib/client/api.js | 9 ++++----- lib/expressapp.js | 1 + lib/server.js | 27 ++++++++++++++++++++++----- package.json | 1 + 6 files changed, 67 insertions(+), 10 deletions(-) create mode 100755 bit-wallet/bit-history diff --git a/bit-wallet/bit b/bit-wallet/bit index fbd840b..c4607a6 100755 --- a/bit-wallet/bit +++ b/bit-wallet/bit @@ -15,6 +15,7 @@ program .command('reject [reason]', 'reject a transaction proposal') .command('broadcast ', 'broadcast a transaction proposal to the Bitcoin network') .command('rm ', 'remove a transaction proposal') + .command('history', 'list of past incoming and outgoing transactions') .command('export', 'export wallet critical data') .command('import', 'import wallet critical data') .command('confirm', 'show copayer\'s data for confirmation') diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history new file mode 100755 index 0000000..b00c101 --- /dev/null +++ b/bit-wallet/bit-history @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +var _ = require('lodash'); +var fs = require('fs'); +var moment = require('moment'); +var program = require('commander'); +var Utils = require('./cli-utils'); +program = Utils.configureCommander(program); + +program + .parse(process.argv); + +var args = program.args; +var client = Utils.getClient(program); + +var txData; + +client.getTxHistory({}, function (err, txs) { + if (_.isEmpty(txs)) + return; + + console.log("* TX History:") + + _.each(txs, function(tx) { + var time = moment().fromNow(tx.time); + switch (tx.action) { + case 'received': + console.log("\t%s: <= %s", time, Utils.renderAmount(tx.amount)); + break; + case 'sent': + console.log("\t%s: %s => %s", time, Utils.renderAmount(tx.amount), tx.addressTo); + break; + case 'moved': + console.log("\t%s: == %s", time, Utils.renderAmount(tx.amount)); + break; + } + }); +}); diff --git a/lib/client/api.js b/lib/client/api.js index 5d1b14a..2ddc878 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -36,6 +36,7 @@ function _decryptMessage(message, encryptingKey) { }; function _processTxps(txps, encryptingKey) { + if (!txps) return; _.each([].concat(txps), function(txp) { txp.encryptedMessage = txp.message; txp.message = _decryptMessage(txp.message, encryptingKey); @@ -482,10 +483,6 @@ API.prototype.getMainAddresses = function(opts, cb) { }); }; -API.prototype.history = function(limit, cb) { - -}; - API.prototype.getBalance = function(cb) { var self = this; @@ -591,8 +588,10 @@ API.prototype.parseTxProposals = function(txData, cb) { }, function(err, wcd) { if (err) return cb(err); + << << << < HEAD var txps = txData.txps; - _processTxps(txps, wcd.sharedEncryptingKey); + _processTxps(txps, wcd.sharedEncryptingKey); === === = + _processTxps(txps, data.sharedEncryptingKey); >>> >>> > bit - history var fake = _.any(txps, function(txp) { return (!Verifier.checkTxProposal(wcd, txp)); diff --git a/lib/expressapp.js b/lib/expressapp.js index 6e88da0..24458ce 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -268,6 +268,7 @@ ExpressApp.start = function(opts) { server.getTxHistory({}, function(err, txs) { if (err) return returnError(err, res, req); res.json(txs); + res.end(); }); }); }); diff --git a/lib/server.js b/lib/server.js index e211872..f0a2619 100644 --- a/lib/server.js +++ b/lib/server.js @@ -80,8 +80,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) { if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found')); var pubKey = opts.readOnly ? copayer.roPubKey : copayer.rwPubKey; - var isValid = server._verifySignature(opts.message, opts.signature, - pubKey); + var isValid = server._verifySignature(opts.message, opts.signature, pubKey); if (!isValid) return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature')); @@ -324,6 +323,20 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { WalletService.prototype._getBlockExplorer = function(provider, network) { var url; + function getTransactionsInsight(url, addresses, cb) { + var request = require('request'); + request({ + method: "POST", + url: url + '/api/addrs/txs', + json: { + addrs: [].concat(addresses).join(',') + } + }, function(err, res, body) { + if (err || res.statusCode != 200) return cb(err || res); + return cb(null, body); + }); + }; + if (this.blockExplorer) return this.blockExplorer; @@ -339,7 +352,9 @@ WalletService.prototype._getBlockExplorer = function(provider, network) { url = 'https://test-insight.bitpay.com:443' break; } - return new Explorers.Insight(url, network); + var bc = new Explorers.Insight(url, network); + bc.getTransactions = _.bind(getTransactionsInsight, bc, url); + return bc; break; } }; @@ -874,7 +889,7 @@ WalletService.prototype._normalizeTxHistory = function(txs) { txid: tx.txid, confirmations: tx.confirmations, fees: parseInt((tx.fees * 1e8).toFixed(0)), - minedTs: !_.isNaN(tx.time) ? tx.time * 1000 : undefined, + time: !_.isNaN(tx.time) ? tx.time : Math.floor(Date.now() / 1000), inputs: inputs, outputs: outputs, }; @@ -937,7 +952,9 @@ WalletService.prototype.getTxHistory = function(opts, cb) { var proposal = indexedProposals[tx.txid]; if (proposal) { tx.message = proposal.message; - tx.actions = proposal.actions; + tx.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; diff --git a/package.json b/package.json index 7671316..4f5841e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "levelup": "^0.19.0", "lodash": "*", "mocha-lcov-reporter": "0.0.1", + "moment": "^2.9.0", "morgan": "*", "npmlog": "^0.1.1", "preconditions": "^1.0.7", From 8605805a30a2ea57ebe03ccb2806c2cedff3c6e1 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 22 Feb 2015 23:36:00 -0300 Subject: [PATCH 06/10] remove redundancy --- bit-wallet/bit-history | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history index b00c101..8d86a2e 100755 --- a/bit-wallet/bit-history +++ b/bit-wallet/bit-history @@ -23,15 +23,16 @@ client.getTxHistory({}, function (err, txs) { _.each(txs, function(tx) { var time = moment().fromNow(tx.time); + var amount = Utils.renderAmount(tx.amount); switch (tx.action) { case 'received': - console.log("\t%s: <= %s", time, Utils.renderAmount(tx.amount)); + console.log("\t%s: <= %s", time, amount); break; case 'sent': - console.log("\t%s: %s => %s", time, Utils.renderAmount(tx.amount), tx.addressTo); + console.log("\t%s: %s => %s", time, amount, tx.addressTo); break; case 'moved': - console.log("\t%s: == %s", time, Utils.renderAmount(tx.amount)); + console.log("\t%s: == %s", time, amount); break; } }); From 632913c561e6597a3ddec7d817fe59c90d4fb3ee Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 23 Feb 2015 11:11:16 -0300 Subject: [PATCH 07/10] fix tx date --- bit-wallet/bit-history | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history index 8d86a2e..0680b99 100755 --- a/bit-wallet/bit-history +++ b/bit-wallet/bit-history @@ -22,7 +22,7 @@ client.getTxHistory({}, function (err, txs) { console.log("* TX History:") _.each(txs, function(tx) { - var time = moment().fromNow(tx.time); + var time = moment(tx.time * 1000).fromNow(); var amount = Utils.renderAmount(tx.amount); switch (tx.action) { case 'received': From 63ac70b50f4ba406fbded01d71a113a356158500 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 23 Feb 2015 12:37:37 -0300 Subject: [PATCH 08/10] better output --- bit-wallet/bit-history | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history index 0680b99..e372cf9 100755 --- a/bit-wallet/bit-history +++ b/bit-wallet/bit-history @@ -24,16 +24,19 @@ client.getTxHistory({}, function (err, txs) { _.each(txs, function(tx) { var time = moment(tx.time * 1000).fromNow(); var amount = Utils.renderAmount(tx.amount); + var confirmations = tx.confirmations || 0; switch (tx.action) { case 'received': - console.log("\t%s: <= %s", time, amount); - break; - case 'sent': - console.log("\t%s: %s => %s", time, amount, tx.addressTo); + direction = '<='; break; case 'moved': - console.log("\t%s: == %s", time, amount); + direction = '=='; + break; + case 'sent': + direction = '=>'; break; } + + console.log("\t%s: %s %s %s (%s confirmations)", time, direction, tx.action, amount, confirmations); }); }); From c42205c1de0b00fa8a4d9843f7e784a30457a080 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 23 Feb 2015 17:15:38 -0300 Subject: [PATCH 09/10] add proposal info --- bit-wallet/bit-history | 3 ++- lib/server.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bit-wallet/bit-history b/bit-wallet/bit-history index e372cf9..7f250c2 100755 --- a/bit-wallet/bit-history +++ b/bit-wallet/bit-history @@ -25,6 +25,7 @@ client.getTxHistory({}, function (err, txs) { var time = moment(tx.time * 1000).fromNow(); var amount = Utils.renderAmount(tx.amount); var confirmations = tx.confirmations || 0; + var proposal = tx.proposalId ? '["' + tx.message + '" by ' + tx.creatorName + '] ' : ''; switch (tx.action) { case 'received': direction = '<='; @@ -37,6 +38,6 @@ client.getTxHistory({}, function (err, txs) { break; } - console.log("\t%s: %s %s %s (%s confirmations)", time, direction, tx.action, amount, confirmations); + console.log("\t%s: %s %s %s %s(%s confirmations)", time, direction, tx.action, amount, proposal, confirmations); }); }); diff --git a/lib/server.js b/lib/server.js index f0a2619..a93bef0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -951,6 +951,8 @@ WalletService.prototype.getTxHistory = function(opts, cb) { 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) { return _.pick(action, ['createdOn', 'type', 'copayerId', 'copayerName', 'comment']); From cb576baef014e7abe141631e9bc8a377eb4c1e56 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 23 Feb 2015 17:18:35 -0300 Subject: [PATCH 10/10] rebase --- lib/client/api.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 2ddc878..ef4d4e6 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -588,10 +588,8 @@ API.prototype.parseTxProposals = function(txData, cb) { }, function(err, wcd) { if (err) return cb(err); - << << << < HEAD var txps = txData.txps; - _processTxps(txps, wcd.sharedEncryptingKey); === === = - _processTxps(txps, data.sharedEncryptingKey); >>> >>> > bit - history + _processTxps(txps, wcd.sharedEncryptingKey); var fake = _.any(txps, function(txp) { return (!Verifier.checkTxProposal(wcd, txp));