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/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 29b620b..aeb6905 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) @@ -567,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); @@ -612,12 +614,12 @@ 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); 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 +628,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); @@ -664,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); @@ -673,7 +675,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); @@ -689,9 +691,7 @@ 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, }); @@ -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/lib/storage.js b/lib/storage.js index 81e54ea..4e333f5 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,10 +33,7 @@ var opKey = function(key) { return key ? '!' + key : ''; }; -var MAX_TS = Array(14).join('9'); -var opKeyTs = function(key) { - return key ? '!' + zeroPad(key, 14) : ''; -}; +var MAX_TS = _.repeat('9', 14); var KEY = { @@ -106,16 +103,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) { @@ -293,8 +280,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) { @@ -309,15 +294,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; @@ -333,15 +309,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; @@ -390,11 +357,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; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index aa71dff..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 = {}; @@ -86,10 +87,6 @@ fsmock._set = function(name, data) { var blockExplorerMock = {}; -blockExplorerMock.utxos = []; - - - blockExplorerMock.getUnspentUtxos = function(dummy, cb) { var ret = _.map(blockExplorerMock.utxos || [], function(x) { @@ -112,16 +109,26 @@ blockExplorerMock.setUtxo = function(address, amount, m) { }); }; - blockExplorerMock.broadcast = function(raw, cb) { blockExplorerMock.lastBroadcasted = raw; 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 = []; }; + + describe('client API ', function() { var clients, app; @@ -217,10 +224,8 @@ describe('client API ', function() { done(); }); }); - }); - describe('Storage Encryption', function() { beforeEach(function() { _.each(_.range(3), function(i) { @@ -326,7 +331,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 +567,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 +828,8 @@ describe('client API ', function() { }); }); }); - }); - describe('Wallet Backups and Mobility', function() { it('round trip #import #export', function(done) { @@ -869,9 +872,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 +904,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) { @@ -1315,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) {}); + }); }); diff --git a/test/integration/server.js b/test/integration/server.js index 4373b7a..820ffb4 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); @@ -302,7 +301,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(); }); @@ -345,6 +344,21 @@ describe('Copay server', 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() { @@ -397,7 +411,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(); }); @@ -451,6 +465,40 @@ describe('Copay server', function() { }); }); + 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 = { walletId: walletId, @@ -471,7 +519,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(); }); @@ -579,7 +627,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) { @@ -621,7 +669,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(); }); @@ -652,7 +700,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(); }); @@ -710,7 +758,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(); }); @@ -723,7 +771,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(); }); @@ -736,7 +784,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(); @@ -758,6 +806,16 @@ describe('Copay server', function() { }); }); + 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() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 120, null, TestData.copayers[0].privKey); @@ -803,6 +861,23 @@ describe('Copay server', function() { }); }); + 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() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, null, TestData.copayers[0].privKey); @@ -889,7 +964,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() { @@ -916,20 +991,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({ + txProposalId: 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('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() { @@ -1085,8 +1203,160 @@ 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({ + txProposalId: 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('#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); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, function(err, txp) { + should.not.exist(err); + should.exist(txp); + txp.isAccepted().should.be.true; + txp.isBroadcasted().should.be.false; + 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({ + txProposalId: 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) { + should.exist(err); + 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) { + should.exist(err); + 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({ + txProposalId: 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(); + }); + }); + }); }); + describe('Tx proposal workflow', function() { var server, wallet; beforeEach(function(done) { @@ -1104,7 +1374,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); @@ -1126,7 +1396,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(); }); }, @@ -1212,7 +1482,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(); }); }, @@ -1269,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; @@ -1283,6 +1553,48 @@ 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 own 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.skip('should get someone elses transaction proposal', function(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(); + }); + }); + it.skip('should get accepted/rejected transaction proposal', function(done) {}); + it.skip('should get broadcasted transaction proposal', function(done) {}); + }); describe('#getTxs', function() { var server, wallet, clock; @@ -1623,9 +1935,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) { @@ -1638,6 +1951,58 @@ describe('Copay server', function() { }); }); + 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) { server2.removePendingTx({ @@ -1663,6 +2028,7 @@ describe('Copay server', function() { server.removePendingTx({ txProposalId: txp.id }, function(err) { + err.code.should.equal('TXACTIONED'); err.message.should.contain('other copayers'); done(); }); 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() { 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; + }); + }); +});