diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 8d1f65058..2d5a0aa66 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -2,9 +2,10 @@ angular.module('copayApp.controllers').controller('amountController', amountController); -function amountController(configService, $filter, gettextCatalog, $ionicHistory, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, ongoingProcess, profileService, walletService, $window) { +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 = ''; @@ -12,6 +13,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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; @@ -21,6 +23,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.lastUsedPopularList = []; vm.maxAmount = 0; vm.minAmount = 0; + vm.sendableFunds = ''; vm.showSendMaxButton = false; vm.showSendLimitMaxButton = false; vm.thirdParty = false; @@ -38,9 +41,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.pushDigit = pushDigit; vm.removeDigit = removeDigit; vm.save = save; - vm.sendableFunds = ''; vm.sendMax = sendMax; - vm.errorMessage = ''; + $scope.$on('$ionicView.beforeEnter', onBeforeEnter); $scope.$on('$ionicView.leave', onLeave); @@ -219,11 +221,10 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function initForShapeshift() { + console.log('initForShapeshift()'); if (vm.thirdParty.id === 'shapeshift') { vm.thirdParty.data = vm.thirdParty.data || {}; - vm.thirdParty.data['fromWalletId'] = vm.fromWalletId; - vm.fromWallet = profileService.getWallet(vm.fromWalletId); vm.toWallet = profileService.getWallet(vm.toWalletId); @@ -233,6 +234,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, ongoingProcess.set('connectingShapeshift', true); shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function onMarketData(data) { + console.log('sendmax onMarketData()'); ongoingProcess.set('connectingShapeshift', false); vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); @@ -261,17 +263,27 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, function sendMax() { if (canSendMax) { useSendMax = true; - + finish(); + } 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; + 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); + finish(); + } - vm.amount = (transactionSendableAmount.satoshis * satToUnit).toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); } - finish(); } function updateUnitUI() { @@ -647,6 +659,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function updateAvailableFundsFromWallet(wallet) { + console.log('amount updateAvailableFundsFromWallet()'); var availableFundsInFiat = ''; if (wallet.status && wallet.status.isValid) { walletSpendableAmount.crypto = wallet.status.spendableBalanceStr; @@ -702,15 +715,16 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } 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 = false; + vm.showSendMaxButton = true; vm.showSendLimitMaxButton = false; - transactionSendableAmount.satoshis = 0; + transactionSendableAmount.satoshis = walletSpendableAmount.satoshis; } else if (maxSatoshis) { if (walletSpendableAmount.satoshis > maxSatoshis) { diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index e4083611b..3da170f3b 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -1,11 +1,13 @@ describe('amountController', function(){ var configCache, - configService, + configService, + gettextCatalog, $controller, $ionicHistory, $rootScope, ongoingProcess, platformInfo, + popupService, profileService, rateService, sendFlowService, @@ -37,6 +39,8 @@ 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']); @@ -46,13 +50,21 @@ describe('amountController', function(){ isAndroid: false, isIos: true }; - + popupService = jasmine.createSpyObj(['showAlert']); profileService = jasmine.createSpyObj(['getWallet', 'getWallets']); 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) { + var units = satoshis / 100000000; + var formatted = (units * 10000).toFixed(2) + ' USD'; + cb(formatted); + }); + txFormatService.formatAmountStr.and.callFake(function(coin, satoshis) { + return (satoshis * 100000000).toFixed(8) + ' ' + (coin || 'bch').toUpperCase(); + }); $state = jasmine.createSpyObj(['transitionTo']); $stateParams = {}; @@ -74,7 +86,7 @@ describe('amountController', function(){ $ionicHistory.backView.and.returnValue(backView); var wallet = { - + }; profileService.getWallet.and.returnValue(wallet); profileService.getWallets.and.returnValue([{}]); @@ -85,7 +97,7 @@ describe('amountController', function(){ var amountController = $controller('amountController', { configService: configService, - gettextCatalog: {}, + gettextCatalog: gettextCatalog, $ionicHistory: $ionicHistory, $ionicModal: {}, $ionicScrollDelegate: {}, @@ -93,7 +105,7 @@ describe('amountController', function(){ ongoingProcess: ongoingProcess, platformInfo: platformInfo, profileService: profileService, - popupService: {}, + popupService: popupService, rateService: rateService, $scope: $scope, sendFlowService: sendFlowService, @@ -142,7 +154,195 @@ describe('amountController', function(){ }); - fit ('with available balance higher than max does not allow sendMax', function() { + 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 = { @@ -188,9 +388,7 @@ describe('amountController', function(){ sendMax: false, thirdParty: { id: 'shapeshift', - data: { - fromWalletId: "4cd7673e-7320-4dfa-86e5-d4edb51d460a" - }, + data: {}, }, toAddress: '', toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9'