diff --git a/lib/client/Verifier.js b/lib/client/Verifier.js index 2439497..a653a72 100644 --- a/lib/client/Verifier.js +++ b/lib/client/Verifier.js @@ -35,7 +35,8 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { } // Not signed pub keys - if (!WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) { + if (!copayer.xPubKey || !copayer.xPubKeySignature || + !WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) { log.error('Invalid signatures in server response'); error = true; } diff --git a/lib/client/api.js b/lib/client/api.js index e964507..9cb714a 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -420,7 +420,7 @@ API.prototype.import = function(str, cb) { var xPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString(); - data.publicKeyRing.push(xPubKey); + data.publicKeyRing.unshift(xPubKey); data.copayerId = WalletUtils.xPubToCopayerId(xPubKey); data.n = data.publicKeyRing.length; data.signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey.toWIF(); diff --git a/lib/server.js b/lib/server.js index bc3a287..2ae43d0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -26,15 +26,18 @@ var TxProposal = require('./model/txproposal'); var Notification = require('./model/notification'); var initialized = false; -var storage; +var storage, blockExplorer; /** * Creates an instance of the Copay server. * @constructor */ function CopayServer() { - if (!initialized) throw new Error('Server not initialized'); + if (!initialized) + throw new Error('Server not initialized'); + this.storage = storage; + this.blockExplorer = blockExplorer; this.notifyTicker = 0; }; @@ -45,10 +48,12 @@ nodeutil.inherits(CopayServer, events.EventEmitter); * Initializes global settings for all instances. * @param {Object} opts * @param {Storage} [opts.storage] - The storage provider. + * @param {Storage} [opts.blockExplorer] - The blockExporer provider. */ CopayServer.initialize = function(opts) { opts = opts || {}; storage = opts.storage ||  new Storage(); + blockExplorer = opts.blockExplorer; initialized = true; }; @@ -311,6 +316,9 @@ CopayServer.prototype.verifyMessageSignature = function(opts, cb) { CopayServer.prototype._getBlockExplorer = function(provider, network) { var url; + if (this.blockExplorer) + return this.blockExplorer; + switch (provider) { default: case 'insight': @@ -352,7 +360,6 @@ CopayServer.prototype._getUtxos = function(cb) { var utxos = _.map(inutxos, function(i) { return i.toObject(); }); - self.getPendingTxs({}, function(err, txps) { if (err) return cb(err); diff --git a/lib/walletutils.js b/lib/walletutils.js index 5aacd82..9138f83 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -1,4 +1,5 @@ var _ = require('lodash'); +var $ = require('preconditions').singleton(); var sjcl = require('sjcl'); var Bitcore = require('bitcore'); @@ -14,6 +15,7 @@ function WalletUtils() {}; /* TODO: It would be nice to be compatible with bitcoind signmessage. How * the hash is calculated there? */ WalletUtils.hashMessage = function(text) { + $.checkArgument(text); var buf = new Buffer(text); var ret = crypto.Hash.sha256sha256(buf); ret = new Bitcore.encoding.BufferReader(ret).readReverse(); @@ -22,6 +24,7 @@ WalletUtils.hashMessage = function(text) { WalletUtils.signMessage = function(text, privKey) { + $.checkArgument(text); var priv = new PrivateKey(privKey); var hash = WalletUtils.hashMessage(text); return crypto.ECDSA.sign(hash, priv, 'little').toString(); @@ -29,6 +32,7 @@ WalletUtils.signMessage = function(text, privKey) { WalletUtils.verifyMessage = function(text, signature, pubKey) { + $.checkArgument(signature, text, pubKey); var pub = new PublicKey(pubKey); var hash = WalletUtils.hashMessage(text); @@ -69,6 +73,7 @@ WalletUtils.toSecret = function(walletId, walletPrivKey, network) { }; WalletUtils.fromSecret = function(secret) { + $.checkArgument(secret); var secretSplit = secret.split(':'); var walletId = secretSplit[0]; var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]); @@ -105,7 +110,6 @@ WalletUtils.UNITS = { WalletUtils.parseAmount = function(text) { var regex = '^(\\d*(\\.\\d{0,8})?)\\s*(' + _.keys(WalletUtils.UNITS).join('|') + ')?$'; - var match = new RegExp(regex, 'i').exec(text.trim()); if (!match || match.length === 0) throw new Error('Invalid amount'); diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 7231435..1d5b438 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -39,7 +39,7 @@ helpers.getRequest = function(app) { }; helpers.createAndJoinWallet = function(clients, m, n, cb) { - clients[0].createWallet('wallet name', 'creator copayer', m, n, 'testnet', + clients[0].createWallet('wallet name', 'creator', m, n, 'testnet', function(err, secret) { if (err) return cb(err); if (n == 1) return cb(); @@ -51,12 +51,17 @@ helpers.createAndJoinWallet = function(clients, m, n, cb) { return cb(err); }); }, function(err) { - if (err) return new Error('Could not generate wallet'); - return cb(); + if (err) return cb(err); + return cb(null, { + m: m, + n: n, + secret: secret, + }); }); }); }; + var fsmock = {}; var content = {}; fsmock.readFile = function(name, enc, cb) { @@ -69,9 +74,47 @@ fsmock.writeFile = function(name, data, cb) { content[name] = data; return cb(); }; +fsmock.reset = function() { + content = {}; +}; + +fsmock._get = function(name) { + return content[name]; +}; + + +var utxos = []; +var blockExplorerMock = {}; + + + + +blockExplorerMock.getUnspentUtxos = function(dummy, cb) { + var ret = _.map(utxos || [], function(x) { + x.toObject = function() { + return this; + }; + return x; + }); + return cb(null, ret); +}; + +blockExplorerMock.setUtxo = function(address, amount, m) { + utxos.push({ + txid: Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex'), + vout: Math.floor((Math.random() * 10) + 1), + amount: amount, + address: address.address, + scriptPubKey: Bitcore.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut(), + }); +}; + +blockExplorerMock.reset = function() { + utxos = []; +}; describe('client API ', function() { - var clients; + var clients, app; beforeEach(function() { clients = []; @@ -81,9 +124,10 @@ describe('client API ', function() { var storage = new Storage({ db: db }); - var app = ExpressApp.start({ + app = ExpressApp.start({ CopayServer: { - storage: storage + storage: storage, + blockExplorer: blockExplorerMock, } }); // Generates 5 clients @@ -99,10 +143,11 @@ describe('client API ', function() { client.request = helpers.getRequest(app); clients.push(client); }); - content = {}; + fsmock.reset(); + blockExplorerMock.reset(); }); - describe.only('#getBalance', function() { + describe('Wallet Creation', function() { it('should check balance in a 1-1 ', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function(err) { should.not.exist(err); @@ -112,7 +157,7 @@ describe('client API ', function() { }) }); }); - it('should be able to check balance in a 2-3 wallet ', function(done) { + it('should be able to complete wallets in copayer that joined later', function(done) { helpers.createAndJoinWallet(clients, 2, 3, function(err) { should.not.exist(err); clients[0].getBalance(function(err, x) { @@ -127,105 +172,212 @@ describe('client API ', function() { }) }); }); - }); - describe('#_tryToComplete ', function() { - it('should complete a wallet ', function(done) { - client.storage.fs.readFile = - sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); - - - client.getBalance(function(err, x) { + it('should not allow to join a full wallet ', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { should.not.exist(err); + should.exist(w.secret); + clients[4].joinWallet(w.secret, 'copayer', function(err, result) { + err.should.contain('Request error'); + done(); + }); + }); + }); + it('should fail with a unknown secret', function(done) { + var oldSecret = '3f8e5acb-ceeb-4aae-134f-692d934e3b1c:L2gohj8s2fLKqVU5cQutAVGciutUxczFxLxxXHFsjzLh71ZjkFQQ:T'; + clients[0].joinWallet(oldSecret, 'copayer', function(err, result) { + err.should.contain('Request error'); done(); }); }); - - - it('should handle incomple wallets', function(done) { - var request = sinon.stub(); - - // Wallet request - request.onCall(0).yields(null, { - statusCode: 200, - }, TestData.serverResponse.incompleteWallet); - - client.request = request; - client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); - client.createAddress(function(err, x) { - err.should.contain('Incomplete'); - done(); - }); - }); - it('should reject wallets with bad signatures', function(done) { - var request = sinon.stub(); - // Wallet request - request.onCall(0).yields(null, { - statusCode: 200, - }, TestData.serverResponse.corruptWallet22); + helpers.createAndJoinWallet(clients, 2, 3, function(err) { + should.not.exist(err); - client.request = request; - client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); - client.createAddress(function(err, x) { - err.should.contain('verified'); - done(); + // Get right response + var data = clients[0]._load(function(err, data) { + var url = '/v1/wallets/'; + clients[0]._doGetRequest(url, data, function(err, x) { + + // Tamper data + x.wallet.copayers[0].xPubKey = x.wallet.copayers[1].xPubKey; + + // Tamper response + clients[1]._doGetRequest = sinon.stub().yields(null, x); + + clients[1].getBalance(function(err, x) { + err.should.contain('verified'); + done(); + }); + }); + }); }); }); - it('should reject wallets with missing signatures ', function(done) { - var request = sinon.stub(); - // Wallet request - request.onCall(0).yields(null, { - statusCode: 200, - }, TestData.serverResponse.corruptWallet222); + it('should reject wallets with missing signatures', function(done) { + helpers.createAndJoinWallet(clients, 2, 3, function(err) { + should.not.exist(err); - client.request = request; - client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); - client.createAddress(function(err, x) { - err.should.contain('verified'); - done(); + // Get right response + var data = clients[0]._load(function(err, data) { + var url = '/v1/wallets/'; + clients[0]._doGetRequest(url, data, function(err, x) { + + // Tamper data + delete x.wallet.copayers[1].xPubKey; + + // Tamper response + clients[1]._doGetRequest = sinon.stub().yields(null, x); + + clients[1].getBalance(function(err, x) { + err.should.contain('verified'); + done(); + }); + }); + }); }); }); + it('should reject wallets missing caller"s pubkey', function(done) { - var request = sinon.stub(); - // Wallet request - request.onCall(0).yields(null, { - statusCode: 200, - }, TestData.serverResponse.missingMyPubKey); + helpers.createAndJoinWallet(clients, 2, 3, function(err) { + should.not.exist(err); - client.request = request; - client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); - client.createAddress(function(err, x) { - err.should.contain('verified'); - done(); + // Get right response + var data = clients[0]._load(function(err, data) { + var url = '/v1/wallets/'; + clients[0]._doGetRequest(url, data, function(err, x) { + + // Tamper data. Replace caller's pubkey + x.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey; + // Add a correct signature + x.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage( + x.wallet.copayers[1].xPubKey, data.walletPrivKey), + + // Tamper response + clients[1]._doGetRequest = sinon.stub().yields(null, x); + + clients[1].getBalance(function(err, x) { + err.should.contain('verified'); + done(); + }); + }); + }); }); }); }); - describe('#createAddress ', function() { - it('should check address ', function(done) { - var response = { - createdOn: 1424105995, - address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - path: 'm/2147483647/0/7', - publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] - }; - var request = sinon.mock().yields(null, { - statusCode: 200 - }, response); - client.request = request; - - - client.createAddress(function(err, x) { + describe('Address Creation', function() { + it('should be able to create address in all copayers in a 2-3 wallet', function(done) { + this.timeout(5000); + helpers.createAndJoinWallet(clients, 2, 3, function(err) { should.not.exist(err); - x.address.should.equal('2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq'); - done(); + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + clients[1].createAddress(function(err, x1) { + should.not.exist(err); + should.exist(x1.address); + clients[2].createAddress(function(err, x2) { + should.not.exist(err); + should.exist(x2.address); + done(); + }); + }); + }); }); }); + it('should see balance on address created by others', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + should.not.exist(err); + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + blockExplorerMock.setUtxo(x0, 10, w.m); + clients[0].getBalance(function(err, bal0) { + should.not.exist(err); + bal0.totalAmount.should.equal(10 * 1e8); + bal0.lockedAmount.should.equal(0); + clients[1].getBalance(function(err, bal1) { + bal1.totalAmount.should.equal(10 * 1e8); + bal1.lockedAmount.should.equal(0); + done(); + }); + }); + }); + }); + }); + }); + + + describe('Wallet Backups and Mobility', function() { + + it('round trip #import #export', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { + should.not.exist(err); + clients[0].export(function(err, str) { + should.not.exist(err); + var original = JSON.parse(fsmock._get('client0')); + clients[2].import(str, function(err, wallet) { + should.not.exist(err); + var clone = JSON.parse(fsmock._get('client2')); + delete original.walletPrivKey; // no need to persist it. + clone.should.deep.equal(original); + done(); + }); + + }); + }); + }); + it('should recreate a wallet, create addresses and receive money', function(done) { + var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; + clients[0].import(backup, function(err, wallet) { + should.not.exist(err); + clients[0].reCreateWallet('pepe', function(err, wallet) { + should.not.exist(err); + + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + blockExplorerMock.setUtxo(x0, 10, 2); + clients[0].getBalance(function(err, bal0) { + should.not.exist(err); + bal0.totalAmount.should.equal(10 * 1e8); + bal0.lockedAmount.should.equal(0); + done(); + }); + }); + }); + }); + }); + }); + + + describe.only('Send TXs', function() { + it('Send and broadcast in 1-1 wallet', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + should.exist(x0.address); + blockExplorerMock.setUtxo(x0, 10, 1); + var opts = { + amount: 1000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hola 1-1', + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); +console.log('[clientApi.js.372]',x); //TODO + done(); + }); + }); + }); + }); + }); + /* + describe('TODO', function(x) { it('should detect fake addresses ', function(done) { var response = { createdOn: 1424105995, @@ -246,25 +398,6 @@ describe('client API ', function() { }); - describe('#export & #import 2-2 wallet', function() { - it('round trip ', function(done) { - client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete22)); - client.export(function(err, str) { - should.not.exist(err); - - client.storage.fs.readFile = sinon.stub().yields(null); - client.import(str, function(err, wallet) { - should.not.exist(err); - var wallet = JSON.parse(client.storage.fs.writeFile.getCall(0).args[1]); - TestData.storage.complete22.should.deep.equal(wallet); - - done(); - }); - }); - }); - }); - - describe('#getTxProposals', function() { it('should return tx proposals and decrypt message', function(done) { client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11)); @@ -283,10 +416,6 @@ describe('client API ', function() { }); }); - describe('#recreate', function() { - it.skip('Should recreate a wallet acording stored data', function(done) {}); - }); - describe('#sendTxProposal ', function() { it('should send tx proposal with encrypted message', function(done) { client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11)); @@ -356,4 +485,5 @@ describe('client API ', function() { }); }); }); + */ });