diff --git a/index.html b/index.html index 9c4235557..abd3b8dbe 100644 --- a/index.html +++ b/index.html @@ -49,12 +49,11 @@ Network Error. Attempting to reconnect.. - + - + @@ -91,7 +90,7 @@ + $root.wallet.isComplete() && !$root.wallet.isLocked"> diff --git a/js/controllers/copayers.js b/js/controllers/copayers.js index 551a1cf60..0fd363eeb 100644 --- a/js/controllers/copayers.js +++ b/js/controllers/copayers.js @@ -1,17 +1,16 @@ 'use strict'; angular.module('copayApp.controllers').controller('CopayersController', - function($scope, $rootScope, $location, controllerUtils) { - if (!$rootScope.wallet.isReady()) { + function($scope, $rootScope, $location) { + + if (!$rootScope.wallet.isComplete()) { $rootScope.title = 'Waiting copayers for ' + $rootScope.wallet.getName(); } $scope.loading = false; $scope.secret = $rootScope.wallet.getSecret(); $scope.goToWallet = function() { - controllerUtils.updateAddressList(); $location.path('/homeWallet'); - }; $scope.copayersList = function() { diff --git a/js/controllers/create.js b/js/controllers/create.js index 64ee88bcf..753ca1c8d 100644 --- a/js/controllers/create.js +++ b/js/controllers/create.js @@ -1,10 +1,9 @@ 'use strict'; angular.module('copayApp.controllers').controller('CreateController', - function($scope, $rootScope, $location, $timeout, controllerUtils, backupService, notification, defaults) { + function($scope, $rootScope, $location, $timeout, identityService, backupService, notification, defaults) { $rootScope.fromSetup = true; - $rootScope.starting = false; $scope.loading = false; $scope.walletPassword = $rootScope.walletPassword; $scope.isMobile = !!window.cordova; @@ -45,7 +44,6 @@ angular.module('copayApp.controllers').controller('CreateController', notification.error('Error', 'Please enter the required fields'); return; } - $scope.loading = true; var opts = { requiredCopayers: $scope.requiredCopayers, totalCopayers: $scope.totalCopayers, @@ -53,10 +51,14 @@ angular.module('copayApp.controllers').controller('CreateController', privateKeyHex: $scope.private, networkName: $scope.networkName, }; - $rootScope.iden.createWallet(opts, function(err, w) { - $scope.loading = false; - controllerUtils.installWalletHandlers($scope, w); - controllerUtils.setFocusedWallet(w); + $rootScope.starting = true; + identityService.createWallet(opts, function(err, wallet){ + $rootScope.starting = false; + if (err || !wallet) { + copay.logger.debug(err); + $scope.error = 'Could not create wallet.' + err; + } + $rootScope.$digest() }); }; }); diff --git a/js/controllers/createProfile.js b/js/controllers/createProfile.js index 824c9a0ce..9ddcec3db 100644 --- a/js/controllers/createProfile.js +++ b/js/controllers/createProfile.js @@ -1,15 +1,24 @@ 'use strict'; -angular.module('copayApp.controllers').controller('CreateProfileController', function($scope, $rootScope, $location, notification, controllerUtils, pluginManager, identityService) { - controllerUtils.redirIfLogged(); - $scope.loading = false; +angular.module('copayApp.controllers').controller('CreateProfileController', function($scope, $rootScope, $location, notification, pluginManager, identityService) { + identityService.goWalletHome(); + $scope.createProfile = function(form) { if (form && form.$invalid) { $scope.error('Error', 'Please enter the required fields'); return; } $rootScope.starting = true; - identityService.create($scope, form); + identityService.create( + form.email.$modelValue, form.password.$modelValue, function(err) { + $rootScope.starting = false; + if (err) { + var msg = err.toString(); + if (msg.indexOf('EEXIST')>=0 || msg.indexOf('BADC')>=0 ) { + msg = 'This profile already exists' + } + $scope.error = msg; + } + }); } - }); diff --git a/js/controllers/head.js b/js/controllers/head.js index 33c296d79..cf2be5a91 100644 --- a/js/controllers/head.js +++ b/js/controllers/head.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('HeadController', function($scope, $rootScope, $filter, $timeout, notification, controllerUtils) { +angular.module('copayApp.controllers').controller('HeadController', function($scope, $rootScope, $filter, $timeout, notification, identityService, balanceService) { $scope.username = $rootScope.iden.getName(); $scope.hoverMenu = false; @@ -14,21 +14,19 @@ angular.module('copayApp.controllers').controller('HeadController', function($sc $scope.signout = function() { $rootScope.signingOut = true; - controllerUtils.logout(); + identityService.signout(); }; $scope.refresh = function() { var w = $rootScope.wallet; if (!w) return; - if (w.isReady()) { + if (w.isComplete()) { w.sendWalletReady(); - if ($rootScope.addrInfos.length > 0) { - controllerUtils.clearBalanceCache(w); - controllerUtils.updateBalance(w, function() { - $rootScope.$digest(); - }); - } + balanceService.clearBalanceCache(w); + balanceService.update(w, function() { + $rootScope.$digest(); + }, true); } }; diff --git a/js/controllers/history.js b/js/controllers/history.js index 2328a1de9..97836d698 100644 --- a/js/controllers/history.js +++ b/js/controllers/history.js @@ -2,9 +2,7 @@ var bitcore = require('bitcore'); angular.module('copayApp.controllers').controller('HistoryController', - function($scope, $rootScope, $timeout, controllerUtils, notification, rateService) { - controllerUtils.redirIfNotComplete(); - + function($scope, $rootScope, $filter, rateService) { var w = $rootScope.wallet; $rootScope.title = 'History'; @@ -19,15 +17,11 @@ angular.module('copayApp.controllers').controller('HistoryController', $scope.blockchain_txs = []; $scope.alternativeCurrency = []; - - $scope.selectPage = function(page) { $scope.currentPage = page; $scope.update(); }; - - $scope.downloadHistory = function() { var w = $rootScope.wallet; if (!w) return; @@ -144,6 +138,7 @@ angular.module('copayApp.controllers').controller('HistoryController', _.each(items, function(tx) { tx.ts = tx.minedTs || tx.sentTs; tx.rateTs = Math.floor((tx.ts || now) / 1000); + tx.amount = $filter('noFractionNumber')(tx.amount); }); var index = _.indexBy(items, 'rateTs'); @@ -151,7 +146,8 @@ angular.module('copayApp.controllers').controller('HistoryController', if (!err && res) { _.each(res, function(r) { var tx = index[r.ts]; - tx.alternativeAmount = r.rate != null ? tx.amountSat * rateService.SAT_TO_BTC * r.rate : null; + var alternativeAmount = (r.rate != null ? tx.amountSat * rateService.SAT_TO_BTC * r.rate : null); + tx.alternativeAmount = alternativeAmount ? $filter('noFractionNumber')(alternativeAmount) : null; }); setTimeout(function() { $scope.$digest(); @@ -159,13 +155,10 @@ angular.module('copayApp.controllers').controller('HistoryController', } }); - - $scope.blockchain_txs = w.cached_txs = items; $scope.nbPages = res.nbPages; $scope.totalItems = res.nbItems; - $scope.loading = false; setTimeout(function() { $scope.$digest(); diff --git a/js/controllers/home.js b/js/controllers/home.js index 2e529109f..c5da16cc0 100644 --- a/js/controllers/home.js +++ b/js/controllers/home.js @@ -1,8 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('HomeController', function($scope, $rootScope, $location, notification, controllerUtils, pluginManager, identityService, Compatibility) { - controllerUtils.redirIfLogged(); - +angular.module('copayApp.controllers').controller('HomeController', function($scope, $rootScope, $location, $timeout, notification, identityService, Compatibility) { // This is only for backwards compat, insight api should link to #!/confirmed directly if (getParam('confirmed')) { var hashIndex = window.location.href.indexOf('/?'); @@ -18,6 +16,20 @@ angular.module('copayApp.controllers').controller('HomeController', function($sc Compatibility.check($scope); + $scope.done = function() { + $rootScope.starting = false; + }; + + + $scope.$on("$destroy", function(){ + var iden = $rootScope.iden; + if (iden) { + iden.removeListener('newWallets', $scope.done ); + iden.removeListener('noWallets', $scope.done ); + } + }); + + $scope.openProfile = function(form) { $scope.confirmedEmail = false; if (form && form.$invalid) { @@ -25,7 +37,22 @@ angular.module('copayApp.controllers').controller('HomeController', function($sc return; } $rootScope.starting = true; - identityService.open($scope, form); + identityService.open(form.email.$modelValue, form.password.$modelValue, function(err, iden) { + if (err) { + $rootScope.starting = false; + copay.logger.warn(err); + if ((err.toString() || '').match('PNOTFOUND')) { + $scope.error = 'Invalid email or password'; + } else { + $scope.error = 'Unknown error'; + } + } + + if (iden) { + iden.on('newWallet', $scope.done); + iden.on('noWallets', $scope.done); + } + }); } function getParam(sname) { diff --git a/js/controllers/homeWallet.js b/js/controllers/homeWallet.js index ad25cd973..146fc2fae 100644 --- a/js/controllers/homeWallet.js +++ b/js/controllers/homeWallet.js @@ -1,11 +1,13 @@ 'use strict'; angular.module('copayApp.controllers').controller('HomeWalletController', - function($scope, $rootScope, $timeout, $modal, controllerUtils) { - - controllerUtils.redirIfNotComplete(); - $rootScope.starting = false; + function($scope, $rootScope) { $rootScope.title = 'Home'; + $scope.addr = _.last($rootScope.wallet.getReceiveAddresses()); + // This is necesarry, since wallet can change in homeWallet, without running init() again. + $rootScope.$watch('wallet', function() { + $scope.addr = _.last($rootScope.wallet.getReceiveAddresses()); + }); } ); diff --git a/js/controllers/import.js b/js/controllers/import.js index b7ecf09bc..895bc5790 100644 --- a/js/controllers/import.js +++ b/js/controllers/import.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('ImportController', - function($scope, $rootScope, $location, controllerUtils, notification, isMobile, Compatibility) { + function($scope, $rootScope, $location, identityService, notification, isMobile, Compatibility) { $rootScope.title = 'Import wallet'; $scope.importStatus = 'Importing wallet - Reading backup...'; @@ -18,28 +18,18 @@ angular.module('copayApp.controllers').controller('ImportController', $scope.$digest(); } - $scope._doImport = function(encryptedObj, password) { - updateStatus('Importing wallet - Procesing backup...'); - - copay.Compatibility.importEncryptedWallet($rootScope.iden, encryptedObj, - $scope.password, $scope.importOpts, function(err, wallet) { - if (err) { - $scope.loading = false; - $scope.error = 'Could not read wallet. Please check your password'; - } else { - controllerUtils.installWalletHandlers($scope, wallet); - controllerUtils.setFocusedWallet(wallet); - } - } - ); - }; - $scope.getFile = function() { // If we use onloadend, we need to check the readyState. reader.onloadend = function(evt) { if (evt.target.readyState == FileReader.DONE) { // DONE == 2 var encryptedObj = evt.target.result; - $scope._doImport(encryptedObj, $scope.password); + updateStatus('Importing wallet - Procesing backup...'); + identityService.importWallet(encryptedObj, $scope.password, {}, function(err){ + if (err) { + $scope.loading = false; + $scope.error = 'Could not read wallet. Please check your password'; + } + }); } }; }; @@ -85,8 +75,14 @@ angular.module('copayApp.controllers').controller('ImportController', if (backupFile) { reader.readAsBinaryString(backupFile); } else { - $scope._doImport(backupText, $scope.password); - copay.Compatibility.deleteOldWallet(backupOldWallet); + updateStatus('Importing wallet - Procesing backup...'); + identityService.importWallet(encryptedObj, $scope.password, $scope.importOpts, function(err){ + if (err) { + $scope.loading = false; + $scope.error = 'Could not read wallet. Please check your password'; + } + copay.Compatibility.deleteOldWallet(backupOldWallet); + }); } }; }); diff --git a/js/controllers/importProfile.js b/js/controllers/importProfile.js index 016308052..c83013104 100644 --- a/js/controllers/importProfile.js +++ b/js/controllers/importProfile.js @@ -1,9 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('ImportProfileController', - function($scope, $rootScope, $location, controllerUtils, notification, isMobile, pluginManager, identityService) { - controllerUtils.redirIfLogged(); - + function($scope, $rootScope, $location, notification, isMobile, identityService) { $scope.title = 'Import a backup'; $scope.importStatus = 'Importing wallet - Reading backup...'; $scope.hideAdv = true; @@ -20,16 +18,10 @@ angular.module('copayApp.controllers').controller('ImportProfileController', var password = $scope.password; updateStatus('Importing profile - Setting things up...'); - copay.Identity.importFromEncryptedFullJson(str, password, { - pluginManager: pluginManager, - network: config.network, - networkName: config.networkName, - walletDefaults: config.wallet, - passphraseConfig: config.passphraseConfig, - }, function(err, iden) { + identityService.importProfile(str,password, function(err, iden) { + $scope.loading = false; if (err) { - $scope.loading = false; - + copay.logger.warn(err); if ((err.toString() || '').match('BADSTR')) { $scope.error = 'Bad password or corrupt profile file'; } else if ((err.toString() || '').match('EEXISTS')) { @@ -37,12 +29,8 @@ angular.module('copayApp.controllers').controller('ImportProfileController', } else { $scope.error = 'Unknown error'; } - $scope.$digest(); - - } else { - var firstWallet = iden.getLastFocusedWallet(); - controllerUtils.bindProfile($scope, iden, firstWallet); - } + $rootScope.$digest(); + } }); }; @@ -57,10 +45,8 @@ angular.module('copayApp.controllers').controller('ImportProfileController', }; $scope.import = function(form) { - $scope.loading = true; if (form.$invalid) { - $scope.loading = false; $scope.error = 'Please enter the required fields'; return; } @@ -69,11 +55,11 @@ angular.module('copayApp.controllers').controller('ImportProfileController', var password = form.password.$modelValue; if (!backupFile && !backupText) { - $scope.loading = false; $scope.error = 'Please, select your backup file'; return; } + $scope.loading = true; if (backupFile) { reader.readAsBinaryString(backupFile); } else { diff --git a/js/controllers/join.js b/js/controllers/join.js index 89d33da99..ce0935530 100644 --- a/js/controllers/join.js +++ b/js/controllers/join.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('JoinController', - function($scope, $rootScope, $timeout, isMobile, controllerUtils, notification) { + function($scope, $rootScope, $timeout, isMobile, notification) { $rootScope.fromSetup = false; $scope.loading = false; $scope.isMobile = isMobile.any(); @@ -119,15 +119,13 @@ angular.module('copayApp.controllers').controller('JoinController', } $scope.loading = true; - - $rootScope.iden.joinWallet({ + identityService.joinWallet({ secret: $scope.connectionId, nickname: $scope.nickname, privateHex: $scope.private, - }, function(err, w) { - + }, function(err) { $scope.loading = false; - if (err || !w) { + if (err) { if (err === 'joinError') notification.error('Fatal error connecting to Insight server'); else if (err === 'walletFull') @@ -139,10 +137,6 @@ angular.module('copayApp.controllers').controller('JoinController', else { notification.error('Error', err.message || err); } - controllerUtils.onErrorDigest(); - } else { - controllerUtils.installWalletHandlers($scope, w); - controllerUtils.setFocusedWallet(w); } }); } diff --git a/js/controllers/more.js b/js/controllers/more.js index 7d57c7a89..f3068922a 100644 --- a/js/controllers/more.js +++ b/js/controllers/more.js @@ -1,8 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('MoreController', - function($scope, $rootScope, $location, $filter, controllerUtils, notification, rateService) { - controllerUtils.redirIfNotComplete(); + function($scope, $rootScope, $location, $filter, balanceService, notification, rateService) { var w = $rootScope.wallet; $scope.isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0; @@ -76,7 +75,7 @@ angular.module('copayApp.controllers').controller('MoreController', alternativeIsoCode: $scope.selectedAlternative.isoCode, }); notification.success('Success', $filter('translate')('settings successfully updated')); - controllerUtils.updateBalance(w, function() { + balanceService.update(w, function() { $rootScope.$digest(); }); }; @@ -84,9 +83,9 @@ angular.module('copayApp.controllers').controller('MoreController', $scope.purge = function(deleteAll) { var removed = w.purgeTxProposals(deleteAll); if (removed) { - controllerUtils.updateBalance(w, function() { + balanceService.update(w, function() { $rootScope.$digest(); - }); + }, true); } notification.info('Transactions Proposals Purged', removed + ' ' + $filter('translate')('transaction proposal purged')); }; @@ -99,12 +98,11 @@ angular.module('copayApp.controllers').controller('MoreController', if (err) { notification.error('Error', $filter('translate')('Error updating indexes: ') + err); } - controllerUtils.updateAddressList(); - controllerUtils.updateBalance(w, function() { + balanceService.update(w, function() { notification.info('Finished', 'The balance is updated using the derived addresses'); w.sendIndexes(); $rootScope.$digest(); - }); + }, true); }); }; }); diff --git a/js/controllers/paymentIntent.js b/js/controllers/paymentIntent.js index 93a40f5ce..79f9e9435 100644 --- a/js/controllers/paymentIntent.js +++ b/js/controllers/paymentIntent.js @@ -1,37 +1,13 @@ 'use strict'; -var bitcore = require('bitcore'); +angular.module('copayApp.controllers').controller('PaymentIntentController', function($rootScope, $scope, $modal, $location, balanceService) { -angular.module('copayApp.controllers').controller('PaymentIntentController', function($rootScope, $scope, $modal, $location, controllerUtils) { + $rootScope.title = 'Payment intent'; - $scope.wallets = []; - $rootScope.title = 'Payment intent'; - $rootScope.starting = true; - - var wids = _.pluck($rootScope.iden.listWallets(), 'id'); - _.each(wids, function(wid) { - var w = $rootScope.iden.getWalletById(wid); - if (w && w.isReady()) { - - $scope.wallets.push(w); - $rootScope.starting = false; - controllerUtils.clearBalanceCache(w); - controllerUtils.updateBalance(w, function() { - $rootScope.$digest(); - }, true); - - } - }); - - $scope.open = function() { + $scope.open = function() { var modalInstance = $modal.open({ templateUrl: 'myModalContent.html', - controller: ModalInstanceCtrl, - resolve: { - items: function() { - return $scope.wallets; - } - } + controller: ModalInstanceCtrl }); }; @@ -39,17 +15,31 @@ angular.module('copayApp.controllers').controller('PaymentIntentController', fun // Please note that $modalInstance represents a modal window (instance) dependency. // It is not the same as the $modal service used above. - var ModalInstanceCtrl = function($scope, $modalInstance, items, controllerUtils) { - $scope.wallets = items; + var ModalInstanceCtrl = function($scope, $modalInstance, identityService) { + $scope.loading = true; + $scope.setWallets = function() { + if (!$rootScope.iden) return; + var ret = _.filter($rootScope.iden.listWallets(), function(w) { + return w.balanceInfo && w.balanceInfo.totalBalanceBTC; + }); + $scope.wallets = ret; + $scope.loading = false; + }; + if ($rootScope.iden) { + var iden = $rootScope.iden; + iden.on('newWallet', function() { + $scope.setWallets(); + }); + } $scope.ok = function(selectedItem) { - controllerUtils.setPaymentWallet(selectedItem); + identityService.setPaymentWallet(selectedItem); $modalInstance.close(); }; $scope.cancel = function() { $rootScope.pendingPayment = null; - $location.path('/'); $modalInstance.close(); + $location.path('/homeWallet'); }; }; diff --git a/js/controllers/profile.js b/js/controllers/profile.js index 1d5280968..92238910c 100644 --- a/js/controllers/profile.js +++ b/js/controllers/profile.js @@ -1,5 +1,5 @@ 'use strict'; -angular.module('copayApp.controllers').controller('ProfileController', function($scope, $rootScope, $location, $modal, controllerUtils, backupService) { +angular.module('copayApp.controllers').controller('ProfileController', function($scope, $rootScope, $location, $modal, backupService, identityService) { $scope.username = $rootScope.iden.getName(); $scope.isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0; @@ -14,33 +14,22 @@ angular.module('copayApp.controllers').controller('ProfileController', function( $scope.hideViewProfileBackup = true; }; - $scope.getWallets = function() { - if (!$rootScope.iden) return; - $scope.wallets = []; - var wids = _.pluck($rootScope.iden.listWallets(), 'id'); - _.each(wids, function(wid) { - var w = $rootScope.iden.getWalletById(wid); - $scope.wallets.push(w); - controllerUtils.updateBalance(w, function() { - $rootScope.$digest(); - }, true); - }); - }; - $scope.deleteWallet = function(w) { if (!w) return; - $scope.loading = w.id; - controllerUtils.deleteWallet($scope, w, function() { - if ($rootScope.wallet.id === w.id) { - $rootScope.wallet = null; - var lastFocused = $rootScope.iden.getLastFocusedWallet(); - controllerUtils.bindProfile($scope, $rootScope.iden, lastFocused); - } + identityService.deleteWallet(w, function(err) { $scope.loading = false; - $scope.getWallets(); + if (err) { + log.warn(err); + } }); }; + $scope.setWallets = function() { + if (!$rootScope.iden) return; + $scope.wallets=$rootScope.iden.listWallets(); + }; + + $scope.downloadWalletBackup = function(w) { if (!w) return; backupService.walletDownload(w); diff --git a/js/controllers/receive.js b/js/controllers/receive.js index a701c0392..6481d09b3 100644 --- a/js/controllers/receive.js +++ b/js/controllers/receive.js @@ -1,25 +1,19 @@ 'use strict'; angular.module('copayApp.controllers').controller('ReceiveController', - function($scope, $rootScope, $timeout, $modal, controllerUtils) { - controllerUtils.redirIfNotComplete(); - + function($scope, $rootScope, $timeout, $modal) { $rootScope.title = 'Receive'; $scope.loading = false; $scope.showAll = false; - $scope.isNewAddr = false; $scope.newAddr = function() { var w = $rootScope.wallet; $scope.loading = true; - $scope.isNewAddr = false; - w.generateAddress(null, function() { - $timeout(function() { - controllerUtils.updateAddressList(); - $scope.loading = false; - $scope.isNewAddr = true; - }, 1); - }); + w.generateAddress(null); + $scope.setAddressList(); + $timeout(function() { + $scope.loading = false; + }, 1); }; $scope.openAddressModal = function(address) { @@ -43,53 +37,32 @@ angular.module('copayApp.controllers').controller('ReceiveController', }); }; - $rootScope.$watch('addrInfos', function() { - if ($rootScope.updatingBalance) return; - $scope.addressList(); - }); - $scope.toggleShowAll = function() { $scope.showAll = !$scope.showAll; - $scope.addressList(); + $scope.setAddressList(); }; - $scope.limitAddress = function(elements, isNewAddr) { + $scope.setAddressList = function() { + var w = $rootScope.wallet; + var balance = w.balanceInfo.balanceByAddr; - if(!isNewAddr){ - elements = elements.sort(function(a, b) { - return (+a.isChange - +b.isChange); - }); - } + var addresses = w.getAddressesOrderer(); + if (addresses) { + $scope.addrLength = addresses.length; - if (elements.length <= 1 || $scope.showAll) { - return elements; - } + if (!$scope.showAll) + addresses = addresses.slice(0,3); - // Show last 3 non-change addresses plus those with balance - var addrs = elements.filter(function(e, i) { - return (!e.isChange && i < 3) || (e.balance && e.balance > 0); - }); - - return addrs; - }; - - $scope.addressList = function() { - $scope.addresses = []; - - if ($rootScope.addrInfos) { - var addrInfos = $rootScope.addrInfos; - $scope.addrLength = addrInfos.length; - for (var i = 0; i < addrInfos.length; i++) { - var addrinfo = addrInfos[i]; - $scope.addresses.push({ - 'index': i, - 'address': addrinfo.addressStr, - 'balance': $rootScope.balanceByAddr ? $rootScope.balanceByAddr[addrinfo.addressStr] : 0, - 'isChange': addrinfo.isChange, - 'owned': addrinfo.owned + var list = []; + _.each(addresses, function(address, index){ + list.push({ + 'index': index, + 'address': address, + 'balance': balance ? balance[address] : null, + 'isChange': w.addressIsChange(address), }); - } - $scope.addresses = $scope.limitAddress($scope.addresses, $scope.isNewAddr); + }); + $scope.addresses = list; } }; } diff --git a/js/controllers/send.js b/js/controllers/send.js index d6b1aecb2..085fd1baa 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -3,10 +3,7 @@ var bitcore = require('bitcore'); var preconditions = require('preconditions').singleton(); angular.module('copayApp.controllers').controller('SendController', - function($scope, $rootScope, $window, $timeout, $modal, isMobile, notification, controllerUtils, rateService) { - - controllerUtils.redirIfNotComplete(); - + function($scope, $rootScope, $window, $timeout, $modal, $filter, isMobile, notification, rateService) { var w = $rootScope.wallet; preconditions.checkState(w); preconditions.checkState(w.settings.unitToSatoshi); @@ -33,10 +30,44 @@ angular.module('copayApp.controllers').controller('SendController', $scope.$digest(); }); + $scope.setAlternativeAmount = function(w, tx, cb) { + rateService.whenAvailable(function() { + _.each(tx.outs, function(out) { + var valueSat = out.value * w.settings.unitToSatoshi; + out.alternativeAmount = rateService.toFiat(valueSat, $scope.alternativeIsoCode); + out.alternativeIsoCode = $scope.alternativeIsoCode; + }); + if (cb) return cb(tx); + }); + }; + + + $scope.updateTxs = _.throttle(function() { +console.log('[send.js.44:updateTxs:]'); //TODO + var w = $rootScope.wallet; + if (!w) return; + + var res = w.getPendingTxProposals(); + _.each(res.txs, function(tx) { + $scope.setAlternativeAmount(w, tx); + if (tx.merchant) { + var url = tx.merchant.request_url; + var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1]; + tx.merchant.domain = domain; + } + if (tx.outs) { + _.each(tx.outs, function(out) { + out.value = $filter('noFractionNumber')(out.value); + }); + } + }); + $scope.txps = res.txs; + }, 1000); /** * Setting the two related amounts as properties prevents an infinite * recursion for watches while preserving the original angular updates + * */ Object.defineProperty($scope, "alternative", { @@ -47,7 +78,7 @@ angular.module('copayApp.controllers').controller('SendController', this._alternative = newValue; if (typeof(newValue) === 'number' && $scope.isRateAvailable) { this._amount = parseFloat( - (rateService.fromFiat(newValue, w.settings.alternativeIsoCode) * satToUnit).toFixed(w.settings.unitDecimals), 10); + (rateService.fromFiat(newValue, $scope.alternativeIsoCode) * satToUnit).toFixed(w.settings.unitDecimals), 10); } else { this._amount = 0; } @@ -65,7 +96,7 @@ angular.module('copayApp.controllers').controller('SendController', if (typeof(newValue) === 'number' && $scope.isRateAvailable) { this._alternative = parseFloat( - (rateService.toFiat(newValue * w.settings.unitToSatoshi, w.settings.alternativeIsoCode)).toFixed(2), 10); + (rateService.toFiat(newValue * w.settings.unitToSatoshi, $scope.alternativeIsoCode)).toFixed(2), 10); } else { this._alternative = 0; } @@ -75,13 +106,17 @@ angular.module('copayApp.controllers').controller('SendController', }); - $scope.loadTxs = function() { - controllerUtils.updateTxs(); - setTimeout(function() { - $scope.loading = false; - $rootScope.$digest(); - }, 1); - } + $scope.init = function() { + $rootScope.pendingTxCount = 0; + $scope.updateTxs(); + var w = $rootScope.wallet; + w.on('txProposalEvent', $scope.updateTxs); + }; + + $scope.$on("$destroy", function(){ + var w = $rootScope.wallet; + w.removeListener('txProposalEvent', $scope.updateTxs ); + }); $scope.showAddressBook = function() { return w && _.keys(w.addressBook).length > 0; @@ -89,9 +124,17 @@ angular.module('copayApp.controllers').controller('SendController', if ($rootScope.pendingPayment) { var pp = $rootScope.pendingPayment; - $scope.address = pp.address + ''; - var amount = pp.data.amount / w.settings.unitToSatoshi * 100000000; - $scope.amount = amount; + var amount = pp.data.amount * 100000000 * satToUnit; + var alternativeAmountPayPro = rateService.toFiat((amount + $scope.defaultFee) * w.settings.unitToSatoshi, $scope.alternativeIsoCode); + if (pp.data.merchant) { + $scope.address = 'bitcoin:' + pp.address.data + '?amount=' + amount + '&r=' + pp.data.r; + } + else { + $scope.address = pp.address + ''; + $scope.amount = amount; + $scope.alternative = alternativeAmountPayPro; + } + $scope.alternativeAmountPayPro = $filter('noFractionNumber')(alternativeAmountPayPro, 2); $scope.commentText = pp.data.message; } @@ -116,7 +159,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.error = message; $scope.loading = false; - $scope.loadTxs(); + $scope.updateTxs(); }; $scope.submitForm = function(form) { @@ -153,6 +196,7 @@ angular.module('copayApp.controllers').controller('SendController', comment: commentText, url: (payInfo && payInfo.merchant) ? payInfo.merchant : null, }, function(err, txid, status) { + $scope.loading = false; // reset fields $scope.address = $scope.amount = $scope.commentText = null; form.address.$pristine = form.amount.$pristine = true; @@ -160,7 +204,7 @@ angular.module('copayApp.controllers').controller('SendController', if (err) return $scope._showError(err); $scope.notifyStatus(status); - $scope.loadTxs(); + $scope.updateTxs(); }); }; @@ -381,26 +425,24 @@ angular.module('copayApp.controllers').controller('SendController', w.issueTx(ntxid, function(err, txid, status) { $scope.notifyStatus(status); if (cb) return cb(); - else $scope.loadTxs(); + else $scope.updateTxs(); }); }; $scope.sign = function(ntxid) { $scope.loading = true; $scope.error = $scope.success = null; - w.signAndSend(ntxid, function(err, id, status) { + $scope.loading = false; $scope.notifyStatus(status); - $scope.loadTxs(); + $scope.updateTxs(); }); }; $scope.reject = function(ntxid) { - $scope.loading = true; - $rootScope.txAlertCount = 0; w.reject(ntxid); notification.warning('Transaction rejected', 'You rejected the transaction successfully'); - $scope.loadTxs(); + $scope.updateTxs(); }; $scope.clearMerchant = function(callback) { diff --git a/js/controllers/settings.js b/js/controllers/settings.js index 2756f55d3..b8cfe2f45 100644 --- a/js/controllers/settings.js +++ b/js/controllers/settings.js @@ -1,8 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $route, $location, $anchorScroll, controllerUtils, notification) { - controllerUtils.redirIfLogged(); - +angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $route, $location, $anchorScroll, notification, applicationService) { $scope.title = 'Settings'; $scope.defaultLanguage = config.defaultLanguage || 'en'; $scope.insightLivenet = config.network.livenet.url; @@ -96,16 +94,13 @@ angular.module('copayApp.controllers').controller('SettingsController', function }), })); - // Go home reloading the application - var hashIndex = window.location.href.indexOf('#!/'); - window.location = window.location.href.substr(0, hashIndex); + applicationService.restart(); }; $scope.reset = function() { localStorage.removeItem('config'); - // Go home reloading the application - window.location.reload(); + applicationService.reload(); }; }); diff --git a/js/controllers/sidebar.js b/js/controllers/sidebar.js index e64de73db..5da6e83ab 100644 --- a/js/controllers/sidebar.js +++ b/js/controllers/sidebar.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('SidebarController', function($scope, $rootScope, $location, controllerUtils) { +angular.module('copayApp.controllers').controller('SidebarController', function($scope, $rootScope, $location, $timeout, identityService) { $scope.menu = [{ 'title': 'Home', @@ -24,21 +24,6 @@ angular.module('copayApp.controllers').controller('SidebarController', function( 'link': 'more' }]; - $scope.refresh = function() { - var w = $rootScope.wallet; - if (!w) return; - - if (w.isReady()) { - w.sendWalletReady(); - if ($rootScope.addrInfos.length > 0) { - controllerUtils.clearBalanceCache(w); - controllerUtils.updateBalance(w, function() { - $rootScope.$digest(); - }); - } - } - }; - $scope.signout = function() { $scope.$emit('signout'); }; @@ -47,35 +32,51 @@ angular.module('copayApp.controllers').controller('SidebarController', function( return item.link && item.link == $location.path().split('/')[1]; }; - if ($rootScope.wallet) { - $rootScope.$watch('wallet.id', function() { - $scope.walletSelection = false; - $scope.getWallets(); - }); - } - $scope.switchWallet = function(wid) { - controllerUtils.setFocusedWallet(wid); + $scope.walletSelection = false; + identityService.setFocusedWallet(wid); + identityService.goWalletHome(); }; $scope.toggleWalletSelection = function() { $scope.walletSelection = !$scope.walletSelection; if (!$scope.walletSelection) return; - - $scope.getWallets(); + $scope.setWallets(); }; - $scope.getWallets = function() { + + $scope.init = function() { + // This should be called only once. + + // focused wallet change + if ($rootScope.wallet) { + $rootScope.$watch('wallet', function() { + $scope.walletSelection = false; + $scope.setWallets(); + }); + } + + // wallet list chane + if ($rootScope.iden) { + var iden = $rootScope.iden; + iden.on('newWallet', function() { + $scope.walletSelection = false; + $scope.setWallets(); + }); + iden.on('deleteWallet', function() { + $scope.walletSelection = false; + $scope.setWallets(); + }); + + } + + }; + + $scope.setWallets = function() { if (!$rootScope.iden) return; - $scope.wallets = []; - var wids = _.pluck($rootScope.iden.listWallets(), 'id'); - _.each(wids, function(wid) { - if (controllerUtils.isFocusedWallet(wid)) return; - var w = $rootScope.iden.getWalletById(wid); - $scope.wallets.push(w); - controllerUtils.updateBalance(w, function() { - $rootScope.$digest(); - }) + var ret = _.filter($rootScope.iden.listWallets(), function(w) { + return !identityService.isFocused(w.getId()); }); + $scope.wallets = ret; }; }); diff --git a/js/controllers/warning.js b/js/controllers/warning.js index 75f19ff56..62f42b94b 100644 --- a/js/controllers/warning.js +++ b/js/controllers/warning.js @@ -1,16 +1,14 @@ 'use strict'; -angular.module('copayApp.controllers').controller('WarningController', function($scope, $rootScope, $location, controllerUtils) { - +angular.module('copayApp.controllers').controller('WarningController', function($scope, $rootScope, $location, identityService) { $scope.checkLock = function() { if (!$rootScope.tmp || !$rootScope.tmp.getLock()) { - controllerUtils.redirIfLogged(); + console.log('[warning.js.7] TODO LOCK'); //TODO } }; - $scope.signout = function() { - controllerUtils.logout(); + identityService.signout(); }; $scope.ignoreLock = function() { @@ -22,7 +20,8 @@ angular.module('copayApp.controllers').controller('WarningController', function( } else { w.ignoreLock = 1; $scope.loading = true; - controllerUtils.startNetwork(w, $scope); + //controllerUtils.startNetwork(w, $scope); + // TODO } }; }); diff --git a/js/models/Async.js b/js/models/Async.js index b7b9bca33..067cc50f1 100644 --- a/js/models/Async.js +++ b/js/models/Async.js @@ -28,7 +28,6 @@ function Network(opts) { if (opts.transports) { this.socketOptions['transports'] = opts.transports; } - this.socket = this.createSocket(); } nodeUtil.inherits(Network, EventEmitter); @@ -236,7 +235,7 @@ Network.prototype._onMessage = function(enc) { } }; -Network.prototype._setupConnectionHandlers = function(opts, cb) { +Network.prototype._setupSocketHandlers = function(opts, cb) { preconditions.checkState(this.socket); log.debug('setting up connection', opts); var self = this; @@ -274,10 +273,7 @@ Network.prototype._setupConnectionHandlers = function(opts, cb) { }); self.socket.on('error', self._onError.bind(self)); - self.socket.on('no messages', self.emit.bind(self, 'no messages')); - - - var pubkey = self.getKey().public.toString('hex'); + self.socket.on('no_messages', self.emit.bind(self, 'no_messages')); self.socket.on('connect', function() { var pubkey = self.getKey().public.toString('hex'); log.debug('Async subscribing to pubkey:', pubkey); @@ -287,11 +283,8 @@ Network.prototype._setupConnectionHandlers = function(opts, cb) { self.socket.on('disconnect', function() { self.socket.emit('subscribe', pubkey); }); - if (typeof cb === 'function') cb(); }); - - }; Network.prototype._onError = function(err) { @@ -348,7 +341,9 @@ Network.prototype.start = function(opts, openCallback) { this.privkey = opts.privkey; this.setCopayerId(opts.copayerId); this.maxPeers = opts.maxPeers || this.maxPeers; - this._setupConnectionHandlers(opts, openCallback); + + this.socket = this.createSocket(); + this._setupSocketHandlers(opts, openCallback); }; Network.prototype.createSocket = function() { diff --git a/js/models/Identity.js b/js/models/Identity.js index fbb5c8783..23329ca77 100644 --- a/js/models/Identity.js +++ b/js/models/Identity.js @@ -1,19 +1,22 @@ 'use strict'; -var preconditions = require('preconditions').singleton(); - var _ = require('lodash'); -var bitcore = require('bitcore'); +var preconditions = require('preconditions').singleton(); +var inherits = require('inherits'); +var events = require('events'); var log = require('../log'); var async = require('async'); -var cryptoUtil = require('../util/crypto'); -var version = require('../../version').version; +var bitcore = require('bitcore'); + var TxProposals = require('./TxProposals'); var PublicKeyRing = require('./PublicKeyRing'); var PrivateKey = require('./PrivateKey'); var Wallet = require('./Wallet'); var PluginManager = require('./PluginManager'); -var Async = module.exports.Async = require('./Async'); +var Async = require('./Async'); + +var version = require('../../version').version; +var cryptoUtil = require('../util/crypto'); /** * @desc @@ -56,9 +59,14 @@ function Identity(opts) { this.walletDefaults = opts.walletDefaults || {}; this.version = opts.version || version; + this.walletIds = opts.walletIds || {}; this.wallets = opts.wallets || {}; + this.focusedTimestamps = opts.focusedTimestamps || {}; }; + +inherits(Identity, events.EventEmitter); + Identity.getStoragePrefix = function() { return 'profile::'; }; @@ -96,7 +104,11 @@ Identity.create = function(opts, cb) { /** - * Open an Identity from the given storage + * Open an Identity from the given storage. + * + * After opening a profile, and setting its wallet event handlers, + * the client must run .netStart on each + * (probably on iden's newWallet handler * * @param {Object} opts * @param {Object} opts.storage @@ -105,42 +117,77 @@ Identity.create = function(opts, cb) { * @param {Function} cb */ Identity.open = function(opts, cb) { + preconditions.checkArgument(_.isObject(opts)); + preconditions.checkArgument(_.isFunction(cb)); + var storage = opts.storage || opts.pluginManager.get('DB'); storage.setCredentials(opts.email, opts.password, opts); storage.getItem(Identity.getKeyForEmail(opts.email), function(err, data) { + var exported; if (err) { return cb(err); } - return Identity.createFromPartialJson(data, opts, cb); + try { + exported = JSON.parse(data); + } catch (e) { + return cb(e); + } + return cb(null, new Identity(_.extend(opts, exported))); }); }; + /** - * Creates an Identity, retrieves all Wallets remotely, and activates network + * readAndBindWallet + * + * @param {string} wid walletId to be readed + * @param {function} cb * - * @param {string} jsonString - a string containing a json object with options to rebuild the identity - * @param {Object} opts - * @param {Function} cb */ -Identity.createFromPartialJson = function(jsonString, opts, callback) { - var exported; - try { - exported = JSON.parse(jsonString); - } catch (e) { - return callback('Invalid JSON'); +Identity.prototype.readAndBindWallet = function(walletId, cb) { + var self = this; + self.retrieveWalletFromStorage(walletId, {}, function(error, wallet) { + if (!error) { + self.bindWallet(wallet); + } + return cb(error); + }); +}; + + +Identity.prototype.emitAndKeepAlive = function(args) { + var args = Array.prototype.slice.call(arguments); + log.debug('Ident Emitting:', args); + //this.keepAlive(); // TODO + this.emit.apply(this, arguments); +}; + + +/** + * @desc open profile's wallets. Call it AFTER setting + * the proper even listeners. no callback. + * + */ +Identity.prototype.openWallets = function() { + var self = this; + + + if (_.isEmpty(self.walletIds)) { + self.emitAndKeepAlive('noWallets') + return; } - var identity = new Identity(_.extend(opts, exported)); - async.map(exported.walletIds, function(walletId, callback) { - identity.retrieveWalletFromStorage(walletId, {}, function(error, wallet) { - if (!error) { - identity.wallets[wallet.getId()] = wallet; - identity.bindWallet(wallet); - wallet.netStart(); - } - callback(error, wallet); - }); - }, function(err) { - return callback(err, identity); + + // First read the lastFocused wallet + self.walletIds.sort(function(a, b) { + var va = self.focusedTimestamps[a] || 0; + var vb = self.focusedTimestamps[b] || 0; + + return va < vb ? 1 : (va === vb ? 0 : -1); + }); + + // opens the wallets, in the order they were last accessed. Emits open events (newWallet) + async.eachSeries(self.walletIds, function(walletId, a_cb) { + self.readAndBindWallet(walletId, a_cb); }); }; @@ -148,16 +195,16 @@ Identity.createFromPartialJson = function(jsonString, opts, callback) { * @param {string} walletId * @param {} opts * opts.importWallet - * @param {Function} callback + * @param {Function} cb */ -Identity.prototype.retrieveWalletFromStorage = function(walletId, opts, callback) { +Identity.prototype.retrieveWalletFromStorage = function(walletId, opts, cb) { var self = this; var importFunction = opts.importWallet || Wallet.fromUntrustedObj; this.storage.getItem(Wallet.getStorageKey(walletId), function(error, walletData) { if (error) { - return callback(error); + return cb(error); } try { log.info('## OPENING Wallet:', walletId); @@ -169,18 +216,15 @@ Identity.prototype.retrieveWalletFromStorage = function(walletId, opts, callback blockchainOpts: self.blockchainOpts, skipFields: [] }; - - return callback(null, importFunction(walletData, readOpts)); - } catch (e) { - log.debug("ERROR: ", e.message); if (e && e.message && e.message.indexOf('MISSOPTS') !== -1) { - return callback(new Error('WERROR: Could not read: ' + walletId + ': ' + e.message)); + return cb(new Error('WERROR: Could not read: ' + walletId + ': ' + e.message)); } else { - return callback(e); + return cb(e); } } + return cb(null, importFunction(walletData, readOpts)); }); }; @@ -219,9 +263,9 @@ Identity.storeWalletDebounced = _.debounce(function(identity, wallet, cb) { Identity.prototype.toObj = function() { return _.extend({ - walletIds: _.keys(this.wallets) + walletIds: _.isEmpty(this.wallets) ? this.walletsIds : _.keys(this.wallets), }, - _.pick(this, 'version', 'fullName', 'password', 'email')); + _.pick(this, 'version', 'fullName', 'password', 'email', 'focusedTimestamps')); }; Identity.prototype.exportEncryptedWithWalletInfo = function(opts) { @@ -270,15 +314,14 @@ Identity.prototype._cleanUp = function() { /** * @desc Closes the wallet and disconnects all services */ -Identity.prototype.close = function(cb) { - async.map(this.wallets, function(wallet, callback) { - wallet.close(callback); - }, cb); +Identity.prototype.close = function() { + var self = this; + self.store({}, function(err) { + self.emitAndKeepAlive('closed'); + }); }; -// TODO: Add feedback function -// Identity.prototype.importWalletFromObj = function(obj, opts, cb) { var self = this; preconditions.checkArgument(cb); @@ -292,28 +335,30 @@ Identity.prototype.importWalletFromObj = function(obj, opts, cb) { var w = importFunction(obj, readOpts); if (!w) return cb(new Error('Could not decrypt')); - log.debug('Wallet decryped:' + w.getName()); + log.debug('Wallet decrypted:' + w.getName()); self._checkVersion(w.version); log.debug('Updating Indexes for wallet:' + w.getName()); w.updateIndexes(function(err) { log.debug('Adding wallet to profile:' + w.getName()); - self.addWallet(w); self.bindWallet(w); - - var writeOpts = _.extend({ - noWallets: true - }, opts); - - self.storeWallet(w, function(err) { - if (err) return cb(err); - self.store(writeOpts, function(err) { - return cb(err, w); - }); - }); + self.storeWallet(w, cb); }); }; + +Identity.prototype.importMultipleWalletsFromObj = function(objs, opts) { + var self = this; + opts = opts || {}; + + async.eachSeries(objs, function(walletData, cb) { + if (!walletData) + return cb(); + self.importWalletFromObj(walletData, opts, cb); + }); +}; + + /** * @param {Wallet} wallet * @param {Function} cb @@ -321,8 +366,8 @@ Identity.prototype.importWalletFromObj = function(obj, opts, cb) { Identity.prototype.closeWallet = function(wallet, cb) { preconditions.checkState(wallet, 'Wallet not found'); + var self = this; wallet.close(function(err) { - delete self.wallets[wid]; return cb(err); }); }; @@ -370,33 +415,18 @@ Identity.importFromFullJson = function(str, password, opts, cb) { iden.store(opts, function(err) { if (err) return cb(err); //profile already exists - opts.failIfExists = false; - async.map(json.wallets, function(walletData, callback) { - - if (!walletData) - return callback(); - - iden.importWalletFromObj(walletData, opts, function(err, w) { - if (err) return callback(err); - log.debug('Wallet ' + w.getId() + ' imported'); - callback(); - }); - }, function(err, results) { - if (err) return cb(err); - - iden.store(opts, function(err) { - return cb(err, iden); - }); - }); - + return cb(null, iden, json.wallets); }); - - }; + +/** + * @desc binds a wallet's events and emits 'newWallet' + * @param {string} walletId Wallet id to be binded + * @emits newWallet (walletId) + */ Identity.prototype.bindWallet = function(w) { var self = this; - self.wallets[w.getId()] = w; log.debug('Binding wallet:' + w.getName()); @@ -421,6 +451,8 @@ Identity.prototype.bindWallet = function(w) { w.on('publicKeyRingUpdated', function() { Identity.storeWalletDebounced(self, w); }); + + this.emitAndKeepAlive('newWallet', w.getId()); }; /** @@ -494,12 +526,10 @@ Identity.prototype.createWallet = function(opts, cb) { var self = this; var w = new walletClass(opts); - this.addWallet(w); self.bindWallet(w); - w.netStart(); + self.updateFocusedTimestamp(w.getId()); self.storeWallet(w, function(err) { if (err) return cb(err); - self.store({ noWallets: true }, function(err) { @@ -508,12 +538,6 @@ Identity.prototype.createWallet = function(opts, cb) { }); }; -Identity.prototype.addWallet = function(wallet) { - preconditions.checkArgument(wallet); - preconditions.checkArgument(wallet.getId); - this.wallets[wallet.getId()] = wallet; -}; - /** * @desc Checks if a version is compatible with the current version * @param {string} inVersion - a version, with major, minor, and revision, period-separated (x.y.z) @@ -561,11 +585,17 @@ Identity.prototype.listWallets = function() { Identity.prototype.deleteWallet = function(walletId, cb) { var self = this; + var w = this.getWalletById(walletId); + w.close(); + delete this.wallets[walletId]; + delete this.focusedTimestamps[walletId]; + this.storage.removeItem(Wallet.getStorageKey(walletId), function(err) { if (err) { return cb(err); } + self.emitAndKeepAlive('deletedWallet', walletId); self.store(null, cb); }); }; @@ -581,11 +611,26 @@ Identity.prototype.decodeSecret = function(secret) { } }; -Identity.prototype.getLastFocusedWallet = function() { - if (_.keys(this.wallets).length == 0) return; - return _.max(this.wallets, function(wallet) { - return wallet.focusedTimestamp || 0; - }); +/** + * getLastFocusedWalletId + * + * @return {string} walletId + */ +Identity.prototype.getLastFocusedWalletId = function() { + var max = _.max(this.focusedTimestamp); + var aId = _.findKey(this.wallets) || this.walletIds[0]; + + if (!max) + return aId; + + return _.findKey(this.focusedTimestamps, function(ts) { + return ts == max; + }) || aId; +}; + +Identity.prototype.updateFocusedTimestamp = function(wid) { + preconditions.checkArgument(wid && this.wallets[wid]); + this.focusedTimestamps[wid] = Date.now(); }; /** diff --git a/js/models/Insight.js b/js/models/Insight.js index 7d187cb2d..e1cde179a 100644 --- a/js/models/Insight.js +++ b/js/models/Insight.js @@ -220,8 +220,7 @@ Insight.prototype.subscribe = function(addresses) { if (!self.subscribed[address]) { var handler = handlerFor(self, address); self.subscribed[address] = handler; - log.debug('Subcribe to: ', address); - +// log.debug('Subscribe to: ', address); s.emit('subscribe', address); s.on(address, handler); } diff --git a/js/models/PublicKeyRing.js b/js/models/PublicKeyRing.js index 10fa59e4e..6963e8bab 100644 --- a/js/models/PublicKeyRing.js +++ b/js/models/PublicKeyRing.js @@ -42,10 +42,20 @@ function PublicKeyRing(opts) { this.publicKeysCache = {}; this.nicknameFor = opts.nicknameFor || {}; this.copayerIds = []; - this.addressToPath = {}; + this.resetCache(); }; +PublicKeyRing.prototype.resetCache = function() { + this.cache = {}; + this.cache.addressToPath = {}; + this.cache.pathToAddress = {}; + + // Non persistent cache + this._isChange = {}; +}; + + /** * @desc Returns an object with only the keys needed to rebuild a PublicKeyRing * @@ -85,22 +95,42 @@ PublicKeyRing.trim = function(data) { * @param {object} data - a serialized version of PublicKeyRing {@see PublicKeyRing#trim} * @return {PublicKeyRing} - the deserialized object */ -PublicKeyRing.fromObj = function(data) { - preconditions.checkArgument(!(data instanceof PublicKeyRing), 'bad data format: Did you use .toObj()?'); - var opts = PublicKeyRing.trim(data); +PublicKeyRing.fromObj = function(opts) { + preconditions.checkArgument(!(opts instanceof PublicKeyRing), 'bad opts format: Did you use .toObj()?'); // Support old indexes schema if (!Array.isArray(opts.indexes)) { opts.indexes = HDParams.update(opts.indexes, opts.totalCopayers); } - var ret = new PublicKeyRing(opts); + var pkr = new PublicKeyRing(opts); for (var k in opts.copayersExtPubKeys) { - ret.addCopayer(opts.copayersExtPubKeys[k]); + pkr.addCopayer(opts.copayersExtPubKeys[k]); } - return ret; + if (opts.cache && opts.cache.addressToPath) { + log.info('PublicKeyRing: Using address cache'); + pkr.cache.addressToPath = opts.cache.addressToPath; + pkr.rebuildCache(); + } + + return pkr; +}; + + +PublicKeyRing.prototype.rebuildCache = function() { + if (!this.cache.addressToPath) + return; + + var self = this; + _.each(this.cache.addressToPath, function(path, address) { + self.cache.pathToAddress[path] = address; + }); +}; + +PublicKeyRing.fromUntrustedObj = function(opts) { + return PublicKeyRing.fromObj(PublicKeyRing.trim(opts)); }; /** @@ -120,10 +150,21 @@ PublicKeyRing.prototype.toObj = function() { copayersExtPubKeys: this.copayersHK.map(function(b) { return b.extendedPublicKeyString(); }), - nicknameFor: this.nicknameFor + nicknameFor: this.nicknameFor, + + // We only store addressToPath and derive the reset from it + cache: { + addressToPath: this.cache.addressToPath + }, }; }; + +PublicKeyRing.prototype.toTrimmedObj = function() { + return PublicKeyRing.trim(this.toObj()); +} + + /** * @desc * Retrieve a copayer's public key as a hexadecimal encoded string @@ -285,20 +326,12 @@ PublicKeyRing.prototype.addCopayer = function(newHexaExtendedPublicKey, nickname PublicKeyRing.prototype.getPubKeys = function(index, isChange, copayerIndex) { this._checkKeys(); + log.warn('Slow pubkey derivation...'); var path = HDPath.Branch(index, isChange, copayerIndex); - var pubKeys = this.publicKeysCache[path]; - if (!pubKeys) { - pubKeys = _.map(this.copayersHK, function(hdKey) { - return hdKey.derive(path).eckey.public; - }); - this.publicKeysCache[path] = pubKeys.map(function(pk) { - return pk.toString('hex'); - }); - } else { - pubKeys = pubKeys.map(function(s) { - return new Buffer(s, 'hex'); - }); - } + var pubKeys = _.map(this.copayersHK, function(hdKey) { + return hdKey.derive(path).eckey.public; + }); + return pubKeys; }; @@ -332,29 +365,23 @@ PublicKeyRing.prototype.getRedeemScript = function(index, isChange, copayerIndex * @param {number} copayerIndex - the index of the copayer that requested the derivation * @returns {bitcore.Address} */ -PublicKeyRing.prototype.getAddress = function(index, isChange, id) { +PublicKeyRing.prototype._getAddress = function(index, isChange, id) { var copayerIndex = this.getCosigner(id); - if (!this._cachedAddress(index, isChange, id)) { - var script = this.getRedeemScript(index, isChange, copayerIndex); - var address = Address.fromScript(script, this.network.name); - this.addressToPath[address.toString()] = HDPath.FullBranch(index, isChange, copayerIndex); - this._cacheAddress(index, isChange, copayerIndex, address); - } - return this._cachedAddress(index, isChange, copayerIndex); + var path = HDPath.FullBranch(index, isChange, copayerIndex); + if (this.cache.pathToAddress[path]) + return this.cache.pathToAddress[path]; + + log.info('Generating Address:', index, isChange, copayerIndex); + var script = this.getRedeemScript(index, isChange, copayerIndex); + var address = Address.fromScript(script, this.network.name).toString(); + + this._cacheAddress(address, path, isChange); + return address; }; -PublicKeyRing.prototype._cacheAddress = function(index, isChange, copayerIndex, address) { - var changeIndex = isChange ? 1 : 0; - if (!this._cacheAddressMap) this._cacheAddressMap = {}; - if (!this._cacheAddressMap[index]) this._cacheAddressMap[index] = {}; - if (!this._cacheAddressMap[index][changeIndex]) this._cacheAddressMap[index][changeIndex] = {}; - this._cacheAddressMap[index][changeIndex][copayerIndex] = address; -}; -PublicKeyRing.prototype._cachedAddress = function(index, isChange, copayerIndex) { - var changeIndex = isChange ? 1 : 0; - if (!this._cacheAddressMap) return undefined; - if (!this._cacheAddressMap[index]) return undefined; - if (!this._cacheAddressMap[index][changeIndex]) return undefined; - return this._cacheAddressMap[index][changeIndex][copayerIndex]; + +PublicKeyRing.prototype._cacheAddress = function(address, path, isChange) { + this.cache.addressToPath[address] = path; + this.cache.pathToAddress[path] = address; }; /** @@ -384,26 +411,12 @@ PublicKeyRing.prototype.getHDParams = function(id) { * @return {HDPath} */ PublicKeyRing.prototype.pathForAddress = function(address) { - var path = this.addressToPath[address]; + this._checkCache(); + var path = this.cache.addressToPath[address]; if (!path) throw new Error('Couldn\'t find path for address ' + address); return path; }; -/** - * @desc - * Get the hexadecimal representation of a P2SH script - * - * @param {number} index - index to use when generating the address - * @param {boolean} isChange - generate a change address or a receive addres - * @param {number|string} pubkey - index of the copayer, or his public key - * @returns {string} hexadecimal encoded P2SH hash - */ -PublicKeyRing.prototype.getScriptPubKeyHex = function(index, isChange, pubkey) { - var copayerIndex = this.getCosigner(pubkey); - var addr = this.getAddress(index, isChange, copayerIndex); - return Script.createP2SH(addr.payload()).getBuffer().toString('hex'); -}; - /** * @desc * Generates a new address and updates the last index used @@ -416,26 +429,44 @@ PublicKeyRing.prototype.getScriptPubKeyHex = function(index, isChange, pubkey) { */ PublicKeyRing.prototype.generateAddress = function(isChange, pubkey) { isChange = !!isChange; - var HDParams = this.getHDParams(pubkey); - var index = isChange ? HDParams.getChangeIndex() : HDParams.getReceiveIndex(); - var ret = this.getAddress(index, isChange, HDParams.copayerIndex); - HDParams.increment(isChange); + var hdParams = this.getHDParams(pubkey); + var index = isChange ? hdParams.getChangeIndex() : hdParams.getReceiveIndex(); + var ret = this._getAddress(index, isChange, hdParams.copayerIndex); + hdParams.increment(isChange); return ret; }; /** - * @desc - * Retrieve the addresses from a getAddressInfo return object + * @desc Is an address is from this wallet? * - * {@see PublicKeyRing#getAddressInfo} - * @returns {string[]} the result of retrieving the addresses from calling + * @param {string} address + * @return {boolean} */ -PublicKeyRing.prototype.getAddresses = function(opts) { - return this.getAddressesInfo(opts).map(function(info) { - return info.address; - }); +PublicKeyRing.prototype.addressIsOwn = function(address) { + return !!this.cache.addressToPath[address]; }; +/** + * @desc Is an address is a change address? + * + * @param {string} address + * @return {boolean} + */ +PublicKeyRing.prototype.addressIsChange = function(address) { + this._checkCache(); + + var path = this.cache.addressToPath[address]; + if (!path) + return null; + + var p = HDPath.indexesForPath(path); + + //Memoization Only, never stored. + this._isChange[address] = p.isChange; + return !!this._isChange[address]; +}; + + /** * @desc * Maps a copayer's public key to his index in the keyring @@ -461,68 +492,98 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) { return index; }; + + +PublicKeyRing.prototype.buildAddressCache = function() { + var ret = []; + var self = this; + _.each(this.indexes, function(index) { + for (var i = 0; i < index.receiveIndex; i++) { + self._getAddress(i, false, index.copayerIndex); + } + for (var i = 0; i < index.changeIndex; i++) { + self._getAddress(i, true, index.copayerIndex); + } + }); +}; + + +PublicKeyRing.prototype.size = function(opts) { + var self = this; + return _.reduce(this.indexes, function(sum, index) { + return sum + index.receiveIndex + index.changeIndex + }, 0); +}; + +PublicKeyRing.prototype._checkCache = function(opts) { + if (_.isEmpty(this.cache.addressToPath)) { + this.buildAddressCache(); + } + if (_.size(this.cache.addressToPath) !== this.size()) { + this.buildAddressCache(); + } +}; + + /** * @desc * Gets information about addresses for a copayer * - * @see PublicKeyRing#getAddressesInfoForIndex * @param {Object} opts - * @param {string|number} pubkey - the pubkey or index of a copayer in the ring * @returns {AddressInfo[]} */ -PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) { - - var ret = []; - var self = this; - var copayerIndex = pubkey && this.getCosigner(pubkey); - this.indexes.forEach(function(index) { - ret = ret.concat(self.getAddressesInfoForIndex(index, opts, copayerIndex)); - }); - return ret; +PublicKeyRing.prototype.getAddresses = function() { + this._checkCache(); + return _.keys(this.cache.addressToPath); }; /** - * @typedef AddressInfo - * @property {bitcore.Address} address - the address generated - * @property {string} addressStr - the base58 encoded address - * @property {boolean} isChange - true if it's a change address - * @property {boolean} owned - true if it's an address generated by a copayer + * getAddressesOrderer + * {@link Wallet#getAddressesOrderer} + * + * @param pubkey + * @return {string[]} */ +PublicKeyRing.prototype.getAddressesOrderer = function(pubkey) { + this._checkCache(); + + var info = _.map(this.cache.addressToPath, function(path, addr) { + var p = HDPath.indexesForPath(path); + p.address = addr; + return p; + }); + + var copayerIndex = this.getCosigner(pubkey); + var l = info.length; + + var sortedInfo = _.sortBy(info, function(i) { + var goodness = ( (i.copayerIndex !== copayerIndex) ? 2 * l : 0 ) +( i.isChange ? l : 0 ) + l - i.addressIndex; + return goodness; + }); + + return _.pluck(sortedInfo, 'address'); +}; + + + /** * @desc - * Retrieves info about addresses generated by a copayer + * Gets information about addresses for a copayer * - * @param {HDParams} index - HDParams of the copayer * @param {Object} opts - * @param {boolean} opts.excludeChange - don't append information about change addresses - * @param {boolean} opts.excludeMain - don't append information about receive addresses - * @param {string|number|undefined} copayerIndex - copayer index, pubkey, or undefined to fetch info - * about shared addresses - * @return {AddressInfo[]} a list of AddressInfo + * @returns {AddressInfo[]} */ -PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayerIndex) { - opts = opts || {}; - var isOwned = index.copayerIndex === HDPath.SHARED_INDEX || index.copayerIndex === copayerIndex; - var ret = []; - var appendAddressInfo = function(address, isChange) { - ret.push({ - address: address, - addressStr: address.toString(), - isChange: isChange, - owned: isOwned - }); - }; +PublicKeyRing.prototype.getReceiveAddresses = function() { + this._checkCache(); - for (var i = 0; !opts.excludeChange && i < index.changeIndex; i++) { - appendAddressInfo(this.getAddress(i, true, index.copayerIndex), true); - } - for (var i = 0; !opts.excludeMain && i < index.receiveIndex; i++) { - appendAddressInfo(this.getAddress(i, false, index.copayerIndex), false); - } - - return ret; + var self = this; + return _.filter(this.getAddresses(), function(addr) { + return !self.addressIsChange(addr); + }); }; + + /** * @desc * Retrieve the public keys for all cosigners for a given path @@ -530,43 +591,43 @@ PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayer * @param {string} path - the BIP32 path * @return {Buffer[]} the public keys, in buffer format */ -PublicKeyRing.prototype.getForPath = function(path) { +PublicKeyRing.prototype._getForPath = function(path) { var p = HDPath.indexesForPath(path); return this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex); }; -/** - * @desc - * Retrieve the public keys for all cosigners for multiple paths - * @see PublicKeyRing#getForPath - * - * @param {string[]} paths - the BIP32 paths - * @return {Array[]} the public keys, in buffer format (matrix of Buffer, Buffer[][]) - */ -PublicKeyRing.prototype.getForPaths = function(paths) { - preconditions.checkArgument(!_.isUndefined(paths)); - preconditions.checkArgument(_.isArray(paths)); - preconditions.checkArgument(_.all(paths, _.isString)); - - return paths.map(this.getForPath.bind(this)); -}; /** * @desc * Retrieve the public keys for derived addresses and the public keys for copayers * - * @TODO: Should this exist? A user should just call getForPath(paths) + * @TODO: Should this exist? A user should just call _getForPath(paths) * * @param {string[]} paths - the paths to be derived * @return {Object} with keys pubKeys and copayerIds */ PublicKeyRing.prototype.forPaths = function(paths) { return { - pubKeys: paths.map(this.getForPath.bind(this)), + pubKeys: paths.map(this._getForPath.bind(this)), copayerIds: this.copayerIds, } }; +/** + * @desc + * Retrieve the public keys for all cosigners for multiple paths + * + * @param {string[]} paths - the BIP32 paths + * @return {Array[]} the public keys, in buffer format (matrix of Buffer, Buffer[][]) + */ +PublicKeyRing.prototype._getForPaths = function(paths) { + preconditions.checkArgument(!_.isUndefined(paths)); + preconditions.checkArgument(_.isArray(paths)); + preconditions.checkArgument(_.all(paths, _.isString)); + + return paths.map(this._getForPath.bind(this)); +}; + /** * @desc * Returns a map from a pubkey of an address to the id that generated it @@ -584,7 +645,7 @@ PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { inKeyMap[pubkeys[i]] = 1; }; - var keys = this.getForPaths(paths); + var keys = this._getForPaths(paths); for (var i in keys) { for (var copayerIndex in keys[i]) { var kHex = keys[i][copayerIndex].toString('hex'); @@ -693,25 +754,6 @@ PublicKeyRing.prototype._mergePubkeys = function(inPKR) { return hasChanged; }; -/** - * @desc - * Merges this public key ring with another one, optionally ignoring the - * wallet id - * - * @param {PublicKeyRing} inPkr - * @param {boolean} ignoreId - * @return {boolean} true if the internal state has changed - */ -PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { - this._checkInPKR(inPKR, ignoreId); - - var hasChanged = false; - hasChanged |= this.mergeIndexes(inPKR.indexes); - hasChanged |= this._mergePubkeys(inPKR); - - return !!hasChanged; -}; - /** * @desc @@ -734,4 +776,25 @@ PublicKeyRing.prototype.mergeIndexes = function(indexes) { } +/** + * @desc + * Merges this public key ring with another one, optionally ignoring the + * wallet id + * + * @param {PublicKeyRing} inPkr + * @param {boolean} ignoreId + * @return {boolean} true if the internal state has changed + */ +PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { + this._checkInPKR(inPKR, ignoreId); + + var hasChanged = false; + hasChanged |= this.mergeIndexes(inPKR.indexes); + hasChanged |= this._mergePubkeys(inPKR); + + return !!hasChanged; +}; + + + module.exports = PublicKeyRing; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 43ea774a4..be28d3808 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -95,7 +95,6 @@ function Wallet(opts) { this.registeredPeerIds = []; this.addressBook = opts.addressBook || {}; this.publicKey = this.privateKey.publicHex; - this.focusedTimestamp = opts.focusedTimestamp || 0; this.syncedTimestamp = opts.syncedTimestamp || 0; this.lastMessageFrom = {}; @@ -126,7 +125,7 @@ Wallet.TX_SIGNED_AND_BROADCASTED = 'txSignedAndBroadcasted'; Wallet.prototype.emitAndKeepAlive = function(args) { var args = Array.prototype.slice.call(arguments); - log.debug('Wallet Emitting:', args); + log.debug('Wallet:'+ this.getName() + ' Emitting:', args); this.keepAlive(); this.emit.apply(this, arguments); }; @@ -158,7 +157,6 @@ Wallet.PERSISTED_PROPERTIES = [ 'txProposals', 'privateKey', 'addressBook', - 'focusedTimestamp', 'syncedTimestamp', 'secretNumber', ]; @@ -314,7 +312,7 @@ Wallet.prototype.changeSettings = function(settings) { Wallet.prototype._onPublicKeyRing = function(senderId, data) { log.debug('Wallet:' + this.id + ' RECV PUBLICKEYRING:', data); - var inPKR = PublicKeyRing.fromObj(data.publicKeyRing); + var inPKR = PublicKeyRing.fromUntrustedObj(data.publicKeyRing); var wasIncomplete = !this.publicKeyRing.isComplete(); var hasChanged; @@ -631,7 +629,7 @@ Wallet.prototype._onAddressBook = function(senderId, data) { var self = this, hasChange; _.each(data.addressBook, function(value, key) { - if (!self.addressBook[key] && Address.validate(key)) { + if (key && !self.addressBook[key] && Address.validate(key)) { self.addressBook[key] = _.pick(value, ['createdTs', 'label']); @@ -647,18 +645,6 @@ Wallet.prototype._onAddressBook = function(senderId, data) { } }; -/** - * @desc Updates the wallet's last modified timestamp and triggers a save - * @param {number} ts - the timestamp - */ -Wallet.prototype.updateFocusedTimestamp = function(ts) { - preconditions.checkArgument(ts); - preconditions.checkArgument(_.isNumber(ts)); - preconditions.checkArgument(ts > 2999999999, 'use miliseconds'); - this.focusedTimestamp = ts; -}; - - Wallet.prototype.updateSyncedTimestamp = function(ts) { preconditions.checkArgument(ts); preconditions.checkArgument(_.isNumber(ts)); @@ -877,15 +863,9 @@ Wallet.decodeSecret = function(secretB) { } }; -/** - * @desc Locks other sessions from connecting to the wallet - * @see Async#lockIncommingConnections - */ -Wallet.prototype._lockIncomming = function() { - this.network.lockIncommingConnections(this.publicKeyRing.getAllCopayerIds()); -}; -Wallet.prototype._setBlockchainListeners = function() { +Wallet.prototype._setupBlockchainHandlers = function() { + var self = this; self.blockchain.removeAllListeners(); self.subscribeToAddresses(); @@ -900,14 +880,12 @@ Wallet.prototype._setBlockchainListeners = function() { log.debug('Wallet:' + self.id + 'blockchain disconnect event'); self.emitAndKeepAlive('insightError'); }); + self.blockchain.on('tx', function(tx) { log.debug('Wallet:' + self.id + ' blockchain tx event'); - var addresses = self.getAddressesInfo(); - var addr = _.findWhere(addresses, { - addressStr: tx.address - }); - if (addr) { - self.emitAndKeepAlive('tx', tx.address, addr.isChange); + var addresses = self.getAddresses(); + if (_.indexOf(addresses,tx.address)>=0) { + self.emitAndKeepAlive('tx', tx.address, self.addressIsChange(tx.address)); } }); @@ -916,6 +894,19 @@ Wallet.prototype._setBlockchainListeners = function() { } } +Wallet.prototype._setupNetworkHandlers = function() { + var self = this; + + var net = this.network; + net.removeAllListeners(); + net.on('connect', self._onConnect.bind(self)); + net.on('data', self._onData.bind(self)); + net.on('no_messages', self._onNoMessages.bind(self)); + net.on('connect_error', function() { + self.emitAndKeepAlive('connectionError'); + }); +}; + /** * @desc Sets up the networking with other peers. * @@ -925,43 +916,26 @@ Wallet.prototype._setBlockchainListeners = function() { * @emits ready * @emits txProposalsUpdated * - * @TODO: FIX PROTOCOL -- emit with a space is shitty - * @emits no messages */ Wallet.prototype.netStart = function() { var self = this; + if (self.netStarted) + return; + + + self._setupBlockchainHandlers(); + self.netStarted= true; + if (!this.isShared()) { self.emitAndKeepAlive('ready'); return; } - var net = this.network; - net.removeAllListeners(); - net.on('connect', self._onConnect.bind(self)); - net.on('data', self._onData.bind(self)); - net.on('no messages', self._onNoMessages.bind(self)); - net.on('connect_error', function() { - self.emitAndKeepAlive('connectionError'); - }); - - if (this.publicKeyRing.isComplete()) { - this._lockIncomming(); - } - - - - if (net.started) { - log.debug('Wallet:' + self.getName() + ': Wallet networking was ready') - self.emitAndKeepAlive('ready', net.getPeer()); - return; - } - - + self._setupNetworkHandlers(); var myId = self.getMyCopayerId(); var myIdPriv = self.getMyCopayerIdPriv(); - var startOpts = { copayerId: myId, privkey: myIdPriv, @@ -970,11 +944,12 @@ Wallet.prototype.netStart = function() { secretNumber: self.secretNumber, }; - + if (this.publicKeyRing.isComplete()) { + this.network.lockIncommingConnections(this.publicKeyRing.getAllCopayerIds()); + } log.debug('Wallet:' + self.id + ' Starting network.'); - net.start(startOpts, function() { - self._setBlockchainListeners(); - self.emitAndKeepAlive('ready', net.getPeer()); + this.network.start(startOpts, function() { + self.emitAndKeepAlive('ready'); }); }; @@ -1058,7 +1033,6 @@ Wallet.prototype.toObj = function() { privateKey: this.privateKey ? this.privateKey.toObj() : undefined, addressBook: this.addressBook, syncedTimestamp: this.syncedTimestamp || 0, - focusedTimestamp: this.focusedTimestamp || 0, secretNumber: this.secretNumber, }; @@ -1102,7 +1076,6 @@ Wallet.fromUntrustedObj = function(obj, readOpts) { * @param {string} o.networkName - 'livenet' or 'testnet' * @param {Object} o.publicKeyRing - PublicKeyRing to be deserialized by {@link PublicKeyRing#fromObj} * @param {number} o.syncedTimestamp - ts of the last synced message with insifht (in microseconds, as insight returns ts) - * @param {number} o.focusedTimestamp - last time this wallet was focused (open) by a user (in miliseconds) * @param {Object} o.txProposals - TxProposals to be deserialized by {@link TxProposals#fromObj} * @param {string} o.nickname - user's nickname * @@ -1179,8 +1152,6 @@ Wallet.fromObj = function(o, readOpts) { } opts.syncedTimestamp = o.syncedTimestamp || 0; - opts.focusedTimestamp = o.focusedTimestamp || 0; - opts.blockchainOpts = readOpts.blockchainOpts; opts.networkOpts = readOpts.networkOpts; @@ -1307,7 +1278,7 @@ Wallet.prototype.sendWalletId = function(recipients) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendPublicKeyRing = function(recipients) { - var publicKeyRingObj = this.publicKeyRing.toObj(); + var publicKeyRingObj = this.publicKeyRing.toTrimmedObj(); this._sendToPeers(recipients, { type: 'publicKeyRing', @@ -1375,21 +1346,15 @@ Wallet.prototype._doGenerateAddress = function(isChange) { return this.publicKeyRing.generateAddress(isChange, this.publicKey); }; -/** - * @callback addressCallback - * @param {string} addr - all the addresses of the wallet - */ /** * @desc Generate a new address * @param {boolean} isChange - whether to generate a change address or a receive address - * @param {addressCallback} cb * @return {string[]} a list of all the addresses generated so far for the wallet */ -Wallet.prototype.generateAddress = function(isChange, cb) { +Wallet.prototype.generateAddress = function(isChange) { var addr = this._doGenerateAddress(isChange); this.sendIndexes(); this._newAddresses(); - if (cb) return cb(addr); return addr; }; @@ -1418,6 +1383,35 @@ Wallet.prototype._getActionList = function(txp) { return peers; }; +/** + * @desc Retrieve Pendings Transaction proposals (see {@link TxProposals}) + * @return {Object[]} each object returned represents a transaction proposal + */ +Wallet.prototype.getPendingTxProposalsCount = function() { + var self = this; + var txps = this.txProposals.txps; + var maxRejectCount = this.maxRejectCount(); + var myId = this.getMyCopayerId(); + var pending =0, pendingForUs = 0; + + _.each(txps, function(inTxp, ntxid) { + if (!inTxp.isPending(maxRejectCount)) + return; + + pending++; + + if (!inTxp.signedBy[myId] && !inTxp.rejectedBy[myId] ) + pendingForUs++ + }); + + + return { + pending: pending, + pendingForUs: pendingForUs, + }; +}; + + /** * @desc Retrieve Pendings Transaction proposals (see {@link TxProposals}) * @return {Object[]} each object returned represents a transaction proposal @@ -1426,7 +1420,6 @@ Wallet.prototype.getPendingTxProposals = function() { var self = this; var ret = []; ret.txs = []; - var pendingForUs = 0; var txps = this.txProposals.txps; var maxRejectCount = this.maxRejectCount(); var satToUnit = 1 / this.settings.unitToSatoshi; @@ -1438,14 +1431,11 @@ Wallet.prototype.getPendingTxProposals = function() { var txp = _.clone(inTxp); txp.ntxid = ntxid; - pendingForUs++; var addresses = {}; var outs = JSON.parse(txp.builder.vanilla.outs); outs.forEach(function(o) { - if (!self.addressIsOwn(o.address)) { - if (!addresses[o.address]) addresses[o.address] = 0; - addresses[o.address] += (o.amountSatStr || Math.round(o.amount * bitcore.util.COIN)); - }; + if (!addresses[o.address]) addresses[o.address] = 0; + addresses[o.address] += (o.amountSatStr || Math.round(o.amount * bitcore.util.COIN)); }); txp.outs = []; _.each(addresses, function(value, address) { @@ -1461,7 +1451,6 @@ Wallet.prototype.getPendingTxProposals = function() { ret.txs.push(txp); }); - ret.pendingForUs = pendingForUs; return ret; }; @@ -1843,12 +1832,21 @@ Wallet.prototype.parsePaymentRequest = function(options, rawData) { */ Wallet.prototype._getPayProRefundOutputs = function(txp) { var pkr = this.publicKeyRing; - var index = pkr.getHDParams(this.publicKey); var amount = +txp.merchant.total.toString(10); var output = new PayPro.Output(); - var script = pkr.getScriptPubKeyHex(index.changeIndex, true, this.pubkey); - output.set('script', new Buffer(script, 'hex')); + var opts = JSON.parse(txp.builder.vanilla.opts); + if (!opts.remainderOut) { + log.warn('no remainder set. Not setting refund in PayPro'); + return; + } +console.log('[Wallet.js.1842:builder:]',txp.builder.vanilla.opts); //TODO + var addrStr = opts.remainderOut.address; + var addr = new bitcore.Address(addrStr); + var script = bitcore.Script.createP2SH(addr.payload()).getBuffer(); + log.debug('PayPro refund address set to:' + addrStr); + + output.set('script', script); output.set('amount', amount); return [output]; }; @@ -1866,7 +1864,6 @@ Wallet.prototype.createPayProPayment = function(txp) { var tx = txp.builder.build(); var txBuf = tx.serialize(); - var refund_outputs = this._getPayProRefundOutputs(txp); // We send this to the serve after receiving a PaymentRequest var pay = new PayPro(); @@ -1877,9 +1874,11 @@ Wallet.prototype.createPayProPayment = function(txp) { merchant_data = new Buffer(merchant_data, 'hex'); pay.set('merchant_data', merchant_data); } - pay.set('transactions', [txBuf]); - pay.set('refund_to', refund_outputs); + + var refund_outputs = this._getPayProRefundOutputs(txp); + if (refund_outputs) + pay.set('refund_to', refund_outputs); // Unused for now // options.memo = ''; @@ -1980,48 +1979,60 @@ Wallet.prototype.addSeenToTxProposals = function() { /** * @desc Alias for {@link PublicKeyRing#getAddresses} - * @TODO: remove this method and use getAddressesInfo everywhere * @return {Buffer[]} */ -Wallet.prototype.getAddresses = function(opts) { - return this.publicKeyRing.getAddresses(opts); +Wallet.prototype.getAddresses = function() { + return this.publicKeyRing.getAddresses(); +}; + + +/** + * @desc gets the list of addresses, orderder for the caller: + * 1) himselfs first + * 2) receive address first + * 3) last created first + */ +Wallet.prototype.getAddressesOrderer = function() { + return this.publicKeyRing.getAddressesOrderer(this.publicKey); }; /** - * @desc Retrieves all addresses as strings. - * - * @param {Object} opts - Same options as {@link PublicKeyRing#getAddresses} - * @return {string[]} + * @desc Alias for {@link PublicKeyRing#getAddresses} + * @return {Buffer[]} */ -Wallet.prototype.getAddressesStr = function(opts) { - return this.getAddresses(opts).map(function(a) { - return a.toString(); - }); +Wallet.prototype.getReceiveAddresses = function() { + return this.publicKeyRing.getReceiveAddresses(); }; + Wallet.prototype.subscribeToAddresses = function() { if (!this.publicKeyRing.isComplete()) return; - var addrInfo = this.publicKeyRing.getAddressesInfo(); - this.blockchain.subscribe(_.pluck(addrInfo, 'addressStr')); - log.debug('Subscribed to ' + addrInfo.length + ' addresses'); + var addresses = this.getAddresses(); + this.blockchain.subscribe(addresses); + log.debug('Wallet:' + this.getName() + ' Subscribed to:' + addresses.length + ' addresses'); }; -/** - * @desc Alias for {@link PublicKeyRing#getAddressesInfo} - */ -Wallet.prototype.getAddressesInfo = function(opts) { - return this.publicKeyRing.getAddressesInfo(opts, this.publicKey); -}; /** * @desc Returns true if a given address was generated by deriving our master public key * @return {boolean} */ Wallet.prototype.addressIsOwn = function(addrStr) { - return !!this.publicKeyRing.addressToPath[addrStr]; + return this.publicKeyRing.addressIsOwn(addrStr); }; +/** + * @desc Returns true if a given address is a change address (remainder) + * @param addrStr + * @return {boolean} + */ +Wallet.prototype.addressIsChange = function(addrStr) { + return this.publicKeyRing.addressIsChange(addrStr); +}; + + + /** * Estimate a tx fee in satoshis given its input count * (only used when spending all wallet funds) @@ -2108,7 +2119,11 @@ Wallet.prototype.maxRejectCount = function() { // TODO: Can we add cache to getUnspent? Wallet.prototype.getUnspent = function(cb) { var self = this; - this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { + var addresses = this.getAddresses(); + + + log.debug('Wallet ' + this.getName() + ': Getting unspents from ' + addresses.length + ' addresses'); + this.blockchain.getUnspent(addresses, function(err, unspentList) { if (err) { return cb(err); @@ -2316,7 +2331,7 @@ Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utx */ Wallet.prototype.updateIndexes = function(callback) { var self = this; - if (!self.isReady()) + if (!self.isComplete()) return callback(); log.debug('Wallet:' + this.id + ' Updating indexes...'); var tasks = this.publicKeyRing.indexes.map(function(index) { @@ -2370,7 +2385,8 @@ Wallet.prototype.deriveAddresses = function(index, amount, isChange, copayerInde var ret = new Array(amount); for (var i = 0; i < amount; i++) { - ret[i] = this.publicKeyRing.getAddress(index + i, isChange, copayerIndex).toString(); + // TODO + ret[i] = this.publicKeyRing._getAddress(index + i, isChange, copayerIndex).toString(); } return ret; }; @@ -2433,10 +2449,12 @@ Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) * @desc Closes the wallet and disconnects all services */ Wallet.prototype.close = function(cb) { + log.debug('## CLOSING Wallet: ' + this.id); + this.network.removeAllListeners(); this.network.cleanUp(); + this.blockchain.removeAllListeners(); this.blockchain.destroy(); - log.debug('## CLOSING Wallet: ' + this.id); // TODO // this.lock.release(function() { if (cb) return cb(); @@ -2512,7 +2530,7 @@ Wallet.prototype.requiresMultipleSignatures = function() { * @desc Returns true if the keyring is complete * @return {boolean} */ -Wallet.prototype.isReady = function() { +Wallet.prototype.isComplete = function() { return this.publicKeyRing.isComplete(); }; @@ -2532,7 +2550,7 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { } opts = opts || {}; - var addresses = self.getAddressesInfo(); + var addresses = self.getAddresses(); var proposals = self.txProposals.txps; var satToUnit = 1 / self.settings.unitToSatoshi; @@ -2542,33 +2560,26 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { function extractInsOuts(tx) { // Inputs var inputs = _.map(tx.vin, function(item) { - var addr = _.findWhere(addresses, { - addressStr: item.addr - }); return { type: 'in', - address: addr ? addr.addressStr : item.addr, - isMine: !_.isUndefined(addr), - isChange: addr ? !!addr.isChange : false, + address: item.addr, + isMine: self.addressIsOwn(item.addr), + isChange: self.addressIsChange(item.addr), amountSat: item.valueSat, } }); var outputs = _.map(tx.vout, function(item) { - var addr; var itemAddr; // If classic multisig, ignore if (item.scriptPubKey && item.scriptPubKey.addresses.length == 1) { itemAddr = item.scriptPubKey.addresses[0]; - addr = _.findWhere(addresses, { - addressStr: itemAddr, - }); } return { type: 'out', - address: addr ? addr.addressStr : itemAddr, - isMine: !_.isUndefined(addr), - isChange: addr ? !!addr.isChange : false, + address: itemAddr, + isMine: self.addressIsOwn(itemAddr), + isChange: self.addressIsChange(itemAddr), label: self.addressBook[itemAddr] ? self.addressBook[itemAddr].label : undefined, amountSat: parseInt((item.value * bitcore.util.COIN).toFixed(0)), } @@ -2606,6 +2617,8 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { var fees = parseInt((tx.fees * bitcore.util.COIN).toFixed(0)); var amount; + + if (amountIn == (amountOut + amountOutChange + (amountIn > 0 ? fees : 0))) { tx.action = 'moved'; amount = amountOut; @@ -2660,10 +2673,9 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { }; if (addresses.length > 0) { - var addressesStr = _.pluck(addresses, 'addressStr'); var from = (opts.currentPage - 1) * opts.itemsPerPage; var to = opts.currentPage * opts.itemsPerPage; - self.blockchain.getTransactions(addressesStr, from, to, function(err, res) { + self.blockchain.getTransactions(addresses, from, to, function(err, res) { if (err) return cb(err); _.each(res.items, function(tx) { diff --git a/js/plugins/InsightStorage.js b/js/plugins/InsightStorage.js index 25a125be8..2492cde5f 100644 --- a/js/plugins/InsightStorage.js +++ b/js/plugins/InsightStorage.js @@ -85,7 +85,6 @@ InsightStorage.prototype._makeGetRequest = function(passphrase, key, callback) { 'Authorization': authHeader } }; - log.debug('Insight request', getParams); this.request.get(getParams, function(err, response, body) { if (err) { diff --git a/js/routes.js b/js/routes.js index a9e0a59e7..12d8b93f2 100644 --- a/js/routes.js +++ b/js/routes.js @@ -47,26 +47,32 @@ angular }) .when('/homeWallet', { templateUrl: 'views/homeWallet.html', + walletShouldBeComplete: true, logged: true }) .when('/receive', { templateUrl: 'views/receive.html', + walletShouldBeComplete: true, logged: true }) .when('/history', { templateUrl: 'views/history.html', + walletShouldBeComplete: true, logged: true }) .when('/send', { templateUrl: 'views/send.html', + walletShouldBeComplete: true, logged: true }) .when('/more', { templateUrl: 'views/more.html', + walletShouldBeComplete: true, logged: true }) .when('/settings', { templateUrl: 'views/settings.html', + walletShouldBeComplete: true, logged: false }) .when('/warning', { @@ -119,6 +125,9 @@ angular $idle.unwatch(); $location.path('/'); } + if ($rootScope.wallet && !$rootScope.wallet.isComplete() && next.walletShouldBeComplete) { + $location.path('/copayers'); + } } }); }) diff --git a/js/services/applicationService.js b/js/services/applicationService.js new file mode 100644 index 000000000..3612bf0d1 --- /dev/null +++ b/js/services/applicationService.js @@ -0,0 +1,17 @@ +'use strict'; +angular.module('copayApp.services') + .factory('applicationService', function() { + var root = {}; + + root.restart = function() { + // Go home reloading the application + var hashIndex = window.location.href.indexOf('#!/'); + window.location = window.location.href.substr(0, hashIndex); + }; + + root.reload = function() { + window.location.reload(); + }; + + return root; + }); diff --git a/js/services/balanceService.js b/js/services/balanceService.js new file mode 100644 index 000000000..c2be78dcb --- /dev/null +++ b/js/services/balanceService.js @@ -0,0 +1,93 @@ +'use strict'; +var bitcore = require('bitcore'); + +angular.module('copayApp.services') + .factory('balanceService', function($rootScope, $filter, rateService) { + var root = {}; + var _balanceCache = {}; + root.clearBalanceCache = function(w) { + delete _balanceCache[w.getId()]; + }; + + root._fetchBalance = function(w, cb) { + cb = cb || function() {}; + var satToUnit = 1 / w.settings.unitToSatoshi; + var COIN = bitcore.util.COIN; + w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat, safeUnspentCount) { + if (err) return cb(err); + + var r = {}; + r.totalBalance = $filter('noFractionNumber')(balanceSat * satToUnit); + r.totalBalanceBTC = (balanceSat / COIN); + var availableBalanceNr = safeBalanceSat * satToUnit; + r.availableBalance = $filter('noFractionNumber')(safeBalanceSat * satToUnit); + r.availableBalanceBTC = (safeBalanceSat / COIN); + r.safeUnspentCount = safeUnspentCount; + + r.lockedBalance = (balanceSat - safeBalanceSat) * satToUnit; + r.lockedBalanceBTC = (balanceSat - safeBalanceSat) / COIN; + + + if (r.safeUnspentCount) { + var estimatedFee = copay.Wallet.estimatedFee(r.safeUnspentCount); + r.topAmount = (((availableBalanceNr * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); + } + + var balanceByAddr = {}; + for (var ii in balanceByAddrSat) { + balanceByAddr[ii] = balanceByAddrSat[ii] * satToUnit; + } + r.balanceByAddr = balanceByAddr; + + if (rateService.isAvailable()) { + var totalBalanceAlternative = rateService.toFiat(balanceSat, w.settings.alternativeIsoCode); + var lockedBalanceAlternative = rateService.toFiat(balanceSat - safeBalanceSat, w.settings.alternativeIsoCode); + var alternativeConversionRate = rateService.toFiat(100000000, w.settings.alternativeIsoCode); + + r.totalBalanceAlternative = $filter('noFractionNumber')(totalBalanceAlternative, 2); + r.lockedBalanceAlternative = $filter('noFractionNumber')(lockedBalanceAlternative, 2); + r.alternativeConversionRate = $filter('noFractionNumber')(alternativeConversionRate, 2); + + r.alternativeBalanceAvailable = true; + r.alternativeIsoCode = w.settings.alternativeIsoCode; + }; + + r.updatingBalance = false; + + return cb(null, r) + }); + }; + + root.update = function(w, cb, isFocused) { + w = w || $rootScope.wallet; + if (!w || !w.isComplete()) return; + + copay.logger.debug('Updating balance of:', w.getName(), isFocused); + var wid = w.getId(); + + + // cache available? Set the cached values until we updated them + if (_balanceCache[wid]) { + w.balanceInfo = _balanceCache[wid]; + } else { + if (isFocused) + $rootScope.updatingBalance = true; + } + + w.balanceInfo = w.balanceInfo || {}; + w.balanceInfo.updating = true; + + root._fetchBalance(w, function(err, res) { + if (err) throw err; + w.balanceInfo=_balanceCache[wid] = res; + w.balanceInfo.updating = false; + + if (isFocused) { + $rootScope.updatingBalance = false; + } + if (cb) cb(); + }); + }; + + return root; + }); diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js deleted file mode 100644 index 935bc1ee1..000000000 --- a/js/services/controllerUtils.js +++ /dev/null @@ -1,382 +0,0 @@ -'use strict'; -var bitcore = require('bitcore'); - -angular.module('copayApp.services') - .factory('controllerUtils', function($rootScope, $sce, $location, $filter, notification, $timeout, rateService) { - var root = {}; - - - root.redirIfNotComplete = function() { - var w = $rootScope.wallet; - if (w) { - if (!w.isReady()) { - $location.path('/copayers'); - } - } else { - $location.path('/'); - } - }; - - - root.redirIfLogged = function() { - var w = $rootScope.wallet; - if (w) { - if (!w.isReady()) { - $location.path('/copayers'); - } else { - $location.path('homeWallet'); - } - } - }; - - root.logout = function() { - - if ($rootScope.iden) { - $rootScope.iden.store(null, function() { - $rootScope.iden.close(); - - delete $rootScope['wallet']; - delete $rootScope['iden']; - - // Go home reloading the application - var hashIndex = window.location.href.indexOf('#!/'); - window.location = window.location.href.substr(0, hashIndex); - }); - } - }; - - root.onError = function(scope) { - if (scope) { - scope.loading = false; - } - } - - root.onErrorDigest = function(scope, msg) { - root.onError(scope); - if (msg) { - notification.error('Error', msg); - } - }; - - - root.isFocusedWallet = function(wid) { - return $rootScope.wallet && wid === $rootScope.wallet.getId(); - }; - - - - root.installWalletHandlers = function($scope, w) { - - var wid = w.getId(); - w.on('connectionError', function() { - if (root.isFocusedWallet(wid)) { - var message = "Could not connect to the Insight server. Check your settings and network configuration"; - notification.error('Networking Error', message); - root.onErrorDigest($scope); - } - }); - - w.on('corrupt', function(peerId) { - if (root.isFocusedWallet(wid)) { - notification.error('Error', $filter('translate')('Received corrupt message from ') + peerId); - } - }); - w.on('ready', function(myPeerID) { - $scope.loading = false; - if ($rootScope.initialConnection) { - $rootScope.initialConnection = false; - if ($rootScope.pendingPayment) { - $location.path('paymentIntent'); - } else { - root.redirIfLogged(); - } - } - }); - - w.on('tx', function(address, isChange) { - if (!isChange) { - notification.funds('Funds received on ' + w.getName(), address); - } - root.updateBalance(w, function() { - $rootScope.$digest(); - }); - }); - - w.on('balanceUpdated', function() { - root.updateBalance(w, function() { - $rootScope.$digest(); - }); - }); - - w.on('insightReconnected', function() { - $rootScope.reconnecting = false; - root.updateAddressList(w.getId()); - root.updateBalance(w, function() { - $rootScope.$digest(); - }); - }); - - w.on('insightError', function() { - if (root.isFocusedWallet(wid)) { - $rootScope.reconnecting = true; - $rootScope.$digest(); - } - }); - w.on('newAddresses', function() { - root.updateBalance(w); - }); - - w.on('txProposalsUpdated', function() { - if (root.isFocusedWallet(wid)) { - root.updateTxs(); - } - }); - - w.on('paymentACK', function(memo) { - notification.success('Payment Acknowledged', memo); - }); - - w.on('txProposalEvent', function(e) { - - if (root.isFocusedWallet(wid)) { - root.updateTxs(); - } - - // TODO: add wallet name notification - var user = w.publicKeyRing.nicknameForCopayer(e.cId); - var name = w.getName(); - switch (e.type) { - case 'new': - notification.info('[' + name + '] New Transaction', - $filter('translate')('You received a transaction proposal from') + ' ' + user); - break; - case 'signed': - notification.info('[' + name + '] Transaction Signed', - $filter('translate')('A transaction was signed by') + ' ' + user); - break; - case 'signedAndBroadcasted': - notification.info('[' + name + '] Transaction Approved', - $filter('translate')('A transaction was signed and broadcasted by') + ' ' + user); - break; - case 'rejected': - notification.info('[' + name + '] Transaction Rejected', - $filter('translate')('A transaction was rejected by') + ' ' + user); - break; - case 'corrupt': - notification.error('[' + name + '] Transaction Error', - $filter('translate')('Received corrupt transaction from') + ' ' + user); - break; - } - $rootScope.$digest(); - }); - w.on('addressBookUpdated', function(dontDigest) { - if (root.isFocusedWallet(wid)) { - if (!dontDigest) { - $rootScope.$digest(); - } - } - }); - w.on('connect', function(peerID) { - $rootScope.$digest(); - }); - w.on('close', root.onErrorDigest); - w.on('locked', root.onErrorDigest.bind(this)); - - }; - - root.setupGlobalVariables = function(iden) { - notification.enableHtml5Mode(); // for chrome: if support, enable it - $rootScope.unitName = config.unitName; - $rootScope.pendingTxCount = 0; - $rootScope.initialConnection = true; - $rootScope.reconnecting = false; - $rootScope.isCollapsed = true; - - $rootScope.iden = iden; - }; - - - root.rebindWallets = function($scope, iden) { - _.each(iden.listWallets(), function(wallet) { - preconditions.checkState(wallet); - root.installWalletHandlers($scope, wallet); - }); - }; - - root.setPaymentWallet = function(w) { - root.setFocusedWallet(w); - $location.path('/send'); - }; - - root.setFocusedWallet = function(w) { - if (!_.isObject(w)) - w = $rootScope.iden.getWalletById(w); - - preconditions.checkState(w && _.isObject(w)); - - $rootScope.wallet = w; - w.updateFocusedTimestamp(Date.now()); - root.redirIfLogged(); - root.updateTxs(); - root.updateBalance(w, function() { - $rootScope.$digest(); - }) - }; - - root.bindProfile = function($scope, iden, w) { - root.setupGlobalVariables(iden); - root.rebindWallets($scope, iden); - if (w) { - root.setFocusedWallet(w); - } else { - $location.path('/create'); - } - $timeout(function() { - $rootScope.$digest() - }, 1); - }; - - // On the focused wallet - root.updateAddressList = function(wid) { - - if (!wid || root.isFocusedWallet(wid)) { - var w = $rootScope.wallet; - - if (w && w.isReady()) { - $rootScope.addrInfos = w.getAddressesInfo(); - } - } - }; - - var _balanceCache = {}; - root.clearBalanceCache = function(w) { - delete _balanceCache[w.getId()]; - }; - - - root._fetchBalance = function(w, cb) { - cb = cb || function() {}; - var satToUnit = 1 / w.settings.unitToSatoshi; - var COIN = bitcore.util.COIN; - - w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat, safeUnspentCount) { - if (err) return cb(err); - - var r = {}; - r.totalBalance = balanceSat * satToUnit; - r.totalBalanceBTC = (balanceSat / COIN); - r.availableBalance = safeBalanceSat * satToUnit; - r.availableBalanceBTC = (safeBalanceSat / COIN); - r.safeUnspentCount = safeUnspentCount; - - r.lockedBalance = (balanceSat - safeBalanceSat) * satToUnit; - r.lockedBalanceBTC = (balanceSat - safeBalanceSat) / COIN; - - - if (r.safeUnspentCount) { - var estimatedFee = copay.Wallet.estimatedFee(r.safeUnspentCount); - r.topAmount = (((r.availableBalance * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); - } - - var balanceByAddr = {}; - for (var ii in balanceByAddrSat) { - balanceByAddr[ii] = balanceByAddrSat[ii] * satToUnit; - } - r.balanceByAddr = balanceByAddr; - root.updateAddressList(); - - - - if (rateService.isAvailable()) { - r.totalBalanceAlternative = rateService.toFiat(balanceSat, w.settings.alternativeIsoCode); - r.alternativeIsoCode = w.settings.alternativeIsoCode; - r.lockedBalanceAlternative = rateService.toFiat(balanceSat - safeBalanceSat, w.settings.alternativeIsoCode); - r.alternativeConversionRate = rateService.toFiat(100000000, w.settings.alternativeIsoCode); - r.alternativeBalanceAvailable = true; - }; - - r.updatingBalance = false; - - return cb(null, r) - }); - }; - - root._updateScope = function(w, data, scope, cb) { - _.each(data, function(v, k) { - scope[k] = data[k]; - }) - if (cb) return cb(); - }; - - root.updateBalance = function(w, cb, refreshAll) { - - w = w || $rootScope.wallet; - if (!w) return root.onErrorDigest(); - if (!w.isReady()) return; - - w.balanceInfo = {}; - var scope = root.isFocusedWallet(w.id) && !refreshAll ? $rootScope : w.balanceInfo; - - var wid = w.getId(); - - if (_balanceCache[wid]) { - root._updateScope(w, _balanceCache[wid], scope, function() { - if (root.isFocusedWallet(w.id) && !refreshAll) { - setTimeout(function() { - $rootScope.$digest(); - }, 1); - } - }); - } else { - scope.updatingBalance = true; - } - - root._fetchBalance(w, function(err, res) { - if (err) throw err; - _balanceCache[wid] = res; - root._updateScope(w, _balanceCache[wid], scope, function() { - scope.updatingBalance = false; - if (cb) cb(); - }); - }); - }; - - root.setAlternativeAmount = function(w, tx, cb) { - rateService.whenAvailable(function() { - _.each(tx.outs, function(out) { - var valueSat = out.value * w.settings.unitToSatoshi; - out.alternativeAmount = rateService.toFiat(valueSat, w.settings.alternativeIsoCode); - out.alternativeIsoCode = w.settings.alternativeIsoCode; - }); - if (cb) return cb(tx); - }); - }; - - root.updateTxs = function() { - var w = $rootScope.wallet; - if (!w) return; - - var res = w.getPendingTxProposals(); - _.each(res.txs, function(tx) { - root.setAlternativeAmount(w, tx); - if (tx.merchant) { - var url = tx.merchant.request_url; - var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1]; - tx.merchant.domain = domain; - } - }); - $rootScope.txps = res.txs; - $rootScope.pendingTxCount = res.pendingForUs; - }; - - root.deleteWallet = function($scope, w, cb) { - if (!w) return root.onErrorDigest(); - var name = w.getName(); - $rootScope.iden.deleteWallet(w.id, function() { - notification.info(name + ' deleted', $filter('translate')('This wallet was deleted')); - return cb(); - }); - }; - - return root; - }); diff --git a/js/services/identityService.js b/js/services/identityService.js index 6c54b189d..6fb1cd0aa 100644 --- a/js/services/identityService.js +++ b/js/services/identityService.js @@ -1,9 +1,9 @@ 'use strict'; - angular.module('copayApp.services') - .factory('identityService', function($rootScope, $location, $timeout, pluginManager, controllerUtils) { - var root = {}; + .factory('identityService', function($rootScope, $location, $timeout, $filter, pluginManager, notification, pendingTxsService, balanceService, applicationService) { + notification.enableHtml5Mode(); // for chrome: if support, enable it + var root = {}; root.check = function(scope) { copay.Identity.checkIfExistsAny({ pluginManager: pluginManager, @@ -22,10 +22,25 @@ angular.module('copayApp.services') }); }; - root.create = function(scope, form) { + root.goWalletHome = function() { + var w = $rootScope.wallet; + if (w) { + if (!w.isComplete()) { + $location.path('/copayers'); + } else { + if ($rootScope.pendingPayment) { + $location.path('paymentIntent'); + } else { + $location.path('homeWallet'); + } + } + } + }; + + root.create = function(email, password, cb) { copay.Identity.create({ - email: form.email.$modelValue, - password: form.password.$modelValue, + email: email, + password: password, pluginManager: pluginManager, network: config.network, networkName: config.networkName, @@ -33,19 +48,10 @@ angular.module('copayApp.services') passphraseConfig: config.passphraseConfig, failIfExists: true, }, function(err, iden) { - if (err || !iden) { - copay.logger.debug(err); - if (err && (err.match('EEXISTS') || err.match('BADCREDENTIALS'))) { - scope.error = 'User already exists!'; - } else { - scope.error = 'Unknown error when connecting Insight Server'; - } - $rootScope.starting = false; - $timeout(function() { - $rootScope.$digest() - }, 1); - return; - } + if (err) return cb(err); + preconditions.checkState(iden); + root.bind(iden); + var walletOptions = { nickname: iden.fullName, networkName: config.networkName, @@ -55,47 +61,255 @@ angular.module('copayApp.services') name: 'My wallet', }; iden.createWallet(walletOptions, function(err, wallet) { - if (err || !wallet) { - copay.logger.debug(err); - scope.error = 'Could not create default wallet'; - $rootScope.starting = false; - $timeout(function() { - $rootScope.$digest() - }, 1); - return; - } - controllerUtils.bindProfile(scope, iden, wallet.id); + return cb(err); }); }); }; - root.open = function(scope, form) { - copay.Identity.open({ - email: form.email.$modelValue, - password: form.password.$modelValue, + root.open = function(email, password, cb) { + var opts = { + email: email, + password: password, pluginManager: pluginManager, network: config.network, networkName: config.networkName, walletDefaults: config.wallet, passphraseConfig: config.passphraseConfig, - }, function(err, iden) { - if (err && !iden) { - if ((err.toString() || '').match('PNOTFOUND')) { - scope.error = 'Invalid email or password'; - } else { - scope.error = 'Unknown error'; - } - $rootScope.starting = false; - $timeout(function() { - $rootScope.$digest() - }, 1); - } else { - var firstWallet = iden.getLastFocusedWallet(); - controllerUtils.bindProfile(scope, iden, firstWallet); + }; + + copay.Identity.open(opts, function(err, iden) { + if (err) return cb(err); + root.bind(iden); + iden.openWallets(); + return cb(); + }); + }; + + root.deleteWallet = function(w, cb) { + $rootScope.iden.deleteWallet(w.id, cb); + }; + + root.isFocused = function(wid) { + return $rootScope.wallet && wid === $rootScope.wallet.getId(); + }; + + root.setupGlobalVariables = function(iden) { + $rootScope.pendingTxCount = 0; + $rootScope.reconnecting = false; + $rootScope.iden = iden; + }; + + root.setPaymentWallet = function(w) { + root.setFocusedWallet(w); + $location.path('/send'); + }; + + root.setFocusedWallet = function(w, dontUpdateIt) { + if (!_.isObject(w)) + w = $rootScope.iden.getWalletById(w); + preconditions.checkState(w && _.isObject(w)); + + copay.logger.debug('Set focus:', w.getName()); + $rootScope.wallet = w; + + if (!dontUpdateIt) + $rootScope.iden.updateFocusedTimestamp(w.getId()); + + pendingTxsService.update(); + $timeout(function() { + $rootScope.$digest(); + }) + }; + + root.installWalletHandlers = function(w) { + var wid = w.getId(); + w.on('connectionError', function() { + if (root.isFocused(wid)) { + var message = "Could not connect to the Insight server. Check your settings and network configuration"; + notification.error('Networking Error', message); } }); + + w.on('corrupt', function(peerId) { + if (root.isFocused(wid)) { + notification.error('Error', $filter('translate')('Received corrupt message from ') + peerId); + } + }); + w.on('ready', function() { + var isFocused = root.isFocused(wid); + copay.logger.debug('Wallet:' + w.getName() + ' is ready. Focused:', isFocused); + + balanceService.update(w, function() { + $rootScope.$digest(); + }, isFocused); + }); + + w.on('tx', function(address, isChange) { + if (!isChange) { + notification.funds('Funds received on ' + w.getName(), address); + } + balanceService.update(w, function() { + $rootScope.$digest(); + }, root.isFocused(wid)); + }); + + w.on('balanceUpdated', function() { + balanceService.update(w, function() { + $rootScope.$digest(); + }, root.isFocused(wid)); + }); + + w.on('insightReconnected', function() { + $rootScope.reconnecting = false; + balanceService.update(w, function() { + $rootScope.$digest(); + }, root.isFocused(wid)); + }); + + w.on('insightError', function() { + if (root.isFocused(wid)) { + $rootScope.reconnecting = true; + $rootScope.$digest(); + } + }); + w.on('newAddresses', function() { + // Nothing yet + }); + + w.on('txProposalsUpdated', function() { + if (root.isFocused(wid)) { + pendingTxsService.update(); + } + }); + + w.on('paymentACK', function(memo) { + notification.success('Payment Acknowledged', memo); + }); + + w.on('txProposalEvent', function(e) { + if (root.isFocused(wid)) { + pendingTxsService.update(); + } + + // TODO: add wallet name notification + var user = w.publicKeyRing.nicknameForCopayer(e.cId); + var name = w.getName(); + switch (e.type) { + case 'new': + notification.info('[' + name + '] New Transaction', + $filter('translate')('You received a transaction proposal from') + ' ' + user); + break; + case 'signed': + notification.info('[' + name + '] Transaction Signed', + $filter('translate')('A transaction was signed by') + ' ' + user); + break; + case 'signedAndBroadcasted': + notification.info('[' + name + '] Transaction Approved', + $filter('translate')('A transaction was signed and broadcasted by') + ' ' + user); + break; + case 'rejected': + notification.info('[' + name + '] Transaction Rejected', + $filter('translate')('A transaction was rejected by') + ' ' + user); + break; + case 'corrupt': + notification.error('[' + name + '] Transaction Error', + $filter('translate')('Received corrupt transaction from') + ' ' + user); + break; + } + $rootScope.$digest(); + }); + w.on('addressBookUpdated', function(dontDigest) { + if (root.isFocused(wid)) { + if (!dontDigest) { + $rootScope.$digest(); + } + } + }); + w.on('connect', function(peerID) { + $rootScope.$digest(); + }); + // TODO? + // w.on('close', ); + // w.on('locked',); + }; + + root.bind = function(iden) { + preconditions.checkArgument(_.isObject(iden)); + copay.logger.debug('Binding profile...'); + + var self = this; + root.setupGlobalVariables(iden); + + iden.on('newWallet', function(wid) { + var w = iden.getWalletById(wid); + copay.logger.debug('newWallet:', w.getName(), wid, iden.getLastFocusedWalletId()); + root.installWalletHandlers(w); + if (wid == iden.getLastFocusedWalletId()) { + $rootScope.starting = false; + copay.logger.debug('GOT Focused wallet:', w.getName()); + root.setFocusedWallet(w, true); + root.goWalletHome(); + } + // At the end (after all handlers are in place)...start the wallet. + w.netStart(); + }); + + iden.on('noWallets', function() { + $location.path('/create'); + $rootScope.$digest() + }); + + iden.on('deletedWallet', function(wid) { + notification.info('Wallet deleted', $filter('translate')('This wallet was deleted')); + if ($rootScope.wallet.id === wid) { + $rootScope.wallet = null; + var lastFocused = iden.getLastFocusedWalletId(); + root.setFocusedWallet(lastFocused); + } + }); + + iden.on('closed', function() { + delete $rootScope['wallet']; + delete $rootScope['iden']; + applicationService.restart(); + }); + }; + + root.signout = function() { + if ($rootScope.iden) { + $rootScope.iden.close(); + } + }; + + root.createWallet = function(opts, cb) { + $rootScope.iden.createWallet(opts, cb); + }; + + root.importWallet = function(encryptedObj, pass, opts, cb) { + copay.Compatibility.importEncryptedWallet($rootScope.iden, encryptedObj, pass, opts); + }; + + root.joinWallet = function(opts, cb) { + $rootScope.iden.joinWallet(opts, function(err, w) { + return cb(err); + }); + }; + + root.importProfile = function(str, password, cb) { + copay.Identity.importFromEncryptedFullJson(str, password, { + pluginManager: pluginManager, + network: config.network, + networkName: config.networkName, + walletDefaults: config.wallet, + passphraseConfig: config.passphraseConfig, + }, function(err, iden, walletObjs) { + if (err) return cb(err); + root.bind(iden); + iden.importMultipleWalletsFromObj(walletObjs); + return cb(); + }); }; return root; diff --git a/js/services/pendingTxsService.js b/js/services/pendingTxsService.js new file mode 100644 index 000000000..ed0cb10bf --- /dev/null +++ b/js/services/pendingTxsService.js @@ -0,0 +1,15 @@ + +'use strict'; + +angular.module('copayApp.services') + .factory('pendingTxsService', function($rootScope) { + var root = {}; + root.update = function(w) { + var w = $rootScope.wallet; + if (!w) return; + + var ret = w.getPendingTxProposalsCount(); + $rootScope.pendingTxCount = ret.pendingForUs; + }; + return root; + }); diff --git a/test/PublicKeyRing.js b/test/PublicKeyRing.js index 321b81ffe..e575e3e46 100644 --- a/test/PublicKeyRing.js +++ b/test/PublicKeyRing.js @@ -136,7 +136,8 @@ describe('PublicKeyRing model', function() { [true, false].forEach(function(isChange) { for (var i = 0; i < 2; i++) { - var a = w.generateAddress(isChange, k.pub); + var aStr = w.generateAddress(isChange, k.pub); + var a= new bitcore.Address(aStr); a.isValid().should.equal(true); a.isScript().should.equal(true); a.network().name.should.equal('livenet'); @@ -152,27 +153,46 @@ describe('PublicKeyRing model', function() { var setup = getCachedW(); var pubkeyring = setup.w; - var address = pubkeyring.getAddress(3, false, 4); - - (pubkeyring._cacheAddressMap[3][0][4]).should.equal(address); + var address = pubkeyring._getAddress(3, false, 4); + pubkeyring.cache.addressToPath[address].should.equal("m/45'/4/0/3"); }); - it('getAddress cache hit doesn\'t alter state', function() { + it('cach4es calls to getAddress (2)', function() { var setup = getCachedW(); var pubkeyring = setup.w; - var spySave; - - pubkeyring.getAddress(3, false, 4); - - spySave = sinon.stub(pubkeyring, '_cacheAddress'); - spySave.onFirstCall().throws(new Error()); - - pubkeyring.getAddress(3, false, 4); - - spySave.restore(); + var address = pubkeyring.generateAddress(false, setup.pub); + _.indexOf(pubkeyring.getReceiveAddresses(),address).should.be.above(-1); + _.indexOf(pubkeyring.getAddresses(),address).should.be.above(-1); }); - it('should return PublicKeyRing addresses', function() { + + it('cach4es calls to getAddress (3)', function() { + var setup = getCachedW(); + var pubkeyring = setup.w; + var address = pubkeyring.generateAddress(true, setup.pub); + _.indexOf(pubkeyring.getReceiveAddresses(),address).should.be.equal(-1); + _.indexOf(pubkeyring.getAddresses(),address).should.be.above(-1); + }); + + it('should generate one address by default', function() { + var k = createW(); + var w = k.w; + var a = w.getAddresses(); + a.length.should.equal(1); + }); + + it('should generate one address by default', function() { + var k = createW(); + var w = k.w; + + var a = w.getAddresses(); + a.length.should.equal(1); + a = w.getAddresses(); + a.length.should.equal(1); + }); + + + it('should generate 4+1 addresses', function() { var k = createW(); var w = k.w; @@ -184,15 +204,16 @@ describe('PublicKeyRing model', function() { w.generateAddress(isChange, k.pub); } }); + }); - var as = w.getAddressesInfo(); - as.length.should.equal(5); // include pre-generated shared one - for (var j in as) { - var a = as[j]; - a.address.isValid().should.equal(true); - a.addressStr.should.equal(a.address.toString()); - a.isChange.should.equal([false, true, true, false, false][j]); - } + it('should check isChange 4+1 addresses', function() { + var k = createW(); + var w = k.w; + var a = w.getAddresses(); + _.each(a, function(a, j) { + var addr = new bitcore.Address(a); + w.addressIsChange(a).should.equal([false, true, true, false, false][j]); + }); }); @@ -405,7 +426,7 @@ describe('PublicKeyRing model', function() { (function() { PublicKeyRing.fromObj(pkr); - }).should.throw('bad data format: Did you use .toObj()?'); + }).should.throw('format'); }); @@ -432,6 +453,33 @@ describe('PublicKeyRing model', function() { }); + it('#fromObj old backup ', function() { + var pkr = PublicKeyRing.fromObj(JSON.parse(obj)); + should.exist(pkr); + pkr.isComplete().should.equal(true); + pkr.requiredCopayers.should.equal(2); + pkr.totalCopayers.should.equal(2); + }); + + it('#fromObj #toObj rountrip', function() { + var obj2 = PublicKeyRing.fromObj(JSON.parse(obj)).toObj(); + var pkr = PublicKeyRing.fromObj(obj2); + pkr.isComplete().should.equal(true); + pkr.requiredCopayers.should.equal(2); + pkr.totalCopayers.should.equal(2); + }); + + it('#fromUntrustedObj #toObj rountrip', function() { + var obj2 = PublicKeyRing.fromUntrustedObj(JSON.parse(obj)).toObj(); + var pkr = PublicKeyRing.fromUntrustedObj(obj2); + pkr.isComplete().should.equal(true); + pkr.requiredCopayers.should.equal(2); + pkr.totalCopayers.should.equal(2); + }); + + + + it('#getHDParams should return the right one', function() { var config = { networkName: 'livenet', @@ -475,15 +523,15 @@ describe('PublicKeyRing model', function() { }); }); - it('#getForPath should return 5 pubkeys', function() { + it('#_getForPath should return 5 pubkeys', function() { var w = getCachedW().w; - var pubkeys = w.getForPath('m/45\'/2147483647/1/0'); + var pubkeys = w._getForPath('m/45\'/2147483647/1/0'); pubkeys.length.should.equal(5); }); - it('#getForPaths should return 2 arrays of 5 pubkey ', function() { + it('#_getForPaths should return 2 arrays of 5 pubkey ', function() { var w = getCachedW().w; - var pubkeys = w.getForPaths(['m/45\'/2147483647/1/0', 'm/45\'/2147483647/1/1']); + var pubkeys = w._getForPaths(['m/45\'/2147483647/1/0', 'm/45\'/2147483647/1/1']); pubkeys.length.should.equal(2); pubkeys[0].length.should.equal(5); pubkeys[1].length.should.equal(5); @@ -498,4 +546,8 @@ describe('PublicKeyRing model', function() { ret.pubKeys[1].length.should.equal(5); }); + + }); + +var obj = '{"walletId":"0a903a2eb33793d1","networkName":"testnet","requiredCopayers":2,"totalCopayers":2,"indexes":[{"copayerIndex":2147483647,"changeIndex":0,"receiveIndex":1},{"copayerIndex":0,"changeIndex":39,"receiveIndex":0},{"copayerIndex":1,"changeIndex":102,"receiveIndex":39}],"copayersExtPubKeys":["tpubD9peJo88ArhgmJNqRkQmhHt4zAGTYVowsHrDj385xyXyMy4RhWZpV5Qx2mMDUVzpbAD5V9jci5D7cZaHhjLYP8gEkngmTKtSF4Y7V3qkAsy","tpubD8udwzKWwNUgoE2WG7LYsXKf5m1eRtJ1Etp43vnoxViFmrmZ1ND2CkdqGyQtuidcN1CiqdBUvbKegbdsMQaj5VLY2hbA4LEnLDrqkgSzikz"],"nicknameFor":{"03338b105850c7126f1f5b0439b357765b17ead8eed15bcdfdbd28d0e3915b696f":"5@queparece","0286b376d65cc4af0de5932fb8299cbef2ca9ed37ec9fdb0edfd4e9cb74eac45da":"4@queparece"}}'; diff --git a/test/Wallet.js b/test/Wallet.js index bbef0644d..45f843093 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -8,6 +8,7 @@ var Transaction = bitcore.Transaction; var Address = bitcore.Address; var PayPro = bitcore.PayPro; var Buffer = bitcore.Buffer; +var Script = bitcore.Script; function assertObjectEqual(a, b) { @@ -163,17 +164,13 @@ describe('Wallet model', function() { should.exist(w.addressBook); }); - it('should provide some basic features', function(done) { + it('should provide some basic features', function() { var opts = {}; var w = cachedCreateW(); addCopayers(w); w.publicKeyRing.generateAddress(false, w.publicKey); w.publicKeyRing.isComplete().should.equal(true); - w.generateAddress(true).isValid().should.equal(true); - w.generateAddress(true, function(addr) { - addr.isValid().should.equal(true); - done(); - }); + (new bitcore.Address(w.generateAddress(true))).isValid().should.equal(true); }); var unspentTest = [{ @@ -224,14 +221,18 @@ describe('Wallet model', function() { return w; }; + var unSpentTestFromWallet = function(w, addrStr) { + + unspentTest[0].address = addrStr; + var a = new bitcore.Address(addrStr); + unspentTest[0].scriptPubKey = Script.createP2SH(a.payload()).getBuffer().toString('hex'); + }; + it('#create, fail for network', function() { var w = cachedCreateW2(); - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); - + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var f = function() { w._createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', @@ -240,15 +241,14 @@ describe('Wallet model', function() { unspentTest ); }; - f.should.throw(Error); + f.should.throw('networkname'); }); it('#create, check builder opts', function() { var w = cachedCreateW2(); - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', @@ -264,9 +264,7 @@ describe('Wallet model', function() { it('#create, 1 sign', function() { var w = cachedCreateW2(); - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', @@ -288,9 +286,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var comment = 'This is a comment'; - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', @@ -308,9 +304,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var comment = 'Lorem ipsum dolor sit amet, suas euismod vis te, velit deleniti vix an. Pri ex suscipit similique, inermis per'; - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); (function() { w._createTxProposal( @@ -324,7 +318,7 @@ describe('Wallet model', function() { it('#addressIsOwn', function() { var wallet = cachedCreateW2(); - var allAddresses = wallet.getAddressesStr(); + var allAddresses = wallet.getAddresses(); for (var i = 0; i < allAddresses.length; i++) { wallet.addressIsOwn(allAddresses[i]).should.equal(true); } @@ -340,8 +334,7 @@ describe('Wallet model', function() { var ts = Date.now(); for (var isChange = false; !isChange; isChange = true) { for (var index = 0; index < 3; index++) { - unspentTest[0].address = w.publicKeyRing.getAddress(index, isChange, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(index, isChange, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', @@ -531,14 +524,14 @@ describe('Wallet model', function() { - it('#isReady', function() { + it('#isComplete', function() { var w = createW(); w.publicKeyRing.isComplete().should.equal(false); - w.isReady().should.equal(false); + w.isComplete().should.equal(false); var w2 = createW2(); w2.publicKeyRing.isComplete().should.equal(true); - w2.isReady().should.equal(true); + w2.isComplete().should.equal(true); }); it('handle network indexes correctly', function() { @@ -910,7 +903,7 @@ describe('Wallet model', function() { w.issueTx(ntxid, function(err, txid, status) { should.not.exist(err); - txp.getSent().should.be.above(now-1); + txp.getSent().should.be.above(now - 1); txp.sentTxid.should.be.equal(txid); txid.should.equal(1234); status.should.equal(Wallet.TX_BROADCASTED); @@ -1170,16 +1163,14 @@ describe('Wallet model', function() { describe('#subscribeToAddresses', function() { it('should subscribe successfully', function() { var w = cachedCreateW2(); - var addr1 = w.getAddresses()[0].toString(); var addr2 = w.generateAddress().toString(); var addr3 = w.generateAddress(true).toString(); - chai.expect(w.getAddresses().length).to.equal(3); w.blockchain.subscribe = sinon.spy(); w.subscribeToAddresses(); w.blockchain.subscribe.calledOnce.should.equal(true); var arg = w.blockchain.subscribe.getCall(0).args[0]; - chai.expect(_.difference(arg, [addr1, addr2, addr3]).length).to.equal(0); + _.intersection(arg, [addr2, addr3]).length.should.be.equal(2); }); }); @@ -1967,27 +1958,40 @@ describe('Wallet model', function() { - it('should emit notification when tx received', function(done) { + it('should emit notification when tx received', function() { var w = cachedCreateW2(); + + var addr1 = w.generateAddress(false); + sinon.stub(w,'subscribeToAddresses'); + w.blockchain.removeAllListeners = sinon.stub(); - var spy = sinon.spy(w, 'emit'); + w.blockchain.on = sinon.stub(); - w.generateAddress(false, function(addr1) { - w.generateAddress(true, function(addr2) { - w.blockchain.on = sinon.stub().withArgs('tx').yields({ - address: addr1.toString(), - }); - w._setBlockchainListeners(); - spy.calledWith('tx', addr1.toString(), false).should.be.true; - - w.blockchain.on = sinon.stub().withArgs('tx').yields({ - address: addr2.toString(), - }); - w._setBlockchainListeners(); - spy.calledWith('tx', addr2.toString(), true).should.be.true; - done(); - }); + w.blockchain.on.withArgs('tx').yields({ + address: addr1, }); + + var spy = sinon.spy(w, 'emit'); + w._setupBlockchainHandlers(); + spy.calledWith('tx', addr1, false).should.equal(true); + }); + + it('should emit notification when tx received (change addr)', function() { + var w = cachedCreateW2(); + + var addr1 = w.generateAddress(true); + sinon.stub(w,'subscribeToAddresses'); + + w.blockchain.removeAllListeners = sinon.stub(); + w.blockchain.on = sinon.stub(); + + w.blockchain.on.withArgs('tx').yields({ + address: addr1, + }); + + var spy = sinon.spy(w, 'emit'); + w._setupBlockchainHandlers(); + spy.calledWith('tx', addr1, true).should.equal(true); }); describe('#fromObj / #toObj', function() { @@ -2041,13 +2045,28 @@ describe('Wallet model', function() { should.exist(w.txProposals.toObj); should.exist(w.privateKey.toObj); - assertObjectEqual(w.toObj(), JSON.parse(o2)); + var obj = w.toObj(); + + // remove data from new versions + delete obj.publicKeyRing['cache']; + + assertObjectEqual(obj, JSON.parse(o2)); }); }); describe('#getTransactionHistory', function() { + var w; + beforeEach(function() { + w = cachedCreateW2(); + }); + afterEach(function() { + if (w.publicKeyRing.addressIsOwn.restore) + w.publicKeyRing.addressIsOwn.restore(); + if (w.publicKeyRing.addressIsChange.restore) + w.publicKeyRing.addressIsChange.restore(); + }); + it('should return list of txs', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_in_1', @@ -2092,11 +2111,17 @@ describe('Wallet model', function() { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_out_2' - }]); + + sinon.stub(w,'getAddresses').returns([ 'addr_in_1', 'addr_out_2' ]); + var s = sinon.stub(w.publicKeyRing,'addressIsOwn'); + s.withArgs('addr_in_1').returns(true); + s.withArgs('addr_in_2').returns(false); + s.withArgs('addr_out_2').returns(true); + + + var s2 = sinon.stub(w.publicKeyRing,'addressIsChange'); + s2.withArgs('addr_out_1').returns(false); + s2.withArgs('addr_out_2').returns(false); w.getTransactionHistory(function(err, res) { res.should.exist; @@ -2113,7 +2138,6 @@ describe('Wallet model', function() { }); }); it('should return paginated list of txs', function(done) { - var w = cachedCreateW2(); var txs = [{ txid: 'id1', vin: [{ @@ -2161,11 +2185,6 @@ describe('Wallet model', function() { items: txs.slice(2, 3), totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_out_2' - }]); w.getTransactionHistory({ currentPage: 2, @@ -2182,17 +2201,11 @@ describe('Wallet model', function() { }); }); it('should paginate empty list', function(done) { - var w = cachedCreateW2(); var txs = []; w.blockchain.getTransactions = sinon.stub().yields(null, { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_out_2' - }]); w.getTransactionHistory({ currentPage: 2, @@ -2207,7 +2220,6 @@ describe('Wallet model', function() { }); }); it('should compute sent amount correctly', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_in_1', @@ -2234,14 +2246,17 @@ describe('Wallet model', function() { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_in_2' - }, { - addressStr: 'change', - isChange: true, - }]); + + + sinon.stub(w,'getAddresses').returns([ 'addr_in_1', 'addr_in_2', 'change']); + var s = sinon.stub(w.publicKeyRing,'addressIsOwn'); + s.withArgs('addr_in_1').returns(true); + s.withArgs('addr_in_2').returns(true); + s.withArgs('change').returns(true); + + var s2 = sinon.stub(w.publicKeyRing,'addressIsChange'); + s2.withArgs('addr_out_2').returns(false); + s2.withArgs('change').returns(true); w.getTransactionHistory(function(err, res) { res.should.exist; @@ -2253,7 +2268,6 @@ describe('Wallet model', function() { }); }); it('should compute moved amount correctly', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_1', @@ -2280,14 +2294,16 @@ describe('Wallet model', function() { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_1' - }, { - addressStr: 'addr_2' - }, { - addressStr: 'change', - isChange: true, - }]); + + sinon.stub(w,'getAddresses').returns([ 'addr_in_1', 'addr_in_2', 'change']); + var s = sinon.stub(w.publicKeyRing,'addressIsOwn'); + s.withArgs('addr_1').returns(true); + s.withArgs('addr_2').returns(true); + s.withArgs('change').returns(true); + + var s2 = sinon.stub(w.publicKeyRing,'addressIsChange'); + s2.withArgs('addr_1').returns(false); + s2.withArgs('change').returns(true); w.getTransactionHistory(function(err, res) { res.should.exist; @@ -2299,7 +2315,6 @@ describe('Wallet model', function() { }); }); it('should assign label when address in address book', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_in_1', @@ -2349,7 +2364,6 @@ describe('Wallet model', function() { }); }); it('should assign comment from tx proposal if found', function(done) { - var w = cachedCreateW2(); var txs = [{ txid: 'id1', vin: [{ @@ -2483,7 +2497,7 @@ describe('Wallet model', function() { blockchainOpts: {}, }, function(err, w) { should.exist(w); - w.isReady().should.equal(true); + w.isComplete().should.equal(true); var wo = w.toObj(); wo.opts.id.should.equal('48ba2f1ffdfe9708'); wo.opts.spendUnconfirmed.should.equal(true); @@ -2517,7 +2531,7 @@ describe('Wallet model', function() { // DATA - var o = '{"opts":{"id":"dbfe10c3fae71cea", "spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5","networkName":"testnet"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{"walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{},"settings":{"unitName":"BTC","unitToSatoshi":100000000,"unitDecimals":8,"alternativeName":"Argentine Peso","alternativeIsoCode":"ARS"}}'; + var o = '{"opts":{"id":"dbfe10c3fae71cea", "spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5","networkName":"testnet"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{ "cache": { "addressToPath": {}}, "walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{},"settings":{"unitName":"BTC","unitToSatoshi":100000000,"unitDecimals":8,"alternativeName":"Argentine Peso","alternativeIsoCode":"ARS"}}'; }); diff --git a/test/mocks/FakeBlockchain.js b/test/mocks/FakeBlockchain.js index 6af0cc8e3..be2483dcf 100644 --- a/test/mocks/FakeBlockchain.js +++ b/test/mocks/FakeBlockchain.js @@ -17,6 +17,9 @@ FakeBlockchain.prototype.getTransactions = function(addresses, from, to, cb) { FakeBlockchain.prototype.subscribe = function() { }; +FakeBlockchain.prototype.on = function() { +}; + FakeBlockchain.prototype.fixUnspent = function(u) { this.u = u; }; diff --git a/test/performance.js b/test/performance.js deleted file mode 100644 index 8f218e24a..000000000 --- a/test/performance.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -var PrivateKey = copay.PrivateKey; -var PublicKeyRing = copay.PublicKeyRing; - -var getNewEpk = function() { - return new PrivateKey({ - networkName: 'livenet', - }) - .deriveBIP45Branch() - .extendedPublicKeyString(); -} - - -describe('Performance tests', function() { - describe('PrivateKey', function() { - it('should optimize BIP32 private key gen time with cache', function() { - var k1 = new PrivateKey(); - var generateN = 25; - var generated = []; - var start1 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var k = JSON.stringify(k1.get(i, false).storeObj()); - generated.push(k); - } - var delta1 = new Date().getTime() - start1; - var start2 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var k = JSON.stringify(k1.get(i, false).storeObj()); - generated[i].should.equal(k); - } - var delta2 = new Date().getTime() - start2; - delta2.should.be.below(delta1); - }); - }); - describe('PublicKeyRing', function() { - var maxN = 7; - for (var n = 1; n < maxN; n++) { - for (var m = 1; m <= n; m++) { - if ((m === 3 && n === 5) || - (m === 2 && n === 3)) { - var M = m; - var N = n; - (function(M, N) { - it('should optimize BIP32 publickey gen time with cache for ' + M + '-of-' + N, function() { - var pkr1 = new PublicKeyRing({ - totalCopayers: N, - requiredCopayers: M - }); - for (var i = 0; i < N; i++) { - pkr1.addCopayer(getNewEpk()); // add new random ext public key - } - var generateN = 5; - var generated = []; - var start1 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var pubKeys = JSON.stringify(pkr1.getPubKeys(i, false)); - generated.push(pubKeys); - } - var delta1 = new Date().getTime() - start1; - var start2 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var pubKeys = JSON.stringify(pkr1.getPubKeys(i, false)); - generated[i].should.equal(pubKeys); - } - var delta2 = new Date().getTime() - start2; - delta2.should.be.below(delta1); - }); - })(M, N); - } - } - } - - }); -}); diff --git a/test/unit/controllers/controllersSpec.js b/test/unit/controllers/controllersSpec.js index 1343b5ef1..6d0b12aa0 100644 --- a/test/unit/controllers/controllersSpec.js +++ b/test/unit/controllers/controllersSpec.js @@ -15,6 +15,8 @@ saveAs = function(blob, filename) { describe("Unit: Controllers", function() { config.plugins.LocalStorage = true; config.plugins.GoogleDrive = null; + config.plugins.InsightStorage = null; + config.plugins.EncryptedInsightStorage= null; var anAddr = 'mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'; var anAmount = 1000; @@ -50,7 +52,7 @@ describe("Unit: Controllers", function() { var w = {}; w.id = 1234; - w.isReady = sinon.stub().returns(true); + w.isComplete = sinon.stub().returns(true); w.privateKey = {}; w.settings = { unitToSatoshi: 100, @@ -254,13 +256,13 @@ describe("Unit: Controllers", function() { sendForm.amount.$setViewValue(anAmount); sendForm.comment.$setViewValue(aComment); - scope.loadTxs = sinon.spy(); + scope.updateTxs = sinon.spy(); var w = scope.wallet; scope.submitForm(sendForm); sinon.assert.callCount(w.spend, 1); sinon.assert.callCount(w.broadcastTx, 0); - sinon.assert.callCount(scope.loadTxs, 1); + sinon.assert.callCount(scope.updateTxs, 1); var spendArgs = w.spend.getCall(0).args[0]; spendArgs.toAddress.should.equal(anAddr); spendArgs.amountSat.should.equal(anAmount * scope.wallet.settings.unitToSatoshi); @@ -270,12 +272,12 @@ describe("Unit: Controllers", function() { it('should handle big values in 100 BTC', function() { var old = scope.wallet.settings.unitToSatoshi; - scope.wallet.settings.unitToSatoshi = 100000000;; + scope.wallet.settings.unitToSatoshi = 100000000; sendForm.address.$setViewValue(anAddr); sendForm.amount.$setViewValue(100); sendForm.address.$setViewValue(anAddr); - scope.loadTxs = sinon.spy(); + scope.updateTxs = sinon.spy(); scope.submitForm(sendForm); var w = scope.wallet; w.spend.getCall(0).args[0].amountSat.should.equal(100 * scope.wallet.settings.unitToSatoshi); @@ -289,7 +291,7 @@ describe("Unit: Controllers", function() { var old = $rootScope.wallet.settings.unitToSatoshi; - $rootScope.wallet.settings.unitToSatoshi = 100000000;; + $rootScope.wallet.settings.unitToSatoshi = 100000000; sendForm.address.$setViewValue(anAddr); sendForm.amount.$setViewValue(5000); scope.submitForm(sendForm); @@ -602,9 +604,10 @@ describe("Unit: Controllers", function() { it('Delete a wallet', function() { var w = scope.wallet; - scope.deleteWallet(w); - scope.$digest(); - expect(scope.wallet).equal(null); + scope.deleteWallet(w, function() { + scope.$digest(); + expect(scope.wallet).equal(null); + }); }); }); diff --git a/test/unit/directives/directivesSpec.js b/test/unit/directives/directivesSpec.js index e1ef6ffcf..65c9091de 100644 --- a/test/unit/directives/directivesSpec.js +++ b/test/unit/directives/directivesSpec.js @@ -10,7 +10,7 @@ describe("Unit: Testing Directives", function() { beforeEach(inject(function($rootScope) { var w = {}; - w.isReady = sinon.stub().returns(true); + w.isComplete = sinon.stub().returns(true); w.privateKey = {}; w.settings = { unitToSatoshi: 100, diff --git a/test/unit/filters/filtersSpec.js b/test/unit/filters/filtersSpec.js index 91ff9efe2..43bab46f5 100644 --- a/test/unit/filters/filtersSpec.js +++ b/test/unit/filters/filtersSpec.js @@ -9,7 +9,7 @@ describe('Angular Filters', function() { beforeEach(inject(function($rootScope) { var w = {}; - w.isReady = sinon.stub().returns(true); + w.isComplete = sinon.stub().returns(true); w.privateKey = {}; w.settings = { unitToSatoshi: 100, diff --git a/test/unit/services/servicesSpec.js b/test/unit/services/servicesSpec.js index fc044e4b8..3abe1efaf 100644 --- a/test/unit/services/servicesSpec.js +++ b/test/unit/services/servicesSpec.js @@ -26,7 +26,7 @@ describe("Angular services", function() { beforeEach(inject(function($rootScope) { var w = {}; - w.isReady = sinon.stub().returns(true); + w.isComplete = sinon.stub().returns(true); w.privateKey = {}; w.settings = { unitToSatoshi: 100, @@ -65,49 +65,34 @@ describe("Angular services", function() { - describe("Unit: controllerUtils", function() { + describe("Unit: balanceService", function() { - it('should updateBalance in bits', inject(function(controllerUtils, $rootScope) { + it('should updateBalance in bits', inject(function(balanceService, $rootScope) { var w = $rootScope.wallet; - - expect(controllerUtils.updateBalance).not.to.equal(null); + expect(balanceService.update).not.to.equal(null); var Waddr = Object.keys($rootScope.wallet.balanceByAddr)[0]; var a = {}; a[Waddr] = 200; w.getBalance = sinon.stub().yields(null, 100000001, a, 90000002, 5); - var orig =controllerUtils.isFocusedWallet; - controllerUtils.isFocusedWallet = sinon.stub().returns(true); - //retuns values in DEFAULT UNIT(bits) - controllerUtils.updateBalance(null, function() { + balanceService.update(w, function() { + var b = w.balanceInfo; + expect(b.totalBalanceBTC).to.be.equal(1.00000001); + expect(b.availableBalanceBTC).to.be.equal(0.90000002); + expect(b.lockedBalanceBTC).to.be.equal(0.09999999); + expect(b.totalBalance).to.be.equal('1,000,000.01'); + expect(b.availableBalance).to.be.equal('900,000.02'); + expect(b.lockedBalance).to.be.equal(99999.99); - expect($rootScope.totalBalanceBTC).to.be.equal(1.00000001); - expect($rootScope.availableBalanceBTC).to.be.equal(0.90000002); - expect($rootScope.lockedBalanceBTC).to.be.equal(0.09999999); - - expect($rootScope.totalBalance).to.be.equal(1000000.01); - expect($rootScope.availableBalance).to.be.equal(900000.02); - expect($rootScope.lockedBalance).to.be.equal(99999.99); - - expect($rootScope.balanceByAddr[Waddr]).to.equal(2); - expect($rootScope.safeUnspentCount).to.equal(5); - expect($rootScope.topAmount).to.equal(899800.02); - }); - - controllerUtils.isFocusedWallet = orig; + expect(b.balanceByAddr[Waddr]).to.equal(2); + expect(b.safeUnspentCount).to.equal(5); + expect(b.topAmount).to.equal(899800.02); + },false); })); - it('should set the rootScope', inject(function(controllerUtils, $rootScope) { - controllerUtils.setupGlobalVariables(function() { - expect($rootScope.txAlertCount).to.be.equal(0); - expect($rootScope.insightError).to.be.equal(0); - expect($rootScope.isCollapsed).to.be.equal(0); - expect($rootScope.unitName).to.be.equal('bits'); - }); - })); }); describe("Unit: Notification Service", function() { diff --git a/views/copayers.html b/views/copayers.html index 526282a0a..12c4a6a04 100644 --- a/views/copayers.html +++ b/views/copayers.html @@ -1,21 +1,19 @@ - + - Waiting copayers for {{$root.wallet.getName()}} - {{$root.wallet.requiredCopayers}}-of-{{$root.wallet.totalCopayers}} + Waiting copayers... - + Share this secret with your other copayers - {{$root.wallet.requiredCopayers}}-of-{{$root.wallet.totalCopayers}} @@ -27,7 +25,7 @@ - + diff --git a/views/history.html b/views/history.html index 6115b9b10..178dce042 100644 --- a/views/history.html +++ b/views/history.html @@ -1,5 +1,4 @@ - {{$root.title}} @@ -46,8 +45,8 @@ 'text-primary' : btx.action == 'received', 'text-warning': btx.action == 'sent', 'text-gray': btx.action == 'moved'}"> - {{btx.amount| noFractionNumber}} {{$root.wallet.settings.unitName}} - {{btx.alternativeAmount| noFractionNumber}} {{$root.wallet.settings.alternativeIsoCode}} + {{btx.amount}} {{$root.wallet.settings.unitName}} + {{btx.alternativeAmount}} {{$root.wallet.settings.alternativeIsoCode}} @@ -140,5 +139,4 @@ - diff --git a/views/homeWallet.html b/views/homeWallet.html index 1680c8b93..5563d1fd5 100644 --- a/views/homeWallet.html +++ b/views/homeWallet.html @@ -1,5 +1,4 @@ - Home @@ -18,15 +17,15 @@ - {{totalBalance || 0 |noFractionNumber}} + {{$root.wallet.balanceInfo.totalBalance || 0}} {{$root.wallet.settings.unitName}} - {{totalBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}} - N/A - + {{$root.wallet.balanceInfo.totalBalanceAlternative}} {{$root.wallet.balanceInfo.alternativeIsoCode}} + N/A + @@ -41,10 +40,10 @@ Quick receive - + - {{$root.addrInfos[0].addressStr}} + {{addr}} @@ -60,7 +59,5 @@ - - diff --git a/views/includes/sidebar-mobile.html b/views/includes/sidebar-mobile.html index 4cf4e45a2..2a89ab52c 100644 --- a/views/includes/sidebar-mobile.html +++ b/views/includes/sidebar-mobile.html @@ -12,13 +12,13 @@ - Waiting for copayers... - - - - {{totalBalance || 0 |noFractionNumber}} {{$root.wallet.settings.unitName}} - {{totalBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}} - N/A + Waiting for copayers... + + + + {{$root.wallet.balanceInfo.totalBalance || 0}} {{$root.wallet.settings.unitName}} + {{$root.wallet.balanceInfo.totalBalanceAlternative}} {{$root.wallet.balanceInfo.alternativeIsoCode}} + N/A @@ -56,12 +56,12 @@ {{item.name || item.id}} - - - {{item.balanceInfo.totalBalance || 0 |noFractionNumber}} {{item.settings.unitName}} - {{item.balanceInfo.totalBalanceAlternative |noFractionNumber:2}} {{item.balanceInfo.alternativeIsoCode}} + + + {{item.balanceInfo.totalBalance || 0}} {{item.settings.unitName}} + {{item.balanceInfo.totalBalanceAlternative}} {{item.balanceInfo.alternativeIsoCode}} - Waiting for copayers... + Waiting for copayers... diff --git a/views/includes/sidebar.html b/views/includes/sidebar.html index da182e7b8..d1404b0b7 100644 --- a/views/includes/sidebar.html +++ b/views/includes/sidebar.html @@ -1,9 +1,9 @@ - + {{$root.wallet.getName() | limitTo: 1}} - + [ {{$root.wallet.requiredCopayers}} of {{$root.wallet.totalCopayers}} ] @@ -11,18 +11,18 @@ - Waiting for copayers... - - - - {{totalBalance || 0 |noFractionNumber}} {{$root.wallet.settings.unitName}} - {{totalBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}} - N/A + Waiting for copayers... + + + + {{$root.wallet.balanceInfo.totalBalance || 0}} {{$root.wallet.settings.unitName}} + {{$root.wallet.balanceInfo.totalBalanceAlternative}} {{$root.wallet.balanceInfo.alternativeIsoCode}} + N/A - + @@ -34,12 +34,12 @@ - + {{'Locked'|translate}} - - {{lockedBalance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}} - {{lockedBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}} + + {{$root.wallet.balanceInfo.lockedBalance || 0}} {{$root.wallet.settings.unitName}} - {{$root.wallet.balanceInfo.lockedBalanceAlternative}} {{$root.wallet.balanceInfo.alternativeIsoCode}} {{item.name || item.id}} - - - {{item.balanceInfo.totalBalance || 0 |noFractionNumber}} {{item.settings.unitName}} - {{item.balanceInfo.totalBalanceAlternative |noFractionNumber:2}} {{item.balanceInfo.alternativeIsoCode}} + + + {{item.balanceInfo.totalBalance || 0}} {{item.settings.unitName}} + {{item.balanceInfo.totalBalanceAlternative}} {{item.balanceInfo.alternativeIsoCode}} - Waiting for copayers... + Waiting for copayers... @@ -75,7 +75,7 @@ - + {{item.title|translate}} diff --git a/views/includes/transaction.html b/views/includes/transaction.html index aca03ead0..8eca6d50c 100644 --- a/views/includes/transaction.html +++ b/views/includes/transaction.html @@ -9,9 +9,9 @@ - {{out.value |noFractionNumber}} {{$root.wallet.settings.unitName}} + {{out.value}} {{$root.wallet.settings.unitName}} - {{out.alternativeAmount|noFractionNumber}} {{out.alternativeIsoCode}} + {{out.alternativeAmount}} {{out.alternativeIsoCode}} @@ -106,7 +106,7 @@ {{tx.missingSignatures}} signatures missing - Fee: {{tx.fee|noFractionNumber}} {{$root.wallet.settings.unitName}} + Fee: {{tx.fee}} {{$root.wallet.settings.unitName}} Proposal ID: {{tx.ntxid}} diff --git a/views/modals/qr-address.html b/views/modals/qr-address.html index fb82d6b03..d52da6d03 100644 --- a/views/modals/qr-address.html +++ b/views/modals/qr-address.html @@ -13,7 +13,7 @@ - {{address.balance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}} + {{address.balance || 0}} {{$root.wallet.settings.unitName}} diff --git a/views/paymentIntent.html b/views/paymentIntent.html index 2246aac4b..4a1fb750a 100644 --- a/views/paymentIntent.html +++ b/views/paymentIntent.html @@ -1,6 +1,7 @@
diff --git a/views/history.html b/views/history.html index 6115b9b10..178dce042 100644 --- a/views/history.html +++ b/views/history.html @@ -1,5 +1,4 @@