diff --git a/Gruntfile.js b/Gruntfile.js index b70fda526..c0a138d36 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -256,7 +256,7 @@ module.exports = function(grunt) { grunt.registerTask('prod', ['default', 'uglify']); grunt.registerTask('translate', ['nggettext_extract']); grunt.registerTask('test', ['karma:unit']); - grunt.registerTask('test-coveralls', ['karma:prod', 'coveralls']); + grunt.registerTask('test-coveralls', ['browserify', 'karma:prod', 'coveralls']); grunt.registerTask('desktop', ['prod', 'nodewebkit', 'copy:linux', 'compress:linux']); grunt.registerTask('osx', ['prod', 'nodewebkit', 'exec:osx']); grunt.registerTask('release', ['string-replace:dist']); diff --git a/bower.json b/bower.json index bed7802a4..2cc0da959 100644 --- a/bower.json +++ b/bower.json @@ -8,6 +8,7 @@ ], "dependencies": { "angular": "1.4.6", + "angular-mocks": "1.4.10", "angular-foundation": "0.8.0", "angular-gettext": "2.2.1", "angular-moment": "0.10.1", diff --git a/cordova/build.sh b/cordova/build.sh index 8ee808304..882ada2db 100755 --- a/cordova/build.sh +++ b/cordova/build.sh @@ -79,9 +79,6 @@ if [ ! -d $PROJECT ]; then echo "${OpenColor}${Green}* Installing plugins... ${CloseColor}" - cordova plugin add cordova-plugin-disable-bitcode - checkOK - cordova plugin add https://github.com/florentvaldelievre/virtualartifacts-webIntent.git checkOK @@ -158,6 +155,9 @@ if [ ! -d $PROJECT ]; then cordova plugin add cordova-ios-requires-fullscreen checkOK + cordova plugin add cordova-plugin-disable-bitcode + checkOK + fi if $DBGJS diff --git a/package.json b/package.json index 39c4c9099..2318c8d66 100644 --- a/package.json +++ b/package.json @@ -63,21 +63,28 @@ }, "devDependencies": { "adm-zip": "^0.4.7", - "angular": "^1.3.14", - "angular-mocks": "^1.3.14", + "angular": "1.4.6", + "angular-mocks": "1.4.10", "bhttp": "^1.2.1", + "chai": "^3.5.0", "cordova": "5.4.1", "cordova-android": "5.1.1", "grunt-contrib-sass": "^1.0.0", - "grunt-karma": "^0.10.1", - "grunt-karma-coveralls": "^2.5.3", + "grunt-karma": "^1.0.0", + "grunt-karma-coveralls": "^2.5.4", "grunt-node-webkit-builder": "^1.0.2", "grunt-string-replace": "^1.2.1", - "karma": "^0.12.31", - "karma-cli": "0.0.4", - "karma-coverage": "^0.2.7", - "karma-jasmine": "^0.3.5", - "karma-phantomjs-launcher": "^0.1.4", + "karma": "^0.13.22", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^1.0.1", + "karma-cli": "^1.0.0", + "karma-coverage": "^1.0.0", + "karma-mocha": "^1.0.1", + "karma-mocha-reporter": "^2.0.3", + "karma-phantomjs-launcher": "^1.0.0", + "karma-sinon": "^1.0.5", + "mocha": "^2.4.5", + "phantomjs-prebuilt": "^2.1.7", "xcode": "^0.8.2" } } diff --git a/public/views/buyGlidera.html b/public/views/buyGlidera.html index bcab328de..2bb9228b4 100644 --- a/public/views/buyGlidera.html +++ b/public/views/buyGlidera.html @@ -35,7 +35,7 @@
-
+
This operation was disabled because you have a pending first transaction diff --git a/public/views/glideraUri.html b/public/views/glideraUri.html index 361c856e1..20c661c87 100644 --- a/public/views/glideraUri.html +++ b/public/views/glideraUri.html @@ -23,7 +23,7 @@
+ ng-click="index.updateGlidera()" width="100">
diff --git a/public/views/modals/destination-address.html b/public/views/modals/destination-address.html index ee9cf1e51..4d94ab27a 100644 --- a/public/views/modals/destination-address.html +++ b/public/views/modals/destination-address.html @@ -64,9 +64,9 @@
{{w.name || w.id}} - + - Needs backup + {{errorSelectedWallet[w.id] }}
{{w.m}} of {{w.n}} diff --git a/src/js/controllers/backup.js b/src/js/controllers/backup.js index c5653b3e9..b205d63bb 100644 --- a/src/js/controllers/backup.js +++ b/src/js/controllers/backup.js @@ -1,13 +1,21 @@ 'use strict'; angular.module('copayApp.controllers').controller('backupController', - function($rootScope, $scope, $timeout, $log, $state, $compile, go, lodash, profileService, gettext, bwcService, bwsError) { + function($rootScope, $scope, $timeout, $log, $state, $compile, go, lodash, profileService, gettext, bwcService, bwsError, walletService) { var self = this; var fc = profileService.focusedClient; var customWords = []; self.walletName = fc.credentials.walletName; + var handleEncryptedWallet = function(client, cb) { + if (!walletService.isEncrypted(client)) return cb(); + $rootScope.$emit('Local/NeedsPassword', false, function(err, password) { + if (err) return cb(err); + return cb(walletService.unlock(client, password)); + }); + }; + function init() { $scope.passphrase = ''; resetAllButtons(); @@ -46,7 +54,7 @@ angular.module('copayApp.controllers').controller('backupController', function initWords() { var words = fc.getMnemonic(); self.xPrivKey = fc.credentials.xPrivKey; - profileService.lockFC(); + walletService.lock(fc); self.mnemonicWords = words.split(/[\u3000\s]+/); self.shuffledMnemonicWords = lodash.sortBy(self.mnemonicWords);; self.mnemonicHasPassphrase = fc.mnemonicHasPassphrase(); @@ -74,7 +82,7 @@ angular.module('copayApp.controllers').controller('backupController', $scope.$apply(); }, 1); - profileService.unlockFC({}, function(err) { + handleEncryptedWallet(fc, function(err) { if (err) { self.error = bwsError.msg(err, gettext('Could not decrypt')); $log.warn('Error decrypting credentials:', self.error); //TODO diff --git a/src/js/controllers/buyCoinbase.js b/src/js/controllers/buyCoinbase.js index 00596faae..51d37ffa8 100644 --- a/src/js/controllers/buyCoinbase.js +++ b/src/js/controllers/buyCoinbase.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('buyCoinbaseController', - function($scope, $modal, $log, $timeout, lodash, profileService, coinbaseService, animationService, txService, bwsError, addressService) { + function($scope, $modal, $log, $timeout, lodash, profileService, coinbaseService, animationService, bwsError, addressService, walletService) { window.ignoreMobilePause = true; var self = this; @@ -71,17 +71,17 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', }; $scope.selectWallet = function(walletId, walletName) { - if (!profileService.getClient(walletId).isComplete()) { - self.error = bwsError.msg({ - 'code': 'WALLET_NOT_COMPLETE' - }, 'Could not choose the wallet'); - self.error = {errors: [{ message: 'The Wallet could not be selected' }]}; - $modalInstance.dismiss('cancel'); - return; - } - $modalInstance.close({ - 'walletId': walletId, - 'walletName': walletName, + var client = profileService.getClient(walletId); + walletService.isReady(client, function(err) { + if (err) { + self.error = {errors: [{ message: err }]}; + $modalInstance.dismiss('cancel'); + } else { + $modalInstance.close({ + 'walletId': walletId, + 'walletName': walletName, + }); + } }); }; }; diff --git a/src/js/controllers/buyGlidera.js b/src/js/controllers/buyGlidera.js index d3e163e8c..2add687cb 100644 --- a/src/js/controllers/buyGlidera.js +++ b/src/js/controllers/buyGlidera.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('buyGlideraController', - function($scope, $timeout, $modal, profileService, addressService, glideraService, bwsError, lodash, isChromeApp, animationService) { + function($scope, $timeout, $modal, profileService, addressService, glideraService, bwsError, lodash, isChromeApp, animationService, walletService) { var self = this; this.show2faCodeInput = null; @@ -50,14 +50,17 @@ angular.module('copayApp.controllers').controller('buyGlideraController', }; $scope.selectWallet = function(walletId, walletName) { - if (!profileService.getClient(walletId).isComplete()) { - self.error = bwsError.msg({'code': 'WALLET_NOT_COMPLETE'}, 'Could not choose the wallet'); - $modalInstance.dismiss('cancel'); - return; - } - $modalInstance.close({ - 'walletId': walletId, - 'walletName': walletName, + var client = profileService.getClient(walletId); + walletService.isReady(client, function(err) { + if (err) { + self.error = err; + $modalInstance.dismiss('cancel'); + return; + } + $modalInstance.close({ + 'walletId': walletId, + 'walletName': walletName, + }); }); }; }; @@ -94,26 +97,24 @@ angular.module('copayApp.controllers').controller('buyGlideraController', self.gettingBuyPrice = false; if (err) { self.error = 'Could not get exchange information. Please, try again.'; + return; } - else { - self.buyPrice = buyPrice; - } + self.buyPrice = buyPrice; }); }; this.get2faCode = function(token) { var self = this; - this.loading = 'Sending 2FA code...'; + self.error = null; + self.loading = 'Sending 2FA code...'; $timeout(function() { glideraService.get2faCode(token, function(err, sent) { self.loading = null; if (err) { self.error = 'Could not send confirmation code to your phone'; + return; } - else { - self.error = null; - self.show2faCodeInput = sent; - } + self.show2faCodeInput = sent; }); }, 100); }; @@ -139,11 +140,10 @@ angular.module('copayApp.controllers').controller('buyGlideraController', self.loading = null; if (err) { self.error = err; + return; } - else { - self.success = data; - $scope.$emit('Local/GlideraTx'); - } + self.success = data; + $scope.$emit('Local/GlideraTx'); }); }); }, 100); diff --git a/src/js/controllers/glidera.js b/src/js/controllers/glidera.js index df876cf9a..fbce3e95d 100644 --- a/src/js/controllers/glidera.js +++ b/src/js/controllers/glidera.js @@ -3,8 +3,6 @@ angular.module('copayApp.controllers').controller('glideraController', function($rootScope, $scope, $timeout, $modal, profileService, configService, storageService, glideraService, isChromeApp, animationService, lodash) { - window.ignoreMobilePause = true; - this.getAuthenticateUrl = function() { return glideraService.getOauthCodeUrl(); }; diff --git a/src/js/controllers/index.js b/src/js/controllers/index.js index dc4f08ce0..177aa4a21 100644 --- a/src/js/controllers/index.js +++ b/src/js/controllers/index.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, latestReleaseService, bwcService, pushNotificationsService, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, gettextCatalog, amMoment, nodeWebkit, addonManager, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService, coinbaseService, isMobile, addressbookService) { +angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, latestReleaseService, bwcService, pushNotificationsService, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, gettextCatalog, amMoment, nodeWebkit, addonManager, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService, coinbaseService, isMobile, addressbookService, walletService) { var self = this; var SOFT_CONFIRMATION_LIMIT = 12; var errors = bwcService.getErrors(); @@ -161,7 +161,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r } } - profileService.isBackupNeeded(self.walletId, function(needsBackup) { + walletService.isBackupNeeded(fc, function(needsBackup) { self.needsBackup = needsBackup; self.openWallet(function() { if (!self.isComplete) { diff --git a/src/js/controllers/preferences.js b/src/js/controllers/preferences.js index ba2e7ea75..fb3e98b31 100644 --- a/src/js/controllers/preferences.js +++ b/src/js/controllers/preferences.js @@ -1,37 +1,39 @@ 'use strict'; angular.module('copayApp.controllers').controller('preferencesController', - function($scope, $rootScope, $timeout, $log, configService, profileService, txService) { + function($scope, $rootScope, $timeout, $log, configService, profileService, fingerprintService, walletService) { var fc = profileService.focusedClient; + var config = configService.getSync(); $scope.deleted = false; if (fc.credentials && !fc.credentials.mnemonicEncrypted && !fc.credentials.mnemonic) { $scope.deleted = true; } this.init = function() { - var config = configService.getSync(); - var fc = profileService.focusedClient; if (fc) { - $scope.encrypt = fc.hasPrivKeyEncrypted(); + $scope.encrypt = walletService.isEncrypted(fc); this.externalSource = fc.getPrivKeyExternalSourceName() == 'ledger' ? "Ledger" : null; // TODO externalAccount //this.externalIndex = fc.getExternalIndex(); } - var walletId = fc.credentials.walletId; - config.touchIdFor = config.touchIdFor || {}; - $scope.touchid = config.touchIdFor[walletId]; + this.touchidAvailable = fingerprintService.isAvailable(); + $scope.touchid = config.touchIdFor ? config.touchIdFor[fc.credentials.walletId] : null; + }; - if (window.touchidAvailable) - this.touchidAvailable = true; + var handleEncryptedWallet = function(client, cb) { + $rootScope.$emit('Local/NeedsPassword', false, function(err, password) { + if (err) return cb(err); + return cb(walletService.unlock(client, password)); + }); }; var unwatchEncrypt = $scope.$watch('encrypt', function(val) { var fc = profileService.focusedClient; if (!fc) return; - if (val && !fc.hasPrivKeyEncrypted()) { + if (val && !walletService.isEncrypted(fc)) { $rootScope.$emit('Local/NeedsPassword', true, function(err, password) { if (err || !password) { $scope.encrypt = false; @@ -43,8 +45,8 @@ angular.module('copayApp.controllers').controller('preferencesController', }); }); } else { - if (!val && fc.hasPrivKeyEncrypted()) { - profileService.unlockFC({}, function(err) { + if (!val && walletService.isEncrypted(fc)) { + handleEncryptedWallet(fc, function(err) { if (err) { $scope.encrypt = true; return; @@ -68,29 +70,29 @@ angular.module('copayApp.controllers').controller('preferencesController', $scope.touchidError = false; return; } - var walletId = profileService.focusedClient.credentials.walletId; + var walletId = fc.credentials.walletId; var opts = { touchIdFor: {} }; opts.touchIdFor[walletId] = newVal; - txService.setTouchId(function(err) { + fingerprintService.check(fc, function(err) { if (err) { $log.debug(err); $timeout(function() { $scope.touchidError = true; $scope.touchid = oldVal; }, 100); - } else { - configService.set(opts, function(err) { - if (err) { - $log.debug(err); - $scope.touchidError = true; - $scope.touchid = oldVal; - } - }); + return; } + configService.set(opts, function(err) { + if (err) { + $log.debug(err); + $scope.touchidError = true; + $scope.touchid = oldVal; + } + }); }); }); diff --git a/src/js/controllers/sellCoinbase.js b/src/js/controllers/sellCoinbase.js index 0aeb7f959..607244eb5 100644 --- a/src/js/controllers/sellCoinbase.js +++ b/src/js/controllers/sellCoinbase.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('sellCoinbaseController', - function($scope, $modal, $log, $timeout, lodash, profileService, coinbaseService, animationService, txService, bwsError) { + function($scope, $modal, $log, $timeout, lodash, profileService, coinbaseService, animationService, bwsError, configService, walletService, fingerprintService) { window.ignoreMobilePause = true; var self = this; @@ -38,6 +38,14 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', }); }; + var handleEncryptedWallet = function(client, cb) { + if (!walletService.isEncrypted(client)) return cb(); + $rootScope.$emit('Local/NeedsPassword', false, function(err, password) { + if (err) return cb(err); + return cb(walletService.unlock(client, password)); + }); + }; + this.init = function(testnet) { self.otherWallets = otherWallets(testnet); // Choose focused wallet @@ -179,6 +187,9 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', var accountId = account.id; var dataSrc = { name : 'Received from Copay: ' + self.selectedWalletName }; var outputs = []; + var config = configService.getSync(); + var configWallet = config.wallet; + var walletSettings = configWallet.settings; self.loading = 'Creating transaction...'; @@ -202,17 +213,18 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', 'amount': amount, 'message': comment }); - - var opts = { - selectedClient: fc, + + var txp = { toAddress: address, amount: amount, outputs: outputs, message: comment, - payProUrl: null + payProUrl: null, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: walletSettings.feeLevel || 'normal' }; - txService.createTx(opts, function(err, txp) { + walletService.createTx(fc, txp, function(err, createdTxp) { if (err) { $log.debug(err); self.loading = null; @@ -220,10 +232,10 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', $scope.$apply(); return; } - $scope.$emit('Local/NeedsConfirmation', txp, function(accept) { + $scope.$emit('Local/NeedsConfirmation', createdTxp, function(accept) { self.loading = null; if (accept) { - self.confirmTx(txp, function(err, tx) { + self.confirmTx(createdTxp, function(err, tx) { if (err) { self.error = {errors: [{ message: 'Could not create transaction: ' + err.message }]}; return; @@ -266,34 +278,54 @@ angular.module('copayApp.controllers').controller('sellCoinbaseController', }; this.confirmTx = function(txp, cb) { - txService.prepare({selectedClient: fc}, function(err) { + + fingerprintService.check(fc, function(err) { if (err) { $log.debug(err); return cb(err); } - self.loading = 'Sending bitcoin to Coinbase...'; - txService.publishTx(txp, {selectedClient: fc}, function(err, txpPublished) { + + handleEncryptedWallet(fc, function(err) { if (err) { - self.loading = null; $log.debug(err); - return cb({errors: [{ message: 'Transaction could not be published: ' + err.message }]}); - } else { - txService.signAndBroadcast(txpPublished, {selectedClient: fc}, function(err, txp) { + return cb(err); + } + + self.loading = 'Sending bitcoin to Coinbase...'; + walletService.publishTx(fc, txp, function(err, publishedTxp) { + if (err) { + self.loading = null; + $log.debug(err); + return cb({errors: [{ message: 'Transaction could not be published: ' + err.message }]}); + } + + walletService.signTx(fc, publishedTxp, function(err, signedTxp) { + walletService.lock(fc); if (err) { self.loading = null; $log.debug(err); - txService.removeTx(txp, function(err) { + walletService.removeTx(fc, signedTxp, function(err) { if (err) $log.debug(err); }); return cb({errors: [{ message: 'The payment was created but could not be completed: ' + err.message }]}); - } else { + } + + walletService.broadcastTx(fc, signedTxp, function(err, broadcastedTxp) { + if (err) { + self.loading = null; + $log.debug(err); + walletService.removeTx(fc, broadcastedTxp, function(err) { + if (err) $log.debug(err); + }); + return cb({errors: [{ message: 'The payment was created but could not be broadcasted: ' + err.message }]}); + } $timeout(function() { self.loading = null; return cb(null, txp); }, 5000); - } + }); }); - } + }); }); }); }; diff --git a/src/js/controllers/sellGlidera.js b/src/js/controllers/sellGlidera.js index 94e6901b0..129f2fb53 100644 --- a/src/js/controllers/sellGlidera.js +++ b/src/js/controllers/sellGlidera.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('sellGlideraController', - function($scope, $timeout, $log, $modal, configService, profileService, addressService, feeService, glideraService, bwsError, lodash, isChromeApp, animationService, txService) { + function($rootScope, $scope, $timeout, $log, $modal, configService, profileService, addressService, feeService, glideraService, bwsError, lodash, isChromeApp, animationService, walletService, fingerprintService) { var self = this; var config = configService.getSync(); @@ -10,7 +10,6 @@ angular.module('copayApp.controllers').controller('sellGlideraController', this.success = null; this.error = null; this.loading = null; - this.currentSpendUnconfirmed = config.wallet.spendUnconfirmed; var fc; window.ignoreMobilePause = true; @@ -22,6 +21,14 @@ angular.module('copayApp.controllers').controller('sellGlideraController', }); }; + var handleEncryptedWallet = function(client, cb) { + if (!walletService.isEncrypted(client)) return cb(); + $rootScope.$emit('Local/NeedsPassword', false, function(err, password) { + if (err) return cb(err); + return cb(walletService.unlock(client, password)); + }); + }; + this.init = function(testnet) { self.otherWallets = otherWallets(testnet); // Choose focused wallet @@ -92,20 +99,19 @@ angular.module('copayApp.controllers').controller('sellGlideraController', this.getSellPrice = function(token, price) { var self = this; - this.error = null; + self.error = null; if (!price || (price && !price.qty && !price.fiat)) { - this.sellPrice = null; + self.sellPrice = null; return; } - this.gettingSellPrice = true; + self.gettingSellPrice = true; glideraService.sellPrice(token, price, function(err, sellPrice) { self.gettingSellPrice = false; if (err) { self.error = 'Could not get exchange information. Please, try again.'; - } else { - self.error = null; - self.sellPrice = sellPrice; + return; } + self.sellPrice = sellPrice; }); }; @@ -127,87 +133,112 @@ angular.module('copayApp.controllers').controller('sellGlideraController', this.createTx = function(token, permissions, twoFaCode) { var self = this; self.error = null; - - - txService.prepare({selectedClient: fc}, function(err) { - if (err) { - self.error = err; + var outputs = []; + var configWallet = config.wallet; + var walletSettings = configWallet.settings; + + addressService.getAddress(fc.credentials.walletId, null, function(err, refundAddress) { + if (!refundAddress) { + self.loading = null; + self.error = bwsError.msg(err, 'Could not create address'); return; } - self.loading = 'Selling Bitcoin...'; - $timeout(function() { - addressService.getAddress(fc.credentials.walletId, null, function(err, refundAddress) { - if (!refundAddress) { - self.loading = null; - self.error = bwsError.msg(err, 'Could not create address'); + glideraService.getSellAddress(token, function(error, sellAddress) { + if (!sellAddress) { + self.loading = null; + self.error = 'Could not get the destination bitcoin address'; + return; + } + var amount = parseInt((self.sellPrice.qty * 100000000).toFixed(0)); + var comment = 'Glidera transaction'; + + outputs.push({ + 'toAddress': sellAddress, + 'amount': amount, + 'message': comment + }); + + var txp = { + toAddress: sellAddress, + amount: amount, + outputs: outputs, + message: comment, + payProUrl: null, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: walletSettings.feeLevel || 'normal', + customData: { + 'glideraToken': token + } + }; + + self.loading = 'Creating transaction...'; + walletService.createTx(fc, txp, function(err, createdTxp) { + self.loading = null; + if (err) { + self.error = err.message || bwsError.msg(err); return; } - glideraService.getSellAddress(token, function(error, sellAddress) { - if (!sellAddress) { - self.loading = null; - self.error = 'Could not get the destination bitcoin address'; - return; - } - var amount = parseInt((self.sellPrice.qty * 100000000).toFixed(0)); - - feeService.getCurrentFeeValue(function(err, feePerKb) { - if (err) $log.debug(err); - fc.sendTxProposal({ - toAddress: sellAddress, - amount: amount, - message: 'Glidera transaction', - customData: { - 'glideraToken': token - }, - payProUrl: null, - feePerKb: feePerKb, - excludeUnconfirmedUtxos: self.currentSpendUnconfirmed ? false : true - }, function(err, txp) { + $scope.$emit('Local/NeedsConfirmation', createdTxp, function(accept) { + if (accept) { + fingerprintService.check(fc, function(err) { if (err) { - profileService.lockFC(); - $log.error(err); - $timeout(function() { - self.loading = null; - self.error = bwsError.msg(err, 'Error'); - }, 1); + self.error = err.message || bwsError.msg(err); return; } - txService.sign(txp, {selectedClient: fc}, function(err, txp) { + handleEncryptedWallet(fc, function(err) { if (err) { - self.loading = null; - self.error = err; - $scope.$apply(); - } else { - var rawTx = txp.raw; - var data = { - refundAddress: refundAddress, - signedTransaction: rawTx, - priceUuid: self.sellPrice.priceUuid, - useCurrentPrice: self.sellPrice.priceUuid ? false : true, - ip: null - }; - glideraService.sell(token, twoFaCode, data, function(err, data) { + self.error = err.message || bwsError.msg(err); + return; + } + + self.loading = 'Signing transaction...'; + + walletService.publishTx(fc, createdTxp, function(err, publishedTxp) { + if (err) { self.loading = null; + self.error = err.message || bwsError.msg(err); + } + + walletService.signTx(fc, publishedTxp, function(err, signedTxp) { + walletService.lock(fc); + walletService.removeTx(fc, signedTxp, function(err) { + if (err) $log.debug(err); + }); if (err) { - self.error = err; - fc.removeTxProposal(txp, function(err, txp) { + self.loading = null; + self.error = err.message || bwsError.msg(err); + return; + } + var rawTx = signedTxp.raw; + var data = { + refundAddress: refundAddress, + signedTransaction: rawTx, + priceUuid: self.sellPrice.priceUuid, + useCurrentPrice: self.sellPrice.priceUuid ? false : true, + ip: null + }; + self.loading = 'Selling bitcoin...'; + glideraService.sell(token, twoFaCode, data, function(err, data) { + self.loading = null; + if (err) { + self.error = err.message || bwsError.msg(err); $timeout(function() { $scope.$emit('Local/GlideraError'); }, 100); - }); - } else { + return; + } self.success = data; $scope.$emit('Local/GlideraTx'); - } + }); }); - } + }); }); }); - }); + } }); }); - }, 100); + }); }); }; }); diff --git a/src/js/controllers/walletHome.js b/src/js/controllers/walletHome.js index 02aff04e5..5108e4b09 100644 --- a/src/js/controllers/walletHome.js +++ b/src/js/controllers/walletHome.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('walletHomeController', function($scope, $rootScope, $interval, $timeout, $filter, $modal, $log, notification, txStatus, isCordova, isMobile, profileService, lodash, configService, rateService, storageService, bitcore, isChromeApp, gettext, gettextCatalog, nodeWebkit, addressService, ledger, bwsError, confirmDialog, txFormatService, animationService, addressbookService, go, feeService, txService) { +angular.module('copayApp.controllers').controller('walletHomeController', function($scope, $rootScope, $interval, $timeout, $filter, $modal, $log, notification, txStatus, isCordova, isMobile, profileService, lodash, configService, rateService, storageService, bitcore, isChromeApp, gettext, gettextCatalog, nodeWebkit, addressService, ledger, bwsError, confirmDialog, txFormatService, animationService, addressbookService, go, feeService, walletService, fingerprintService) { var self = this; window.ignoreMobilePause = false; @@ -127,6 +127,14 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi }); }; + var handleEncryptedWallet = function(client, cb) { + if (!walletService.isEncrypted(client)) return cb(); + $rootScope.$emit('Local/NeedsPassword', false, function(err, password) { + if (err) return cb(err); + return cb(walletService.unlock(client, password)); + }); + }; + var accept_msg = gettextCatalog.getString('Accept'); var cancel_msg = gettextCatalog.getString('Cancel'); var confirm_msg = gettextCatalog.getString('Confirm'); @@ -251,28 +259,30 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi }; $scope.selectWallet = function(walletId, walletName) { - profileService.isBackupNeeded(walletId, function(needsBackup) { - $scope.needsBackup = {}; - $scope.needsBackup[walletId] = needsBackup; - if (needsBackup) return; + var client = profileService.getClient(walletId); + $scope.errorSelectedWallet = {}; - $scope.gettingAddress = true; - $scope.selectedWalletName = walletName; - $timeout(function() { - $scope.$apply(); - }); + walletService.isReady(client, function(err) { + if (err) $scope.errorSelectedWallet[walletId] = err; + else { + $scope.gettingAddress = true; + $scope.selectedWalletName = walletName; + $timeout(function() { + $scope.$apply(); + }); - addressService.getAddress(walletId, false, function(err, addr) { - $scope.gettingAddress = false; + addressService.getAddress(walletId, false, function(err, addr) { + $scope.gettingAddress = false; - if (err) { - self.error = err; - $modalInstance.dismiss('cancel'); - return; - } + if (err) { + self.error = err; + $modalInstance.dismiss('cancel'); + return; + } - $modalInstance.close(addr); - }); + $modalInstance.close(addr); + }); + } }); }; }; @@ -413,36 +423,60 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi }; $scope.sign = function(txp) { - var fc = profileService.focusedClient; + var client = profileService.focusedClient; $scope.error = null; $scope.loading = true; - - txService.prepareAndSignAndBroadcast(txp, { - reporterFn: self.setOngoingProcess.bind(self) - }, function(err, txp) { - $scope.loading = false; - $scope.$emit('UpdateTx'); - + + fingerprintService.check(client, function(err) { if (err) { + $scope.loading = false; $scope.error = err; - $timeout(function() { - $scope.$digest(); - }); return; } - $modalInstance.close(txp); - return; + + handleEncryptedWallet(client, function(err) { + if (err) { + $scope.loading = false; + $scope.error = err; + return; + } + + walletService.signTx(client, txp, function(err, signedTxp) { + walletService.lock(client); + if (err) { + $scope.loading = false; + $scope.error = err; + return; + } + + if (signedTxp.status == 'accepted') { + walletService.broadcastTx(client, signedTxp, function(err, broadcastedTxp) { + $scope.loading = false; + $scope.$emit('UpdateTx'); + $modalInstance.close(broadcastedTxp); + if (err) { + $scope.loading = false; + $scope.error = err; + } + }); + } else { + $scope.loading = false; + $scope.$emit('UpdateTx'); + $modalInstance.close(signedTxp); + } + }); + }); }); }; $scope.reject = function(txp) { + var client = profileService.focusedClient; self.setOngoingProcess(gettextCatalog.getString('Rejecting payment')); $scope.loading = true; $scope.error = null; - // TODO: This should be in txService $timeout(function() { - fc.rejectTxProposal(txp, null, function(err, txpr) { + walletService.rejectTx(client, txp, function(err, txpr) { self.setOngoingProcess(); $scope.loading = false; if (err) { @@ -458,11 +492,12 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi $scope.remove = function(txp) { + var client = profileService.focusedClient; self.setOngoingProcess(gettextCatalog.getString('Deleting payment')); $scope.loading = true; $scope.error = null; $timeout(function() { - fc.removeTxProposal(txp, function(err, txpb) { + walletService.removeTx(client, txp, function(err) { self.setOngoingProcess(); $scope.loading = false; @@ -479,23 +514,20 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi }; $scope.broadcast = function(txp) { + var client = profileService.focusedClient; self.setOngoingProcess(gettextCatalog.getString('Broadcasting Payment')); $scope.loading = true; $scope.error = null; $timeout(function() { - fc.broadcastTxProposal(txp, function(err, txpb, memo) { + walletService.broadcastTx(client, txp, function(err, txpb) { self.setOngoingProcess(); $scope.loading = false; if (err) { $scope.error = bwsError.msg(err, gettextCatalog.getString('Could not broadcast payment')); $scope.$digest(); - } else { - - if (memo) - $log.info(memo); - - $modalInstance.close(txpb); + return; } + $modalInstance.close(txpb); }); }, 100); }; @@ -876,7 +908,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi this.submitForm = function() { if (!$scope._amount || !$scope._address) return; - var fc = profileService.focusedClient; + var client = profileService.focusedClient; var unitToSat = this.unitToSatoshi; var currentSpendUnconfirmed = configWallet.spendUnconfirmed; @@ -893,7 +925,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi var comment = form.comment.$modelValue; // ToDo: use a credential's (or fc's) function for this - if (comment && !fc.credentials.sharedEncryptingKey) { + if (comment && !client.credentials.sharedEncryptingKey) { var msg = 'Could not add message to imported wallet without shared encrypting key'; $log.warn(msg); return self.setSendError(gettext(msg)); @@ -912,90 +944,100 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi 'message': comment }); - var opts = {}; + var txp = {}; if (!lodash.isEmpty(self.sendMaxInfo)) { - opts.sendMax = true; - opts.inputs = self.sendMaxInfo.inputs; - opts.fee = self.sendMaxInfo.fee; + txp.sendMax = true; + txp.inputs = self.sendMaxInfo.inputs; + txp.fee = self.sendMaxInfo.fee; }else { - opts.amount = amount; + txp.amount = amount; } - opts.toAddress = address; - opts.outputs = outputs; - opts.message = comment; - opts.payProUrl = paypro ? paypro.url : null; - opts.lockedCurrentFeePerKb = self.lockedCurrentFeePerKb; + txp.toAddress = address; + txp.outputs = outputs; + txp.message = comment; + txp.payProUrl = paypro ? paypro.url : null; + txp.excludeUnconfirmedUtxos = configWallet.spendUnconfirmed ? false : true; + txp.feeLevel = walletSettings.feeLevel || 'normal'; self.setOngoingProcess(gettextCatalog.getString('Creating transaction')); - txService.createTx(opts, function(err, txp) { + walletService.createTx(client, txp, function(err, createdTxp) { self.setOngoingProcess(); if (err) { return self.setSendError(err); } - if (!fc.canSign() && !fc.isPrivKeyExternal()) { - self.setOngoingProcess(); + if (!client.canSign() && !client.isPrivKeyExternal()) { $log.info('No signing proposal: No private key'); self.resetForm(); - txStatus.notify(txp, function() { + txStatus.notify(createdTxp, function() { return $scope.$emit('Local/TxProposalAction'); }); - return; } else { - $rootScope.$emit('Local/NeedsConfirmation', txp, function(accept) { - if (accept) self.confirmTx(txp); + $rootScope.$emit('Local/NeedsConfirmation', createdTxp, function(accept) { + if (accept) self.confirmTx(createdTxp); else self.resetForm(); }); } }); }, 100); - }; + }; this.confirmTx = function(txp) { + var client = profileService.focusedClient; var self = this; - $log.info('at .confirmTx'); - txService.prepare({}, function(err) { - $log.info('after .prepare:', err); + fingerprintService.check(client, function(err) { if (err) { - self.setOngoingProcess(); - $log.warn('confirmTx/Prepare error:', err); return self.setSendError(err); } - self.setOngoingProcess(gettextCatalog.getString('Sending transaction')); - txService.publishTx(txp, {}, function(err, txpPublished) { - - $log.info('after .publishTx:', err); + handleEncryptedWallet(client, function(err) { if (err) { - self.setOngoingProcess(); - self.setSendError(err); - return; - } - - txService.signAndBroadcast(txpPublished, { - reporterFn: self.setOngoingProcess.bind(self) - }, function(err, txp) { - $log.info('after .signAndBroadcast:', err); - self.resetForm(); - self.setOngoingProcess(); + return self.setSendError(err); + } + self.setOngoingProcess(gettextCatalog.getString('Sending transaction')); + walletService.publishTx(client, txp, function(err, publishedTxp) { if (err) { - self.error = bwsError.msg(err, gettextCatalog.getString('The payment was created but could not be completed. Please try again from home screen')); - $scope.$emit('Local/TxProposalAction'); - $timeout(function() { - $scope.$digest(); - }, 1); - return; - } + self.setOngoingProcess(); + return self.setSendError(err); + } - $log.info('Transaction status:', txp.status); - go.walletHome(); - txStatus.notify(txp, function() { - $scope.$emit('Local/TxProposalAction', txp.status == 'broadcasted'); + self.setOngoingProcess(gettextCatalog.getString('Signing transaction')); + walletService.signTx(client, publishedTxp, function(err, signedTxp) { + self.setOngoingProcess(); + walletService.lock(client); + if (err) { + $scope.$emit('Local/TxProposalAction'); + return self.setSendError( + err.message ? + err.message : + gettext('The payment was created but could not be completed. Please try again from home screen')); + } + + if (signedTxp.status == 'accepted') { + self.setOngoingProcess(gettextCatalog.getString('Broadcasting transaction')); + walletService.broadcastTx(client, signedTxp, function(err, broadcastedTxp) { + self.setOngoingProcess(); + if (err) { + return self.setSendError(err); + } + self.resetForm(); + go.walletHome(); + txStatus.notify(broadcastedTxp, function() { + $scope.$emit('Local/TxProposalAction', broadcastedTxp.status == 'broadcasted'); + }); + }); + } else { + self.resetForm(); + go.walletHome(); + txStatus.notify(signedTxp, function() { + $scope.$emit('Local/TxProposalAction'); + }); + } }); }); }); @@ -1030,7 +1072,6 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi this.sendMaxInfo = {}; if (this.countDown) $interval.cancel(this.countDown); this._paypro = null; - this.lockedCurrentFeePerKb = null; this.lockAddress = false; this.lockAmount = false; diff --git a/src/js/services/bwsError.js b/src/js/services/bwsError.js index 4443439cf..3c96fbfcd 100644 --- a/src/js/services/bwsError.js +++ b/src/js/services/bwsError.js @@ -127,6 +127,18 @@ angular.module('copayApp.services') case 'WALLET_NOT_COMPLETE': body = gettextCatalog.getString('Wallet is not complete'); break; + case 'WALLET_NEEDS_BACKUP': + body = gettextCatalog.getString('Wallet needs backup'); + break; + case 'MISSING_PARAMETER': + body = gettextCatalog.getString('Missing parameter'); + break; + case 'NO_PASSWORD_GIVEN': + body = gettextCatalog.getString('Spending Password needed'); + break; + case 'PASSWORD_INCORRECT': + body = gettextCatalog.getString('Wrong spending password'); + break; case 'ERROR': body = (err.message || err.error); break; @@ -147,7 +159,7 @@ angular.module('copayApp.services') }; root.cb = function(err, prefix, cb) { - return cb(root.msg(err, prefix)) + return cb(root.msg(err, prefix)); }; return root; diff --git a/src/js/services/fingerprintService.js b/src/js/services/fingerprintService.js new file mode 100644 index 000000000..3d708e7f2 --- /dev/null +++ b/src/js/services/fingerprintService.js @@ -0,0 +1,40 @@ +'use strict'; + +angular.module('copayApp.services').factory('fingerprintService', function(gettextCatalog, configService) { + var root = {}; + + var requestTouchId = function(cb) { + try { + window.plugins.touchid.verifyFingerprint( + gettextCatalog.getString('Scan your fingerprint please'), + function(msg) { + $log.debug('Touch ID OK'); + return cb(); + }, + function(msg) { + $log.debug('Touch ID Failed:' + JSON.stringify(msg)); + return cb(gettextCatalog.getString('Touch ID Failed') + ': ' + msg.localizedDescription); + } + ); + } catch (e) { + $log.debug('Touch ID Failed:' + JSON.stringify(e)); + return cb(gettextCatalog.getString('Touch ID Failed')); + }; + }; + + root.isAvailable = function(client) { + var config = configService.getSync(); + config.touchIdFor = config.touchIdFor || {}; + return (window.touchidAvailable && config.touchIdFor[client.credentials.walletId]); + }; + + root.check = function(client, cb) { + if (root.isAvailable()) { + requestTouchId(cb); + } else { + return cb(); + } + }; + + return root; +}); diff --git a/src/js/services/glideraService.js b/src/js/services/glideraService.js index ece0723a0..dd13bcd53 100644 --- a/src/js/services/glideraService.js +++ b/src/js/services/glideraService.js @@ -9,13 +9,13 @@ angular.module('copayApp.services').factory('glideraService', function($http, $l credentials.HOST = 'https://sandbox.glidera.io'; if (isCordova) { credentials.REDIRECT_URI = 'copay://glidera'; - credentials.CLIENT_ID = 'dfc56e4336e32bb8ba46dde34f3d7d6d'; - credentials.CLIENT_SECRET = '5eb679058f6c7eb81123162323d4fba5'; + credentials.CLIENT_ID = '6163427a2f37d1b2022ececd6d6c9cdd'; + credentials.CLIENT_SECRET = '599cc3af26108c6fece8ab17c3f35867'; } else { credentials.REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'; - credentials.CLIENT_ID = '9915b6ffa6dc3baffb87135ed3873d49'; - credentials.CLIENT_SECRET = 'd74eda05b9c6a228fd5c85cfbd0eb7eb'; + credentials.CLIENT_ID = 'c402f4a753755456e8c384fb65b7be1d'; + credentials.CLIENT_SECRET = '3ce826198e3618d0b8ed341ab91fe4e5'; } } else { diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index 8fa3cf6e9..a9a07da19 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -638,7 +638,7 @@ angular.module('copayApp.services') $log.debug('Encrypting private key for', fc.credentials.walletName); fc.setPrivateKeyEncryption(password); - root.lockFC(); + fc.lock(); root.updateCredentialsFC(function() { $log.debug('Wallet encrypted'); return cb(); @@ -661,51 +661,6 @@ angular.module('copayApp.services') }); }; - root.lockFC = function() { - var fc = root.focusedClient; - try { - fc.lock(); - } catch (e) {}; - }; - - root.unlockFC = function(opts, cb) { - opts = opts || {}; - var fc = opts.selectedClient || root.focusedClient; - - if (!fc.isPrivKeyEncrypted()) - return cb(); - - $log.debug('Wallet is encrypted'); - $rootScope.$emit('Local/NeedsPassword', false, function(err2, password) { - - if (err2) - return cb(err2); - - if (!password) - return cb(gettext('Spending Password needed')); - - try { - fc.unlock(password); - } catch (e) { - $log.warn('Error decrypting wallet:', e); - return cb(gettext('Wrong spending password')); - } - return cb(); - }); - }; - - root.isBackupNeeded = function(walletId, cb) { - var c = root.getClient(walletId); - if (c.isPrivKeyExternal()) return cb(false); - if (!c.credentials.mnemonic) return cb(false); - if (c.credentials.network == 'testnet') return cb(false); - - storageService.getBackupFlag(walletId, function(err, val) { - if (err || val) return cb(false); - return cb(true); - }); - }; - root.getWallets = function(network) { if (!root.profile) return []; diff --git a/src/js/services/txService.js b/src/js/services/txService.js deleted file mode 100644 index 7eb340f27..000000000 --- a/src/js/services/txService.js +++ /dev/null @@ -1,267 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('txService', function($rootScope, profileService, gettextCatalog, lodash, trezor, ledger, configService, bwsError, $log, feeService) { - var root = {}; - - var reportSigningStatus = function(opts) { - opts = opts || {}; - if (!opts.reporterFn) return; - - var fc = opts.selectedClient || profileService.focusedClient; - - if (fc.isPrivKeyExternal()) { - if (fc.getPrivKeyExternalSourceName() == 'ledger') { - opts.reporterFn(gettextCatalog.getString('Requesting Ledger Wallet to sign')); - } else if (fc.getPrivKeyExternalSourceName() == 'trezor') { - opts.reporterFn(gettextCatalog.getString('Requesting Trezor Wallet to sign')); - } - } else { - opts.reporterFn(gettextCatalog.getString('Signing payment')); - } - }; - - var reportBroadcastingStatus = function(opts) { - if (!opts.reporterFn) return; - - opts.reporterFn(gettextCatalog.getString('Broadcasting transaction')); - }; - - var stopReport = function(opts) { - if (!opts.reporterFn) return; - - opts.reporterFn(); - }; - - var requestTouchId = function(cb) { - try { - window.plugins.touchid.verifyFingerprint( - gettextCatalog.getString('Scan your fingerprint please'), - function(msg) { - $log.debug('Touch ID OK'); - return cb(); - }, - function(msg) { - $log.debug('Touch ID Failed:' + JSON.stringify(msg)); - return cb(gettextCatalog.getString('Touch ID Failed') + ': ' + msg.localizedDescription); - } - ); - } catch (e) { - $log.debug('Touch ID Failed:' + JSON.stringify(e)); - return cb(gettextCatalog.getString('Touch ID Failed')); - }; - }; - - root.setTouchId = function(cb) { - if (window.touchidAvailable) { - requestTouchId(cb); - } else { - return cb(); - } - }; - - root.checkTouchId = function(opts, cb) { - opts = opts || {}; - var config = configService.getSync(); - var fc = opts.selectedClient || profileService.focusedClient; - config.touchIdFor = config.touchIdFor || {}; - if (window.touchidAvailable && config.touchIdFor[fc.credentials.walletId]) { - requestTouchId(cb); - } else { - return cb(); - } - }; - - root.prepare = function(opts, cb) { - $log.info("at .prepare"); - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - $log.info('FC Client Dump:' + fc); - if (!fc.canSign() && !fc.isPrivKeyExternal()) - return cb('Cannot sign'); // should never happen, no need to translate - - root.checkTouchId(opts, function(err) { - if (err) { - $log.warn('CheckTouchId error:', err); - return cb(err); - }; - - profileService.unlockFC(opts, function(err) { - if (err) { - $log.warn("prepare/unlockFC error:", err); - return cb(err); - }; - - return cb(); - - }); - }); - }; - - root.removeTx = function(txp, opts, cb) { - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - - fc.removeTxProposal(txp, function(err) { - return cb(err); - }); - }; - - root.createTx = function(opts, cb) { - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - var currentSpendUnconfirmed = configService.getSync().wallet.spendUnconfirmed; - - var getFee = function(cb) { - if (opts.lockedCurrentFeePerKb) { - cb(null, opts.lockedCurrentFeePerKb); - } else { - feeService.getCurrentFeeValue(cb); - } - }; - - if (opts.sendMax) { - fc.createTxProposal(opts, function(err, txp) { - if (err) return cb(err); - else return cb(null, txp); - }); - }else { - getFee(function(err, feePerKb) { - if (err) return cb(err); - - opts.feePerKb = feePerKb; - opts.excludeUnconfirmedUtxos = currentSpendUnconfirmed ? false : true; - fc.createTxProposal(opts, function(err, txp) { - if (err) return cb(err); - else return cb(null, txp); - }); - }); - } - }; - - root.publishTx = function(txp, opts, cb) { - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - fc.publishTxProposal({txp: txp}, function(err, txp) { - if (err) return cb(err); - else return cb(null, txp); - }); - }; - - var _signWithLedger = function(txp, opts, cb) { - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - $log.info('Requesting Ledger Chrome app to sign the transaction'); - - ledger.signTx(txp, fc.credentials.account, function(result) { - $log.debug('Ledger response', result); - if (!result.success) - return cb(result.message || result.error); - - txp.signatures = lodash.map(result.signatures, function(s) { - return s.substring(0, s.length - 2); - }); - return fc.signTxProposal(txp, cb); - }); - }; - - var _signWithTrezor = function(txp, opts, cb) { - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - $log.info('Requesting Trezor to sign the transaction'); - - var xPubKeys = lodash.pluck(fc.credentials.publicKeyRing, 'xPubKey'); - trezor.signTx(xPubKeys, txp, fc.credentials.account, function(err, result) { - if (err) return cb(err); - - $log.debug('Trezor response', result); - txp.signatures = result.signatures; - return fc.signTxProposal(txp, cb); - }); - }; - - root.sign = function(txp, opts, cb) { - $log.info('at .sign'); - opts = opts || {}; - var fc = opts.selectedClient || profileService.focusedClient; - - if (fc.isPrivKeyExternal()) { - switch (fc.getPrivKeyExternalSourceName()) { - case 'ledger': - return _signWithLedger(txp, opts, cb); - case 'trezor': - return _signWithTrezor(txp, opts, cb); - default: - var msg = 'Unsupported External Key:' + fc.getPrivKeyExternalSourceName(); - $log.error(msg); - return cb(msg); - } - } else { - - txp.signatures = null; - $log.info('at .sign: (isEncrypted):', fc.isPrivKeyEncrypted()); - $log.info('txp BEFORE .signTxProposal:', txp); - - fc.signTxProposal(txp, function(err, signedTxp) { - $log.info('txp AFTER .signTxProposal:',err, signedTxp); - profileService.lockFC(); - return cb(err, signedTxp); - }); - } - }; - - root.signAndBroadcast = function(txp, opts, cb) { - $log.info('at .signAndBroadcast'); - opts = opts || {}; - reportSigningStatus(opts); - - var fc = opts.selectedClient || profileService.focusedClient; - root.sign(txp, opts, function(err, txp) { - if (err) { - stopReport(opts); - return bwsError.cb(err, gettextCatalog.getString('Could not accept payment'), cb); - }; - - if (txp.status != 'accepted') { - stopReport(opts); - return cb(null, txp); - } - - reportBroadcastingStatus(opts); - fc.broadcastTxProposal(txp, function(err, txp, memo) { - stopReport(opts); - - if (err) { - return bwsError.cb(err, gettextCatalog.getString('Could not broadcast payment'), cb); - }; - - $log.debug('Transaction signed and broadcasted') - - if (memo) - $log.info(memo); - - return cb(null, txp); - }); - }); - }; - - root.prepareAndSignAndBroadcast = function(txp, opts, cb) { - opts = opts || {}; - $log.info('at .prepareSignAndBroadcast'); - - root.prepare(opts, function(err) { - if (err) { - $log.warn('Prepare error:' + err); - stopReport(opts); - return cb(err); - }; - root.signAndBroadcast(txp, opts, function(err, txp) { - if (err) { - stopReport(opts); - return cb(err); - }; - return cb(null, txp); - }); - }); - }; - return root; -}); diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js new file mode 100644 index 000000000..c2d060a53 --- /dev/null +++ b/src/js/services/walletService.js @@ -0,0 +1,201 @@ +'use strict'; + +angular.module('copayApp.services').factory('walletService', function($log, lodash, trezor, ledger, storageService) { + var root = {}; + + var _signWithLedger = function(client, txp, cb) { + $log.info('Requesting Ledger Chrome app to sign the transaction'); + + ledger.signTx(txp, client.credentials.account, function(result) { + $log.debug('Ledger response', result); + if (!result.success) + return cb(result.message || result.error); + + txp.signatures = lodash.map(result.signatures, function(s) { + return s.substring(0, s.length - 2); + }); + return client.signTxProposal(txp, cb); + }); + }; + + var _signWithTrezor = function(client, txp, cb) { + $log.info('Requesting Trezor to sign the transaction'); + + var xPubKeys = lodash.pluck(client.credentials.publicKeyRing, 'xPubKey'); + trezor.signTx(xPubKeys, txp, client.credentials.account, function(err, result) { + if (err) return cb(err); + + $log.debug('Trezor response', result); + txp.signatures = result.signatures; + return client.signTxProposal(txp, cb); + }); + }; + + root.isBackupNeeded = function(client, cb) { + if (client.isPrivKeyExternal()) return cb(false); + if (!client.credentials.mnemonic) return cb(false); + if (client.credentials.network == 'testnet') return cb(false); + + storageService.getBackupFlag(client.credentials.walletId, function(err, val) { + if (err) $log.error(err); + if (val) return cb(false); + return cb(true); + }); + }; + + root.isReady = function(client, cb) { + if(!client.isComplete()) + return cb('WALLET_NOT_COMPLETE'); + root.isBackupNeeded(client, function(needsBackup) { + if (needsBackup) + return cb('WALLET_NEEDS_BACKUP'); + return cb(); + }); + }; + + root.isEncrypted = function(client) { + if (lodash.isEmpty(client)) return; + var isEncrypted = client.isPrivKeyEncrypted(); + if (isEncrypted) $log.debug('Wallet is encrypted'); + return isEncrypted; + }; + + root.lock = function(client) { + try { + client.lock(); + } catch (e) { + $log.warn('Encrypting wallet:', e); + }; + }; + + root.unlock = function(client, password) { + if (lodash.isEmpty(client)) + return 'MISSING_PARAMETER'; + if (lodash.isEmpty(password)) + return 'NO_PASSWORD_GIVEN'; + try { + client.unlock(password); + } catch (e) { + $log.warn('Decrypting wallet:', e); + return 'PASSWORD_INCORRECT'; + } + }; + + root.createTx = function(client, txp, cb) { + if (lodash.isEmpty(txp) || lodash.isEmpty(client)) + return cb('MISSING_PARAMETER'); + + if (txp.sendMax) { + client.createTxProposal(txp, function(err, createdTxp) { + if (err) return cb(err); + else return cb(null, createdTxp); + }); + } else { + client.getFeeLevels(client.credentials.network, function(err, levels) { + if (err) return cb(err); + + var feeLevelValue = lodash.find(levels, { + level: txp.feeLevel + }); + + if (!feeLevelValue || !feeLevelValue.feePerKB) + return cb({ + message: 'Could not get dynamic fee for level: ' + feeLevel + }); + + $log.debug('Dynamic fee: ' + txp.feeLevel + ' ' + feeLevelValue.feePerKB + ' SAT'); + + txp.feePerKb = feeLevelValue.feePerKB; + client.createTxProposal(txp, function(err, createdTxp) { + if (err) return cb(err); + else { + $log.debug('Transaction created'); + return cb(null, createdTxp); + } + }); + }); + } + }; + + root.publishTx = function(client, txp, cb) { + if (lodash.isEmpty(txp) || lodash.isEmpty(client)) + return cb('MISSING_PARAMETER'); + + client.publishTxProposal({txp: txp}, function(err, publishedTx) { + if (err) return cb(err); + else { + $log.debug('Transaction published'); + return cb(null, publishedTx); + } + }); + }; + + root.signTx = function(client, txp, cb) { + if (lodash.isEmpty(txp) || lodash.isEmpty(client)) + return cb('MISSING_PARAMETER'); + + if (client.isPrivKeyExternal()) { + switch (client.getPrivKeyExternalSourceName()) { + case 'ledger': + return _signWithLedger(client, txp, cb); + case 'trezor': + return _signWithTrezor(client, txp, cb); + default: + var msg = 'Unsupported External Key:' + client.getPrivKeyExternalSourceName(); + $log.error(msg); + return cb(msg); + } + } else { + + try { + client.signTxProposal(txp, function(err, signedTxp) { + $log.debug('Transaction signed'); + return cb(err, signedTxp); + }); + } catch (e) { + $log.warn('Error at signTxProposal:', e); + return cb(e); + } + } + }; + + root.broadcastTx = function(client, txp, cb) { + if (lodash.isEmpty(txp) || lodash.isEmpty(client)) + return cb('MISSING_PARAMETER'); + + if (txp.status != 'accepted') + return cb('TX_NOT_ACCEPTED'); + + client.broadcastTxProposal(txp, function(err, broadcastedTxp, memo) { + if (err) + return cb(err); + + $log.debug('Transaction broadcasted'); + if (memo) $log.info(memo); + + return cb(null, broadcastedTxp); + }); + }; + + root.rejectTx = function(client, txp, cb) { + if (lodash.isEmpty(txp) || lodash.isEmpty(client)) + return cb('MISSING_PARAMETER'); + + client.rejectTxProposal(txp, null, function(err, rejectedTxp) { + $log.debug('Transaction rejected'); + return cb(err, rejectedTxp); + }); + }; + + root.removeTx = function(client, txp, cb) { + if (lodash.isEmpty(txp) || lodash.isEmpty(client)) + return cb('MISSING_PARAMETER'); + + client.removeTxProposal(txp, function(err) { + $log.debug('Transaction removed'); + return cb(err); + }); + }; + + return root; +}); diff --git a/test/controllers/sidebar.test.js b/test/controllers/sidebar.test.js deleted file mode 100644 index ffa7d7137..000000000 --- a/test/controllers/sidebar.test.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -describe('sidebarController', function(){ - var scope, controller; - - beforeEach(angular.mock.module('copayApp.controllers')); - - it('wallet selection', function(){ - expect(true).not.toBeUndefined(); - }); -}); diff --git a/test/karma.conf.js b/test/karma.conf.js index 881d6b189..0c252bd3f 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -10,7 +10,7 @@ module.exports = function(config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], + frameworks: ['mocha', 'chai'], // list of files / patterns to load in the browser @@ -20,16 +20,20 @@ module.exports = function(config) { 'bower_components/moment/min/moment-with-locales.js', 'bower_components/angular/angular.js', 'bower_components/angular-ui-router/release/angular-ui-router.js', - 'bower_components/angular-foundation/mm-foundation.js', 'bower_components/angular-foundation/mm-foundation-tpls.js', - 'bower_components/angular-animate/angular-animate.js', + 'bower_components/angular-touch/angular-touch.js', + 'bower_components/fastclick/lib/fastclick.js', + 'bower_components/stateful-fastclick/dist/stateful-fastclick.js', + 'bower_components/angular-stateful-fastclick/lib/angular-stateful-fastclick.js', 'bower_components/angular-moment/angular-moment.js', 'bower_components/ng-lodash/build/ng-lodash.js', - 'bower_components/angular-qrcode/qrcode.js', + 'bower_components/angular-qrcode/angular-qrcode.js', 'bower_components/angular-gettext/dist/angular-gettext.js', - 'bower_components/angular-touch/angular-touch.js', 'bower_components/angular-ui-switch/angular-ui-switch.js', - 'node_modules/angular-mocks/angular-mocks.js', + 'bower_components/angular-sanitize/angular-sanitize.js', + 'bower_components/ng-csv/build/ng-csv.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'angular-bitcore-wallet-client/angular-bitcore-wallet-client.js', 'src/js/**/*.js', 'test/**/*.js' ], @@ -38,7 +42,7 @@ module.exports = function(config) { // list of files to exclude exclude: [ 'src/js/translations.js', - 'src/js/version.js', + // 'src/js/version.js', 'test/karma.conf.js', 'test/old/*' ], @@ -54,7 +58,7 @@ module.exports = function(config) { // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', , 'coverage'], + reporters: ['mocha', 'coverage'], // optionally, configure the reporter coverageReporter: { @@ -91,6 +95,16 @@ module.exports = function(config) { // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: ['PhantomJS'], + plugins: [ + 'karma-mocha-reporter', + 'karma-coverage', + 'karma-mocha', + 'karma-chai', + 'karma-sinon', + 'karma-phantomjs-launcher', + 'karma-chrome-launcher', + ], + // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits diff --git a/test/mocks/stateMocks.js b/test/mocks/stateMocks.js deleted file mode 100644 index 5a71a5469..000000000 --- a/test/mocks/stateMocks.js +++ /dev/null @@ -1,30 +0,0 @@ -angular.module('stateMock',[]); -angular.module('stateMock').service("$state", function($q){ - this.expectedTransitions = []; - this.transitionTo = function(stateName){ - if(this.expectedTransitions.length > 0){ - var expectedState = this.expectedTransitions.shift(); - if(expectedState !== stateName){ - throw Error("Expected transition to state: " + expectedState + " but transitioned to " + stateName ); - } - }else{ - throw Error("No more transitions were expected! Tried to transition to "+ stateName ); - } - console.log("Mock transition to: " + stateName); - var deferred = $q.defer(); - var promise = deferred.promise; - deferred.resolve(); - return promise; - } - this.go = this.transitionTo; - this.expectTransitionTo = function(stateName){ - this.expectedTransitions.push(stateName); - } - - - this.ensureAllTransitionsHappened = function(){ - if(this.expectedTransitions.length > 0){ - throw Error("Not all transitions happened!"); - } - } -}); diff --git a/test/old/sidebar.test.js b/test/old/sidebar.test.js new file mode 100644 index 000000000..d3a197e69 --- /dev/null +++ b/test/old/sidebar.test.js @@ -0,0 +1,11 @@ +// 'use strict'; +// +// describe('sidebarController', function(){ +// var scope, controller; +// +// beforeEach(angular.mock.module('copayApp.controllers')); +// +// it('dummy test', function(){ +// should.exist(true); +// }); +// }); diff --git a/test/walletService.test.js b/test/walletService.test.js new file mode 100644 index 000000000..006fddcf0 --- /dev/null +++ b/test/walletService.test.js @@ -0,0 +1,25 @@ +describe('walletService', function() { + + var walletService; + + + // Adds walletService's module dependencies + beforeEach(function() { + module('ngLodash'); + module('gettext'); + module('bwcModule'); + module('copayApp.services'); + }); + + + beforeEach(inject(function(_walletService_) { + walletService = _walletService_; + })); + + describe('walletService', function() { + it('should be defined', function() { + should.exist(walletService); + }); + }); + +});