From 8908b5ef802be475cd0ceeb41191ae7016b8e973 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 3 Sep 2018 20:12:20 +1200 Subject: [PATCH 01/15] Send max button behaviour is now conditional based on min and max limits. --- i18n/po/template.pot | 6 +- src/js/controllers/amount.js | 187 +++++++++++++++++++++++++++-------- www/views/amount.html | 7 +- 3 files changed, 157 insertions(+), 43 deletions(-) diff --git a/i18n/po/template.pot b/i18n/po/template.pot index 9c0e3bdc6..eed30bbb8 100644 --- a/i18n/po/template.pot +++ b/i18n/po/template.pot @@ -3849,4 +3849,8 @@ msgstr "" #: src/js/services/incomingData.js:129 msgid "This invoice is no longer accepting payments" -msgstr "" \ No newline at end of file +msgstr "" + +#: www/views/amount.html.js:60 +msgid "Send Maximum Amount" +msgstr "" diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index f796f9559..e814bc92f 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -11,6 +11,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.alternativeUnit = ''; vm.amount = '0'; vm.availableFunds = ''; + vm.canSendAllAvailableFunds = true; // 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 +21,12 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.lastUsedPopularList = []; vm.maxAmount = 0; vm.minAmount = 0; + vm.showSendMaxButton = false; + vm.showSendLimitMaxButton = false; vm.thirdParty = false; vm.unit = ''; + // Functions vm.changeUnit = changeUnit; vm.close = close; vm.findCurrency = findCurrency; @@ -34,6 +38,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.pushDigit = pushDigit; vm.removeDigit = removeDigit; vm.save = save; + vm.sendableFunds = ''; vm.sendMax = sendMax; vm.errorMessage = ''; @@ -46,10 +51,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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 +60,18 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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,27 +92,17 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.minAmount = parseFloat(passthroughParams.minAmount); vm.maxAmount = parseFloat(passthroughParams.maxAmount); + vm.isRequestingSpecificAmount = !passthroughParams.fromWalletId; + vm.showSendMaxButton = !vm.isRequestingSpecificAmount; + 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); - - shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(data) { - vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); - vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); - }); - } + initForShapeshift(); } } - vm.isRequestingSpecificAmount = !passthroughParams.fromWalletId; + var config = configService.getSync().wallet.settings; @@ -217,6 +218,30 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, $ionicHistory.goBack(); } + function initForShapeshift() { + 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); + + vm.showSendMaxButton = false; + vm.showSendLimitMaxButton = false; + vm.canSendAllAvailableFunds = false; + + shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(data) { + 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(); @@ -232,7 +257,18 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function sendMax() { - useSendMax = true; + if (canSendMax) { + useSendMax = true; + + } 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 = (transactionSendableAmount.satoshis * unitToSatoshi).toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); + } finish(); } @@ -353,8 +389,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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) @@ -374,8 +410,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } 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) @@ -584,24 +620,23 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, availableUnits[altUnitIndex].shortName = newAltCurrency.isoCode; fiatCode = newAltCurrency.isoCode; updateAvailableFundsStringIfNeeded(); + updateMaximumButtonIfNeeded(); updateUnitUI(); close(); }); } 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; }); } }); @@ -610,9 +645,10 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function updateAvailableFundsFromWallet(wallet) { + var availableFundsInFiat = ''; if (wallet.status && wallet.status.isValid) { - availableFundsInCrypto = wallet.status.spendableBalanceStr; - availableSatoshis = wallet.status.spendableAmount; + walletSpendableAmount.crypto = wallet.status.spendableBalanceStr; + walletSpendableAmount.satoshis = wallet.status.spendableAmount; if (wallet.status.alternativeBalanceAvailable) { availableFundsInFiat = wallet.status.spendableBalanceAlternative + ' ' + wallet.status.alternativeIsoCode; } else { @@ -626,20 +662,93 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } else { availableFundsInFiat = ''; } - availableFundsInCrypto = wallet.cachedStatus.spendableBalanceStr; - availableSatoshis = wallet.cachedStatus.spendableAmount; + walletSpendableAmount.crypto = wallet.cachedStatus.spendableBalanceStr; + walletSpendableAmount.satoshis = wallet.cachedStatus.spendableAmount; } else { - availableFundsInFiat = ''; - availableFundsInCrypto = ''; - availableSatoshis = null; + walletSpendableAmount.crypto = ''; + walletSpendableAmount.satoshis = null; } if (availableUnits[unitIndex].isFiat) { - vm.availableFunds = availableFundsInFiat || availableFundsInCrypto; + vm.availableFunds = availableFundsInFiat || walletSpendableAmount.crypto; } else { - vm.availableFunds = availableFundsInCrypto; + vm.availableFunds = walletSpendableAmount.crypto; + } + + setMaximumButtonFromWallet(wallet); + } + + function 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; + }); + } + }); + } } } + + function setMaximumButtonFromWallet(wallet) { + 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.showSendLimitMaxButton = false; + transactionSendableAmount.satoshis = 0; + + } 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 { + 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 = true; + vm.showSendMaxButton = true; + vm.showSendLimitMaxButton = false; + transactionSendableAmount.satoshis = walletSpendableAmount.satoshis; + } + + } 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 (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; + }); + } + }); + } + } + + } } diff --git a/www/views/amount.html b/www/views/amount.html index 939937be8..114292026 100644 --- a/www/views/amount.html +++ b/www/views/amount.html @@ -53,11 +53,12 @@
-
+
From b7dda8b6cabe6b30be43a9526c51893bab12f1e3 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 3 Sep 2018 20:17:46 +1200 Subject: [PATCH 02/15] Currency in send max button now updates with currency change. --- src/js/controllers/amount.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index e814bc92f..60912e42b 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -620,7 +620,6 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, availableUnits[altUnitIndex].shortName = newAltCurrency.isoCode; fiatCode = newAltCurrency.isoCode; updateAvailableFundsStringIfNeeded(); - updateMaximumButtonIfNeeded(); updateUnitUI(); close(); }); @@ -641,6 +640,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } }); } + updateMaximumButtonIfNeeded(); } } @@ -681,6 +681,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function updateMaximumButtonIfNeeded() { + console.log('sendmax updateMaximumButtonIfNeeded()'); if (vm.showSendMaxButton || vm.showSendLimitMaxButton) { transactionSendableAmount.fiat = ''; vm.sendableFunds = transactionSendableAmount.crypto; From 52f18c3c9bfe34e9f374aceaa843f3f89e78b8ed Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 4 Sep 2018 09:13:09 +1200 Subject: [PATCH 03/15] Got the Enter Amount screen unit tests running. --- src/js/controllers/amount.spec.js | 34 ++++++++++++++++++++++--------- test/karma.conf.js | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index ed64da836..00d82e520 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -7,6 +7,9 @@ describe('amountController', function(){ platformInfo, profileService, rateService, + sendFlowService, + shapeshiftService, + txFormatService, $stateParams; @@ -39,9 +42,12 @@ describe('amountController', function(){ isIos: true }; - profileService = jasmine.createSpyObj(['getWallets']); + profileService = jasmine.createSpyObj(['getWallet', 'getWallets']); rateService = jasmine.createSpyObj(['fromFiat', 'whenAvailable']); + sendFlowService = jasmine.createSpyObj(['getStateClone']); + shapeshiftService = jasmine.createSpyObj(['shiftIt']); + txFormatService = jasmine.createSpyObj(['formatAlternativeStr', 'formatAmountStr']); $stateParams = {}; @@ -61,6 +67,11 @@ describe('amountController', function(){ stateName: 'ignoreme' }; $ionicHistory.backView.and.returnValue(backView); + + var wallet = { + + }; + profileService.getWallet.and.returnValue(wallet); profileService.getWallets.and.returnValue([{}]); rateService.fromFiat.and.returnValue(12); // satoshis or coins? @@ -80,22 +91,25 @@ describe('amountController', function(){ popupService: {}, rateService: rateService, $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, $state: {}, $stateParams: $stateParams, - txFormatService: {}, + txFormatService: txFormatService, walletService: {} }); - var data = { - stateParams: { - fromWalletId: 'fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b', - toAddress: 'qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s' - } + var sendFlowState = { + fromWalletId: 'fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b', + toAddress: 'qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s' }; - $scope.$emit('$ionicView.beforeEnter', data); - expect($scope.fromWalletId).toBe('fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b'); - expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'); + sendFlowService.getStateClone.and.returnValue(sendFlowState); + + $scope.$emit('$ionicView.beforeEnter', {}); + + //expect($scope.fromWalletId).toBe('fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b'); + //expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'); }); }); \ No newline at end of file diff --git a/test/karma.conf.js b/test/karma.conf.js index b4f64af73..22efcd1c8 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -17,7 +17,7 @@ module.exports = function(config) { files: [ 'node_modules/angular/angular.js', - 'bitanalytics/bitanalytics-0.1.0.js', + 'bitanalytics/bitanalytics.js', // From Gruntfile.js 'bower_components/qrcode-generator/js/qrcode.js', From b2178c84e31aa5a9b09b89057250826be310beec Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 4 Sep 2018 11:02:52 +1200 Subject: [PATCH 04/15] When available funds are higher than Shapeshift max limit, the send max button changes to send max limit. --- src/js/controllers/amount.js | 29 ++++---- src/js/controllers/amount.spec.js | 117 +++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 60912e42b..05c4b9d8b 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -81,11 +81,12 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, if (data.direction == "back") { sendFlowService.popState(); } - console.log('amount onBeforeEnter after back sendflow ', sendFlowService.state); + //console.log('amount onBeforeEnter after back sendflow ', sendFlowService.state); initCurrencies(); passthroughParams = sendFlowService.getStateClone(); + console.log('sendflow Amount on BeforeEnter after back', passthroughParams); vm.fromWalletId = passthroughParams.fromWalletId; vm.toWalletId = passthroughParams.toWalletId; @@ -95,6 +96,14 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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(); + if (passthroughParams.thirdParty) { vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object if (vm.thirdParty) { @@ -102,12 +111,6 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } } - - - var config = configService.getSync().wallet.settings; - - setAvailableUnits(); - updateUnitUI(); var reNr = /^[1234567890\.]$/; var reOp = /^[\*\+\-\/]$/; @@ -136,10 +139,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, }); } - unitToSatoshi = config.unitToSatoshi; - satToUnit = 1 / unitToSatoshi; - unitDecimals = config.unitDecimals; - + resetAmount(); processAmount(); @@ -220,9 +220,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, function initForShapeshift() { if (vm.thirdParty.id === 'shapeshift') { - if (!vm.thirdParty.data) { - vm.thirdParty.data = {}; - } + vm.thirdParty.data = vm.thirdParty.data || {}; + vm.thirdParty.data['fromWalletId'] = vm.fromWalletId; vm.fromWallet = profileService.getWallet(vm.fromWalletId); @@ -232,6 +231,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.showSendLimitMaxButton = false; vm.canSendAllAvailableFunds = false; + shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(data) { vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); @@ -239,6 +239,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, setMaximumButtonFromWallet(vm.fromWallet); }); + } } diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index 00d82e520..d198f3591 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -10,6 +10,7 @@ describe('amountController', function(){ sendFlowService, shapeshiftService, txFormatService, + $scope, $stateParams; @@ -21,7 +22,7 @@ describe('amountController', function(){ configCache = { wallet: { settings: { - + unitToSatoshi: 100000000 } } }; @@ -44,9 +45,9 @@ describe('amountController', function(){ profileService = jasmine.createSpyObj(['getWallet', 'getWallets']); - rateService = jasmine.createSpyObj(['fromFiat', 'whenAvailable']); + rateService = jasmine.createSpyObj(['fromFiat', 'listAlternatives', 'updateRates', 'whenAvailable']); sendFlowService = jasmine.createSpyObj(['getStateClone']); - shapeshiftService = jasmine.createSpyObj(['shiftIt']); + shapeshiftService = jasmine.createSpyObj(['getMarketData']); txFormatService = jasmine.createSpyObj(['formatAlternativeStr', 'formatAmountStr']); $stateParams = {}; @@ -112,4 +113,114 @@ 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"} + ]); + + }); + + fit ('with available balance higher than max does not allow 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: {}, + platformInfo: platformInfo, + profileService: profileService, + popupService: {}, + rateService: rateService, + $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, + $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: { + fromWalletId: "4cd7673e-7320-4dfa-86e5-d4edb51d460a" + }, + }, + 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); + + + + }); + }); + }); \ No newline at end of file From 4dc3e7c2e83202080d8762f3ff22834e6bed3858 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 4 Sep 2018 12:21:57 +1200 Subject: [PATCH 05/15] Enter Amount displaying ongoing progress indicator when contacting Shapeshift. Send max button now displays max limit amount when available funds are above the Shapeshift limit. --- src/js/controllers/amount.js | 9 +++++---- www/views/amount.html | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 05c4b9d8b..ff4ec3628 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -2,7 +2,7 @@ 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, profileService, walletService, $window) { +function amountController(configService, $filter, gettextCatalog, $ionicHistory, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, ongoingProcess, profileService, walletService, $window) { var vm = this; vm.allowSend = false; @@ -231,8 +231,9 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, vm.showSendLimitMaxButton = false; vm.canSendAllAvailableFunds = false; - - shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(data) { + ongoingProcess.set('connectingShapeshift', true); + shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function onMarketData(data) { + ongoingProcess.set('connectingShapeshift', false); vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); @@ -740,7 +741,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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) { diff --git a/www/views/amount.html b/www/views/amount.html index 114292026..51397f3bf 100644 --- a/www/views/amount.html +++ b/www/views/amount.html @@ -53,11 +53,11 @@
-
+
From 4315d16f7333b6320e46697be3b02121e34c749d Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 4 Sep 2018 12:52:54 +1200 Subject: [PATCH 06/15] The Enter Amount screen now correctly sets the amount when sendMax() is called when available funds exceed the max limit. --- src/js/controllers/amount.js | 6 ++--- src/js/controllers/amount.spec.js | 31 +++++++++++++++++++++---- src/js/controllers/review.controller.js | 8 +++++-- src/js/services/sendFlowService.js | 4 ++-- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index ff4ec3628..8d1f65058 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -269,7 +269,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, altUnitIndex = unitIndex; unitIndex = tempIndex; } - vm.amount = (transactionSendableAmount.satoshis * unitToSatoshi).toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); + vm.amount = (transactionSendableAmount.satoshis * satToUnit).toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); } finish(); } @@ -487,9 +487,9 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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 = { diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index d198f3591..e4083611b 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -4,6 +4,7 @@ describe('amountController', function(){ $controller, $ionicHistory, $rootScope, + ongoingProcess, platformInfo, profileService, rateService, @@ -11,6 +12,7 @@ describe('amountController', function(){ shapeshiftService, txFormatService, $scope, + $state, $stateParams; @@ -37,6 +39,8 @@ describe('amountController', function(){ $ionicHistory = jasmine.createSpyObj(['backView']); + ongoingProcess = jasmine.createSpyObj(['set']); + platformInfo = { isChromeApp: false, isAndroid: false, @@ -46,10 +50,10 @@ describe('amountController', function(){ profileService = jasmine.createSpyObj(['getWallet', 'getWallets']); rateService = jasmine.createSpyObj(['fromFiat', 'listAlternatives', 'updateRates', 'whenAvailable']); - sendFlowService = jasmine.createSpyObj(['getStateClone']); + sendFlowService = jasmine.createSpyObj(['getStateClone', 'pushState']); shapeshiftService = jasmine.createSpyObj(['getMarketData']); txFormatService = jasmine.createSpyObj(['formatAlternativeStr', 'formatAmountStr']); - + $state = jasmine.createSpyObj(['transitionTo']); $stateParams = {}; inject(function(_$controller_, _$rootScope_){ @@ -86,7 +90,7 @@ describe('amountController', function(){ $ionicModal: {}, $ionicScrollDelegate: {}, nodeWebkitService: {}, - ongoingProcess: {}, + ongoingProcess: ongoingProcess, platformInfo: platformInfo, profileService: profileService, popupService: {}, @@ -159,7 +163,7 @@ describe('amountController', function(){ $ionicModal: {}, $ionicScrollDelegate: {}, nodeWebkitService: {}, - ongoingProcess: {}, + ongoingProcess: ongoingProcess, platformInfo: platformInfo, profileService: profileService, popupService: {}, @@ -167,7 +171,7 @@ describe('amountController', function(){ $scope: $scope, sendFlowService: sendFlowService, shapeshiftService: shapeshiftService, - $state: {}, + $state: $state, $stateParams: $stateParams, txFormatService: txFormatService, walletService: {} @@ -218,8 +222,25 @@ describe('amountController', function(){ 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'); }); }); diff --git a/src/js/controllers/review.controller.js b/src/js/controllers/review.controller.js index b377bef58..554774252 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.getStateClone(); 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/sendFlowService.js b/src/js/services/sendFlowService.js index 62989b3c5..9981fdbcb 100644 --- a/src/js/services/sendFlowService.js +++ b/src/js/services/sendFlowService.js @@ -12,7 +12,7 @@ angular // A separate state variable so we can ensure it is cleared of everything, // even other properties added that this service does not know about. (such as "coin") state: { - amount: '', + amount: 0, displayAddress: null, fromWalletId: '', sendMax: false, @@ -42,7 +42,7 @@ angular function clearCurrent() { console.log("sendFlow clearCurrent()"); service.state = { - amount: '', + amount: 0, displayAddress: null, fromWalletId: '', sendMax: false, From 3ab535a36be601eb5beedb51d6d4b2280080fa18 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 4 Sep 2018 19:47:50 +1200 Subject: [PATCH 07/15] Improved handling of available funds below min amount. --- src/js/controllers/amount.js | 44 +++--- src/js/controllers/amount.spec.js | 216 ++++++++++++++++++++++++++++-- 2 files changed, 236 insertions(+), 24 deletions(-) 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' From 6c8a1cfd5a2f3e49672f07da81728524c995385b Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Tue, 4 Sep 2018 20:48:16 +1200 Subject: [PATCH 08/15] Bugfix for using cached status in Enter Amount screen. --- src/js/controllers/amount.js | 2 +- src/js/controllers/amount.spec.js | 161 +++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 2d5a0aa66..d30da03ad 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -670,7 +670,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, availableFundsInFiat = ''; } - } else if (wallet.cachedStatus && wallet.status.isValid) { + } else if (wallet.cachedStatus && wallet.cachedStatus.isValid) { if (wallet.cachedStatus.alternativeBalanceAvailable) { availableFundsInFiat = wallet.cachedStatus.spendableBalanceAlternative + ' ' + wallet.cachedStatus.alternativeIsoCode; diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index 3da170f3b..fdef97109 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -57,14 +57,23 @@ describe('amountController', function(){ 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 = {}; @@ -86,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([{}]); @@ -129,6 +141,8 @@ describe('amountController', function(){ //expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'); }); + + describe('Shapeshift', function() { var walletFrom; var walletTo; @@ -442,4 +456,149 @@ describe('amountController', function(){ }); }); + + 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 From 001cd82afb1b7dac54bbd820adf203e8b19a74cf Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasma Date: Tue, 4 Sep 2018 17:52:53 +0200 Subject: [PATCH 09/15] getMarketData can return an error --- src/js/controllers/amount.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index d30da03ad..24a3cc2d1 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -236,6 +236,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function onMarketData(data) { console.log('sendmax onMarketData()'); ongoingProcess.set('connectingShapeshift', false); + + if (data.error) { + popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), typeof data.error === 'string'?data.error:(data.error.message?data.error.message:''), function () { + $ionicHistory.goBack(); + }); + } + vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); From d01baa606082983565b7418539388ec296664517 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Wed, 5 Sep 2018 18:16:01 +1200 Subject: [PATCH 10/15] Better handling of Shapeshift error. --- src/js/controllers/amount.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index 24a3cc2d1..fdbcfe5bf 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -238,15 +238,20 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, ongoingProcess.set('connectingShapeshift', false); if (data.error) { - popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), typeof data.error === 'string'?data.error:(data.error.message?data.error.message:''), function () { - $ionicHistory.goBack(); + // TODO: translatable string. use goBack(), don't execute the code beyond. + popupService.showAlert( + gettextCatalog.getString('Shapeshift Error'), + typeof data.error === 'string' ? data.error : (data.error.message ? data.error.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); } - - vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum); - vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit); - - setMaximumButtonFromWallet(vm.fromWallet); }); From 394317bc46b516db3a477fbcfced7e09e668054c Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 6 Sep 2018 11:27:48 +1200 Subject: [PATCH 11/15] Better default error message for Shapeshift error. --- i18n/po/template.pot | 4 ++++ src/js/controllers/amount.js | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/i18n/po/template.pot b/i18n/po/template.pot index eed30bbb8..10b3de712 100644 --- a/i18n/po/template.pot +++ b/i18n/po/template.pot @@ -3854,3 +3854,7 @@ msgstr "" #: www/views/amount.html.js:60 msgid "Send Maximum Amount" msgstr "" + +#: src/js/controllers/amount.controller.js:239 +msgid "Unknown error." +msgstr "" \ No newline at end of file diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index fdbcfe5bf..cc78058bd 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -221,7 +221,6 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function initForShapeshift() { - console.log('initForShapeshift()'); if (vm.thirdParty.id === 'shapeshift') { vm.thirdParty.data = vm.thirdParty.data || {}; @@ -234,17 +233,17 @@ 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); if (data.error) { - // TODO: translatable string. use goBack(), don't execute the code beyond. + var defaultErrorMessage = gettextCatalog.getString('Unknown error.'); popupService.showAlert( gettextCatalog.getString('Shapeshift Error'), - typeof data.error === 'string' ? data.error : (data.error.message ? data.error.message : ''), + typeof data.error === 'string' ? data.error : (data.error.message ? data.error.message : defaultErrorMessage), function () { goBack(); - }); + } + ); } else { vm.thirdParty.data.minAmount = vm.minAmount = parseFloat(data.minimum); From 3de34dbf156aeb680471b57f95f9c37693e05938 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 6 Sep 2018 11:34:00 +1200 Subject: [PATCH 12/15] Renaming to follow new convention. --- src/js/app.js | 2 ++ src/js/controllers/{amount.js => amount.controller.js} | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) rename src/js/controllers/{amount.js => amount.controller.js} (99%) diff --git a/src/js/app.js b/src/js/app.js index 745ceef50..6bef6f615 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' ]; @@ -29,4 +30,5 @@ angular.module('copayApp.services', []); angular.module('copayApp.controllers', []); angular.module('copayApp.directives', []); angular.module('copayApp.addons', []); +angular.module('bitcoincom.controllers', []); angular.module('bitcoincom.directives', []); diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.controller.js similarity index 99% rename from src/js/controllers/amount.js rename to src/js/controllers/amount.controller.js index cc78058bd..1e6af76b1 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.controller.js @@ -1,6 +1,10 @@ 'use strict'; -angular.module('copayApp.controllers').controller('amountController', amountController); +(function(){ + +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; @@ -780,3 +784,4 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } } +})(); \ No newline at end of file From 57ce93ccb80bb64beed61d345113aa2a57f2ba5f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Thu, 6 Sep 2018 18:28:27 +0900 Subject: [PATCH 13/15] fix parenthesis --- src/js/services/shapeshift.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 176f0c3141ebdf0d8a267d16f938406ebe6cb715 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Thu, 6 Sep 2018 20:16:25 +0900 Subject: [PATCH 14/15] Fix to provide an amount (canSendMax was never set to false when shapeshift) --- src/js/controllers/amount.controller.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/js/controllers/amount.controller.js b/src/js/controllers/amount.controller.js index 0c3be6bd3..398872c03 100644 --- a/src/js/controllers/amount.controller.js +++ b/src/js/controllers/amount.controller.js @@ -109,14 +109,6 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, setAvailableUnits(); updateUnitUI(); - if (passthroughParams.thirdParty) { - vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object - if (vm.thirdParty) { - initShapeshift(); - } - } - - var reNr = /^[1234567890\.]$/; var reOp = /^[\*\+\-\/]$/; @@ -216,6 +208,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, var fromWallet = profileService.getWallet(passthroughParams.fromWalletId); updateAvailableFundsFromWallet(fromWallet); } + + if (passthroughParams.thirdParty) { + vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object + if (vm.thirdParty) { + initShapeshift(); + } + } } } @@ -269,7 +268,6 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, if (canSendMax) { useSendMax = true; finish(); - } else { var transactionSendableAmountInUnits = transactionSendableAmount.satoshis * satToUnit; if (vm.minAmount && transactionSendableAmountInUnits < vm.minAmount) { @@ -286,7 +284,6 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } vm.amount = transactionSendableAmountInUnits.toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); finish(); - } } } @@ -526,7 +523,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, $state.transitionTo('tabs.paymentRequest.confirm', confirmData); } else { sendFlowService.goNext(confirmData); - $scope.useSendMax = null; + useSendMax = false; } } @@ -741,7 +738,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, 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 = true; + canSendMax = false; vm.showSendMaxButton = true; vm.showSendLimitMaxButton = false; transactionSendableAmount.satoshis = walletSpendableAmount.satoshis; From 6a8f8ca33b3c723f4283bd23976add38c3da5d46 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Thu, 6 Sep 2018 20:29:50 +0900 Subject: [PATCH 15/15] Fix to send the amount if shapeshift case --- src/js/controllers/amount.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/controllers/amount.controller.js b/src/js/controllers/amount.controller.js index 398872c03..07f31bb3e 100644 --- a/src/js/controllers/amount.controller.js +++ b/src/js/controllers/amount.controller.js @@ -283,6 +283,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, unitIndex = tempIndex; } vm.amount = transactionSendableAmountInUnits.toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT); + useSendMax = true; finish(); } } @@ -507,7 +508,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } var confirmData = { - amount: useSendMax ? undefined : satoshis, + amount: (useSendMax && canSendMax) ? undefined : satoshis, displayAddress: passthroughParams.displayAddress, fromWalletId: passthroughParams.fromWalletId, sendMax: useSendMax,