From bcdd8073959f048115cb80db59c6fd426498f6b5 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 15 Feb 2015 15:46:29 -0300 Subject: [PATCH 1/4] test rejection flow --- lib/model/txproposal.js | 19 +++---- lib/model/txproposalaction.js | 4 +- lib/server.js | 2 +- test/integration.js | 103 +++++++++++++++++++++++++++++----- 4 files changed, 100 insertions(+), 28 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 11546fd..f7c23e0 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -132,21 +132,16 @@ TxProposal.prototype.getActors = function() { * @return {Object} type / createdOn */ TxProposal.prototype.getActionBy = function(copayerId) { - var a = this.actions[copayerId]; - if (!a) return null; - - return { - type: a.type, - createdOn: a.createdOn, - }; + return this.actions[copayerId]; }; -TxProposal.prototype.addAction = function(copayerId, type, signatures, xpub) { +TxProposal.prototype.addAction = function(copayerId, type, comment, signatures, xpub) { var action = new TxProposalAction({ copayerId: copayerId, type: type, signatures: signatures, xpub: xpub, + comment: comment, }); this.actions[copayerId] = action; this._updateStatus(); @@ -191,19 +186,19 @@ TxProposal.prototype.sign = function(copayerId, signatures, xpub) { var t = this._getBitcoreTx(); try { this._addSignaturesToBitcoreTx(t, signatures, xpub); - this.addAction(copayerId, 'accept', signatures, xpub); + this.addAction(copayerId, 'accept', null, signatures, xpub); return true; } catch (e) { return false; } }; -TxProposal.prototype.reject = function(copayerId) { - this.addAction(copayerId, 'reject'); +TxProposal.prototype.reject = function(copayerId, reason) { + this.addAction(copayerId, 'reject', reason); }; TxProposal.prototype.isPending = function() { - return !_.any(['boradcasted', 'rejected'], this.status); + return !_.contains(['boradcasted', 'rejected'], this.status); }; TxProposal.prototype.isAccepted = function() { diff --git a/lib/model/txproposalaction.js b/lib/model/txproposalaction.js index 13fa98b..a215657 100644 --- a/lib/model/txproposalaction.js +++ b/lib/model/txproposalaction.js @@ -8,9 +8,10 @@ function TxProposalAction(opts) { this.type = opts.type || (opts.signatures ? 'accept' : 'reject'); this.signatures = opts.signatures; this.xpub = opts.xpub; + this.comment = opts.comment; }; -TxProposalAction.fromObj = function (obj) { +TxProposalAction.fromObj = function(obj) { var x = new TxProposalAction(); x.createdOn = obj.createdOn; @@ -18,6 +19,7 @@ TxProposalAction.fromObj = function (obj) { x.type = obj.type; x.signatures = obj.signatures; x.xpub = obj.xpub; + x.comment = obj.comment; return x; }; diff --git a/lib/server.js b/lib/server.js index 21b4d7f..579129a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -686,7 +686,7 @@ CopayServer.prototype.rejectTx = function(opts, cb) { if (txp.status != 'pending') return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending')); - txp.reject(self.copayerId); + txp.reject(self.copayerId, opts.reason); self.storage.storeTx(self.walletId, txp, function(err) { if (err) return cb(err); diff --git a/test/integration.js b/test/integration.js index 1faf0c1..8f6fb4c 100644 --- a/test/integration.js +++ b/test/integration.js @@ -877,6 +877,7 @@ describe('Copay server', function() { server.rejectTx({ txProposalId: txid, + reason: 'some reason', }, function(err) { should.not.exist(err); server.getPendingTxs({}, function(err, txs) { @@ -887,8 +888,9 @@ describe('Copay server', function() { var actors = tx.getActors(); actors.length.should.equal(1); actors[0].should.equal(wallet.copayers[0].id); - tx.getActionBy(wallet.copayers[0].id).type.should.equal('reject'); - + var action = tx.getActionBy(wallet.copayers[0].id); + action.type.should.equal('reject'); + action.comment.should.equal('some reason'); done(); }); }); @@ -1120,14 +1122,14 @@ describe('Copay server', function() { }); describe('Tx proposal workflow', function() { - var server, wallet, utxos; + var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; server.createAddress({}, function(err, address) { - helpers.createUtxos(server, wallet, _.range(1, 9), function(inutxos) { - utxos = inutxos; + helpers.createUtxos(server, wallet, _.range(1, 9), function(utxos) { + helpers.stubBlockExplorer(server, utxos); done(); }); }); @@ -1135,7 +1137,6 @@ describe('Copay server', function() { }); it('other copayers should see pending proposal created by one copayer', function(done) { - helpers.stubBlockExplorer(server, utxos); var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); server.createTx(txOpts, function(err, txp) { should.not.exist(err); @@ -1152,15 +1153,94 @@ describe('Copay server', function() { }); }); - it.skip('tx proposals should not be broadcast until quorum is reached', function(done) { + it.skip('tx proposals should not be broadcast until quorum is reached', function(done) {}); + it('tx proposals should accept as many rejections as possible without finally rejecting', function(done) { + var txpId; + async.series([ + + function(next) { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + txpId = txp.id; + should.not.exist(err); + should.exist.txp; + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + _.keys(txp.actions).should.be.empty; + next(); + }); + }, + function(next) { + server.rejectTx({ + txProposalId: txpId, + reason: 'just because' + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + txp.isPending().should.be.true; + txp.isRejected().should.be.false; + txp.isAccepted().should.be.false; + _.keys(txp.actions).length.should.equal(1); + var action = txp.actions[wallet.copayers[0].id]; + action.type.should.equal('reject'); + action.comment.should.equal('just because'); + next(); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { + server.rejectTx({ + txProposalId: txpId, + reason: 'some other reason' + }, function(err) { + should.not.exist(err); + next(); + }); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(0); + next(); + }); + }, + function(next) { + server.getTx({ + id: txpId + }, function(err, txp) { + should.not.exist(err); + txp.isPending().should.be.false; + txp.isRejected().should.be.true; + txp.isAccepted().should.be.false; + _.keys(txp.actions).length.should.equal(2); + next(); + }); + }, + ], + function() { + done(); + }); }); - it.skip('tx proposals should accept as many rejections as possible without finally rejecting', function(done) {}); - it.skip('proposal creator should be able to delete proposal if there are no other signatures', function(done) {}); }); + describe('#getTxs', function() { var server, wallet, clock; @@ -1256,8 +1336,6 @@ describe('Copay server', function() { var server, wallet; beforeEach(function(done) { - if (server) return done(); - console.log('\tCreating TXS...'); helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; @@ -1304,9 +1382,6 @@ describe('Copay server', function() { }); }); - - - it('should pull the first 5 notifications after wallet creation', function(done) { server.getNotifications({ minTs: 0, From 9020c70dd242d17f8c83404f5974c493b439f185 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 15 Feb 2015 16:15:45 -0300 Subject: [PATCH 2/4] test acceptance flow --- lib/model/txproposal.js | 6 ++- test/integration.js | 97 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index f7c23e0..7fc7347 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -198,7 +198,7 @@ TxProposal.prototype.reject = function(copayerId, reason) { }; TxProposal.prototype.isPending = function() { - return !_.contains(['boradcasted', 'rejected'], this.status); + return !_.contains(['broadcasted', 'rejected'], this.status); }; TxProposal.prototype.isAccepted = function() { @@ -211,6 +211,10 @@ TxProposal.prototype.isRejected = function() { return votes['reject'] >= this.requiredRejections; }; +TxProposal.prototype.isBroadcasted = function() { + return this.status == 'broadcasted'; +}; + TxProposal.prototype.setBroadcasted = function(txid) { this.txid = txid; this.status = 'broadcasted'; diff --git a/test/integration.js b/test/integration.js index 8f6fb4c..25a1990 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1122,14 +1122,15 @@ describe('Copay server', function() { }); describe('Tx proposal workflow', function() { - var server, wallet; + var server, wallet, utxos; beforeEach(function(done) { helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; server.createAddress({}, function(err, address) { - helpers.createUtxos(server, wallet, _.range(1, 9), function(utxos) { - helpers.stubBlockExplorer(server, utxos); + helpers.createUtxos(server, wallet, _.range(1, 9), function(inUtxos) { + utxos = inUtxos; + helpers.stubBlockExplorer(server, utxos, '999'); done(); }); }); @@ -1153,11 +1154,96 @@ describe('Copay server', function() { }); }); - it.skip('tx proposals should not be broadcast until quorum is reached', function(done) {}); + it('tx proposals should not be broadcast until quorum is reached', function(done) { + var txpId; + async.waterfall([ + + function(next) { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + txpId = txp.id; + should.not.exist(err); + should.exist.txp; + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + _.keys(txp.actions).should.be.empty; + next(null, txp); + }); + }, + function(txp, next) { + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + server.signTx({ + txProposalId: txpId, + signatures: signatures, + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + txp.isPending().should.be.true; + txp.isRejected().should.be.false; + txp.isAccepted().should.be.false; + _.keys(txp.actions).length.should.equal(1); + var action = txp.actions[wallet.copayers[0].id]; + action.type.should.equal('accept'); + next(null, txp); + }); + }, + function(txp, next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { + helpers.stubBlockExplorer(server, utxos, '999'); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey); + server.signTx({ + txProposalId: txpId, + signatures: signatures, + }, function(err) { + should.not.exist(err); + next(); + }); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(0); + next(); + }); + }, + function(next) { + server.getTx({ + id: txpId + }, function(err, txp) { + should.not.exist(err); + txp.isPending().should.be.false; + txp.isRejected().should.be.false; + txp.isAccepted().should.be.true; + txp.isBroadcasted().should.be.true; + txp.txid.should.equal('999'); + _.keys(txp.actions).length.should.equal(2); + next(); + }); + }, + ], + function() { + done(); + }); + + }); it('tx proposals should accept as many rejections as possible without finally rejecting', function(done) { var txpId; - async.series([ + async.waterfall([ function(next) { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); @@ -1203,6 +1289,7 @@ describe('Copay server', function() { }, function(next) { helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { + helpers.stubBlockExplorer(server, utxos, '999'); server.rejectTx({ txProposalId: txpId, reason: 'some other reason' From 528634d0a15c37e830dbbead06a907d3f87b2b0e Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 15 Feb 2015 16:17:37 -0300 Subject: [PATCH 3/4] remove redundant test --- test/integration.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration.js b/test/integration.js index 25a1990..ff0fb2e 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1323,8 +1323,6 @@ describe('Copay server', function() { done(); }); }); - - it.skip('proposal creator should be able to delete proposal if there are no other signatures', function(done) {}); }); From 15b79bfd79b327187651511630a74dce4938ea9b Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 15 Feb 2015 16:50:50 -0300 Subject: [PATCH 4/4] cleaner code --- test/integration.js | 283 +++++++++++++++++++++----------------------- 1 file changed, 138 insertions(+), 145 deletions(-) diff --git a/test/integration.js b/test/integration.js index ff0fb2e..e05ad37 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1158,26 +1158,52 @@ describe('Copay server', function() { var txpId; async.waterfall([ - function(next) { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); - server.createTx(txOpts, function(err, txp) { - txpId = txp.id; - should.not.exist(err); - should.exist.txp; - next(); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - txps.length.should.equal(1); - var txp = txps[0]; - _.keys(txp.actions).should.be.empty; - next(null, txp); - }); - }, - function(txp, next) { - var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + function(next) { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + txpId = txp.id; + should.not.exist(err); + should.exist.txp; + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + _.keys(txp.actions).should.be.empty; + next(null, txp); + }); + }, + function(txp, next) { + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + server.signTx({ + txProposalId: txpId, + signatures: signatures, + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + txp.isPending().should.be.true; + txp.isRejected().should.be.false; + txp.isAccepted().should.be.false; + _.keys(txp.actions).length.should.equal(1); + var action = txp.actions[wallet.copayers[0].id]; + action.type.should.equal('accept'); + next(null, txp); + }); + }, + function(txp, next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { + helpers.stubBlockExplorer(server, utxos, '999'); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey); server.signTx({ txProposalId: txpId, signatures: signatures, @@ -1185,143 +1211,110 @@ describe('Copay server', function() { should.not.exist(err); next(); }); - }, - function(next) { - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - txps.length.should.equal(1); - var txp = txps[0]; - txp.isPending().should.be.true; - txp.isRejected().should.be.false; - txp.isAccepted().should.be.false; - _.keys(txp.actions).length.should.equal(1); - var action = txp.actions[wallet.copayers[0].id]; - action.type.should.equal('accept'); - next(null, txp); - }); - }, - function(txp, next) { - helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { - helpers.stubBlockExplorer(server, utxos, '999'); - var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey); - server.signTx({ - txProposalId: txpId, - signatures: signatures, - }, function(err) { - should.not.exist(err); - next(); - }); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - txps.length.should.equal(0); - next(); - }); - }, - function(next) { - server.getTx({ - id: txpId - }, function(err, txp) { - should.not.exist(err); - txp.isPending().should.be.false; - txp.isRejected().should.be.false; - txp.isAccepted().should.be.true; - txp.isBroadcasted().should.be.true; - txp.txid.should.equal('999'); - _.keys(txp.actions).length.should.equal(2); - next(); - }); - }, - ], - function() { - done(); - }); - + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(0); + next(); + }); + }, + function(next) { + server.getTx({ + id: txpId + }, function(err, txp) { + should.not.exist(err); + txp.isPending().should.be.false; + txp.isRejected().should.be.false; + txp.isAccepted().should.be.true; + txp.isBroadcasted().should.be.true; + txp.txid.should.equal('999'); + _.keys(txp.actions).length.should.equal(2); + done(); + }); + }, + ]); }); it('tx proposals should accept as many rejections as possible without finally rejecting', function(done) { var txpId; async.waterfall([ - function(next) { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); - server.createTx(txOpts, function(err, txp) { - txpId = txp.id; - should.not.exist(err); - should.exist.txp; - next(); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - txps.length.should.equal(1); - var txp = txps[0]; - _.keys(txp.actions).should.be.empty; - next(); - }); - }, - function(next) { + function(next) { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); + server.createTx(txOpts, function(err, txp) { + txpId = txp.id; + should.not.exist(err); + should.exist.txp; + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + _.keys(txp.actions).should.be.empty; + next(); + }); + }, + function(next) { + server.rejectTx({ + txProposalId: txpId, + reason: 'just because' + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + var txp = txps[0]; + txp.isPending().should.be.true; + txp.isRejected().should.be.false; + txp.isAccepted().should.be.false; + _.keys(txp.actions).length.should.equal(1); + var action = txp.actions[wallet.copayers[0].id]; + action.type.should.equal('reject'); + action.comment.should.equal('just because'); + next(); + }); + }, + function(next) { + helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { + helpers.stubBlockExplorer(server, utxos, '999'); server.rejectTx({ txProposalId: txpId, - reason: 'just because' + reason: 'some other reason' }, function(err) { should.not.exist(err); next(); }); - }, - function(next) { - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - txps.length.should.equal(1); - var txp = txps[0]; - txp.isPending().should.be.true; - txp.isRejected().should.be.false; - txp.isAccepted().should.be.false; - _.keys(txp.actions).length.should.equal(1); - var action = txp.actions[wallet.copayers[0].id]; - action.type.should.equal('reject'); - action.comment.should.equal('just because'); - next(); - }); - }, - function(next) { - helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { - helpers.stubBlockExplorer(server, utxos, '999'); - server.rejectTx({ - txProposalId: txpId, - reason: 'some other reason' - }, function(err) { - should.not.exist(err); - next(); - }); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - txps.length.should.equal(0); - next(); - }); - }, - function(next) { - server.getTx({ - id: txpId - }, function(err, txp) { - should.not.exist(err); - txp.isPending().should.be.false; - txp.isRejected().should.be.true; - txp.isAccepted().should.be.false; - _.keys(txp.actions).length.should.equal(2); - next(); - }); - }, - ], - function() { - done(); - }); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(0); + next(); + }); + }, + function(next) { + server.getTx({ + id: txpId + }, function(err, txp) { + should.not.exist(err); + txp.isPending().should.be.false; + txp.isRejected().should.be.true; + txp.isAccepted().should.be.false; + _.keys(txp.actions).length.should.equal(2); + done(); + }); + }, + ]); }); });