diff --git a/lib/model/wallet.js b/lib/model/wallet.js index cfeb66a..73e8853 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -25,6 +25,7 @@ Wallet.create = function(opts) { x.m = opts.m; x.n = opts.n; x.status = 'pending'; + x.scanning = false; x.publicKeyRing = []; x.addressIndex = 0; x.copayers = []; @@ -44,6 +45,7 @@ Wallet.fromObj = function(obj) { x.m = obj.m; x.n = obj.n; x.status = obj.status; + x.scanning = obj.scanning; x.publicKeyRing = obj.publicKeyRing; x.copayers = _.map(obj.copayers, function(copayer) { return Copayer.fromObj(copayer); @@ -135,6 +137,10 @@ Wallet.prototype.isComplete = function() { return this.status == 'complete'; }; +Wallet.prototype.isScanning = function() { + return this.scanning; +}; + Wallet.prototype.createAddress = function(isChange) { $.checkState(this.isComplete()); diff --git a/lib/server.js b/lib/server.js index 4073123..4f95fc6 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1180,8 +1180,7 @@ WalletService.prototype.scan = function(opts, cb) { 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')); + if (!wallet.isComplete()) return cb(new ClientError('Wallet is not complete')); var derivators = []; _.each([false, true], function(isChange) { @@ -1201,8 +1200,42 @@ WalletService.prototype.scan = function(opts, cb) { }, cb); }); }); +}; + +/** + * Start a scan process. + * + * @param {Object} opts + * @param {Boolean} opts.includeCopayerBranches (defaults to false) + */ +WalletService.prototype.startScan = function(opts, cb) { + var self = this; + + function scanFinished(err) { + var data = {}; + if (err) { + data.result = 'error'; + data.error = err; + } else { + data.result = 'success'; + } + self._notify('ScanFinished', data); + }; + + 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')); + + setTimeout(function() { + self.scan(opts, scanFinished); + }, 0); + return cb() + }); + }); }; + module.exports = WalletService; module.exports.ClientError = ClientError; diff --git a/test/integration/server.js b/test/integration/server.js index a5558b6..eb0136f 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -2518,11 +2518,18 @@ describe('Wallet service', function() { }); describe('#scan', function() { + var server, wallet; var scanConfigOld = WalletService.scanConfig; - beforeEach(function() { + beforeEach(function(done) { this.timeout(5000); WalletService.scanConfig.SCAN_WINDOW = 2; WalletService.scanConfig.DERIVATION_DELAY = 0; + + helpers.createAndJoinWallet(1, 2, function(s, w) { + server = s; + wallet = w; + done(); + }); }); afterEach(function() { WalletService.scanConfig = scanConfigOld; @@ -2530,76 +2537,70 @@ describe('Wallet service', function() { it('should scan main addresses', function(done) { helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']); - helpers.createAndJoinWallet(1, 2, function(server, wallet) { - var expectedPaths = [ - 'm/2147483647/0/0', - 'm/2147483647/0/1', - 'm/2147483647/0/2', - 'm/2147483647/0/3', - 'm/2147483647/1/0', - 'm/2147483647/1/1', - ]; - server.scan({}, function(err) { - should.not.exist(err); - server.storage.fetchAddresses(wallet.id, function(err, addresses) { - should.exist(addresses); - addresses.length.should.equal(expectedPaths.length); - var paths = _.pluck(addresses, 'path'); - _.difference(paths, expectedPaths).length.should.equal(0); - server.createAddress({}, function(err, address) { - should.not.exist(err); - address.path.should.equal('m/2147483647/0/4'); - done(); - }); - }) - }); + var expectedPaths = [ + 'm/2147483647/0/0', + 'm/2147483647/0/1', + 'm/2147483647/0/2', + 'm/2147483647/0/3', + 'm/2147483647/1/0', + 'm/2147483647/1/1', + ]; + server.scan({}, function(err) { + should.not.exist(err); + server.storage.fetchAddresses(wallet.id, function(err, addresses) { + should.exist(addresses); + addresses.length.should.equal(expectedPaths.length); + var paths = _.pluck(addresses, 'path'); + _.difference(paths, expectedPaths).length.should.equal(0); + server.createAddress({}, function(err, address) { + should.not.exist(err); + address.path.should.equal('m/2147483647/0/4'); + done(); + }); + }) }); }); it('should scan main addresses & copayer addresses', function(done) { helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']); - helpers.createAndJoinWallet(1, 2, function(server, wallet) { - var expectedPaths = [ - 'm/2147483647/0/0', - 'm/2147483647/0/1', - 'm/2147483647/0/2', - 'm/2147483647/0/3', - 'm/2147483647/1/0', - 'm/2147483647/1/1', - 'm/0/0/0', - 'm/0/0/1', - 'm/0/1/0', - 'm/0/1/1', - 'm/1/0/0', - 'm/1/0/1', - 'm/1/1/0', - 'm/1/1/1', - ]; - server.scan({ - includeCopayerBranches: true - }, function(err) { - should.not.exist(err); - server.storage.fetchAddresses(wallet.id, function(err, addresses) { - should.exist(addresses); - addresses.length.should.equal(expectedPaths.length); - var paths = _.pluck(addresses, 'path'); - _.difference(paths, expectedPaths).length.should.equal(0); - done(); - }) - }); + var expectedPaths = [ + 'm/2147483647/0/0', + 'm/2147483647/0/1', + 'm/2147483647/0/2', + 'm/2147483647/0/3', + 'm/2147483647/1/0', + 'm/2147483647/1/1', + 'm/0/0/0', + 'm/0/0/1', + 'm/0/1/0', + 'm/0/1/1', + 'm/1/0/0', + 'm/1/0/1', + 'm/1/1/0', + 'm/1/1/1', + ]; + server.scan({ + includeCopayerBranches: true + }, function(err) { + should.not.exist(err); + server.storage.fetchAddresses(wallet.id, function(err, addresses) { + should.exist(addresses); + addresses.length.should.equal(expectedPaths.length); + var paths = _.pluck(addresses, 'path'); + _.difference(paths, expectedPaths).length.should.equal(0); + done(); + }) }); }); it('should restore wallet balance', function(done) { async.waterfall([ function(next) { - helpers.createAndJoinWallet(1, 2, function(server, wallet) { - helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) { - should.exist(utxos); - helpers.stubAddressActivity(_.pluck(utxos, 'address')); - server.getBalance({}, function(err, balance) { - balance.totalAmount.should.equal(helpers.toSatoshi(6)); - next(null, server, wallet); - }); + helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) { + should.exist(utxos); + helpers.stubAddressActivity(_.pluck(utxos, 'address')); + server.getBalance({}, function(err, balance) { + balance.totalAmount.should.equal(helpers.toSatoshi(6)); + next(null, server, wallet); }); }); }, @@ -2834,6 +2835,56 @@ describe('Wallet service', function() { }); }); }); + + describe('#startScan', function() { + var server, wallet; + var scanConfigOld = WalletService.scanConfig; + beforeEach(function(done) { + this.timeout(5000); + WalletService.scanConfig.SCAN_WINDOW = 2; + WalletService.scanConfig.DERIVATION_DELAY = 0; + + helpers.createAndJoinWallet(1, 2, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + afterEach(function() { + WalletService.scanConfig = scanConfigOld; + WalletService.onNotification(function() {}); + }); + + it('should start an asynchronous scan', function(done) { + helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']); + var expectedPaths = [ + 'm/2147483647/0/0', + 'm/2147483647/0/1', + 'm/2147483647/0/2', + 'm/2147483647/0/3', + 'm/2147483647/1/0', + 'm/2147483647/1/1', + ]; + WalletService.onNotification(function(n) { + if (n.type == 'ScanFinished') { + server.storage.fetchAddresses(wallet.id, function(err, addresses) { + should.exist(addresses); + addresses.length.should.equal(expectedPaths.length); + var paths = _.pluck(addresses, 'path'); + _.difference(paths, expectedPaths).length.should.equal(0); + server.createAddress({}, function(err, address) { + should.not.exist(err); + address.path.should.equal('m/2147483647/0/4'); + done(); + }); + }) + } + }); + server.startScan({}, function(err) { + should.not.exist(err); + }); + }); + }); });