diff --git a/Gruntfile.js b/Gruntfile.js index ab3434ee7..bff09ff9a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,8 +37,7 @@ module.exports = function(grunt) { 'src/js/routes.js', 'src/js/services/*.js', 'src/js/models/*.js', - 'src/js/controllers/*.js', - 'src/js/trezor.js' + 'src/js/controllers/*.js' ], tasks: ['concat:js'] } @@ -79,7 +78,8 @@ module.exports = function(grunt) { 'src/js/translations.js', 'src/js/version.js', 'src/js/init.js', - 'src/js/trezor.js' + 'src/js/trezor-url.js', + 'bower_components/trezor-connect/login.js' ], dest: 'public/js/copay.js' }, diff --git a/baseDerivation.md b/baseDerivation.md new file mode 100644 index 000000000..e238e4f4c --- /dev/null +++ b/baseDerivation.md @@ -0,0 +1,28 @@ + +Copay accepts three base derivation paths: + + * m/44' + * m/48' (only used for MULTISIGNATURE, HARDWARE Wallets) + * m/45' (deprecated and it is only supported for old wallets) + +Both m/44 and m/48 follow the BIP44 standard: + +m/XX'/'/ + +Supported cointypes are: 0: Livenet, and 1: Testnet + +If you need to import a wallet from a mnemonic using an account different +from the default (0), use, for example: + + m/44'/0'/11' + +to import account 11. + +In case you have a multisignature wallet originally created from a hardware device, and you had loose access to the device, you will need to enter the 24 mnemonic backup (from the device) and a path like: + + + m/48'/0'/8' + +for a multisignature wallet, account 8. + +Finally, note that TREZOR use 1-based account numbers, so if your are trying for example to recover TREZOR multisig account #8, you should enter `m/48'/0'/7'`. diff --git a/bower.json b/bower.json index 0ed4c3e88..45f6bf216 100644 --- a/bower.json +++ b/bower.json @@ -8,7 +8,7 @@ ], "dependencies": { "angular": "1.4.6", - "angular-bitcore-wallet-client": "1.1.2", + "angular-bitcore-wallet-client": "1.1.6", "angular-foundation": "0.7.0", "angular-gettext": "2.1.0", "angular-moment": "0.10.1", @@ -21,6 +21,7 @@ "foundation-icon-fonts": "*", "moment": "2.10.3", "ng-lodash": "0.2.3", - "qrcode-decoder-js": "*" + "qrcode-decoder-js": "*", + "trezor-connect": "~1.0.1" } } diff --git a/browser-extensions/chrome/build.sh b/browser-extensions/chrome/build.sh index 034b910ec..a5c4179c7 100755 --- a/browser-extensions/chrome/build.sh +++ b/browser-extensions/chrome/build.sh @@ -55,6 +55,12 @@ echo $CMD $CMD checkOK +cd $BUILDDIR/../.. +CMD="rsync -rLRv ./bower_components/trezor-connect/chrome/* $APPDIR" +echo $CMD +$CMD +checkOK + # Zipping chrome-extension echo "${OpenColor}${Green}* Zipping all chrome-extension files...${CloseColor}" cd $BUILDDIR diff --git a/browser-extensions/chrome/manifest.json b/browser-extensions/chrome/manifest.json index c628f0073..200f024b4 100644 --- a/browser-extensions/chrome/manifest.json +++ b/browser-extensions/chrome/manifest.json @@ -6,7 +6,8 @@ "permissions": [ "storage", "notifications", - "videoCapture" + "videoCapture", + "webview" ], "app": { "background": { diff --git a/public/views/create.html b/public/views/create.html index a47499fce..79f17bf97 100644 --- a/public/views/create.html +++ b/public/views/create.html @@ -56,21 +56,19 @@ -
-
- -
-
- -
+
+ +
+
+
@@ -98,74 +96,80 @@
+
- - - - - - - +
+ +
- +
+ +
- - +
+ + + + WARNING: Passphrase cannot be recovered. Be sure to write it down. The wallet can not be restored without the passphrase. + + +
- -
-
+
+ +
-
- - - - WARNING: Passphrase cannot be recovered. Be sure to write it down. The wallet can not be restored without the passphrase. - - -
+
+ +
+
+ +
+
+ +
+
+ +
+
+
- - + + +
diff --git a/public/views/export.html b/public/views/export.html index cf2880e81..756ea1e0a 100644 --- a/public/views/export.html +++ b/public/views/export.html @@ -45,7 +45,7 @@

- -
-

+
+

Spending Restrictions

-
    -
  • +
      +
    • Request Password
    • diff --git a/public/views/preferencesInformation.html b/public/views/preferencesInformation.html index 56eedefc4..eb510141c 100644 --- a/public/views/preferencesInformation.html +++ b/public/views/preferencesInformation.html @@ -15,6 +15,7 @@ +
    • Wallet Id @@ -29,6 +30,8 @@
    • + +
    • Wallet Network @@ -52,6 +55,39 @@
    • +
    • + Hardware Wallet + + {{index.externalSource}} + +
    • + +
    • + + + No private key + +
    • + +
    • + Account ({{derivationStrategy}}) + + #{{index.account}} + +
    • + + + +

      Copayers

      +
    • + + {{copayer.name}} ({{'Me'|translate}}) + + + {{copayer.name}} + +
    • +

      Extended Public Keys

    • diff --git a/public/views/walletHome.html b/public/views/walletHome.html index 353f5198f..b91cde671 100644 --- a/public/views/walletHome.html +++ b/public/views/walletHome.html @@ -118,13 +118,11 @@
      -
      +

      {{(index.alias || index.walletName)}}

      - Multisignature wallet - ({{index.m}}-of-{{index.n}})
      @@ -444,7 +442,7 @@ -->
      -
      +
      @@ -457,7 +455,7 @@
      -
      +
      @@ -467,18 +465,16 @@
      -

      +

      Initial transaction history synchronization can take some minutes for wallets with many transactions.
      Please stand by. -

      -
      - +
      + {{index.txProgress}} Transactions
      + Downloaded +
      +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +
      + +
      + +
      diff --git a/src/js/controllers/create.js b/src/js/controllers/create.js index d7f84738f..490cbd82c 100644 --- a/src/js/controllers/create.js +++ b/src/js/controllers/create.js @@ -1,11 +1,12 @@ 'use strict'; angular.module('copayApp.controllers').controller('createController', - function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isCordova, gettext, ledger, trezor, isMobile) { + function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isCordova, gettext, ledger, trezor, isMobile, isChromeApp, isDevel, derivationPathHelper) { var self = this; var defaults = configService.getDefaults(); this.isWindowsPhoneApp = isMobile.Windows() && isCordova; + $scope.account = 1; /* For compressed keys, m*73 + n*34 <= 496 */ var COPAYER_PAIR_LIMITS = { @@ -25,6 +26,7 @@ angular.module('copayApp.controllers').controller('createController', var defaults = configService.getDefaults(); $scope.bwsurl = defaults.bws.url; + $scope.derivationPath = derivationPathHelper.default; // ng-repeat defined number of times instead of repeating over array? this.getNumber = function(num) { @@ -38,11 +40,47 @@ angular.module('copayApp.controllers').controller('createController', $scope.requiredCopayers = Math.min(parseInt(n / 2 + 1), maxReq); }; + var updateSeedSourceSelect = function(n) { + + self.seedOptions = [{ + id: 'new', + label: gettext('New Random Seed'), + }, { + id: 'set', + label: gettext('Specify Seed...'), + }]; + $scope.seedSource = self.seedOptions[0]; + + if (n > 1 && isChromeApp) + self.seedOptions.push({ + id: 'ledger', + label: gettext('Ledger Hardware Wallet'), + }); + + if (isChromeApp || isDevel) { + self.seedOptions.push({ + id: 'trezor', + label: gettext('Trezor Hardware Wallet'), + }); + } + }; + this.TCValues = lodash.range(2, defaults.limits.totalCopayers + 1); $scope.totalCopayers = defaults.wallet.totalCopayers; this.setTotalCopayers = function(tc) { updateRCSelect(tc); + updateSeedSourceSelect(tc); + self.seedSourceId = $scope.seedSource.id; + }; + + + this.setSeedSource = function(src) { + self.seedSourceId = $scope.seedSource.id; + + $timeout(function() { + $rootScope.$apply(); + }); }; this.create = function(form) { @@ -50,23 +88,36 @@ angular.module('copayApp.controllers').controller('createController', this.error = gettext('Please enter the required fields'); return; } + var opts = { m: $scope.requiredCopayers, n: $scope.totalCopayers, name: form.walletName.$modelValue, myName: $scope.totalCopayers > 1 ? form.myName.$modelValue : null, networkName: form.isTestnet.$modelValue ? 'testnet' : 'livenet', - bwsurl: $scope.bwsurl + bwsurl: $scope.bwsurl, }; - var setSeed = form.setSeed.$modelValue; + var setSeed = self.seedSourceId == 'set'; if (setSeed) { - var words = form.privateKey.$modelValue; + + var words = form.privateKey.$modelValue || ''; if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) { opts.extendedPrivateKey = words; } else { opts.mnemonic = words; } opts.passphrase = form.passphrase.$modelValue; + + var pathData = derivationPathHelper.parse($scope.derivationPath); + if (!pathData) { + this.error = gettext('Invalid derivation path'); + return; + } + + opts.account = pathData.account; + opts.networkName = pathData.networkName; + opts.derivationStrategy = pathData.derivationStrategy; + } else { opts.passphrase = form.createPassphrase.$modelValue; } @@ -76,14 +127,21 @@ angular.module('copayApp.controllers').controller('createController', return; } - if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { - self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR'; + if (self.seedSourceId == 'ledger' || self.seedSourceId == 'trezor') { + var account = $scope.account; + if (!account || account < 1) { + this.error = gettext('Invalid account number'); + return; + } - var src = form.hwLedger.$modelValue ? ledger : trezor; + if ( self.seedSourceId == 'trezor') + account = account - 1; - // TODO : account - var account = 0; - src.getInfoForNewWallet(account, function(err, lopts) { + opts.account = account; + self.hwWallet = self.seedSourceId == 'ledger' ? 'Ledger' : 'Trezor'; + var src = self.seedSourceId == 'ledger' ? ledger : trezor; + + src.getInfoForNewWallet(opts.n > 1, account, function(err, lopts) { self.hwWallet = false; if (err) { self.error = err; @@ -141,4 +199,7 @@ angular.module('copayApp.controllers').controller('createController', $scope.$on("$destroy", function() { $rootScope.hideWalletNavigation = false; }); + + updateSeedSourceSelect(1); + self.setSeedSource('new'); }); diff --git a/src/js/controllers/import.js b/src/js/controllers/import.js index b473c8fdc..f89c3dd21 100644 --- a/src/js/controllers/import.js +++ b/src/js/controllers/import.js @@ -1,12 +1,14 @@ 'use strict'; angular.module('copayApp.controllers').controller('importController', - function($scope, $rootScope, $location, $timeout, $log, profileService, configService, notification, go, sjcl, gettext, lodash, ledger, trezor) { + function($scope, $rootScope, $location, $timeout, $log, profileService, configService, notification, go, sjcl, gettext, lodash, ledger, trezor, isChromeApp, isDevel, derivationPathHelper) { var self = this; var reader = new FileReader(); var defaults = configService.getDefaults(); $scope.bwsurl = defaults.bws.url; + $scope.derivationPath = derivationPathHelper.default; + $scope.account = 1; window.ignoreMobilePause = true; $scope.$on('$destroy', function() { @@ -15,6 +17,27 @@ angular.module('copayApp.controllers').controller('importController', }, 100); }); + var updateSeedSourceSelect = function() { + self.seedOptions = []; + + if (isChromeApp) { + self.seedOptions.push({ + id: 'ledger', + label: gettext('Ledger Hardware Wallet'), + }); + } + + if (isChromeApp || isDevel) { + self.seedOptions.push({ + id: 'trezor', + label: gettext('Trezor Hardware Wallet'), + }); + $scope.seedSource = self.seedOptions[0]; + } + }; + + + this.setType = function(type) { $scope.type = type; this.error = null; @@ -173,23 +196,23 @@ angular.module('copayApp.controllers').controller('importController', } opts.passphrase = form.passphrase.$modelValue || null; - opts.networkName = form.isTestnet.$modelValue ? 'testnet' : 'livenet'; + + var pathData = derivationPathHelper.parse($scope.derivationPath); + if (!pathData) { + this.error = gettext('Invalid derivation path'); + return; + } + opts.account = pathData.account; + opts.networkName = pathData.networkName; + opts.derivationStrategy = pathData.derivationStrategy; + _importMnemonic(words, opts); }; - this.importTrezor = function(form) { + this.importTrezor = function(account, isMultisig) { var self = this; - if (form.$invalid) { - this.error = gettext('There is an error in the form'); - $timeout(function() { - $scope.$apply(); - }); - return; - } - self.hwWallet = 'Trezor'; - // TODO account - trezor.getInfoForNewWallet(0, function(err, lopts) { + trezor.getInfoForNewWallet(isMultisig, account, function(err, lopts) { self.hwWallet = false; if (err) { self.error = err; @@ -217,18 +240,53 @@ angular.module('copayApp.controllers').controller('importController', }, 100); }; - this.importLedger = function(form) { - var self = this; - if (form.$invalid) { + this.importHW = function(form) { + if (form.$invalid || $scope.account < 0 ) { this.error = gettext('There is an error in the form'); $timeout(function() { $scope.$apply(); }); return; } - self.hwWallet = 'Ledger'; - // TODO account - ledger.getInfoForNewWallet(0, function(err, lopts) { + this.error = ''; + + var account = + $scope.account; + + if (self.seedSourceId == 'trezor') { + if ( account < 1) { + this.error = gettext('Invalid account number'); + return; + } + account = account - 1; + } + var isMultisig = form.isMultisig.$modelValue; + + switch (self.seedSourceId) { + case ('ledger'): + self.hwWallet = 'Ledger'; + self.importLedger(account); + break; + case ('trezor'): + self.hwWallet = 'Trezor'; + self.importTrezor(account, isMultisig); + break; + default: + throw ('Error: bad source id'); + }; + }; + + this.setSeedSource = function() { + if (!$scope.seedSource) return; + self.seedSourceId = $scope.seedSource.id; + + $timeout(function() { + $rootScope.$apply(); + }); + }; + + this.importLedger = function(account) { + var self = this; + ledger.getInfoForNewWallet(true, account, function(err, lopts) { self.hwWallet = false; if (err) { self.error = err; @@ -255,4 +313,6 @@ angular.module('copayApp.controllers').controller('importController', }, 100); }; + updateSeedSourceSelect(); + self.setSeedSource('new'); }); diff --git a/src/js/controllers/index.js b/src/js/controllers/index.js index b01b9b6f3..26154dd1b 100644 --- a/src/js/controllers/index.js +++ b/src/js/controllers/index.js @@ -7,7 +7,8 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.isChromeApp = isChromeApp; self.isSafari = isMobile.Safari(); self.onGoingProcess = {}; - self.limitHistory = 6; + self.historyShowLimit = 10; + self.updatingTxHistory = {}; function strip(number) { return (parseFloat(number.toPrecision(12))); @@ -83,7 +84,9 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.currentFeeLevel = null; self.notAuthorized = false; self.txHistory = []; - self.txHistoryUnique = {}; + self.completeHistory = []; + self.txProgress = 0; + self.historyShowShowAll = false; self.balanceByAddress = null; self.pendingTxProposalsCountForUs = null; self.setSpendUnconfirmed(); @@ -106,7 +109,13 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.isComplete = fc.isComplete(); self.canSign = fc.canSign(); self.isPrivKeyExternal = fc.isPrivKeyExternal(); + self.isPrivKeyEncrypted = fc.isPrivKeyEncrypted(); self.externalSource = fc.getPrivKeyExternalSourceName(); + self.account = fc.credentials.account; + + if (self.externalSource == 'trezor') + self.account++; + self.txps = []; self.copayers = []; self.updateColor(); @@ -115,6 +124,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.initGlidera(); + self.setCustomBWSFlag(); if (fc.isPrivKeyExternal()) { self.needsBackup = false; self.openWallet(); @@ -127,6 +137,13 @@ angular.module('copayApp.controllers').controller('indexController', function($r }); }; + self.setCustomBWSFlag = function() { + var defaults = configService.getDefaults(); + var config = configService.getSync(); + + self.usingCustomBWS = config.bwsFor && (config.bwsFor[self.walletId] != defaults.bws.url); + }; + self.setTab = function(tab, reset, tries, switchState) { tries = tries || 0; @@ -485,12 +502,13 @@ angular.module('copayApp.controllers').controller('indexController', function($r var SAFE_CONFIRMATIONS = 6; - self.setTxHistory = function(txs) { + self.processNewTxs = function(txs) { var config = configService.getSync().wallet.settings; var now = Math.floor(Date.now() / 1000); - self.txHistoryUnique = {}; - + var txHistoryUnique = {}; + var ret = []; self.hasUnsafeConfirmed = false; + lodash.each(txs, function(tx) { tx = txFormatService.processTx(tx); @@ -505,13 +523,15 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.hasUnsafeConfirmed = true; } - if (!self.txHistoryUnique[tx.txid]) { - self.txHistory.push(tx); - self.txHistoryUnique[tx.txid] = true; + if (!txHistoryUnique[tx.txid]) { + ret.push(tx); + txHistoryUnique[tx.txid] = true; } else { $log.debug('Ignoring duplicate TX in history: ' + tx.txid) } }); + + return ret; }; self.updateAlias = function() { @@ -735,13 +755,6 @@ angular.module('copayApp.controllers').controller('indexController', function($r }); }; - self.stopSync = function(remoteTx, localTx) { - if (remoteTx.txid == localTx.txid) - return true; - else - return false; - } - self.removeSoftConfirmedTx = function(txs) { return lodash.map(txs, function(tx) { if (tx.confirmations >= SOFT_CONFIRMATION_LIMIT) @@ -749,17 +762,14 @@ angular.module('copayApp.controllers').controller('indexController', function($r }); } - self.getConfirmedTxs = function(cb) { - var fc = profileService.focusedClient; - var c = fc.credentials; + self.getConfirmedTxs = function(walletId, cb) { - storageService.getTxHistory(c.walletId, function(err, txs) { + storageService.getTxHistory(walletId, function(err, txs) { if (err) return cb(err); var localTxs = []; if (!txs) { - self.showWaitingSign = true; return cb(null, localTxs); } @@ -772,72 +782,107 @@ angular.module('copayApp.controllers').controller('indexController', function($r }); } - self.updateLocalTxHistory = function(cb) { - self.getConfirmedTxs(function(err, txsFromLocal) { + self.updateLocalTxHistory = function(client, cb) { + var requestLimit = 6; + var walletId = client.credentials.walletId; + + self.getConfirmedTxs(walletId, function(err, txsFromLocal) { if (err) return cb(err); + var endingTxid = txsFromLocal[0] ? txsFromLocal[0].txid : null; - var fc = profileService.focusedClient; - var c = fc.credentials; - fillTxsObject(); + function getNewTxs(newTxs, skip, i_cb) { - function fillTxsObject(txsResult, index) { - txsResult = txsResult || []; - index = index || 0; + self.getTxsFromServer(client, skip, endingTxid, requestLimit, function(err, res, shouldContinue) { + if (err) return i_cb(err); - self.makeTxHistoryRequest(txsResult, index, txsFromLocal[0], function(err, newIndex, exitLoop) { - if (err) return cb(err); - if (exitLoop) { - self.txHistory = []; - self.setTxHistory(lodash.compact(txsResult.concat(txsFromLocal))); - return storageService.setTxHistory(JSON.stringify(self.txHistory), c.walletId, function() { - return cb(null); - }); + + newTxs = newTxs.concat(lodash.compact(res)); + skip = skip + requestLimit; + + $log.debug('Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue); + + if (!shouldContinue) { + newTxs = self.processNewTxs(newTxs); + $log.debug('Finish Sync: New Txs: ' + newTxs.length); + return i_cb(null, newTxs); } - fillTxsObject(txsResult, newIndex); + + if (walletId == profileService.focusedClient.credentials.walletId) + self.txProgress = newTxs.length; + + $timeout(function() { + $rootScope.$apply(); + }); + getNewTxs(newTxs, skip, i_cb); }); }; + + getNewTxs([], 0, function(err, txs) { + if (err) return cb(err); + + var newHistory = lodash.compact(txs.concat(txsFromLocal)); + $log.debug('Tx History synced. Total Txs: ' + newHistory.length); + + if (walletId == profileService.focusedClient.credentials.walletId) { + self.completeHistory = newHistory; + self.txHistory = newHistory.slice(0, self.historyShowLimit); + self.historyShowShowAll = newHistory.length >= self.historyShowLimit; + } + + return storageService.setTxHistory(JSON.stringify(newHistory), walletId, function() { + return cb(); + }); + }); }); } + self.showAllHistory = function() { + self.historyShowShowAll = false; + self.historyRendering = true; + $timeout(function() { + $rootScope.$apply(); + $timeout(function() { + self.historyRendering = false; + self.txHistory = self.completeHistory; + }, 100); + }, 100); + }; - self.makeTxHistoryRequest = function(txsResult, index, endingTx, cb) { - var fc = profileService.focusedClient; - var c = fc.credentials; - var exitLoop = false; + self.getTxsFromServer = function(client, skip, endingTxid, limit, cb) { + var res = []; - fc.getTxHistory({ - skip: index, - limit: self.limitHistory + 1 - }, function(err, txsFromBWC) { + client.getTxHistory({ + skip: skip, + limit: limit + }, function(err, txsFromServer) { if (err) return cb(err); - if (!txsFromBWC[0]) - exitLoop = true; + if (!txsFromServer.length) + return cb(); - lodash.each(txsFromBWC, function(t) { - if (!endingTx) txsResult.push(t); - else { - if (!self.stopSync(t, endingTx) && !exitLoop) { - txsResult.push(t); - } else { - exitLoop = true; - } - } + var res = lodash.takeWhile(txsFromServer, function(tx) { + return tx.txid != endingTxid; }); - index = index + self.limitHistory; - return cb(null, index, exitLoop); + + return cb(null, res, res.length == limit); }); - } + }; self.updateHistory = function() { + var fc = profileService.focusedClient; + var walletId = fc.credentials.walletId; + + if (!fc.isComplete() || self.updatingTxHistory[walletId]) return; + $log.debug('Updating Transaction History'); self.txHistoryError = false; - self.updatingTxHistory = true; + self.updatingTxHistory[walletId] = true; $timeout(function() { - self.updateLocalTxHistory(function(err) { - if (err) self.txHistoryError = true; - self.updatingTxHistory = false; - self.showWaitingSign = false; + self.updateLocalTxHistory(fc, function(err) { + self.updatingTxHistory[walletId] = false; + if (err) + self.txHistoryError = true; + $rootScope.$apply(); }); }); @@ -1288,4 +1333,12 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.setFocusedWallet(); }); }); + + $rootScope.$on('Local/NewEncryptionSetting', function() { + var fc = profileService.focusedClient; + self.isPrivKeyEncrypted = fc.isPrivKeyEncrypted(); + $timeout(function() { + $rootScope.$apply(); + }); + }); }); diff --git a/src/js/controllers/join.js b/src/js/controllers/join.js index 9b5c044ed..31ae0158d 100644 --- a/src/js/controllers/join.js +++ b/src/js/controllers/join.js @@ -1,11 +1,12 @@ 'use strict'; angular.module('copayApp.controllers').controller('joinController', - function($scope, $rootScope, $timeout, go, notification, profileService, configService, isCordova, storageService, applicationService, $modal, gettext, lodash, ledger, trezor) { + function($scope, $rootScope, $timeout, go, notification, profileService, configService, isCordova, storageService, applicationService, $modal, gettext, lodash, ledger, trezor, isChromeApp, isDevel,derivationPathHelper) { var self = this; var defaults = configService.getDefaults(); $scope.bwsurl = defaults.bws.url; + $scope.derivationPath = derivationPathHelper.default; this.onQrCodeScanned = function(data) { $scope.secret = data; @@ -13,6 +14,42 @@ angular.module('copayApp.controllers').controller('joinController', $scope.joinForm.secret.$render(); }; + + var updateSeedSourceSelect = function() { + self.seedOptions = [{ + id: 'new', + label: gettext('New Random Seed'), + }, { + id: 'set', + label: gettext('Specify Seed...'), + }]; + $scope.seedSource = self.seedOptions[0]; + + + if (isChromeApp) { + self.seedOptions.push({ + id: 'ledger', + label: gettext('Ledger Hardware Wallet'), + }); + } + + if (isChromeApp || isDevel) { + self.seedOptions.push({ + id: 'trezor', + label: gettext('Trezor Hardware Wallet'), + }); + } + }; + + this.setSeedSource = function(src) { + self.seedSourceId = $scope.seedSource.id; + self.accountValues = lodash.range(1, 100); + + $timeout(function() { + $rootScope.$apply(); + }); + }; + this.join = function(form) { if (form && form.$invalid) { self.error = gettext('Please enter the required fields'); @@ -23,10 +60,10 @@ angular.module('copayApp.controllers').controller('joinController', var opts = { secret: form.secret.$modelValue, myName: form.myName.$modelValue, - bwsurl: $scope.bwsurl + bwsurl: $scope.bwsurl, } - var setSeed = form.setSeed.$modelValue; + var setSeed = self.seedSourceId =='set'; if (setSeed) { var words = form.privateKey.$modelValue; if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) { @@ -35,6 +72,15 @@ angular.module('copayApp.controllers').controller('joinController', opts.mnemonic = words; } opts.passphrase = form.passphrase.$modelValue; + + var pathData = derivationPathHelper.parse($scope.derivationPath); + if (!pathData) { + this.error = gettext('Invalid derivation path'); + return; + } + opts.account = pathData.account; + opts.networkName = pathData.networkName; + opts.derivationStrategy = pathData.derivationStrategy; } else { opts.passphrase = form.createPassphrase.$modelValue; } @@ -44,12 +90,21 @@ angular.module('copayApp.controllers').controller('joinController', return; } - if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { - self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR'; - var src = form.hwLedger.$modelValue ? ledger : trezor; + if (self.seedSourceId == 'ledger' || self.seedSourceId == 'trezor') { + var account = $scope.account; + if (!account || account < 1) { + this.error = gettext('Invalid account number'); + return; + } - var account = 0; - src.getInfoForNewWallet(account, function(err, lopts) { + if ( self.seedSourceId == 'trezor') + account = account - 1; + + opts.account = account; + self.hwWallet = self.seedSourceId == 'ledger' ? 'Ledger' : 'Trezor'; + var src = self.seedSourceId == 'ledger' ? ledger : trezor; + + src.getInfoForNewWallet(true, account, function(err, lopts) { self.hwWallet = false; if (err) { self.error = err; @@ -82,4 +137,7 @@ angular.module('copayApp.controllers').controller('joinController', }); }, 100); }; + + updateSeedSourceSelect(); + self.setSeedSource('new'); }); diff --git a/src/js/controllers/preferences.js b/src/js/controllers/preferences.js index d5c29fadf..b06098502 100644 --- a/src/js/controllers/preferences.js +++ b/src/js/controllers/preferences.js @@ -55,6 +55,7 @@ angular.module('copayApp.controllers').controller('preferencesController', return; } profileService.setPrivateKeyEncryptionFC(password, function() { + $rootScope.$emit('Local/NewEncryptionSetting'); $scope.encrypt = true; }); }); @@ -66,6 +67,7 @@ angular.module('copayApp.controllers').controller('preferencesController', return; } profileService.disablePrivateKeyEncryptionFC(function(err) { + $rootScope.$emit('Local/NewEncryptionSetting'); if (err) { $scope.encrypt = true; $log.error(err); diff --git a/src/js/controllers/preferencesBwsUrl.js b/src/js/controllers/preferencesBwsUrl.js index c27786220..d7d4afc6d 100644 --- a/src/js/controllers/preferencesBwsUrl.js +++ b/src/js/controllers/preferencesBwsUrl.js @@ -13,7 +13,7 @@ angular.module('copayApp.controllers').controller('preferencesBwsUrlController', this.bwsurl = (config.bwsFor && config.bwsFor[walletId]) || defaults.bws.url; this.resetDefaultUrl = function() { - this.bwsurl = 'https://bws.bitpay.com/bws/api'; + this.bwsurl = defaults.bws.url; }; this.save = function() { @@ -50,4 +50,4 @@ angular.module('copayApp.controllers').controller('preferencesBwsUrlController', }); }); }; - }); \ No newline at end of file + }); diff --git a/src/js/controllers/preferencesInformation.js b/src/js/controllers/preferencesInformation.js index 3969f26b5..6ecbcfcab 100644 --- a/src/js/controllers/preferencesInformation.js +++ b/src/js/controllers/preferencesInformation.js @@ -7,7 +7,7 @@ angular.module('copayApp.controllers').controller('preferencesInformation', var c = fc.credentials; this.init = function() { - var basePath = profileService.getUtils().getBaseAddressDerivationPath(c.derivationStrategy, c.network, 0); + var basePath = c.getBaseAddressDerivationPath(); $scope.walletName = c.walletName; $scope.walletId = c.walletId; diff --git a/src/js/controllers/walletHome.js b/src/js/controllers/walletHome.js index 60b9e42b0..ae84e4ba4 100644 --- a/src/js/controllers/walletHome.js +++ b/src/js/controllers/walletHome.js @@ -106,36 +106,6 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi var cancel_msg = gettextCatalog.getString('Cancel'); var confirm_msg = gettextCatalog.getString('Confirm'); - $scope.openCopayersModal = function(copayers, copayerId) { - $rootScope.modalOpened = true; - var fc = profileService.focusedClient; - - var ModalInstanceCtrl = function($scope, $modalInstance) { - $scope.copayers = copayers; - $scope.copayerId = copayerId; - $scope.color = fc.backgroundColor; - $scope.cancel = function() { - $modalInstance.dismiss('cancel'); - }; - }; - var modalInstance = $modal.open({ - templateUrl: 'views/modals/copayers.html', - windowClass: animationService.modalAnimated.slideUp, - controller: ModalInstanceCtrl, - }); - - var disableCloseModal = $rootScope.$on('closeModal', function() { - modalInstance.dismiss('cancel'); - }); - - modalInstance.result.finally(function() { - $rootScope.modalOpened = false; - disableCloseModal(); - var m = angular.element(document.getElementsByClassName('reveal-modal')); - m.addClass(animationService.modalAnimated.slideOutDown); - }); - }; - $scope.openDestinationAddressModal = function(wallets, address) { $rootScope.modalOpened = true; var fc = profileService.focusedClient; diff --git a/src/js/routes.js b/src/js/routes.js index 1533ba74a..326527899 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -21,13 +21,14 @@ angular $logProvider.debugEnabled(true); $provide.decorator('$log', ['$delegate', - function($delegate) { + function($delegate, isDevel) { var historicLog = historicLogProvider.$get(); ['debug', 'info', 'warn', 'error', 'log'].forEach(function(level) { + if (isDevel && level == 'error') return; + var orig = $delegate[level]; $delegate[level] = function() { - if (level == 'error') console.log(arguments); diff --git a/src/js/services/derivationPathHelper.js b/src/js/services/derivationPathHelper.js new file mode 100644 index 000000000..04588692d --- /dev/null +++ b/src/js/services/derivationPathHelper.js @@ -0,0 +1,46 @@ +'use strict'; + +angular.module('copayApp.services').factory('derivationPathHelper', function(lodash) { + var root = {}; + + root.default = "m/44'/0'/0'" + root.parse = function(str) { + var arr = str.split('/'); + + var ret = {}; + + if (arr[0] != 'm') + return false; + + switch (arr[1]) { + case "44'": + ret.derivationStrategy = 'BIP44'; + break; + case "48'": + ret.derivationStrategy = 'BIP48'; + break; + default: + return false; + }; + + switch (arr[2]) { + case "0'": + ret.networkName = 'livenet'; + break; + case "1'": + ret.networkName = 'testnet'; + break; + default: + return false; + }; + + var match = arr[3].match(/(\d+)'/); + if (!match) + return false; + ret.account = + match[1] + + return ret; + }; + + return root; +}); diff --git a/src/js/services/hwWallet.js b/src/js/services/hwWallet.js new file mode 100644 index 000000000..55d3ccc79 --- /dev/null +++ b/src/js/services/hwWallet.js @@ -0,0 +1,50 @@ +'use strict'; + +angular.module('copayApp.services') + .factory('hwWallet', function($log, bwcService) { + var root = {}; + + // Ledger magic number to get xPub without user confirmation + root.ENTROPY_INDEX_PATH = "0xb11e/"; + root.UNISIG_ROOTPATH = 44; + root.MULTISIG_ROOTPATH = 48; + root.LIVENET_PATH = 0; + + root._err = function(data) { + var msg = 'Hardware Wallet Error: ' + (data.error || data.message || 'unknown'); + $log.warn(msg); + return msg; + }; + + + root.getRootPath = function(device, isMultisig, account) { + if (!isMultisig) return root.UNISIG_ROOTPATH; + + // Compat + if (device == 'ledger' && account ==0) return root.UNISIG_ROOTPATH; + + return root.MULTISIG_ROOTPATH; + }; + + root.getAddressPath = function(device, isMultisig, account) { + return root.getRootPath(device,isMultisig,account) + "'/" + root.LIVENET_PATH + "'/" + account + "'"; + } + + root.getEntropyPath = function(device, isMultisig, account) { + var path; + + // Old ledger wallet compat + if (device == 'ledger' && account == 0) + return root.ENTROPY_INDEX_PATH + "0'"; + + return root.ENTROPY_INDEX_PATH + root.getRootPath(device,isMultisig,account) + "'/" + account + "'"; + }; + + root.pubKeyToEntropySource = function(xPubKey) { + var b = bwcService.getBitcore(); + var x = b.HDPublicKey(xPubKey); + return x.publicKey.toString(); + }; + + return root; + }); diff --git a/src/js/services/isDevel.js b/src/js/services/isDevel.js new file mode 100644 index 000000000..f96ea53e4 --- /dev/null +++ b/src/js/services/isDevel.js @@ -0,0 +1,5 @@ +'use strict'; + +angular.module('copayApp.services').factory('isDevel', function(nodeWebkit, isChromeApp, isMobile) { + return !isMobile.any() && !isChromeApp && !nodeWebkit.isDefined(); +}); diff --git a/src/js/services/ledger.js b/src/js/services/ledger.js index e646e06dd..bb54ae0ab 100644 --- a/src/js/services/ledger.js +++ b/src/js/services/ledger.js @@ -1,43 +1,27 @@ 'use strict'; angular.module('copayApp.services') - .factory('ledger', function($log, bwcService, gettext) { + .factory('ledger', function($log, bwcService, gettext, hwWallet) { var root = {}; var LEDGER_CHROME_ID = "kkdpmhnladdopljabkgpacgpliggeeaf"; - // Ledger magic number to get xPub without user confirmation - root.ENTROPY_INDEX_PATH = "0xb11e/"; - root.callbacks = {}; - root.hasSession = function() { root._message({ command: "has_session" }); } - root.getEntropySource = function(account, callback) { - var path = root.ENTROPY_INDEX_PATH + account + "'"; - var xpub = root.getXPubKey(path, function(data) { - if (!data.success) { - $log.warn(data.message); - return callback(data); - } - - var b = bwcService.getBitcore(); - - var x = b.HDPublicKey(data.xpubkey); - data.entropySource = x.publicKey.toString(); - return callback(data); + root.getEntropySource = function(isMultisig, account, callback) { + root.getXPubKey(hwWallet.getEntropyPath('ledger', isMultisig, account), function(data) { + if (!data.success) + return callback(hwWallet._err(data)); + + return callback(null, hwWallet.pubKeyToEntropySource(data.xpubkey)); }); }; - root.getXPubKeyForAddresses = function(account, callback) { - return root.getXPubKey(root._getPath(account), callback); - }; - root.getXPubKey = function(path, callback) { - $log.debug('Ledger deriving xPub path:', path); root.callbacks["get_xpubkey"] = callback; root._messageAfterSession({ @@ -47,35 +31,36 @@ angular.module('copayApp.services') }; - root.getInfoForNewWallet = function(account, callback) { + root.getInfoForNewWallet = function(isMultisig, account, callback) { var opts = {}; - root.getEntropySource(account, function(data) { - if (!data.success) { - $log.warn(data.message); - return callback(data.message); - } - opts.entropySource = data.entropySource; - root.getXPubKeyForAddresses(account, function(data) { + root.getEntropySource(isMultisig, account, function(err, entropySource) { + if (err) return callback(err); + + opts.entropySource = entropySource; + root.getXPubKey(hwWallet.getAddressPath('ledger', isMultisig, account), function(data) { if (!data.success) { $log.warn(data.message); return callback(data); } opts.extendedPublicKey = data.xpubkey; opts.externalSource = 'ledger'; - opts.externalIndex = account; + opts.account = account; + + // Old ledger compat + opts.derivationStrategy = account ? 'BIP48' : 'BIP44'; return callback(null, opts); }); }); }; - root._signP2SH = function(txp, account, callback) { + root._signP2SH = function(txp, account, isMultisig, callback) { root.callbacks["sign_p2sh"] = callback; var redeemScripts = []; var paths = []; - var tx = bwcService.getUtils().buildTx(txp); + var tx = bwcService.buildTx(txp); for (var i = 0; i < tx.inputs.length; i++) { redeemScripts.push(new ByteString(tx.inputs[i].redeemScript.toBuffer().toString('hex'), GP.HEX).toString()); - paths.push(root._getPath(account) + txp.inputs[i].path.substring(1)); + paths.push(hwWallet.getAddressPath('ledger', isMultisig, account) + txp.inputs[i].path.substring(1)); } var splitTransaction = root._splitTransaction(new ByteString(tx.toString(), GP.HEX)); var inputs = []; @@ -98,12 +83,15 @@ angular.module('copayApp.services') }; root.signTx = function(txp, account, callback) { + + // TODO Compat + var isMultisig = true; if (txp.addressType == 'P2PKH') { var msg = 'P2PKH wallets are not supported with ledger'; $log.error(msg); return callback(msg); } else { - root._signP2SH(txp, account, callback); + root._signP2SH(txp, account, isMultisig, callback); } } @@ -154,10 +142,6 @@ angular.module('copayApp.services') } } - root._getPath = function(account) { - return "44'/0'/" + account + "'"; - } - root._splitTransaction = function(transaction) { var result = {}; var inputs = []; diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index 0170aebfe..bb55cf604 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -46,8 +46,6 @@ angular.module('copayApp.services') client.setNotificationsInterval(BACKGROUND_UPDATE_PERIOD); }); root.focusedClient.setNotificationsInterval(FOREGROUND_UPDATE_PERIOD); - - console.log('[profileService.js.49] SETTING...'); //TODO } return cb(); @@ -175,14 +173,17 @@ angular.module('copayApp.services') var walletClient = bwcService.getClient(); var network = opts.networkName || 'livenet'; + if (opts.mnemonic) { try { opts.mnemonic = root._normalizeMnemonic(opts.mnemonic); walletClient.seedFromMnemonic(opts.mnemonic, { network: network, passphrase: opts.passphrase, - account: 0, + account: opts.account || 0, + derivationStrategy: opts.derivationStrategy || 'BIP44', }); + } catch (ex) { $log.info(ex); return cb(gettext('Could not create: Invalid wallet seed')); @@ -197,7 +198,8 @@ angular.module('copayApp.services') } else if (opts.extendedPublicKey) { try { walletClient.seedFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, { - account: 0 + account: opts.account || 0, + derivationStrategy: opts.derivationStrategy || 'BIP44', }); } catch (ex) { $log.warn("Creating wallet from Extended Public Key Arg:", ex, opts); @@ -311,7 +313,7 @@ angular.module('copayApp.services') walletId: walletId }); - delete root.walletClients[walletId]; + delete root.walletClients[walletId]; root.focusedClient = null; storageService.clearLastAddress(walletId, function(err) { @@ -361,7 +363,7 @@ angular.module('copayApp.services') root.setWalletClients(); root.setAndStoreFocus(walletId, function() { - storageService.storeProfile(root.profile, function(err){ + storageService.storeProfile(root.profile, function(err) { return cb(err, walletId); }); }); @@ -420,7 +422,7 @@ angular.module('copayApp.services') walletClient.importFromMnemonic(words, { network: opts.networkName, passphrase: opts.passphrase, - account: 0, + account: opts.account || 0, }, function(err) { if (err) return bwsError.cb(err, gettext('Could not import'), cb); @@ -437,7 +439,8 @@ angular.module('copayApp.services') $log.debug('Importing Wallet XPubKey'); walletClient.importFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, { - account: 0 + account: opts.account || 0, + derivationStrategy: opts.derivationStrategy || 'BIP44', }, function(err) { if (err) { @@ -594,7 +597,7 @@ angular.module('copayApp.services') var fc = root.focusedClient; $log.info('Requesting Ledger Chrome app to sign the transaction'); - ledger.signTx(txp, 0, function(result) { + ledger.signTx(txp, fc.credentials.account, function(result) { $log.debug('Ledger response', result); if (!result.success) return cb(result.message || result.error); @@ -612,7 +615,7 @@ angular.module('copayApp.services') $log.info('Requesting Trezor to sign the transaction'); var xPubKeys = lodash.pluck(fc.credentials.publicKeyRing, 'xPubKey'); - trezor.signTx(xPubKeys, txp, 0, function(err, result) { + trezor.signTx(xPubKeys, txp, fc.credentials.account, function(err, result) { if (err) return cb(err); $log.debug('Trezor response', result); diff --git a/src/js/services/trezor.js b/src/js/services/trezor.js index c06b61f55..7c1827814 100644 --- a/src/js/services/trezor.js +++ b/src/js/services/trezor.js @@ -1,39 +1,21 @@ 'use strict'; angular.module('copayApp.services') - .factory('trezor', function($log, $timeout, bwcService, gettext, lodash, bitcore) { + .factory('trezor', function($log, $timeout, gettext, lodash, bitcore, hwWallet) { var root = {}; var SETTLE_TIME = 3000; - - root.ENTROPY_INDEX_PATH = "0xb11e/"; root.callbacks = {}; - - root._err = function(data) { - var msg = 'TREZOR Error: ' + (data.error || data.message || 'unknown'); - $log.warn(msg); - return msg; - }; - - root.getEntropySource = function(account, callback) { - var path = root.ENTROPY_INDEX_PATH + account + "'"; - var xpub = root.getXPubKey(path, function(data) { - if (!data.success) { - return callback(root._err(data)); - } - - var b = bwcService.getBitcore(); - - var x = b.HDPublicKey(data.xpubkey); - data.entropySource = x.publicKey.toString(); - return callback(null, data); + root.getEntropySource = function(isMultisig, account, callback) { + root.getXPubKey(hwWallet.getEntropyPath('trezor', isMultisig, account), function(data) { + if (!data.success) + return callback(hwWallet._err(data)); + + return callback(null, hwWallet.pubKeyToEntropySource(data.xpubkey)); }); }; - root.getXPubKeyForAddresses = function(account, callback) { - return root.getXPubKey(root._getPath(account), callback); - }; root.getXPubKey = function(path, callback) { $log.debug('TREZOR deriving xPub path:', path); @@ -41,20 +23,25 @@ angular.module('copayApp.services') }; - root.getInfoForNewWallet = function(account, callback) { + root.getInfoForNewWallet = function(isMultisig, account, callback) { var opts = {}; - root.getEntropySource(account, function(err, data) { + root.getEntropySource(isMultisig, account, function(err, data) { if (err) return callback(err); - opts.entropySource = data.entropySource; + opts.entropySource = data; $log.debug('Waiting TREZOR to settle...'); $timeout(function() { - root.getXPubKeyForAddresses(account, function(data) { - if (!data.success) - return callback(root._err(data)); + + root.getXPubKey(hwWallet.getAddressPath('trezor', isMultisig, account), function(data) { + if (!data.success) + return callback(hwWallet._err(data)); opts.extendedPublicKey = data.xpubkey; opts.externalSource = 'trezor'; - opts.externalIndex = account; + opts.account = account; + + if (isMultisig) + opts.derivationStrategy = 'BIP48'; + return callback(null, opts); }); }, SETTLE_TIME); @@ -107,10 +94,13 @@ angular.module('copayApp.services') if (txp.addressType == 'P2PKH') { + $log.debug("Trezor signing uni-sig p2pkh. Account:", account); + var inAmount = 0; inputs = lodash.map(txp.inputs, function(i) { + $log.debug("Trezor TX input path:", i.path); var pathArr = i.path.split('/'); - var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + var n = [hwWallet.UNISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; inAmount += i.satoshis; return { address_n: n, @@ -121,8 +111,9 @@ angular.module('copayApp.services') var change = inAmount - txp.fee - txp.amount; if (change > 0) { + $log.debug("Trezor TX change path:", txp.changeAddress.path); var pathArr = txp.changeAddress.path.split('/'); - var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + var n = [hwWallet.UNISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; tmpOutputs.push({ address_n: n, @@ -133,8 +124,9 @@ angular.module('copayApp.services') } else { - // P2SH Wallet + // P2SH Wallet, multisig wallet var inAmount = 0; + $log.debug("Trezor signing multi-sig p2sh. Account:", account); var sigs = xPubKeys.map(function(v) { return ''; @@ -142,8 +134,9 @@ angular.module('copayApp.services') inputs = lodash.map(txp.inputs, function(i) { + $log.debug("Trezor TX input path:", i.path); var pathArr = i.path.split('/'); - var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + var n = [hwWallet.MULTISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; var np = n.slice(3); inAmount += i.satoshis; @@ -171,8 +164,9 @@ angular.module('copayApp.services') var change = inAmount - txp.fee - txp.amount; if (change > 0) { + $log.debug("Trezor TX change path:", txp.changeAddress.path); var pathArr = txp.changeAddress.path.split('/'); - var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + var n = [hwWallet.MULTISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; var np = n.slice(3); var orderedPubKeys = root._orderPubKeys(xPubKeys, np); @@ -183,18 +177,6 @@ angular.module('copayApp.services') }; })); - // 6D - // 6C - // Addr: 3HFkHufeSaqJtqby8G9RiajaL6HdQDypRT - // - // - //(sin reverse) - // 6C - // 6D - // Addr: 3KCPRDXpmovs9nFvJHJjjsyoBDXXUZ2Frg - // "asm" : "2 03e53b2f69e1705b253029aae2591fbd0e799ed8071c8588a545b2d472dd12df88 0379797abc21d6f82c7f0aba78fd3888d8ae75ec56a10509b20feedbeac20285d9 2 OP_CHECKMULTISIG", - // - tmpOutputs.push({ address_n: n, amount: change, @@ -226,17 +208,13 @@ angular.module('copayApp.services') outputs = JSON.parse(JSON.stringify(outputs)); $log.debug('Signing with TREZOR', inputs, outputs); - TrezorConnect.signTx(inputs, outputs, function(result) { - if (!data.success) - return callback(root._err(data)); + TrezorConnect.signTx(inputs, outputs, function(res) { + if (!res.success) + return callback(hwWallet._err(res)); - callback(null, result); + callback(null, res); }); }; - root._getPath = function(account) { - return "44'/0'/" + account + "'"; - } - return root; }); diff --git a/src/js/trezor-url.js b/src/js/trezor-url.js new file mode 100644 index 000000000..7b2255280 --- /dev/null +++ b/src/js/trezor-url.js @@ -0,0 +1,2 @@ +window.TREZOR_CHROME_URL = './bower_components/trezor-connect/chrome/wrapper.html'; + diff --git a/src/js/trezor.js b/src/js/trezor.js deleted file mode 100644 index 350f90c9a..000000000 --- a/src/js/trezor.js +++ /dev/null @@ -1,372 +0,0 @@ -window.TrezorConnect = (function () { - 'use strict'; - - var CONNECT_ORIGIN = 'https://trezor.github.io'; - var CONNECT_PATH = CONNECT_ORIGIN + '/connect'; - var CONNECT_POPUP = CONNECT_PATH + '/popup/popup.html'; - - var ERR_TIMED_OUT = 'Loading timed out'; - var ERR_WINDOW_CLOSED = 'Window closed'; - var ERR_ALREADY_WAITING = 'Already waiting for a response'; - - var manager = new PopupManager( - CONNECT_POPUP, - CONNECT_ORIGIN, - 'trezor-connect', - function () { - var w = 600; - var h = 500; - var x = (screen.width - w) / 2; - var y = (screen.height - h) / 3; - var params = - 'height=' + h + - ',width=' + w + - ',left=' + x + - ',top=' + y + - ',menubar=no' + - ',toolbar=no' + - ',location=no' + - ',personalbar=no' + - ',status=no'; - return params; - } - ); - - /** - * Public API. - */ - function TrezorConnect() { - - /** - * Popup errors. - */ - this.ERR_TIMED_OUT = ERR_TIMED_OUT; - this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED; - this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING; - - /** - * @typedef XPubKeyResult - * @param {boolean} success - * @param {?string} error - * @param {?string} xpubkey serialized extended public key - * @param {?string} path BIP32 serializd path of the key - */ - - /** - * Load BIP32 extended public key by path. - * - * Path can be specified either in the string form ("m/44'/1/0") or as - * raw integer array. In case you omit the path, user is asked to select - * a BIP32 account to export, and the result contains m/44'/0'/x' node - * of the account. - * - * @param {?(string|array)} path - * @param {function(XPubKeyResult)} callback - */ - this.getXPubKey = function (path, callback) { - if (typeof path === 'string') { - path = parseHDPath(path); - } - manager.sendWithChannel({ - 'type': 'xpubkey', - 'path': path - }, function (result) { - manager.close(); - callback(result); - }); - }; - - /** - * @typedef SignTxResult - * @param {boolean} success - * @param {?string} error - * @param {?string} serialized_tx serialized tx, in hex, including signatures - * @param {?array} signatures array of input signatures, in hex - */ - - /** - * Sign a transaction in the device and return both serialized - * transaction and the signatures. - * - * @param {array} inputs - * @param {array} outputs - * @param {function(SignTxResult)} callback - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto - */ - this.signTx = function (inputs, outputs, callback) { - manager.sendWithChannel({ - 'type': 'signtx', - 'inputs': inputs, - 'outputs': outputs - }, function (result) { - manager.close(); - callback(result); - }); - }; - - /** - * @typedef RequestLoginResult - * @param {boolean} success - * @param {?string} error - * @param {?string} public_key public key used for signing, in hex - * @param {?string} signature signature, in hex - */ - - /** - * Sign a login challenge for active origin. - * - * @param {?string} hosticon - * @param {string} challenge_hidden - * @param {string} challenge_visual - * @param {string|function(RequestLoginResult)} callback - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto - */ - this.requestLogin = function ( - hosticon, - challenge_hidden, - challenge_visual, - callback - ) { - if (typeof callback === 'string') { - // special case for a login through button. - // `callback` is name of global var - callback = window[callback]; - } - if (!callback) { - throw new TypeError('TrezorConnect: login callback not found'); - } - manager.sendWithChannel({ - 'type': 'login', - 'icon': hosticon, - 'challenge_hidden': challenge_hidden, - 'challenge_visual': challenge_visual - }, function (result) { - manager.close(); - callback(result); - }); - }; - - var LOGIN_CSS = - ''; - - var LOGIN_ONCLICK = - 'TrezorConnect.requestLogin(' - + "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" - + ')'; - - var LOGIN_HTML = - '
      ' - + ' ' - + ' ' - + ' @text@' - + ' ' - + ' ' - + ' What is TREZOR?' - + ' ' - + '
      '; - - /** - * Find elements and replace them with login buttons. - * It's not required to use these special elements, feel free to call - * `TrezorConnect.requestLogin` directly. - */ - this.renderLoginButtons = function () { - var elements = document.getElementsByTagName('trezor:login'); - - for (var i = 0; i < elements.length; i++) { - var e = elements[i]; - var text = e.getAttribute('text') || 'Sign in with TREZOR'; - var callback = e.getAttribute('callback') || ''; - var hosticon = e.getAttribute('icon') || ''; - var challenge_hidden = e.getAttribute('challenge_hidden') || ''; - var challenge_visual = e.getAttribute('challenge_visual') || ''; - - // it's not valid to put markup into attributes, so let users - // supply a raw text and make TREZOR bold - text = text.replace('TREZOR', 'TREZOR'); - - e.parentNode.innerHTML = - LOGIN_CSS + LOGIN_HTML - .replace('@text@', text) - .replace('@callback@', callback) - .replace('@hosticon@', hosticon) - .replace('@challenge_hidden@', challenge_hidden) - .replace('@challenge_visual@', challenge_visual); - } - }; - } - - var exports = new TrezorConnect(); - exports.renderLoginButtons(); - return exports; - - /* - * `getXPubKey()` - */ - - function parseHDPath(string) { - return string - .toLowerCase() - .split('/') - .filter(function (p) { return p !== 'm'; }) - .map(function (p) { - var n = parseInt(p); - if (p[p.length - 1] === "'") { // hardened index - n = n | 0x80000000; - } - return n; - }); - } - - /* - * Popup management - */ - - function Popup(url, name, params) { - var w = window.open(url, name, params); - - var interval; - var iterate = function () { - if (w.closed) { - clearInterval(interval); - if (this.onclose) { - this.onclose(); - } - } - }.bind(this); - interval = setInterval(iterate, 100); - - this.window = w; - this.onclose = null; - } - - function Channel(target, origin, waiting) { - - var respond = function (data) { - if (waiting) { - var callback = waiting; - waiting = null; - callback(data); - } - }; - - var receive = function (event) { - if (event.source === target && event.origin === origin) { - respond(event.data); - } - }; - - window.addEventListener('message', receive); - - this.respond = respond; - - this.close = function () { - window.removeEventListener('message', receive); - }; - - this.send = function (value, callback) { -console.log('[trezor.js.270:value:]',value); //TODO - if (waiting === null) { - waiting = callback; - target.postMessage(value, origin); - } else { - throw new Error(ERR_ALREADY_WAITING); - } - }; - } - - function ConnectedChannel(url, origin, name, params) { - - var ready = function () { - clearTimeout(this.timeout); - this.popup.onclose = null; - this.ready = true; - this.onready(); - }.bind(this); - - var closed = function () { - clearTimeout(this.timeout); - this.channel.close(); - this.onerror(new Error(ERR_WINDOW_CLOSED)); - }.bind(this); - - var timedout = function () { - this.popup.onclose = null; - this.popup.window.close(); - this.channel.close(); - this.onerror(new Error(ERR_TIMED_OUT)); - }.bind(this); - - this.popup = new Popup(url, name, params); - this.channel = new Channel(this.popup.window, origin, ready); - this.timeout = setTimeout(timedout, 5000); - - this.popup.onclose = closed; - - this.ready = false; - this.onready = null; - this.onerror = null; - } - - function PopupManager(url, origin, name, onparams) { - var cc = null; - - var closed = function () { - cc.channel.respond(new Error(ERR_WINDOW_CLOSED)); - cc.channel.close(); - cc = null; - }; - - var open = function (callback) { - cc = new ConnectedChannel(url, origin, name, onparams()); - cc.onready = function () { - cc.popup.onclose = closed; - callback(cc.channel); - }; - cc.onerror = function (error) { - cc = null; - callback(error); - }; - }; - - this.close = function () { - if (cc) { - cc.popup.window.close(); - } - }; - - this.waitForChannel = function (callback) { - if (cc) { - if (cc.ready) { - callback(cc.channel); - } else { - callback(new Error(ERR_ALREADY_WAITING)); - } - } else { - open(callback); - } - }; - - this.sendWithChannel = function (message, callback) { - var onresponse = function (response) { - if (response instanceof Error) { - callback({success: false, error: response.message}); - } else { - callback(response); - } - }; - var onchannel = function (channel) { - if (channel instanceof Error) { - callback({success: false, error: channel.message}); - } else { - channel.send(message, onresponse); - } - } - this.waitForChannel(onchannel); - }; - } - -}());