diff --git a/src/js/controllers/confirm.js b/src/js/controllers/confirm.js index 105c51f7f..c9ce5943a 100644 --- a/src/js/controllers/confirm.js +++ b/src/js/controllers/confirm.js @@ -1,14 +1,37 @@ 'use strict'; angular.module('copayApp.controllers').controller('confirmController', function($rootScope, $scope, $interval, $filter, $timeout, $ionicScrollDelegate, gettextCatalog, walletService, platformInfo, lodash, configService, rateService, $stateParams, $window, $state, $log, profileService, bitcore, txFormatService, ongoingProcess, $ionicModal, popupService, $ionicHistory, $ionicConfig, payproService, feeService, bwcError) { - var cachedTxp = {}; - var feeLevel; - var feePerKb; - var toAmount; - var isChromeApp = platformInfo.isChromeApp; + var countDown = null; - var cachedSendMax = {}; - $scope.isCordova = platformInfo.isCordova; + var CONFIRM_LIMIT_USD = 20; + + var tx = {}; + + // Config Related values + var config = configService.getSync(); + var walletConfig = config.wallet; + var unitToSatoshi = walletConfig.settings.unitToSatoshi; + var unitDecimals = walletConfig.settings.unitDecimals; + var satToUnit = 1 / unitToSatoshi; + var configFeeLevel = walletConfig.settings.feeLevel ? walletConfig.settings.feeLevel : 'normal'; + + + // Platform info + var isChromeApp = platformInfo.isChromeApp; + var isCordova = platformInfo.isCordova; + + + function refresh() { + $timeout(function() { + $scope.$apply(); + }, 1); + } + + + $scope.showWalletSelector = function() { + $scope.walletSelector = true; + refresh(); + }; $scope.$on("$ionicView.beforeLeave", function(event, data) { $ionicConfig.views.swipeBackEnabled(true); @@ -18,305 +41,338 @@ angular.module('copayApp.controllers').controller('confirmController', function( $ionicConfig.views.swipeBackEnabled(false); }); + + function exitWithError(err) { + $log.info('Error setting wallet selector:' + err); + popupService.showAlert(gettextCatalog.getString(), bwcError.msg(err), function() { + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.send'); + }); + }; + + function setNoWallet(msg) { + $scope.wallet = null; + $scope.noWalletMessage = gettextCatalog.getString(msg); + $log.warn('Not ready to make the payment:' + msg); + $timeout(function() { + $scope.$apply(); + }); + }; + $scope.$on("$ionicView.beforeEnter", function(event, data) { - toAmount = data.stateParams.toAmount; - cachedSendMax = {}; + function setWalletSelector(network, minAmount, cb) { + + // no min amount? (sendMax) => look for no empty wallets + minAmount = minAmount || 1; + + $scope.wallets = profileService.getWallets({ + onlyComplete: true, + network: network + }); + + if (!$scope.wallets || !$scope.wallets.length) { + setNoWallet('No wallets available'); + return cb(); + } + + var filteredWallets = []; + var index = 0; + var walletsUpdated = 0; + + lodash.each($scope.wallets, function(w) { + walletService.getStatus(w, {}, function(err, status) { + if (err || !status) { + $log.error(err); + } else { + walletsUpdated++; + w.status = status; + + if (!status.availableBalanceSat) + $log.debug('No balance available in: ' + w.name); + + if (status.availableBalanceSat > minAmount) { + filteredWallets.push(w); + } + } + + if (++index == $scope.wallets.length) { + if (!walletsUpdated) + return cb('Could not update any wallet'); + + if (lodash.isEmpty(filteredWallets)) { + setNoWallet('Insufficent funds'); + } + $scope.wallets = lodash.clone(filteredWallets); + return cb(); + } + }); + }); + }; + + // Setup $scope + + // Grab stateParams + tx = { + toAmount: parseInt(data.stateParams.toAmount), + sendMax: data.stateParams.useSendMax == 'true' ? true : false, + toAddress: data.stateParams.toAddress, + description: data.stateParams.description, + paypro: data.stateParams.paypro, + + feeLevel: configFeeLevel, + spendUnconfirmed: walletConfig.spendUnconfirmed, + + // Vanity tx info (not in the real tx) + recipientType: data.stateParams.recipientType || null, + toName: data.stateParams.toName, + toEmail: data.stateParams.toEmail, + toColor: data.stateParams.toColor, + network: (new bitcore.Address(data.stateParams.toAddress)).network.name, + txp: {}, + }; + + + // Other Scope vars + $scope.isCordova = isCordova; $scope.showAddress = false; - $scope.useSendMax = data.stateParams.useSendMax == 'true' ? true : false; - $scope.recipientType = data.stateParams.recipientType || null; - $scope.toAddress = data.stateParams.toAddress; - $scope.toName = data.stateParams.toName; - $scope.toEmail = data.stateParams.toEmail; - $scope.toColor = data.stateParams.toColor; - $scope.description = data.stateParams.description; - $scope.paypro = data.stateParams.paypro; - $scope.insufficientFunds = false; - $scope.noMatchingWallet = false; - $scope.paymentExpired = { - value: false - }; - $scope.remainingTimeStr = { - value: null - }; - $scope.network = (new bitcore.Address($scope.toAddress)).network.name; - setFee(); - resetValues(); - setwallets(); - applyButtonText(); + + updateTx(tx, null, {}, function() { + + $scope.walletSelectorTitle = gettextCatalog.getString('Send from'); + + setWalletSelector(tx.network, tx.toAmount, function(err) { + if (err) { + return exitWithError('Could not update wallets'); + } + + if ($scope.wallets.length > 1) { + $scope.showWalletSelector(); + } else if ($scope.wallets.length) { + setWallet($scope.wallets[0], tx); + } + }); + + }); }); - function setFee(customFeeLevel, cb) { - feeService.getCurrentFeeValue($scope.network, customFeeLevel, function(err, currentFeePerKb) { - var config = configService.getSync().wallet; - var configFeeLevel = (config.settings && config.settings.feeLevel) ? config.settings.feeLevel : 'normal'; - feePerKb = currentFeePerKb; - feeLevel = customFeeLevel ? customFeeLevel : configFeeLevel; - $scope.feeLevel = feeService.feeOpts[feeLevel]; - if (cb) return cb(); + + function getSendMaxInfo(tx, wallet, cb) { + if (!tx.sendMax) return cb(); + + //ongoingProcess.set('retrievingInputs', true); + walletService.getSendMaxInfo(wallet, { + feePerKb: tx.feeRate, + excludeUnconfirmedUtxos: !tx.spendUnconfirmed, + returnInputs: true, + }, cb); + }; + + + function getTxp(tx, wallet, dryRun, cb) { + + // ToDo: use a credential's (or fc's) function for this + if (tx.description && !wallet.credentials.sharedEncryptingKey) { + var msg = gettextCatalog.getString('Could not add message to imported wallet without shared encrypting key'); + $log.warn(msg); + return setSendError(msg); + } + + if (tx.toAmount > Number.MAX_SAFE_INTEGER) { + var msg = gettextCatalog.getString('Amount too big'); + $log.warn(msg); + return setSendError(msg); + } + + var txp = {}; + + txp.outputs = [{ + 'toAddress': tx.toAddress, + 'amount': tx.toAmount, + 'message': tx.description + }]; + + if (tx.sendMaxInfo) { + txp.inputs = tx.sendMaxInfo.inputs; + txp.fee = tx.sendMaxInfo.fee; + } else { + txp.feeLevel = tx.feeLevel; + } + + txp.message = tx.description; + + if (tx.paypro) { + txp.payProUrl = tx.paypro.url; + } + txp.excludeUnconfirmedUtxos = !tx.spendUnconfirmed; + txp.dryRun = dryRun; + walletService.createTx(wallet, txp, function(err, ctxp) { + if (err) { + setSendError(err); + return cb(err); + } + return cb(null, ctxp); + }); + }; + + function updateTx(tx, wallet, opts, cb) { + + if (opts.clearCache) { + tx.txp = {}; + } + + $scope.tx = tx; + + function updateAmount() { + if (!tx.toAmount) return; + + // Amount + tx.amountStr = txFormatService.formatAmountStr(tx.toAmount); + tx.amountValueStr = tx.amountStr.split(' ')[0]; + tx.amountUnitStr = tx.amountStr.split(' ')[1]; + txFormatService.formatAlternativeStr(tx.toAmount, function(v) { + tx.alternativeAmountStr = v; + }); + } + + updateAmount(); + refresh(); + + feeService.getFeeRate(tx.network, tx.feeLevel, function(err, feeRate) { + if (err) return cb(err); + + tx.feeRate = feeRate; + tx.feeLevelName = feeService.feeOpts[tx.feeLevel]; + + // End of quick refresh, before wallet is selected. + if (!wallet) + return cb(); + + getSendMaxInfo(lodash.clone(tx), wallet, function(err, sendMaxInfo) { + if (err) { + var msg = gettextCatalog.getString('Error getting SendMax information'); + return setSendError(msg); + } + + if (sendMaxInfo) { + + $log.debug('Send max info', sendMaxInfo); + + if (tx.sendMax && sendMaxInfo.amount == 0) { + setNoWallet('Insufficent funds'); + popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Not enough funds for fee')); + return cb('no_funds'); + } + + tx.sendMaxInfo = sendMaxInfo; + tx.toAmount = tx.sendMaxInfo.amount; + updateAmount(); + showSendMaxWarning(sendMaxInfo); + } + refresh(); + + // txp already generated for this wallet? + if (tx.txp[wallet.id]) + return cb(); + + getTxp(lodash.clone(tx), wallet, opts.dryRun, function(err, txp) { + if (err) return cb(err); + + txp.feeStr = txFormatService.formatAmountStr(txp.fee); + txFormatService.formatAlternativeStr(txp.fee, function(v) { + txp.alternativeFeeStr = v; + }); + txp.feeRatePerStr = (txp.fee / (txp.amount + txp.fee) * 100).toFixed(2) + '%'; + + + tx.txp[wallet.id] = txp; + $log.debug('Confirm. TX Fully Updated for wallet:' + wallet.id, tx); + + return cb(); + }); + }); }); } function useSelectedWallet() { - if (!$scope.useSendMax) displayValues(); + + if (!$scope.useSendMax) { + showAmount(tx.toAmount); + } + $scope.onWalletSelect($scope.wallet); } - function applyButtonText(multisig) { - $scope.buttonText = $scope.isCordova ? gettextCatalog.getString('Slide') + ' ' : gettextCatalog.getString('Click') + ' '; + function setButtonText(isMultisig, isPayPro) { + $scope.buttonText = gettextCatalog.getString(isCordova ? 'Slide' : 'Click') + ' '; - if ($scope.paypro) { + if (isPayPro) { $scope.buttonText += gettextCatalog.getString('to pay'); - } else if (multisig) { + } else if (isMultisig) { $scope.buttonText += gettextCatalog.getString('to accept'); } else $scope.buttonText += gettextCatalog.getString('to send'); }; - function setwallets() { - $scope.wallets = profileService.getWallets({ - onlyComplete: true, - network: $scope.network - }); - - if (!$scope.wallets || !$scope.wallets.length) { - $scope.noMatchingWallet = true; - displayValues(); - $log.warn('No ' + $scope.network + ' wallets to make the payment'); - $timeout(function() { - $scope.$apply(); - }); - return; - } - - var filteredWallets = []; - var index = 0; - var enoughFunds = false; - var walletsUpdated = 0; - - lodash.each($scope.wallets, function(w) { - walletService.getStatus(w, {}, function(err, status) { - if (err || !status) { - $log.error(err); - } else { - walletsUpdated++; - w.status = status; - if (!status.availableBalanceSat) $log.debug('No balance available in: ' + w.name); - if (status.availableBalanceSat > toAmount) { - filteredWallets.push(w); - enoughFunds = true; - } - } - - if (++index == $scope.wallets.length) { - if (!lodash.isEmpty(filteredWallets)) { - $scope.wallets = lodash.clone(filteredWallets); - if ($scope.useSendMax) { - if ($scope.wallets.length > 1) - $scope.showWalletSelector(); - else { - $scope.wallet = $scope.wallets[0]; - $scope.getSendMaxInfo(); - } - } else initConfirm(); - } else { - - // Were we able to update any wallet? - if (walletsUpdated) { - if (!enoughFunds) $scope.insufficientFunds = true; - displayValues(); - $log.warn('No wallet available to make the payment'); - } else { - popupService.showAlert(gettextCatalog.getString('Could not update wallets'), bwcError.msg(err), function() { - $ionicHistory.nextViewOptions({ - disableAnimate: true, - historyRoot: true - }); - $ionicHistory.clearHistory(); - $state.go('tabs.send'); - }); - } - - } - $timeout(function() { - $scope.$apply(); - }); - } - }); - }); - }; $scope.toggleAddress = function() { $scope.showAddress = !$scope.showAddress; }; - var initConfirm = function() { - if ($scope.paypro) _paymentTimeControl($scope.paypro.expires); - displayValues(); - if ($scope.wallets.length > 1) $scope.showWalletSelector(); - else setWallet($scope.wallets[0]); - $timeout(function() { - $scope.$apply(); - }); - }; + function showSendMaxWarning(sendMaxInfo) { - function displayValues() { - toAmount = parseInt(toAmount); - $scope.amountStr = txFormatService.formatAmountStr(toAmount); - $scope.displayAmount = getDisplayAmount($scope.amountStr); - $scope.displayUnit = getDisplayUnit($scope.amountStr); - txFormatService.formatAlternativeStr(toAmount, function(v) { - $scope.alternativeAmountStr = v; - }); - }; - - function resetValues() { - $scope.displayAmount = $scope.displayUnit = $scope.fee = $scope.feeFiat = $scope.feeRateStr = $scope.alternativeAmountStr = $scope.insufficientFunds = $scope.noMatchingWallet = null; - $scope.showAddress = false; - }; - - $scope.getSendMaxInfo = function() { - resetValues(); - var config = configService.getSync().wallet; - - ongoingProcess.set('retrievingInputs', true); - walletService.getSendMaxInfo($scope.wallet, { - feePerKb: feePerKb, - excludeUnconfirmedUtxos: !config.spendUnconfirmed, - returnInputs: true, - }, function(err, resp) { - ongoingProcess.set('retrievingInputs', false); - if (err) { - popupService.showAlert(gettextCatalog.getString('Error'), err); - return; + function verifyExcludedUtxos() { + var warningMsg = []; + if (sendMaxInfo.utxosBelowFee > 0) { + warningMsg.push(gettextCatalog.getString("A total of {{amountBelowFeeStr}} were excluded. These funds come from UTXOs smaller than the network fee provided.", { + amountBelowFeeStr: txFormatService.formatAmountStr(sendMaxInfo.amountBelowFee) + })); } - if (resp.amount == 0) { - $scope.insufficientFunds = true; - popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Not enough funds for fee')); - return; + if (sendMaxInfo.utxosAboveMaxSize > 0) { + warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", { + amountAboveMaxSizeStr: txFormatService.formatAmountStr(sendMaxInfo.amountAboveMaxSize) + })); } + return warningMsg.join('\n'); + }; - $scope.sendMaxInfo = { - sendMax: true, - amount: resp.amount, - inputs: resp.inputs, - fee: resp.fee, - feePerKb: feePerKb, - }; - - cachedSendMax[$scope.wallet.id] = $scope.sendMaxInfo; - - var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", { - fee: txFormatService.formatAmountStr(resp.fee) - }); - var warningMsg = verifyExcludedUtxos(); - - if (!lodash.isEmpty(warningMsg)) - msg += '\n' + warningMsg; - - popupService.showAlert(null, msg, function() { - setSendMaxValues(resp); - - createTx($scope.wallet, true, function(err, txp) { - if (err) return; - cachedTxp[$scope.wallet.id] = txp; - apply(txp); - }); - }); - - function verifyExcludedUtxos() { - var warningMsg = []; - if (resp.utxosBelowFee > 0) { - warningMsg.push(gettextCatalog.getString("A total of {{amountBelowFeeStr}} were excluded. These funds come from UTXOs smaller than the network fee provided.", { - amountBelowFeeStr: txFormatService.formatAmountStr(resp.amountBelowFee) - })); - } - - if (resp.utxosAboveMaxSize > 0) { - warningMsg.push(gettextCatalog.getString("A total of {{amountAboveMaxSizeStr}} were excluded. The maximum size allowed for a transaction was exceeded.", { - amountAboveMaxSizeStr: txFormatService.formatAmountStr(resp.amountAboveMaxSize) - })); - } - return warningMsg.join('\n'); - }; + var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees.", { + fee: txFormatService.formatAmountStr(sendMaxInfo.fee) }); - }; + var warningMsg = verifyExcludedUtxos(); - function setSendMaxValues(data) { - resetValues(); - var config = configService.getSync().wallet; - var unitToSatoshi = config.settings.unitToSatoshi; - var satToUnit = 1 / unitToSatoshi; - var unitDecimals = config.settings.unitDecimals; + if (!lodash.isEmpty(warningMsg)) + msg += '\n' + warningMsg; - $scope.amountStr = txFormatService.formatAmountStr(data.amount, true); - $scope.displayAmount = getDisplayAmount($scope.amountStr); - $scope.displayUnit = getDisplayUnit($scope.amountStr); - $scope.fee = txFormatService.formatAmountStr(data.fee); - txFormatService.formatAlternativeStr(data.fee, function(v) { - $scope.feeFiat = v; - }); - toAmount = parseFloat((data.amount * satToUnit).toFixed(unitDecimals)); - txFormatService.formatAlternativeStr(data.amount, function(v) { - $scope.alternativeAmountStr = v; - }); - $scope.feeRateStr = (data.fee / (data.amount + data.fee) * 100).toFixed(2) + '%'; - $timeout(function() { - $scope.$apply(); - }); - }; - - $scope.$on('accepted', function(event) { - $scope.approve(); - }); - - $scope.showWalletSelector = function() { - $scope.walletSelectorTitle = gettextCatalog.getString('Send from'); - if (!$scope.useSendMax && ($scope.insufficientFunds || $scope.noMatchingWallet)) return; - $scope.showWallets = true; + popupService.showAlert(null, msg, function() {}); }; $scope.onWalletSelect = function(wallet) { - if ($scope.useSendMax) { - $scope.wallet = wallet; - if (cachedSendMax[wallet.id]) { - $log.debug('Send max cached for wallet:', wallet.id); - setSendMaxValues(cachedSendMax[wallet.id]); - return; - } - $scope.getSendMaxInfo(); - } else - setWallet(wallet); - - applyButtonText(wallet.credentials.m > 1); + setWallet(wallet, tx); }; - $scope.showDescriptionPopup = function() { + $scope.showDescriptionPopup = function(tx) { var message = gettextCatalog.getString('Add description'); var opts = { - defaultText: $scope.description + defaultText: tx.description }; popupService.showPrompt(null, message, opts, function(res) { - if (typeof res != 'undefined') $scope.description = res; + if (typeof res != 'undefined') tx.description = res; $timeout(function() { $scope.$apply(); }); }); }; - function getDisplayAmount(amountStr) { - return $scope.amountStr.split(' ')[0]; - }; - - function getDisplayUnit(amountStr) { - return $scope.amountStr.split(' ')[1]; - }; - function _paymentTimeControl(expirationTime) { - $scope.paymentExpired.value = false; + $scope.paymentExpired = false; setExpirationTime(); countDown = $interval(function() { @@ -334,12 +390,12 @@ angular.module('copayApp.controllers').controller('confirmController', function( var totalSecs = expirationTime - now; var m = Math.floor(totalSecs / 60); var s = totalSecs % 60; - $scope.remainingTimeStr.value = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2); + $scope.remainingTimeStr = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2); }; function setExpiredValues() { - $scope.paymentExpired.value = true; - $scope.remainingTimeStr.value = gettextCatalog.getString('Expired'); + $scope.paymentExpired = true; + $scope.remainingTimeStr = gettextCatalog.getString('Expired'); if (countDown) $interval.cancel(countDown); $timeout(function() { $scope.$apply(); @@ -347,31 +403,27 @@ angular.module('copayApp.controllers').controller('confirmController', function( }; }; - function setWallet(wallet, delayed) { - var stop; + /* sets a wallet on the UI, creates a TXPs for that wallet */ + + function setWallet(wallet, tx) { + $scope.wallet = wallet; - $scope.fee = $scope.txp = null; - if (stop) { - $timeout.cancel(stop); - stop = null; - } - if (cachedTxp[wallet.id]) { - apply(cachedTxp[wallet.id]); - } else { - stop = $timeout(function() { - createTx(wallet, true, function(err, txp) { - if (err) return; - cachedTxp[wallet.id] = txp; - apply(txp); - }); - }, delayed ? 2000 : 1); - } + setButtonText(wallet.credentials.m > 1, !!tx.paypro); + + if (tx.paypro) + _paymentTimeControl(tx.paypro.expires); + + updateTx(tx, wallet, { + dryRun: true + }, function(err) { + $timeout(function() { + $ionicScrollDelegate.resize(); + $scope.$apply(); + }, 10); + + }); - $timeout(function() { - $ionicScrollDelegate.resize(); - $scope.$apply(); - }, 10); }; var setSendError = function(msg) { @@ -382,75 +434,6 @@ angular.module('copayApp.controllers').controller('confirmController', function( popupService.showAlert(gettextCatalog.getString('Error at confirm'), bwcError.msg(msg)); }; - function apply(txp) { - $scope.fee = txFormatService.formatAmountStr(txp.fee); - txFormatService.formatAlternativeStr(txp.fee, function(v) { - $scope.feeFiat = v; - }); - $scope.txp = txp; - $scope.feeRateStr = (txp.fee / (txp.amount + txp.fee) * 100).toFixed(2) + '%'; - $timeout(function() { - $scope.$apply(); - }); - }; - - var createTx = function(wallet, dryRun, cb) { - var config = configService.getSync().wallet; - var currentSpendUnconfirmed = config.spendUnconfirmed; - var paypro = $scope.paypro; - var toAddress = $scope.toAddress; - var description = $scope.description; - var unitToSatoshi = config.settings.unitToSatoshi; - var unitDecimals = config.settings.unitDecimals; - - // ToDo: use a credential's (or fc's) function for this - if (description && !wallet.credentials.sharedEncryptingKey) { - var msg = gettextCatalog.getString('Could not add message to imported wallet without shared encrypting key'); - $log.warn(msg); - return setSendError(msg); - } - - if (toAmount > Number.MAX_SAFE_INTEGER) { - var msg = gettextCatalog.getString('Amount too big'); - $log.warn(msg); - return setSendError(msg); - } - - var txp = {}; - var amount; - - if ($scope.useSendMax) amount = parseFloat((toAmount * unitToSatoshi).toFixed(0)); - else amount = toAmount; - - txp.outputs = [{ - 'toAddress': toAddress, - 'amount': amount, - 'message': description - }]; - - if ($scope.sendMaxInfo) { - txp.inputs = $scope.sendMaxInfo.inputs; - txp.fee = $scope.sendMaxInfo.fee; - } else - txp.feeLevel = feeLevel; - - txp.message = description; - - if (paypro) { - txp.payProUrl = paypro.url; - } - txp.excludeUnconfirmedUtxos = !currentSpendUnconfirmed; - txp.dryRun = dryRun; - - walletService.createTx(wallet, txp, function(err, ctxp) { - if (err) { - setSendError(err); - return cb(err); - } - return cb(null, ctxp); - }); - }; - $scope.openPPModal = function() { $ionicModal.fromTemplateUrl('views/modals/paypro.html', { scope: $scope @@ -464,14 +447,11 @@ angular.module('copayApp.controllers').controller('confirmController', function( $scope.payproModal.hide(); }; - $scope.approve = function(onSendStatusChange) { + $scope.approve = function(tx, wallet, onSendStatusChange) { - var wallet = $scope.wallet; - if (!wallet) { - return; - } + if (!tx || !wallet) return; - if ($scope.paypro && $scope.paymentExpired.value) { + if ($scope.paymentExpired) { popupService.showAlert(null, gettextCatalog.getString('This bitcoin payment request has expired.')); $scope.sendStatus = ''; $timeout(function() { @@ -481,46 +461,54 @@ angular.module('copayApp.controllers').controller('confirmController', function( } ongoingProcess.set('creatingTx', true, onSendStatusChange); - createTx(wallet, false, function(err, txp) { + getTxp(lodash.clone(tx), wallet, false, function(err, txp) { ongoingProcess.set('creatingTx', false, onSendStatusChange); if (err) return; - var config = configService.getSync(); - var spendingPassEnabled = walletService.isEncrypted(wallet); - var touchIdEnabled = config.touchIdFor && config.touchIdFor[wallet.id]; - var isCordova = $scope.isCordova; - var bigAmount = parseFloat(txFormatService.formatToUSD(txp.amount)) > 20; - var message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', { - amountStr: $scope.amountStr, - name: wallet.name - }); - var okText = gettextCatalog.getString('Confirm'); - var cancelText = gettextCatalog.getString('Cancel'); + // confirm txs for more that 20usd, if not spending/touchid is enabled + function confirmTx(cb) { + if (walletService.isEncrypted(wallet)) + return cb(); - if (!spendingPassEnabled && !touchIdEnabled) { - if (isCordova) { - if (bigAmount) { - popupService.showConfirm(null, message, okText, cancelText, function(ok) { - if (!ok) { - $scope.sendStatus = ''; - $timeout(function() { - $scope.$apply(); - }); - return; - } - publishAndSign(wallet, txp, onSendStatusChange); - }); - } else publishAndSign(wallet, txp, onSendStatusChange); - } else { - popupService.showConfirm(null, message, okText, cancelText, function(ok) { - if (!ok) { - $scope.sendStatus = ''; - return; - } - publishAndSign(wallet, txp, onSendStatusChange); - }); + var amountUsd = parseFloat(txFormatService.formatToUSD(txp.amount)); + if (amountUsd <= CONFIRM_LIMIT_USD) + return cb(); + + var message = gettextCatalog.getString('Sending {{amountStr}} from your {{name}} wallet', { + amountStr: tx.amountStr, + name: wallet.name + }); + var okText = gettextCatalog.getString('Confirm'); + var cancelText = gettextCatalog.getString('Cancel'); + popupService.showConfirm(null, message, okText, cancelText, function(ok) { + return cb(!ok); + }); + }; + + function publishAndSign() { + if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { + $log.info('No signing proposal: No private key'); + + return walletService.onlyPublish(wallet, txp, function(err) { + if (err) setSendError(err); + }, onSendStatusChange); } - } else publishAndSign(wallet, txp, onSendStatusChange); + + walletService.publishAndSign(wallet, txp, function(err, txp) { + if (err) return setSendError(err); + }, onSendStatusChange); + }; + + confirmTx(function(nok) { + if (nok) { + $scope.sendStatus = ''; + $timeout(function() { + $scope.$apply(); + }); + return; + } + publishAndSign(); + }); }); }; @@ -543,68 +531,47 @@ angular.module('copayApp.controllers').controller('confirmController', function( $scope.statusChangeHandler = statusChangeHandler; - $scope.onConfirm = function() { - $scope.approve(statusChangeHandler); - }; - $scope.onSuccessConfirm = function() { - var previousView = $ionicHistory.viewHistory().backView && $ionicHistory.viewHistory().backView.stateName; - - $ionicHistory.nextViewOptions({ - disableAnimate: true - }); - $ionicHistory.removeBackView(); $scope.sendStatus = ''; - $ionicHistory.nextViewOptions({ disableAnimate: true, historyRoot: true }); - $ionicHistory.clearHistory(); $state.go('tabs.send').then(function() { + $ionicHistory.clearHistory(); $state.transitionTo('tabs.home'); }); }; - function publishAndSign(wallet, txp, onSendStatusChange) { + $scope.chooseFeeLevel = function(tx, wallet) { - if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { - $log.info('No signing proposal: No private key'); + var scope = $rootScope.$new(true); + scope.network = tx.network; + scope.feeLevel = tx.feeLevel; + scope.noSave = true; - return walletService.onlyPublish(wallet, txp, function(err) { - if (err) setSendError(err); - }, onSendStatusChange); - } - - walletService.publishAndSign(wallet, txp, function(err, txp) { - if (err) return setSendError(err); - }, onSendStatusChange); - }; - - $scope.chooseFeeLevel = function() { - - $scope.customFeeLevel = feeLevel; $ionicModal.fromTemplateUrl('views/modals/chooseFeeLevel.html', { - scope: $scope, + scope: scope, }).then(function(modal) { - $scope.chooseFeeLevelModal = modal; - $scope.openModal(); + scope.chooseFeeLevelModal = modal; + scope.openModal(); }); - $scope.openModal = function() { - $scope.chooseFeeLevelModal.show(); + scope.openModal = function() { + scope.chooseFeeLevelModal.show(); }; - $scope.hideModal = function(customFeeLevel) { - if (customFeeLevel) { - cachedTxp = {}; - cachedSendMax = {}; - ongoingProcess.set('gettingFeeLevels', true); - setFee(customFeeLevel, function() { - ongoingProcess.set('gettingFeeLevels', false); - resetValues(); - if ($scope.wallet) useSelectedWallet(); - }) - } - $scope.chooseFeeLevelModal.hide(); + + scope.hideModal = function(customFeeLevel) { + $log.debug('Custom fee level choosen:' + customFeeLevel + ' was:' + tx.feeLevel); + if (tx.feeLevel == customFeeLevel) + scope.chooseFeeLevelModal.hide(); + + tx.feeLevel = customFeeLevel; + updateTx(tx, wallet, { + clearCache: true, + dryRun: true, + }, function() { + scope.chooseFeeLevelModal.hide(); + }); }; }; diff --git a/src/js/controllers/paperWallet.js b/src/js/controllers/paperWallet.js index 542a3a97d..321bb665f 100644 --- a/src/js/controllers/paperWallet.js +++ b/src/js/controllers/paperWallet.js @@ -60,7 +60,7 @@ angular.module('copayApp.controllers').controller('paperWalletController', $scope.wallet.buildTxFromPrivateKey($scope.privateKey, destinationAddress, null, function(err, testTx) { if (err) return cb(err); var rawTxLength = testTx.serialize().length; - feeService.getCurrentFeeValue('livenet', null, function(err, feePerKB) { + feeService.getCurrentFeeRate('livenet', null, function(err, feePerKB) { var opts = {}; opts.fee = Math.round((feePerKB * rawTxLength) / 2000); $scope.wallet.buildTxFromPrivateKey($scope.privateKey, destinationAddress, opts, function(err, tx) { diff --git a/src/js/controllers/preferencesFee.js b/src/js/controllers/preferencesFee.js index 6ac32a1bf..c08932cd2 100644 --- a/src/js/controllers/preferencesFee.js +++ b/src/js/controllers/preferencesFee.js @@ -2,13 +2,14 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', function($scope, $timeout, $ionicHistory, lodash, gettextCatalog, configService, feeService, ongoingProcess, popupService) { - $scope.save = function(newFee) { + var network; - if ($scope.customFeeLevel) { - $scope.currentFeeLevel = newFee; - updateCurrentValues(); + $scope.save = function(newFee) { + $scope.currentFeeLevel = newFee; + updateCurrentValues(); + + if ($scope.noSave) return; - } var opts = { wallet: { @@ -20,8 +21,6 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu configService.set(opts, function(err) { if (err) $log.debug(err); - $scope.currentFeeLevel = newFee; - updateCurrentValues(); $timeout(function() { $scope.$apply(); }); @@ -33,8 +32,10 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu }); $scope.init = function() { + + $scope.network = $scope.network || 'livenet'; $scope.feeOpts = feeService.feeOpts; - $scope.currentFeeLevel = $scope.customFeeLevel ? $scope.customFeeLevel : feeService.getCurrentFeeLevel(); + $scope.currentFeeLevel = $scope.feeLevel || feeService.getCurrentFeeLevel(); $scope.loadingFee = true; feeService.getFeeLevels(function(err, levels) { $scope.loadingFee = false; @@ -51,16 +52,19 @@ angular.module('copayApp.controllers').controller('preferencesFeeController', fu var updateCurrentValues = function() { if (lodash.isEmpty($scope.feeLevels) || lodash.isEmpty($scope.currentFeeLevel)) return; - var feeLevelValue = lodash.find($scope.feeLevels['livenet'], { + + var value = lodash.find($scope.feeLevels[$scope.network], { level: $scope.currentFeeLevel }); - if (lodash.isEmpty(feeLevelValue)) { + + if (lodash.isEmpty(value)) { $scope.feePerSatByte = null; $scope.avgConfirmationTime = null; return; } - $scope.feePerSatByte = (feeLevelValue.feePerKB / 1000).toFixed(); - $scope.avgConfirmationTime = feeLevelValue.nbBlocks * 10; + + $scope.feePerSatByte = (value.feePerKB / 1000).toFixed(); + $scope.avgConfirmationTime = value.nbBlocks * 10; }; $scope.chooseNewFee = function() { diff --git a/src/js/directives/clickToAccept.js b/src/js/directives/clickToAccept.js index 2127075dd..bb31b45b4 100644 --- a/src/js/directives/clickToAccept.js +++ b/src/js/directives/clickToAccept.js @@ -8,9 +8,7 @@ angular.module('copayApp.directives') transclude: true, scope: { sendStatus: '=clickSendStatus', - hasWalletChosen: '=hasWalletChosen', - insufficientFunds: '=insufficientFunds', - noMatchingWallet: '=noMatchingWallet' + isDisabled: '=isDisabled', }, link: function(scope, element, attrs) { scope.$watch('sendStatus', function() { diff --git a/src/js/directives/noLowFee.js b/src/js/directives/noLowFee.js index b332272e5..68046056b 100644 --- a/src/js/directives/noLowFee.js +++ b/src/js/directives/noLowFee.js @@ -12,7 +12,7 @@ angular.module('copayApp.directives') elem.bind('click', function() { configService.whenAvailable(function(config) { - if (config.wallet.settings.feeLevel.match(/conomy/)) { + if (config.wallet.settings.feeLevel && config.wallet.settings.feeLevel.match(/conomy/)) { $log.debug('Economy Fee setting... disabling link:' + elem.text()); popupService.showAlert('Low Fee Error', 'Please change your Bitcoin Network Fee Policy setting to Normal or higher to use this service', function() { $ionicHistory.goBack(); diff --git a/src/js/directives/slideToAccept.js b/src/js/directives/slideToAccept.js index e57aaecff..3f4db0ef9 100644 --- a/src/js/directives/slideToAccept.js +++ b/src/js/directives/slideToAccept.js @@ -9,7 +9,7 @@ angular.module('copayApp.directives') scope: { sendStatus: '=slideSendStatus', onConfirm: '&slideOnConfirm', - wallet: '=hasWalletChosen' + isDisabled: '=isDisabled' }, link: function(scope, element, attrs) { diff --git a/src/js/services/bwcError.js b/src/js/services/bwcError.js index 9740af88e..417350868 100644 --- a/src/js/services/bwcError.js +++ b/src/js/services/bwcError.js @@ -110,7 +110,7 @@ angular.module('copayApp.services') body = gettextCatalog.getString('Amount below minimum allowed'); break; case 'INCORRECT_ADDRESS_NETWORK': - body = gettextCatalog.getString('Incorrect address network'); + body = gettextCatalog.getString('Incorrect network address'); break; case 'COPAYER_REGISTERED': body = gettextCatalog.getString('Key already associated with an existing wallet'); diff --git a/src/js/services/feeService.js b/src/js/services/feeService.js index 889ffb83e..c88b867ca 100644 --- a/src/js/services/feeService.js +++ b/src/js/services/feeService.js @@ -1,8 +1,10 @@ 'use strict'; -angular.module('copayApp.services').factory('feeService', function($log, $stateParams, bwcService, walletService, configService, gettext, lodash, txFormatService, gettextCatalog) { +angular.module('copayApp.services').factory('feeService', function($log, $timeout, $stateParams, bwcService, walletService, configService, gettext, lodash, txFormatService, gettextCatalog) { var root = {}; + var CACHE_TIME_TS = 60; // 1 min + // Constant fee options to translate root.feeOpts = { urgent: gettext('Urgent'), @@ -12,22 +14,26 @@ angular.module('copayApp.services').factory('feeService', function($log, $stateP superEconomy: gettext('Super Economy') }; + var cache = { + updateTs: 0, + }; + root.getCurrentFeeLevel = function() { return configService.getSync().wallet.settings.feeLevel || 'normal'; }; - root.getCurrentFeeValue = function(network, customFeeLevel, cb) { - network = network || 'livenet'; - var feeLevel = customFeeLevel || root.getCurrentFeeLevel(); - root.getFeeLevels(function(err, levels) { + root.getFeeRate = function(network, feeLevel, cb) { + network = network || 'livenet'; + + root.getFeeLevels(function(err, levels, fromCache) { if (err) return cb(err); - var feeLevelValue = lodash.find(levels[network], { + var feeLevelRate = lodash.find(levels[network], { level: feeLevel }); - if (!feeLevelValue || !feeLevelValue.feePerKB) { + if (!feeLevelRate || !feeLevelRate.feePerKB) { return cb({ message: gettextCatalog.getString("Could not get dynamic fee for level: {{feeLevel}}", { feeLevel: feeLevel @@ -35,14 +41,26 @@ angular.module('copayApp.services').factory('feeService', function($log, $stateP }); } - var fee = feeLevelValue.feePerKB; - $log.debug('Dynamic fee: ' + feeLevel + ' ' + fee + ' SAT'); + var feeRate = feeLevelRate.feePerKB; - return cb(null, fee); + if (!fromCache) $log.debug('Dynamic fee: ' + feeLevel + '/' + network +' ' + (feeLevelRate.feePerKB / 1000).toFixed() + ' SAT/B'); + + return cb(null, feeRate); }); }; + root.getCurrentFeeRate = function(network, cb) { + return root.getFeeRate(network, root.getCurrentFeeLevel(), cb); + }; + root.getFeeLevels = function(cb) { + + if (cache.updateTs > Date.now() - CACHE_TIME_TS * 1000 ) { + $timeout( function() { + return cb(null, cache.data, true); + }, 1); + } + var walletClient = bwcService.getClient(); var unitName = configService.getSync().wallet.settings.unitName; @@ -51,10 +69,14 @@ angular.module('copayApp.services').factory('feeService', function($log, $stateP if (errLivenet || errTestnet) { return cb(gettextCatalog.getString('Could not get dynamic fee')); } - return cb(null, { + + cache.updateTs =Date.now(); + cache.data = { 'livenet': levelsLivenet, 'testnet': levelsTestnet - }); + }; + + return cb(null, cache.data); }); }); }; diff --git a/src/js/services/sendMax.js b/src/js/services/sendMax.js index fa716e260..142679f2a 100644 --- a/src/js/services/sendMax.js +++ b/src/js/services/sendMax.js @@ -10,7 +10,7 @@ angular.module('copayApp.services').service('sendMaxService', function(feeServic * */ this.getInfo = function(wallet, cb) { - feeService.getCurrentFeeValue(wallet.credentials.network, null, function(err, feePerKb) { + feeService.getCurrentFeeRate(wallet.credentials.network, null, function(err, feePerKb) { if (err) return cb(err); var config = configService.getSync().wallet; diff --git a/www/views/buyAmazon.html b/www/views/buyAmazon.html index 03e2e4063..04440fa9e 100644 --- a/www/views/buyAmazon.html +++ b/www/views/buyAmazon.html @@ -53,23 +53,17 @@ + is-disabled="!wallet"> Confirm purchase + is-disabled="!wallet"> Slide to buy
{{amountUnitStr}}
- {{buyRequestInfo.amount.amount}} {{buyRequestInfo.amount.currency}} + {{buyRequestInfo.amount.amount}} {{buyRequestInfo.amount.currency}} @ ${{buyPrice.amount}} per BTC
@@ -59,7 +59,7 @@ {{fee.type}} fee - {{fee.amount.amount}} {{fee.amount.currency}} + {{fee.amount.amount}} {{fee.amount.currency}}
@@ -74,23 +74,17 @@ + is-disabled="!selectedPaymentMethodId.value || !buyRequestInfo || !wallet"> Confirm purchase + is-disabled="!selectedPaymentMethodId.value || !buyRequestInfo || !wallet"> Slide to buy
{{amountUnitStr}}
- {{buyInfo.subtotal}} {{buyInfo.currency}} - {{buyInfo.qty}} BTC + {{buyInfo.subtotal}} {{buyInfo.currency}} + {{buyInfo.qty}} BTC @ ${{buyInfo.price}} per BTC
@@ -64,23 +64,17 @@ + is-disabled="!buyInfo || !wallet"> Confirm purchase + is-disabled="!buyInfo || !wallet"> Slide to buy - +
- Sending - Sending maximum amount + Sending + Sending maximum amount
-
{{displayAmount || '...'}} {{displayUnit}}
-
{{alternativeAmountStr || '...'}}
+
{{tx.amountValueStr || '...'}} {{tx.amountUnitStr}}
+
{{tx.alternativeAmountStr || '...'}}
-
+
Payment Expires: - {{remainingTimeStr.value}} - Expired + {{remainingTimeStr}} + Expired
@@ -32,36 +32,36 @@ -
- - {{toName}} +
+ + {{tx.toName}}
-
- - - {{paypro.domain || paypro.toAddress}} - {{toName}} +
+ + + {{tx.paypro.domain || tx.paypro.toAddress}} + {{tx.toName}}
-
-
- - {{toName}} +
+ + {{tx.toName}}
- - {{toName}} - {{toAddress}} + + {{tx.toName}} + {{tx.toAddress}}
- + From
@@ -77,46 +77,39 @@
-
- {{'Fee:' | translate}} {{feeLevel | translate}} - {{fee || '...'}} +
+ {{'Fee:' | translate}} {{tx.feeLevelName | translate}} + {{tx.txp[wallet.id].feeStr || '...'}} - {{feeFiat || '...'}} - {{feeRateStr}} of the transaction + {{tx.txp[wallet.id].alternativeFeeStr || '...'}} - {{tx.txp[wallet.id].feeRatePerStr}} of the transaction
- + Add Memo - {{description}} + {{tx.description}} -
- No wallets available -
-
- Insufficient funds +
+ {{noWalletMessage}}
+ is-disabled="!wallet"> {{buttonText}} + is-disabled="!wallet"> {{buttonText}} diff --git a/www/views/includes/clickToAccept.html b/www/views/includes/clickToAccept.html index 06807ce14..4aa4aea16 100644 --- a/www/views/includes/clickToAccept.html +++ b/www/views/includes/clickToAccept.html @@ -1,4 +1,4 @@ -
- +
@@ -20,6 +20,7 @@ {{feePerSatByte}} satoshis/byte ...
+
[{{network}}]
@@ -28,7 +29,10 @@
- +
diff --git a/www/views/modals/paypro.html b/www/views/modals/paypro.html index 78dd4380a..18e3ae670 100644 --- a/www/views/modals/paypro.html +++ b/www/views/modals/paypro.html @@ -9,51 +9,51 @@
-
{{displayAmount || '...'}} {{displayUnit}}
-
{{alternativeAmountStr || '...'}}
+
{{tx.amountValueStr || '...'}} {{tx.amountUnitStr}}
+
{{tx.alternativeAmountStr || '...'}}
-
+
{{'Pay To'|translate}} - {{paypro.domain}} + {{tx.paypro.domain}}
-
+
{{'Address'|translate}} - {{paypro.toAddress}} + {{tx.paypro.toAddress}}
{{'Certified by'|translate}} - + - {{paypro.caName}} {{'(Trusted)' | translate}} + {{tx.paypro.caName}} {{'(Trusted)' | translate}} - - + + Self-signed Certificate - - {{paypro.caName}}
+ + {{tx.paypro.caName}}
WARNING: UNTRUSTED CERTIFICATE
-
+
{{'Memo'|translate}} - {{paypro.memo}} + {{tx.paypro.memo}}
-
+
{{'Expires'|translate}} - {{paypro.expires * 1000 | amTimeAgo }} + {{tx.paypro.expires * 1000 | amTimeAgo }}
diff --git a/www/views/sellCoinbase.html b/www/views/sellCoinbase.html index dffd935fe..5827efda6 100644 --- a/www/views/sellCoinbase.html +++ b/www/views/sellCoinbase.html @@ -17,11 +17,11 @@
{{amountUnitStr}}
- {{sellRequestInfo.amount.amount}} {{sellRequestInfo.amount.currency}} + {{sellRequestInfo.amount.amount}} {{sellRequestInfo.amount.currency}} @ ${{sellPrice.amount}} per BTC
-
+
@@ -62,16 +62,16 @@ will be sent to your Coinbase account, and sold when Coinbase accepts the transaction (usually one hour).
-
Estimated sale value: +
Estimated sale value: - {{sellRequestInfo.total.amount | currency : '' : 2}} + {{sellRequestInfo.total.amount | currency : '' : 2}} {{sellRequestInfo.total.currency}}
-
Still sell if price fall until: +
Still sell if price fall until: - {{(sellRequestInfo.total.amount - - (selectedPriceSensitivity.data.value / 100) * sellRequestInfo.total.amount) | currency : '' : 2}} + {{(sellRequestInfo.total.amount - + (selectedPriceSensitivity.data.value / 100) * sellRequestInfo.total.amount) | currency : '' : 2}} {{sellRequestInfo.total.currency}}
@@ -107,23 +107,17 @@ + is-disabled="!selectedPaymentMethodId.value || !sellRequestInfo || !wallet"> Confirm sale + is-disabled="!selectedPaymentMethodId.value || !sellRequestInfo || !wallet"> Slide to sell
{{amountUnitStr}}
- {{sellInfo.subtotal}} {{sellInfo.currency}} + {{sellInfo.subtotal}} {{sellInfo.currency}} {{sellInfo.qty}} BTC @ ${{sellInfo.price}} per BTC
-
+
@@ -64,23 +64,17 @@ + is-disabled="!sellInfo || !wallet"> Confirm sale + is-disabled="!sellInfo || !wallet"> Slide to sell + is-disabled="!cardInfo || !wallet"> Add funds + is-disabled="!cardInfo || !wallet"> Slide to confirm