diff --git a/old/index.js b/old/index.js index 2cca1d93c..71cdeed98 100644 --- a/old/index.js +++ b/old/index.js @@ -535,14 +535,6 @@ angular.module('copayApp.controllers').controller('indexController', function($r } }; - self.removeAndMarkSoftConfirmedTx = function(txs) { - return lodash.filter(txs, function(tx) { - if (tx.confirmations >= SOFT_CONFIRMATION_LIMIT) - return tx; - tx.recent = true; - }); - } - self.showMore = function() { $timeout(function() { @@ -1188,9 +1180,9 @@ console.log('[index.js:1063] walletImported'); //TODO }); $rootScope.$on('Local/NewFocusedWallet', function() { -console.log('[index.js.1200:NewFocusedWallet:] TODO'); //TODO + console.log('[index.js.1200:NewFocusedWallet:] TODO'); //TODO -return; + return; uxLanguage.update(); diff --git a/package.json b/package.json index df700bb1d..1000dd9c2 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "//":"PLEASE! Do not edit this file directly", "//":" Modify it at app-template/", - "name": "copay", - "description": "A Secure Bitcoin Wallet", + "name": "bitpay", + "description": "The BitPay Bitcoin Wallet", "author": "BitPay", - "version": "2.5.0", + "version": "0.1.1", "keywords": [ "wallet", "copay", diff --git a/public/views/modals/tx-details.html b/public/views/modals/tx-details.html index 076c02080..a4582dfba 100644 --- a/public/views/modals/tx-details.html +++ b/public/views/modals/tx-details.html @@ -149,11 +149,11 @@
- - diff --git a/public/views/tab-home.html b/public/views/tab-home.html index 28cd452b1..3191aaf27 100644 --- a/public/views/tab-home.html +++ b/public/views/tab-home.html @@ -3,9 +3,9 @@ Home - + -

Payment Proposals

+

Payment Proposals

diff --git a/src/js/controllers/index.js b/src/js/controllers/index.js new file mode 100644 index 000000000..825a7fac1 --- /dev/null +++ b/src/js/controllers/index.js @@ -0,0 +1,1334 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, $ionicScrollDelegate, $ionicPopup, $ionicSideMenuDelegate, $httpBackend, latestReleaseService, feeService, bwcService, pushNotificationsService, lodash, go, profileService, configService, rateService, storageService, addressService, gettext, gettextCatalog, amMoment, addonManager, bwcError, txFormatService, uxLanguage, glideraService, coinbaseService, platformInfo, addressbookService, openURLService, ongoingProcess) { + + var self = this; + var SOFT_CONFIRMATION_LIMIT = 12; + var errors = bwcService.getErrors(); + var historyUpdateInProgress = {}; + var isChromeApp = platformInfo.isChromeApp; + var isCordova = platformInfo.isCordova; + var isNW = platformInfo.isNW; + + var ret = {}; + ret.isCordova = isCordova; + ret.isChromeApp = isChromeApp; + ret.isSafari = platformInfo.isSafari; + ret.isWindowsPhoneApp = platformInfo.isWP; + ret.historyShowLimit = 10; + ret.historyShowMoreLimit = 10; + ret.isSearching = false; + ret.prevState = 'walletHome'; + ret.physicalScreenWidth = ((window.innerWidth > 0) ? window.innerWidth : screen.width); + + ret.appConfig = window.appConfig; + + // Only for testing + //storageService.checkQuota(); + + ret.menu = [{ + 'title': gettext('Receive'), + 'icon': { + false: 'icon-receive', + true: 'icon-receive-active' + }, + 'link': 'receive' + }, { + 'title': gettext('Activity'), + 'icon': { + false: 'icon-activity', + true: 'icon-activity-active' + }, + 'link': 'walletHome' + }, { + 'title': gettext('Send'), + 'icon': { + false: 'icon-send', + true: 'icon-send-active' + }, + 'link': 'send' + }]; + + ret.addonViews = addonManager.addonViews(); + ret.txTemplateUrl = addonManager.txTemplateUrl() || 'views/includes/transaction.html'; + + ret.tab = 'walletHome'; + var vanillaScope = ret; + + if (isNW) { + latestReleaseService.checkLatestRelease(function(err, newRelease) { + if (err) { + $log.warn(err); + return; + } + + if (newRelease) + $scope.newRelease = gettext('There is a new version of Copay. Please update'); + }); + } + + function strip(number) { + return (parseFloat(number.toPrecision(12))); + }; + + self.goHome = function() { + go.walletHome(); + }; + + self.allowRefresher = function() { + if ($ionicSideMenuDelegate.getOpenRatio() != 0) self.allowPullToRefresh = false; + } + + self.hideBalance = function() { + storageService.getHideBalanceFlag(self.walletId, function(err, shouldHideBalance) { + if (err) self.shouldHideBalance = false; + else self.shouldHideBalance = (shouldHideBalance == 'true') ? true : false; + }); + } + + self.onHold = function() { + self.shouldHideBalance = !self.shouldHideBalance; + storageService.setHideBalanceFlag(self.walletId, self.shouldHideBalance.toString(), function() {}); + } + + self.setWalletPreferencesTitle = function() { + return gettext("Wallet Preferences"); + } + + self.cleanInstance = function() { + $log.debug('Cleaning Index Instance'); + lodash.each(self, function(v, k) { + if (lodash.isFunction(v)) return; + // This are to prevent flicker in mobile: + if (k == 'hasProfile') return; + if (k == 'tab') return; + if (k == 'noFocusedWallet') return; + if (k == 'backgroundColor') return; + if (k == 'physicalScreenWidth') return; + if (k == 'loadingWallet') { + self.loadingWallet = true; + return; + } + if (!lodash.isUndefined(vanillaScope[k])) { + self[k] = vanillaScope[k]; + return; + } + + delete self[k]; + }); + }; + + self.setFocusedWallet = function() { + var fc = profileService.focusedClient; + if (!fc) return; + + self.cleanInstance(); + self.loadingWallet = true; + self.setSpendUnconfirmed(); + + $timeout(function() { + $rootScope.$apply(); + + self.hasProfile = true; + self.isSingleAddress = false; + self.noFocusedWallet = false; + self.updating = false; + + // Credentials Shortcuts + self.m = fc.credentials.m; + self.n = fc.credentials.n; + self.network = fc.credentials.network; + self.copayerId = fc.credentials.copayerId; + self.copayerName = fc.credentials.copayerName; + self.requiresMultipleSignatures = fc.credentials.m > 1; + self.isShared = fc.credentials.n > 1; + self.walletName = fc.credentials.walletName; + self.walletId = fc.credentials.walletId; + self.isComplete = fc.isComplete(); + self.canSign = fc.canSign(); + self.isPrivKeyExternal = fc.isPrivKeyExternal(); + self.isPrivKeyEncrypted = fc.isPrivKeyEncrypted(); + self.externalSource = fc.getPrivKeyExternalSourceName(); + self.account = fc.credentials.account; + self.incorrectDerivation = fc.keyDerivationOk === false; + + if (self.externalSource == 'trezor') + self.account++; + + self.txps = []; + self.copayers = []; + self.updateColor(); + self.updateAlias(); + self.setAddressbook(); + + self.initGlidera(); + self.initCoinbase(); + + self.hideBalance(); + + self.setCustomBWSFlag(); + + if (!self.isComplete) { + $log.debug('Wallet not complete BEFORE update... redirecting'); + go.path('copayers'); + } else { + if (go.is('copayers')) { + $log.debug('Wallet Complete BEFORE update... redirect to home'); + go.walletHome(); + } + } + + profileService.needsBackup(fc, function(needsBackup) { + self.needsBackup = needsBackup; + self.openWallet(function() { + if (!self.isComplete) { + $log.debug('Wallet not complete after update... redirecting'); + go.path('copayers'); + } else { + if (go.is('copayers')) { + $log.debug('Wallet Complete after update... redirect to home'); + go.walletHome(); + } + } + }); + }); + }); + }; + + self.setCustomBWSFlag = function() { + var defaults = configService.getDefaults(); + var config = configService.getSync(); + + self.usingCustomBWS = config.bwsFor && config.bwsFor[self.walletId] && (config.bwsFor[self.walletId] != defaults.bws.url); + }; + + + self.setTab = function(tab, reset, tries, switchState) { + tries = tries || 0; + + // check if the whole menu item passed + if (typeof tab == 'object') { + if (tab.open) { + if (tab.link) { + self.tab = tab.link; + } + tab.open(); + return; + } else { + return self.setTab(tab.link, reset, tries, switchState); + } + } + if (self.tab === tab && !reset) + return; + + if (!document.getElementById('menu-' + tab) && ++tries < 5) { + return $timeout(function() { + self.setTab(tab, reset, tries, switchState); + }, 300); + } + + if (!self.tab || !go.is('walletHome')) + self.tab = 'walletHome'; + + var changeTab = function() { + if (document.getElementById(self.tab)) { + document.getElementById(self.tab).className = 'tab-out tab-view ' + self.tab; + var old = document.getElementById('menu-' + self.tab); + if (old) { + old.className = ''; + } + } + + if (document.getElementById(tab)) { + document.getElementById(tab).className = 'tab-in tab-view ' + tab; + var newe = document.getElementById('menu-' + tab); + if (newe) { + newe.className = 'active'; + } + } + + self.tab = tab; + $rootScope.$emit('Local/TabChanged', tab); + }; + + if (switchState && !go.is('walletHome')) { + go.path('walletHome', function() { + changeTab(); + }); + return; + } + + changeTab(); + }; + + + self.setSpendUnconfirmed = function(spendUnconfirmed) { + self.spendUnconfirmed = spendUnconfirmed || configService.getSync().wallet.spendUnconfirmed; + }; + + self.updateBalance = function() { + var fc = profileService.focusedClient; + $timeout(function() { + ongoingProcess.set('updatingBalance', true); + $log.debug('Updating Balance'); + fc.getBalance(function(err, balance) { + ongoingProcess.set('updatingBalance', false); + if (err) { + self.handleError(err); + return; + } + $log.debug('Wallet Balance:', balance); + self.setBalance(balance); + }); + }); + }; + + self.updatePendingTxps = function() { + var fc = profileService.focusedClient; + $timeout(function() { + self.updating = true; + $log.debug('Updating PendingTxps'); + fc.getTxProposals({}, function(err, txps) { + self.updating = false; + if (err) { + self.handleError(err); + } else { + $log.debug('Wallet PendingTxps:', txps); + self.setPendingTxps(txps); + } + $rootScope.$apply(); + }); + }); + }; + + // This handles errors from BWS/index which normally + // trigger from async events (like updates). + // Debounce function avoids multiple popups + var _handleError = function(err) { + $log.warn('Client ERROR: ', err); + if (err instanceof errors.NOT_AUTHORIZED) { + self.notAuthorized = true; + go.walletHome(); + } else if (err instanceof errors.NOT_FOUND) { + self.showErrorPopup(gettext('Could not access Wallet Service: Not found')); + } else { + var msg = "" + $scope.$emit('Local/ClientError', (err.error ? err.error : err)); + var msg = bwcError.msg(err, gettext('Error at Wallet Service')); + self.showErrorPopup(msg); + } + }; + + self.handleError = lodash.debounce(_handleError, 1000); + + self.openWallet = function(cb) { + var fc = profileService.focusedClient; + $timeout(function() { + $rootScope.$apply(); + self.updating = true; + self.updateError = false; + fc.openWallet(function(err, walletStatus) { + self.updating = false; + if (err) { + self.updateError = true; + self.handleError(err); + return; + } + $log.debug('Wallet Opened'); + + self.updateAll(lodash.isObject(walletStatus) ? { + walletStatus: walletStatus, + cb: cb, + } : { + cb: cb + }); + $rootScope.$apply(); + }); + }); + }; + + self.setPendingTxps = function(txps) { + self.pendingTxProposalsCountForUs = 0; + var now = Math.floor(Date.now() / 1000); + + /* Uncomment to test multiple outputs */ + /* + var txp = { + message: 'test multi-output', + fee: 1000, + createdOn: new Date() / 1000, + outputs: [] + }; + function addOutput(n) { + txp.outputs.push({ + amount: 600, + toAddress: '2N8bhEwbKtMvR2jqMRcTCQqzHP6zXGToXcK', + message: 'output #' + (Number(n) + 1) + }); + }; + lodash.times(150, addOutput); + txps.push(txp); + */ + + lodash.each(txps, function(tx) { + + tx = txFormatService.processTx(tx); + + // no future transactions... + if (tx.createdOn > now) + tx.createdOn = now; + + var action = lodash.find(tx.actions, { + copayerId: self.copayerId + }); + + if (!action && tx.status == 'pending') { + tx.pendingForUs = true; + } + + if (action && action.type == 'accept') { + tx.statusForUs = 'accepted'; + } else if (action && action.type == 'reject') { + tx.statusForUs = 'rejected'; + } else { + tx.statusForUs = 'pending'; + } + + if (!tx.deleteLockTime) + tx.canBeRemoved = true; + + if (tx.creatorId != self.copayerId) { + self.pendingTxProposalsCountForUs = self.pendingTxProposalsCountForUs + 1; + } + addonManager.formatPendingTxp(tx); + }); + self.txps = txps; + }; + + var SAFE_CONFIRMATIONS = 6; + + self.processNewTxs = function(txs) { + var config = configService.getSync().wallet.settings; + var now = Math.floor(Date.now() / 1000); + var txHistoryUnique = {}; + var ret = []; + self.hasUnsafeConfirmed = false; + + lodash.each(txs, function(tx) { + tx = txFormatService.processTx(tx); + + // no future transactions... + if (tx.time > now) + tx.time = now; + + if (tx.confirmations >= SAFE_CONFIRMATIONS) { + tx.safeConfirmed = SAFE_CONFIRMATIONS + '+'; + } else { + tx.safeConfirmed = false; + self.hasUnsafeConfirmed = true; + } + + if (tx.note) { + delete tx.note.encryptedEditedByName; + delete tx.note.encryptedBody; + } + + if (!txHistoryUnique[tx.txid]) { + ret.push(tx); + txHistoryUnique[tx.txid] = true; + } else { + $log.debug('Ignoring duplicate TX in history: ' + tx.txid) + } + }); + + return ret; + }; + + self.updateAlias = function() { + var config = configService.getSync(); + config.aliasFor = config.aliasFor || {}; + self.alias = config.aliasFor[self.walletId]; + var fc = profileService.focusedClient; + fc.alias = self.alias; + }; + + self.updateColor = function() { + var config = configService.getSync(); + config.colorFor = config.colorFor || {}; + self.backgroundColor = config.colorFor[self.walletId] || '#4A90E2'; + var fc = profileService.focusedClient; + fc.backgroundColor = self.backgroundColor; + if (isCordova && StatusBar.isVisible) { + StatusBar.backgroundColorByHexString(fc.backgroundColor); + } + }; + + self.setBalance = function(balance) { + if (!balance) return; + var config = configService.getSync().wallet.settings; + var COIN = 1e8; + + + // Address with Balance + self.balanceByAddress = balance.byAddress; + + // Spend unconfirmed funds + if (self.spendUnconfirmed) { + self.totalBalanceSat = balance.totalAmount; + self.lockedBalanceSat = balance.lockedAmount; + self.availableBalanceSat = balance.availableAmount; + self.totalBytesToSendMax = balance.totalBytesToSendMax; + self.pendingAmount = null; + } else { + self.totalBalanceSat = balance.totalConfirmedAmount; + self.lockedBalanceSat = balance.lockedConfirmedAmount; + self.availableBalanceSat = balance.availableConfirmedAmount; + self.totalBytesToSendMax = balance.totalBytesToSendConfirmedMax; + self.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount; + } + + // Selected unit + self.unitToSatoshi = config.unitToSatoshi; + self.satToUnit = 1 / self.unitToSatoshi; + self.unitName = config.unitName; + + //STR + self.totalBalanceStr = profileService.formatAmount(self.totalBalanceSat) + ' ' + self.unitName; + self.lockedBalanceStr = profileService.formatAmount(self.lockedBalanceSat) + ' ' + self.unitName; + self.availableBalanceStr = profileService.formatAmount(self.availableBalanceSat) + ' ' + self.unitName; + + if (self.pendingAmount) { + self.pendingAmountStr = profileService.formatAmount(self.pendingAmount) + ' ' + self.unitName; + } else { + self.pendingAmountStr = null; + } + + self.alternativeName = config.alternativeName; + self.alternativeIsoCode = config.alternativeIsoCode; + + // Check address + addressService.isUsed(self.walletId, balance.byAddress, function(err, used) { + if (used) { + $log.debug('Address used. Creating new'); + $rootScope.$emit('Local/AddressIsUsed'); + } + }); + + rateService.whenAvailable(function() { + + var totalBalanceAlternative = rateService.toFiat(self.totalBalanceSat, self.alternativeIsoCode); + var lockedBalanceAlternative = rateService.toFiat(self.lockedBalanceSat, self.alternativeIsoCode); + var alternativeConversionRate = rateService.toFiat(100000000, self.alternativeIsoCode); + + self.totalBalanceAlternative = $filter('formatFiatAmount')(totalBalanceAlternative); + self.lockedBalanceAlternative = $filter('formatFiatAmount')(lockedBalanceAlternative); + self.alternativeConversionRate = $filter('formatFiatAmount')(alternativeConversionRate); + + self.alternativeBalanceAvailable = true; + + self.isRateAvailable = true; + $rootScope.$apply(); + }); + + if (!rateService.isAvailable()) { + $rootScope.$apply(); + } + }; + + + self.showMore = function() { + $timeout(function() { + if (self.isSearching) { + self.txHistorySearchResults = self.result.slice(0, self.nextTxHistory); + $log.debug('Total txs: ', self.txHistorySearchResults.length + '/' + self.result.length); + if (self.txHistorySearchResults.length >= self.result.length) + self.historyShowMore = false; + } else { + self.txHistory = self.completeHistory.slice(0, self.nextTxHistory); + $log.debug('Total txs: ', self.txHistory.length + '/' + self.completeHistory.length); + if (self.txHistory.length >= self.completeHistory.length) + self.historyShowMore = false; + } + self.nextTxHistory += self.historyShowMoreLimit; + $scope.$broadcast('scroll.infiniteScrollComplete'); + }, 100); + }; + + + + + self.toggleLeftMenu = function() { + profileService.isDisclaimerAccepted(function(val) { + if (val) go.toggleLeftMenu(); + else + $log.debug('Disclaimer not accepted, cannot open menu'); + }); + }; + + self.initGlidera = function(accessToken) { + self.glideraEnabled = configService.getSync().glidera.enabled; + self.glideraTestnet = configService.getSync().glidera.testnet; + var network = self.glideraTestnet ? 'testnet' : 'livenet'; + + self.glideraToken = null; + self.glideraError = null; + self.glideraPermissions = null; + self.glideraEmail = null; + self.glideraPersonalInfo = null; + self.glideraTxs = null; + self.glideraStatus = null; + + if (!self.glideraEnabled) return; + + glideraService.setCredentials(network); + + var getToken = function(cb) { + if (accessToken) { + cb(null, accessToken); + } else { + storageService.getGlideraToken(network, cb); + } + }; + + getToken(function(err, accessToken) { + if (err || !accessToken) return; + else { + glideraService.getAccessTokenPermissions(accessToken, function(err, p) { + if (err) { + self.glideraError = err; + } else { + self.glideraToken = accessToken; + self.glideraPermissions = p; + self.updateGlidera({ + fullUpdate: true + }); + } + }); + } + }); + }; + + self.updateGlidera = function(opts) { + if (!self.glideraToken || !self.glideraPermissions) return; + var accessToken = self.glideraToken; + var permissions = self.glideraPermissions; + + opts = opts || {}; + + glideraService.getStatus(accessToken, function(err, data) { + self.glideraStatus = data; + }); + + glideraService.getLimits(accessToken, function(err, limits) { + self.glideraLimits = limits; + }); + + if (permissions.transaction_history) { + glideraService.getTransactions(accessToken, function(err, data) { + self.glideraTxs = data; + }); + } + + if (permissions.view_email_address && opts.fullUpdate) { + glideraService.getEmail(accessToken, function(err, data) { + self.glideraEmail = data.email; + }); + } + if (permissions.personal_info && opts.fullUpdate) { + glideraService.getPersonalInfo(accessToken, function(err, data) { + self.glideraPersonalInfo = data; + }); + } + + }; + + self.initCoinbase = function(accessToken) { + self.coinbaseEnabled = configService.getSync().coinbase.enabled; + self.coinbaseTestnet = configService.getSync().coinbase.testnet; + var network = self.coinbaseTestnet ? 'testnet' : 'livenet'; + + self.coinbaseToken = null; + self.coinbaseError = null; + self.coinbasePermissions = null; + self.coinbaseEmail = null; + self.coinbasePersonalInfo = null; + self.coinbaseTxs = null; + self.coinbaseStatus = null; + + if (!self.coinbaseEnabled) return; + + coinbaseService.setCredentials(network); + + var getToken = function(cb) { + if (accessToken) { + cb(null, accessToken); + } else { + storageService.getCoinbaseToken(network, cb); + } + }; + + getToken(function(err, accessToken) { + if (err || !accessToken) return; + else { + coinbaseService.getAccounts(accessToken, function(err, a) { + if (err) { + self.coinbaseError = err; + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + } + } else { + self.coinbaseToken = accessToken; + lodash.each(a.data, function(account) { + if (account.primary && account.type == 'wallet') { + self.coinbaseAccount = account; + self.updateCoinbase(); + } + }); + } + }); + } + }); + }; + + self.updateCoinbase = lodash.debounce(function(opts) { + if (!self.coinbaseToken || !self.coinbaseAccount) return; + var accessToken = self.coinbaseToken; + var accountId = self.coinbaseAccount.id; + + opts = opts || {}; + + if (opts.updateAccount) { + coinbaseService.getAccount(accessToken, accountId, function(err, a) { + if (err) { + self.coinbaseError = err; + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + } + return; + } + self.coinbaseAccount = a.data; + }); + } + + coinbaseService.getCurrentUser(accessToken, function(err, u) { + if (err) { + self.coinbaseError = err; + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + } + return; + } + self.coinbaseUser = u.data; + }); + + coinbaseService.getPendingTransactions(function(err, txs) { + self.coinbasePendingTransactions = lodash.isEmpty(txs) ? null : txs; + lodash.forEach(txs, function(dataFromStorage, txId) { + if ((dataFromStorage.type == 'sell' && dataFromStorage.status == 'completed') || + (dataFromStorage.type == 'buy' && dataFromStorage.status == 'completed') || + dataFromStorage.status == 'error' || + (dataFromStorage.type == 'send' && dataFromStorage.status == 'completed')) return; + coinbaseService.getTransaction(accessToken, accountId, txId, function(err, tx) { + if (err) { + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + return; + } + coinbaseService.savePendingTransaction(dataFromStorage, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + return; + } + _updateCoinbasePendingTransactions(dataFromStorage, tx.data); + self.coinbasePendingTransactions[txId] = dataFromStorage; + if (tx.data.type == 'send' && tx.data.status == 'completed' && tx.data.from) { + coinbaseService.sellPrice(accessToken, dataFromStorage.sell_price_currency, function(err, s) { + if (err) { + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + return; + } + coinbaseService.savePendingTransaction(dataFromStorage, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + return; + } + var newSellPrice = s.data.amount; + var variance = Math.abs((newSellPrice - dataFromStorage.sell_price_amount) / dataFromStorage.sell_price_amount * 100); + if (variance < dataFromStorage.price_sensitivity.value) { + self.sellPending(tx.data); + } else { + var error = { + errors: [{ + message: 'Price falls over the selected percentage' + }] + }; + coinbaseService.savePendingTransaction(dataFromStorage, { + status: 'error', + error: error + }, function(err) { + if (err) $log.debug(err); + }); + } + }); + } else if (tx.data.type == 'buy' && tx.data.status == 'completed' && tx.data.buy) { + self.sendToCopay(dataFromStorage); + } else { + coinbaseService.savePendingTransaction(dataFromStorage, {}, function(err) { + if (err) $log.debug(err); + }); + } + }); + }); + }); + + }, 1000); + + var _updateCoinbasePendingTransactions = function(obj /*, …*/ ) { + for (var i = 1; i < arguments.length; i++) { + for (var prop in arguments[i]) { + var val = arguments[i][prop]; + if (typeof val == "object") + _updateCoinbasePendingTransactions(obj[prop], val); + else + obj[prop] = val ? val : obj[prop]; + } + } + return obj; + }; + + self.refreshCoinbaseToken = function() { + var network = self.coinbaseTestnet ? 'testnet' : 'livenet'; + storageService.getCoinbaseRefreshToken(network, function(err, refreshToken) { + if (!refreshToken) return; + coinbaseService.refreshToken(refreshToken, function(err, data) { + if (err) { + self.coinbaseError = err; + } else if (data && data.access_token && data.refresh_token) { + storageService.setCoinbaseToken(network, data.access_token, function() { + storageService.setCoinbaseRefreshToken(network, data.refresh_token, function() { + $timeout(function() { + self.initCoinbase(data.access_token); + }, 100); + }); + }); + } + }); + }); + }; + + self.sendToCopay = function(tx) { + if (!tx) return; + var data = { + to: tx.toAddr, + amount: tx.amount.amount, + currency: tx.amount.currency, + description: 'To Copay Wallet' + }; + coinbaseService.sendTo(self.coinbaseToken, self.coinbaseAccount.id, data, function(err, res) { + if (err) { + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + return; + } + coinbaseService.savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + } else { + if (!res.data.id) { + coinbaseService.savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + return; + } + coinbaseService.getTransaction(self.coinbaseToken, self.coinbaseAccount.id, res.data.id, function(err, sendTx) { + coinbaseService.savePendingTransaction(tx, { + remove: true + }, function(err) { + coinbaseService.savePendingTransaction(sendTx.data, {}, function(err) { + $timeout(function() { + self.updateCoinbase({ + updateAccount: true + }); + }, 1000); + }); + }); + }); + } + }); + }; + + self.sellPending = function(tx) { + if (!tx) return; + var data = tx.amount; + data['commit'] = true; + coinbaseService.sellRequest(self.coinbaseToken, self.coinbaseAccount.id, data, function(err, res) { + if (err) { + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + return; + } + coinbaseService.savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + } else { + if (!res.data.transaction) { + coinbaseService.savePendingTransaction(tx, { + status: 'error', + error: err + }, function(err) { + if (err) $log.debug(err); + }); + return; + } + coinbaseService.savePendingTransaction(tx, { + remove: true + }, function(err) { + coinbaseService.getTransaction(self.coinbaseToken, self.coinbaseAccount.id, res.data.transaction.id, function(err, updatedTx) { + coinbaseService.savePendingTransaction(updatedTx.data, {}, function(err) { + if (err) $log.debug(err); + $timeout(function() { + self.updateCoinbase({ + updateAccount: true + }); + }, 1000); + }); + }); + }); + } + }); + }; + + self.isInFocus = function(walletId) { + var fc = profileService.focusedClient; + return fc && fc.credentials.walletId == walletId; + }; + + self.setAddressbook = function(ab) { + if (ab) { + self.addressbook = ab; + return; + } + + addressbookService.list(function(err, ab) { + if (err) { + $log.error('Error getting the addressbook'); + return; + } + self.addressbook = ab; + }); + }; + + $rootScope.$on('$stateChangeSuccess', function(ev, to, toParams, from, fromParams) { + self.prevState = from.name || 'walletHome'; + self.tab = 'walletHome'; + }); + + $rootScope.$on('Local/ValidatingWalletEnded', function(ev, walletId, isOK) { + + if (self.isInFocus(walletId)) { + // NOTE: If the user changed the wallet, the flag is already turn off. + self.incorrectDerivation = isOK === false; + } + }); + + $rootScope.$on('Local/ClearHistory', function(event) { + $log.debug('The wallet transaction history has been deleted'); + self.txHistory = self.completeHistory = self.txHistorySearchResults = []; + self.debounceUpdateHistory(); + }); + + $rootScope.$on('Local/AddressbookUpdated', function(event, ab) { + self.setAddressbook(ab); + }); + + // UX event handlers + $rootScope.$on('Local/ColorUpdated', function(event) { + self.updateColor(); + $timeout(function() { + $rootScope.$apply(); + }); + }); + + $rootScope.$on('Local/AliasUpdated', function(event) { + self.updateAlias(); + $timeout(function() { + $rootScope.$apply(); + }); + }); + + $rootScope.$on('Local/SpendUnconfirmedUpdated', function(event, spendUnconfirmed) { + self.setSpendUnconfirmed(spendUnconfirmed); + self.updateAll(); + }); + + $rootScope.$on('Local/GlideraUpdated', function(event, accessToken) { + self.initGlidera(accessToken); + }); + + $rootScope.$on('Local/CoinbaseUpdated', function(event, accessToken) { + self.initCoinbase(accessToken); + }); + + $rootScope.$on('Local/GlideraTx', function(event, accessToken, permissions) { + self.updateGlidera(); + }); + + $rootScope.$on('Local/CoinbaseTx', function(event) { + self.updateCoinbase({ + updateAccount: true + }); + }); + + $rootScope.$on('Local/GlideraError', function(event) { + self.debouncedUpdate(); + }); + + $rootScope.$on('Local/UnitSettingUpdated', function(event) { + self.updateAll({ + triggerTxUpdate: true, + }); + }); + + $rootScope.$on('Local/WalletCompleted', function(event, walletId) { + if (self.isInFocus(walletId)) { + // reset main wallet variables + self.setFocusedWallet(); + go.walletHome(); + } + }); + + self.debouncedUpdate = function() { + var now = Date.now(); + var oneHr = 1000 * 60 * 60; + + if (!self.lastUpdate || (now - self.lastUpdate) > oneHr) { + self.updateAll({ + quiet: true, + triggerTxUpdate: true + }); + } + }; + + $rootScope.$on('Local/Resume', function(event) { + $log.debug('### Resume event'); + profileService.isDisclaimerAccepted(function(v) { + if (!v) { + $log.debug('Disclaimer not accepted, resume to home'); + go.path('disclaimer'); + } + }); + self.debouncedUpdate(); + }); + + $rootScope.$on('Local/BackupDone', function(event, walletId) { + self.needsBackup = false; + $log.debug('Backup done'); + storageService.setBackupFlag(walletId || self.walletId, function(err) { + $log.debug('Backup stored'); + }); + }); + + $rootScope.$on('Local/DeviceError', function(event, err) { + self.showErrorPopup(err, function() { + if (isCordova && navigator && navigator.app) { + navigator.app.exitApp(); + } + }); + }); + + $rootScope.$on('Local/WalletImported', function(event, walletId) { + self.needsBackup = false; + storageService.setBackupFlag(walletId, function() { + $log.debug('Backup done stored'); + addressService.expireAddress(walletId, function(err) { + $timeout(function() { + self.txHistory = self.completeHistory = self.txHistorySearchResults = []; + storageService.removeTxHistory(walletId, function() { + self.startScan(walletId); + }); + }, 500); + }); + }); + }); + + $rootScope.$on('NewIncomingTx', function() { + self.newTx = true; + self.updateAll({ + walletStatus: null, + untilItChanges: true, + triggerTxUpdate: true, + }); + }); + + + $rootScope.$on('NewBlock', function() { + if (self.glideraEnabled) { + $timeout(function() { + self.updateGlidera(); + }); + } + if (self.coinbaseEnabled) { + $timeout(function() { + self.updateCoinbase(); + }); + } + if (self.pendingAmount) { + self.updateAll({ + walletStatus: null, + untilItChanges: null, + triggerTxUpdate: true, + }); + } else if (self.hasUnsafeConfirmed) { + $log.debug('Wallet has transactions with few confirmations. Updating.') + if (self.network == 'testnet') { + self.throttledUpdateHistory(); + } else { + self.debounceUpdateHistory(); + } + } + }); + + $rootScope.$on('BalanceUpdated', function(e, n) { + self.setBalance(n.data); + }); + + + //untilItChange TRUE + lodash.each(['NewOutgoingTx', 'NewOutgoingTxByThirdParty'], function(eventName) { + $rootScope.$on(eventName, function(event) { + self.newTx = true; + self.updateAll({ + walletStatus: null, + untilItChanges: true, + triggerTxUpdate: true, + }); + }); + }); + + //untilItChange FALSE + lodash.each(['NewTxProposal', 'TxProposalFinallyRejected', 'TxProposalRemoved', 'NewOutgoingTxByThirdParty', + 'Local/GlideraTx' + ], function(eventName) { + $rootScope.$on(eventName, function(event) { + self.updateAll({ + walletStatus: null, + untilItChanges: null, + triggerTxUpdate: true, + }); + }); + }); + + + //untilItChange Maybe + $rootScope.$on('Local/TxProposalAction', function(event, untilItChanges) { + self.newTx = untilItChanges; + self.updateAll({ + walletStatus: null, + untilItChanges: untilItChanges, + triggerTxUpdate: true, + }); + }); + + $rootScope.$on('ScanFinished', function() { + $log.debug('Scan Finished. Updating history'); + storageService.removeTxHistory(self.walletId, function() { + self.updateAll({ + walletStatus: null, + triggerTxUpdate: true, + }); + }); + }); + + lodash.each(['TxProposalRejectedBy', 'TxProposalAcceptedBy'], function(eventName) { + $rootScope.$on(eventName, function() { + var f = function() { + if (self.updating) { + return $timeout(f, 200); + }; + self.updatePendingTxps(); + }; + f(); + }); + }); + + $rootScope.$on('Local/NoWallets', function(event) { + $timeout(function() { + self.hasProfile = true; + self.noFocusedWallet = true; + self.isComplete = null; + self.walletName = null; + uxLanguage.update(); + }); + }); + + $rootScope.$on('Local/NewFocusedWallet', function() { + console.log('[index.js.1200:NewFocusedWallet:] TODO'); //TODO + + return; + + + uxLanguage.update(); + self.setFocusedWallet(); + self.updateHistory(); + storageService.getCleanAndScanAddresses(function(err, walletId) { + + if (walletId && profileService.walletClients[walletId]) { + $log.debug('Clear last address cache and Scan ', walletId); + addressService.expireAddress(walletId, function(err) { + self.startScan(walletId); + }); + storageService.removeCleanAndScanAddresses(function() { + $rootScope.$emit('Local/NewFocusedWalletReady'); + }); + } else { + $rootScope.$emit('Local/NewFocusedWalletReady'); + } + }); + }); + + $rootScope.$on('Local/SetTab', function(event, tab, reset) { + self.setTab(tab, reset); + }); + + $rootScope.$on('disclaimerAccepted', function(event) { + $scope.isDisclaimerAccepted = true; + }); + + $rootScope.$on('Local/WindowResize', function() { + self.physicalScreenWidth = ((window.innerWidth > 0) ? window.innerWidth : screen.width); + }); + + $rootScope.$on('Local/NeedsConfirmation', function(event, txp, cb) { + + function openConfirmationPopup(txp, cb) { + + var config = configService.getSync(); + + $scope.color = config.colorFor[txp.walletId] || '#4A90E2'; + $scope.tx = txFormatService.processTx(txp); + + self.confirmationPopup = $ionicPopup.show({ + templateUrl: 'views/includes/confirm-tx.html', + scope: $scope, + }); + + $scope.processFee = function(amount, fee) { + var walletSettings = configService.getSync().wallet.settings; + var feeAlternativeIsoCode = walletSettings.alternativeIsoCode; + + $scope.feeLevel = feeService.feeOpts[feeService.getCurrentFeeLevel()]; + $scope.feeAlternativeStr = parseFloat((rateService.toFiat(fee, feeAlternativeIsoCode)).toFixed(2), 10) + ' ' + feeAlternativeIsoCode; + $scope.feeRateStr = (fee / (amount + fee) * 100).toFixed(2) + '%'; + }; + + $scope.cancel = function() { + return cb(); + }; + + $scope.accept = function() { + return cb(true); + }; + } + + openConfirmationPopup(txp, function(accept) { + self.confirmationPopup.close(); + return cb(accept); + }); + }); + + $rootScope.$on('Local/NeedsPassword', function(event, isSetup, cb) { + + function openPasswordPopup(isSetup, cb) { + $scope.data = {}; + $scope.data.password = null; + $scope.isSetup = isSetup; + $scope.isVerification = false; + $scope.loading = false; + var pass = null; + + self.passwordPopup = $ionicPopup.show({ + templateUrl: 'views/includes/password.html', + scope: $scope, + }); + + $scope.cancel = function() { + return cb('No spending password given'); + }; + + $scope.keyPress = function(event) { + if (!$scope.data.password || $scope.loading) return; + if (event.keyCode == 13) $scope.set(); + } + + $scope.set = function() { + $scope.loading = true; + $scope.error = null; + + $timeout(function() { + if (isSetup && !$scope.isVerification) { + $scope.loading = false; + $scope.isVerification = true; + pass = $scope.data.password; + $scope.data.password = null; + return; + } + if (isSetup && pass != $scope.data.password) { + $scope.loading = false; + $scope.error = gettext('Spending Passwords do not match'); + $scope.isVerification = false; + $scope.data.password = null; + pass = null; + return; + } + return cb(null, $scope.data.password); + }, 100); + }; + }; + + openPasswordPopup(isSetup, function(err, pass) { + self.passwordPopup.close(); + return cb(err, pass); + }); + + }); + + $rootScope.$on('Local/EmailUpdated', function(event, email) { + self.preferences.email = email; + }); + + lodash.each(['NewCopayer', 'CopayerUpdated'], function(eventName) { + $rootScope.$on(eventName, function() { + // Re try to open wallet (will triggers) + self.setFocusedWallet(); + }); + }); + + $rootScope.$on('Local/NewEncryptionSetting', function() { + var fc = profileService.focusedClient; + self.isPrivKeyEncrypted = fc.isPrivKeyEncrypted(); + $timeout(function() { + $rootScope.$apply(); + }); + }); + + + /* Start setup */ + lodash.assign(self, vanillaScope); + openURLService.init(); +}); diff --git a/src/js/controllers/preferences.js b/src/js/controllers/preferences.js index a23706cdc..5137bd1ac 100644 --- a/src/js/controllers/preferences.js +++ b/src/js/controllers/preferences.js @@ -11,7 +11,7 @@ angular.module('copayApp.controllers').controller('preferencesController', $scope.externalSource = null; if (wallet) { - walletService.updateStatus(wallet, {}, function(err, status) {}); + walletService.getStatus(wallet, {}, function(err, status) {}); var config = configService.getSync(); config.aliasFor = config.aliasFor || {}; $scope.alias = config.aliasFor[walletId] || wallet.credentials.walletName; diff --git a/src/js/controllers/preferencesUnit.js b/src/js/controllers/preferencesUnit.js index c74efc0de..abdee5625 100644 --- a/src/js/controllers/preferencesUnit.js +++ b/src/js/controllers/preferencesUnit.js @@ -7,22 +7,19 @@ angular.module('copayApp.controllers').controller('preferencesUnitController', f $scope.currentUnit = config.wallet.settings.unitCode; } - $scope.unitList = [ - { - name: 'bits (1,000,000 bits = 1BTC)', - shortName: 'bits', - value: 100, - decimals: 2, - code: 'bit', - }, - { - name: 'BTC', - shortName: 'BTC', - value: 100000000, - decimals: 8, - code: 'btc', - } - ]; + $scope.unitList = [{ + name: 'bits (1,000,000 bits = 1BTC)', + shortName: 'bits', + value: 100, + decimals: 2, + code: 'bit', + }, { + name: 'BTC', + shortName: 'BTC', + value: 100000000, + decimals: 8, + code: 'btc', + }]; $scope.save = function(newUnit) { var opts = { diff --git a/src/js/controllers/tab-home.js b/src/js/controllers/tab-home.js index c37f9557b..579069381 100644 --- a/src/js/controllers/tab-home.js +++ b/src/js/controllers/tab-home.js @@ -88,23 +88,21 @@ angular.module('copayApp.controllers').controller('tabHomeController', self.updateAllClients = function() { var txps = []; - var wallets = profileService.getWallets(); - var l = wallets.length, - i = 0; + var i = $scope.wallets.length; - lodash.each(wallets, function(wallet) { - walletService.updateStatus(wallet, {}, function(err) { - var status = wallet.status; + lodash.each($scope.wallets, function(wallet) { + walletService.getStatus(wallet, {}, function(err, status) { if (err) { - console.log('[tab-home.js.35:err:]',$log.error(err)); //TODO - return; + console.log('[tab-home.js.35:err:]', $log.error(err)); //TODO + return; } // TODO if (status.pendingTxps && status.pendingTxps[0]) { txps = txps.concat(status.pendingTxps); } - if (++i == l) { + if (--i == 0) { setPendingTxps(txps); } + wallet.status = status; }); }); } diff --git a/src/js/controllers/walletDetails.js b/src/js/controllers/walletDetails.js index 7728104a0..05376c1d6 100644 --- a/src/js/controllers/walletDetails.js +++ b/src/js/controllers/walletDetails.js @@ -1,36 +1,16 @@ 'use strict'; -angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, profileService, lodash, configService, gettext, gettextCatalog, platformInfo, go, walletService) { - +angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, bwcError, profileService, lodash, configService, gettext, gettextCatalog, platformInfo, go, walletService) { var isCordova = platformInfo.isCordova; var isWP = platformInfo.isWP; var isAndroid = platformInfo.isAndroid; var isChromeApp = platformInfo.isChromeApp; - var self = this; - $rootScope.shouldHideMenuBar = false; - $rootScope.wpInputFocused = false; - var config = configService.getSync(); - var configWallet = config.wallet; - var walletSettings = configWallet.settings; - var ret = {}; + var errorPopup; + + var HISTORY_SHOW_LIMIT = 10; - // INIT. Global value - ret.unitToSatoshi = walletSettings.unitToSatoshi; - ret.satToUnit = 1 / ret.unitToSatoshi; - ret.unitName = walletSettings.unitName; - ret.alternativeIsoCode = walletSettings.alternativeIsoCode; - ret.alternativeName = walletSettings.alternativeName; - ret.alternativeAmount = 0; - ret.unitDecimals = walletSettings.unitDecimals; - ret.isCordova = isCordova; - ret.addresses = []; - ret.isMobile = platformInfo.isMobile; - ret.isWindowsPhoneApp = platformInfo.isWP; - ret.countDown = null; - ret.sendMaxInfo = {}; - ret.showAlternative = false; $scope.openSearchModal = function() { var fc = profileService.focusedClient; @@ -46,7 +26,7 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun }); }; - this.openTxModal = function(btx) { + $scope.openTxModal = function(btx) { var self = this; $scope.btx = lodash.cloneDeep(btx); @@ -61,32 +41,100 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun }); }; - $scope.update = function() { - walletService.updateStatus(wallet, { - force: true - }, function(err, status) { - if (err) {} // TODO + $scope.recreate = function() { + walletService.recreate(); + }; + + $scope.updateStatus = function(force) { + $scope.updatingStatus = true; + $scope.updateStatusError = false; + $timeout(function() { + walletService.getStatus(wallet, { + force: !!force, + }, function(err, status) { + $scope.updatingStatus = false; + if (err) { + $scope.status = null; + $scope.updateStatusError = true; + return; + } + $scope.status = status; + }); + }) + }; + + $scope.updateTxHistory = function() { + + if ($scope.updatingTxHistory) return; + + $scope.updatingTxHistory = true; + $scope.updateTxHistoryError = false; + $scope.updatingTxHistoryProgress = null; + + var progressFn = function(txs) { + $scope.updatingTxHistoryProgress = txs ? txs.length : 0; + completeTxHistory = txs; + $scope.showHistory(); + $scope.$digest(); + }; + + $timeout(function() { + walletService.getTxHistory(wallet, { + progressFn: progressFn, + }, function(err, txHistory) { + $scope.updatingTxHistory = false; + if (err) { + $scope.txHistory = null; + $scope.updateTxHistoryError = true; + return; + } + completeTxHistory = txHistory; + + $scope.showHistory(); + $scope.$apply(); + }); }); }; + $scope.showHistory = function() { + if ($scope.isSearching) { + $scope.txHistorySearchResults = filteredTxHistory ? filteredTxHistory.slice(0, (currentTxHistoryPage + 1) * HISTORY_SHOW_LIMIT) : []; + $scope.txHistoryShowMore = filteredTxHistory.length > $scope.txHistorySearchResults.length; + } else { + $scope.txHistory = completeTxHistory ? completeTxHistory.slice(0, (currentTxHistoryPage + 1) * HISTORY_SHOW_LIMIT) : []; + $scope.txHistoryShowMore = completeTxHistory.length > $scope.txHistory.length; + } + }; + + $scope.showMore = function() { + currentTxHistoryPage++; + $scope.showHistory(); + $scope.$broadcast('scroll.infiniteScrollComplete'); + }; + + $scope.updateAll = function()  { + $scope.updateStatus(false); + $scope.updateTxHistory(); + } + $scope.hideToggle = function() { console.log('[walletDetails.js.70:hideToogle:] TODO'); //TODO }; - if (!$stateParams.walletId) { - $log.debug('No wallet provided... using the first one'); - $stateParams.walletId = profileService.getWallets({ - onlyComplete: true - })[0].id; - } + var currentTxHistoryPage; + var completeTxHistory; + var wallet; + $scope.init = function() { + currentTxHistoryPage = 0; + completeTxHistory = []; - var wallet = profileService.getWallet($stateParams.walletId); - $scope.wallet = wallet; + wallet = profileService.getWallet($stateParams.walletId); + $scope.wallet = wallet; + $scope.requiresMultipleSignatures = wallet.credentials.m > 1; + $scope.newTx = false; + + $scope.updateAll(); + }; - if (wallet) { - walletService.updateStatus(wallet, {}, function(err, status) { - if (err) {} // TODO - }); - } }); diff --git a/src/js/services/txFormatService.js b/src/js/services/txFormatService.js index d4a75873a..bfcaeb1c8 100644 --- a/src/js/services/txFormatService.js +++ b/src/js/services/txFormatService.js @@ -3,17 +3,6 @@ angular.module('copayApp.services').factory('txFormatService', function(bwcService, rateService, configService, lodash) { var root = {}; - - // // RECEIVE - // // Check address - // root.isUsed(wallet.walletId, balance.byAddress, function(err, used) { - // if (used) { - // $log.debug('Address used. Creating new'); - // $rootScope.$emit('Local/AddressIsUsed'); - // } - // }); - // - root.Utils = bwcService.getUtils(); @@ -57,8 +46,8 @@ angular.module('copayApp.services').factory('txFormatService', function(bwcServi }; root.processTx = function(tx) { - if (!tx || tx.action == 'invalid') - return tx; + if (!tx || tx.action == 'invalid') + return tx; // New transaction output format if (tx.outputs && tx.outputs.length) { @@ -77,7 +66,7 @@ angular.module('copayApp.services').factory('txFormatService', function(bwcServi }, 0); } tx.toAddress = tx.outputs[0].toAddress; - } + } tx.amountStr = root.formatAmountStr(tx.amount); tx.alternativeAmountStr = root.formatAlternativeStr(tx.amount); diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js index e8a082ace..00fd0c854 100644 --- a/src/js/services/walletService.js +++ b/src/js/services/walletService.js @@ -1,7 +1,7 @@ 'use strict'; // DO NOT INCLUDE STORAGE HERE \/ \/ -angular.module('copayApp.services').factory('walletService', function($log, $timeout, lodash, trezor, ledger, storageService, configService, rateService, uxLanguage, bwcService, $filter, gettextCatalog, bwcError, $ionicPopup, fingerprintService, ongoingProcess, gettext, $rootScope, txStatus, txFormatService, $ionicModal) { +angular.module('copayApp.services').factory('walletService', function($log, $timeout, lodash, trezor, ledger, storageService, configService, rateService, uxLanguage, $filter, gettextCatalog, bwcError, $ionicPopup, fingerprintService, ongoingProcess, gettext, $rootScope, txStatus, txFormatService, $ionicModal) { // DO NOT INCLUDE STORAGE HERE ^^ // // @@ -9,6 +9,10 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim var root = {}; + root.WALLET_STATUS_MAX_TRIES = 7; + root.WALLET_STATUS_DELAY_BETWEEN_TRIES = 1.4 * 1000; + root.SOFT_CONFIRMATION_LIMIT = 12; + root.SAFE_CONFIRMATIONS = 6; // UI Related root.openStatusModal = function(type, txp, cb) { @@ -27,8 +31,15 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; - - + // // RECEIVE + // // Check address + // root.isUsed(wallet.walletId, balance.byAddress, function(err, used) { + // if (used) { + // $log.debug('Address used. Creating new'); + // $rootScope.$emit('Local/AddressIsUsed'); + // } + // }); + // var _signWithLedger = function(wallet, txp, cb) { $log.info('Requesting Ledger Chrome app to sign the transaction'); @@ -78,16 +89,6 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; - var _walletStatusHash = function(walletStatus) { - var bal; - if (walletStatus) { - bal = walletStatus.balance.totalAmount; - } else { - bal = self.totalBalanceSat; - } - return bal; - }; - // TODO // This handles errors from BWS/index which normally @@ -102,7 +103,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim console.log('[walletService.js.93] TODO NOT AUTH'); //TODO // TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO - self.notAuthorized = true; + wallet.notAuthorized = true; go.walletHome(); } else if (err instanceof errors.NOT_FOUND) { root.showErrorPopup(gettext('Could not access Wallet Service: Not found')); @@ -115,170 +116,127 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }; root.handleError = lodash.debounce(_handleError, 1000); - - root.setBalance = function(wallet, balance) { - if (!balance) return; - - var config = configService.getSync().wallet.settings; - var COIN = 1e8; - - // Address with Balance - wallet.balanceByAddress = balance.byAddress; - - // Spend unconfirmed funds - if (wallet.spendUnconfirmed) { - wallet.totalBalanceSat = balance.totalAmount; - wallet.lockedBalanceSat = balance.lockedAmount; - wallet.availableBalanceSat = balance.availableAmount; - wallet.totalBytesToSendMax = balance.totalBytesToSendMax; - wallet.pendingAmount = null; - } else { - wallet.totalBalanceSat = balance.totalConfirmedAmount; - wallet.lockedBalanceSat = balance.lockedConfirmedAmount; - wallet.availableBalanceSat = balance.availableConfirmedAmount; - wallet.totalBytesToSendMax = balance.totalBytesToSendConfirmedMax; - wallet.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount; - } - - // Selected unit - wallet.unitToSatoshi = config.unitToSatoshi; - wallet.satToUnit = 1 / wallet.unitToSatoshi; - wallet.unitName = config.unitName; - - //STR - wallet.totalBalanceStr = txFormatService.formatAmount(wallet.totalBalanceSat) + ' ' + wallet.unitName; - wallet.lockedBalanceStr = txFormatService.formatAmount(wallet.lockedBalanceSat) + ' ' + wallet.unitName; - wallet.availableBalanceStr = txFormatService.formatAmount(wallet.availableBalanceSat) + ' ' + wallet.unitName; - - if (wallet.pendingAmount) { - wallet.pendingAmountStr = txFormatService.formatAmount(wallet.pendingAmount) + ' ' + wallet.unitName; - } else { - wallet.pendingAmountStr = null; - } - - wallet.alternativeName = config.alternativeName; - wallet.alternativeIsoCode = config.alternativeIsoCode; - - rateService.whenAvailable(function() { - - var totalBalanceAlternative = rateService.toFiat(wallet.totalBalanceSat, wallet.alternativeIsoCode); - var lockedBalanceAlternative = rateService.toFiat(wallet.lockedBalanceSat, wallet.alternativeIsoCode); - var alternativeConversionRate = rateService.toFiat(100000000, wallet.alternativeIsoCode); - - wallet.totalBalanceAlternative = $filter('formatFiatAmount')(totalBalanceAlternative); - wallet.lockedBalanceAlternative = $filter('formatFiatAmount')(lockedBalanceAlternative); - wallet.alternativeConversionRate = $filter('formatFiatAmount')(alternativeConversionRate); - - wallet.alternativeBalanceAvailable = true; - wallet.isRateAvailable = true; - }); - }; - - root.setStatus = function(wallet, status) { - wallet.status = status; - wallet.statusUpdatedOn = Date.now(); - wallet.isValid = true; - root.setBalance(wallet, status.balance); - wallet.email = status.preferences.email; - wallet.copayers = status.wallet.copayers; - }; - - root.updateStatus = function(wallet, opts, cb, initStatusHash, tries) { - tries = tries || 0; + root.getStatus = function(wallet, opts, cb) { opts = opts || {}; - if (wallet.isValid && !opts.force) - return; - - - var walletId = wallet.id; - - if (opts.untilItChanges && lodash.isUndefined(initStatusHash)) { - initStatusHash = _walletStatusHash(); - $log.debug('Updating status until it changes. initStatusHash:' + initStatusHash) - } - - var get = function(cb) { - if (opts.walletStatus) - return cb(null, opts.walletStatus); - else { - return wallet.getStatus({ - twoStep: true - }, function(err, ret) { - if (err) - return cb(bwcError.msg(err, gettext('Could not update Wallet'))); - // TODO?? - // self.isSingleAddress = !!ret.wallet.singleAddress; - // self.updating = ret.wallet.scanStatus == 'running'; - return cb(null, ret); - }); - } + function get(cb) { + wallet.getStatus({ + twoStep: true + }, function(err, ret) { + if (err) { + return cb(bwcError.msg(err, gettext('Could not update Wallet'))); + } + return cb(null, ret); + }); }; - // If not untilItChanges...trigger history update now - if (opts.triggerTxUpdate && !opts.untilItChanges) { - $timeout(function() { - root.debounceUpdateHistory(); - }, 1); - } + function cacheBalance(wallet, balance) { + if (!balance) return; - $timeout(function() { + var config = configService.getSync().wallet.settings; - // if (!opts.quiet) - // self.updating = true; + var cache = wallet.cachedStatus; + + // Address with Balance + cache.balanceByAddress = balance.byAddress; + + // Spend unconfirmed funds + if (cache.spendUnconfirmed) { + cache.totalBalanceSat = balance.totalAmount; + cache.lockedBalanceSat = balance.lockedAmount; + cache.availableBalanceSat = balance.availableAmount; + cache.totalBytesToSendMax = balance.totalBytesToSendMax; + cache.pendingAmount = null; + } else { + cache.totalBalanceSat = balance.totalConfirmedAmount; + cache.lockedBalanceSat = balance.lockedConfirmedAmount; + cache.availableBalanceSat = balance.availableConfirmedAmount; + cache.totalBytesToSendMax = balance.totalBytesToSendConfirmedMax; + cache.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount; + } + + // Selected unit + cache.unitToSatoshi = config.unitToSatoshi; + cache.satToUnit = 1 / cache.unitToSatoshi; + cache.unitName = config.unitName; + + //STR + cache.totalBalanceStr = txFormatService.formatAmount(cache.totalBalanceSat) + ' ' + cache.unitName; + cache.lockedBalanceStr = txFormatService.formatAmount(cache.lockedBalanceSat) + ' ' + cache.unitName; + cache.availableBalanceStr = txFormatService.formatAmount(cache.availableBalanceSat) + ' ' + cache.unitName; + + if (cache.pendingAmount) { + cache.pendingAmountStr = txFormatService.formatAmount(cache.pendingAmount) + ' ' + cache.unitName; + } else { + cache.pendingAmountStr = null; + } + + cache.alternativeName = config.alternativeName; + cache.alternativeIsoCode = config.alternativeIsoCode; + + rateService.whenAvailable(function() { + + var totalBalanceAlternative = rateService.toFiat(cache.totalBalanceSat, cache.alternativeIsoCode); + var lockedBalanceAlternative = rateService.toFiat(cache.lockedBalanceSat, cache.alternativeIsoCode); + var alternativeConversionRate = rateService.toFiat(100000000, cache.alternativeIsoCode); + + cache.totalBalanceAlternative = $filter('formatFiatAmount')(totalBalanceAlternative); + cache.lockedBalanceAlternative = $filter('formatFiatAmount')(lockedBalanceAlternative); + cache.alternativeConversionRate = $filter('formatFiatAmount')(alternativeConversionRate); + + cache.alternativeBalanceAvailable = true; + cache.isRateAvailable = true; + }); + }; + + function isStatusCached() { + return wallet.cachedStatus && wallet.cachedStatus.isValid; + }; + + function cacheStatus(status) { + wallet.cachedStatus = status ||  {}; + var cache = wallet.cachedStatus; + cache.statusUpdatedOn = Date.now(); + cache.isValid = true; + cache.email = status.preferences ? status.preferences.email : null; + cacheBalance(wallet, status.balance); + }; + + function walletStatusHash(status) { + return status ? status.balance.totalAmount : wallet.totalBalanceSat; + }; + + function _getStatus(initStatusHash, tries, cb) { + if (isStatusCached() && !opts.force) return cb(null, wallet.cachedStatus); + + tries = tries || 0; $log.debug('Updating Status:', wallet.credentials.walletName, tries); - get(function(err, walletStatus) { - var currentStatusHash = _walletStatusHash(walletStatus); + get(function(err, status) { + if (err) return cb(err); + + var currentStatusHash = walletStatusHash(status); $log.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries); - if (!err && opts.untilItChanges && initStatusHash == currentStatusHash && tries < 7 && walletId == profileService.focusedClient.credentials.walletId) { + if (opts.untilItChanges && + initStatusHash == currentStatusHash && + tries < root.WALLET_STATUS_MAX_TRIES && + walletId == wallet.credentials.walletId) { return $timeout(function() { $log.debug('Retrying update... ' + walletId + ' Try:' + tries) - return root.updateStatus(wallet, { - walletStatus: null, - untilItChanges: true, - triggerTxUpdate: opts.triggerTxUpdate, - }, cb, initStatusHash, ++tries); - }, 1400 * tries); + return _getStatus(initStatusHash, ++tries, cb); + }, root.WALLET_STATUS_DELAY_BETWEEN_TRIES * tries); } - if (err) { - root.handleError(err); - return cb(err); - } $log.debug('Got Wallet Status for:' + wallet.credentials.walletName); - root.setStatus(wallet, walletStatus); + cacheStatus(status); - // self.setPendingTxps(walletStatus.pendingTxps); - // - // // Status Shortcuts - // self.lastUpdate = Date.now(); - // self.walletName = walletStatus.wallet.name; - // self.walletSecret = walletStatus.wallet.secret; - // self.walletStatus = walletStatus.wallet.status; - // self.walletScanStatus = walletStatus.wallet.scanStatus; - // self.copayers = walletStatus.wallet.copayers; - // self.preferences = walletStatus.preferences; - // self.setBalance(walletStatus.balance); - // self.otherWallets = lodash.filter(profileService.getWallets(self.network), function(w) { - // return w.id != self.walletId; - // }); - // - // Notify external addons or plugins - - // TODO - if (opts.triggerTxUpdate && opts.untilItChanges) { - $timeout(function() { - root.debounceUpdateHistory(); - }, 1); - } - return cb(); - // } else { - // self.loadingWallet = false; - // } + // wallet.setPendingTxps(status.pendingTxps); + return cb(null, status); }); - }); + }; + + _getStatus(walletStatusHash(), 0, cb); }; var getSavedTxs = function(walletId, cb) { @@ -321,14 +279,60 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; + var removeAndMarkSoftConfirmedTx = function(txs) { + return lodash.filter(txs, function(tx) { + if (tx.confirmations >= root.SOFT_CONFIRMATION_LIMIT) + return tx; + tx.recent = true; + }); + } - var updateLocalTxHistory = function(wallet, cb) { + var processNewTxs = function(wallet, txs) { + var config = configService.getSync().wallet.settings; + var now = Math.floor(Date.now() / 1000); + var txHistoryUnique = {}; + var ret = []; + wallet.hasUnsafeConfirmed = false; + + lodash.each(txs, function(tx) { + tx = txFormatService.processTx(tx); + + // no future transactions... + if (tx.time > now) + tx.time = now; + + if (tx.confirmations >= root.SAFE_CONFIRMATIONS) { + tx.safeConfirmed = root.SAFE_CONFIRMATIONS + '+'; + } else { + tx.safeConfirmed = false; + wallet.hasUnsafeConfirmed = true; + } + + if (tx.note) { + delete tx.note.encryptedEditedByName; + delete tx.note.encryptedBody; + } + + if (!txHistoryUnique[tx.txid]) { + ret.push(tx); + txHistoryUnique[tx.txid] = true; + } else { + $log.debug('Ignoring duplicate TX in history: ' + tx.txid) + } + }); + + return ret; + }; + + var updateLocalTxHistory = function(wallet, progressFn, cb) { var FIRST_LIMIT = 5; var LIMIT = 50; var requestLimit = FIRST_LIMIT; var walletId = wallet.credentials.walletId; var config = configService.getSync().wallet.settings; + progressFn = progressFn || function() {}; + var fixTxsUnit = function(txs) { if (!txs || !txs[0] || !txs[0].amountStr) return; @@ -342,7 +346,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim $log.debug('Fixing Tx Cache Unit to:' + name) lodash.each(txs, function(tx) { - tx.amountStr = txFormatService.formatAmount(tx.amount) + name; + tx.amountStr = txFormatService.formatAmount(tx.amount) + name; tx.feeStr = txFormatService.formatAmount(tx.fees) + name; }); }; @@ -352,54 +356,33 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim fixTxsUnit(txsFromLocal); - var confirmedTxs = self.removeAndMarkSoftConfirmedTx(txsFromLocal); + var confirmedTxs = removeAndMarkSoftConfirmedTx(txsFromLocal); var endingTxid = confirmedTxs[0] ? confirmedTxs[0].txid : null; var endingTs = confirmedTxs[0] ? confirmedTxs[0].time : null; // First update - if (walletId == profileService.focusedClient.credentials.walletId) { - self.completeHistory = txsFromLocal; - setCompactTxHistory(); - } + wallet.completeHistory = txsFromLocal; - if (historyUpdateInProgress[walletId]) - return; - - historyUpdateInProgress[walletId] = true; - - function getNewTxs(newTxs, skip, i_cb) { + function getNewTxs(newTxs, skip, cb) { getTxsFromServer(wallet, skip, endingTxid, requestLimit, function(err, res, shouldContinue) { - if (err) return i_cb(err); + if (err) return cb(err); + + newTxs = newTxs.concat(processNewTxs(wallet, lodash.compact(res))); + + progressFn(newTxs); - newTxs = newTxs.concat(lodash.compact(res)); skip = skip + requestLimit; $log.debug('Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue); if (!shouldContinue) { - newTxs = self.processNewTxs(newTxs); $log.debug('Finished Sync: New / soft confirmed Txs: ' + newTxs.length); - return i_cb(null, newTxs); + return cb(null, newTxs); } requestLimit = LIMIT; - getNewTxs(newTxs, skip, i_cb); - - // Progress update - if (walletId == profileService.focusedClient.credentials.walletId) { - self.txProgress = newTxs.length; - if (self.completeHistory < FIRST_LIMIT && txsFromLocal.length == 0) { - $log.debug('Showing partial history'); - var newHistory = self.processNewTxs(newTxs); - newHistory = lodash.compact(newHistory.concat(confirmedTxs)); - self.completeHistory = newHistory; - setCompactTxHistory(); - } - $timeout(function() { - $rootScope.$apply(); - }); - } + getNewTxs(newTxs, skip, cb); }); }; @@ -445,9 +428,8 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim $log.debug('Tx History synced. Total Txs: ' + newHistory.length); // Final update - if (walletId == profileService.focusedClient.credentials.walletId) { - self.completeHistory = newHistory; - setCompactTxHistory(); + if (walletId == wallet.credentials.walletId) { + wallet.completeHistory = newHistory; } return storageService.setTxHistory(historyToSave, walletId, function() { @@ -461,30 +443,18 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }; - root.updateHistory = function(wallet) { + root.getTxHistory = function(wallet, opts, cb) { + opts = opts || {}; + var walletId = wallet.credentials.walletId; - if (!wallet.isComplete()) return; - + if (!wallet.isComplete()) return cb(); $log.debug('Updating Transaction History'); - self.txHistoryError = false; - self.updatingTxHistory = true; - $timeout(function() { - updateLocalTxHistory(wallet, function(err) { - historyUpdateInProgress[walletId] = self.updatingTxHistory = false; - self.loadingWallet = false; - self.txProgress = 0; - if (err) - self.txHistoryError = true; - - $timeout(function() { - self.newTx = false - }, 1000); - - $rootScope.$apply(); - }); + updateLocalTxHistory(wallet, opts.progressFn, function(err) { + if (err) return cb(err); + return cb(err, wallet.completeHistory); }); }; @@ -673,20 +643,11 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; - var setCompactTxHistory = function() { - - // TODO - self.isSearching = false; - self.nextTxHistory = self.historyShowMoreLimit; - self.txHistory = self.completeHistory ? self.completeHistory.slice(0, self.historyShowLimit) : null; - self.historyShowMore = self.completeHistory ? self.completeHistory.length > self.historyShowLimit : null; - }; - root.debounceUpdateHistory = lodash.debounce(function() { root.updateHistory(); }, 1000); - self.throttledUpdateHistory = lodash.throttle(function() { + root.throttledUpdateHistory = lodash.throttle(function() { root.updateHistory(); }, 5000); @@ -707,19 +668,18 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim root.recreate = function(wallet, cb) { ongoingProcess.set('recreating', true); wallet.recreateWallet(function(err) { - self.notAuthorized = false; + wallet.notAuthorized = false; ongoingProcess.set('recreating', false); if (err) { - self.handleError(err); - $rootScope.$apply(); + wallet.handleError(err); return; } profileService.bindWalletClient(wallet, { force: true }); - self.startScan(wallet); + wallet.startScan(wallet); }); }; @@ -727,16 +687,16 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim $log.debug('Scanning wallet ' + wallet.credentials.walletId); if (!wallet.isComplete()) return; - // self.updating = true; + // wallet.updating = true; wallet.startScan({ includeCopayerBranches: true, }, function(err) { // TODO - // if (err && self.walletId == walletId) { - // self.updating = false; - // self.handleError(err); + // if (err && wallet.walletId == walletId) { + // wallet.updating = false; + // wallet.handleError(err); // $rootScope.$apply(); // } }); @@ -911,14 +871,14 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim if (err) return cb(err); ongoingProcess.set('signingTx', true); - root.signTx(wallet, publishedTxp, function(err, signedTxp) { + root.signTx(wallet, publishedTxp, function(err, signedTxp) { ongoingProcess.set('signingTx', false); root.lock(wallet); if (err) { // TODO? //$scope.$emit('Local/TxProposalAction'); - var msg = err.message ? + var msg = err.message ? err.message : gettext('The payment was created but could not be completed. Please try again from home screen'); return cb(err);