diff --git a/README.md b/README.md index 8dfb666..3dc8060 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,12 @@ Returns: Returns: * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.status` is probably needed in this case. + +`/v1/addresses/scan`: Start an address scan process looking for activity. + + Optional Arguments: + * includeCopayerBranches: Scan all copayer branches following BIP45 recommendation (defaults to false). + ## DELETE Endpoinds `/v1/txproposals/:id/`: Deletes a transaction proposal. Only the creator can delete a TX Proposal, and only if it has no other signatures or rejections diff --git a/lib/expressapp.js b/lib/expressapp.js index bb73d52..74f0bd6 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -53,6 +53,8 @@ ExpressApp.start = function(opts) { //var accessLogStream = fs.createWriteStream(__dirname + '/access.log', {flags: 'a'}) //app.use(morgan('combined', {stream: accessLogStream})) app.use(require('morgan')('dev')); + } else { + log.level = 'silent'; } @@ -284,6 +286,16 @@ ExpressApp.start = function(opts) { }); }); + router.post('/v1/addresses/scan/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.startScan(req.body, function(err) { + if (err) return returnError(err, res, req); + res.end(); + }); + }); + }); + + app.use(opts.basePath || '/bws/api', router); return app; }; 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 a72a2dd..07ba841 100644 --- a/lib/server.js +++ b/lib/server.js @@ -253,10 +253,11 @@ WalletService.prototype._emit = function(eventName, args) { /** * _notify * - * @param type - * @param data + * @param {String} type + * @param {Object} data + * @param {Boolean} isGlobal - If true, the notification is not issued on behalf of any particular copayer (defaults to false) */ -WalletService.prototype._notify = function(type, data) { +WalletService.prototype._notify = function(type, data, isGlobal) { var self = this; log.debug('Notification', type, data); @@ -270,7 +271,7 @@ WalletService.prototype._notify = function(type, data) { type: type, data: data, ticker: this.notifyTicker++, - creatorId: copayerId, + creatorId: isGlobal ? null : copayerId, walletId: walletId, }); this.storage.storeNotification(walletId, n, function() { @@ -1143,8 +1144,6 @@ WalletService.prototype.scan = function(opts, cb) { opts = opts || {}; - var allAddresses = []; - function deriveAddresses(size, derivator, cb) { async.mapSeries(_.range(size), function(i, next) { setTimeout(function() { @@ -1160,6 +1159,7 @@ WalletService.prototype.scan = function(opts, cb) { function scanBranch(derivator, cb) { var activity = true; + var allAddresses = []; async.whilst(function() { return activity; }, function(next) { @@ -1172,15 +1172,16 @@ WalletService.prototype.scan = function(opts, cb) { next(); }); }); - }, cb); + }, function(err) { + return cb(err, _.flatten(allAddresses)); + }); }; 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) { @@ -1193,17 +1194,49 @@ WalletService.prototype.scan = function(opts, cb) { }); async.eachSeries(derivators, function(derivator, next) { - scanBranch(derivator, next); - }, function(err) { - if (err) return cb(err); - self.storage.storeAddressAndWallet(wallet, _.flatten(allAddresses), function(err) { - return cb(err); + scanBranch(derivator, function(err, addresses) { + if (err) return next(err); + self.storage.storeAddressAndWallet(wallet, addresses, next); }); - }); + }, 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, true); + }; + + 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/lib/wsapp.js b/lib/wsapp.js index d307be2..2a56a38 100644 --- a/lib/wsapp.js +++ b/lib/wsapp.js @@ -20,7 +20,7 @@ var io, bcMonitor; var WsApp = function() {}; -WsApp._unauthorized = function() { +WsApp._unauthorized = function(socket) { socket.emit('unauthorized'); socket.disconnect(); }; @@ -52,10 +52,10 @@ WsApp.start = function(server) { socket.emit('challenge', socket.nonce); socket.on('authorize', function(data) { - if (data.message != socket.nonce) return WsApp.unauthorized(); + if (data.message != socket.nonce) return WsApp._unauthorized(socket); WalletService.getInstanceWithAuth(data, function(err, service) { - if (err) return WsApp.unauthorized(); + if (err) return WsApp._unauthorized(socket); socket.join(service.walletId); socket.emit('authorized'); diff --git a/package.json b/package.json index 1aa796c..9d9d755 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bitcore-wallet-service", "description": "A service for Mutisig HD Bitcoin Wallets", "author": "BitPay Inc", - "version": "0.0.18", + "version": "0.0.19", "keywords": [ "bitcoin", "copay", diff --git a/test/integration/server.js b/test/integration/server.js index a5558b6..fd31c01 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,57 @@ 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') { + should.not.exist(n.creatorId); + 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); + }); + }); + }); });