commit
53d74b3f6e
|
@ -93,6 +93,32 @@ TxProposal.prototype.getRawTx = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getActors
|
||||||
|
*
|
||||||
|
* @return {String[]} copayerIds that performed actions in this proposal (accept / reject)
|
||||||
|
*/
|
||||||
|
TxProposal.prototype.getActors = function() {
|
||||||
|
return _.keys(this.actions);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getActionBy
|
||||||
|
*
|
||||||
|
* @param {String} copayerId
|
||||||
|
* @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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
TxProposal.prototype.addAction = function(copayerId, type, signatures) {
|
TxProposal.prototype.addAction = function(copayerId, type, signatures) {
|
||||||
var action = new TxProposalAction({
|
var action = new TxProposalAction({
|
||||||
copayerId: copayerId,
|
copayerId: copayerId,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
var $ = require('preconditions').singleton();
|
var $ = require('preconditions').singleton();
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
@ -482,6 +481,46 @@ CopayServer.prototype.removeWallet = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removePendingTx
|
||||||
|
*
|
||||||
|
* @param opts
|
||||||
|
* @param {string} opts.id - The tx id.
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
CopayServer.prototype.removePendingTx = function(opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!Utils.checkRequired(opts, ['id']))
|
||||||
|
return cb(new ClientError('Required argument missing'));
|
||||||
|
|
||||||
|
Utils.runLocked(self.walletId, cb, function(cb) {
|
||||||
|
|
||||||
|
self.storage.fetchTx(self.walletId, opts.id, function(err, txp) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (!txp)
|
||||||
|
return cb(new ClientError('Transaction proposal not found'));
|
||||||
|
|
||||||
|
if (!txp.isPending())
|
||||||
|
return cb(new ClientError('Transaction proposal not pending'));
|
||||||
|
|
||||||
|
|
||||||
|
if (txp.creatorId !== self.copayerId)
|
||||||
|
return cb(new ClientError('Not allowed to erase this TX'));
|
||||||
|
|
||||||
|
var actors = txp.getActors();
|
||||||
|
|
||||||
|
if (actors.length > 1)
|
||||||
|
return cb(new ClientError('Not allowed to erase this TX'));
|
||||||
|
|
||||||
|
if (actors.length == 1 && actors[0] !== self.copayerId)
|
||||||
|
return cb(new ClientError('Not allowed to erase this TX'));
|
||||||
|
|
||||||
|
self.storage.removeTx(self.walletId, opts.id, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
CopayServer.prototype._broadcastTx = function(txp, cb) {
|
CopayServer.prototype._broadcastTx = function(txp, cb) {
|
||||||
var raw = txp.getRawTx();
|
var raw = txp.getRawTx();
|
||||||
|
@ -510,8 +549,9 @@ CopayServer.prototype.signTx = function(opts, cb) {
|
||||||
}, function(err, txp) {
|
}, function(err, txp) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
if (!txp) return cb(new ClientError('Transaction proposal not found'));
|
if (!txp) return cb(new ClientError('Transaction proposal not found'));
|
||||||
|
|
||||||
var action = _.find(txp.actions, {
|
var action = _.find(txp.actions, {
|
||||||
copayerId: opts.copayerId
|
copayerId: self.copayerId
|
||||||
});
|
});
|
||||||
if (action)
|
if (action)
|
||||||
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
|
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
|
||||||
|
|
|
@ -190,11 +190,10 @@ Storage.prototype.storeTx = function(walletId, txp, cb) {
|
||||||
Storage.prototype.removeTx = function(walletId, txProposalId, cb) {
|
Storage.prototype.removeTx = function(walletId, txProposalId, cb) {
|
||||||
var ops = [{
|
var ops = [{
|
||||||
type: 'del',
|
type: 'del',
|
||||||
key: KEY.TXP(walletId, txp.id),
|
key: KEY.TXP(walletId, txProposalId),
|
||||||
}, {
|
}, {
|
||||||
type: 'del',
|
type: 'del',
|
||||||
key: KEY.PENDING_TXP(walletId, txp.id),
|
key: KEY.PENDING_TXP(walletId, txProposalId),
|
||||||
value: txp,
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
this.db.batch(ops, cb);
|
this.db.batch(ops, cb);
|
||||||
|
|
|
@ -66,7 +66,7 @@ helpers.createAndJoinWallet = function(m, n, cb) {
|
||||||
|
|
||||||
helpers.getAuthServer(copayerIds[0], function(s) {
|
helpers.getAuthServer(copayerIds[0], function(s) {
|
||||||
s.getWallet({}, function(err, w) {
|
s.getWallet({}, function(err, w) {
|
||||||
cb(s, w, _.take(TestData.copayers, w.n));
|
cb(s, w, _.take(TestData.copayers, w.n), copayerIds);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -130,11 +130,11 @@ helpers.stubBlockExplorer = function(server, utxos, txid) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
helpers.clientSign = function(tx, xpriv, n) {
|
helpers.clientSign = function(tx, xprivHex) {
|
||||||
//Derive proper key to sign, for each input
|
//Derive proper key to sign, for each input
|
||||||
var privs = [],
|
var privs = [],
|
||||||
derived = {};
|
derived = {};
|
||||||
var xpriv = new Bitcore.HDPrivateKey(TestData.copayers[0].xPrivKey);
|
var xpriv = new Bitcore.HDPrivateKey(xprivHex);
|
||||||
|
|
||||||
_.each(tx.inputs, function(i) {
|
_.each(tx.inputs, function(i) {
|
||||||
if (!derived[i.path]) {
|
if (!derived[i.path]) {
|
||||||
|
@ -146,7 +146,7 @@ helpers.clientSign = function(tx, xpriv, n) {
|
||||||
var t = new Bitcore.Transaction();
|
var t = new Bitcore.Transaction();
|
||||||
|
|
||||||
_.each(tx.inputs, function(i) {
|
_.each(tx.inputs, function(i) {
|
||||||
t.from(i, i.publicKeys, n);
|
t.from(i, i.publicKeys, tx.requiredSignatures);
|
||||||
});
|
});
|
||||||
|
|
||||||
t.to(tx.toAddress, tx.amount)
|
t.to(tx.toAddress, tx.amount)
|
||||||
|
@ -435,7 +435,7 @@ describe('Copay server', function() {
|
||||||
describe('#verifyMessageSignature', function() {
|
describe('#verifyMessageSignature', function() {
|
||||||
var server, wallet;
|
var server, wallet;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
helpers.createAndJoinWallet(2, 2, function(s, w) {
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
||||||
server = s;
|
server = s;
|
||||||
wallet = w;
|
wallet = w;
|
||||||
done();
|
done();
|
||||||
|
@ -559,7 +559,7 @@ describe('Copay server', function() {
|
||||||
describe('#createTx', function() {
|
describe('#createTx', function() {
|
||||||
var server, wallet, copayerPriv;
|
var server, wallet, copayerPriv;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
helpers.createAndJoinWallet(2, 2, function(s, w, c) {
|
helpers.createAndJoinWallet(2, 3, function(s, w, c) {
|
||||||
server = s;
|
server = s;
|
||||||
wallet = w;
|
wallet = w;
|
||||||
copayerPriv = c;
|
copayerPriv = c;
|
||||||
|
@ -765,14 +765,66 @@ describe('Copay server', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#signTx', function() {
|
|
||||||
var server, wallet, copayerPriv, txid;
|
describe('#rejectTx', function() {
|
||||||
|
var server, wallet, copayerPriv, txid, copayerIds;
|
||||||
|
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
helpers.createAndJoinWallet(2, 2, function(s, w, c) {
|
helpers.createAndJoinWallet(2, 3, function(s, w, c, ids) {
|
||||||
server = s;
|
server = s;
|
||||||
wallet = w;
|
wallet = w;
|
||||||
copayerPriv = c;
|
copayerPriv = c;
|
||||||
|
copayerIds = ids;
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(utxos) {
|
||||||
|
helpers.stubBlockExplorer(server, utxos);
|
||||||
|
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, copayerPriv[0].privKey);
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
tx.should.exist;
|
||||||
|
txid = tx.id;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject a TX', function(done) {
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
var tx = txs[0];
|
||||||
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
|
server.rejectTx({
|
||||||
|
txProposalId: txid,
|
||||||
|
}, function(err) {
|
||||||
|
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(copayerIds[0]);
|
||||||
|
tx.getActionBy(copayerIds[0]).type.should.equal('reject');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#signTx', function() {
|
||||||
|
var server, wallet, copayerPriv, txid, copayerIds;
|
||||||
|
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 3, function(s, w, c, ids) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
copayerPriv = c;
|
||||||
|
copayerIds = ids;
|
||||||
server.createAddress({}, function(err, address) {
|
server.createAddress({}, function(err, address) {
|
||||||
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(utxos) {
|
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(utxos) {
|
||||||
helpers.stubBlockExplorer(server, utxos);
|
helpers.stubBlockExplorer(server, utxos);
|
||||||
|
@ -793,12 +845,39 @@ describe('Copay server', function() {
|
||||||
var tx = txs[0];
|
var tx = txs[0];
|
||||||
tx.id.should.equal(txid);
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey, wallet.n);
|
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey);
|
||||||
server.signTx({
|
server.signTx({
|
||||||
txProposalId: txid,
|
txProposalId: txid,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
should.not.exist(err);
|
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(copayerIds[0]);
|
||||||
|
tx.getActionBy(copayerIds[0]).type.should.equal('accept');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail to sign with a xpriv from other copayer', function(done) {
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
var tx = txs[0];
|
||||||
|
tx.id.should.equal(txid);
|
||||||
|
var signatures = helpers.clientSign(tx, TestData.copayers[1].xPrivKey);
|
||||||
|
server.signTx({
|
||||||
|
txProposalId: txid,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
err.code.should.contain('BADSIG');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -809,7 +888,7 @@ describe('Copay server', function() {
|
||||||
var tx = txs[0];
|
var tx = txs[0];
|
||||||
tx.id.should.equal(txid);
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey, wallet.n);
|
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey);
|
||||||
signatures[0] = 1;
|
signatures[0] = 1;
|
||||||
|
|
||||||
server.signTx({
|
server.signTx({
|
||||||
|
@ -821,6 +900,7 @@ describe('Copay server', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail on invalid signature', function(done) {
|
it('should fail on invalid signature', function(done) {
|
||||||
server.getPendingTxs({}, function(err, txs) {
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
var tx = txs[0];
|
var tx = txs[0];
|
||||||
|
@ -836,6 +916,47 @@ describe('Copay server', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail when signing a TX previously rejected', function(done) {
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
var tx = txs[0];
|
||||||
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
|
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey);
|
||||||
|
server.signTx({
|
||||||
|
txProposalId: txid,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
server.rejectTx({
|
||||||
|
txProposalId: txid,
|
||||||
|
}, function(err) {
|
||||||
|
err.code.should.contain('CVOTED');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when rejected a previously signed TX', function(done) {
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
var tx = txs[0];
|
||||||
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
|
server.rejectTx({
|
||||||
|
txProposalId: txid,
|
||||||
|
}, function(err) {
|
||||||
|
var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey);
|
||||||
|
server.signTx({
|
||||||
|
txProposalId: txid,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
err.code.should.contain('CVOTED');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -866,7 +987,7 @@ describe('Copay server', function() {
|
||||||
server.getPendingTxs({}, function(err, txps) {
|
server.getPendingTxs({}, function(err, txps) {
|
||||||
var txp = txps[0];
|
var txp = txps[0];
|
||||||
txp.id.should.equal(txpid);
|
txp.id.should.equal(txpid);
|
||||||
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey, wallet.n);
|
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey);
|
||||||
server.signTx({
|
server.signTx({
|
||||||
txProposalId: txpid,
|
txProposalId: txpid,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
|
@ -892,7 +1013,7 @@ describe('Copay server', function() {
|
||||||
server.getPendingTxs({}, function(err, txps) {
|
server.getPendingTxs({}, function(err, txps) {
|
||||||
var txp = txps[0];
|
var txp = txps[0];
|
||||||
txp.id.should.equal(txpid);
|
txp.id.should.equal(txpid);
|
||||||
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey, wallet.n);
|
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey);
|
||||||
server.signTx({
|
server.signTx({
|
||||||
txProposalId: txpid,
|
txProposalId: txpid,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
|
@ -1130,4 +1251,90 @@ describe('Copay server', function() {
|
||||||
}, cat);
|
}, cat);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('#removePendingTx', function() {
|
||||||
|
var server, wallet, copayerPriv, txp;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 3, function(s, w, c) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
copayerPriv = c;
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) {
|
||||||
|
helpers.stubBlockExplorer(server, utxos);
|
||||||
|
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', copayerPriv[0].privKey);
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
txp = txs[0];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow creator to remove an unsigned TX', function(done) {
|
||||||
|
server.removePendingTx({
|
||||||
|
id: txp.id
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
txs.length.should.equal(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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],
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
server.removePendingTx({
|
||||||
|
id: txp.id
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
txs.length.should.equal(0);
|
||||||
|
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({
|
||||||
|
id: txp.id
|
||||||
|
}, function(err) {
|
||||||
|
err.message.should.contain('Not allowed');
|
||||||
|
server2.getPendingTxs({}, function(err, txs) {
|
||||||
|
txs.length.should.equal(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow creator copayer to remove an TX signed by other copayer', function(done) {
|
||||||
|
helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
|
||||||
|
var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey);
|
||||||
|
server2.signTx({
|
||||||
|
txProposalId: txp.id,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.removePendingTx({
|
||||||
|
id: txp.id
|
||||||
|
}, function(err) {
|
||||||
|
err.message.should.contain('Not allowed');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue