From 4d1cb07a7925bfa9e030e6450f82440b4ab46563 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 23 Feb 2015 18:31:27 -0300 Subject: [PATCH 01/15] propose server tests --- lib/server.js | 6 +++--- test/integration/server.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index 29b620b..0b43c3c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -617,7 +617,7 @@ WalletService.prototype.removePendingTx = function(opts, cb) { if (err) return cb(err); if (!txp.isPending()) - return cb(new ClientError('Transaction proposal not pending')); + return cb(new ClientError('TXNOTPENDING', 'Transaction proposal not pending')); if (txp.creatorId !== self.copayerId) @@ -626,7 +626,7 @@ WalletService.prototype.removePendingTx = function(opts, cb) { var actors = txp.getActors(); if (actors.length > 1 || (actors.length == 1 && actors[0] !== self.copayerId)) - return cb(new ClientError('Cannot remove a proposal signed/rejected by other copayers')); + return cb(new ClientError('TXACTIONED', 'Cannot remove a proposal signed/rejected by other copayers')); self._notify('transactionProposalRemoved'); self.storage.removeTx(self.walletId, txp.id, cb); @@ -673,7 +673,7 @@ WalletService.prototype.signTx = function(opts, cb) { }); if (action) return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal')); - if (txp.status != 'pending') + if (!txp.isPending()) return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending')); var copayer = wallet.getCopayer(self.copayerId); diff --git a/test/integration/server.js b/test/integration/server.js index 4373b7a..4c55b6a 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -345,6 +345,8 @@ describe('Copay server', function() { done(); }); }); + + it.skip('should fail to create wallet with invalid pubKey argument', function(done) {}); }); describe('#joinWallet', function() { @@ -451,6 +453,8 @@ describe('Copay server', function() { }); }); + it.skip('should fail two wallets with same xPubKey', function(done) {}); + it('should fail to join with bad formated signature', function(done) { var copayerOpts = { walletId: walletId, @@ -758,6 +762,8 @@ describe('Copay server', function() { }); }); + it.skip('should fail to create tx for invalid amount', function(done) {}); + it('should fail to create tx when insufficient funds', function(done) { helpers.stubUtxos(server, wallet, [100], function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 120, null, TestData.copayers[0].privKey); @@ -803,6 +809,8 @@ describe('Copay server', function() { }); }); + it.skip('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {}); + it('should create tx when there is a pending tx and enough UTXOs', function(done) { helpers.stubUtxos(server, wallet, [10.1, 10.2, 10.3], function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, null, TestData.copayers[0].privKey); @@ -930,6 +938,8 @@ describe('Copay server', function() { }); }); }); + + it.skip('should fail to reject non-pending TX', function(done) {}); }); describe('#signTx', function() { @@ -1087,6 +1097,17 @@ describe('Copay server', function() { }); }); + describe.skip('#broadcastTx', function() { + it.skip('should keep tx as accepted if unable to broadcast it', function(done) {}); + it.skip('should brodcast a tx', function(done) { + // TODO: check final status == 'broadcasted' & broadcastedOn + }); + it.skip('should fail to brodcast an already broadcasted tx', function(done) {}); + it.skip('should brodcast a not yet accepted tx', function(done) {}); + it.skip('should brodcast a tx', function(done) {}); + }); + + describe('Tx proposal workflow', function() { var server, wallet; beforeEach(function(done) { @@ -1638,6 +1659,8 @@ describe('Copay server', function() { }); }); + it.skip('should fail to remove non-pending TX', function(done) {}); + it('should not allow non-creator copayer to remove an unsigned TX ', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { server2.removePendingTx({ @@ -1663,6 +1686,7 @@ describe('Copay server', function() { server.removePendingTx({ txProposalId: txp.id }, function(err) { + err.code.should.equal('TXACTIONED'); err.message.should.contain('other copayers'); done(); }); From 66e173be43c926843fac7d63f4a936cf52ba6d9b Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 24 Feb 2015 10:36:14 -0300 Subject: [PATCH 02/15] tests --- lib/server.js | 10 +- test/integration/server.js | 265 +++++++++++++++++++++++++++++++++---- 2 files changed, 244 insertions(+), 31 deletions(-) diff --git a/lib/server.js b/lib/server.js index 0b43c3c..ee5b583 100644 --- a/lib/server.js +++ b/lib/server.js @@ -119,8 +119,8 @@ WalletService.prototype.createWallet = function(opts, cb) { try { pubKey = new PublicKey.fromString(opts.pubKey); - } catch (e) { - return cb(e.toString()); + } catch (ex) { + return cb(new ClientError('Invalid public key')); }; var wallet = Wallet.create({ @@ -512,6 +512,9 @@ WalletService.prototype.createTx = function(opts, cb) { if (toAddress.network != wallet.getNetworkName()) return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); + if (opts.amount <= 0) + return cb(new ClientError('Invalid amount')); + if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); @@ -538,8 +541,7 @@ WalletService.prototype.createTx = function(opts, cb) { try { self._selectUtxos(txp, utxos); } catch (ex) { - console.log('[server.js.523:ex:]', ex); //TODO - return cb(new ClientError(ex)); + return cb(new ClientError(ex.toString())); } if (!txp.inputs) diff --git a/test/integration/server.js b/test/integration/server.js index 4c55b6a..990704d 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -302,7 +302,7 @@ describe('Copay server', function() { }; server.createWallet(opts, function(err, walletId) { should.not.exist(walletId); - err.should.exist; + should.exist(err); err.message.should.contain('name'); done(); }); @@ -346,7 +346,20 @@ describe('Copay server', function() { }); }); - it.skip('should fail to create wallet with invalid pubKey argument', function(done) {}); + it('should fail to create wallet with invalid pubKey argument', function(done) { + var opts = { + name: 'my wallet', + m: 2, + n: 3, + pubKey: 'dummy', + }; + server.createWallet(opts, function(err, walletId) { + should.not.exist(walletId); + should.exist(err); + err.message.should.contain('Invalid public key'); + done(); + }); + }); }); describe('#joinWallet', function() { @@ -399,7 +412,7 @@ describe('Copay server', function() { }; server.joinWallet(copayerOpts, function(err, result) { should.not.exist(result); - err.should.exist; + should.exist(err); err.message.should.contain('name'); done(); }); @@ -453,7 +466,39 @@ describe('Copay server', function() { }); }); - it.skip('should fail two wallets with same xPubKey', function(done) {}); + it('should fail two wallets with same xPubKey', function(done) { + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey, + xPubKeySignature: TestData.copayers[0].xPubKeySignature, + }; + server.joinWallet(copayerOpts, function(err) { + should.not.exist(err); + + var walletOpts = { + name: 'my other wallet', + m: 1, + n: 1, + pubKey: TestData.keyPair.pub, + }; + server.createWallet(walletOpts, function(err, walletId) { + should.not.exist(err); + copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey, + xPubKeySignature: TestData.copayers[0].xPubKeySignature, + }; + server.joinWallet(copayerOpts, function(err) { + should.exist(err); + err.code.should.equal('CREGISTERED'); + err.message.should.equal('Copayer ID already registered on server'); + done(); + }); + }); + }); + }); it('should fail to join with bad formated signature', function(done) { var copayerOpts = { @@ -475,7 +520,7 @@ describe('Copay server', function() { xPubKey: TestData.copayers[0].xPubKey[0], }; server.joinWallet(copayerOpts, function(err) { - err.should.exist; + should.exist(err); err.message.should.contain('argument missing'); done(); }); @@ -583,7 +628,7 @@ describe('Copay server', function() { it('should not create address if unable to store it', function(done) { sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error'); server.createAddress({}, function(err, address) { - err.should.exist; + should.exist(err); should.not.exist(address); server.getMainAddresses({}, function(err, addresses) { @@ -625,7 +670,7 @@ describe('Copay server', function() { helpers.getAuthServer(result.copayerId, function(server) { server.createAddress({}, function(err, address) { should.not.exist(address); - err.should.exist; + should.exist(err); err.message.should.contain('not complete'); done(); }); @@ -656,7 +701,7 @@ describe('Copay server', function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, null, TestData.copayers[0].privKey); server.createTx(txOpts, function(err, tx) { should.not.exist(tx); - err.should.exist; + should.exist(err); err.message.should.contain('not complete'); done(); }); @@ -714,7 +759,7 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, tx) { should.not.exist(tx); - err.should.exist; + should.exist(err); err.message.should.equal('Invalid proposal signature'); done(); }); @@ -727,7 +772,7 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, tx) { should.not.exist(tx); - err.should.exist; + should.exist(err); err.message.should.equal('Invalid proposal signature'); done(); }); @@ -740,7 +785,7 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, tx) { should.not.exist(tx); - err.should.exist; + should.exist(err); err.code.should.equal('INVALIDADDRESS'); err.message.should.equal('Invalid address'); done(); @@ -762,7 +807,15 @@ describe('Copay server', function() { }); }); - it.skip('should fail to create tx for invalid amount', function(done) {}); + it('should fail to create tx for invalid amount', function(done) { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0, null, TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + should.exist(err); + err.message.should.equal('Invalid amount'); + done(); + }); + }); it('should fail to create tx when insufficient funds', function(done) { helpers.stubUtxos(server, wallet, [100], function() { @@ -809,7 +862,22 @@ describe('Copay server', function() { }); }); - it.skip('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {}); + it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) { + helpers.stubUtxos(server, wallet, [10], function() { + var bitcoreStub = sinon.stub(Bitcore, 'Transaction'); + bitcoreStub.throws({ + name: 'dummy', + message: 'dummy exception' + }); + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2, null, TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.message.should.equal('dummy exception'); + bitcoreStub.restore(); + done(); + }); + }); + }); it('should create tx when there is a pending tx and enough UTXOs', function(done) { helpers.stubUtxos(server, wallet, [10.1, 10.2, 10.3], function() { @@ -897,7 +965,7 @@ describe('Copay server', function() { var server, wallet, txid; beforeEach(function(done) { - helpers.createAndJoinWallet(2, 3, function(s, w) { + helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; helpers.stubUtxos(server, wallet, _.range(1, 9), function() { @@ -924,22 +992,63 @@ describe('Copay server', function() { should.not.exist(err); server.getPendingTxs({}, function(err, txs) { should.not.exist(err); - var tx = txs[0]; - tx.id.should.equal(txid); - - var actors = tx.getActors(); - actors.length.should.equal(1); - actors[0].should.equal(wallet.copayers[0].id); - var action = tx.getActionBy(wallet.copayers[0].id); - action.type.should.equal('reject'); - action.comment.should.equal('some reason'); - done(); + txs.should.be.empty; + server.getTx({ + id: txid + }, function(err, tx) { + var actors = tx.getActors(); + actors.length.should.equal(1); + actors[0].should.equal(wallet.copayers[0].id); + var action = tx.getActionBy(wallet.copayers[0].id); + action.type.should.equal('reject'); + action.comment.should.equal('some reason'); + done(); + }); }); }); }); }); - it.skip('should fail to reject non-pending TX', function(done) {}); + it('should fail to reject non-pending TX', function(done) { + async.waterfall([ + + function(next) { + server.getPendingTxs({}, function(err, txs) { + var tx = txs[0]; + tx.id.should.equal(txid); + next(); + }); + }, + function(next) { + server.rejectTx({ + txProposalId: txid, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.should.be.empty; + next(); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server) { + server.rejectTx({ + txProposalId: txid, + reason: 'some other reason', + }, function(err) { + should.exist(err); + err.code.should.equal('TXNOTPENDING'); + done(); + }); + }); + }, + ]); + }); }); describe('#signTx', function() { @@ -1095,6 +1204,57 @@ describe('Copay server', function() { }); }); }); + + it('should fail to sign a non-pending TX', function(done) { + async.waterfall([ + + function(next) { + server.rejectTx({ + txProposalId: txid, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server) { + server.rejectTx({ + txProposalId: txid, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + next(); + }); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.should.be.empty; + next(); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[2].id, function(server) { + server.getTx({ + id: txid + }, function(err, tx) { + should.not.exist(err); + var signatures = helpers.clientSign(tx, TestData.copayers[2].xPrivKey); + server.signTx({ + txProposalId: txid, + signatures: signatures, + }, function(err) { + should.exist(err); + err.code.should.equal('TXNOTPENDING'); + done(); + }); + }); + }); + }, + ]); + }); }); describe.skip('#broadcastTx', function() { @@ -1644,9 +1804,10 @@ describe('Copay server', function() { it('should allow creator to remove an signed TX by himself', function(done) { var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); server.signTx({ - txProposalId: txp[0], + txProposalId: txp.id, signatures: signatures, }, function(err) { + should.not.exist(err); server.removePendingTx({ txProposalId: txp.id }, function(err) { @@ -1659,7 +1820,57 @@ describe('Copay server', function() { }); }); - it.skip('should fail to remove non-pending TX', function(done) {}); + it('should fail to remove non-pending TX', function(done) { + async.waterfall([ + + function(next) { + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server) { + server.rejectTx({ + txProposalId: txp.id, + }, function(err) { + should.not.exist(err); + next(); + }); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[2].id, function(server) { + server.rejectTx({ + txProposalId: txp.id, + }, function(err) { + should.not.exist(err); + next(); + }); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.should.be.empty; + next(); + }); + }, + function(next) { + server.removePendingTx({ + txProposalId: txp.id + }, function(err) { + should.exist(err); + err.code.should.equal('TXNOTPENDING'); + done(); + }); + }, + ]); + }); it('should not allow non-creator copayer to remove an unsigned TX ', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { From 5f687ea5dfc8fe753110023ee24abebc75115b20 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 24 Feb 2015 12:27:44 -0300 Subject: [PATCH 03/15] broadcast tests --- lib/model/txproposal.js | 4 +- lib/server.js | 24 ++++++++-- test/integration/server.js | 95 +++++++++++++++++++++++++++++++++----- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 2729963..ce7de15 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -52,6 +52,7 @@ TxProposal.fromObj = function(obj) { x.requiredRejections = obj.requiredRejections; x.status = obj.status; x.txid = obj.txid; + x.broadcastedOn = obj.broadcastedOn; x.inputPaths = obj.inputPaths; x.actions = _.map(obj.actions, function(action) { return TxProposalAction.fromObj(action); @@ -172,7 +173,7 @@ TxProposal.prototype._addSignaturesToBitcoreTx = function(t, signatures, xpub) { sigtype: Bitcore.crypto.Signature.SIGHASH_ALL, publicKey: pub, }; - t.inputs[i].addSignature(t,s); + t.inputs[i].addSignature(t, s); i++; } catch (e) {}; }); @@ -220,6 +221,7 @@ TxProposal.prototype.isBroadcasted = function() { TxProposal.prototype.setBroadcasted = function(txid) { this.txid = txid; this.status = 'broadcasted'; + this.broadcastedOn = Math.floor(Date.now() / 1000); }; module.exports = TxProposal; diff --git a/lib/server.js b/lib/server.js index ee5b583..98114f7 100644 --- a/lib/server.js +++ b/lib/server.js @@ -691,12 +691,30 @@ WalletService.prototype.signTx = function(opts, cb) { copayerId: self.copayerId, }); - // TODO: replace with .isAccepted() - if (txp.status == 'accepted') { - + if (txp.isAccepted()) { self._notify('TxProposalFinallyAccepted', { txProposalId: opts.txProposalId, }); +<<<<<<< HEAD +======= + + self._broadcastTx(txp, function(err, txid) { + if (err) return cb(err, txp); + + txp.setBroadcasted(txid); + self.storage.storeTx(self.walletId, txp, function(err) { + if (err) return cb(err); + + self._notify('NewOutgoingTx', { + txProposalId: opts.txProposalId, + txid: txid + }); + return cb(null, txp); + }); + }); + } else { + return cb(null, txp); +>>>>>>> broadcast tests } return cb(null, txp); diff --git a/test/integration/server.js b/test/integration/server.js index 990704d..cf251e1 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1257,14 +1257,87 @@ describe('Copay server', function() { }); }); - describe.skip('#broadcastTx', function() { - it.skip('should keep tx as accepted if unable to broadcast it', function(done) {}); - it.skip('should brodcast a tx', function(done) { - // TODO: check final status == 'broadcasted' & broadcastedOn - }); - it.skip('should fail to brodcast an already broadcasted tx', function(done) {}); - it.skip('should brodcast a not yet accepted tx', function(done) {}); - it.skip('should brodcast a tx', function(done) {}); + describe('#broadcastTx', function() { + var server, wallet, txpid; + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, [10, 10], function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, 'some message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + helpers.stubBroadcastFail(); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, function(err, txp) { + should.exist(err); + should.exist(txp); + txp.isAccepted().should.be.true; + txpid = txp.id; + done(); + }); + }); + }); + }); + }); + + it('should brodcast a tx', function(done) { + var clock = sinon.useFakeTimers(1234000); + helpers.stubBroadcast('999'); + server.broadcastTx({ + txProposalId: txpid + }, function(err) { + should.not.exist(err); + server.getTx({ + id: txpid + }, function(err, txp) { + should.not.exist(err); + txp.txid.should.equal('999'); + txp.isBroadcasted().should.be.true; + txp.broadcastedOn.should.equal(1234); + clock.restore(); + done(); + }); + }); + }); + + it('should fail to brodcast an already broadcasted tx', function(done) { + helpers.stubBroadcast('999'); + server.broadcastTx({ + txProposalId: txpid + }, function(err) { + should.not.exist(err); + server.broadcastTx({ + txProposalId: txpid + }, function(err, txid) { + should.exist(err); + should.not.exist(txid); + err.code.should.equal('TXALREADYBROADCASTED'); + done(); + }); + }); + }); + + it('should fail to brodcast a not yet accepted tx', function(done) { + helpers.stubBroadcast('999'); + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, 'some other message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + server.broadcastTx({ + txProposalId: txp.id + }, function(err, txid) { + should.exist(err); + should.not.exist(txid); + err.code.should.equal('TXNOTACCEPTED'); + done(); + }); + }); + }); }); @@ -1285,7 +1358,7 @@ describe('Copay server', function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); server.createTx(txOpts, function(err, txp) { should.not.exist(err); - should.exist.txp; + should.exist(txp); helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) { server2.getPendingTxs({}, function(err, txps) { should.not.exist(err); @@ -1307,7 +1380,7 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, txp) { txpId = txp.id; should.not.exist(err); - should.exist.txp; + should.exist(txp); next(); }); }, @@ -1393,7 +1466,7 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, txp) { txpId = txp.id; should.not.exist(err); - should.exist.txp; + should.exist(txp); next(); }); }, From 376d8ad68864e182e32193a5c350e5f3b41189f7 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 10:37:21 -0300 Subject: [PATCH 04/15] rebase --- lib/server.js | 20 -------------------- test/integration/server.js | 32 ++++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/server.js b/lib/server.js index 98114f7..46a2efd 100644 --- a/lib/server.js +++ b/lib/server.js @@ -695,26 +695,6 @@ WalletService.prototype.signTx = function(opts, cb) { self._notify('TxProposalFinallyAccepted', { txProposalId: opts.txProposalId, }); -<<<<<<< HEAD -======= - - self._broadcastTx(txp, function(err, txid) { - if (err) return cb(err, txp); - - txp.setBroadcasted(txid); - self.storage.storeTx(self.walletId, txp, function(err) { - if (err) return cb(err); - - self._notify('NewOutgoingTx', { - txProposalId: opts.txProposalId, - txid: txid - }); - return cb(null, txp); - }); - }); - } else { - return cb(null, txp); ->>>>>>> broadcast tests } return cb(null, txp); diff --git a/test/integration/server.js b/test/integration/server.js index cf251e1..dfa701f 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -228,12 +228,11 @@ describe('Copay server', function() { .privateKey .toString(); - var message = 'hola'; - var sig = WalletUtils.signMessage(message, priv); + var sig = WalletUtils.signMessage('hello world', priv); WalletService.getInstanceWithAuth({ copayerId: wallet.copayers[0].id, - message: message, + message: 'hello world', signature: sig, }, function(err, server) { should.not.exist(err); @@ -1274,7 +1273,7 @@ describe('Copay server', function() { txProposalId: txp.id, signatures: signatures, }, function(err, txp) { - should.exist(err); + should.not.exist(err); should.exist(txp); txp.isAccepted().should.be.true; txpid = txp.id; @@ -1313,9 +1312,8 @@ describe('Copay server', function() { should.not.exist(err); server.broadcastTx({ txProposalId: txpid - }, function(err, txid) { + }, function(err) { should.exist(err); - should.not.exist(txid); err.code.should.equal('TXALREADYBROADCASTED'); done(); }); @@ -1330,14 +1328,32 @@ describe('Copay server', function() { should.exist(txp); server.broadcastTx({ txProposalId: txp.id - }, function(err, txid) { + }, function(err) { should.exist(err); - should.not.exist(txid); err.code.should.equal('TXNOTACCEPTED'); done(); }); }); }); + + it('should keep tx as accepted if unable to broadcast it', function(done) { + helpers.stubBroadcastFail(); + server.broadcastTx({ + txProposalId: txpid + }, function(err) { + should.exist(err); + server.getTx({ + id: txpid + }, function(err, txp) { + should.not.exist(err); + should.not.exist(txp.txid); + txp.isBroadcasted().should.be.false; + should.not.exist(txp.broadcastedOn); + txp.isAccepted().should.be.true; + done(); + }); + }); + }); }); From eabdd4f35a13b9be69262752f4b0349319012119 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 10:41:55 -0300 Subject: [PATCH 05/15] fix #getTx argument --- lib/server.js | 12 ++++++------ test/integration/server.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/server.js b/lib/server.js index 46a2efd..aeb6905 100644 --- a/lib/server.js +++ b/lib/server.js @@ -569,13 +569,13 @@ WalletService.prototype.createTx = function(opts, cb) { /** * Retrieves a tx from storage. * @param {Object} opts - * @param {string} opts.id - The tx id. + * @param {string} opts.txProposalId - The tx id. * @returns {Object} txProposal */ WalletService.prototype.getTx = function(opts, cb) { var self = this; - self.storage.fetchTx(self.walletId, opts.id, function(err, txp) { + self.storage.fetchTx(self.walletId, opts.txProposalId, function(err, txp) { if (err) return cb(err); if (!txp) return cb(new ClientError('Transaction proposal not found')); return cb(null, txp); @@ -614,7 +614,7 @@ WalletService.prototype.removePendingTx = function(opts, cb) { Utils.runLocked(self.walletId, cb, function(cb) { self.getTx({ - id: opts.txProposalId, + txProposalId: opts.txProposalId, }, function(err, txp) { if (err) return cb(err); @@ -666,7 +666,7 @@ WalletService.prototype.signTx = function(opts, cb) { if (err) return cb(err); self.getTx({ - id: opts.txProposalId + txProposalId: opts.txProposalId }, function(err, txp) { if (err) return cb(err); @@ -719,7 +719,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) { if (err) return cb(err); self.getTx({ - id: opts.txProposalId + txProposalId: opts.txProposalId }, function(err, txp) { if (err) return cb(err); @@ -761,7 +761,7 @@ WalletService.prototype.rejectTx = function(opts, cb) { return cb(new ClientError('Required argument missing')); self.getTx({ - id: opts.txProposalId + txProposalId: opts.txProposalId }, function(err, txp) { if (err) return cb(err); diff --git a/test/integration/server.js b/test/integration/server.js index dfa701f..0e0a223 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -993,7 +993,7 @@ describe('Copay server', function() { should.not.exist(err); txs.should.be.empty; server.getTx({ - id: txid + txProposalId: txid }, function(err, tx) { var actors = tx.getActors(); actors.length.should.equal(1); @@ -1237,7 +1237,7 @@ describe('Copay server', function() { function(next) { helpers.getAuthServer(wallet.copayers[2].id, function(server) { server.getTx({ - id: txid + txProposalId: txid }, function(err, tx) { should.not.exist(err); var signatures = helpers.clientSign(tx, TestData.copayers[2].xPrivKey); @@ -1292,7 +1292,7 @@ describe('Copay server', function() { }, function(err) { should.not.exist(err); server.getTx({ - id: txpid + txProposalId: txpid }, function(err, txp) { should.not.exist(err); txp.txid.should.equal('999'); @@ -1343,7 +1343,7 @@ describe('Copay server', function() { }, function(err) { should.exist(err); server.getTx({ - id: txpid + txProposalId: txpid }, function(err, txp) { should.not.exist(err); should.not.exist(txp.txid); @@ -1539,7 +1539,7 @@ describe('Copay server', function() { }, function(next) { server.getTx({ - id: txpId + txProposalId: txpId }, function(err, txp) { should.not.exist(err); txp.isPending().should.be.false; From 52896dfb7f71eb28687b596e7ad30f1dc45e0998 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 10:46:53 -0300 Subject: [PATCH 06/15] remove unused broadcast stub --- test/integration/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/server.js b/test/integration/server.js index 0e0a223..74f14f3 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1267,7 +1267,6 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, txp) { should.not.exist(err); should.exist(txp); - helpers.stubBroadcastFail(); var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); server.signTx({ txProposalId: txp.id, @@ -1276,6 +1275,7 @@ describe('Copay server', function() { should.not.exist(err); should.exist(txp); txp.isAccepted().should.be.true; + txp.isBroadcasted().should.be.false; txpid = txp.id; done(); }); From fec62dfbd95d4255cf09319c508df9bf936a50ff Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 12:25:17 -0300 Subject: [PATCH 07/15] code clean in storage.js --- lib/storage.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/storage.js b/lib/storage.js index 81e54ea..968300f 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -22,7 +22,7 @@ var Storage = function(opts) { }; var zeroPad = function(x, length) { - return (Array(length).join('0') + parseInt(x)).slice(-length); + return _.padLeft(parseInt(x), length, '0'); }; var walletPrefix = function(id) { @@ -33,7 +33,8 @@ var opKey = function(key) { return key ? '!' + key : ''; }; -var MAX_TS = Array(14).join('9'); +var MAX_TS = _.repeat('9', 14); + var opKeyTs = function(key) { return key ? '!' + zeroPad(key, 14) : ''; }; @@ -106,16 +107,6 @@ Storage.prototype.fetchCopayerLookup = function(copayerId, cb) { }); }; -Storage.prototype.fetchNotification = function(walletId, notificationId, cb) { - this.db.get(KEY.NOTIFICATION(walletId, notificationId), function(err, data) { - if (err) { - if (err.notFound) return cb(); - return cb(err); - } - return cb(null, Notification.fromObj(data)); - }); -}; - Storage.prototype._completeTxData = function(walletId, txs, cb) { var txList = [].concat(txs); this.fetchWallet(walletId, function(err, wallet) { From eb47e069fdd0ddf2bbabd333e4c4c0aacf3c1fd1 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 12:34:45 -0300 Subject: [PATCH 08/15] remove unused code --- lib/storage.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/lib/storage.js b/lib/storage.js index 968300f..6f20d4f 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -284,8 +284,6 @@ Storage.prototype._delByKey = function(key, cb) { }) .on('error', function(err) { if (err.notFound) return cb(); - - console.log('[storage.js.252]'); //TODO return cb(err); }) .on('end', function(err) { @@ -300,15 +298,6 @@ Storage.prototype._delByKey = function(key, cb) { }); }; -Storage.prototype.removeAllPendingTxs = function(walletId, cb) { - this._delByKey(KEY.PENDING_TXP(walletId), cb); -}; - -Storage.prototype.removeAllTxs = function(walletId, cb) { - this._delByKey(KEY.TXP(walletId), cb); -}; - - Storage.prototype._removeCopayers = function(walletId, cb) { var self = this; @@ -324,15 +313,6 @@ Storage.prototype._removeCopayers = function(walletId, cb) { }); }; -Storage.prototype._removeAllNotifications = function(walletId, cb) { - this._delByKey(KEY.NOTIFICATION(walletId), cb); -}; - - -Storage.prototype._removeAllAddresses = function(walletId, cb) { - this._delByKey(KEY.ADDRESS(walletId), cb); -}; - Storage.prototype.removeWallet = function(walletId, cb) { var self = this; @@ -381,11 +361,6 @@ Storage.prototype.storeAddressAndWallet = function(wallet, address, cb) { this.db.batch(ops, cb); }; -Storage.prototype.removeAddress = function(walletId, address, cb) { - this.db.del(KEY.ADDRESS(walletId, address.address), cb); -}; - - Storage.prototype._dump = function(cb, fn) { fn = fn || console.log; From eba051849cf15c2f6dda7c89173b35c0df3ea17a Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 12:34:53 -0300 Subject: [PATCH 09/15] add #getTx tests --- test/integration/server.js | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/integration/server.js b/test/integration/server.js index 74f14f3..b42271d 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1553,6 +1553,45 @@ describe('Copay server', function() { }); }); + describe('#getTx', function() { + var server, wallet, txpid; + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 3, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, 10, function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, 'some message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + txpid = txp.id; + done(); + }); + }); + }); + }); + + it('should get transaction proposal', function(done) { + server.getTx({ + txProposalId: txpid + }, function(err, txp) { + should.not.exist(err); + should.exist(txp); + txp.id.should.equal(txpid); + done(); + }); + }); + it('should fail to get non-existent transaction proposal', function(done) { + server.getTx({ + txProposalId: 'dummy' + }, function(err, txp) { + should.exist(err); + should.not.exist(txp); + err.message.should.contain('not found'); + done(); + }); + }); + }); describe('#getTxs', function() { var server, wallet, clock; From 0285d4924c8d80b16292d822e546eebefd526c11 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 12:46:54 -0300 Subject: [PATCH 10/15] reorganize model tests --- test/{ => models}/addressmanager.js | 2 +- test/{ => models}/txproposal.js | 2 +- test/{ => models}/wallet.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename test/{ => models}/addressmanager.js (94%) rename test/{ => models}/txproposal.js (98%) rename test/{ => models}/wallet.js (98%) diff --git a/test/addressmanager.js b/test/models/addressmanager.js similarity index 94% rename from test/addressmanager.js rename to test/models/addressmanager.js index d496a87..31236d3 100644 --- a/test/addressmanager.js +++ b/test/models/addressmanager.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); -var AddressManager = require('../lib/model/addressmanager'); +var AddressManager = require('../../lib/model/addressmanager'); describe('AddressManager', function() { diff --git a/test/txproposal.js b/test/models/txproposal.js similarity index 98% rename from test/txproposal.js rename to test/models/txproposal.js index 08e29f9..1b646c1 100644 --- a/test/txproposal.js +++ b/test/models/txproposal.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); -var TXP = require('../lib/model/txproposal'); +var TXP = require('../../lib/model/txproposal'); var Bitcore = require('bitcore'); diff --git a/test/wallet.js b/test/models/wallet.js similarity index 98% rename from test/wallet.js rename to test/models/wallet.js index c8b7620..54d0959 100644 --- a/test/wallet.js +++ b/test/models/wallet.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); -var Wallet = require('../lib/model/wallet'); +var Wallet = require('../../lib/model/wallet'); describe('Wallet', function() { From d36d8840f9ef0c663b100a5013d1b5d5249489b2 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 12:58:25 -0300 Subject: [PATCH 11/15] test utils.js --- test/utils.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/utils.js diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..a309281 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,51 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var Utils = require('../lib/utils'); + +describe('Utils', function() { + describe('#checkRequired', function() { + it('should check required fields', function() { + var obj = { + id: 'id', + name: 'name', + array: ['a', 'b'], + }; + var fixtures = [{ + args: 'id', + check: true + }, { + args: ['id'], + check: true + }, { + args: ['id, name'], + check: false + }, { + args: ['id', 'name'], + check: true + }, { + args: 'array', + check: true + }, { + args: 'dummy', + check: false + }, { + args: ['dummy1', 'dummy2'], + check: false + }, { + args: ['id', 'dummy'], + check: false + }, ]; + _.each(fixtures, function(f) { + Utils.checkRequired(obj, f.args).should.equal(f.check); + }); + }); + it('should fail to check required fields on non-object', function() { + var obj = 'dummy'; + Utils.checkRequired(obj, 'name').should.be.false; + }); + }); +}); From 98a87233110fc1c619754704e0a425bfa3c21ff9 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 13:00:29 -0300 Subject: [PATCH 12/15] remove unused func in storage --- lib/storage.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/storage.js b/lib/storage.js index 6f20d4f..4e333f5 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -35,10 +35,6 @@ var opKey = function(key) { var MAX_TS = _.repeat('9', 14); -var opKeyTs = function(key) { - return key ? '!' + zeroPad(key, 14) : ''; -}; - var KEY = { WALLET: function(walletId) { From 2326fa298829b95bf3cce9b49cb138de4924ab41 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 13:08:11 -0300 Subject: [PATCH 13/15] remove debug func --- lib/expressapp.js | 11 +-------- test/integration/clientApi.js | 45 +++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/lib/expressapp.js b/lib/expressapp.js index 24458ce..e1c4a63 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -64,7 +64,7 @@ ExpressApp.start = function(opts) { var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; if (!opts.disableLogs) - log.info('Client Err: ' + status + ' ' + req.url + ' ' + err); + log.info('Client Err: ' + status + ' ' + req.url + ' ' + err); res.status(status).json({ code: err.code, @@ -273,15 +273,6 @@ ExpressApp.start = function(opts) { }); }); - - - // TODO: DEBUG only! - router.get('/v1/dump', function(req, res) { - var server = WalletService.getInstance(); - server.storage._dump(function() { - res.end(); - }); - }); app.use(opts.basePath || '/copay/api', router); return app; }; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index aa71dff..dc07e67 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -84,13 +84,11 @@ fsmock._set = function(name, data) { return content[name] = data; }; - var blockExplorerMock = {}; blockExplorerMock.utxos = []; - blockExplorerMock.getUnspentUtxos = function(dummy, cb) { var ret = _.map(blockExplorerMock.utxos || [], function(x) { var y = _.clone(x); @@ -112,7 +110,6 @@ blockExplorerMock.setUtxo = function(address, amount, m) { }); }; - blockExplorerMock.broadcast = function(raw, cb) { blockExplorerMock.lastBroadcasted = raw; return cb(null, (new Bitcore.Transaction(raw)).id); @@ -122,6 +119,8 @@ blockExplorerMock.reset = function() { blockExplorerMock.utxos = []; }; + + describe('client API ', function() { var clients, app; @@ -217,10 +216,8 @@ describe('client API ', function() { done(); }); }); - }); - describe('Storage Encryption', function() { beforeEach(function() { _.each(_.range(3), function(i) { @@ -326,7 +323,6 @@ describe('client API ', function() { it.skip('should not ask for password if not needed (readwrite)', function(done) {}); }); - describe('Wallet Creation', function() { it('should check balance in a 1-1 ', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function(err) { @@ -563,6 +559,7 @@ describe('client API ', function() { }); }); }); + describe('Air gapped related flows', function() { it('should be able get Tx proposals from a file', function(done) { helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { @@ -823,10 +820,8 @@ describe('client API ', function() { }); }); }); - }); - describe('Wallet Backups and Mobility', function() { it('round trip #import #export', function(done) { @@ -869,9 +864,8 @@ describe('client API ', function() { }); }); - describe('Transaction Proposals Creation and Locked funds', function() { - it('Should lock and release funds', function(done) { + it('Should lock and release funds through rejection', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { clients[0].createAddress(function(err, x0) { should.not.exist(err); @@ -902,6 +896,37 @@ describe('client API ', function() { }); }); }); + it('Should lock and release funds through removal', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + blockExplorerMock.setUtxo(x0, 1, 2); + blockExplorerMock.setUtxo(x0, 1, 2); + var opts = { + amount: 120000000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hello 1-1', + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + + clients[0].sendTxProposal(opts, function(err, y) { + err.code.should.contain('INSUFFICIENTFUNDS'); + + clients[0].removeTxProposal(x, function(err) { + should.not.exist(err); + + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + }); + }); it('Should keep message and refusal texts', function(done) { helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { clients[0].createAddress(function(err, x0) { From 9260ab202ee90c9f07a0f066d1791228bfdaf5d4 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 13:18:26 -0300 Subject: [PATCH 14/15] propose new tests for #getTx --- test/integration/server.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration/server.js b/test/integration/server.js index b42271d..820ffb4 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1571,7 +1571,7 @@ describe('Copay server', function() { }); }); - it('should get transaction proposal', function(done) { + it('should get own transaction proposal', function(done) { server.getTx({ txProposalId: txpid }, function(err, txp) { @@ -1581,6 +1581,7 @@ describe('Copay server', function() { done(); }); }); + it.skip('should get someone elses transaction proposal', function(done) {}); it('should fail to get non-existent transaction proposal', function(done) { server.getTx({ txProposalId: 'dummy' @@ -1591,6 +1592,8 @@ describe('Copay server', function() { done(); }); }); + it.skip('should get accepted/rejected transaction proposal', function(done) {}); + it.skip('should get broadcasted transaction proposal', function(done) {}); }); describe('#getTxs', function() { From 4b1e27f7501b9c34a2dfd61544a506e7dca025ed Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Feb 2015 13:53:28 -0300 Subject: [PATCH 15/15] test tx history from client --- test/integration/clientApi.js | 45 ++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index dc07e67..3a66e37 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -14,6 +14,7 @@ var Bitcore = require('bitcore'); var WalletUtils = require('../../lib/walletutils'); var ExpressApp = require('../../lib/expressapp'); var Storage = require('../../lib/storage'); +var TestData = require('../testdata'); var helpers = {}; @@ -84,10 +85,8 @@ fsmock._set = function(name, data) { return content[name] = data; }; -var blockExplorerMock = {}; -blockExplorerMock.utxos = []; - +var blockExplorerMock = {}; blockExplorerMock.getUnspentUtxos = function(dummy, cb) { var ret = _.map(blockExplorerMock.utxos || [], function(x) { @@ -115,8 +114,17 @@ blockExplorerMock.broadcast = function(raw, cb) { return cb(null, (new Bitcore.Transaction(raw)).id); }; +blockExplorerMock.setHistory = function(txs) { + blockExplorerMock.txHistory = txs; +}; + +blockExplorerMock.getTransactions = function(addresses, cb) { + return cb(null, blockExplorerMock.txHistory || []); +}; + blockExplorerMock.reset = function() { blockExplorerMock.utxos = []; + blockExplorerMock.txHistory = []; }; @@ -1340,4 +1348,35 @@ describe('client API ', function() { }); }); }); + + describe('Transaction history', function() { + it('should get transaction history', function(done) { + blockExplorerMock.setHistory(TestData.history); + helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + clients[0].getTxHistory({}, function(err, txs) { + should.not.exist(err); + should.exist(txs); + txs.length.should.equal(2); + done(); + }); + }); + }); + }); + it('should get empty transaction history when there are no addresses', function(done) { + blockExplorerMock.setHistory(TestData.history); + helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + clients[0].getTxHistory({}, function(err, txs) { + should.not.exist(err); + should.exist(txs); + txs.length.should.equal(0); + done(); + }); + }); + }); + it.skip('should get transaction history decorated with proposal', function(done) {}); + it.skip('should get paginated transaction history', function(done) {}); + }); });