diff --git a/lib/client/api.js b/lib/client/api.js index 9b3b03f..fc73851 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -92,7 +92,7 @@ function API(opts) { util.inherits(API, events.EventEmitter); API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) { - this.credentials = Credentials.seedFromExtendedPrivateKey(xPrivKey); + this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey); }; API.prototype.seedFromAirGapped = function(seed) { @@ -254,15 +254,25 @@ API.prototype.openWallet = function(cb) { if (err) return cb(err); var wallet = ret.wallet; - if (wallet.status != 'complete') - return cb('Wallet Incomplete'); + if (wallet.status != 'complete') return cb('Wallet Incomplete'); - if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { - return cb(new ServerCompromisedError( - 'Copayers in the wallet could not be verified to have known the wallet secret')); + if (!!self.credentials.walletPrivKey) { + if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { + return cb(new ServerCompromisedError( + 'Copayers in the wallet could not be verified to have known the wallet secret')); + } + } else { + log.warn('Could not perform verification of other copayers in the wallet'); } self.credentials.addPublicKeyRing(_.pluck(wallet.copayers, 'xPubKey')); + if (!self.credentials.hasWalletInfo()) { + var me = _.find(wallet.copayers, { + id: self.credentials.copayerId + }); + self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, null, me.name); + } + return cb(null, true); }); }; diff --git a/lib/client/credentials.js b/lib/client/credentials.js index 4a8806d..29f9373 100644 --- a/lib/client/credentials.js +++ b/lib/client/credentials.js @@ -105,6 +105,10 @@ Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walle } }; +Credentials.prototype.hasWalletInfo = function() { + return !!this.walletId; +}; + Credentials.prototype.addPublicKeyRing = function(publicKeyRing) { this.publicKeyRing = _.clone(publicKeyRing); }; diff --git a/lib/client/verifier.js b/lib/client/verifier.js index 84f1518..f45f101 100644 --- a/lib/client/verifier.js +++ b/lib/client/verifier.js @@ -18,6 +18,7 @@ Verifier.checkAddress = function(credentials, address) { }; Verifier.checkCopayers = function(credentials, copayers) { + $.checkState(credentials.walletPrivKey); var walletPubKey = Bitcore.PrivateKey.fromString(credentials.walletPrivKey).toPublicKey().toString(); if (copayers.length != credentials.n) { diff --git a/test/integration/client.js b/test/integration/client.js index a2ad67e..0f4adac 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('lodash'); +var $ = require('preconditions').singleton(); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); @@ -20,6 +21,7 @@ var TestData = require('../testdata'); var helpers = {}; helpers.getRequest = function(app) { + $.checkArgument(app); return function(args, cb) { var req = request(app); var r = req[args.method](args.relUrl); @@ -38,6 +40,13 @@ helpers.getRequest = function(app) { }; }; +helpers.newClient = function(app) { + $.checkArgument(app); + return new Client({ + request: helpers.getRequest(app), + }); +}; + helpers.createAndJoinWallet = function(clients, m, n, cb) { clients[0].createWallet('wallet name', 'creator', m, n, 'testnet', function(err, secret) { @@ -148,9 +157,7 @@ describe('client API ', function() { }); // Generates 5 clients clients = _.map(_.range(5), function(i) { - return new Client({ - request: helpers.getRequest(app), - }); + return helpers.newClient(app); }); blockExplorerMock.reset(); }); @@ -179,10 +186,8 @@ describe('client API ', function() { }); var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); - var client = new Client({ - storage: s2, - }); - client.request = helpers.getRequest(app); + var client = helpers.newClient(app); + client.storage = s2; client.createWallet('1', '2', 1, 1, 'testnet', function(err) { err.code.should.equal('ERROR'); @@ -206,10 +211,8 @@ describe('client API ', function() { }); var s2 = sinon.stub(); s2.load = sinon.stub().yields(null); - var client = new Client({ - storage: s2, - }); - client.request = helpers.getRequest(app); + var client = helpers.newClient(app); + client.storage = s2; client.createWallet('1', '2', 1, 1, 'testnet', function(err) { err.code.should.equal('ERROR'); @@ -865,92 +868,120 @@ describe('client API ', function() { it.skip('should get paginated transaction history', function(done) {}); }); - describe('Export & Import', function() { - var address, importedClient; - beforeEach(function(done) { - importedClient = null; - helpers.createAndJoinWallet(clients, 1, 1, function() { - clients[0].createAddress(function(err, addr) { - should.not.exist(err); - should.exist(addr.address); - address = addr.address; - done(); + describe('Mobility, backup & recovery', function() { + describe('Export & Import', function() { + describe('Success', function() { + var address, importedClient; + beforeEach(function(done) { + importedClient = null; + helpers.createAndJoinWallet(clients, 1, 1, function() { + clients[0].createAddress(function(err, addr) { + should.not.exist(err); + should.exist(addr.address); + address = addr.address; + done(); + }); + }); + }); + afterEach(function(done) { + importedClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.length.should.equal(1); + list[0].address.should.equal(address); + done(); + }); }); - }); - }); - afterEach(function(done) { - importedClient.getMainAddresses({}, function(err, list) { - should.not.exist(err); - should.exist(list); - list.length.should.equal(1); - list[0].address.should.equal(address); - done(); - }); - }); - it('should export & import', function() { - var exported = clients[0].export(); + it('should export & import', function() { + var exported = clients[0].export(); - importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported); - }); - it.skip('should export & import compressed', function() { - var walletId = clients[0].credentials.walletId; - var walletName = clients[0].credentials.walletName; - var copayerName = clients[0].credentials.copayerName; + importedClient = helpers.newClient(app); + importedClient.import(exported); + }); + it.skip('should export & import compressed', function() { + var walletId = clients[0].credentials.walletId; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; - var exported = clients[0].export({ - compressed: true - }); + var exported = clients[0].export({ + compressed: true + }); - importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported, { - compressed: true - }); - importedClient.credentials.walletId.should.equal(walletId); - importedClient.credentials.walletName.should.equal(walletName); - importedClient.credentials.copayerName.should.equal(copayerName); - }); - it('should export & import encrypted', function() { - var xPrivKey = clients[0].credentials.xPrivKey; - should.exist(xPrivKey); + importedClient = helpers.newClient(app); + importedClient.import(exported, { + compressed: true + }); + importedClient.credentials.walletId.should.equal(walletId); + importedClient.credentials.walletName.should.equal(walletName); + importedClient.credentials.copayerName.should.equal(copayerName); + }); + it('should export & import encrypted', function() { + var xPrivKey = clients[0].credentials.xPrivKey; + should.exist(xPrivKey); - var exported = clients[0].export({ - password: '123' - }); - exported.should.not.contain(xPrivKey); + var exported = clients[0].export({ + password: '123' + }); + exported.should.not.contain(xPrivKey); - importedClient = new Client({ - request: helpers.getRequest(app), + importedClient = helpers.newClient(app); + importedClient.import(exported, { + password: '123' + }); + should.exist(importedClient.credentials.xPrivKey); + importedClient.credentials.xPrivKey.should.equal(xPrivKey); + }); + it('should export & import compressed & encrypted', function() { + var exported = clients[0].export({ + compressed: true, + password: '123' + }); + + importedClient = helpers.newClient(app); + importedClient.import(exported, { + compressed: true, + password: '123' + }); + }); }); - importedClient.import(exported, { - password: '123' + describe('Fail', function() { + it.skip('should fail to export compressed & import uncompressed', function() {}); + it.skip('should fail to export uncompressed & import compressed', function() {}); + it.skip('should fail to export unencrypted & import with password', function() {}); + it.skip('should fail to export encrypted & import with incorrect password', function() {}); }); - should.exist(importedClient.credentials.xPrivKey); - importedClient.credentials.xPrivKey.should.equal(xPrivKey); }); - it('should export & import compressed & encrypted', function() { - var exported = clients[0].export({ - compressed: true, - password: '123' - }); - importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported, { - compressed: true, - password: '123' + describe('Recovery', function() { + it('should be able to regain access to a 1-1 wallet with just the xPriv', function(done) { + helpers.createAndJoinWallet(clients, 1, 1, function() { + var xpriv = clients[0].credentials.xPrivKey; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; + + clients[0].createAddress(function(err, addr) { + should.not.exist(err); + should.exist(addr); + + var recoveryClient = helpers.newClient(app); + recoveryClient.seedFromExtendedPrivateKey(xpriv); + recoveryClient.openWallet(function(err) { + console.log(err); + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); + }); + }); + }); + }); }); }); - it.skip('should fail to export compressed & import uncompressed', function() {}); - it.skip('should fail to export uncompressed & import compressed', function() {}); - it.skip('should fail to export unencrypted & import with password', function() {}); - it.skip('should fail to export encrypted & import with incorrect password', function() {}); }); describe('Air gapped related flows', function() { @@ -960,9 +991,7 @@ describe('client API ', function() { }); var seed = airgapped.getSeed(); - var proxy = new Client({ - request: helpers.getRequest(app), - }); + var proxy = helpers.newClient(app); proxy.seedFromAirGapped(seed); should.not.exist(proxy.credentials.xPrivKey); proxy.createWallet('wallet name', 'creator', 1, 1, 'testnet', function(err) { @@ -981,9 +1010,7 @@ describe('client API ', function() { }); var seed = airgapped.getSeed(); - var proxy = new Client({ - request: helpers.getRequest(app), - }); + var proxy = helpers.newClient(app); proxy.seedFromAirGapped(seed); async.waterfall([