diff --git a/public/views/walletDetails.html b/public/views/walletDetails.html index 7e4558cad..6082966b2 100644 --- a/public/views/walletDetails.html +++ b/public/views/walletDetails.html @@ -226,7 +226,7 @@ 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/walletDetails.js b/src/js/controllers/walletDetails.js index 87a802166..299c91cf6 100644 --- a/src/js/controllers/walletDetails.js +++ b/src/js/controllers/walletDetails.js @@ -85,9 +85,7 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun $scope.wallet = wallet; if (wallet) { - walletService.updateStatus(wallet, { - triggerTxUpdate: true - }, function(err, status) { + walletService.updateStatus(wallet, {}, function(err, status) { console.log(status); if (err) {} // TODO }); diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js index 43c55ac1f..76048874a 100644 --- a/src/js/services/walletService.js +++ b/src/js/services/walletService.js @@ -9,6 +9,8 @@ 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.HISTORY_SHOW_LIMIT = 10; @@ -107,90 +109,10 @@ 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) { + root.getStatus = function(wallet, opts, cb) { opts = opts || {}; - function updateBalance(cb) { - if (wallet.isValid && !opts.force) - return cb(); - }; - - function updateHistory() { - if (!opts.triggerTxUpdate) return; - $timeout(function() { - root.updateHistory(wallet); - }); - }; - - function getStatus(cb) { + function get(cb) { wallet.getStatus({ twoStep: true }, function(err, ret) { @@ -201,61 +123,112 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; - function walletStatusHash(walletStatus) { - var bal; - if (walletStatus) { - bal = walletStatus.balance.totalAmount; + function cacheBalance(wallet, balance) { + if (!balance) return; + + var config = configService.getSync().wallet.settings; + + // 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 { - bal = wallet.totalBalanceSat; + wallet.totalBalanceSat = balance.totalConfirmedAmount; + wallet.lockedBalanceSat = balance.lockedConfirmedAmount; + wallet.availableBalanceSat = balance.availableConfirmedAmount; + wallet.totalBytesToSendMax = balance.totalBytesToSendConfirmedMax; + wallet.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount; } - return bal; - }; - function doUpdate(initStatusHash, tries) { - if (wallet.isValid && !opts.force) return cb(); + // Selected unit + wallet.unitToSatoshi = config.unitToSatoshi; + wallet.satToUnit = 1 / wallet.unitToSatoshi; + wallet.unitName = config.unitName; - tries = tries || 0; + //STR + wallet.totalBalanceStr = root.formatAmount(wallet.totalBalanceSat) + ' ' + wallet.unitName; + wallet.lockedBalanceStr = root.formatAmount(wallet.lockedBalanceSat) + ' ' + wallet.unitName; + wallet.availableBalanceStr = root.formatAmount(wallet.availableBalanceSat) + ' ' + wallet.unitName; - $timeout(function() { - $log.debug('Updating Status:', wallet.credentials.walletName, tries); - getStatus(function(err, walletStatus) { - if (err) return cb(err); + if (wallet.pendingAmount) { + wallet.pendingAmountStr = root.formatAmount(wallet.pendingAmount) + ' ' + wallet.unitName; + } else { + wallet.pendingAmountStr = null; + } - var currentStatusHash = walletStatusHash(walletStatus); - $log.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries); - if (opts.untilItChanges && - initStatusHash == currentStatusHash && - tries < 7 && - walletId == wallet.credentials.walletId) { - return $timeout(function() { - $log.debug('Retrying update... ' + walletId + ' Try:' + tries) - return root.doUpdate(initStatusHash, ++tries, cb); - }, 1400 * tries); - } + wallet.alternativeName = config.alternativeName; + wallet.alternativeIsoCode = config.alternativeIsoCode; - $log.debug('Got Wallet Status for:' + wallet.credentials.walletName); + rateService.whenAvailable(function() { - root.setStatus(wallet, walletStatus); + var totalBalanceAlternative = rateService.toFiat(wallet.totalBalanceSat, wallet.alternativeIsoCode); + var lockedBalanceAlternative = rateService.toFiat(wallet.lockedBalanceSat, wallet.alternativeIsoCode); + var alternativeConversionRate = rateService.toFiat(100000000, wallet.alternativeIsoCode); - // wallet.setPendingTxps(walletStatus.pendingTxps); - return cb(); - }); + wallet.totalBalanceAlternative = $filter('formatFiatAmount')(totalBalanceAlternative); + wallet.lockedBalanceAlternative = $filter('formatFiatAmount')(lockedBalanceAlternative); + wallet.alternativeConversionRate = $filter('formatFiatAmount')(alternativeConversionRate); + + wallet.alternativeBalanceAvailable = true; + wallet.isRateAvailable = true; }); }; - // trigger history update now? - if (!opts.untilItChanges) updateHistory(); + function cacheStatus = (status) { + wallet.status = status; + wallet.statusUpdatedOn = Date.now(); + wallet.isValid = true; + cacheBalance(wallet, status.balance); + wallet.email = status.preferences.email; + }; - doUpdate(walletStatusHash(), 0, function(err) { + function walletStatusHash(status) { + return status ? status.balance.totalAmount : wallet.totalBalanceSat; + }; + + function _getStatus(initStatusHash, tries, cb) { + if (wallet.isValid && !opts.force) return cb(null, wallet.status); + + tries = tries || 0; + + $log.debug('Updating Status:', wallet.credentials.walletName, tries); + get(function(err, status) { + if (err) return cb(err); + + var currentStatusHash = walletStatusHash(status); + $log.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries); + 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 _getStatus(initStatusHash, ++tries, cb); + }, root.WALLET_STATUS_DELAY_BETWEEN_TRIES * tries); + } + + $log.debug('Got Wallet Status for:' + wallet.credentials.walletName); + + root.cacheStatus(wallet, status); + + // wallet.setPendingTxps(status.pendingTxps); + return cb(null, status); + }); + }; + + _getStatus(walletStatusHash(), 0, function(err, status) { if (err) { root.handleError(err); return cb(err); } - - if (opts.untilItChanges) updateHistory(); - return cb(); + return cb(null, status); }) - }; var getSavedTxs = function(walletId, cb) { @@ -363,7 +336,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; }); }; @@ -479,11 +452,10 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }; - root.updateHistory = function(wallet) { + root.updateHistory = function(wallet, cb) { var walletId = wallet.credentials.walletId; - if (!wallet.isComplete()) return; - + if (!wallet.isComplete()) return cb(); $log.debug('Updating Transaction History'); wallet.txHistoryError = false; @@ -499,7 +471,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim $timeout(function() { wallet.newTx = false - }, 1000); + }); }); }); @@ -927,7 +899,7 @@ 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);