From 9ce342bba706784cbfa9dd846d28134952382353 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 26 Sep 2015 08:26:31 -0300 Subject: [PATCH 1/6] WIP trezor support, wallet creation working --- Gruntfile.js | 6 +- public/views/create.html | 24 +- src/js/controllers/create.js | 14 +- src/js/controllers/join.js | 4 +- src/js/controllers/walletHome.js | 1 + src/js/services/profileService.js | 35 ++- src/js/services/trezor.js | 114 +++++++++ src/js/trezor.js | 368 ++++++++++++++++++++++++++++++ 8 files changed, 542 insertions(+), 24 deletions(-) create mode 100644 src/js/services/trezor.js create mode 100644 src/js/trezor.js diff --git a/Gruntfile.js b/Gruntfile.js index 98e6ef651..8aead0647 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..c11944d16 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/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 b747b5f85..a7c5492d1 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, trezor) { + 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'); 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 c3f852371..45e0c6cb2 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 / network - ledger.getInfoForNewWallet(0, opts.networkName, function(err, lopts) { - self.ledger = false; + if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { + self.hwWallet = form.hwLedger.$modelValue ? 'Leger' : 'TREZOR'; + var src= form.hwLedger.$modelValue ? leger : 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/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 dbb677d84..907483da2 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -268,6 +268,7 @@ console.log('[profileService.js.239:walletClient:]',walletClient); //TODO 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; @@ -568,7 +569,11 @@ console.log('[profileService.js.239:walletClient:]',walletClient); //TODO var fc = root.focusedClient; $log.info('Requesting Trezor to sign the transaction'); - trezor.signTx(txp, 0, function(result) { +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); diff --git a/src/js/services/trezor.js b/src/js/services/trezor.js index 385428042..54b9df534 100644 --- a/src/js/services/trezor.js +++ b/src/js/services/trezor.js @@ -58,8 +58,8 @@ angular.module('copayApp.services') }; - root.signTx = function(txp, account, callback) { - console.log('[trezor.js.66:txp:]', txp); //TODO + root.signTx = function(xPubKeys, txp, account, callback) { + console.log('[trezor.js.66:txp:]', xPubKeys, txp); //TODO var inputs = [], outputs = []; @@ -109,7 +109,65 @@ angular.module('copayApp.services') } } else { - $log.error('TODO: multisig'); + + // 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 = [parseInt(pathArr[1]), parseInt(pathArr[2])]; + inAmount += i.satoshis; + + var pubkeys = lodash(xPubKeys.map(function(v) { + return { + node: v, + address_n: n, + }; + })).reverse().value(); +console.log('[trezor.js.121:pubkeys:]',pubkeys); //TODO + + 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 pubkeys = lodash(xPubKeys.map(function(v) { + return { + node: v, + address_n: n, + }; + })).reverse().value(); + + tmpOutputs.push({ + address_n: n, + amount: change, + script_type: 'PAYTOMULTISIG', + multisig: { + pubkeys: pubkeys, + signatures: sigs, + m: txp.requiredSignatures, + } + }); + } } // Shuffle outputs for improved privacy @@ -118,7 +176,7 @@ angular.module('copayApp.services') outputs[order] = tmpOutputs.shift(); }); - if (tmpOutputs.length) + if (tmpOutputs.length) return cb("Error creating transaction: tmpOutput order"); } else { outputs = tmpOutputs; From fb6e72e9112d8580ccb431f881aa4bd51f3904b0 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 2 Oct 2015 12:01:57 -0300 Subject: [PATCH 5/6] trezor multisig WIP --- src/js/controllers/create.js | 4 ++-- src/js/controllers/join.js | 4 ++-- src/js/services/trezor.js | 38 ++++++++++++++++++++++++++---------- src/js/trezor.js | 8 ++++++-- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/js/controllers/create.js b/src/js/controllers/create.js index a7c5492d1..84493520f 100644 --- a/src/js/controllers/create.js +++ b/src/js/controllers/create.js @@ -73,9 +73,9 @@ angular.module('copayApp.controllers').controller('createController', } if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { - self.hwWallet = form.hwLedger.$modelValue ? 'Leger' : 'TREZOR'; + self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR'; - var src= form.hwLedger.$modelValue ? leger : trezor; + var src= form.hwLedger.$modelValue ? ledger : trezor; // TODO : account var account = 0; diff --git a/src/js/controllers/join.js b/src/js/controllers/join.js index 45e0c6cb2..8910fc617 100644 --- a/src/js/controllers/join.js +++ b/src/js/controllers/join.js @@ -42,8 +42,8 @@ angular.module('copayApp.controllers').controller('joinController', } if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) { - self.hwWallet = form.hwLedger.$modelValue ? 'Leger' : 'TREZOR'; - var src= form.hwLedger.$modelValue ? leger : trezor; + self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR'; + var src= form.hwLedger.$modelValue ? ledger : trezor; var account = 0; src.getInfoForNewWallet(account, function(err, lopts) { diff --git a/src/js/services/trezor.js b/src/js/services/trezor.js index 54b9df534..4b84748bc 100644 --- a/src/js/services/trezor.js +++ b/src/js/services/trezor.js @@ -4,6 +4,8 @@ angular.module('copayApp.services') .factory('trezor', function($log, $timeout, bwcService, gettext, lodash) { var root = {}; + var SETTLE_TIME = 3000; + root.ENTROPY_INDEX_PATH = "0xb11e/"; root.callbacks = {}; @@ -53,11 +55,10 @@ angular.module('copayApp.services') opts.externalIndex = account; return callback(null, opts); }); - }, 5000); + }, SETTLE_TIME); }); }; - root.signTx = function(xPubKeys, txp, account, callback) { console.log('[trezor.js.66:txp:]', xPubKeys, txp); //TODO @@ -121,16 +122,16 @@ angular.module('copayApp.services') 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 = [parseInt(pathArr[1]), parseInt(pathArr[2])]; + var np = n.slice(3); + inAmount += i.satoshis; - var pubkeys = lodash(xPubKeys.map(function(v) { + var pubkeys = xPubKeys.map(function(v) { return { node: v, - address_n: n, + address_n: np, }; - })).reverse().value(); -console.log('[trezor.js.121:pubkeys:]',pubkeys); //TODO + }); return { address_n: n, @@ -149,13 +150,26 @@ console.log('[trezor.js.121:pubkeys:]',pubkeys); //TODO 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 pubkeys = lodash(xPubKeys.map(function(v) { + var pubkeys = xPubKeys.map(function(v) { return { node: v, - address_n: n, + address_n: np, }; - })).reverse().value(); + }); + + // 6D + // 6C + // Addr: 3HFkHufeSaqJtqby8G9RiajaL6HdQDypRT + // + // + //(sin reverse) + // 6C + // 6D + // Addr: 3KCPRDXpmovs9nFvJHJjjsyoBDXXUZ2Frg + // "asm" : "2 03e53b2f69e1705b253029aae2591fbd0e799ed8071c8588a545b2d472dd12df88 0379797abc21d6f82c7f0aba78fd3888d8ae75ec56a10509b20feedbeac20285d9 2 OP_CHECKMULTISIG", + // tmpOutputs.push({ address_n: n, @@ -172,6 +186,7 @@ console.log('[trezor.js.121:pubkeys:]',pubkeys); //TODO // Shuffle outputs for improved privacy if (tmpOutputs.length > 1) { + outputs = new Array(tmpOutputs.length); lodash.each(txp.outputOrder, function(order) { outputs[order] = tmpOutputs.shift(); }); @@ -182,6 +197,9 @@ console.log('[trezor.js.121:pubkeys:]',pubkeys); //TODO 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) { diff --git a/src/js/trezor.js b/src/js/trezor.js index a509c0eba..350f90c9a 100644 --- a/src/js/trezor.js +++ b/src/js/trezor.js @@ -49,15 +49,18 @@ window.TrezorConnect = (function () { * @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. + * 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 {?(string|array)} path * @param {function(XPubKeyResult)} callback */ this.getXPubKey = function (path, callback) { @@ -265,6 +268,7 @@ window.TrezorConnect = (function () { }; this.send = function (value, callback) { +console.log('[trezor.js.270:value:]',value); //TODO if (waiting === null) { waiting = callback; target.postMessage(value, origin); From 2a1fcf1e5a7c3d4aeb0ad2c341b0c13fe70aae00 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 2 Oct 2015 17:18:54 -0300 Subject: [PATCH 6/6] fix pubkey order --- src/js/services/trezor.js | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/js/services/trezor.js b/src/js/services/trezor.js index 4b84748bc..d9d7e15de 100644 --- a/src/js/services/trezor.js +++ b/src/js/services/trezor.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.services') - .factory('trezor', function($log, $timeout, bwcService, gettext, lodash) { + .factory('trezor', function($log, $timeout, bwcService, gettext, lodash, bitcore) { var root = {}; var SETTLE_TIME = 3000; @@ -59,8 +59,28 @@ angular.module('copayApp.services') }); }; + 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) { - console.log('[trezor.js.66:txp:]', xPubKeys, txp); //TODO var inputs = [], outputs = []; @@ -126,12 +146,13 @@ angular.module('copayApp.services') inAmount += i.satoshis; - var pubkeys = xPubKeys.map(function(v) { + var orderedPubKeys = root._orderPubKeys(xPubKeys, np); + var pubkeys = lodash(orderedPubKeys.map(function(v) { return { node: v, address_n: np, }; - }); + })); return { address_n: n, @@ -152,12 +173,13 @@ angular.module('copayApp.services') var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])]; var np = n.slice(3); - var pubkeys = xPubKeys.map(function(v) { + var orderedPubKeys = root._orderPubKeys(xPubKeys, np); + var pubkeys = lodash(orderedPubKeys.map(function(v) { return { node: v, address_n: np, }; - }); + })); // 6D // 6C