From 29920abdb3771940d0f5d4eb5f551a3f19d93507 Mon Sep 17 00:00:00 2001 From: Yemel Jardi Date: Wed, 18 Jun 2014 10:58:34 -0300 Subject: [PATCH 1/3] Add wallet addresses index discovery on importing backup --- js/models/blockchain/Insight.js | 21 +++++++++++ js/models/core/AddressIndex.js | 2 +- js/models/core/PublicKeyRing.js | 2 - js/models/core/Wallet.js | 66 +++++++++++++++++++++++++++++++++ js/models/core/WalletFactory.js | 5 +++ test/test.Wallet.js | 44 ++++++++++++++++++++++ test/test.blockchain.Insight.js | 30 +++++++++++++++ 7 files changed, 167 insertions(+), 3 deletions(-) diff --git a/js/models/blockchain/Insight.js b/js/models/blockchain/Insight.js index 897dd22c8..7803fbd10 100644 --- a/js/models/blockchain/Insight.js +++ b/js/models/blockchain/Insight.js @@ -159,6 +159,27 @@ Insight.prototype.sendRawTransaction = function(rawtx, cb) { }); }; +Insight.prototype.checkActivity = function(addresses, cb) { + if (!addresses) throw new Error('address must be set'); + + this.getTransactions(addresses, function onResult(txs) { + var flatArray = function (xss) { return xss.reduce(function(r, xs) { return r.concat(xs); }, []); }; + var getInputs = function (t) { return t.vin.map(function (vin) { return vin.addr }); }; + var getOutputs = function (t) { return flatArray( + t.vout.map(function (vout) { return vout.scriptPubKey.addresses; }) + );}; + + var activityMap = new Array(addresses.length); + var activeAddress = flatArray(txs.map(function(t) { return getInputs(t).concat(getOutputs(t)); })); + activeAddress.forEach(function (addr) { + var index = addresses.indexOf(addr); + if (index != -1) activityMap[index] = true; + }); + + cb(null, activityMap); + }); +}; + Insight.prototype._request = function(options, callback) { diff --git a/js/models/core/AddressIndex.js b/js/models/core/AddressIndex.js index 8bfe2ffb6..7920b44a2 100644 --- a/js/models/core/AddressIndex.js +++ b/js/models/core/AddressIndex.js @@ -30,7 +30,7 @@ AddressIndex.prototype.toObj = function() { AddressIndex.prototype.checkRange = function(index, isChange) { if ((isChange && index > this.changeIndex) || (!isChange && index > this.receiveIndex)) { - throw new Error('Out of bounds at index %d isChange: %d', index, isChange); + throw new Error('Out of bounds at index ' + index + ' isChange: ' + isChange); } }; diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index 94cc110f4..fdb0ad8e8 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -157,8 +157,6 @@ PublicKeyRing.prototype.getPubKeys = function(index, isChange) { // TODO this could be cached PublicKeyRing.prototype.getRedeemScript = function (index, isChange) { - this.indexes.checkRange(index, isChange); - var pubKeys = this.getPubKeys(index, isChange); var script = Script.createMultisig(this.requiredCopayers, pubKeys); return script; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 2bb53df60..925bdb674 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -8,6 +8,7 @@ var coinUtil = bitcore.util; var buffertools = bitcore.buffertools; var Builder = bitcore.TransactionBuilder; var http = require('http'); +var async = require('async'); var EventEmitter = imports.EventEmitter || require('events').EventEmitter; var copay = copay || require('../../../copay'); var SecureRandom = bitcore.SecureRandom; @@ -711,6 +712,71 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos return ntxid; }; +Wallet.prototype.updateIndexes = function(callback) { + var self = this; + var start = self.publicKeyRing.indexes.changeIndex; + self.indexDiscovery(start, true, 20, function(err, changeIndex) { + if (err) return callback(err); + if (changeIndex != -1) + self.publicKeyRing.indexes.changeIndex = changeIndex + 1; + + start = self.publicKeyRing.indexes.receiveIndex; + self.indexDiscovery(start, false, 20, function(err, receiveIndex) { + if (err) return callback(err); + if (receiveIndex != -1) + self.publicKeyRing.indexes.receiveIndex = receiveIndex + 1; + + self.emit('publicKeyRingUpdated'); + self.store(); + callback(); + }); + }); +} + +Wallet.prototype.deriveAddresses = function(index, amout, isChange) { + var ret = new Array(amout); + for(var i = 0; i < amout; i++) { + ret[i] = this.publicKeyRing.getAddress(index + i, isChange).toString(); + } + return ret; +} + +// This function scans the publicKeyRing branch starting at index @start and reports the index with last activity, +// using a scan window of @gap. The argument @change defines the branch to scan: internal or external. +// Returns -1 if no activity is found in range. +Wallet.prototype.indexDiscovery = function(start, change, gap, cb) { + var scanIndex = start; + var lastActive = -1; + var hasActivity = false; + + var self = this; + async.doWhilst( + function _do(next) { + // Optimize window to minimize the derivations. + var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; + var addresses = self.deriveAddresses(scanIndex, scanWindow, change); + self.blockchain.checkActivity(addresses, function(err, actives){ + if (err) throw err; + + // Check for new activities in the newlly scanned addresses + var recentActive = actives.reduce(function(r, e, i) { + return e ? scanIndex + i : r; + }, lastActive); + hasActivity = lastActive != recentActive; + lastActive = recentActive; + scanIndex += scanWindow; + next(); + }); + }, + function _while() { return hasActivity; }, + function _finnaly(err) { + if (err) return cb(err); + cb(null, lastActive); + } + ); +} + + Wallet.prototype.disconnect = function() { this.log('## DISCONNECTING'); this.network.disconnect(); diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index 6ecc70b46..b84233e4e 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -71,6 +71,11 @@ WalletFactory.prototype.fromEncryptedObj = function(base64, password) { var walletObj = this.storage.import(base64); if (!walletObj) return false; var w = this.fromObj(walletObj); + var self = this; + w.updateIndexes(function(err) { + if (err) throw err; + self.log('Indexes updated'); + }); return w; }; diff --git a/test/test.Wallet.js b/test/test.Wallet.js index c1557b3b6..5b894eba6 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -593,4 +593,48 @@ describe('Wallet model', function() { w.getNetworkName().should.equal('testnet'); }); + var mockFakeActivity = function(w, isChange, f) { + var ADDRESSES = w.deriveAddresses(0, 20, isChange); + w.blockchain.checkActivity = function(addresses, cb) { + var activity = new Array(addresses.length); + for(var i=0; i Date: Wed, 18 Jun 2014 14:24:48 -0300 Subject: [PATCH 2/3] Add more tests --- test/test.Wallet.js | 54 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 5b894eba6..a5d4f06e7 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -593,18 +593,23 @@ describe('Wallet model', function() { w.getNetworkName().should.equal('testnet'); }); - var mockFakeActivity = function(w, isChange, f) { - var ADDRESSES = w.deriveAddresses(0, 20, isChange); + var mockFakeActivity = function(w, f) { + var ADDRESSES_CHANGE = w.deriveAddresses(0, 20, true); + var ADDRESSES_RECEIVE = w.deriveAddresses(0, 20, false); w.blockchain.checkActivity = function(addresses, cb) { var activity = new Array(addresses.length); - for(var i=0; i Date: Thu, 19 Jun 2014 11:35:38 -0300 Subject: [PATCH 3/3] move update indexes to the loading screen --- js/controllers/import.js | 25 ++++++++++--------------- js/models/core/WalletFactory.js | 15 ++++++++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/js/controllers/import.js b/js/controllers/import.js index 639f6a4a1..2157b4f00 100644 --- a/js/controllers/import.js +++ b/js/controllers/import.js @@ -6,21 +6,16 @@ angular.module('copayApp.controllers').controller('ImportController', var reader = new FileReader(); var _importBackup = function(encryptedObj) { Passphrase.getBase64Async($scope.password, function(passphrase){ - var w, errMsg; - try { - w = walletFactory.fromEncryptedObj(encryptedObj, passphrase); - } catch(e) { - errMsg = e.message; - } - if (!w) { - $scope.loading = false; - $rootScope.$flashMessage = { message: errMsg || 'Wrong password', type: 'error'}; - $rootScope.$digest(); - return; - } - $rootScope.wallet = w; - - controllerUtils.startNetwork($rootScope.wallet, $scope); + walletFactory.import(encryptedObj, passphrase, function(err, w) { + if (err) { + $scope.loading = false; + $rootScope.$flashMessage = { message: err.errMsg || 'Wrong password', type: 'error'}; + $rootScope.$digest(); + return; + } + $rootScope.wallet = w; + controllerUtils.startNetwork($rootScope.wallet, $scope); + }); }); }; diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index b84233e4e..8576bb901 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -71,14 +71,19 @@ WalletFactory.prototype.fromEncryptedObj = function(base64, password) { var walletObj = this.storage.import(base64); if (!walletObj) return false; var w = this.fromObj(walletObj); - var self = this; - w.updateIndexes(function(err) { - if (err) throw err; - self.log('Indexes updated'); - }); return w; }; +WalletFactory.prototype.import = function(base64, password, cb) { + var self = this; + var w = self.fromEncryptedObj(base64, password); + w.updateIndexes(function(err) { + if (err) return cb(err); + self.log('Indexes updated'); + cb(null, w); + }); +} + WalletFactory.prototype.read = function(walletId) { if (!this._checkRead(walletId)) return false;