diff --git a/src/js/controllers/addresses.js b/src/js/controllers/addresses.js new file mode 100644 index 000000000..9b31e431f --- /dev/null +++ b/src/js/controllers/addresses.js @@ -0,0 +1,199 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('addressesController', function($scope, $stateParams, $state, $timeout, $ionicHistory, $ionicPopover, $ionicScrollDelegate, configService, popupService, gettextCatalog, ongoingProcess, lodash, profileService, walletService, platformInfo) { + var UNUSED_ADDRESS_LIMIT = 5; + var BALANCE_ADDRESS_LIMIT = 5; + var MENU_ITEM_HEIGHT = 55; + var config; + var unitName; + var unitToSatoshi; + var satToUnit; + var unitDecimals; + var withBalance; + $scope.showInfo = false; + $scope.showMore = false; + $scope.allAddressesView = false; + $scope.isCordova = platformInfo.isCordova; + $scope.wallet = profileService.getWallet($stateParams.walletId); + + function init() { + ongoingProcess.set('gettingAddresses', true); + walletService.getMainAddresses($scope.wallet, {}, function(err, addresses) { + if (err) { + ongoingProcess.set('gettingAddresses', false); + return popupService.showAlert(gettextCatalog.getString('Error'), err); + } + + var allAddresses = addresses; + + walletService.getBalance($scope.wallet, {}, function(err, resp) { + ongoingProcess.set('gettingAddresses', false); + if (err) { + return popupService.showAlert(gettextCatalog.getString('Error'), err); + } + + withBalance = resp.byAddress; + var idx = lodash.indexBy(withBalance, 'address'); + $scope.noBalance = lodash.reject(allAddresses, function(x) { + return idx[x.address]; + }); + + processPaths($scope.noBalance); + processPaths(withBalance); + + $scope.latestUnused = lodash.slice($scope.noBalance, 0, UNUSED_ADDRESS_LIMIT); + $scope.latestWithBalance = lodash.slice(withBalance, 0, BALANCE_ADDRESS_LIMIT); + + lodash.each(withBalance, function(a) { + a.balanceStr = (a.amount * satToUnit).toFixed(unitDecimals) + ' ' + unitName; + }); + + $scope.viewAll = { + value: $scope.noBalance.length > UNUSED_ADDRESS_LIMIT || withBalance.length > BALANCE_ADDRESS_LIMIT + }; + $scope.allAddresses = $scope.noBalance.concat(withBalance); + $scope.$digest(); + }); + }); + }; + + function processPaths(list) { + lodash.each(list, function(n) { + n.path = n.path.replace(/^m/g, 'xpub'); + }); + }; + + $scope.newAddress = function() { + if ($scope.gapReached) return; + + ongoingProcess.set('generatingNewAddress', true); + walletService.getAddress($scope.wallet, true, function(err, addr) { + if (err) { + ongoingProcess.set('generatingNewAddress', false); + $scope.gapReached = true; + $timeout(function() { + $scope.$digest(); + }); + return; + } + + walletService.getMainAddresses($scope.wallet, { + limit: 1 + }, function(err, _addr) { + ongoingProcess.set('generatingNewAddress', false); + if (err) return popupService.showAlert(gettextCatalog.getString('Error'), err); + if (addr != _addr[0].address) return popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('New address could not be generated. Please try again.')); + + $scope.noBalance = [_addr[0]].concat($scope.noBalance); + $scope.latestUnused = lodash.slice($scope.noBalance, 0, UNUSED_ADDRESS_LIMIT); + $scope.viewAll = { + value: $scope.noBalance.length > UNUSED_ADDRESS_LIMIT + }; + $scope.$digest(); + }); + }); + }; + + $scope.viewAllAddresses = function() { + $state.go('tabs.receive.allAddresses', { + walletId: $scope.wallet.id + }); + }; + + $scope.showInformation = function() { + $timeout(function() { + $scope.showInfo = !$scope.showInfo; + $ionicScrollDelegate.resize(); + }); + }; + + $scope.readMore = function() { + $timeout(function() { + $scope.showMore = !$scope.showMore; + $ionicScrollDelegate.resize(); + }); + }; + + $scope.showMenu = function(allAddresses, $event) { + var scanObj = { + text: gettextCatalog.getString('Scan addresses for funds'), + action: scan, + }; + + var sendAddressesObj = { + text: gettextCatalog.getString('Send addresses by email'), + action: sendByEmail, + } + + $scope.items = allAddresses ? [sendAddressesObj] : [scanObj]; + $scope.height = $scope.items.length * MENU_ITEM_HEIGHT; + + $ionicPopover.fromTemplateUrl('views/includes/menu-popover.html', { + scope: $scope + }).then(function(popover) { + $scope.menu = popover; + $scope.menu.show($event); + }); + }; + + var scan = function() { + walletService.startScan($scope.wallet); + $scope.menu.hide(); + $ionicHistory.clearHistory(); + $state.go('tabs.home'); + }; + + var sendByEmail = function() { + function formatDate(ts) { + var dateObj = new Date(ts * 1000); + if (!dateObj) { + $log.debug('Error formating a date'); + return 'DateError'; + } + if (!dateObj.toJSON()) { + return ''; + } + return dateObj.toJSON(); + }; + + ongoingProcess.set('sendingByEmail', true); + $timeout(function() { + var body = 'Copay Wallet "' + $scope.walletName + '" Addresses\n Only Main Addresses are shown.\n\n'; + body += "\n"; + body += $scope.allAddresses.map(function(v) { + return ('* ' + v.address + ' ' + 'xpub' + v.path.substring(1) + ' ' + formatDate(v.createdOn)); + }).join("\n"); + ongoingProcess.set('sendingByEmail', false); + + window.plugins.socialsharing.shareViaEmail( + body, + 'Copay Addresses', + null, // TO: must be null or an array + null, // CC: must be null or an array + null, // BCC: must be null or an array + null, // FILES: can be null, a string, or an array + function() {}, + function() {} + ); + + $scope.menu.hide(); + }); + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + $scope.allAddressesView = data.stateName == 'tabs.receive.allAddresses' ? true : false; + $timeout(function() { + $scope.$apply(); + }); + }); + + $scope.$on("$ionicView.afterEnter", function(event, data) { + config = configService.getSync().wallet.settings; + unitToSatoshi = config.unitToSatoshi; + satToUnit = 1 / unitToSatoshi; + unitName = config.unitName; + unitDecimals = config.unitDecimals; + + if (!$scope.allAddresses || $scope.allAddresses.length < 0) init(); + }); +}); diff --git a/src/js/controllers/preferencesInformation.js b/src/js/controllers/preferencesInformation.js index 2f2da8b00..983b9cef8 100644 --- a/src/js/controllers/preferencesInformation.js +++ b/src/js/controllers/preferencesInformation.js @@ -1,62 +1,15 @@ 'use strict'; angular.module('copayApp.controllers').controller('preferencesInformation', - function($scope, $log, $timeout, $ionicHistory, $ionicScrollDelegate, platformInfo, gettextCatalog, lodash, profileService, configService, $stateParams, walletService, $state) { - var base = 'xpub'; + function($scope, $log, $ionicHistory, platformInfo, lodash, profileService, configService, $stateParams, $state) { var wallet = profileService.getWallet($stateParams.walletId); var walletId = wallet.id; - var config = configService.getSync(); - var b = 1; + var colorCounter = 1; + var BLACK_WALLET_COLOR = '#202020'; $scope.isCordova = platformInfo.isCordova; config.colorFor = config.colorFor || {}; - $scope.sendAddrs = function() { - function formatDate(ts) { - var dateObj = new Date(ts * 1000); - if (!dateObj) { - $log.debug('Error formating a date'); - return 'DateError'; - } - if (!dateObj.toJSON()) { - return ''; - } - return dateObj.toJSON(); - }; - - $timeout(function() { - wallet.getMainAddresses({ - doNotVerify: true - }, function(err, addrs) { - if (err) { - $log.warn(err); - return; - }; - - var body = 'Copay Wallet "' + $scope.walletName + '" Addresses\n Only Main Addresses are shown.\n\n'; - body += "\n"; - body += addrs.map(function(v) { - return ('* ' + v.address + ' ' + base + v.path.substring(1) + ' ' + formatDate(v.createdOn)); - }).join("\n"); - - window.plugins.socialsharing.shareViaEmail( - body, - 'Copay Addresses', - null, // TO: must be null or an array - null, // CC: must be null or an array - null, // BCC: must be null or an array - null, // FILES: can be null, a string, or an array - function() {}, - function() {} - ); - - $timeout(function() { - $scope.$apply(); - }, 1000); - }); - }, 100); - }; - $scope.saveBlack = function() { function save(color) { var opts = { @@ -71,14 +24,8 @@ angular.module('copayApp.controllers').controller('preferencesInformation', }); }; - if (b != 5) return b++; - save('#202020'); - }; - - $scope.scan = function() { - walletService.startScan(wallet); - $ionicHistory.removeBackView(); - $state.go('tabs.home'); + if (colorCounter != 5) return colorCounter++; + save(BLACK_WALLET_COLOR); }; $scope.$on("$ionicView.enter", function(event, data) { @@ -95,29 +42,5 @@ angular.module('copayApp.controllers').controller('preferencesInformation', $scope.M = c.m; $scope.N = c.n; $scope.pubKeys = lodash.pluck(c.publicKeyRing, 'xPubKey'); - $scope.addrs = null; - - wallet.getMainAddresses({ - doNotVerify: true - }, function(err, addrs) { - if (err) { - $log.warn(err); - return; - }; - var last10 = [], - i = 0, - e = addrs.pop(); - while (i++ < 10 && e) { - e.path = base + e.path.substring(1); - last10.push(e); - e = addrs.pop(); - } - $scope.addrs = last10; - $timeout(function() { - $ionicScrollDelegate.resize(); - $scope.$apply(); - }, 10); - }); }); - }); diff --git a/src/js/controllers/tab-receive.js b/src/js/controllers/tab-receive.js index ea8b9778a..cae89c917 100644 --- a/src/js/controllers/tab-receive.js +++ b/src/js/controllers/tab-receive.js @@ -48,6 +48,12 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi }, 100); }; + $scope.showAddresses = function() { + $state.transitionTo('tabs.receive.addresses', { + walletId: $scope.wallet.credentials.walletId + }); + }; + $scope.openBackupNeededModal = function() { $ionicModal.fromTemplateUrl('views/includes/backupNeededPopup.html', { scope: $scope, diff --git a/src/js/routes.js b/src/js/routes.js index cd935dda2..e00498966 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -616,6 +616,31 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } }) + /* + * + * Addresses + * + */ + + .state('tabs.receive.addresses', { + url: '/addresses/:walletId', + views: { + 'tab-receive@tabs': { + controller: 'addressesController', + templateUrl: 'views/addresses.html' + } + } + }) + .state('tabs.receive.allAddresses', { + url: '/allAddresses/:walletId', + views: { + 'tab-receive@tabs': { + controller: 'addressesController', + templateUrl: 'views/allAddresses.html' + } + } + }) + /* * * Init backup flow diff --git a/src/js/services/onGoingProcess.js b/src/js/services/onGoingProcess.js index acede937d..0d6977173 100644 --- a/src/js/services/onGoingProcess.js +++ b/src/js/services/onGoingProcess.js @@ -34,6 +34,9 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti 'validatingWords': gettext('Validating recovery phrase...'), 'loadingTxInfo': gettext('Loading transaction info...'), 'sendingFeedback': gettext('Sending feedback...'), + 'generatingNewAddress': gettext('Generating new address...'), + 'gettingAddresses': gettext('Getting addresses...'), + 'sendingByEmail': gettext('Preparing addresses...'), }; root.clear = function() { diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js index 3e5da9097..b00722c76 100644 --- a/src/js/services/walletService.js +++ b/src/js/services/walletService.js @@ -773,6 +773,21 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; + root.getMainAddresses = function(wallet, opts, cb) { + opts = opts || {}; + opts.reverse = true; + wallet.getMainAddresses(opts, function(err, addresses) { + return cb(err, addresses); + }); + }; + + root.getBalance = function(wallet, opts, cb) { + opts = opts || {}; + wallet.getBalance(opts, function(err, resp) { + return cb(err, resp); + }); + }; + root.getAddress = function(wallet, forceNew, cb) { storageService.getLastAddress(wallet.id, function(err, addr) { if (err) return cb(err); diff --git a/src/sass/views/addresses.scss b/src/sass/views/addresses.scss new file mode 100644 index 000000000..5401945f7 --- /dev/null +++ b/src/sass/views/addresses.scss @@ -0,0 +1,89 @@ +#addresses { + .addr { + &-explanation, &-button-group { + padding: 0 1rem; + margin: 1rem 0; + } + &-description { + text-align: center; + font-size: 15px; + color: $mid-gray; + margin: 1rem 0; + a { + font-weight: bold; + cursor: pointer; + cursor: hand; + } + } + &-balance { + margin-top: 4px; + color: #5DD263; + } + &-path { + margin-top: 4px; + color: #B8B8B8; + } + } + + .banner-icon { + margin-top: 25px; + i { + box-shadow: $hovering-box-shadow; + } + } + + .addr-list { + .item { + color: $dark-gray; + padding-top: 1.3rem; + padding-bottom: 1.3rem; + &.has-addr-value { + padding-top: .65rem; + padding-bottom: .65rem; + } + &.item-divider { + color: $mid-gray; + padding-bottom: .5rem; + font-size: .9rem; + } + &.view-all { + margin: 20px 0px 20px 0px; + cursor: pointer; + cursor: hand; + i { + font-size: 35px; + margin-right: 5px; + color: #647ce8; + } + span { + color: #647ce8; + font-weight: bold; + } + } + i { + font-size: 35px; + margin-right: 2px; + } + } + .box-error { + padding: 25px; + background-color: #E65555; + color: #F4F4F4; + h5 { + margin: 5px; + color: #F4F4F4; + text-align: center; + font-weight: bold; + } + a { + font-weight: bold; + color: #F4F4F4; + cursor: pointer; + cursor: hand; + } + } + .item-note { + color: $light-gray; + } + } +} diff --git a/src/sass/views/includes/menu-popover.scss b/src/sass/views/includes/menu-popover.scss new file mode 100644 index 000000000..93139b42d --- /dev/null +++ b/src/sass/views/includes/menu-popover.scss @@ -0,0 +1,12 @@ +#menu-popover { + border-radius: 5px; + .list { + .item { + cursor: pointer; + cursor: hand; + &:hover { + background-color: #E4E2E2; + } + } + } +} diff --git a/src/sass/views/tab-receive.scss b/src/sass/views/tab-receive.scss index ebb9e3ef2..4d36162a4 100644 --- a/src/sass/views/tab-receive.scss +++ b/src/sass/views/tab-receive.scss @@ -89,6 +89,8 @@ .bit-address { font-size: .8rem; // left:10%; + cursor: pointer; + cursor: hand; position: absolute; transition: all .15s ease; width:100%; @@ -117,7 +119,6 @@ .item { padding-top: 5px; padding-bottom: 5px; - display: inline-block; font-size: .7rem; @media(min-width:350px) { font-size:.9rem; diff --git a/src/sass/views/views.scss b/src/sass/views/views.scss index f67826f61..08bcddf5e 100644 --- a/src/sass/views/views.scss +++ b/src/sass/views/views.scss @@ -15,6 +15,7 @@ @import "bitpayCardIntro"; @import "bitpayCardPreferences"; @import "address-book"; +@import "addresses"; @import "wallet-backup-phrase"; @import "zero-state"; @import "onboarding/onboarding"; @@ -26,6 +27,7 @@ @import "export"; @import "import"; @import "join"; +@import "includes/menu-popover"; @import "includes/walletActivity"; @import "includes/wallets"; @import "includes/modals/modals"; diff --git a/www/views/addresses.html b/www/views/addresses.html new file mode 100644 index 000000000..a4a3b6962 --- /dev/null +++ b/www/views/addresses.html @@ -0,0 +1,72 @@ + + + {{'Wallet Addresses' | translate}} + + + + + + + + + + +
+
+
+ Each bitcoin wallet can generate billions of addresses from your 12-word backup. A new address is automatically generated and shown each time your recive a payment. Why? +
+
+ +
+
+ It's a good idea to avoid reusing addresses-this both protects your privacy and keeps your bitcoins secure against hypothetical attacks by quantum computers. Hide +
+
+ +
+
+ Unused Addresses + +
+ +
+
+
Unused Addresses Limit
+

The maximum number of consecutive unused addresses (20) has been reached. When one of your unused addresses receives a payment, a new address will be generated and shown in your Receive tab. Read more

+

The restore process will stop when 20 addresses are generated in a row which contain no funds. To safely generate more addresses, make a payment to one of the unused addresses which has already been generated. Read less

+
+ +
+ {{u.address}} +
+ {{u.path}} {{u.createdOn * 1000 | amDateFormat:'MMMM Do YYYY, hh:mm a'}} +
+
+
+ +
+
+ Addresses With Balance +
+ +
+ {{w.address}} +
{{w.balanceStr}}
+
+
+ +
+ View All Addresses + +
+
+
+
+
diff --git a/www/views/allAddresses.html b/www/views/allAddresses.html new file mode 100644 index 000000000..8ece9164d --- /dev/null +++ b/www/views/allAddresses.html @@ -0,0 +1,28 @@ + + + {{'All Addresses' | translate}} + + + + + + + + +
+
+ +
+ {{a.address}} +
+ {{a.path}} {{a.createdOn * 1000 | amDateFormat:'MMMM Do YYYY, hh:mm a'}} +
+
{{a.balanceStr}}
+
+ +
+
+
+
diff --git a/www/views/includes/menu-popover.html b/www/views/includes/menu-popover.html new file mode 100644 index 000000000..f5acfe6ab --- /dev/null +++ b/www/views/includes/menu-popover.html @@ -0,0 +1,9 @@ + + +
+
+ {{i.text}} +
+
+
+
diff --git a/www/views/preferencesInformation.html b/www/views/preferencesInformation.html index 5871585c5..5795790a2 100644 --- a/www/views/preferencesInformation.html +++ b/www/views/preferencesInformation.html @@ -80,38 +80,11 @@
Copayer {{$index}} - Copayer {{$index}} {{pk}} ({{basePath}})
-
-
- Latest Wallet Addresses -
-
-
- Only “main” addresses are shown below. This excludes “change” address. -
-
-
- {{a.address}} - {{a.path}} · {{a.createdOn *1000 | amDateFormat:'MMMM Do YYYY, h:mm a' }} -
-
-
- Please note: due to resource constraints, this list of addresses is not verified locally. A compromised BWS node could return addresses which are not controlled by this wallet. -
-
- - -
-
Balance By Address diff --git a/www/views/tab-receive.html b/www/views/tab-receive.html index 31b386f28..d07713685 100644 --- a/www/views/tab-receive.html +++ b/www/views/tab-receive.html @@ -64,19 +64,14 @@ Share
-
-
- - Next Address -
-
-
+
... - {{walletAddrs[wallet.id]}} + {{walletAddrs[wallet.id]}} +