diff --git a/TODO.txt b/TODO.txt index 9600e23..7ec5061 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,8 +1,7 @@ -- When creating a wallet, the id should be generated by the server and returned to the client to be used as part of the wallet secret. -- Check not blank & length < 100 for both wallet.name & copayer.name +- Check length < 100 for both wallet.name & copayer.name - Proposal with spent input should be tagged as invalid or removed - Cron job to broadcast accepted txps that failed to broadcast (we may need to track broadcast attempts for this). - +- Payment protocol - check parameters for KEY at storage diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 3a9d277..5ea96c0 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -147,7 +147,7 @@ TxProposal.prototype.reject = function(copayerId) { }; TxProposal.prototype.isPending = function() { - return this.status === 'pending'; + return !_.any(['boradcasted', 'rejected'], this.status); }; TxProposal.prototype.isAccepted = function() { diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 8bf50d2..5191523 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -2,6 +2,7 @@ var _ = require('lodash'); var util = require('util'); +var $ = require('preconditions').singleton(); var Bitcore = require('bitcore'); var BitcoreAddress = Bitcore.Address; @@ -111,9 +112,15 @@ Wallet.prototype.getPublicKey = function(copayerId, path) { return copayer.getPublicKey(path); }; +Wallet.prototype.isComplete = function() { + return this.status == 'complete'; +}; + Wallet.prototype.createAddress = function(isChange) { + $.checkState(this.isComplete()); + var path = this.addressManager.getNewAddressPath(isChange); - + var publicKeys = _.map(this.copayers, function(copayer) { var xpub = new Bitcore.HDPublicKey(copayer.xPubKey); return xpub.derive(path).publicKey; diff --git a/lib/server.js b/lib/server.js index 5a1c49e..60bba4d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -91,6 +91,7 @@ CopayServer.prototype.createWallet = function(opts, cb) { Utils.checkRequired(opts, ['name', 'm', 'n', 'pubKey']); + if (_.isEmpty(opts.name)) return cb(new ClientError('Invalid wallet name')); if (!Wallet.verifyCopayerLimits(opts.m, opts.n)) return cb(new ClientError('Invalid combination of required copayers / total copayers')); @@ -113,7 +114,7 @@ CopayServer.prototype.createWallet = function(opts, cb) { }); self.storage.storeWallet(wallet, function(err) { - return cb(err,wallet.id); + return cb(err, wallet.id); }); }; @@ -156,6 +157,8 @@ CopayServer.prototype.joinWallet = function(opts, cb) { Utils.checkRequired(opts, ['walletId', 'name', 'xPubKey', 'xPubKeySignature']); + if (_.isEmpty(opts.name)) return cb(new ClientError('Invalid copayer name')); + Utils.runLocked(opts.walletId, cb, function(cb) { self.storage.fetchWallet(opts.walletId, function(err, wallet) { if (err) return cb(err); @@ -188,33 +191,22 @@ CopayServer.prototype.joinWallet = function(opts, cb) { /** * Creates a new address. * @param {Object} opts - * @param {truthy} opts.isChange - Indicates whether this is a regular address or a change address. * @returns {Address} address */ CopayServer.prototype.createAddress = function(opts, cb) { var self = this; - var isChange = opts.isChange || false; - - Utils.checkRequired(opts, ['isChange']); Utils.runLocked(self.walletId, cb, function(cb) { self.getWallet({}, function(err, wallet) { if (err) return cb(err); + if (!wallet.isComplete()) return cb(new ClientError('Wallet is not complete')); - var address = wallet.createAddress(opts.isChange); + var address = wallet.createAddress(false); - self.storage.storeAddress(wallet.id, address, function(err) { + self.storage.storeAddressAndWallet(wallet, address, function(err) { if (err) return cb(err); - self.storage.storeWallet(wallet, function(err) { - if (err) { - self.storage.removeAddress(wallet.id, address, function() { - return cb(err); - }); - } else { - return cb(null, address); - } - }); + return cb(null, address); }); }); }); @@ -399,45 +391,54 @@ CopayServer.prototype.createTx = function(opts, cb) { Utils.checkRequired(opts, ['toAddress', 'amount']); - - // TODO? - // Check some parameters like: - // amount > dust - - self.getWallet({}, function(err, wallet) { - if (err) return cb(err); - - self._getUtxos(function(err, utxos) { + Utils.runLocked(self.walletId, cb, function(cb) { + self.getWallet({}, function(err, wallet) { if (err) return cb(err); + if (!wallet.isComplete()) return cb(new ClientError('Wallet is not complete')); - var changeAddress = wallet.createAddress(true).address; - - utxos = _.reject(utxos, { - locked: true - }); - - var txp = new TxProposal({ - creatorId: self.copayerId, - toAddress: opts.toAddress, - amount: opts.amount, - changeAddress: changeAddress, - requiredSignatures: wallet.m, - maxRejections: wallet.n - wallet.m, - }); - - txp.inputs = self._selectUtxos(txp, utxos); - if (!txp.inputs) { - return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); + var toAddress; + try { + toAddress = new Bitcore.Address(opts.toAddress); + } catch (ex) { + return cb(new ClientError('INVALIDADDRESS', 'Invalid address')); } + if (toAddress.network != wallet.getNetworkName()) return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); - txp.inputPaths = _.pluck(txp.inputs, 'path'); - - // no need to do this now: // TODO remove this comment - //self._createRawTx(txp); - self.storage.storeTx(wallet.id, txp, function(err) { + self._getUtxos(function(err, utxos) { if (err) return cb(err); - return cb(null, txp); + var changeAddress = wallet.createAddress(true); + + utxos = _.reject(utxos, { + locked: true + }); + + var txp = new TxProposal({ + creatorId: self.copayerId, + toAddress: opts.toAddress, + amount: opts.amount, + message: opts.message, + changeAddress: changeAddress.address, + requiredSignatures: wallet.m, + maxRejections: wallet.n - wallet.m, + }); + + txp.inputs = self._selectUtxos(txp, utxos); + if (!txp.inputs) { + return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); + } + + txp.inputPaths = _.pluck(txp.inputs, 'path'); + + self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) { + if (err) return cb(err); + + self.storage.storeTx(wallet.id, txp, function(err) { + if (err) return cb(err); + + return cb(null, txp); + }); + }); }); }); }); diff --git a/lib/storage.js b/lib/storage.js index 275d4a6..ac99d15 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -25,7 +25,7 @@ var opKey = function(key) { var MAX_TS = '999999999999'; var opKeyTs = function(key) { - return key ? '!' + ('000000000000'+key).slice(-12) : ''; + return key ? '!' + ('000000000000' + key).slice(-12) : ''; }; @@ -201,8 +201,17 @@ Storage.prototype.fetchAddresses = function(walletId, cb) { }); }; -Storage.prototype.storeAddress = function(walletId, address, cb) { - this.db.put(KEY.ADDRESS(walletId, address.address), address, cb); +Storage.prototype.storeAddressAndWallet = function(wallet, address, cb) { + var ops = [{ + type: 'put', + key: KEY.WALLET(wallet.id), + value: wallet, + }, { + type: 'put', + key: KEY.ADDRESS(wallet.id, address.address), + value: address, + }, ]; + this.db.batch(ops, cb); }; Storage.prototype.removeAddress = function(walletId, address, cb) { diff --git a/test/integration.js b/test/integration.js index 479fa93..75f430b 100644 --- a/test/integration.js +++ b/test/integration.js @@ -68,6 +68,7 @@ helpers.getAuthServer = function(copayerId, cb) { message: 'dummy', signature: 'dummy', }, function(err, server) { + if (err || !server) throw new Error('Could not login as copayerId ' + copayerId); signatureStub.restore(); return cb(server); }); @@ -129,9 +130,7 @@ helpers.createUtxos = function(server, wallet, amounts, cb) { var addresses = []; async.each(amounts, function(a, next) { - server.createAddress({ - isChange: false, - }, function(err, address) { + server.createAddress({}, function(err, address) { addresses.push(address); next(err); }); @@ -252,26 +251,18 @@ describe('Copay server', function() { }); }); - // non sense with server generated UUIDs - it.skip('should fail to recreate existing wallet', function(done) { + it('should fail to create wallet with no name', function(done) { var opts = { - id: '123', - name: 'my wallet', + name: '', m: 2, n: 3, pubKey: aPubKey, }; - server.createWallet(opts, function(err) { - should.not.exist(err); - server.storage.fetchWallet('123', function(err, wallet) { - should.not.exist(err); - wallet.id.should.equal('123'); - wallet.name.should.equal('my wallet'); - server.createWallet(opts, function(err) { - should.exist(err); - done(); - }); - }); + server.createWallet(opts, function(err, walletId) { + should.not.exist(walletId); + err.should.exist; + err.message.should.contain('name'); + done(); }); }); @@ -315,46 +306,63 @@ describe('Copay server', function() { }); describe('#joinWallet', function() { - var server; - beforeEach(function() { + var server, walletId; + beforeEach(function(done) { server = new CopayServer(); - }); - - it('should join existing wallet', function(done) { var walletOpts = { name: 'my wallet', m: 2, n: 3, pubKey: keyPair.pub, }; - - server.createWallet(walletOpts, function(err, walletId) { + server.createWallet(walletOpts, function(err, wId) { should.not.exist(err); - var copayerOpts = { - walletId: walletId, - name: 'me', - xPubKey: aXPubKey, - xPubKeySignature: aXPubKeySignature, - }; - server.joinWallet(copayerOpts, function(err, copayerId) { - should.not.exist(err); - helpers.getAuthServer(copayerId, function(server) { - server.getWallet({}, function(err, wallet) { - wallet.id.should.equal(walletId); - wallet.copayers.length.should.equal(1); - var copayer = wallet.copayers[0]; - copayer.name.should.equal('me'); - copayer.id.should.equal(copayerId); - done(); - }); + should.exist.walletId; + walletId = wId; + done(); + }); + }); + + it('should join existing wallet', function(done) { + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: aXPubKey, + xPubKeySignature: aXPubKeySignature, + }; + server.joinWallet(copayerOpts, function(err, copayerId) { + should.not.exist(err); + helpers.getAuthServer(copayerId, function(server) { + server.getWallet({}, function(err, wallet) { + wallet.id.should.equal(walletId); + wallet.copayers.length.should.equal(1); + var copayer = wallet.copayers[0]; + copayer.name.should.equal('me'); + copayer.id.should.equal(copayerId); + done(); }); }); }); }); + it('should fail to join with no name', function(done) { + var copayerOpts = { + walletId: walletId, + name: '', + xPubKey: someXPubKeys[0], + xPubKeySignature: someXPubKeysSignatures[0], + }; + server.joinWallet(copayerOpts, function(err, copayerId) { + should.not.exist(copayerId); + err.should.exist; + err.message.should.contain('name'); + done(); + }); + }); + it('should fail to join non-existent wallet', function(done) { var copayerOpts = { - walletId: '234', + walletId: '123', name: 'me', xPubKey: 'dummy', xPubKeySignature: 'dummy', @@ -366,143 +374,77 @@ describe('Copay server', function() { }); it('should fail to join full wallet', function(done) { - var walletOpts = { - name: 'my wallet', - m: 1, - n: 1, - pubKey: keyPair.pub, - }; - server.createWallet(walletOpts, function(err,walletId) { - should.not.exist(err); - var copayer1Opts = { - walletId: walletId, - id: '111', + helpers.createAndJoinWallet(1, 1, function(s, wallet) { + var copayerOpts = { + walletId: wallet.id, name: 'me', - xPubKey: someXPubKeys[0], - xPubKeySignature: someXPubKeysSignatures[0], - }; - var copayer2Opts = { - walletId: walletId, - id: '222', - name: 'me 2', xPubKey: someXPubKeys[1], xPubKeySignature: someXPubKeysSignatures[1], }; - server.joinWallet(copayer1Opts, function(err, copayer1Id) { - should.not.exist(err); - helpers.getAuthServer(copayer1Id, function(server) { - server.getWallet({}, function(err, wallet) { - wallet.status.should.equal('complete'); - server.joinWallet(copayer2Opts, function(err, copayer2Id) { - should.exist(err); - err.code.should.equal('WFULL'); - err.message.should.equal('Wallet full'); - done(); - }); - }); - }); + server.joinWallet(copayerOpts, function(err) { + should.exist(err); + err.code.should.equal('WFULL'); + err.message.should.equal('Wallet full'); + done(); }); }); }); it('should fail to re-join wallet', function(done) { - var walletOpts = { - name: 'my wallet', - m: 1, - n: 1, - pubKey: keyPair.pub, + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: someXPubKeys[0], + xPubKeySignature: someXPubKeysSignatures[0], }; - server.createWallet(walletOpts, function(err, walletId) { + server.joinWallet(copayerOpts, function(err) { should.not.exist(err); - var copayerOpts = { - walletId: walletId, - id: '111', - name: 'me', - xPubKey: someXPubKeys[0], - xPubKeySignature: someXPubKeysSignatures[0], - }; server.joinWallet(copayerOpts, function(err) { - should.not.exist(err); - server.joinWallet(copayerOpts, function(err) { - should.exist(err); - err.code.should.equal('CINWALLET'); - err.message.should.equal('Copayer already in wallet'); - done(); - }); + should.exist(err); + err.code.should.equal('CINWALLET'); + err.message.should.equal('Copayer already in wallet'); + done(); }); }); }); - it('should fail to join with bad formated signature', function(done) { - var walletOpts = { - id: '123', - name: 'my wallet', - m: 1, - n: 1, - pubKey: aPubKey, + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: someXPubKeys[0], + xPubKeySignature: 'bad sign', }; - server.createWallet(walletOpts, function(err, walletId) { - should.not.exist(err); - var copayerOpts = { - walletId: walletId, - name: 'me', - xPubKey: someXPubKeys[0], - xPubKeySignature: 'bad sign', - }; - server.joinWallet(copayerOpts, function(err) { - err.message.should.equal('Bad request'); - done(); - }); + server.joinWallet(copayerOpts, function(err) { + err.message.should.equal('Bad request'); + done(); }); }); - it('should fail to join with null signature', function(done) { - var walletOpts = { - id: '123', - name: 'my wallet', - m: 1, - n: 1, - pubKey: aPubKey, + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: someXPubKeys[0], }; - server.createWallet(walletOpts, function(err) { - should.not.exist(err); - var copayerOpts = { - walletId: '123', - id: '111', - name: 'me', - xPubKey: someXPubKeys[0], - }; - try { - server.joinWallet(copayerOpts, function(err) {}); - } catch (e) { - e.should.contain('xPubKeySignature'); - done(); - } - }); + try { + server.joinWallet(copayerOpts, function(err) {}); + } catch (e) { + e.should.contain('xPubKeySignature'); + done(); + } }); it('should fail to join with wrong signature', function(done) { - var walletOpts = { - id: '123', - name: 'my wallet', - m: 1, - n: 1, - pubKey: aPubKey, + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: someXPubKeys[0], + xPubKeySignature: someXPubKeysSignatures[1], }; - server.createWallet(walletOpts, function(err, walletId) { - should.not.exist(err); - var copayerOpts = { - walletId: walletId, - name: 'me', - xPubKey: someXPubKeys[0], - xPubKeySignature: someXPubKeysSignatures[0], - }; - server.joinWallet(copayerOpts, function(err) { - err.message.should.equal('Bad request'); - done(); - }); + server.joinWallet(copayerOpts, function(err) { + err.message.should.equal('Bad request'); + done(); }); }); @@ -566,10 +508,8 @@ describe('Copay server', function() { }); }); - it('should create main address', function(done) { - server.createAddress({ - isChange: false, - }, function(err, address) { + it('should create address', function(done) { + server.createAddress({}, function(err, address) { should.not.exist(err); address.should.exist; address.address.should.equal('36JdLEUDa6UwCfMhhkdZ2VFnDrGUoLedsR'); @@ -578,84 +518,66 @@ describe('Copay server', function() { }); }); - - it('should create change address', function(done) { - server.createAddress({ - isChange: true, - }, function(err, address) { + it('should fail to create address when wallet is not complete', function(done) { + var server = new CopayServer(); + var walletOpts = { + name: 'my wallet', + m: 2, + n: 3, + pubKey: keyPair.pub, + }; + server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); - address.should.exist; - address.address.should.equal('3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH'); - address.path.should.equal('m/2147483647/1/0'); - done(); - }); - }); - - it.skip('should fail to create address when wallet is not complete', function(done) {}); - - it('should create many addresses on simultaneous requests', function(done) { - async.map(_.range(10), function(i, cb) { - server.createAddress({ - isChange: false, - }, cb); - }, function(err, addresses) { - addresses.length.should.equal(10); - addresses[0].path.should.equal('m/2147483647/0/0'); - addresses[9].path.should.equal('m/2147483647/0/9'); - // No two identical addresses - _.keys(_.groupBy(addresses, 'address')).length.should.equal(10); - done(); - }); - }); - - it('should not create address if unable to store wallet', function(done) { - var storeWalletStub = sinon.stub(server.storage, 'storeWallet'); - storeWalletStub.yields('dummy error'); - - server.createAddress({ - isChange: true, - }, function(err, address) { - err.should.exist; - should.not.exist(address); - - server.getAddresses({}, function(err, addresses) { - addresses.length.should.equal(0); - - server.storage.storeWallet.restore(); - server.createAddress({ - isChange: true, - }, function(err, address) { - should.not.exist(err); - address.should.exist; - address.address.should.equal('3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH'); - address.path.should.equal('m/2147483647/1/0'); - done(); + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: aXPubKey, + xPubKeySignature: aXPubKeySignature, + }; + server.joinWallet(copayerOpts, function(err, copayerId) { + should.not.exist(err); + helpers.getAuthServer(copayerId, function(server) { + server.createAddress({}, function(err, address) { + should.not.exist(address); + err.should.exist; + err.message.should.contain('not complete'); + done(); + }); }); }); }); }); - it('should not create address if unable to store addresses', function(done) { - var storeAddressStub = sinon.stub(server.storage, 'storeAddress'); - storeAddressStub.yields('dummy error'); + it('should create many addresses on simultaneous requests', function(done) { + var N = 5; + async.map(_.range(N), function(i, cb) { + server.createAddress({}, cb); + }, function(err, addresses) { + addresses.length.should.equal(N); + _.each(_.range(N), function(i) { + addresses[i].path.should.equal('m/2147483647/0/' + i); + }); + // No two identical addresses + _.uniq(_.pluck(addresses, 'address')).length.should.equal(N); + done(); + }); + }); - server.createAddress({ - isChange: true, - }, function(err, address) { + 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.not.exist(address); server.getAddresses({}, function(err, addresses) { addresses.length.should.equal(0); - server.storage.storeAddress.restore(); - server.createAddress({ - isChange: true, - }, function(err, address) { + server.storage.storeAddressAndWallet.restore(); + server.createAddress({}, function(err, address) { should.not.exist(err); address.should.exist; - address.address.should.equal('3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH'); - address.path.should.equal('m/2147483647/1/0'); + address.address.should.equal('36JdLEUDa6UwCfMhhkdZ2VFnDrGUoLedsR'); + address.path.should.equal('m/2147483647/0/0'); done(); }); }); @@ -669,29 +591,25 @@ describe('Copay server', function() { helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; - server.createAddress({ - isChange: false, - }, function(err, address) { + server.createAddress({}, function(err, address) { done(); }); }); }); it('should create a tx', function(done) { - helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) { helpers.stubBlockExplorer(server, utxos); var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(80), message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); tx.should.exist; + tx.message.should.equal('some message'); tx.isAccepted().should.equal.false; tx.isRejected().should.equal.false; server.getPendingTxs({}, function(err, txs) { @@ -708,7 +626,75 @@ describe('Copay server', function() { }); }); - it.skip('should fail to create tx when wallet is not complete', function(done) {}); + it('should fail to create tx when wallet is not complete', function(done) { + var server = new CopayServer(); + var walletOpts = { + name: 'my wallet', + m: 2, + n: 3, + pubKey: keyPair.pub, + }; + server.createWallet(walletOpts, function(err, walletId) { + should.not.exist(err); + var copayerOpts = { + walletId: walletId, + name: 'me', + xPubKey: aXPubKey, + xPubKeySignature: aXPubKeySignature, + }; + server.joinWallet(copayerOpts, function(err, copayerId) { + should.not.exist(err); + helpers.getAuthServer(copayerId, function(server, wallet) { + var txOpts = { + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(80), + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + err.should.exist; + err.message.should.contain('not complete'); + done(); + }); + }); + }); + }); + }); + + it('should fail to create tx for address invalid address', function(done) { + helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); + var txOpts = { + toAddress: 'invalid address', + amount: helpers.toSatoshi(80), + }; + + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + err.should.exist; + err.code.should.equal('INVALIDADDRESS'); + err.message.should.equal('Invalid address'); + done(); + }); + }); + }); + + it('should fail to create tx for address of different network', function(done) { + helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) { + helpers.stubBlockExplorer(server, utxos); + var txOpts = { + toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD', // testnet + amount: helpers.toSatoshi(80), + }; + + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + err.should.exist; + err.code.should.equal('INVALIDADDRESS'); + err.message.should.equal('Incorrect address network'); + done(); + }); + }); + }); it('should fail to create tx when insufficient funds', function(done) { helpers.createUtxos(server, wallet, helpers.toSatoshi([100]), function(utxos) { @@ -716,9 +702,6 @@ describe('Copay server', function() { var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(120), - message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { @@ -738,15 +721,16 @@ describe('Copay server', function() { }); }); + it.skip('should fail to create tx when insufficient funds for fee', function(done) {}); + + it.skip('should fail to create tx for dust amount', function(done) {}); + it('should create tx when there is a pending tx and enough UTXOs', function(done) { helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) { helpers.stubBlockExplorer(server, utxos); var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(12), - message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); @@ -755,9 +739,6 @@ describe('Copay server', function() { var txOpts2 = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 8, - message: 'some message 2', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts2, function(err, tx) { should.not.exist(err); @@ -783,9 +764,6 @@ describe('Copay server', function() { var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(12), - message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); @@ -794,9 +772,6 @@ describe('Copay server', function() { var txOpts2 = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(24), - message: 'some message 2', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts2, function(err, tx) { err.code.should.equal('INSUFFICIENTFUNDS'); @@ -815,7 +790,42 @@ describe('Copay server', function() { }); }); }); + }); + it('should create tx using different UTXOs for simultaneous requests', function(done) { + var N = 5; + helpers.createUtxos(server, wallet, helpers.toSatoshi(_.times(N, function() { + return 100; + })), function(utxos) { + helpers.stubBlockExplorer(server, utxos); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(helpers.toSatoshi(N * 100)); + balance.lockedAmount.should.equal(helpers.toSatoshi(0)); + + var txOpts = { + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(80), + }; + async.map(_.range(N), function(i, cb) { + server.createTx(txOpts, function(err, tx) { + cb(err, tx); + }); + }, function(err) { + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(N); + _.uniq(_.pluck(txs, 'changeAddress')).length.should.equal(N); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(helpers.toSatoshi(N * 100)); + balance.lockedAmount.should.equal(balance.totalAmount); + done(); + }); + }); + }); + }); + }); }); }); @@ -826,17 +836,12 @@ describe('Copay server', function() { helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; - server.createAddress({ - isChange: false, - }, 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.stubBlockExplorer(server, utxos); var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(10), - message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); @@ -897,20 +902,16 @@ describe('Copay server', function() { }); }); }); - }); describe('#signTx and broadcast', function() { var server, wallet, utxos; - beforeEach(function(done) { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; - server.createAddress({ - isChange: false, - }, function(err, address) { + server.createAddress({}, function(err, address) { helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(inutxos) { utxos = inutxos; done(); @@ -924,9 +925,6 @@ describe('Copay server', function() { var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(10), - message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, txp) { should.not.exist(err); @@ -952,14 +950,10 @@ describe('Copay server', function() { it('should keep tx as *accepted* if unable to broadcast it', function(done) { - helpers.stubBlockExplorer(server, utxos); var txOpts = { toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: helpers.toSatoshi(10), - message: 'some message', - otToken: 'dummy', - requestSignature: 'dummy', }; server.createTx(txOpts, function(err, txp) { should.not.exist(err); @@ -978,14 +972,11 @@ describe('Copay server', function() { server.getPendingTxs({}, function(err, txps) { should.not.exist(err); - txps.length.should.equal(0); - server.getTx({ - id: txpid - }, function(err, txp) { - txp.status.should.equal('accepted'); - should.not.exist(txp.txid); - done(); - }); + txps.length.should.equal(1); + var txp = txps[0]; + txp.status.should.equal('accepted'); + should.not.exist(txp.txid); + done(); }); }); }); @@ -993,10 +984,46 @@ describe('Copay server', function() { }); }); - describe('Multisignature wallet', function() { - it.skip('all copayers should see pending proposal created by one copayer', function(done) {}); + describe('Tx proposal workflow', function() { + 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, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(inutxos) { + utxos = inutxos; + done(); + }); + }); + }); + }); - it.skip('tx proposals should not be broadcast until quorum is reached', function(done) {}); + it('other copayers should see pending proposal created by one copayer', function(done) { + helpers.stubBlockExplorer(server, utxos); + var txOpts = { + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: helpers.toSatoshi(10), + message: 'some message', + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist.txp; + helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) { + server2.getPendingTxs({}, function(err, txps) { + should.not.exist(err); + txps.length.should.equal(1); + txps[0].id.should.equal(txp.id); + txps[0].message.should.equal('some message'); + done(); + }); + }); + }); + }); + + it.skip('tx proposals should not be broadcast until quorum is reached', function(done) { + + }); it.skip('tx proposals should accept as many rejections as possible without finally rejecting', function(done) {}); @@ -1007,7 +1034,7 @@ describe('Copay server', function() { var server, wallet, clock; beforeEach(function(done) { - if (server) + if (server) return done(); this.timeout(5000); @@ -1016,9 +1043,7 @@ describe('Copay server', function() { helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; - server.createAddress({ - isChange: false, - }, function(err, address) { + server.createAddress({}, function(err, address) { helpers.createUtxos(server, wallet, helpers.toSatoshi(_.range(10)), function(utxos) { helpers.stubBlockExplorer(server, utxos); var txOpts = { @@ -1026,14 +1051,13 @@ describe('Copay server', function() { amount: helpers.toSatoshi(0.1), }; async.eachSeries(_.range(10), function(i, next) { - clock.tick(10000); - server.createTx(txOpts, function(err, tx) { - next(); - }); - }, function(err) { - return done(err); - } - ); + clock.tick(10000); + server.createTx(txOpts, function(err, tx) { + next(); + }); + }, function(err) { + return done(err); + }); }); }); }); diff --git a/test/wallet.js b/test/wallet.js index ae0be20..c8b7620 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -12,7 +12,7 @@ describe('Wallet', function() { describe('#fromObj', function() { it('read a wallet', function() { var w = Wallet.fromObj(testWallet); - w.status.should.equal('complete'); + w.isComplete().should.be.true; }); }); describe('#createAddress', function() {