diff --git a/i18n/po/template.pot b/i18n/po/template.pot index ebac52c11..2b1068ad4 100644 --- a/i18n/po/template.pot +++ b/i18n/po/template.pot @@ -3860,6 +3860,14 @@ msgstr "" msgid "This invoice is no longer accepting payments" msgstr "" +#: www/views/amount.html.js:60 +msgid "Send Maximum Amount" +msgstr "" + +#: src/js/controllers/amount.controller.js:239 +msgid "Unknown error." +msgstr "" + #: www/views/paperWallet.html:48 msgid "No Bitcoin Cash wallet to transfer funds to found." msgstr "" @@ -3878,6 +3886,7 @@ msgstr "" #: www/views/paperWallet.html:104 msgid "No Bitcoin Core found." +msgstr "" #: src/js/controllers/tab-scan.js:120 msgid "Scan Failed" diff --git a/src/js/app.js b/src/js/app.js index 503da9f52..62ff2e6f4 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -19,6 +19,7 @@ var modules = [ 'copayApp.controllers', 'copayApp.directives', 'copayApp.addons', + 'bitcoincom.controllers', 'bitcoincom.directives', 'bitcoincom.services' ]; @@ -30,5 +31,6 @@ angular.module('copayApp.services', []); angular.module('copayApp.controllers', []); angular.module('copayApp.directives', []); angular.module('copayApp.addons', []); +angular.module('bitcoincom.controllers', []); angular.module('bitcoincom.directives', []); angular.module('bitcoincom.services', []); diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.controller.js similarity index 69% rename from src/js/controllers/amount.js rename to src/js/controllers/amount.controller.js index c1cd9a118..07f31bb3e 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.controller.js @@ -1,16 +1,23 @@ 'use strict'; -angular.module('copayApp.controllers').controller('amountController', amountController); +(function(){ -function amountController(configService, $filter, gettextCatalog, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, profileService, walletService, $window, ongoingProcess, popupService) { +angular + .module('bitcoincom.controllers') + .controller('amountController', amountController); + +function amountController(configService, $filter, gettextCatalog, $ionicHistory, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, ongoingProcess, popupService, profileService, walletService, $window) { var vm = this; + // Variables vm.allowSend = false; vm.altCurrencyList = []; vm.alternativeAmount = ''; vm.alternativeUnit = ''; vm.amount = '0'; vm.availableFunds = ''; + vm.canSendAllAvailableFunds = true; + vm.errorMessage = ''; // Use insufficient for logic, as when the amount is invalid, funds being // either sufficent or insufficient doesn't make sense. vm.fundsAreInsufficient = false; @@ -20,9 +27,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ vm.lastUsedPopularList = []; vm.maxAmount = 0; vm.minAmount = 0; + vm.sendableFunds = ''; + vm.showSendMaxButton = false; + vm.showSendLimitMaxButton = false; vm.thirdParty = false; vm.unit = ''; + // Functions vm.changeUnit = changeUnit; vm.close = close; vm.findCurrency = findCurrency; @@ -35,7 +46,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ vm.removeDigit = removeDigit; vm.save = save; vm.sendMax = sendMax; - vm.errorMessage = ''; + $scope.$on('$ionicView.beforeEnter', onBeforeEnter); $scope.$on('$ionicView.leave', onLeave); @@ -46,10 +57,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ var altCurrencyModal = null; var altUnitIndex = 0; - var availableFundsInCrypto = ''; - var availableFundsInFiat = ''; - var availableSatoshis = null; var availableUnits = []; + var canSendMax = true; var fiatCode; var isNW = platformInfo.isNW; var isAndroid = platformInfo.isAndroid; @@ -57,10 +66,18 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ var lastUsedAltCurrencyList = []; var passthroughParams = {}; var satToUnit; + var transactionSendableAmount = { + crypto: '', + satoshis: null + }; var unitDecimals; var unitIndex = 0; var unitToSatoshi; var useSendMax = false; + var walletSpendableAmount = { + crypto: '', + satoshis: null + }; function onLeave() { angular.element($window).off('keydown'); @@ -81,40 +98,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ vm.minAmount = parseFloat(passthroughParams.minAmount); vm.maxAmount = parseFloat(passthroughParams.maxAmount); - if (passthroughParams.thirdParty) { - vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object - if (vm.thirdParty) { - if (vm.thirdParty.id === 'shapeshift') { - if (!vm.thirdParty.data) { - vm.thirdParty.data = {}; - } - vm.thirdParty.data['fromWalletId'] = vm.fromWalletId; - - vm.fromWallet = profileService.getWallet(vm.fromWalletId); - vm.toWallet = profileService.getWallet(vm.toWalletId); - - ongoingProcess.set('connectingShapeshift', true); - shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(err, data) { - - if (err) { - // Error stop here - ongoingProcess.set('connectingShapeshift', false); - popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.toString(), function () { - $ionicHistory.goBack(); - }); - } else { - vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); - vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); - ongoingProcess.set('connectingShapeshift', false); - } - }); - } - } - } - vm.isRequestingSpecificAmount = !passthroughParams.fromWalletId; + vm.showSendMaxButton = !vm.isRequestingSpecificAmount; var config = configService.getSync().wallet.settings; + unitToSatoshi = config.unitToSatoshi; + satToUnit = 1 / unitToSatoshi; + unitDecimals = config.unitDecimals; setAvailableUnits(); updateUnitUI(); @@ -146,10 +136,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ }); } - unitToSatoshi = config.unitToSatoshi; - satToUnit = 1 / unitToSatoshi; - unitDecimals = config.unitDecimals; - + resetAmount(); processAmount(); @@ -221,6 +208,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ var fromWallet = profileService.getWallet(passthroughParams.fromWalletId); updateAvailableFundsFromWallet(fromWallet); } + + if (passthroughParams.thirdParty) { + vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object + if (vm.thirdParty) { + initShapeshift(); + } + } } } @@ -228,6 +222,34 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ sendFlowService.router.goBack(); } + function initShapeshift() { + if (vm.thirdParty.id === 'shapeshift') { + vm.thirdParty.data = vm.thirdParty.data || {}; + + vm.fromWallet = profileService.getWallet(vm.fromWalletId); + vm.toWallet = profileService.getWallet(vm.toWalletId); + + vm.showSendMaxButton = false; + vm.showSendLimitMaxButton = false; + vm.canSendAllAvailableFunds = false; + + ongoingProcess.set('connectingShapeshift', true); + shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function onMarketData(err, data) { + ongoingProcess.set('connectingShapeshift', false); + if (err) { + // Error stop here + popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.message, function () { + goBack(); + }); + } else { + vm.thirdParty.data.minAmount = vm.minAmount = parseFloat(data.minimum); + vm.thirdParty.data.maxAmount = vm.maxAmount = parseFloat(data.maxLimit); + setMaximumButtonFromWallet(vm.fromWallet); + } + }); + } + } + function paste(value) { vm.amount = value; processAmount(); @@ -243,8 +265,28 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ } function sendMax() { - useSendMax = true; - finish(); + if (canSendMax) { + useSendMax = true; + finish(); + } else { + var transactionSendableAmountInUnits = transactionSendableAmount.satoshis * satToUnit; + if (vm.minAmount && transactionSendableAmountInUnits < vm.minAmount) { + popupService.showAlert( + gettextCatalog.getString('Insufficient funds'), + gettextCatalog.getString('Amount below minimum allowed') + ); + } else { + // Need to be precise, so use crypto directly rather than fiat with exchange rate + if (availableUnits[unitIndex].isFiat) { + var tempIndex = altUnitIndex; + altUnitIndex = unitIndex; + unitIndex = tempIndex; + } + vm.amount = transactionSendableAmountInUnits.toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); + useSendMax = true; + finish(); + } + } } function updateUnitUI() { @@ -364,8 +406,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ amountInCrypto = a; var amountInSatoshis = a * unitToSatoshi; vm.fundsAreInsufficient = !!passthroughParams.fromWalletId - && availableSatoshis !== null - && availableSatoshis < amountInSatoshis; + && walletSpendableAmount.satoshis !== null + && walletSpendableAmount.satoshis < amountInSatoshis; vm.alternativeAmount = txFormatService.formatAmount(amountInSatoshis, true); vm.allowSend = lodash.isNumber(a) @@ -385,8 +427,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ } else { amountInCrypto = result; vm.fundsAreInsufficient = passthroughParams.fromWalletId - && availableSatoshis !== null - && availableSatoshis < result * unitToSatoshi; + && walletSpendableAmount.satoshis !== null + && walletSpendableAmount.satoshis < result * unitToSatoshi; vm.alternativeAmount = $filter('formatFiatAmount')(toFiat(result)); vm.allowSend = lodash.isNumber(result) @@ -460,13 +502,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ var satoshis = 0; if (unit.isFiat) { - satoshis = (fromFiat(uiAmount) * unitToSatoshi).toFixed(0); + satoshis = Math.floor(fromFiat(uiAmount) * unitToSatoshi); } else { - satoshis = (uiAmount * unitToSatoshi).toFixed(0); + satoshis = Math.floor(uiAmount * unitToSatoshi); } var confirmData = { - amount: useSendMax ? undefined : satoshis, + amount: (useSendMax && canSendMax) ? undefined : satoshis, displayAddress: passthroughParams.displayAddress, fromWalletId: passthroughParams.fromWalletId, sendMax: useSendMax, @@ -482,7 +524,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ $state.transitionTo('tabs.paymentRequest.confirm', confirmData); } else { sendFlowService.goNext(confirmData); - $scope.useSendMax = null; + useSendMax = false; } } @@ -600,18 +642,73 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ } function updateAvailableFundsStringIfNeeded() { - if (passthroughParams.fromWalletId && availableSatoshis !== null) { - availableFundsInFiat = ''; - vm.availableFunds = availableFundsInCrypto; + if (passthroughParams.fromWalletId && walletSpendableAmount.satoshis !== null) { + vm.availableFunds = walletSpendableAmount.crypto; if (availableUnits[unitIndex].isFiat) { var coin = availableUnits[altUnitIndex].id; - txFormatService.formatAlternativeStr(coin, availableSatoshis, function formatCallback(formatted){ - if (formatted) { - availableFundsInFiat = formatted; + txFormatService.formatAlternativeStr(coin, walletSpendableAmount.satoshis, function formatCallback(formatted){ + if (formatted) { $scope.$apply(function() { - vm.availableFunds = availableFundsInFiat; + vm.availableFunds = formatted; + }); + } + }); + } + updateMaximumButtonIfNeeded(); + } + } + + function updateAvailableFundsFromWallet(wallet) { + console.log('amount updateAvailableFundsFromWallet()'); + var availableFundsInFiat = ''; + if (wallet.status && wallet.status.isValid) { + walletSpendableAmount.crypto = wallet.status.spendableBalanceStr; + walletSpendableAmount.satoshis = wallet.status.spendableAmount; + if (wallet.status.alternativeBalanceAvailable) { + availableFundsInFiat = wallet.status.spendableBalanceAlternative + ' ' + wallet.status.alternativeIsoCode; + } else { + availableFundsInFiat = ''; + } + + } else if (wallet.cachedStatus && wallet.cachedStatus.isValid) { + + if (wallet.cachedStatus.alternativeBalanceAvailable) { + availableFundsInFiat = wallet.cachedStatus.spendableBalanceAlternative + ' ' + wallet.cachedStatus.alternativeIsoCode; + } else { + availableFundsInFiat = ''; + } + walletSpendableAmount.crypto = wallet.cachedStatus.spendableBalanceStr; + walletSpendableAmount.satoshis = wallet.cachedStatus.spendableAmount; + + } else { + + walletSpendableAmount.crypto = ''; + walletSpendableAmount.satoshis = null; + } + + if (availableUnits[unitIndex].isFiat) { + vm.availableFunds = availableFundsInFiat || walletSpendableAmount.crypto; + } else { + vm.availableFunds = walletSpendableAmount.crypto; + } + + setMaximumButtonFromWallet(wallet); + } + + function updateMaximumButtonIfNeeded() { + console.log('sendmax updateMaximumButtonIfNeeded()'); + if (vm.showSendMaxButton || vm.showSendLimitMaxButton) { + transactionSendableAmount.fiat = ''; + vm.sendableFunds = transactionSendableAmount.crypto; + + if (availableUnits[unitIndex].isFiat) { + var coin = availableUnits[altUnitIndex].id; + txFormatService.formatAlternativeStr(coin, transactionSendableAmount.satoshis, function formatCallback(formatted){ + if (formatted) { + $scope.$apply(function onApply() { + vm.sendableFunds = formatted; }); } }); @@ -619,37 +716,59 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $ } } - function updateAvailableFundsFromWallet(wallet) { - if (wallet.status && wallet.status.isValid) { - availableFundsInCrypto = wallet.status.spendableBalanceStr; - availableSatoshis = wallet.status.spendableAmount; - if (wallet.status.alternativeBalanceAvailable) { - availableFundsInFiat = wallet.status.spendableBalanceAlternative + ' ' + wallet.status.alternativeIsoCode; + function setMaximumButtonFromWallet(wallet) { + console.log('sendmax setMaximumButtonFromWallet()'); + var minSatoshis = vm.minAmount * unitToSatoshi; + var maxSatoshis = vm.maxAmount * unitToSatoshi; + + if (minSatoshis > walletSpendableAmount.satoshis) { + console.log('sendmax Hiding max buttons as minimum is too high.'); + canSendMax = false; + vm.showSendMaxButton = true; + vm.showSendLimitMaxButton = false; + transactionSendableAmount.satoshis = walletSpendableAmount.satoshis; + + } else if (maxSatoshis) { + if (walletSpendableAmount.satoshis > maxSatoshis) { + console.log('sendmax Showing max limit button as available is greater than max limit.'); + canSendMax = false; + vm.showSendMaxButton = false; + vm.showSendLimitMaxButton = true; + transactionSendableAmount.satoshis = maxSatoshis; } else { - availableFundsInFiat = ''; + console.log('sendmax Showing sendmax as all available as less than max limit.'); + // Enabling send max here is a little dangerous, if they receive funds between pressing + // this and the calculation in the Review screen. + canSendMax = false; + vm.showSendMaxButton = true; + vm.showSendLimitMaxButton = false; + transactionSendableAmount.satoshis = walletSpendableAmount.satoshis; } - } else if (wallet.cachedStatus && wallet.status.isValid) { + } else { + console.log('sendmax Showing sendmax as all available because no limits.'); + canSendMax = true; + vm.showSendMaxButton = true; + vm.showSendLimitMaxButton = false; + transactionSendableAmount.satoshis = walletSpendableAmount.satoshis; + } - if (wallet.cachedStatus.alternativeBalanceAvailable) { - availableFundsInFiat = wallet.cachedStatus.spendableBalanceAlternative + ' ' + wallet.cachedStatus.alternativeIsoCode; - } else { - availableFundsInFiat = ''; + if (vm.showSendMaxButton || vm.showSendLimitMaxButton) { + console.log('sendmax Setting max button text'); + transactionSendableAmount.crypto = txFormatService.formatAmountStr(wallet.coin, transactionSendableAmount.satoshis); + vm.sendableFunds = transactionSendableAmount.crypto; + + if (availableUnits[unitIndex].isFiat) { + txFormatService.formatAlternativeStr(wallet.coin, transactionSendableAmount.satoshis, function onFormat(formatted){ + if (formatted) { + $scope.$apply(function onApply() { + vm.sendableFunds = formatted; + }); + } + }); } - availableFundsInCrypto = wallet.cachedStatus.spendableBalanceStr; - availableSatoshis = wallet.cachedStatus.spendableAmount; - - } else { - - availableFundsInFiat = ''; - availableFundsInCrypto = ''; - availableSatoshis = null; } - if (availableUnits[unitIndex].isFiat) { - vm.availableFunds = availableFundsInFiat || availableFundsInCrypto; - } else { - vm.availableFunds = availableFundsInCrypto; - } } } +})(); \ No newline at end of file diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index 20b403a4d..fdef97109 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -1,14 +1,20 @@ describe('amountController', function(){ var configCache, - configService, + configService, + gettextCatalog, $controller, $ionicHistory, $rootScope, + ongoingProcess, platformInfo, + popupService, profileService, rateService, sendFlowService, shapeshiftService, + txFormatService, + $scope, + $state, $stateParams; @@ -20,7 +26,7 @@ describe('amountController', function(){ configCache = { wallet: { settings: { - + unitToSatoshi: 100000000 } } }; @@ -33,20 +39,42 @@ describe('amountController', function(){ }); configService.getSync.and.returnValue(configCache); + gettextCatalog = jasmine.createSpyObj(['getString']); + gettextCatalog.getString.and.callFake(function(str){ return str; }); $ionicHistory = jasmine.createSpyObj(['backView']); + ongoingProcess = jasmine.createSpyObj(['set']); + platformInfo = { isChromeApp: false, isAndroid: false, isIos: true }; - + popupService = jasmine.createSpyObj(['showAlert']); profileService = jasmine.createSpyObj(['getWallet', 'getWallets']); - rateService = jasmine.createSpyObj(['fromFiat', 'whenAvailable']); - sendFlowService = jasmine.createSpyObj(['getStateClone']); - shapeshiftService = jasmine.createSpyObj(['shiftIt']); + rateService = jasmine.createSpyObj(['fromFiat', 'listAlternatives', 'updateRates', 'whenAvailable']); + sendFlowService = jasmine.createSpyObj(['getStateClone', 'pushState']); + shapeshiftService = jasmine.createSpyObj(['getMarketData']); + txFormatService = jasmine.createSpyObj(['formatAlternativeStr', 'formatAmountStr']); + txFormatService.formatAlternativeStr.and.callFake(function(coin, satoshis, cb) { + if (typeof satoshis !== "number") { + throw "satoshis in formatAlternativeStr() is not a number." + } + var units = satoshis / 100000000; + var formatted = (units * 10000).toFixed(2) + ' USD'; + cb(formatted); + }); + + txFormatService.formatAmountStr.and.callFake(function(coin, satoshis) { + if (typeof satoshis !== "number") { + throw "satoshis in formatAmountStr() is not a number." + } + return (satoshis * 100000000).toFixed(8) + ' ' + (coin || 'bch').toUpperCase(); + }); + + $state = jasmine.createSpyObj(['transitionTo']); $stateParams = {}; inject(function(_$controller_, _$rootScope_){ @@ -67,7 +95,10 @@ describe('amountController', function(){ $ionicHistory.backView.and.returnValue(backView); var wallet = { - + status: { + isValid: true, + spendableAmount: 123456 + } }; profileService.getWallet.and.returnValue(wallet); profileService.getWallets.and.returnValue([{}]); @@ -78,22 +109,22 @@ describe('amountController', function(){ var amountController = $controller('amountController', { configService: configService, - gettextCatalog: {}, + gettextCatalog: gettextCatalog, $ionicHistory: $ionicHistory, $ionicModal: {}, $ionicScrollDelegate: {}, nodeWebkitService: {}, - ongoingProcess: {}, + ongoingProcess: ongoingProcess, platformInfo: platformInfo, profileService: profileService, - popupService: {}, + popupService: popupService, rateService: rateService, $scope: $scope, sendFlowService: sendFlowService, shapeshiftService: shapeshiftService, $state: {}, $stateParams: $stateParams, - txFormatService: {}, + txFormatService: txFormatService, walletService: {} }); @@ -110,4 +141,464 @@ describe('amountController', function(){ //expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'); }); + + + describe('Shapeshift', function() { + var walletFrom; + var walletTo; + + beforeEach(function(){ + walletFrom = {}; + walletTo = {}; + + profileService.getWallet.and.callFake(function(walletId){ + if (walletId === '4cd7673e-7320-4dfa-86e5-d4edb51d460a') { + return walletFrom; + } else if (walletId === 'bf00af8f-0788-4b57-b30a-0390747407e9') { + return walletTo; + } else { + return null; + } + }); + + rateService.listAlternatives.and.returnValue([ + {name: "Australian Dollar", isoCode: "AUD"}, + {name: "United States Dollar", isoCode: "USD"} + ]); + + }); + + it ('with available balance below limit, shows sendMax for triggering alert', function() { + + walletFrom.coin = 'btc'; + walletFrom.status = { + isValid: true, + spendableAmount: 789 + }; + walletTo.coin = 'bch'; + + profileService.getWallets.and.returnValue([{}]); + rateService.fromFiat.and.returnValue(12); + + var $scope = $rootScope.$new(); + + var amountController = $controller('amountController', { + configService: configService, + gettextCatalog: gettextCatalog, + $ionicHistory: $ionicHistory, + $ionicModal: {}, + $ionicScrollDelegate: {}, + nodeWebkitService: {}, + ongoingProcess: ongoingProcess, + platformInfo: platformInfo, + profileService: profileService, + popupService: popupService, + rateService: rateService, + $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, + $state: $state, + $stateParams: $stateParams, + txFormatService: txFormatService, + walletService: {} + }); + + rateService.whenAvailable.and.callFake(function(cb){ + cb(); + }); + + var sendFlowState = { + amount: '', + displayAddress: null, + fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a', + sendMax: false, + thirdParty: { + id: 'shapeshift', + data: {}, + }, + toAddress: '', + toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9' + }; + + sendFlowService.getStateClone.and.returnValue(sendFlowState); + + var reqCoinIn = ''; + var reqCoinOut = ''; + shapeshiftService.getMarketData.and.callFake(function(coinIn, coinOut, cb){ + reqCoinIn = coinIn; + reqCoinOut = coinOut; + cb({ + maxLimit: '0.6846239', + minimum: '0.00013692' + }); + }); + + $scope.$emit('$ionicView.beforeEnter', {}); + + expect(rateService.updateRates.calls.any()).toEqual(true); + + expect(reqCoinIn).toBe('btc'); + expect(reqCoinOut).toBe('bch'); + + expect(amountController.maxAmount).toBe(0.68462390); + expect(amountController.minAmount).toBe(0.00013692); + + expect(amountController.showSendMaxButton).toEqual(true); + expect(amountController.showSendLimitMaxButton).toEqual(false); + + expect(amountController.sendableFunds).toEqual('0.08 USD'); + + // Now hit the Send Max button + amountController.sendMax(); + + expect(popupService.showAlert.calls.argsFor(0)[0]).toEqual('Insufficient funds'); + expect(popupService.showAlert.calls.argsFor(0)[1]).toEqual('Amount below minimum allowed'); + expect(sendFlowService.pushState.calls.any()).toEqual(false); + expect($state.transitionTo.calls.any()).toEqual(false); + }); + + it ('with available balance between limits, uses sendMax', function() { + + walletFrom.coin = 'btc'; + walletFrom.status = { + isValid: true, + spendableAmount: 456789 + }; + walletTo.coin = 'bch'; + + profileService.getWallets.and.returnValue([{}]); + rateService.fromFiat.and.returnValue(12); + + var $scope = $rootScope.$new(); + + var amountController = $controller('amountController', { + configService: configService, + gettextCatalog: {}, + $ionicHistory: $ionicHistory, + $ionicModal: {}, + $ionicScrollDelegate: {}, + nodeWebkitService: {}, + ongoingProcess: ongoingProcess, + platformInfo: platformInfo, + profileService: profileService, + popupService: {}, + rateService: rateService, + $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, + $state: $state, + $stateParams: $stateParams, + txFormatService: txFormatService, + walletService: {} + }); + + rateService.whenAvailable.and.callFake(function(cb){ + cb(); + }); + + var sendFlowState = { + amount: '', + displayAddress: null, + fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a', + sendMax: false, + thirdParty: { + id: 'shapeshift', + data: {}, + }, + toAddress: '', + toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9' + }; + + sendFlowService.getStateClone.and.returnValue(sendFlowState); + + var reqCoinIn = ''; + var reqCoinOut = ''; + shapeshiftService.getMarketData.and.callFake(function(coinIn, coinOut, cb){ + reqCoinIn = coinIn; + reqCoinOut = coinOut; + cb({ + maxLimit: '0.6846239', + minimum: '0.00013692' + }); + }); + + $scope.$emit('$ionicView.beforeEnter', {}); + + expect(rateService.updateRates.calls.any()).toEqual(true); + + expect(reqCoinIn).toBe('btc'); + expect(reqCoinOut).toBe('bch'); + + expect(amountController.maxAmount).toBe(0.68462390); + expect(amountController.minAmount).toBe(0.00013692); + + expect(amountController.showSendMaxButton).toEqual(true); + expect(amountController.showSendLimitMaxButton).toEqual(false); + + // Now hit the Send Max button + var pushedState = null; + sendFlowService.pushState.and.callFake(function (sendFlowState){ + pushedState = sendFlowState; + }); + + amountController.sendMax(); + + expect(pushedState.amount).toBeUndefined(); + expect(pushedState.fromWalletId).toEqual('4cd7673e-7320-4dfa-86e5-d4edb51d460a'); + expect(pushedState.sendMax).toEqual(true); + expect(pushedState.toWalletId).toEqual('bf00af8f-0788-4b57-b30a-0390747407e9'); + + expect(pushedState.thirdParty.id).toEqual('shapeshift'); + expect(pushedState.thirdParty.data.maxAmount).toEqual(0.6846239); + expect(pushedState.thirdParty.data.minAmount).toEqual(0.00013692); + + expect($state.transitionTo.calls.count()).toEqual(1); + expect($state.transitionTo.calls.argsFor(0)[0]).toEqual('tabs.send.review'); + }); + + it ('with available balance higher than max, uses send limit max instead of sendMax', function() { + + walletFrom.coin = 'btc'; + walletFrom.status = { + isValid: true, + spendableAmount: 123456789 + }; + walletTo.coin = 'bch'; + + profileService.getWallets.and.returnValue([{}]); + rateService.fromFiat.and.returnValue(12); // satoshis or coins? + + var $scope = $rootScope.$new(); + + var amountController = $controller('amountController', { + configService: configService, + gettextCatalog: {}, + $ionicHistory: $ionicHistory, + $ionicModal: {}, + $ionicScrollDelegate: {}, + nodeWebkitService: {}, + ongoingProcess: ongoingProcess, + platformInfo: platformInfo, + profileService: profileService, + popupService: {}, + rateService: rateService, + $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, + $state: $state, + $stateParams: $stateParams, + txFormatService: txFormatService, + walletService: {} + }); + + rateService.whenAvailable.and.callFake(function(cb){ + cb(); + }); + + var sendFlowState = { + amount: '', + displayAddress: null, + fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a', + sendMax: false, + thirdParty: { + id: 'shapeshift', + data: {}, + }, + toAddress: '', + toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9' + }; + + sendFlowService.getStateClone.and.returnValue(sendFlowState); + + var reqCoinIn = ''; + var reqCoinOut = ''; + shapeshiftService.getMarketData.and.callFake(function(coinIn, coinOut, cb){ + reqCoinIn = coinIn; + reqCoinOut = coinOut; + cb({ + maxLimit: '0.6846239', + minimum: '0.00013692' + }); + }); + + $scope.$emit('$ionicView.beforeEnter', {}); + + expect(rateService.updateRates.calls.any()).toEqual(true); + + expect(reqCoinIn).toBe('btc'); + expect(reqCoinOut).toBe('bch'); + + expect(amountController.maxAmount).toBe(0.6846239); + expect(amountController.minAmount).toBe(0.00013692); + + expect(amountController.showSendMaxButton).toEqual(false); + expect(amountController.showSendLimitMaxButton).toEqual(true); + + // Now hit the Send Max button + var pushedState = null; + sendFlowService.pushState.and.callFake(function (sendFlowState){ + pushedState = sendFlowState; + }); + + amountController.sendMax(); + + expect(pushedState.amount).toEqual(68462390); + expect(pushedState.fromWalletId).toEqual('4cd7673e-7320-4dfa-86e5-d4edb51d460a'); + expect(pushedState.sendMax).toEqual(false); + expect(pushedState.toWalletId).toEqual('bf00af8f-0788-4b57-b30a-0390747407e9'); + + expect(pushedState.thirdParty.id).toEqual('shapeshift'); + expect(pushedState.thirdParty.data.maxAmount).toEqual(0.6846239); + expect(pushedState.thirdParty.data.minAmount).toEqual(0.00013692); + + expect($state.transitionTo.calls.count()).toEqual(1); + expect($state.transitionTo.calls.argsFor(0)[0]).toEqual('tabs.send.review'); + }); + }); + + + describe('Wallet transfer', function() { + var walletFrom; + var walletTo; + + beforeEach(function(){ + walletFrom = {}; + walletTo = {}; + + profileService.getWallet.and.callFake(function(walletId){ + if (walletId === '4cd7673e-7320-4dfa-86e5-d4edb51d460a') { + return walletFrom; + } else if (walletId === 'bf00af8f-0788-4b57-b30a-0390747407e9') { + return walletTo; + } else { + return null; + } + }); + + rateService.listAlternatives.and.returnValue([ + {name: "Australian Dollar", isoCode: "AUD"}, + {name: "United States Dollar", isoCode: "USD"} + ]); + + }); + + it('wallet transfer send max.', function() { + + walletFrom.coin = 'btc'; + walletFrom.status = { + isValid: true, + spendableAmount: 123456789 + }; + + profileService.getWallets.and.returnValue([{}]); + + var $scope = $rootScope.$new(); + + var amountController = $controller('amountController', { + configService: configService, + gettextCatalog: gettextCatalog, + $ionicHistory: $ionicHistory, + $ionicModal: {}, + $ionicScrollDelegate: {}, + nodeWebkitService: {}, + ongoingProcess: ongoingProcess, + platformInfo: platformInfo, + profileService: profileService, + popupService: popupService, + rateService: rateService, + $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, + $state: $state, + $stateParams: $stateParams, + txFormatService: txFormatService, + walletService: {} + }); + + var sendFlowState = { + fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a', + toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9' + }; + + sendFlowService.getStateClone.and.returnValue(sendFlowState); + + $scope.$emit('$ionicView.beforeEnter', {}); + + expect(amountController.showSendMaxButton).toEqual(true); + expect(amountController.showSendLimitMaxButton).toEqual(false); + + expect(amountController.sendableFunds).toEqual('12345.68 USD'); + + // Now hit the Send Max button + var pushedState = null; + sendFlowService.pushState.and.callFake(function (sendFlowState){ + pushedState = sendFlowState; + }); + + amountController.sendMax(); + + expect(pushedState.amount).toBeUndefined(); + expect(pushedState.fromWalletId).toEqual('4cd7673e-7320-4dfa-86e5-d4edb51d460a'); + expect(pushedState.sendMax).toEqual(true); + expect(pushedState.toWalletId).toEqual('bf00af8f-0788-4b57-b30a-0390747407e9'); + + expect($state.transitionTo.calls.count()).toEqual(1); + expect($state.transitionTo.calls.argsFor(0)[0]).toEqual('tabs.send.review'); + }); + + + // This situation was seen in real life + it('wallet transfer with valid cached status only.', function() { + + walletFrom.coin = 'btc'; + walletFrom.status = { + isValid: false, + }; + walletFrom.cachedStatus = { + isValid: true, + spendableAmount: 5678 + }; + + profileService.getWallets.and.returnValue([{}]); + + var $scope = $rootScope.$new(); + + var amountController = $controller('amountController', { + configService: configService, + gettextCatalog: gettextCatalog, + $ionicHistory: $ionicHistory, + $ionicModal: {}, + $ionicScrollDelegate: {}, + nodeWebkitService: {}, + ongoingProcess: ongoingProcess, + platformInfo: platformInfo, + profileService: profileService, + popupService: popupService, + rateService: rateService, + $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, + $state: $state, + $stateParams: $stateParams, + txFormatService: txFormatService, + walletService: {} + }); + + var sendFlowState = { + fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a', + toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9' + }; + + sendFlowService.getStateClone.and.returnValue(sendFlowState); + + $scope.$emit('$ionicView.beforeEnter', {}); + + expect(amountController.showSendMaxButton).toEqual(true); + expect(amountController.showSendLimitMaxButton).toEqual(false); + + expect(amountController.sendableFunds).toEqual('0.57 USD'); + }); + + }); + }); \ No newline at end of file diff --git a/src/js/controllers/review.controller.js b/src/js/controllers/review.controller.js index c82838f7c..15f982f2f 100644 --- a/src/js/controllers/review.controller.js +++ b/src/js/controllers/review.controller.js @@ -78,11 +78,15 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit function onBeforeEnter(event, data) { - console.log('walletSelector onBeforeEnter sendflow ', sendFlowService.state); + console.log('review onBeforeEnter sendflow ', sendFlowService.state); defaults = configService.getDefaults(); sendFlowData = sendFlowService.state.getClone(); originWalletId = sendFlowData.fromWalletId; - satoshis = parseInt(sendFlowData.amount, 10); + if (typeof sendFlowData.amount === 'string') { + satoshis = parseInt(sendFlowData.amount, 10); + } else { + satoshis = sendFlowData.amount; + } toAddress = sendFlowData.toAddress; destinationWalletId = sendFlowData.toWalletId; diff --git a/src/js/services/send-flow-state.service.js b/src/js/services/send-flow-state.service.js index c19317515..960e6f306 100644 --- a/src/js/services/send-flow-state.service.js +++ b/src/js/services/send-flow-state.service.js @@ -11,7 +11,7 @@ angular var service = { // Variables state: { - amount: '', + amount: 0, displayAddress: null, fromWalletId: '', sendMax: false, @@ -67,7 +67,7 @@ angular $log.debug("send-flow-state clearCurrent()"); service.state = { - amount: '', + amount: 0, displayAddress: null, fromWalletId: '', sendMax: false, diff --git a/src/js/services/shapeshift.service.js b/src/js/services/shapeshift.service.js index 77f0de297..73410e478 100644 --- a/src/js/services/shapeshift.service.js +++ b/src/js/services/shapeshift.service.js @@ -57,7 +57,7 @@ angular function shiftIt(coinIn, coinOut, withdrawalAddress, returnAddress, amount, cb) { // Test if the amount is correct depending on the min and max if (!amount || typeof amount !== 'number') { - cb(new Error(gettextCatalog.getString('Amount is not defined')))); + cb(new Error(gettextCatalog.getString('Amount is not defined'))); } else if (amount < service.marketData.minimum) { cb(new Error(gettextCatalog.getString('Amount is below the minimun'))); } else if (amount > service.marketData.maxLimit) { diff --git a/www/views/amount.html b/www/views/amount.html index 939937be8..51397f3bf 100644 --- a/www/views/amount.html +++ b/www/views/amount.html @@ -53,11 +53,12 @@
-
+