diff --git a/Gruntfile.js b/Gruntfile.js index eb4bb2eb0..8771b85eb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,7 +9,7 @@ module.exports = function(grunt) { pkg: grunt.file.readJSON('package.json'), exec: { get_nwjs_for_pkg: { - command: 'if [ ! -d ./cache/0.19.5-pkg/osx64/nwjs.app ]; then cd ./cache; curl https://dl.nwjs.io/v0.19.5-mas-beta/nwjs-mas-v0.19.5-osx-x64.zip --output nwjs.zip; unzip nwjs.zip; mkdir -p ./0.19.5-pkg/osx64; cp -R ./nwjs-mas-v0.19.5-osx-x64/nwjs.app ./0.19.5-pkg/osx64/; fi' + command: 'if [ ! -d ./cache/0.19.4/osx64/nwjs.app ]; then mkdir -p ./cache/0.19.4/osx64; curl https://dl.nwjs.io/v0.19.5-mas-beta/nwjs-mas-v0.19.5-osx-x64.zip --output ./cache/nwjs.zip; unzip ./cache/nwjs.zip -d ./cache; cp -R ./cache/nwjs-mas-v0.19.5-osx-x64/nwjs.app ./cache/0.19.4/osx64/; fi' }, create_others_dist: { command: 'sh webkitbuilds/create-others-dist.sh "<%= pkg.name %>" "<%= pkg.fullVersion %>" "<%= pkg.nameCaseNoSpace %>" "<%= pkg.title %>"' @@ -296,10 +296,10 @@ module.exports = function(grunt) { }, pkg: { options: { - appName: '<%= pkg.nameCaseNoSpace %>', + appName: '<%= pkg.title %>', platforms: ['osx64'], buildDir: './webkitbuilds/pkg', - version: '0.19.5', + version: '0.19.4', macIcns: './resources/<%= pkg.name %>/mac/pkg/app.icns', exeIco: './www/img/app/logo.ico', macPlist: { diff --git a/app-template/config-template.xml b/app-template/config-template.xml index 2f8e3db04..8686b7b36 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -85,6 +85,12 @@ + + + + + + @@ -102,6 +108,7 @@ + diff --git a/app-template/create-pkg-dist.sh b/app-template/create-pkg-dist.sh index c0b4d266d..66fe589d0 100644 --- a/app-template/create-pkg-dist.sh +++ b/app-template/create-pkg-dist.sh @@ -24,8 +24,9 @@ ln -s ../resources/bitcoin.com/mac/pkg/build.cfg build.cfg rm build_mas.py ln -s ../resources/bitcoin.com/mac/pkg/build_mas.py build_mas.py -echo "Signing ${APP_NAME}" -export APP_PATH="pkg/${APP_NAME}/osx64/${APP_NAME}" +echo "Signing ${APP_FULLNAME}" +export CURRENT_PATH=`pwd` +export APP_PATH="pkg/${APP_FULLNAME}/osx64/${APP_FULLNAME}" export TMP_PATH="tmp" export DIST_PATH="dist" @@ -36,10 +37,16 @@ if [ ! -d $DIST_PATH ]; then mkdir $DIST_PATH fi -python build_mas.py -C build.cfg -O "${TMP_PATH}/${APP_NAME}.app" -I "${APP_PATH}.app" -P "$DIST_PATH/${APP_PACKAGE}-wallet-${APP_VERSION}-osx.pkg" +cd "${APP_PATH}.app/Contents/Versions" +ln -s "55.0.2883.87" "Current" + +cd $CURRENT_PATH +chmod -vR 777 "${APP_PATH}.app/Contents" + +python build_mas.py -C build.cfg -O "${TMP_PATH}/${APP_FULLNAME}.app" -I "${APP_PATH}.app" -P "$DIST_PATH/${APP_PACKAGE}-wallet-${APP_VERSION}-osx.pkg" echo "Signing Done" echo "Done." -exit +exit \ No newline at end of file diff --git a/i18n/po/template.pot b/i18n/po/template.pot index 9c0e3bdc6..56318008c 100644 --- a/i18n/po/template.pot +++ b/i18n/po/template.pot @@ -511,7 +511,7 @@ msgid "Cannot Create Wallet" msgstr "" #: src/js/services/profileService.js:442 -msgid "Cannot join the same wallet more that once" +msgid "Cannot join the same wallet more than once" msgstr "" #: www/views/includes/bitpayCardsCard.html:2 @@ -2916,10 +2916,15 @@ msgid "Sweep" msgstr "" #: www/views/includes/incomingDataMenu.html:89 -#: www/views/paperWallet.html:3 +msgctxt "List item" msgid "Sweep paper wallet" msgstr "" +#: www/views/paperWallet.html:3 +msgctxt "Page title" +msgid "Sweep Paper Wallet" +msgstr "" + #: src/js/services/onGoingProcess.js:33 msgid "Sweeping Wallet..." msgstr "" @@ -3783,6 +3788,10 @@ msgstr "" msgid "Bitcoin Cash Games" msgstr "" +#: www/views/includes/community.html:29 +msgid "Share the Wallet App" +msgstr "" + #: src/js/services/bitcoincomService.js:28 msgid "News" msgstr "" @@ -3849,4 +3858,47 @@ msgstr "" #: src/js/services/incomingData.js:129 msgid "This invoice is no longer accepting payments" +msgstr "" + +#: www/views/paperWallet.html:48 +msgid "No Bitcoin Cash wallet to transfer funds to found." +msgstr "" + +#: www/views/paperWallet.html:54 +msgid "No Bitcoin Cash found." +msgstr "" + +#: www/views/paperWallet.html:60 +msgid "Bitcoin Core found:" +msgstr "" + +#: www/views/paperWallet.html:98 +msgid "No Bitcoin Core wallet to transfer funds to found." +msgstr "" + +#: www/views/paperWallet.html:104 +msgid "No Bitcoin Core found." + +#: src/js/controllers/tab-scan.js:120 +msgid "Scan Failed" +msgstr "" + +#: src/js/controllers/tab-scan.js:121 +msgid "Data not recognised." +msgstr "" + +#: src/js/controllers/tab-scan.js:121 +msgid "Unsupported" +msgstr "" + +#: src/js/controllers/tab-scan.js:121 +msgid "Testnet is not supported." +msgstr "" + +#: www/views/includes/incomingDataMenu.html:81 +msgid "URL" +msgstr "" + +#: www/views/includes/incomingDataMenu.html:90 +msgid "Open in web browser" msgstr "" \ No newline at end of file diff --git a/resources/bitcoin.com/ios/icon/AppIcon24x24@2x.png b/resources/bitcoin.com/ios/icon/AppIcon24x24@2x.png new file mode 100644 index 000000000..67dddbb02 Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon24x24@2x.png differ diff --git a/resources/bitcoin.com/ios/icon/AppIcon27.5x27.5@2x.png b/resources/bitcoin.com/ios/icon/AppIcon27.5x27.5@2x.png new file mode 100644 index 000000000..0033e910b Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon27.5x27.5@2x.png differ diff --git a/resources/bitcoin.com/ios/icon/AppIcon44x44@2x.png b/resources/bitcoin.com/ios/icon/AppIcon44x44@2x.png new file mode 100644 index 000000000..ac78920fc Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon44x44@2x.png differ diff --git a/resources/bitcoin.com/ios/icon/AppIcon86x86@2x.png b/resources/bitcoin.com/ios/icon/AppIcon86x86@2x.png new file mode 100644 index 000000000..5a0e05b71 Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon86x86@2x.png differ diff --git a/resources/bitcoin.com/ios/icon/AppIcon98x98@2x.png b/resources/bitcoin.com/ios/icon/AppIcon98x98@2x.png new file mode 100644 index 000000000..a0aaf7bd7 Binary files /dev/null and b/resources/bitcoin.com/ios/icon/AppIcon98x98@2x.png differ diff --git a/resources/bitcoin.com/ios/icon/icon-1024.png b/resources/bitcoin.com/ios/icon/icon-1024.png new file mode 100644 index 000000000..2950ff8de Binary files /dev/null and b/resources/bitcoin.com/ios/icon/icon-1024.png differ diff --git a/resources/bitcoin.com/ios/icon/icon-20.png b/resources/bitcoin.com/ios/icon/icon-20.png new file mode 100644 index 000000000..323da6465 Binary files /dev/null and b/resources/bitcoin.com/ios/icon/icon-20.png differ diff --git a/resources/bitcoin.com/mac/pkg/build_mas.py b/resources/bitcoin.com/mac/pkg/build_mas.py index d067abacd..90f7ad5f0 100755 --- a/resources/bitcoin.com/mac/pkg/build_mas.py +++ b/resources/bitcoin.com/mac/pkg/build_mas.py @@ -178,11 +178,17 @@ def codesign_app(config, args): plistlib.writePlist(child_entitlements, tmp_child_entitlements) info('Child entitlements: %s' % tmp_child_entitlements) - framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True) - system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework)) - helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True) - system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp)) + + libffmpeg = glob(os.path.join(args.output, 'Contents/Versions/55.0.2883.87/nwjs Framework.framework/Versions/A'), 'libffmpeg.dylib', returnOnFound=True) + system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, libffmpeg)) + libnode = glob(os.path.join(args.output, 'Contents/Versions/55.0.2883.87/nwjs Framework.framework/Versions/A'), 'libnode.dylib', returnOnFound=True) + system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, libnode)) + helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True) + system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp)) + framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True) + system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework)) + ## sign parent app (_, tmp_parent_entitlements) = tempfile.mkstemp() if config.has_option('Sign', 'ParentEntitlements'): diff --git a/resources/bitcoin.com/mac/pkg/entitlements-parent.plist b/resources/bitcoin.com/mac/pkg/entitlements-parent.plist index 12d6997e3..b39edb569 100644 --- a/resources/bitcoin.com/mac/pkg/entitlements-parent.plist +++ b/resources/bitcoin.com/mac/pkg/entitlements-parent.plist @@ -5,7 +5,9 @@ com.apple.security.app-sandbox com.apple.security.application-groups - $GROUPID + + 299HJ3G3BP.com.bitcoin.mwallet.mac + com.apple.security.files.user-selected.read-only com.apple.security.network.client diff --git a/src/js/app.js b/src/js/app.js index 745ceef50..503da9f52 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -19,7 +19,8 @@ var modules = [ 'copayApp.controllers', 'copayApp.directives', 'copayApp.addons', - 'bitcoincom.directives' + 'bitcoincom.directives', + 'bitcoincom.services' ]; var copayApp = window.copayApp = angular.module('copayApp', modules); @@ -30,3 +31,4 @@ angular.module('copayApp.controllers', []); angular.module('copayApp.directives', []); angular.module('copayApp.addons', []); angular.module('bitcoincom.directives', []); +angular.module('bitcoincom.services', []); diff --git a/src/js/controllers/addressbookView.js b/src/js/controllers/addressbookView.js index 89c1cd924..16df9e559 100644 --- a/src/js/controllers/addressbookView.js +++ b/src/js/controllers/addressbookView.js @@ -21,28 +21,14 @@ angular.module('copayApp.controllers').controller('addressbookViewController', f }); $scope.sendTo = function() { - $ionicHistory.removeBackView(); - sendFlowService.clear(); - $state.go('tabs.send'); - $timeout(function() { - var to = ''; - if ($scope.addressbookEntry.coin == 'bch') { - var a = 'bitcoincash:' + $scope.addressbookEntry.address; - to = bitcoinCashJsService.readAddress(a).legacy; - } else { - to = $scope.addressbookEntry.address; - } + var stateParams = { + data: $scope.addressbookEntry.address, + toName: $scope.addressbookEntry.name, + toEmail: $scope.addressbookEntry.email, + coin: $scope.addressbookEntry.coin + }; - var stateParams = { - toAddress: to, - toName: $scope.addressbookEntry.name, - toEmail: $scope.addressbookEntry.email, - coin: $scope.addressbookEntry.coin - }; - - sendFlowService.pushState(stateParams); - $state.transitionTo('tabs.send.origin'); - }, 100); + sendFlowService.start(stateParams); }; $scope.remove = function(addressbookEntry) { diff --git a/src/js/controllers/amount.js b/src/js/controllers/amount.js index f796f9559..e861b36ff 100644 --- a/src/js/controllers/amount.js +++ b/src/js/controllers/amount.js @@ -68,13 +68,14 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, function onBeforeEnter(event, data) { if (data.direction == "back") { - sendFlowService.popState(); + sendFlowService.state.pop(); } - console.log('amount onBeforeEnter after back sendflow ', sendFlowService.state); initCurrencies(); - passthroughParams = sendFlowService.getStateClone(); + passthroughParams = sendFlowService.state.getClone(); + + console.log('amount onBeforeEnter after back sendflow ', passthroughParams); vm.fromWalletId = passthroughParams.fromWalletId; vm.toWalletId = passthroughParams.toWalletId; @@ -214,7 +215,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, } function goBack() { - $ionicHistory.goBack(); + sendFlowService.router.goBack(); } function paste(value) { @@ -467,11 +468,10 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory, confirmData.thirdParty = vm.thirdParty; } - sendFlowService.pushState(confirmData); if (!confirmData.fromWalletId) { $state.transitionTo('tabs.paymentRequest.confirm', confirmData); } else { - $state.transitionTo('tabs.send.review', confirmData); + sendFlowService.goNext(confirmData); $scope.useSendMax = null; } } diff --git a/src/js/controllers/amount.spec.js b/src/js/controllers/amount.spec.js index ed64da836..20b403a4d 100644 --- a/src/js/controllers/amount.spec.js +++ b/src/js/controllers/amount.spec.js @@ -7,6 +7,8 @@ describe('amountController', function(){ platformInfo, profileService, rateService, + sendFlowService, + shapeshiftService, $stateParams; @@ -39,9 +41,11 @@ 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']); $stateParams = {}; @@ -61,6 +65,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 +89,25 @@ describe('amountController', function(){ popupService: {}, rateService: rateService, $scope: $scope, + sendFlowService: sendFlowService, + shapeshiftService: shapeshiftService, $state: {}, $stateParams: $stateParams, 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/src/js/controllers/onboarding/tour.js b/src/js/controllers/onboarding/tour.js index 47261006b..f6cf0afe2 100644 --- a/src/js/controllers/onboarding/tour.js +++ b/src/js/controllers/onboarding/tour.js @@ -1,6 +1,6 @@ 'use strict'; angular.module('copayApp.controllers').controller('tourController', - function($scope, $state, $log, $timeout, $filter, ongoingProcess, profileService, rateService, popupService, gettextCatalog, startupService, storageService, walletService, $q) { + function ($scope, $state, $log, $timeout, $filter, ongoingProcess, configService, profileService, rateService, popupService, gettextCatalog, lodash, startupService, storageService, uxLanguage, walletService, $q) { $scope.data = { index: 0 @@ -46,62 +46,90 @@ angular.module('copayApp.controllers').controller('tourController', creatingWallet = true; ongoingProcess.set('creatingWallet', true); $timeout(function() { - profileService.createDefaultWallet(function(err, walletClients) { - if (err) { - $log.warn(err); + uxLanguage.init(function(lang) { + var rateCode = uxLanguage.getRateCode(lang); + console.log("When Available: rateService"); + rateService.whenAvailable(function() { + var alternatives = rateService.listAlternatives(true); - return $timeout(function() { - $log.warn('Retrying to create default wallet.....:' + ++retryCount); - if (retryCount > 3) { - ongoingProcess.set('creatingWallet', false); - popupService.showAlert( - gettextCatalog.getString('Cannot Create Wallet'), err, - function() { - retryCount = 0; - return $scope.createDefaultWallet(); - }, gettextCatalog.getString('Retry')); - } else { - return $scope.createDefaultWallet(); - } - }, 2000); - }; - - ongoingProcess.set('creatingWallet', false); - var bchWallet = walletClients[0]; - var btcWallet = walletClients[1]; - var bchWalletId = bchWallet.credentials.walletId; - var btcWalletId = btcWallet.credentials.walletId; - - function createAddressPromise(wallet) { - return $q(function(resolve, reject) { - walletService.getAddress(wallet, true, function(e, addr) { - if (e) reject(e); - resolve(addr); + var newAltCurrency = lodash.find(alternatives, { + 'isoCode': rateCode }); + + configService.whenAvailable(function(config) { + var opts = { + wallet: { + settings: { + alternativeName: newAltCurrency.name, + alternativeIsoCode: newAltCurrency.isoCode, + } + } + }; + configService.set(opts, function(err) { + if (err) $log.warn(err); + + profileService.createDefaultWallet(function(err, walletClients) { + if (err) { + $log.warn(err); + + return $timeout(function() { + $log.warn('Retrying to create default wallet.....:' + ++retryCount); + if (retryCount > 3) { + ongoingProcess.set('creatingWallet', false); + popupService.showAlert( + gettextCatalog.getString('Cannot Create Wallet'), err, + function() { + retryCount = 0; + return $scope.createDefaultWallet(); + }, gettextCatalog.getString('Retry')); + } else { + return $scope.createDefaultWallet(); + } + }, 2000); + } + ; + + ongoingProcess.set('creatingWallet', false); + var bchWallet = walletClients[0]; + var btcWallet = walletClients[1]; + var bchWalletId = bchWallet.credentials.walletId; + var btcWalletId = btcWallet.credentials.walletId; + + function createAddressPromise(wallet) { + return $q(function (resolve, reject) { + walletService.getAddress(wallet, true, function (e, addr) { + if (e) reject(e); + resolve(addr); + }); + }); + } + + function goToCollectEmail() { + $state.go('onboarding.collectEmail', { + bchWalletId: bchWalletId, + btcWalletId: btcWalletId + }); + } + + var bchAddressPromise = createAddressPromise(bchWallet); + var btcAddressPromise = createAddressPromise(btcWallet); + ongoingProcess.set('generatingNewAddress', true); + + $q.all([bchAddressPromise, btcAddressPromise]).then(function (addresses) { + ongoingProcess.set('generatingNewAddress', false); + $state.go('tabs.home'); + }, function (e) { + ongoingProcess.set('generatingNewAddress', false); + $log.warn(e); + popupService.showAlert(gettextCatalog.getString('Error'), e); + $state.go('tabs.home'); + }); + }); + }); + }); + $log.debug('Setting default currency : ' + newAltCurrency); }); - } - - function goToCollectEmail() { - $state.go('onboarding.collectEmail', { - bchWalletId: bchWalletId, - btcWalletId: btcWalletId - }); - } - - var bchAddressPromise = createAddressPromise(bchWallet); - var btcAddressPromise = createAddressPromise(btcWallet); - ongoingProcess.set('generatingNewAddress', true); - - $q.all([bchAddressPromise, btcAddressPromise]).then(function(addresses) { - ongoingProcess.set('generatingNewAddress', false); - $state.go('tabs.home'); - }, function(e) { - ongoingProcess.set('generatingNewAddress', false); - $log.warn(e); - popupService.showAlert(gettextCatalog.getString('Error'), e); - $state.go('tabs.home'); - }); - }); - }, 300); - }; - }); + }) + }, 300); + }; + }); diff --git a/src/js/controllers/review.controller.js b/src/js/controllers/review.controller.js index b377bef58..dbf14937f 100644 --- a/src/js/controllers/review.controller.js +++ b/src/js/controllers/review.controller.js @@ -80,7 +80,7 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit function onBeforeEnter(event, data) { console.log('walletSelector onBeforeEnter sendflow ', sendFlowService.state); defaults = configService.getDefaults(); - sendFlowData = sendFlowService.getStateClone(); + sendFlowData = sendFlowService.state.getClone(); originWalletId = sendFlowData.fromWalletId; satoshis = parseInt(sendFlowData.amount, 10); toAddress = sendFlowData.toAddress; @@ -403,7 +403,7 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit } function goBack() { - $ionicHistory.goBack(); + sendFlowService.router.goBack(); } function handleDestinationAsAddress(address, originCoin) { @@ -766,8 +766,12 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit ((processName === 'signingTx') && vm.originWallet.m > 1) || (processName == 'sendingTx' && !vm.originWallet.canSign() && !vm.originWallet.isPrivKeyExternal()) ) && !isOn) { + // Show the popup vm.sendStatus = 'success'; + // Clear the send flow service state + sendFlowService.state.clear(); + if ($state.current.name === "tabs.send.review") { // XX SP: Otherwise all open wallets on other devices play this sound if you have been in a send flow before on that device. soundService.play('misc/payment_sent.mp3'); } diff --git a/src/js/controllers/shapeshift.js b/src/js/controllers/shapeshift.js index 43e0790d1..0dac21a11 100644 --- a/src/js/controllers/shapeshift.js +++ b/src/js/controllers/shapeshift.js @@ -6,22 +6,6 @@ angular.module('copayApp.controllers').controller('shapeshiftController', functi $scope.showMyAddress = showMyAddress; - function generateAddress(wallet, cb) { - if (!wallet) return; - walletService.getAddress(wallet, false, function(err, addr) { - if (err) { - popupService.showAlert(err); - } - return cb(addr); - }); - } - - function showToWallets() { - $scope.toWallets = $scope.fromWallet.coin === 'btc' ? walletsBch : walletsBtc; - $scope.onToWalletSelect($scope.toWallets[0]); - $scope.singleToWallet = $scope.toWallets.length === 1; - } - $scope.$on("$ionicView.beforeEnter", function(event, data) { walletsBtc = profileService.getWallets({coin: 'btc'}); walletsBch = profileService.getWallets({coin: 'bch'}); @@ -62,18 +46,7 @@ angular.module('copayApp.controllers').controller('shapeshiftController', functi id: 'shapeshift' } }; - - // Starting new send flow, so ensure everything is reset - sendFlowService.clear(); - $state.go('tabs.home').then(function() { - $ionicHistory.clearHistory(); - $state.go('tabs.send').then(function() { - $timeout(function () { - sendFlowService.pushState(stateParams); - $state.transitionTo('tabs.send.origin'); - }, 60); - }); - }); + sendFlowService.start(stateParams); } function showMyAddress() { diff --git a/src/js/controllers/tab-home.js b/src/js/controllers/tab-home.controller.js similarity index 99% rename from src/js/controllers/tab-home.js rename to src/js/controllers/tab-home.controller.js index 318fcece2..229848df8 100644 --- a/src/js/controllers/tab-home.js +++ b/src/js/controllers/tab-home.controller.js @@ -122,8 +122,7 @@ angular.module('copayApp.controllers').controller('tabHomeController', }; $scope.startFreshSend = function() { - sendFlowService.clear(); - $state.go('tabs.send'); + sendFlowService.start(); } $scope.openExternalLink = function() { diff --git a/src/js/controllers/tab-receive.js b/src/js/controllers/tab-receive.js index 66d1799f8..320afe320 100644 --- a/src/js/controllers/tab-receive.js +++ b/src/js/controllers/tab-receive.js @@ -18,10 +18,10 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi $scope.displayBalanceAsFiat = true; $scope.requestSpecificAmount = function() { - sendFlowService.pushState({ - toWalletId: $scope.wallet.credentials.walletId + sendFlowService.start({ + toWalletId: $scope.wallet.credentials.walletId, + isRequestAmount: true }); - $state.go('tabs.paymentRequest.amount'); }; $scope.setAddress = function(newAddr, copyAddress) { diff --git a/src/js/controllers/tab-scan.js b/src/js/controllers/tab-scan.controller.js similarity index 89% rename from src/js/controllers/tab-scan.js rename to src/js/controllers/tab-scan.controller.js index 4a654d91d..14368ee1c 100644 --- a/src/js/controllers/tab-scan.js +++ b/src/js/controllers/tab-scan.controller.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('tabScanController', function($scope, $log, $timeout, scannerService, incomingData, $state, $ionicHistory, $rootScope, $ionicNavBarDelegate) { +angular.module('copayApp.controllers').controller('tabScanController', function(gettextCatalog, popupService, $scope, $log, $timeout, scannerService, incomingDataService, $state, $ionicHistory, $rootScope, $ionicNavBarDelegate) { var scannerStates = { unauthorized: 'unauthorized', @@ -111,7 +111,18 @@ angular.module('copayApp.controllers').controller('tabScanController', function( // Sometimes (testing in Chrome, when reading QR Code) data is an object // that has a string data.result. contents = contents.result || contents; - incomingData.redir(contents); + incomingDataService.redir(contents, function onError(err) { + if (err) { + var title = gettextCatalog.getString('Scan Failed'); + popupService.showAlert(title, err.message, function onAlertShown() { + // Enable another scan since we won't receive incomingDataMenu.menuHidden + activate(); + }); + } else { + scannerService.resumePreview(); + + } + }); } $rootScope.$on('incomingDataMenu.menuHidden', function() { diff --git a/src/js/controllers/tab-send.js b/src/js/controllers/tab-send.controller.js similarity index 80% rename from src/js/controllers/tab-send.js rename to src/js/controllers/tab-send.controller.js index 9ac6c35cb..03a9562e8 100644 --- a/src/js/controllers/tab-send.js +++ b/src/js/controllers/tab-send.controller.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('tabSendController', function($scope, $rootScope, $log, $timeout, $ionicScrollDelegate, $ionicLoading, addressbookService, profileService, lodash, $state, walletService, incomingData, popupService, platformInfo, sendFlowService, bwcError, gettextCatalog, scannerService, configService, bitcoinCashJsService, $ionicPopup, $ionicNavBarDelegate, clipboardService) { +angular.module('copayApp.controllers').controller('tabSendController', function(bitcoinUriService, $scope, $log, $timeout, $ionicScrollDelegate, addressbookService, profileService, lodash, $state, walletService, platformInfo, sendFlowService, gettextCatalog, configService, $ionicPopup, $ionicNavBarDelegate, clipboardService, incomingDataService) { var clipboardHasAddress = false; var clipboardHasContent = false; var originalList; @@ -29,7 +29,7 @@ angular.module('copayApp.controllers').controller('tabSendController', function( $scope.$on("$ionicView.enter", function(event, data) { - var stateParams = sendFlowService.getStateClone(); + var stateParams = sendFlowService.state.getClone(); $scope.fromWallet = profileService.getWallet(stateParams.fromWalletId); clipboardService.readFromClipboard(function(text) { @@ -39,7 +39,9 @@ angular.module('copayApp.controllers').controller('tabSendController', function( $scope.clipboardHasAddress = false; $scope.clipboardHasContent = false; - if ((text.indexOf('bitcoincash:') === 0 || text[0] === 'C' || text[0] === 'H' || text[0] === 'p' || text[0] === 'q') && text.replace('bitcoincash:', '').length === 42) { // CashAddr + var parsed = bitcoinUriService.parse(text); + console.log('parsed', parsed); + if (parsed.isValid && parsed.publicAddress && parsed.coin === 'bch' && !parsed.testnet) { // CashAddr $scope.clipboardHasAddress = true; } else if ((text[0] === "1" || text[0] === "3" || text.substring(0, 3) === "bc1") && text.length >= 26 && text.length <= 35) { // Legacy Addresses $scope.clipboardHasAddress = true; @@ -60,11 +62,6 @@ angular.module('copayApp.controllers').controller('tabSendController', function( }); $scope.findContact = function(search) { - - if (incomingData.redir(search)) { - return; - } - if (!search || search.length < 1) { $scope.list = originalList; $timeout(function() { @@ -73,12 +70,16 @@ angular.module('copayApp.controllers').controller('tabSendController', function( return; } - var result = lodash.filter(originalList, function(item) { - var val = item.name; - return lodash.startsWith(val.toLowerCase(), search.toLowerCase()); + var params = sendFlowService.state.getClone(); + params.data = search; + sendFlowService.start(params, function onError() { + var result = lodash.filter(originalList, function(item) { + var val = item.name; + return lodash.startsWith(val.toLowerCase(), search.toLowerCase()); + }); + + $scope.list = result; }); - - $scope.list = result; }; var hasWallets = function() { @@ -184,27 +185,18 @@ angular.module('copayApp.controllers').controller('tabSendController', function( $log.debug('Got toAddress:' + toAddress + ' | ' + item.name); - var stateParams = sendFlowService.getStateClone(); - stateParams.toAddress = toAddress, + var stateParams = sendFlowService.state.getClone(); + stateParams.toAddress = toAddress; stateParams.coin = item.coin; - sendFlowService.pushState(stateParams); - - if (!stateParams.fromWalletId) { // If we have no toAddress or fromWallet - $state.transitionTo('tabs.send.origin'); - } else { - $state.transitionTo('tabs.send.amount'); - } - + sendFlowService.start(stateParams); }); }; $scope.startWalletToWalletTransfer = function() { console.log('startWalletToWalletTransfer()'); - var params = sendFlowService.getStateClone(); - sendFlowService.pushState(params); - $state.transitionTo('tabs.send.wallet-to-wallet', { - fromWalletId: sendFlowService.fromWalletId - }); + var params = sendFlowService.state.getClone(); + params.isWalletTransfer = true; + sendFlowService.start(params); } // This could probably be enhanced refactoring the routes abstract states @@ -238,7 +230,7 @@ angular.module('copayApp.controllers').controller('tabSendController', function( }); if (data.direction == "back") { - sendFlowService.clear(); + sendFlowService.state.clear(); } }); diff --git a/src/js/controllers/tabsController.js b/src/js/controllers/tabsController.js index b3de6c70f..b78274ecb 100644 --- a/src/js/controllers/tabsController.js +++ b/src/js/controllers/tabsController.js @@ -1,11 +1,13 @@ 'use strict'; -angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingData, lodash, popupService, gettextCatalog, scannerService, sendFlowService) { +angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingDataService, lodash, popupService, gettextCatalog, scannerService, sendFlowService) { $scope.onScan = function(data) { - if (!incomingData.redir(data)) { - popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Invalid data')); - } + incomingDataService.redir(data, function onError(err) { + if (err) { + popupService.showAlert(gettextCatalog.getString('Error'), err.message); + } + }); }; $scope.setScanFn = function(scanFn) { @@ -16,8 +18,7 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro }; $scope.startFreshSend = function() { - sendFlowService.clear(); - $state.go('tabs.send'); + sendFlowService.start(); }; $scope.importInit = function() { @@ -28,7 +29,6 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro }; $scope.chooseScanner = function() { - sendFlowService.clear(); var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP; if (!isWindowsPhoneApp) { @@ -38,10 +38,14 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro scannerService.useOldScanner(function(err, contents) { if (err) { - popupService.showAlert(gettextCatalog.getString('Error'), err); - return; + popupService.showAlert(gettextCatalog.getString('Error'), err.message); + } else { + incomingDataService.redir(contents, function onError(err) { + if (err) { + popupService.showAlert(gettextCatalog.getString('Error'), err.message); + } + }); } - incomingData.redir(contents); }); }; diff --git a/src/js/controllers/walletDetails.js b/src/js/controllers/wallet-details.controller.js similarity index 94% rename from src/js/controllers/walletDetails.js rename to src/js/controllers/wallet-details.controller.js index f984a9209..365ce86a3 100644 --- a/src/js/controllers/walletDetails.js +++ b/src/js/controllers/wallet-details.controller.js @@ -26,27 +26,6 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun }; var setPendingTxps = function(txps) { - - /* Uncomment to test multiple outputs */ - - // var txp = { - // message: 'test multi-output', - // fee: 1000, - // createdOn: new Date() / 1000, - // outputs: [], - // wallet: $scope.wallet - // }; - // - // function addOutput(n) { - // txp.outputs.push({ - // amount: 600, - // toAddress: '2N8bhEwbKtMvR2jqMRcTCQqzHP6zXGToXcK', - // message: 'output #' + (Number(n) + 1) - // }); - // }; - // lodash.times(15, addOutput); - // txps.push(txp); - if (!txps) { $scope.txps = []; return; @@ -386,8 +365,6 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun }); $scope.$on("$ionicView.beforeEnter", function(event, data) { - sendFlowService.clear(); - configService.whenAvailable(function (config) { $scope.selectedPriceDisplay = config.wallet.settings.priceDisplay; @@ -485,16 +462,10 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun } $scope.goToSend = function() { - sendFlowService.startSend({ + sendFlowService.start({ fromWalletId: $scope.wallet.id }); - // Go home first so that the Home tab works properly - $state.go('tabs.home').then(function () { - $ionicHistory.clearHistory(); - $state.go('tabs.send'); - }); - }; $scope.goToReceive = function() { $state.go('tabs.home', { diff --git a/src/js/controllers/walletSelectorController.js b/src/js/controllers/wallet-selector.controller.js similarity index 81% rename from src/js/controllers/walletSelectorController.js rename to src/js/controllers/wallet-selector.controller.js index 777871e44..06e6179da 100644 --- a/src/js/controllers/walletSelectorController.js +++ b/src/js/controllers/wallet-selector.controller.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $rootScope, $state, $log, $ionicHistory, sendFlowService, configService, gettextCatalog, profileService, txFormatService) { +angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $state, sendFlowService, configService, gettextCatalog, profileService, txFormatService) { var fromWalletId = ''; var priceDisplayAsFiat = false; @@ -12,31 +12,22 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu function onBeforeEnter(event, data) { if (data.direction == "back") { - sendFlowService.popState(); + sendFlowService.state.pop(); } - console.log('walletSelector onBeforeEnter after back sendflow', sendFlowService.state); - $scope.params = sendFlowService.getStateClone(); + $scope.params = sendFlowService.state.getClone(); + + console.log('walletSelector onBeforeEnter after back sendflow', $scope.params); var config = configService.getSync().wallet.settings; priceDisplayAsFiat = config.priceDisplay === 'fiat'; unitDecimals = config.unitDecimals; unitsFromSatoshis = 1 / config.unitToSatoshi; - switch($state.current.name) { - case 'tabs.send.wallet-to-wallet': - $scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets'); - break; - case 'tabs.send.destination': - if ($scope.params.fromWalletId && !$scope.params.thirdParty) { - $scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets'); - } - break; - default: - if (!$scope.params.thirdParty) { - $scope.sendFlowTitle = gettextCatalog.getString('Send'); - } - // nop + if ($scope.params.isWalletTransfer) { + $scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets'); + } else if (!$scope.params.thirdParty) { + $scope.sendFlowTitle = gettextCatalog.getString('Send'); } $scope.coin = false; // Wallets to show (for destination screen or contacts) @@ -99,21 +90,12 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu $scope.requestAmountSecondary = fiatAmount; $scope.requestCurrencySecondary = fiatCurrrency; } + $scope.$apply(); } }); } } - function getNextStep(params) { - if (!params.toWalletId && !params.toAddress) { // If we have no toAddress or fromWallet - return 'tabs.send.destination'; - } else if (!params.amount) { // If we have no amount - return 'tabs.send.amount'; - } else { // If we do have them - return 'tabs.send.review'; - } - } - function handleThirdPartyIfShapeshift() { console.log($scope.thirdParty, $scope.coin); if ($scope.thirdParty.id === 'shapeshift' && $scope.type === 'destination') { // Shapeshift wants to know the @@ -191,20 +173,17 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu $scope.useWallet = function(wallet) { - var params = sendFlowService.getStateClone(); + var params = sendFlowService.state.getClone(); if ($scope.type === 'origin') { // we're on the origin screen, set wallet to send from params.fromWalletId = wallet.id; } else { // we're on the destination screen, set wallet to send to params.toWalletId = wallet.id; } - sendFlowService.pushState(params); - var nextStep = getNextStep(params); - console.log('walletSelector nextStep', nextStep); - $state.transitionTo(nextStep, $scope.params); + sendFlowService.goNext(params); }; $scope.goBack = function() { - $ionicHistory.goBack(); + sendFlowService.router.goBack(); } }); \ No newline at end of file diff --git a/src/js/directives/incomingDataMenu.js b/src/js/directives/incomingDataMenu.js index 21478102b..78856e62f 100644 --- a/src/js/directives/incomingDataMenu.js +++ b/src/js/directives/incomingDataMenu.js @@ -1,23 +1,28 @@ 'use strict'; angular.module('copayApp.directives') - .directive('incomingDataMenu', function($timeout, $rootScope, $state, externalLinkService) { + .directive('incomingDataMenu', function($timeout, $rootScope, $state, externalLinkService, sendFlowService, bitcoinCashJsService) { return { restrict: 'E', templateUrl: 'views/includes/incomingDataMenu.html', link: function(scope, element, attrs) { $rootScope.$on('incomingDataMenu.showMenu', function(event, data) { $timeout(function() { - scope.data = data.data; - scope.type = data.type; - scope.showMenu = true; - scope.https = false; + scope.data = data; - if (scope.type === 'url') { - if (scope.data.indexOf('https://') === 0) { - scope.https = true; - } + if (scope.data.parsed.privateKey) { + scope.type = "privateKey"; + } else if (scope.data.parsed.url) { + scope.type = "url"; + } else if (scope.data.parsed.publicAddress) { + scope.type = "bitcoinAddress"; + var prefix = scope.data.parsed.isTestnet ? 'bchtest:' : 'bitcoincash:'; + scope.data.toAddress = (prefix + scope.data.parsed.publicAddress.cashAddr) || scope.data.parsed.publicAddress.legacy || scope.data.parsed.publicAddress.bitpay; + } else { + scope.type = "text"; } + + scope.showMenu = true; }); }); scope.hide = function() { @@ -28,18 +33,9 @@ angular.module('copayApp.directives') externalLinkService.open(url); }; scope.sendPaymentToAddress = function(bitcoinAddress) { - var noPrefixInAddress = 0; - if (bitcoinAddress.toLowerCase().indexOf('bitcoin') < 0) { - noPrefixInAddress = 1; - } scope.showMenu = false; - $state.go('tabs.send').then(function() { - $timeout(function() { - $state.transitionTo('tabs.send.amount', { - toAddress: bitcoinAddress, - noPrefix: noPrefixInAddress - }); - }, 50); + sendFlowService.start({ + data: bitcoinAddress }); }; scope.addToAddressBook = function(bitcoinAddress) { diff --git a/src/js/directives/shapeshiftCoinTrader.js b/src/js/directives/shapeshiftCoinTrader.js index 60cc66bdf..793f380fb 100644 --- a/src/js/directives/shapeshiftCoinTrader.js +++ b/src/js/directives/shapeshiftCoinTrader.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService, incomingData, ongoingProcess) { +angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService, incomingDataService, ongoingProcess) { return { restrict: 'E', transclude: true, @@ -111,7 +111,8 @@ angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function orderId: $scope.depositInfo.orderId }; - if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) { + // How to handle this + if (incomingDataService.redir(sendAddress, 'shapeshift', shapeshiftData)) { ongoingProcess.set('connectingShapeshift', false); return; } diff --git a/src/js/services/bitcoin-uri.service.js b/src/js/services/bitcoin-uri.service.js new file mode 100644 index 000000000..3da84f3da --- /dev/null +++ b/src/js/services/bitcoin-uri.service.js @@ -0,0 +1,416 @@ +'use strict'; + +// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +// https://github.com/bitcoin/bips/blob/master/bip-0072.mediawiki + +(function(){ + + angular + .module('bitcoincom.services') + .factory('bitcoinUriService', bitcoinUriService); + + function bitcoinUriService(bitcoinCashJsService, bwcService, $log) { + var bch = bitcoinCashJsService.getBitcoinCashJs(); + var bitcore = bwcService.getBitcore(); + + var service = { + parse: parse + }; + + return service; + + function bitpayAddrOnMainnet(address) { + var Address = bch.Address; + var BitpayFormat = Address.BitpayFormat; + + var mainnet = bch.Networks.mainnet; + + var result = null; + if (address[0] == 'C') { + try { + result = Address.fromString(address, mainnet, 'pubkeyhash', BitpayFormat); + } catch (e) {}; + + } else if (address[0] == 'H') { + try { + result = Address.fromString(address, mainnet, 'scripthash', BitpayFormat); + } catch (e) {}; + + } + return result; + } + + function cashAddrOnMainnet(address) { + var Address = bch.Address; + var CashAddrFormat = Address.CashAddrFormat; + + var mainnet = bch.Networks.mainnet; + + var prefixed = 'bitcoincash:' + address; + var result = null; + if (address[0] == 'q') { + try { + result = Address.fromString(prefixed, mainnet, 'pubkeyhash', CashAddrFormat); + } catch (e) {}; + + } else if (address[0] == 'p') { + try { + result = Address.fromString(prefixed, mainnet, 'scripthash', CashAddrFormat); + } catch (e) {}; + + } + return result; + } + + function cashAddrOnTestnet(address) { + var Address = bch.Address; + var CashAddrFormat = Address.CashAddrFormat; + + var testnet = bch.Networks.testnet; + + var prefixed = 'bchtest:' + address; + var result = null; + if (address[0] == 'q') { + try { + result = Address.fromString(prefixed, testnet, 'pubkeyhash', CashAddrFormat); + } catch (e) {}; + + } else if (address[0] == 'p') { + try { + result = Address.fromString(prefixed, testnet, 'scripthash', CashAddrFormat); + } catch (e) {}; + + } + return result; + } + + function infoFromWalletImportText(data) { + var split = data.split('|'); + // Copay seems to use extra parameter for coin. + if (split.length < 5 || split.length > 6) { + return null; + } + + var type = parseInt(split[0], 10); + if (isNaN(type)) { + return null; + } + + var data = split[1]; + var network = split[2]; + if (!(network === 'livenet' || network === 'testnet')) { + return null; + } + var isTestnet = network === 'testnet'; + + var derivationPath = split[3]; + if (!/^m\/\d+'\/\d+'\/\d+'$/.test(derivationPath)) { + return null; + } + + var hasPassphraseText = split[4]; + if (!(hasPassphraseText === 'true' || hasPassphraseText === 'false')) { + return null; + } + var hasPassphrase = hasPassphraseText === 'true'; + + var coin; // Intentionally undefined as may not be present + if (split.length > 5) { + var coinText = split[5]; + if (!(coinText === 'bch' || coinText === 'btc')) { + return null; + } + coin = coinText; + } + + return { + type: type, + data: data, + isTestnet: isTestnet, + derivationPath: derivationPath, + hasPassphrase: hasPassphrase, + coin: coin + }; + } + + /* + For parsing: + BIP21 + BIP72 + + returns: + { + amount: '', + amountInSatoshis: 0, + bareUrl: '', + coin: '', + copayInvitation: '', + import: { // testnet info in root, coin info in root if available + data: '', + derivationPath: '', + hasPassphrase: false, + type: 1, + }, + isValid: false, + label: '', + message: '', + other: { + somethingIDontUnderstand: 'Its value' + }, + privateKey: { + encrypted: '', + wif: '' + }'', + publicAddress: { + bitpay: '', + cashAddr: '', + legacy: '', + }, + req: { + "req-param0": '', + "req-param1": '' + }, + testnet: false, + url: '' // For BIP70 + } + + Only fields that are present in the data are defined in the returned object. Both privateKey and publicAddress only have 1 field defined, if they exist at all. + The exception to this is the coin property, which is determined from other data, such as the prefix or address type. + + */ + + function parse(data) { + var parsed = { + isValid: false + }; + + if (typeof data !== 'string') { + return parsed; + } + + // Identify prefix + var trimmed = data.trim(); + var colonSplit = /^([\w-]*):?(.*)$/.exec(trimmed); + if (!colonSplit) { + return parsed; + } + + var addressAndParams = ''; + var preColonLower = colonSplit[1].toLowerCase(); + if (preColonLower === 'bitcoin') { + parsed.coin = 'btc'; + addressAndParams = colonSplit[2].trim(); + console.log('Is btc'); + + } else if (/^(?:bitcoincash)|(?:bitcoin-cash)$/.test(preColonLower)) { + parsed.coin = 'bch'; + parsed.test = false; + addressAndParams = colonSplit[2].trim(); + console.log('Is bch'); + + } else if (/^(?:bchtest)$/.test(preColonLower)) { + parsed.coin = 'bch'; + parsed.isTestnet = true; + addressAndParams = colonSplit[2].trim(); + console.log('Is bch'); + + } else if (colonSplit[2] === '') { + // No colon and no coin specifier. + addressAndParams = colonSplit[1].trim(); + console.log('No prefix.'); + + } else if (/^https?$/.test(colonSplit[1])) { // Plain URL + addressAndParams = trimmed; + + } else if (colonSplit[2].indexOf('|') == 0) { // Import + addressAndParams = trimmed + } else { + // Something we don't recognise + return parsed; + } + + // Remove erroneous leading slashes + //var leadingSlashes = /^\/*([^\/]+(?:.*))$/.exec(addressAndParams); + var leadingSlashes = /^\/*(.*)$/.exec(addressAndParams); + if (!leadingSlashes) { + return parsed; + } + addressAndParams = leadingSlashes[1]; + + var questionMarkSplit = /^([^\?]*)\??([^\?]*)$/.exec(addressAndParams); + if (!questionMarkSplit) { + return parsed; + } + + var address = questionMarkSplit[1]; + var params = questionMarkSplit[2]; + + if (params.length > 0) { + var paramsSplit = params.split('&'); + var others; + var req; + var paramCount = paramsSplit.length; + for(var i = 0; i < paramCount; i++) { + var param = paramsSplit[i]; + var valueSplit = param.split('='); + if (valueSplit.length !== 2) { + return parsed; + } + + var key = valueSplit[0]; + var value = valueSplit[1]; + var decodedValue = decodeURIComponent(value); + switch(key) { + case 'amount': + var amount = parseFloat(decodedValue); + if (amount) { // Checking for NaN, or no numbers at all etc. & convert to satoshi + parsed.amount = decodedValue; // Need to check if a currency is precised + parsed.amountInSatoshis = amount * 100000000 + } else { + return parsed; + } + break; + + case 'label': + parsed.label = decodedValue; + break; + + case 'message': + parsed.message = decodedValue; + break; + + case 'r': + // Could use a more comprehesive regex to test URL validity, but then how would we know + // which part of the validation it failed? + if (decodedValue.startsWith('https://')) { + parsed.url = decodedValue; + } else { + return parsed; + } + break; + + default: + if (key.startsWith('req-')) { + req = req || {}; + req[key] = decodedValue; + } else { + others = others || {}; + others[key] = decodedValue; + } + } + + }; + } + + parsed.others = others; + parsed.req = req; + + + if (address) { + var addressLowerCase = address.toLowerCase(); + var copayInvitationRe = /^[0-9A-HJ-NP-Za-km-z]{70,80}$/; + //var legacyRe = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/; + //var legacyTestnetRe = /^[mn][a-km-zA-HJ-NP-Z1-9]{25,34}$/; + var privateKeyEncryptedRe = /^6P[1-9A-HJ-NP-Za-km-z]{56}$/; + var privateKeyForUncompressedPublicKeyRe = /^5[1-9A-HJ-NP-Za-km-z]{50}$/; + var privateKeyForUncompressedPublicKeyTestnetRe = /^9[1-9A-HJ-NP-Za-km-z]{50}$/; + var privateKeyForCompressedPublicKeyRe = /^[KL][1-9A-HJ-NP-Za-km-z]{51}$/; + var privateKeyForCompressedPublicKeyTestnetRe = /^[c][1-9A-HJ-NP-Za-km-z]{51}$/; + var urlRe = /^https?:\/\/.+/; + + var bitpayAddrMainnet = bitpayAddrOnMainnet(address); + var cashAddrTestnet = cashAddrOnTestnet(addressLowerCase); + var cashAddrMainnet = cashAddrOnMainnet(addressLowerCase); + var importInfo = infoFromWalletImportText(address); + var privateKey = ''; + + if (parsed.isTestnet && cashAddrTestnet) { + parsed.address = addressLowerCase; + parsed.coin = 'bch'; + parsed.publicAddress = { + cashAddr: addressLowerCase + }; + parsed.isValid = true; + + } else if (cashAddrMainnet) { + parsed.coin = 'bch'; + parsed.publicAddress = { + cashAddr: addressLowerCase + }; + parsed.isTestnet = false; + parsed.isValid = true; + + } else if (bitcore.Address.isValid(address, 'livenet')) { + parsed.publicAddress = { + legacy: address + }; + parsed.isTestnet = false; + parsed.isValid = true; + + } else if (bitcore.Address.isValid(address, 'testnet')) { + parsed.publicAddress = { + legacy: address + }; + parsed.isTestnet = true; + parsed.isValid = true; + + } else if (bitpayAddrMainnet) { + parsed.coin = 'bch'; + parsed.publicAddress = { + bitpay: address + }; + parsed.isTestnet = false; + parsed.isValid = true; + + } else if (copayInvitationRe.test(address) ) { + parsed.copayInvitation = address; + parsed.isValid = true; + + } else if (privateKeyForUncompressedPublicKeyRe.test(address) || privateKeyForCompressedPublicKeyRe.test(address)) { + privateKey = address; + try { + new bitcore.PrivateKey(privateKey, 'livenet'); + parsed.privateKey = { wif: privateKey }; + parsed.isTestnet = false; + parsed.isValid = true; + } catch (e) {} + + } else if (privateKeyForUncompressedPublicKeyTestnetRe.test(address) || privateKeyForCompressedPublicKeyTestnetRe.test(address)) { + privateKey = address; + try { + new bitcore.PrivateKey(privateKey, 'testnet'); + parsed.privateKey = { wif: privateKey }; + parsed.isTestnet = true; + parsed.isValid = true; + } catch (e) {} + + } else if (privateKeyEncryptedRe.test(address)) { + parsed.privateKey = { encrypted: address }; + parsed.isValid = true; + + } else if (urlRe.test(address)) { + parsed.bareUrl = trimmed; + parsed.isValid = true; + + } else if (importInfo) { + parsed.import = { + type: importInfo.type, + data: importInfo.data, + derivationPath: importInfo.derivationPath, + hasPassphrase: importInfo.hasPassphrase + }; + parsed.coin = importInfo.coin; + parsed.isTestnet = importInfo.isTestnet; + parsed.isValid = true; + } + + } else { + parsed.isValid = !!parsed.url; // BIP72 + } + + return parsed; + } + + } + +})(); diff --git a/src/js/services/bitcoin-uri.service.spec.js b/src/js/services/bitcoin-uri.service.spec.js new file mode 100644 index 000000000..2ddbd0d2e --- /dev/null +++ b/src/js/services/bitcoin-uri.service.spec.js @@ -0,0 +1,429 @@ +describe('bitcoinUriService', function() { + var bitcoinUriService; + + beforeEach(function() { + module('bitcoinCashJsModule'); + module('bitcoincom.services'); + module('bwcModule'); + + inject(function($injector){ + bitcoinUriService = $injector.get('bitcoinUriService'); + }); + }); + + + it('Bitcoin BIP72', function() { + + var parsed = bitcoinUriService.parse('bitcoin:?r=https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('btc'); + expect(parsed.isTestnet).toBeUndefined(); + expect(parsed.publicAddress).toBeUndefined(); + expect(parsed.url).toBe('https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr'); + }); + + it('Bitcoin Cash BIP72', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:?r=https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress).toBeUndefined(); + expect(parsed.isTestnet).toBeUndefined(); + expect(parsed.url).toBe('https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu'); + }); + + it('Bitcoin Cash prefix with legacy address', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.legacy).toBe('1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin Cash prefix with legacy address on testnet', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.legacy).toBe('mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn'); + expect(parsed.isTestnet).toBe(true); + }); + + it('Bitcoin Cash uri with extended params', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc?unknown=something&mystery=Melton%20probang&req-one=ichi&req-beta=Ni%20san'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.others.mystery).toBe('Melton probang'); + expect(parsed.others.unknown).toBe('something'); + expect(parsed.publicAddress.cashAddr).toBe('qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc'); + expect(parsed.req['req-beta']).toBe('Ni san'); + expect(parsed.req['req-one']).toBe('ichi'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin Cash uri with invalid amount', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:qq0knhwj4d5zy3kdph24w6etq58vwzua6sm7lhcmuk?amount=three'); + + expect(parsed.isValid).toBe(false); + }); + + + it('Bitcoin testnet address', function() { + + var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBeUndefined(); + expect(parsed.publicAddress.legacy).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + expect(parsed.isTestnet).toBe(true); + }); + + it('Bitcoin uri', function() { + + var parsed = bitcoinUriService.parse('bitcoin:15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('btc'); + expect(parsed.publicAddress.legacy).toBe('15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin uri with encoded label', function() { + + var parsed = bitcoinUriService.parse('bitcoin:1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT?label=Mr.%20Smith'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('btc'); + expect(parsed.label).toBe('Mr. Smith'); + expect(parsed.publicAddress.legacy).toBe('1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin uri with params', function() { + + var parsed = bitcoinUriService.parse('bitcoin:12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu?amount=20.3&label=Luke-Jr&message=Donation%20for%20project%20xyz'); + + expect(parsed.isValid).toBe(true); + expect(parsed.amount).toBe('20.3'); + expect(parsed.amountInSatoshis).toBe(2030000000); + expect(parsed.coin).toBe('btc'); + expect(parsed.label).toBe('Luke-Jr'); + expect(parsed.publicAddress.legacy).toBe('12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu'); + expect(parsed.message).toBe('Donation for project xyz'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin uri with slash', function() { + + var parsed = bitcoinUriService.parse('bitcoin:/1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('btc'); + expect(parsed.publicAddress.legacy).toBe('1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin uri with slashes', function() { + + var parsed = bitcoinUriService.parse('bitcoin://18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('btc'); + expect(parsed.publicAddress.legacy).toBe('18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitcoin uri with space', function() { + + var parsed = bitcoinUriService.parse('bitcoin: 19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('btc'); + expect(parsed.publicAddress.legacy).toBe('19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d'); + expect(parsed.isTestnet).toBe(false); + }); + + it('Bitpay without prefix', function() { + + var parsed = bitcoinUriService.parse('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.bitpay).toBe('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz'); + expect(parsed.isTestnet).toBe(false); + }); + + it('legacy address', function() { + + var parsed = bitcoinUriService.parse('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBeUndefined(); + expect(parsed.publicAddress.legacy).toBe('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW'); + expect(parsed.isTestnet).toBe(false); + }); + + it('cashAddr testnet with prefix', function() { + + var parsed = bitcoinUriService.parse('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt'); + expect(parsed.isTestnet).toBe(true); + }); + + it('cashAddr uppercase', function() { + + var parsed = bitcoinUriService.parse('BITCOINCASH:QZZG9NMC5VX8GAP6XFATX3TWNSDN2YRMCSSULSMY44'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qzzg9nmc5vx8gap6xfatx3twnsdn2yrmcssulsmy44'); + expect(parsed.isTestnet).toBe(false); + }); + + it('cashAddr with dash', function() { + + var parsed = bitcoinUriService.parse('bitcoin-cash:qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0'); + expect(parsed.isTestnet).toBe(false); + }); + + it('cashAddr with prefix', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92'); + expect(parsed.isTestnet).toBe(false); + }); + + it('cashAddr with slash', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:/qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a'); + expect(parsed.isTestnet).toBe(false); + }); + + it('cashAddr with slashes', function() { + + var parsed = bitcoinUriService.parse('bitcoincash://qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c'); + expect(parsed.isTestnet).toBe(false); + }); + + it('cashAddr with space', function() { + + var parsed = bitcoinUriService.parse('bitcoincash: qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce'); + expect(parsed.isTestnet).toBe(false); + }); + + + it('cashAddr with space on testnet', function() { + + var parsed = bitcoinUriService.parse('bchtest: qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j'); + expect(parsed.isTestnet).toBe(true); + }); + + it('cashAddr without prefix', function() { + + var parsed = bitcoinUriService.parse('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq'); + + expect(parsed.isValid).toBe(true); + expect(parsed.coin).toBe('bch'); + expect(parsed.publicAddress.cashAddr).toBe('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq'); + expect(parsed.isTestnet).toBe(false); + }); + + it('copay invitation', function() { + + var parsed = bitcoinUriService.parse('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch'); + + expect(parsed.isValid).toBe(true); + expect(parsed.copayInvitation).toBe('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch'); + }); + + + it ('import BCH wallet no password', function() { + var parsed = bitcoinUriService.parse("1|suggest route obvious broccoli good position hidden tone history around final lobster|livenet|m/44'/0'/0'|false"); + + expect(parsed.isValid).toBe(true); + expect(parsed.import.type).toBe(1); + expect(parsed.import.data).toBe('suggest route obvious broccoli good position hidden tone history around final lobster'); + expect(parsed.isTestnet).toBe(false); + expect(parsed.import.derivationPath).toBe("m/44'/0'/0'"); + expect(parsed.import.hasPassphrase).toBe(false); + }); + + it ('import BCH wallet with passphrase', function() { + var parsed = bitcoinUriService.parse("1|fringe hazard all hobby trap myth fire stand sock empty soon east|livenet|m/44'/0'/0'|true"); + + expect(parsed.isValid).toBe(true); + expect(parsed.import.type).toBe(1); + expect(parsed.import.data).toBe('fringe hazard all hobby trap myth fire stand sock empty soon east'); + expect(parsed.isTestnet).toBe(false); + expect(parsed.import.derivationPath).toBe("m/44'/0'/0'"); + expect(parsed.import.hasPassphrase).toBe(true); + }); + + it ('import BTC wallet testnet', function() { + // From copay + var parsed = bitcoinUriService.parse("1|cat wealth column firm wet sauce tornado era feature monster click eyebrow|testnet|m/44'/1'/0'|false|btc"); + + expect(parsed.isValid).toBe(true); + expect(parsed.import.type).toBe(1); + expect(parsed.import.data).toBe('cat wealth column firm wet sauce tornado era feature monster click eyebrow'); + expect(parsed.isTestnet).toBe(true); + expect(parsed.import.derivationPath).toBe("m/44'/1'/0'"); + expect(parsed.import.hasPassphrase).toBe(false); + }); + + // Invalid addresses from https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md + it('invalid cashAddr style 1', function() { + var parsed = bitcoinUriService.parse('prefix:x64nx6hz'); + expect(parsed.isValid).toBe(false); + }); + + it('invalid cashAddr style 2', function() { + var parsed = bitcoinUriService.parse('p:gpf8m4h7'); + expect(parsed.isValid).toBe(false); + }); + + it('invalid cashAddr style 3', function() { + var parsed = bitcoinUriService.parse('bitcoincash:qpzry9x8gf2tvdw0s3jn54khce6mua7lcw20ayyn'); + expect(parsed.isValid).toBe(false); + }); + + it('invalid cashAddr style 4', function() { + var parsed = bitcoinUriService.parse('bchtest:testnetaddress4d6njnut'); + expect(parsed.isValid).toBe(false); + }); + + it('invalid cashAddr style 5', function() { + var parsed = bitcoinUriService.parse('bchreg:555555555555555555555555555555555555555555555udxmlmrz'); + expect(parsed.isValid).toBe(false); + }); + + it('non-string', function() { + + var parsed = bitcoinUriService.parse([1, 2, 3, 4]); + + expect(parsed.isValid).toBe(false); + }); + + it('private key encrypted with BIP38', function() { + + var parsed = bitcoinUriService.parse('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX'); + + expect(parsed.isValid).toBe(true); + expect(parsed.privateKey.encrypted).toBe('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX'); + }); + + it('private key for compressed pubkey mainnet', function() { + + var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ'); + + expect(parsed.isValid).toBe(true); + expect(parsed.privateKey.wif).toBe('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ'); + expect(parsed.isTestnet).toBe(false); + }); + + it('private key for compressed pubkey mainnet with wrong checksum', function() { + + var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTu'); + + expect(parsed.isValid).toBe(false); + }); + + it('private key for compressed pubkey testnet', function() { + + var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm'); + + expect(parsed.isValid).toBe(true); + expect(parsed.privateKey.wif).toBe('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm'); + expect(parsed.isTestnet).toBe(true); + }); + + it('private key for compressed pubkey testnet with wrong checksum', function() { + + var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMM'); + + expect(parsed.isValid).toBe(false); + }); + + it('private key for uncompressed pubkey mainnet', function() { + + var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx'); + + expect(parsed.isValid).toBe(true); + expect(parsed.privateKey.wif).toBe('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx'); + expect(parsed.isTestnet).toBe(false); + }); + + it('private key for uncompressed pubkey mainnet with wrong checksum', function() { + + var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTTwx'); + + expect(parsed.isValid).toBe(false); + }); + + it('private key for uncompressed pubkey testnet', function() { + + var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc'); + + expect(parsed.isValid).toBe(true); + expect(parsed.privateKey.wif).toBe('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc'); + expect(parsed.isTestnet).toBe(true); + }); + + it('private key for uncompressed pubkey testnet with wrong checksum', function() { + + var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcC'); + + expect(parsed.isValid).toBe(false); + }); + + it('URL only, http', function() { + + var parsed = bitcoinUriService.parse('http://paperwallet.bitcoin.com'); + + expect(parsed.isValid).toBe(true); + expect(parsed.bareUrl).toBe('http://paperwallet.bitcoin.com'); + }); + + it('URL only, https with query', function() { + + var parsed = bitcoinUriService.parse('https://purse.io/?one=two&three=four'); + + expect(parsed.isValid).toBe(true); + expect(parsed.bareUrl).toBe('https://purse.io/?one=two&three=four'); + }); + +}); \ No newline at end of file diff --git a/src/js/services/incoming-data.service.js b/src/js/services/incoming-data.service.js new file mode 100644 index 000000000..eece6d17c --- /dev/null +++ b/src/js/services/incoming-data.service.js @@ -0,0 +1,79 @@ +'use strict'; + +/** + * incomingDataService is an intermediate to redirect either to the sendFlow + * or to import/join a wallet. + */ +angular.module('copayApp.services').factory('incomingDataService', function(bitcoinUriService, $log, $state, $rootScope, scannerService, sendFlowService, gettextCatalog) { + + var root = {}; + + root.showMenu = function(data) { + $rootScope.$broadcast('incomingDataMenu.showMenu', data); + }; + + root.redir = function(data, cbError) { + var parsed = bitcoinUriService.parse(data); + + console.log(parsed); + $log.debug(parsed); + + + if (parsed.isValid) { + if (parsed.isTestnet) { + if (cbError) { + var errorMessage = gettextCatalog.getString('Testnet is not supported.'); + cbError(new Error(errorMessage)); + } + } else { + scannerService.pausePreview(); + + /** + * Strategy for the action + */ + if (parsed.copayInvitation) { + $state.go('tabs.home').then(function() { + $state.transitionTo('tabs.add.join', { + url: data + }); + }); + } else if (parsed.import) { + $state.go('tabs.home').then(function() { + $state.transitionTo('tabs.add.import', { + code: data + }); + }); + } else if ( + !parsed.isValid + || parsed.privateKey + || (sendFlowService.state.isEmpty() && !parsed.url && !parsed.amount) + ) { + root.showMenu({ + original: data, + parsed: parsed + }); + } else { + var state = sendFlowService.state.getClone(); + state.data = data; + + sendFlowService.start(state, function onError(err) { + /** + * OnError, open the menu (link not validated) + */ + root.showMenu({ + original: data, + parsed: parsed + }); + }); + } + } + } else { + if (cbError) { + var errorMessage = gettextCatalog.getString('Data not recognised.'); + cbError(new Error(errorMessage)); + } + } + }; + + return root; +}); diff --git a/src/js/services/incomingData.js b/src/js/services/incomingData.js deleted file mode 100644 index 0bf708d8a..000000000 --- a/src/js/services/incomingData.js +++ /dev/null @@ -1,475 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('incomingData', function($log, $state, $timeout, $ionicHistory, bitcore, bitcoreCash, $rootScope, payproService, scannerService, sendFlowService, appConfigService, popupService, gettextCatalog, bitcoinCashJsService) { - - var root = {}; - - root.showMenu = function(data) { - $rootScope.$broadcast('incomingDataMenu.showMenu', data); - }; - - root.redir = function(data, serviceId, serviceData) { - var originalAddress = null; - var noPrefixInAddress = 0; - - if (data.toLowerCase().indexOf('bitcoin') < 0) { - noPrefixInAddress = 1; - } - - if (typeof(data) == 'string' && !(/^bitcoin(cash)?:\?r=[\w+]/).exec(data) && (data.toLowerCase().indexOf('bitcoincash:') >= 0 || data[0] == 'q' || data[0] == 'p' || data[0] == 'C' || data[0] == 'H')) { - try { - noPrefixInAddress = 0; - - if (data[0] == 'p' || data[0] == 'q') { - data = 'bitcoincash:' + data; - } - var paramString = ''; - if (data.indexOf('?') >= 0) { - paramString = data.substring(data.indexOf('?')); - data = data.substring(0, data.indexOf('?')); - } - - if (data.indexOf('BITCOINCASH:') >= 0) { - data = data.toLowerCase(); - } - originalAddress = data.replace('bitcoincash:', ''); - var legacyAddress = bitcoinCashJsService.readAddress(data).legacy; - data = 'bitcoincash:' + legacyAddress + paramString; - } catch (ex) {} - } - - $log.debug('Processing incoming data: ' + data); - - function sanitizeUri(data) { - // Fixes when a region uses comma to separate decimals - var regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i; - var match = regex.exec(data); - if (!match || match.length === 0) { - return data; - } - var value = match[0].replace(',', '.'); - var newUri = data.replace(regex, value); - - // mobile devices, uris like copay://glidera - newUri.replace('://', ':'); - - return newUri; - } - - function getParameterByName(name, url) { - if (!url) return; - name = name.replace(/[\[\]]/g, "\\$&"); - var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), - results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, " ")); - } - - function checkPrivateKey(privateKey) { - try { - new bitcore.PrivateKey(privateKey, 'livenet'); - } catch (err) { - return false; - } - return true; - } - - function goSend(addr, amount, message, coin, serviceId, serviceData) { - $state.go('tabs.send', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.send' ? false : true - }); - // Timeout is required to enable the "Back" button - $timeout(function() { - var params = sendFlowService.getStateClone(); - - if (amount) { - params.amount = amount; - } - - if (addr) { - params.toAddress = addr; - params.displayAddress = originalAddress ? originalAddress : addr; - } - - if (coin) { - params.coin = coin; - } - - if (noPrefixInAddress) { - params.noPrefixInAddress = noPrefixInAddress; - } - - if (serviceId) { - params.thirdParty = []; - params.thirdParty.id = serviceId; - params.thirdParty.data = serviceData; - sendFlowService.pushState(params); - $state.transitionTo('tabs.send.amount'); - } else { - sendFlowService.pushState(params); - $state.transitionTo('tabs.send.origin'); - } - }, 100); - } - // data extensions for Payment Protocol with non-backwards-compatible request - if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) { - var coin = data.indexOf('bitcoincash') >= 0 ? 'bch' : 'btc'; - data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, '')); - if (coin == 'bch') { - payproService.getPayProDetailsViaHttp(data, function onGetPayProDetailsViaHttp(err, details) { - if (err) { - var message = err.toString(); - if (typeof err.data === 'string') { - // i.e. 'This invoice is no longer accepting payments' - message = gettextCatalog.getString(err.data); - } - popupService.showAlert(gettextCatalog.getString('Error'), message) - } else { - handlePayPro(details, coin); - } - }); - } else { - payproService.getPayProDetails(data, coin, function onGetPayProDetails(err, details) { - if (err) { - popupService.showAlert(gettextCatalog.getString('Error'), err); - } else { - handlePayPro(details, coin); - } - }); - } - return true; - } - - data = sanitizeUri(data); - - // Bitcoin URL - if (bitcore.URI.isValid(data)) { - var coin = 'btc'; - var parsed = new bitcore.URI(data); - - var addr = parsed.address ? parsed.address.toString() : ''; - var message = parsed.message; - - var amount = parsed.amount ? parsed.amount : ''; - - if (parsed.r) { - payproService.getPayProDetails(parsed.r, coin, function(err, details) { - if (err) { - if (addr && amount) goSend(addr, amount, message, coin, serviceId, serviceData); - else popupService.showAlert(gettextCatalog.getString('Error'), err); - } else handlePayPro(details, coin); - }); - } else { - goSend(addr, amount, message, coin, serviceId, serviceData); - } - return true; - // Cash URI - } else if (bitcoreCash.URI.isValid(data)) { - var coin = 'bch'; - var parsed = new bitcoreCash.URI(data); - - var addr = parsed.address ? parsed.address.toString() : ''; - var message = parsed.message; - - var amount = parsed.amount ? parsed.amount : ''; - - // paypro not yet supported on cash - if (parsed.r) { - payproService.getPayProDetails(parsed.r, coin, function(err, details) { - if (err) { - if (addr && amount) - goSend(addr, amount, message, coin, serviceId, serviceData); - else - popupService.showAlert(gettextCatalog.getString('Error'), err); - } - handlePayPro(details, coin); - }); - } else { - goSend(addr, amount, message, coin, serviceId, serviceData); - } - return true; - - // Cash URI with bitcoin (btc) address version number? - } else if (bitcore.URI.isValid(data.replace(/^bitcoincash:/,'bitcoin:'))) { - $log.debug('Handling bitcoincash URI with legacy address'); - var coin = 'bch'; - var parsed = new bitcore.URI(data.replace(/^bitcoincash:/,'bitcoin:')); - - var oldAddr = parsed.address ? parsed.address.toString() : ''; - if (!oldAddr) return false; - - var addr = ''; - - var a = bitcore.Address(oldAddr).toObject(); - addr = bitcoreCash.Address.fromObject(a).toString(); - - // Translate address - $log.debug('address transalated to:' + addr); - popupService.showConfirm( - gettextCatalog.getString('Bitcoin cash Payment'), - gettextCatalog.getString('Payment address was translated to new Bitcoin Cash address format: ' + addr), - gettextCatalog.getString('OK'), - gettextCatalog.getString('Cancel'), - function(ret) { - if (!ret) return false; - - var message = parsed.message; - var amount = parsed.amount ? parsed.amount : ''; - - // paypro not yet supported on cash - if (parsed.r) { - payproService.getPayProDetails(parsed.r, coin, function(err, details) { - if (err) { - if (addr && amount) - goSend(addr, amount, message, coin, serviceId, serviceData); - else - popupService.showAlert(gettextCatalog.getString('Error'), err); - } - handlePayPro(details, coin); - }); - } else { - goSend(addr, amount, message, coin, serviceId, serviceData); - } - } - ); - return true; - // Plain URL - } else if (/^https?:\/\//.test(data)) { - payproService.getPayProDetails(data, coin, function(err, details) { - if (err) { - if ($state.includes('tabs.scan')) { - root.showMenu({ - data: data, - type: 'url' - }); - } - return; - } - handlePayPro(details); - return true; - }); - // Plain Address - } else if (bitcore.Address.isValid(data, 'livenet') || bitcore.Address.isValid(data, 'testnet')) { - if ($state.includes('tabs.scan')) { - root.showMenu({ - data: data, - type: 'bitcoinAddress' - }); - } else { - goToAmountPage(data); - } - } else if (bitcoreCash.Address.isValid(data, 'livenet')) { - if ($state.includes('tabs.scan')) { - root.showMenu({ - data: data, - type: 'bitcoinAddress', - coin: 'bch', - }); - } else { - goToAmountPage(data, 'bch'); - } - } else if (data && data.indexOf(appConfigService.name + '://glidera') === 0) { - var code = getParameterByName('code', data); - $ionicHistory.nextViewOptions({ - disableAnimate: true - }); - $state.go('tabs.home', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.home' ? false : true - }).then(function() { - $ionicHistory.nextViewOptions({ - disableAnimate: true - }); - $state.transitionTo('tabs.buyandsell.glidera', { - code: code - }); - }); - return true; - - } else if (data && data.indexOf(appConfigService.name + '://coinbase') === 0) { - var code = getParameterByName('code', data); - $ionicHistory.nextViewOptions({ - disableAnimate: true - }); - $state.go('tabs.home', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.home' ? false : true - }).then(function() { - $ionicHistory.nextViewOptions({ - disableAnimate: true - }); - $state.transitionTo('tabs.buyandsell.coinbase', { - code: code - }); - }); - return true; - - // BitPayCard Authentication - } else if (data && data.indexOf(appConfigService.name + '://') === 0) { - - // Disable BitPay Card - if (!appConfigService._enabledExtensions.debitcard) return false; - - var secret = getParameterByName('secret', data); - var email = getParameterByName('email', data); - var otp = getParameterByName('otp', data); - var reason = getParameterByName('r', data); - - $state.go('tabs.home', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.home' ? false : true - }).then(function() { - switch (reason) { - default: - case '0': - /* For BitPay card binding */ - $state.transitionTo('tabs.bitpayCardIntro', { - secret: secret, - email: email, - otp: otp - }); - break; - } - }); - return true; - - // Join - } else if (data && data.match(/^copay:[0-9A-HJ-NP-Za-km-z]{70,80}$/)) { - $state.go('tabs.home', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.home' ? false : true - }).then(function() { - $state.transitionTo('tabs.add.join', { - url: data - }); - }); - return true; - - // Old join - } else if (data && data.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/)) { - $state.go('tabs.home', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.home' ? false : true - }).then(function() { - $state.transitionTo('tabs.add.join', { - url: data - }); - }); - return true; - } else if (data && (data.substring(0, 2) == '6P' || checkPrivateKey(data))) { - root.showMenu({ - data: data, - type: 'privateKey' - }); - } else if (data && ((data.substring(0, 2) == '1|') || (data.substring(0, 2) == '2|') || (data.substring(0, 2) == '3|'))) { - $state.go('tabs.home').then(function() { - $state.transitionTo('tabs.add.import', { - code: data - }); - }); - return true; - - } else { - if ($state.includes('tabs.scan')) { - root.showMenu({ - data: data, - type: 'text' - }); - } - } - return false; - }; - - function goToAmountPage(toAddress, coin) { - $state.go('tabs.send', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.send' ? false : true - }); - $timeout(function() { - var stateParams = { - toAddress: toAddress, - displayAddress: toAddress, - coin: coin, - noPrefix: 1 - }; - sendFlowService.pushState(stateParams); - $state.transitionTo('tabs.send.origin'); - }, 100); - } - - function handlePayPro(payProData, coin) { - - console.log(payProData); - - var toAddr = payProData.toAddress; - var amount = payProData.amount; - var paymentUrl = payProData.url; - var expires = payProData.expires; - var time = payProData.time; - - if (coin === 'bch') { - var displayAddr = payProData.outputs[0].address; - toAddr = bitcoinCashJsService.readAddress('bitcoincash:' + displayAddr).legacy; - amount = payProData.outputs[0].amount; - paymentUrl = payProData.paymentUrl; - expires = Math.floor(new Date(expires).getTime() / 1000) - time = Math.ceil(new Date(time).getTime() / 1000) - } - - var name = payProData.domain; - - if (payProData.memo.indexOf('eGifter') > -1) { - name = 'eGifter' - } else if (paymentUrl.indexOf('https://bitpay.com') > -1) { - name = 'BitPay'; - } - - var thirdPartyData = { - id: 'bip70', - amount: amount, - caTrusted: true, - name: name, - domain: payProData.domain, - expires: expires, - memo: payProData.memo, - network: 'livenet', - requiredFeeRate: payProData.requiredFeeRate, - selfSigned: 0, - time: time, - displayAddress: displayAddr, - toAddress: toAddr, - url: paymentUrl, - verified: true - }; - - var stateParams = { - amount: thirdPartyData.amount, - toAddress: thirdPartyData.toAddress, - coin: coin, - thirdParty: thirdPartyData - }; - - // fee - if (thirdPartyData.requiredFeeRate) { - stateParams.requiredFeeRate = thirdPartyData.requiredFeeRate * 1024; - } - - // This does not make sense, thirdPartyData gets added by stateParams below - //sendFlowService.pushState(thirdPartyData); - - scannerService.pausePreview(); - $state.go('tabs.send', {}, { - 'reload': true, - 'notify': $state.current.name == 'tabs.send' ? false : true - }).then(function() { - $timeout(function() { - sendFlowService.pushState(stateParams); // Need to do more here - $state.transitionTo('tabs.send.origin'); - }); - }); - } - - return root; -}); diff --git a/src/js/services/openURL.js b/src/js/services/openURL.js index 0f4d6c666..2cf8d95a5 100644 --- a/src/js/services/openURL.js +++ b/src/js/services/openURL.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingData, appConfigService) { +angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingDataService, appConfigService) { var root = {}; var handleOpenURL = function(args) { @@ -23,9 +23,12 @@ angular.module('copayApp.services').factory('openURLService', function($rootScop document.addEventListener('handleopenurl', handleOpenURL, false); - if (!incomingData.redir(url)) { - $log.warn('Unknown URL! : ' + url); - } + incomingDataService.redir(url, function onError(err) { + if (err) { + $log.warn('Unknown URL! : ' + url); + popupService.showAlert(gettextCatalog.getString('Error'), err.message); + } + }); }; var handleResume = function() { diff --git a/src/js/services/send-flow-router.service.js b/src/js/services/send-flow-router.service.js new file mode 100644 index 000000000..32aa8420b --- /dev/null +++ b/src/js/services/send-flow-router.service.js @@ -0,0 +1,85 @@ +'use strict'; + +(function(){ + +angular + .module('bitcoincom.services') + .factory('sendFlowRouterService', sendFlowRouterService); + + function sendFlowRouterService( + sendFlowStateService + , $state, $ionicHistory, $timeout + ) { + + var service = { + // Functions + start: start, + goNext: goNext, + goBack: goBack, + }; + + return service; + + /** + * Start new send flow + */ + function start() { + var state = sendFlowStateService.state; + + if (state.isRequestAmount) { + $state.go('tabs.paymentRequest.amount'); + } else { + if ($state.current.name != 'tabs.send') { + $state.go('tabs.home').then(function () { + $ionicHistory.clearHistory(); + $state.go('tabs.send').then(function () { + $timeout(function () { + goNext(); + }, 60); + }); + }); + } else { + goNext(); + } + } + } + + /** + * Go to the next page + * Routing strategy : https://bitcoindotcom.atlassian.net/wiki/x/BQDWKQ + */ + function goNext() { + var state = sendFlowStateService.state; + + var needsDestination = !state.toWalletId && !state.toAddress; + var needsOrigin = !state.fromWalletId; + var needsAmount = !state.amount && !state.sendMax; + + if (needsDestination) { + if (!state.isWalletTransfer && !state.thirdParty) { + $state.go('tabs.send'); + return; + } else if (!needsOrigin) { + $state.go('tabs.send.destination'); + return; + } + } + + if (needsOrigin) { + $state.go('tabs.send.origin'); + } else if (needsAmount) { + $state.go('tabs.send.amount'); + } else { + $state.go('tabs.send.review'); + } + } + + /** + * Go to the previous page + */ + function goBack() { + $ionicHistory.goBack(); + } + }; + +})(); \ No newline at end of file diff --git a/src/js/services/sendFlowService.js b/src/js/services/send-flow-state.service.js similarity index 50% rename from src/js/services/sendFlowService.js rename to src/js/services/send-flow-state.service.js index 62989b3c5..bec2c8a3c 100644 --- a/src/js/services/sendFlowService.js +++ b/src/js/services/send-flow-state.service.js @@ -3,14 +3,13 @@ (function(){ angular - .module('copayApp.services') - .factory('sendFlowService', sendFlowService); + .module('bitcoincom.services') + .factory('sendFlowStateService', sendFlowStateService); - function sendFlowService($log) { + function sendFlowStateService($log) { var service = { - // 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") + // Variables state: { amount: '', displayAddress: null, @@ -18,29 +17,55 @@ angular sendMax: false, thirdParty: null, toAddress: '', - toWalletId: '' + toWalletId: '', + coin: '', + isRequestAmount: false, + isWalletTransfer: false }, previousStates: [], // Functions + init: init, clear: clear, - getStateClone: getStateClone, + getClone: getClone, map: map, - popState: popState, - pushState: pushState, - startSend: startSend + pop: pop, + push: push, + isEmpty: isEmpty }; return service; + /** + * Init state & stack + * @param {Object} params + */ + function init(params) { + $log.debug("send-flow-state init()"); + + clear(); + + if (params) { + push(params); + } + } + + /** + * Clear a state & stack + */ function clear() { - console.log("sendFlow clear()"); + $log.debug("send-flow-state clear()"); + clearCurrent(); service.previousStates = []; } + /** + * Clear current state only + */ function clearCurrent() { - console.log("sendFlow clearCurrent()"); + $log.debug("send-flow-state clearCurrent()"); + service.state = { amount: '', displayAddress: null, @@ -48,14 +73,17 @@ angular sendMax: false, thirdParty: null, toAddress: '', - toWalletId: '' + toWalletId: '', + coin: '', + isRequestAmount: false, + isWalletTransfer: false } } /** - * Handy for debugging + * Get a clone of the current state */ - function getStateClone() { + function getClone() { var currentState = {}; Object.keys(service.state).forEach(function forCurrentParam(key) { if (typeof service.state[key] !== 'function' && key !== 'previousStates') { @@ -66,22 +94,21 @@ angular } /** - * Clears all previous state + * Fill in the current state from the params + * @param {Object} params */ - function startSend(params) { - console.log('startSend()'); - clear(); - map(params); - } - function map(params) { Object.keys(params).forEach(function forNewParam(key) { service.state[key] = params[key]; }); }; - function popState() { - console.log('sendFlow pop'); + /** + * Pop state + */ + function pop() { + $log.debug('send-flow-state pop'); + if (service.previousStates.length) { var params = service.previousStates.pop(); clearCurrent(); @@ -91,13 +118,25 @@ angular } }; - function pushState(params) { - console.log('sendFlow push'); - var currentParams = getStateClone(); + /** + * Push state + * @param {Object} params + */ + function push(params) { + $log.debug('send-flow-state push'); + + var currentParams = getClone(); service.previousStates.push(currentParams); clearCurrent(); map(params); }; + + /** + * Is empty stack + */ + function isEmpty() { + return service.previousStates.length == 0; + }; }; })(); \ No newline at end of file diff --git a/src/js/services/send-flow.service.js b/src/js/services/send-flow.service.js new file mode 100644 index 000000000..1b02c0d34 --- /dev/null +++ b/src/js/services/send-flow.service.js @@ -0,0 +1,149 @@ +'use strict'; + +(function(){ + +angular + .module('bitcoincom.services') + .factory('sendFlowService', sendFlowService); + + function sendFlowService( + sendFlowStateService, sendFlowRouterService + , bitcoinUriService, payproService, bitcoinCashJsService + , popupService, gettextCatalog + , $state, $log + ) { + + var service = { + // Variables + state: sendFlowStateService, + router: sendFlowRouterService, + + // Functions + start: start, + goNext: goNext, + goBack: goBack + }; + + return service; + + /** + * Start a new send flow + * @param {Object} params + * @param {Function} onError + */ + function start(params, onError) { + $log.debug('send-flow start()'); + + if (params && params.data) { + var res = bitcoinUriService.parse(params.data); + + if (res.isValid) { + + // If BIP70 (url) + if (res.url) { + var url = res.url; + var coin = res.coin || ''; + payproService.getPayProDetails(url, coin, function onGetPayProDetails(err, payProData) { + if (err) { + popupService.showAlert(gettextCatalog.getString('Error'), err); + } else { + var name = payProData.domain; + + // Detect some merchant that we know + if (payProData.memo.indexOf('eGifter') > -1) { + name = 'eGifter' + } else if (paymentUrl.indexOf('https://bitpay.com') > -1) { + name = 'BitPay'; + } + + // Init thirdParty + var thirdPartyData = { + id: 'bip70', + caTrusted: true, + name: name, + domain: payProData.domain, + expires: payProData.expires, + memo: payProData.memo, + network: 'livenet', + requiredFeeRate: payProData.requiredFeeRate, + selfSigned: 0, + time: payProData.time, + url: payProData.url, + verified: true + }; + + // Fill in params + params.amount = payProData.amount, + params.toAddress = payProData.toAddress, + params.coin = coin, + params.thirdParty = thirdPartyData + } + + // Resolve + _next(); + }); + } else { + if (res.coin) { + params.coin = res.coin; + } + + if (res.amountInSatoshis) { + params.amount = res.amountInSatoshis; + } + + if (res.publicAddress) { + var prefix = res.isTestnet ? 'bchtest:' : 'bitcoincash:'; + params.displayAddress = res.publicAddress.cashAddr || res.publicAddress.legacy || res.publicAddress.bitpay; + var formatAddress = res.publicAddress.cashAddr ? prefix + params.displayAddress : params.displayAddress; + params.toAddress = bitcoinCashJsService.readAddress(formatAddress).legacy; + } + + _next(); + } + } else { + if (onError) { + onError(); + } + } + } else { + _next(); + } + + + // Next used for sync the async task + function _next() { + sendFlowStateService.init(params); + + // Routing strategy to -> send-flow-router.service + sendFlowRouterService.start(); + } + } + + /** + * Go to the next step + * @param {Object} state + */ + function goNext(state) { + $log.debug('send-flow goNext()'); + + // Save the current route before leaving + state.route = $state.current.name; + + // Save the state and redirect the user + sendFlowStateService.push(state); + sendFlowRouterService.goNext(); + } + + /** + * Go to the previous step + */ + function goBack() { + $log.debug('send-flow goBack()'); + + // Remove the state on top and redirect the user + sendFlowStateService.pop(); + sendFlowRouterService.goBack(); + } + }; + +})(); \ No newline at end of file diff --git a/src/js/services/shapeshiftService.js b/src/js/services/shapeshiftService.js index 1ce9672ce..b1d2f6e7d 100644 --- a/src/js/services/shapeshiftService.js +++ b/src/js/services/shapeshiftService.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.services').factory('shapeshiftService', function ($http, $interval, $log, lodash, moment, ongoingProcess, shapeshiftApiService, storageService, configService, incomingData, platformInfo, servicesService) { +angular.module('copayApp.services').factory('shapeshiftService', function ($http, $interval, $log, lodash, moment, ongoingProcess, shapeshiftApiService, storageService, configService, incomingDataService, platformInfo, servicesService) { var root = {}; root.ShiftState = 'Shift'; root.coinIn = ''; @@ -111,7 +111,7 @@ angular.module('copayApp.services').factory('shapeshiftService', function ($http toAddress: txData.deposit }; // - // if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) { + // if (incomingDataService.redir(sendAddress, 'shapeshift', shapeshiftData)) { ongoingProcess.set('connectingShapeshift', false); // return; // } diff --git a/src/sass/buttons.scss b/src/sass/buttons.scss index df0ce8945..886d730c7 100644 --- a/src/sass/buttons.scss +++ b/src/sass/buttons.scss @@ -1,5 +1,5 @@ %button-standard { - width: 85%; + width: 90%; max-width: 300px; margin-left: auto; margin-right: auto; diff --git a/src/sass/components/fee-summary.scss b/src/sass/components/fee-summary.scss index e09af4be3..5c9488b60 100644 --- a/src/sass/components/fee-summary.scss +++ b/src/sass/components/fee-summary.scss @@ -1,11 +1,11 @@ .fee-summary { - position: relative; + background-color: #F2F2F2; + box-sizing: border-box; display: flex; flex-direction: column; - width: 100%; padding: 5px 12px 15px; - box-sizing: border-box; - background-color: #F2F2F2; + position: relative; + width: 100%; &:before { content: ''; diff --git a/src/sass/mixins/layout.scss b/src/sass/mixins/layout.scss index 269a50320..e7a1af9da 100644 --- a/src/sass/mixins/layout.scss +++ b/src/sass/mixins/layout.scss @@ -59,6 +59,8 @@ .button { font-weight: bold; font-size: 19px; + line-height: 26px; + padding: 8px 6px; } } .button-first-contact img { diff --git a/src/sass/views/review.scss b/src/sass/views/review.scss index c530a1cef..474e1ea3f 100644 --- a/src/sass/views/review.scss +++ b/src/sass/views/review.scss @@ -6,8 +6,10 @@ } .fee-summary { - position: absolute; bottom: 92px; + bottom: calc(92px + constant(safe-area-inset-bottom)); /* iOS 11.0 */ + bottom: calc(92px + env(safe-area-inset-bottom)); /* iOS 11.2 */ + position: absolute; } .shapeshift-banner, .bitpay-banner, .egifter-banner { diff --git a/src/sass/views/tab-home.scss b/src/sass/views/tab-home.scss index 55080ab7b..f9ace4655 100644 --- a/src/sass/views/tab-home.scss +++ b/src/sass/views/tab-home.scss @@ -83,14 +83,14 @@ .button { border: 2px solid; border-radius: 47px; - padding: 0 15px 0 15px; + padding: 8px 2px 8px 2px; text-align: center; width: 100%; max-width: 300px; font-size: 19px; font-weight: bolder; min-height: auto; - line-height: 36px; + line-height: 19px; } } .wallet-coin-logo { diff --git a/src/sass/views/tab-send.scss b/src/sass/views/tab-send.scss index bf77984fd..564e90d66 100644 --- a/src/sass/views/tab-send.scss +++ b/src/sass/views/tab-send.scss @@ -105,7 +105,7 @@ width: auto; margin: 2px 0 4px; } - height: 60px; + min-height: 65px; line-height: 16px; margin-right: 0px; width: 95%; diff --git a/src/sass/views/walletDetails.scss b/src/sass/views/walletDetails.scss index 30b5da430..4e94f6a29 100644 --- a/src/sass/views/walletDetails.scss +++ b/src/sass/views/walletDetails.scss @@ -209,14 +209,14 @@ .button { border: 2px solid; border-radius: 47px; - padding: 0 15px 0 15px; + padding: 6px 2px 6px 2px; text-align: center; width: 100%; max-width: 300px; font-size: 19px; font-weight: bolder; min-height: auto; - line-height: 36px; + line-height: 19px; } } } 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', diff --git a/www/views/includes/community.html b/www/views/includes/community.html index 86841a77c..ad7dcb169 100644 --- a/www/views/includes/community.html +++ b/www/views/includes/community.html @@ -26,7 +26,7 @@
- Share the Wallet App + Share the Wallet App diff --git a/www/views/includes/incomingDataMenu.html b/www/views/includes/incomingDataMenu.html index ca4b78dfc..e60d7e956 100644 --- a/www/views/includes/incomingDataMenu.html +++ b/www/views/includes/incomingDataMenu.html @@ -9,21 +9,21 @@
- {{data}} + {{data.original}}
- +
Add as a contact
- +
Send payment to this address
- +
Copy to clipboard
@@ -38,11 +38,11 @@
Text
- {{data}} + {{data.original}}
-
+
Copy to clipboard
@@ -57,16 +57,40 @@
Private Key
- {{data}} + {{data.original}}
-
+
Sweep paper wallet
- + + +
Copy to clipboard
+ +
+ + Cancel + + + +
- No Bitcoin Cash wallet to transfer funds to found. + No Bitcoin Cash wallet to transfer funds to found.
-

No Bitcoin Cash found

+

No Bitcoin Cash found.

-

Bitcoin found:

+

Bitcoin Core found:

{{btcBalanceText}}
@@ -95,13 +95,13 @@
- No Bitcoin wallet to transfer funds to found. + No Bitcoin Core wallet to transfer funds to found.
-

No Bitcoin found

+

No Bitcoin Core found.