diff --git a/Gruntfile.js b/Gruntfile.js index 4b4533a27..ab3434ee7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,7 +37,8 @@ module.exports = function(grunt) { 'src/js/routes.js', 'src/js/services/*.js', 'src/js/models/*.js', - 'src/js/controllers/*.js' + 'src/js/controllers/*.js', + 'src/js/trezor.js' ], tasks: ['concat:js'] } @@ -77,7 +78,8 @@ module.exports = function(grunt) { 'src/js/controllers/*.js', 'src/js/translations.js', 'src/js/version.js', - 'src/js/init.js' + 'src/js/init.js', + 'src/js/trezor.js' ], dest: 'public/js/copay.js' }, diff --git a/public/views/create.html b/public/views/create.html index f6d7ff43f..fb5ef4b47 100644 --- a/public/views/create.html +++ b/public/views/create.html @@ -8,7 +8,7 @@
-
+
@@ -21,7 +21,7 @@
-
+
@@ -30,7 +30,7 @@
- Connecting to Ledger Wallet... + Connecting to {{create.hwWallet}} Wallet...
@@ -102,10 +102,16 @@
-
diff --git a/public/views/import.html b/public/views/import.html index 46afd5f61..ced2981aa 100644 --- a/public/views/import.html +++ b/public/views/import.html @@ -5,7 +5,7 @@
-
+
@@ -17,7 +17,7 @@ Importing wallet...
-
+
@@ -26,26 +26,14 @@
- Connecting to Ledger Wallet... + Connecting to {{import.hwWallet}} Wallet...
- -
+
Ledger + ng-class="{'selected': type=='hwWallet'}" + ng-click="import.setType('hwWallet')" translate>Hardware Wallet
-
- -
- -
@@ -166,14 +149,15 @@
-
+
- -
- - {{import.error|translate}} - -
+
+ + {{import.error|translate}} + +
+ +
+
+ + +
+
+ +
diff --git a/public/views/join.html b/public/views/join.html index ec9be8f5c..cc3999a04 100644 --- a/public/views/join.html +++ b/public/views/join.html @@ -6,7 +6,7 @@
-
+
@@ -19,7 +19,7 @@
-
+
@@ -28,7 +28,7 @@
- Connecting to Ledger Wallet... + Connecting to {{join.hwWallet}} Wallet...
@@ -76,12 +76,17 @@
-
- -
+
+ + + + -
- -
diff --git a/src/js/controllers/create.js b/src/js/controllers/create.js index 60181be9f..84493520f 100644 --- a/src/js/controllers/create.js +++ b/src/js/controllers/create.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('createController', - function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isMobile, isCordova, gettext, isChromeApp, ledger) { + function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isCordova, gettext, ledger, trezor, isMobile) { var self = this; var defaults = configService.getDefaults(); @@ -42,10 +42,6 @@ angular.module('copayApp.controllers').controller('createController', updateRCSelect(tc); }; - this.isChromeApp = function() { - return isChromeApp; - }; - this.create = function(form) { if (form && form.$invalid) { this.error = gettext('Please enter the required fields'); @@ -76,11 +72,15 @@ angular.module('copayApp.controllers').controller('createController', return; } - if (form.hwLedger.$modelValue) { - self.ledger = true; + if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { + self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR'; + + var src= form.hwLedger.$modelValue ? ledger : trezor; + // TODO : account - ledger.getInfoForNewWallet(0, function(err, lopts) { - self.ledger = false; + var account = 0; + src.getInfoForNewWallet(account, function(err, lopts) { + self.hwWallet = false; if (err) { self.error = err; $scope.$apply(); diff --git a/src/js/controllers/import.js b/src/js/controllers/import.js index 1965f5ce3..31242e8d4 100644 --- a/src/js/controllers/import.js +++ b/src/js/controllers/import.js @@ -1,12 +1,11 @@ 'use strict'; angular.module('copayApp.controllers').controller('importController', - function($scope, $rootScope, $location, $timeout, $log, profileService, notification, go, isMobile, isCordova, sjcl, gettext, lodash, ledger) { + function($scope, $rootScope, $location, $timeout, $log, profileService, notification, go, isMobile, sjcl, gettext, lodash, ledger, trezor) { var self = this; this.isSafari = isMobile.Safari(); - this.isCordova = isCordova; var reader = new FileReader(); window.ignoreMobilePause = true; @@ -182,6 +181,44 @@ angular.module('copayApp.controllers').controller('importController', _importMnemonic(words, opts); }; + this.importTrezor = function(form) { + 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) { + self.hwWallet = false; + if (err) { + self.error = err; + $scope.$apply(); + return; + } + lopts.externalSource = 'trezor'; + self.loading = true; + $log.debug('Import opts', lopts); + profileService.importExtendedPublicKey(lopts, function(err, walletId) { + self.loading = false; + if (err) { + self.error = err; + return $timeout(function() { + $scope.$apply(); + }); + } + $rootScope.$emit('Local/WalletImported', walletId); + notification.success(gettext('Success'), gettext('Your wallet has been imported correctly')); + go.walletHome(); + }); + }, 100); + }; + + + this.importLedger = function(form) { var self = this; if (form.$invalid) { @@ -191,16 +228,15 @@ angular.module('copayApp.controllers').controller('importController', }); return; } - self.ledger = true; + self.hwWallet = 'Ledger'; // TODO account ledger.getInfoForNewWallet(0, function(err, lopts) { - self.ledger = false; + self.hwWallet = false; if (err) { self.error = err; $scope.$apply(); return; } - lopts.externalIndex = $scope.externalIndex; lopts.externalSource = 'ledger'; self.loading = true; $log.debug('Import opts', lopts); diff --git a/src/js/controllers/join.js b/src/js/controllers/join.js index 6d24589be..8910fc617 100644 --- a/src/js/controllers/join.js +++ b/src/js/controllers/join.js @@ -1,14 +1,10 @@ 'use strict'; angular.module('copayApp.controllers').controller('joinController', - function($scope, $rootScope, $timeout, go, isMobile, notification, profileService, isCordova, isChromeApp, $modal, gettext, lodash, ledger) { + function($scope, $rootScope, $timeout, go, notification, profileService, isCordova, $modal, gettext, lodash, ledger, trezor) { var self = this; - this.isChromeApp = function() { - return isChromeApp; - }; - this.onQrCodeScanned = function(data) { $scope.secret = data; $scope.joinForm.secret.$setViewValue(data); @@ -45,11 +41,13 @@ angular.module('copayApp.controllers').controller('joinController', return; } - if (form.hwLedger.$modelValue) { - self.ledger = true; - // TODO account - ledger.getInfoForNewWallet(0, function(err, lopts) { - self.ledger = false; + if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { + self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR'; + var src= form.hwLedger.$modelValue ? ledger : trezor; + + var account = 0; + src.getInfoForNewWallet(account, function(err, lopts) { + self.hwWallet = false; if (err) { self.error = err; $scope.$apply(); diff --git a/src/js/controllers/walletHome.js b/src/js/controllers/walletHome.js index 036a986c6..3e7d253ac 100644 --- a/src/js/controllers/walletHome.js +++ b/src/js/controllers/walletHome.js @@ -1220,4 +1220,5 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi this.setAddress(); this.setSendFormInputs(); } + }); diff --git a/src/js/services/bwsError.js b/src/js/services/bwsError.js index a4fde659d..896914145 100644 --- a/src/js/services/bwsError.js +++ b/src/js/services/bwsError.js @@ -25,7 +25,7 @@ angular.module('copayApp.services') body = gettextCatalog.getString('Copayer already in this wallet'); break; case 'COPAYER_REGISTERED': - body = gettextCatalog.getString('Wallet already registered'); + body = gettextCatalog.getString('Key already associated with an existing wallet'); break; case 'COPAYER_VOTED': body = gettextCatalog.getString('Copayer already voted on this spend proposal'); diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index 99e88db91..907483da2 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -1,6 +1,6 @@ 'use strict'; angular.module('copayApp.services') - .factory('profileService', function profileServiceFactory($rootScope, $location, $timeout, $filter, $log, lodash, storageService, bwcService, configService, notificationService, isChromeApp, isCordova, gettext, gettextCatalog, nodeWebkit, bwsError, uxLanguage, ledger, bitcore) { + .factory('profileService', function profileServiceFactory($rootScope, $location, $timeout, $filter, $log, lodash, storageService, bwcService, configService, notificationService, isChromeApp, isCordova, gettext, gettextCatalog, nodeWebkit, bwsError, uxLanguage, ledger, bitcore, trezor) { var root = {}; @@ -236,6 +236,7 @@ angular.module('copayApp.services') root._seedWallet(opts, function(err, walletClient) { if (err) return cb(err); +console.log('[profileService.js.239:walletClient:]',walletClient); //TODO walletClient.createWallet(opts.name, opts.myName || 'me', opts.m, opts.n, { network: opts.networkName }, function(err, secret) { @@ -267,6 +268,7 @@ angular.module('copayApp.services') return cb(gettext('Cannot join the same wallet more that once')); } } catch (ex) { + $log.debug(ex); return cb(gettext('Bad wallet invitation')); } opts.networkName = walletData.network; @@ -275,7 +277,7 @@ angular.module('copayApp.services') root._seedWallet(opts, function(err, walletClient) { if (err) return cb(err); - walletClient.joinWallet(opts.secret, opts.myName || 'me', function(err) { + walletClient.joinWallet(opts.secret, opts.myName || 'me', {}, function(err) { if (err) return bwsError.cb(err, gettext('Could not join wallet'), cb); root.profile.credentials.push(JSON.parse(walletClient.export())); @@ -551,8 +553,9 @@ angular.module('copayApp.services') $log.info('Requesting Ledger Chrome app to sign the transaction'); ledger.signTx(txp, 0, function(result) { + $log.debug('Ledger response',result); if (!result.success) - return cb(result); + return cb(result.message || result.error); txp.signatures = lodash.map(result.signatures, function(s) { return s.substring(0, s.length - 2); @@ -561,16 +564,40 @@ angular.module('copayApp.services') }); }; + + root._signWithTrezor = function(txp, cb) { + var fc = root.focusedClient; + $log.info('Requesting Trezor to sign the transaction'); + +console.log('[profileService.js.570] xPub:', fc.credentials.xPubKey); //TODO + var xPubKeys = lodash.pluck(fc.credentials.publicKeyRing,'xPubKey'); +console.log('[profileService.js.571:xPubKeys:]',xPubKeys); //TODO + + trezor.signTx(xPubKeys, txp, 0, function(result) { + $log.debug('Trezor response',result); + if (!result.success) + return cb(result.error || result); + + txp.signatures = result.signatures; + return fc.signTxProposal(txp, cb); + }); + }; + + root.signTxProposal = function(txp, cb) { var fc = root.focusedClient; if (fc.isPrivKeyExternal()) { - if (fc.getPrivKeyExternalSourceName() != 'ledger') { - var msg = 'Unsupported External Key:' + fc.getPrivKeyExternalSourceName(); - $log.error(msg); - return cb(msg); + switch (fc.getPrivKeyExternalSourceName()) { + case 'ledger': + return root._signWithLedger(txp, cb); + case 'trezor': + return root._signWithTrezor(txp, cb); + default: + var msg = 'Unsupported External Key:' + fc.getPrivKeyExternalSourceName(); + $log.error(msg); + return cb(msg); } - return root._signWithLedger(txp, cb); } else { return fc.signTxProposal(txp, function(err, signedTxp) { root.lockFC(); diff --git a/src/js/services/trezor.js b/src/js/services/trezor.js new file mode 100644 index 000000000..d9d7e15de --- /dev/null +++ b/src/js/services/trezor.js @@ -0,0 +1,237 @@ +'use strict'; + +angular.module('copayApp.services') + .factory('trezor', function($log, $timeout, bwcService, gettext, lodash, bitcore) { + var root = {}; + + var SETTLE_TIME = 3000; + + root.ENTROPY_INDEX_PATH = "0xb11e/"; + root.callbacks = {}; + + 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.getXPubKeyForAddresses = function(account, callback) { + return root.getXPubKey(root._getPath(account), callback); + }; + + root.getXPubKey = function(path, callback) { + $log.debug('TREZOR deriving xPub path:', path); + TrezorConnect.getXPubKey(path, callback); + }; + + + root.getInfoForNewWallet = function(account, callback) { + var opts = {}; + root.getEntropySource(account, function(data) { + if (!data.success) { + $log.warn(data.message); + return callback(data.message); + } + opts.entropySource = data.entropySource; + $log.debug('Waiting TREZOR to settle...'); + $timeout(function() { + root.getXPubKeyForAddresses(account, function(data) { + if (!data.success) { + $log.warn(data.message); + return callback(data); + } + opts.extendedPublicKey = data.xpubkey; + opts.externalSource = 'trezor'; + opts.externalIndex = account; + return callback(null, opts); + }); + }, SETTLE_TIME); + }); + }; + + root._orderPubKeys = function(xPub, np) { + var xPubKeys = lodash.clone(xPub); + var path = lodash.clone(np); + path.unshift('m'); + path = path.join('/'); + + var keys = lodash.map(xPubKeys, function(x) { + var pub = (new bitcore.HDPublicKey(x)).derive(path).publicKey; + return { + xpub: x, + pub: pub.toString('hex'), + }; + }); + + var sorted = lodash.sortBy(keys, function(x) { + return x.pub; + }); + + return lodash.pluck(sorted, 'xpub'); + }; + + root.signTx = function(xPubKeys, txp, account, callback) { + + var inputs = [], + outputs = []; + var tmpOutputs = []; + + if (txp.type != 'simple') + return callback('Only TXPs type SIMPLE are supported in TREZOR'); + + var toScriptType = 'PAYTOADDRESS'; + if (txp.toAddress.charAt(0) == '2' || txp.toAddress.charAt(0) == '3') + toScriptType = 'PAYTOSCRIPTHASH'; + + + // Add to + tmpOutputs.push({ + address: txp.toAddress, + amount: txp.amount, + script_type: toScriptType, + }); + + + + if (txp.addressType == 'P2PKH') { + + var inAmount = 0; + inputs = lodash.map(txp.inputs, function(i) { + var pathArr = i.path.split('/'); + var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + inAmount += i.satoshis; + return { + address_n: n, + prev_index: i.vout, + prev_hash: i.txid, + }; + }); + + var change = inAmount - txp.fee - txp.amount; + if (change > 0) { + var pathArr = txp.changeAddress.path.split('/'); + var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + + tmpOutputs.push({ + address_n: n, + amount: change, + script_type: 'PAYTOADDRESS' + }); + } + + } else { + + // P2SH Wallet + var inAmount = 0; + + var sigs = xPubKeys.map(function(v) { + return ''; + }); + + + inputs = lodash.map(txp.inputs, function(i) { + var pathArr = i.path.split('/'); + var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + var np = n.slice(3); + + inAmount += i.satoshis; + + var orderedPubKeys = root._orderPubKeys(xPubKeys, np); + var pubkeys = lodash(orderedPubKeys.map(function(v) { + return { + node: v, + address_n: np, + }; + })); + + return { + address_n: n, + prev_index: i.vout, + prev_hash: i.txid, + script_type: 'SPENDMULTISIG', + multisig: { + pubkeys: pubkeys, + signatures: sigs, + m: txp.requiredSignatures, + } + }; + }); + + var change = inAmount - txp.fee - txp.amount; + if (change > 0) { + var pathArr = txp.changeAddress.path.split('/'); + var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; + var np = n.slice(3); + + var orderedPubKeys = root._orderPubKeys(xPubKeys, np); + var pubkeys = lodash(orderedPubKeys.map(function(v) { + return { + node: v, + address_n: np, + }; + })); + + // 6D + // 6C + // Addr: 3HFkHufeSaqJtqby8G9RiajaL6HdQDypRT + // + // + //(sin reverse) + // 6C + // 6D + // Addr: 3KCPRDXpmovs9nFvJHJjjsyoBDXXUZ2Frg + // "asm" : "2 03e53b2f69e1705b253029aae2591fbd0e799ed8071c8588a545b2d472dd12df88 0379797abc21d6f82c7f0aba78fd3888d8ae75ec56a10509b20feedbeac20285d9 2 OP_CHECKMULTISIG", + // + + tmpOutputs.push({ + address_n: n, + amount: change, + script_type: 'PAYTOMULTISIG', + multisig: { + pubkeys: pubkeys, + signatures: sigs, + m: txp.requiredSignatures, + } + }); + } + } + + // Shuffle outputs for improved privacy + if (tmpOutputs.length > 1) { + outputs = new Array(tmpOutputs.length); + lodash.each(txp.outputOrder, function(order) { + outputs[order] = tmpOutputs.shift(); + }); + + if (tmpOutputs.length) + return cb("Error creating transaction: tmpOutput order"); + } else { + outputs = tmpOutputs; + } + + // Prevents: Uncaught DataCloneError: Failed to execute 'postMessage' on 'Window': An object could not be cloned. + inputs = JSON.parse(JSON.stringify(inputs)); + outputs = JSON.parse(JSON.stringify(outputs)); + + $log.debug('Signing with TREZOR', inputs, outputs); + TrezorConnect.signTx(inputs, outputs, function(result) { + callback(result); + }); + }; + + root._getPath = function(account) { + return "44'/0'/" + account + "'"; + } + + return root; + }); diff --git a/src/js/trezor.js b/src/js/trezor.js new file mode 100644 index 000000000..350f90c9a --- /dev/null +++ b/src/js/trezor.js @@ -0,0 +1,372 @@ +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); + }; + } + +}());