diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 11546fd..7fc7347 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(['broadcasted', 'rejected'], this.status); }; TxProposal.prototype.isAccepted = function() { @@ -216,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/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..e05ad37 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(); }); }); @@ -1126,8 +1128,9 @@ describe('Copay server', function() { 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(inUtxos) { + utxos = inUtxos; + helpers.stubBlockExplorer(server, utxos, '999'); done(); }); }); @@ -1135,7 +1138,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 +1154,171 @@ 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); + done(); + }); + }, + ]); }); - it.skip('tx proposals should accept as many rejections as possible without finally rejecting', function(done) {}); + it('tx proposals should accept as many rejections as possible without finally rejecting', function(done) { + var txpId; + async.waterfall([ - it.skip('proposal creator should be able to delete proposal if there are no other signatures', function(done) {}); + 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: '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); + done(); + }); + }, + ]); + }); }); + describe('#getTxs', function() { var server, wallet, clock; @@ -1256,8 +1414,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 +1460,6 @@ describe('Copay server', function() { }); }); - - - it('should pull the first 5 notifications after wallet creation', function(done) { server.getNotifications({ minTs: 0,