From 8c4d58aa4508e3d54c3f69847347e78d09c63b97 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 03:52:32 -0400 Subject: initial trezor support --- app/_locales/en/messages.json | 9 + app/images/connect-icon.svg | 11 + app/scripts/lib/trezor-connect.js | 1138 ++++++++++++++++++++++++++++++++++++ app/scripts/lib/trezorKeyring.js | 255 ++++++++ app/scripts/metamask-controller.js | 63 +- 5 files changed, 1475 insertions(+), 1 deletion(-) create mode 100644 app/images/connect-icon.svg create mode 100644 app/scripts/lib/trezor-connect.js create mode 100644 app/scripts/lib/trezorKeyring.js (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 457c3c3b1..339dd8da2 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -119,6 +119,12 @@ "confirmTransaction": { "message": "Confirm Transaction" }, + "connectHardware": { + "message": "Connect Hardware" + }, + "connect": { + "message": "Connect" + }, "continue": { "message": "Continue" }, @@ -930,6 +936,9 @@ "unknownNetworkId": { "message": "Unknown network ID" }, + "unlock": { + "message": "Unlock" + }, "unlockMessage": { "message": "The decentralized web awaits" }, diff --git a/app/images/connect-icon.svg b/app/images/connect-icon.svg new file mode 100644 index 000000000..84540999a --- /dev/null +++ b/app/images/connect-icon.svg @@ -0,0 +1,11 @@ + + + + background + + + + Layer 1 + + + \ No newline at end of file diff --git a/app/scripts/lib/trezor-connect.js b/app/scripts/lib/trezor-connect.js new file mode 100644 index 000000000..574e88104 --- /dev/null +++ b/app/scripts/lib/trezor-connect.js @@ -0,0 +1,1138 @@ +/* eslint-disable */ +/* prettier-ignore */ + +/** + * (C) 2017 SatoshiLabs + * + * GPLv3 + */ +var TREZOR_CONNECT_VERSION = 4; + +if (!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === "[object Array]"; + }; +} + +var HD_HARDENED = 0x80000000; + +// react sometimes adds some other parameters that should not be there +function _fwStrFix(obj, fw) { + if (typeof fw === "string") { + obj.requiredFirmware = fw; + } + return obj; +} + +("use strict"); + +var chrome = window.chrome; +var IS_CHROME_APP = chrome && chrome.app && chrome.app.window; + +var ERR_TIMED_OUT = "Loading timed out"; +var ERR_WINDOW_CLOSED = "Window closed"; +var ERR_WINDOW_BLOCKED = "Window blocked"; +var ERR_ALREADY_WAITING = "Already waiting for a response"; +var ERR_CHROME_NOT_CONNECTED = "Internal Chrome popup is not responding."; + +var DISABLE_LOGIN_BUTTONS = window.TREZOR_DISABLE_LOGIN_BUTTONS || false; +var CHROME_URL = window.TREZOR_CHROME_URL || "./chrome/wrapper.html"; +var POPUP_ORIGIN = window.TREZOR_POPUP_ORIGIN || "https://connect.trezor.io"; +var POPUP_PATH = + window.TREZOR_POPUP_PATH || POPUP_ORIGIN + "/" + TREZOR_CONNECT_VERSION; +var POPUP_URL = + window.TREZOR_POPUP_URL || + POPUP_PATH + "/popup/popup.html?v=" + new Date().getTime(); + +var POPUP_INIT_TIMEOUT = 15000; + +/** + * Public API. + */ +function TrezorConnect() { + var manager = new PopupManager(); + + /** + * Popup errors. + */ + this.ERR_TIMED_OUT = ERR_TIMED_OUT; + this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED; + this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED; + this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING; + this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED; + + /** + * Open the popup for further communication. All API functions open the + * popup automatically, but if you need to generate some parameters + * asynchronously, use `open` first to avoid popup blockers. + * @param {function(?Error)} callback + */ + this.open = function(callback) { + var onchannel = function(result) { + if (result instanceof Error) { + callback(result); + } else { + callback(); + } + }; + manager.waitForChannel(onchannel); + }; + + /** + * Close the opened popup, if any. + */ + this.close = function() { + manager.close(); + }; + + /** + * Enable or disable closing the opened popup after a successful call. + * @param {boolean} value + */ + this.closeAfterSuccess = function(value) { + manager.closeAfterSuccess = value; + }; + + /** + * Enable or disable closing the opened popup after a failed call. + * @param {boolean} value + */ + this.closeAfterFailure = function(value) { + manager.closeAfterFailure = value; + }; + + /** + * Set bitcore server + * @param {string|Array} value + */ + this.setBitcoreURLS = function(value) { + if (typeof value === "string") { + manager.bitcoreURLS = [value]; + } else if (value instanceof Array) { + manager.bitcoreURLS = value; + } + }; + + /** + * Set currency. Human readable coin name + * @param {string|Array} value + */ + this.setCurrency = function(value) { + if (typeof value === "string") { + manager.currency = value; + } + }; + + /** + * Set currency units (mBTC, BTC) + * @param {string|Array} value + */ + this.setCurrencyUnits = function(value) { + if (typeof value === "string") { + manager.currencyUnits = value; + } + }; + + /** + * Set coin info json url + * @param {string|Array} value + */ + this.setCoinInfoURL = function(value) { + if (typeof value === "string") { + manager.coinInfoURL = value; + } + }; + + /** + * Set max. limit for account discovery + * @param {number} value + */ + this.setAccountDiscoveryLimit = function(value) { + if (!isNaN(value)) manager.accountDiscoveryLimit = value; + }; + + /** + * Set max. gap for account discovery + * @param {number} value + */ + this.setAccountDiscoveryGapLength = function(value) { + if (!isNaN(value)) manager.accountDiscoveryGapLength = value; + }; + + /** + * Set discovery BIP44 coin type + * @param {number} value + */ + this.setAccountDiscoveryBip44CoinType = function(value) { + if (!isNaN(value)) manager.accountDiscoveryBip44CoinType = value; + }; + + /** + * @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 + * @param {?(string|array)} requiredFirmware + */ + this.getXPubKey = function(path, callback, requiredFirmware) { + if (typeof path === "string") { + path = parseHDPath(path); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "xpubkey", + path: path + }, + requiredFirmware + ), + callback + ); + }; + + this.getFreshAddress = function(callback, requiredFirmware) { + var wrapperCallback = function(result) { + if (result.success) { + callback({ success: true, address: result.freshAddress }); + } else { + callback(result); + } + }; + + manager.sendWithChannel( + _fwStrFix( + { + type: "accountinfo" + }, + requiredFirmware + ), + wrapperCallback + ); + }; + + this.getAccountInfo = function(input, callback, requiredFirmware) { + try { + manager.sendWithChannel( + _fwStrFix( + { + type: "accountinfo", + description: input + }, + requiredFirmware + ), + callback + ); + } catch (e) { + callback({ success: false, error: e }); + } + }; + + this.getAllAccountsInfo = function(callback, requiredFirmware) { + try { + manager.sendWithChannel( + _fwStrFix( + { + type: "allaccountsinfo", + description: "all" + }, + requiredFirmware + ), + callback + ); + } catch (e) { + callback({ success: false, error: e }); + } + }; + + this.getBalance = function(callback, requiredFirmware) { + manager.sendWithChannel( + _fwStrFix( + { + type: "accountinfo" + }, + requiredFirmware + ), + callback + ); + }; + + /** + * @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 + * @param {?(string|array)} requiredFirmware + * + * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto + */ + this.signTx = function(inputs, outputs, callback, requiredFirmware, coin) { + manager.sendWithChannel( + _fwStrFix( + { + type: "signtx", + inputs: inputs, + outputs: outputs, + coin: coin + }, + requiredFirmware + ), + callback + ); + }; + + // new implementation with ethereum at beginnig + this.ethereumSignTx = function() { + this.signEthereumTx.apply(this, arguments); + }; + + // old fallback + this.signEthereumTx = function( + address_n, + nonce, + gas_price, + gas_limit, + to, + value, + data, + chain_id, + callback, + requiredFirmware + ) { + if (requiredFirmware == null) { + requiredFirmware = "1.4.0"; // first firmware that supports ethereum + } + if (typeof address_n === "string") { + address_n = parseHDPath(address_n); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "signethtx", + address_n: address_n, + nonce: nonce, + gas_price: gas_price, + gas_limit: gas_limit, + to: to, + value: value, + data: data, + chain_id: chain_id + }, + requiredFirmware + ), + callback + ); + }; + + /** + * @typedef TxRecipient + * @param {number} amount the amount to send, in satoshis + * @param {string} address the address of the recipient + */ + + /** + * Compose a transaction by doing BIP-0044 discovery, letting the user + * select an account, and picking UTXO by internal preferences. + * Transaction is then signed and returned in the same format as + * `signTx`. Only supports BIP-0044 accounts (single-signature). + * + * @param {array} recipients + * @param {function(SignTxResult)} callback + * @param {?(string|array)} requiredFirmware + */ + this.composeAndSignTx = function(recipients, callback, requiredFirmware) { + manager.sendWithChannel( + _fwStrFix( + { + type: "composetx", + recipients: recipients + }, + requiredFirmware + ), + callback + ); + }; + + /** + * @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 + * @param {?(string|array)} requiredFirmware + * + * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto + */ + this.requestLogin = function( + hosticon, + challenge_hidden, + challenge_visual, + callback, + requiredFirmware + ) { + 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( + _fwStrFix( + { + type: "login", + icon: hosticon, + challenge_hidden: challenge_hidden, + challenge_visual: challenge_visual + }, + requiredFirmware + ), + callback + ); + }; + + /** + * @typedef SignMessageResult + * @param {boolean} success + * @param {?string} error + * @param {?string} address address (in base58check) + * @param {?string} signature signature, in base64 + */ + + /** + * Sign a message + * + * @param {string|array} path + * @param {string} message to sign (ascii) + * @param {string|function(SignMessageResult)} callback + * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) + * @param {?(string|array)} requiredFirmware + * + */ + this.signMessage = function( + path, + message, + callback, + opt_coin, + requiredFirmware + ) { + if (typeof path === "string") { + path = parseHDPath(path); + } + if (!opt_coin) { + opt_coin = "Bitcoin"; + } + if (!callback) { + throw new TypeError("TrezorConnect: callback not found"); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "signmsg", + path: path, + message: message, + coin: opt_coin + }, + requiredFirmware + ), + callback + ); + }; + + /** + * Sign an Ethereum message + * + * @param {string|array} path + * @param {string} message to sign (ascii) + * @param {string|function(SignMessageResult)} callback + * @param {?(string|array)} requiredFirmware + * + */ + this.ethereumSignMessage = function( + path, + message, + callback, + requiredFirmware + ) { + if (typeof path === "string") { + path = parseHDPath(path); + } + if (!callback) { + throw new TypeError("TrezorConnect: callback not found"); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "signethmsg", + path: path, + message: message + }, + requiredFirmware + ), + callback + ); + }; + + /** + * Verify message + * + * @param {string} address + * @param {string} signature (base64) + * @param {string} message (string) + * @param {string|function()} callback + * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) + * @param {?(string|array)} requiredFirmware + * + */ + this.verifyMessage = function( + address, + signature, + message, + callback, + opt_coin, + requiredFirmware + ) { + if (!opt_coin) { + opt_coin = "Bitcoin"; + } + if (!callback) { + throw new TypeError("TrezorConnect: callback not found"); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "verifymsg", + address: address, + signature: signature, + message: message, + coin: { coin_name: opt_coin } + }, + requiredFirmware + ), + callback + ); + }; + + /** + * Verify ethereum message + * + * @param {string} address + * @param {string} signature (base64) + * @param {string} message (string) + * @param {string|function()} callback + * @param {?(string|array)} requiredFirmware + * + */ + this.ethereumVerifyMessage = function( + address, + signature, + message, + callback, + requiredFirmware + ) { + if (!callback) { + throw new TypeError("TrezorConnect: callback not found"); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "verifyethmsg", + address: address, + signature: signature, + message: message + }, + requiredFirmware + ), + callback + ); + }; + + /** + * Symmetric key-value encryption + * + * @param {string|array} path + * @param {string} key to show on device display + * @param {string} value hexadecimal value, length a multiple of 16 bytes + * @param {boolean} encrypt / decrypt direction + * @param {boolean} ask_on_encrypt (should user confirm on encrypt?) + * @param {boolean} ask_on_decrypt (should user confirm on decrypt?) + * @param {string|function()} callback + * @param {?(string|array)} requiredFirmware + * + */ + this.cipherKeyValue = function( + path, + key, + value, + encrypt, + ask_on_encrypt, + ask_on_decrypt, + callback, + requiredFirmware + ) { + if (typeof path === "string") { + path = parseHDPath(path); + } + if (typeof value !== "string") { + throw new TypeError("TrezorConnect: Value must be a string"); + } + if (!/^[0-9A-Fa-f]*$/.test(value)) { + throw new TypeError("TrezorConnect: Value must be hexadecimal"); + } + if (value.length % 32 !== 0) { + // 1 byte == 2 hex strings + throw new TypeError( + "TrezorConnect: Value length must be multiple of 16 bytes" + ); + } + if (!callback) { + throw new TypeError("TrezorConnect: callback not found"); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "cipherkeyvalue", + path: path, + key: key, + value: value, + encrypt: !!encrypt, + ask_on_encrypt: !!ask_on_encrypt, + ask_on_decrypt: !!ask_on_decrypt + }, + requiredFirmware + ), + callback + ); + }; + + this.nemGetAddress = function( + address_n, + network, + callback, + requiredFirmware + ) { + if (requiredFirmware == null) { + requiredFirmware = "1.6.0"; // first firmware that supports NEM + } + if (typeof address_n === "string") { + address_n = parseHDPath(address_n); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "nemGetAddress", + address_n: address_n, + network: network + }, + requiredFirmware + ), + callback + ); + }; + + this.nemSignTx = function( + address_n, + transaction, + callback, + requiredFirmware + ) { + if (requiredFirmware == null) { + requiredFirmware = "1.6.0"; // first firmware that supports NEM + } + if (typeof address_n === "string") { + address_n = parseHDPath(address_n); + } + manager.sendWithChannel( + _fwStrFix( + { + type: "nemSignTx", + address_n: address_n, + transaction: transaction + }, + requiredFirmware + ), + callback + ); + }; + + this.pushTransaction = function(rawTx, callback) { + if (!/^[0-9A-Fa-f]*$/.test(rawTx)) { + throw new TypeError("TrezorConnect: Transaction must be hexadecimal"); + } + if (!callback) { + throw new TypeError("TrezorConnect: callback not found"); + } + + manager.sendWithChannel( + { + type: "pushtx", + rawTx: rawTx + }, + callback + ); + }; + + /** + * Display address on device + * + * @param {array} address + * @param {string} coin + * @param {boolean} segwit + * @param {?(string|array)} requiredFirmware + * + */ + this.getAddress = function( + address, + coin, + segwit, + callback, + requiredFirmware + ) { + if (typeof address === "string") { + address = parseHDPath(address); + } + + manager.sendWithChannel( + _fwStrFix( + { + type: "getaddress", + address_n: address, + coin: coin, + segwit: segwit + }, + requiredFirmware + ), + callback + ); + }; + + /** + * Display ethereum address on device + * + * @param {array} address + * @param {?(string|array)} requiredFirmware + * + */ + this.ethereumGetAddress = function(address, callback, requiredFirmware) { + if (typeof address === "string") { + address = parseHDPath(address); + } + + manager.sendWithChannel( + _fwStrFix( + { + type: "ethgetaddress", + address_n: address + }, + requiredFirmware + ), + callback + ); + }; + + 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.outerHTML = (LOGIN_CSS + LOGIN_HTML) + .replace("@text@", text) + .replace("@callback@", callback) + .replace("@hosticon@", hosticon) + .replace("@challenge_hidden@", challenge_hidden) + .replace("@challenge_visual@", challenge_visual) + .replace("@connect_path@", POPUP_PATH); + } + }; +} + +/* + * `getXPubKey()` + */ + +function parseHDPath(string) { + return string + .toLowerCase() + .split("/") + .filter(function(p) { + return p !== "m"; + }) + .map(function(p) { + var hardened = false; + if (p[p.length - 1] === "'") { + hardened = true; + p = p.substr(0, p.length - 1); + } + if (isNaN(p)) { + throw new Error("Not a valid path."); + } + var n = parseInt(p); + if (hardened) { + // hardened index + n = (n | 0x80000000) >>> 0; + } + return n; + }); +} + +/* + * Popup management + */ + +function ChromePopup(url, name, width, height) { + var left = (screen.width - width) / 2; + var top = (screen.height - height) / 2; + var opts = { + id: name, + innerBounds: { + width: width, + height: height, + left: left, + top: top + } + }; + + var closed = function() { + if (this.onclose) { + this.onclose(false); // never report as blocked + } + }.bind(this); + + var opened = function(w) { + this.window = w; + this.window.onClosed.addListener(closed); + }.bind(this); + + chrome.app.window.create(url, opts, opened); + + this.name = name; + this.window = null; + this.onclose = null; +} + +function ChromeChannel(popup, waiting) { + var port = null; + + var respond = function(data) { + if (waiting) { + var w = waiting; + waiting = null; + w(data); + } + }; + + var setup = function(p) { + if (p.name === popup.name) { + port = p; + port.onMessage.addListener(respond); + chrome.runtime.onConnect.removeListener(setup); + } + }; + + chrome.runtime.onConnect.addListener(setup); + + this.respond = respond; + + this.close = function() { + chrome.runtime.onConnect.removeListener(setup); + port.onMessage.removeListener(respond); + port.disconnect(); + port = null; + }; + + this.send = function(value, callback) { + if (waiting === null) { + waiting = callback; + + if (port) { + port.postMessage(value); + } else { + throw new Error(ERR_CHROME_NOT_CONNECTED); + } + } else { + throw new Error(ERR_ALREADY_WAITING); + } + }; +} + +function Popup(url, origin, name, width, height) { + var left = (screen.width - width) / 2; + var top = (screen.height - height) / 2; + var opts = + "width=" + + width + + ",height=" + + height + + ",left=" + + left + + ",top=" + + top + + ",menubar=no" + + ",toolbar=no" + + ",location=no" + + ",personalbar=no" + + ",status=no"; + var w = window.open(url, name, opts); + + var interval; + var blocked = w.closed; + var iterate = function() { + if (w.closed) { + clearInterval(interval); + if (this.onclose) { + this.onclose(blocked); + } + } + }.bind(this); + interval = setInterval(iterate, 100); + + this.window = w; + this.origin = origin; + this.onclose = null; +} + +function Channel(popup, waiting) { + var respond = function(data) { + if (waiting) { + var w = waiting; + waiting = null; + w(data); + } + }; + + var receive = function(event) { + var org1 = event.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; + var org2 = popup.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; + //if (event.source === popup.window && event.origin === popup.origin) { + if (event.source === popup.window && org1 === org2) { + respond(event.data); + } + }; + + window.addEventListener("message", receive); + + this.respond = respond; + + this.close = function() { + window.removeEventListener("message", receive); + }; + + this.send = function(value, callback) { + if (waiting === null) { + waiting = callback; + popup.window.postMessage(value, popup.origin); + } else { + throw new Error(ERR_ALREADY_WAITING); + } + }; +} + +function ConnectedChannel(p) { + var ready = function() { + clearTimeout(this.timeout); + this.popup.onclose = null; + this.ready = true; + this.onready(); + }.bind(this); + + var closed = function(blocked) { + clearTimeout(this.timeout); + this.channel.close(); + if (blocked) { + this.onerror(new Error(ERR_WINDOW_BLOCKED)); + } else { + this.onerror(new Error(ERR_WINDOW_CLOSED)); + } + }.bind(this); + + var timedout = function() { + this.popup.onclose = null; + if (this.popup.window) { + this.popup.window.close(); + } + this.channel.close(); + this.onerror(new Error(ERR_TIMED_OUT)); + }.bind(this); + + if (IS_CHROME_APP) { + this.popup = new ChromePopup(p.chromeUrl, p.name, p.width, p.height); + this.channel = new ChromeChannel(this.popup, ready); + } else { + this.popup = new Popup(p.url, p.origin, p.name, p.width, p.height); + this.channel = new Channel(this.popup, ready); + } + + this.timeout = setTimeout(timedout, POPUP_INIT_TIMEOUT); + + this.popup.onclose = closed; + + this.ready = false; + this.onready = null; + this.onerror = null; +} + +function PopupManager() { + 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({ + name: "trezor-connect", + width: 600, + height: 500, + origin: POPUP_ORIGIN, + path: POPUP_PATH, + url: POPUP_URL, + chromeUrl: CHROME_URL + }); + cc.onready = function() { + cc.popup.onclose = closed; + callback(cc.channel); + }; + cc.onerror = function(error) { + cc = null; + callback(error); + }; + }.bind(this); + + this.closeAfterSuccess = true; + this.closeAfterFailure = true; + + this.close = function() { + if (cc && cc.popup.window) { + cc.popup.window.close(); + } + }; + + this.waitForChannel = function(callback) { + if (cc) { + if (cc.ready) { + callback(cc.channel); + } else { + callback(new Error(ERR_ALREADY_WAITING)); + } + } else { + try { + open(callback); + } catch (e) { + callback(new Error(ERR_WINDOW_BLOCKED)); + } + } + }; + + this.sendWithChannel = function(message, callback) { + message.bitcoreURLS = this.bitcoreURLS || null; + message.currency = this.currency || null; + message.currencyUnits = this.currencyUnits || null; + message.coinInfoURL = this.coinInfoURL || null; + message.accountDiscoveryLimit = this.accountDiscoveryLimit || null; + message.accountDiscoveryGapLength = this.accountDiscoveryGapLength || null; + message.accountDiscoveryBip44CoinType = + this.accountDiscoveryBip44CoinType || null; + + var respond = function(response) { + var succ = response.success && this.closeAfterSuccess; + var fail = !response.success && this.closeAfterFailure; + if (succ || fail) { + this.close(); + } + callback(response); + }.bind(this); + + var onresponse = function(response) { + if (response instanceof Error) { + var error = response; + respond({ success: false, error: error.message }); + } else { + respond(response); + } + }; + + var onchannel = function(channel) { + if (channel instanceof Error) { + var error = channel; + respond({ success: false, error: error.message }); + } else { + channel.send(message, onresponse); + } + }; + + this.waitForChannel(onchannel); + }; +} + +const connect = new TrezorConnect(); +module.exports = connect; \ No newline at end of file diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js new file mode 100644 index 000000000..d99f384f2 --- /dev/null +++ b/app/scripts/lib/trezorKeyring.js @@ -0,0 +1,255 @@ +const { EventEmitter } = require('events') +const ethUtil = require('ethereumjs-util') +// const sigUtil = require('eth-sig-util') +//const { Lock } = require('semaphore-async-await') + +const hdPathString = `m/44'/60'/0'/0` +const keyringType = 'Trezor Hardware Keyring' + +const TrezorConnect = require('./trezor-connect.js') +const HDKey = require('hdkey') +const TREZOR_FIRMWARE_VERSION = '1.4.0' +const log = require('loglevel') + +class TrezorKeyring extends EventEmitter { + constructor (opts = {}) { + super() + this.type = keyringType + //this.lock = new Lock() + this.accounts = [] + this.hdk = new HDKey() + this.deserialize(opts) + this.page = 0 + this.perPage = 5 + } + + serialize () { + return Promise.resolve({ hdPath: this.hdPath, accounts: this.accounts }) + } + + deserialize (opts = {}) { + this.hdPath = opts.hdPath || hdPathString + this.accounts = opts.accounts || [] + return Promise.resolve() + } + + unlock () { + if (this.hdk.publicKey) return Promise.resolve() + + return new Promise((resolve, reject) => { + TrezorConnect.getXPubKey( + this.hdPath, + response => { + log.debug('TREZOR CONNECT RESPONSE: ') + log.debug(response) + if (response.success) { + this.hdk.publicKey = new Buffer(response.publicKey, 'hex') + this.hdk.chainCode = new Buffer(response.chainCode, 'hex') + resolve() + } else { + reject(response.error || 'Unknown error') + } + }, + TREZOR_FIRMWARE_VERSION + ) + }) + } + + addAccounts (n = 1) { + return new Promise((resolve, reject) => { + return this.unlock() + .then(_ => { + const pathBase = 'm' + const from = n + const to = n + 1 + + this.accounts = [] + + for (let i = from; i < to; i++) { + const dkey = this.hdk.derive(`${pathBase}/${i}`) + const address = ethUtil + .publicToAddress(dkey.publicKey, true) + .toString('hex') + this.accounts.push(ethUtil.toChecksumAddress(address)) + this.page = 0 + } + resolve(this.accounts) + }) + .catch(e => { + reject(e) + }) + }) + } + + async getPage () { + return new Promise((resolve, reject) => { + return this.unlock() + .then(_ => { + const pathBase = 'm' + const from = this.page === 0 ? 0 : (this.page - 1) * this.perPage + const to = from + this.perPage + + const accounts = [] + + for (let i = from; i < to; i++) { + const dkey = this.hdk.derive(`${pathBase}/${i}`) + const address = ethUtil + .publicToAddress(dkey.publicKey, true) + .toString('hex') + accounts.push({ + address: ethUtil.toChecksumAddress(address), + balance: 0, + index: i, + }) + } + resolve(accounts) + }) + .catch(e => { + reject(e) + }) + }) + } + + async getPrevAccountSet () { + this.page-- + return await this.getPage() + } + + async getNextAccountSet () { + this.page++ + return await this.getPage() + } + + getAccounts () { + return Promise.resolve(this.accounts.slice()) + } + + // tx is an instance of the ethereumjs-transaction class. + async signTransaction (address, tx) { + throw new Error('Not supported on this device') + /* + await this.lock.acquire() + try { + + // Look before we leap + await this._checkCorrectTrezorAttached() + + let accountId = await this._findAddressId(address) + let eth = await this._getEth() + tx.v = tx._chainId + let TrezorSig = await eth.signTransaction( + this._derivePath(accountId), + tx.serialize().toString('hex') + ) + tx.v = parseInt(TrezorSig.v, 16) + tx.r = '0x' + TrezorSig.r + tx.s = '0x' + TrezorSig.s + + // Since look before we leap check is racy, also check that signature is for account expected + let addressSignedWith = ethUtil.bufferToHex(tx.getSenderAddress()) + if (addressSignedWith.toLowerCase() !== address.toLowerCase()) { + throw new Error( + `Signature is for ${addressSignedWith} but expected ${address} - is the correct Trezor device attached?` + ) + } + + return tx + + } finally { + await this.lock.release() + }*/ + } + + async signMessage (withAccount, data) { + throw new Error('Not supported on this device') + } + + // For personal_sign, we need to prefix the message: + async signPersonalMessage (withAccount, message) { + throw new Error('Not supported on this device') + /* + await this.lock.acquire() + try { + // Look before we leap + await this._checkCorrectTrezorAttached() + + let accountId = await this._findAddressId(withAccount) + let eth = await this._getEth() + let msgHex = ethUtil.stripHexPrefix(message) + let TrezorSig = await eth.signPersonalMessage( + this._derivePath(accountId), + msgHex + ) + let signature = this._personalToRawSig(TrezorSig) + + // Since look before we leap check is racy, also check that signature is for account expected + let addressSignedWith = sigUtil.recoverPersonalSignature({ + data: message, + sig: signature, + }) + if (addressSignedWith.toLowerCase() !== withAccount.toLowerCase()) { + throw new Error( + `Signature is for ${addressSignedWith} but expected ${withAccount} - is the correct Trezor device attached?` + ) + } + + return signature + + } finally { + await this.lock.release() + } */ + } + + async signTypedData (withAccount, typedData) { + throw new Error('Not supported on this device') + } + + async exportAccount (address) { + throw new Error('Not supported on this device') + } + + async _findAddressId (addr) { + const result = this.accounts.indexOf(addr) + if (result === -1) throw new Error('Unknown address') + else return result + } + + async _addressFromId (i) { + /* Must be called with lock acquired + const eth = await this._getEth() + return (await eth.getAddress(this._derivePath(i))).address*/ + const result = this.accounts[i] + if (!result) throw new Error('Unknown address') + else return result + } + + async _checkCorrectTrezorAttached () { + return true + /* Must be called with lock acquired + if (this.accounts.length > 0) { + const expectedFirstAccount = this.accounts[0] + let actualFirstAccount = await this._addressFromId(0) + if (expectedFirstAccount !== actualFirstAccount) { + throw new Error( + `Incorrect Trezor device attached - expected device containg account ${expectedFirstAccount}, but found ${actualFirstAccount}` + ) + } + }*/ + } + + _derivePath (i) { + return this.hdPath + '/' + i + } + + _personalToRawSig (TrezorSig) { + var v = TrezorSig['v'] - 27 + v = v.toString(16) + if (v.length < 2) { + v = '0' + v + } + return '0x' + TrezorSig['r'] + TrezorSig['s'] + v + } +} + +TrezorKeyring.type = keyringType +module.exports = TrezorKeyring \ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a362e3826..dd5a5616f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -48,6 +48,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const cleanErrorStack = require('./lib/cleanErrorStack') const DiagnosticsReporter = require('./lib/diagnostics-reporter') const log = require('loglevel') +const TrezorKeyring = require("./lib/trezorKeyring"); module.exports = class MetamaskController extends EventEmitter { @@ -130,9 +131,11 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, }) - + // key mgmt + const additionalKeyrings = [TrezorKeyring] this.keyringController = new KeyringController({ + keyringTypes: additionalKeyrings, initState: initState.KeyringController, getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, @@ -363,6 +366,10 @@ module.exports = class MetamaskController extends EventEmitter { resetAccount: nodeify(this.resetAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), + // trezor + connectHardware: nodeify(this.connectHardware, this), + unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + // vault management submitPassword: nodeify(this.submitPassword, this), @@ -523,6 +530,60 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController.setSelectedAddress(address) } + // + // Hardware + // + + /** + * Fetch account list from a trezor device. + * + * @returns [] accounts + */ + async connectHardware (deviceName, page) { + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware Keyring' + )[0] + if (!keyring) { + throw new Error('MetamaskController - No Trezor Hardware Keyring found') + } + + const accounts = page === -1 ? await keyring.getPrevAccountSet() : await keyring.getNextAccountSet() + + return accounts + + } + + /** + * Imports an account from a trezor device. + * + * @returns {} keyState + */ + async unlockTrezorAccount (index) { + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware Keyring' + )[0] + if (!keyring) { + throw new Error('MetamaskController - No Trezor Hardware Keyring found') + } + + const oldAccounts = await keyringController.getAccounts() + const keyState = await keyringController.addNewAccount(keyring) + const newAccounts = await keyringController.getAccounts() + + this.preferencesController.setAddresses(newAccounts) + newAccounts.forEach(address => { + if (!oldAccounts.includes(address)) { + this.preferencesController.setSelectedAddress(address) + } + }) + + const { identities } = this.preferencesController.store.getState() + return { ...keyState, identities } + } + + // // Account Management // -- cgit v1.2.3 From f5f66f59d7d215adf402a1e580c452e634480f69 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 18:48:42 -0400 Subject: clean up --- app/scripts/lib/trezorKeyring.js | 16 +++++++++++----- app/scripts/metamask-controller.js | 7 ++++--- 2 files changed, 15 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js index d99f384f2..83787f3d2 100644 --- a/app/scripts/lib/trezorKeyring.js +++ b/app/scripts/lib/trezorKeyring.js @@ -1,10 +1,9 @@ const { EventEmitter } = require('events') const ethUtil = require('ethereumjs-util') // const sigUtil = require('eth-sig-util') -//const { Lock } = require('semaphore-async-await') const hdPathString = `m/44'/60'/0'/0` -const keyringType = 'Trezor Hardware Keyring' +const keyringType = 'Trezor Hardware' const TrezorConnect = require('./trezor-connect.js') const HDKey = require('hdkey') @@ -15,7 +14,6 @@ class TrezorKeyring extends EventEmitter { constructor (opts = {}) { super() this.type = keyringType - //this.lock = new Lock() this.accounts = [] this.hdk = new HDKey() this.deserialize(opts) @@ -24,16 +22,22 @@ class TrezorKeyring extends EventEmitter { } serialize () { - return Promise.resolve({ hdPath: this.hdPath, accounts: this.accounts }) + return Promise.resolve({ + hdPath: this.hdPath, + accounts: this.accounts, + page: this.page, + }) } deserialize (opts = {}) { this.hdPath = opts.hdPath || hdPathString this.accounts = opts.accounts || [] + this.page = opts.page || 0 return Promise.resolve() } unlock () { + if (this.hdk.publicKey) return Promise.resolve() return new Promise((resolve, reject) => { @@ -56,6 +60,7 @@ class TrezorKeyring extends EventEmitter { } addAccounts (n = 1) { + return new Promise((resolve, reject) => { return this.unlock() .then(_ => { @@ -82,6 +87,7 @@ class TrezorKeyring extends EventEmitter { } async getPage () { + return new Promise((resolve, reject) => { return this.unlock() .then(_ => { @@ -252,4 +258,4 @@ class TrezorKeyring extends EventEmitter { } TrezorKeyring.type = keyringType -module.exports = TrezorKeyring \ No newline at end of file +module.exports = TrezorKeyring diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index dd5a5616f..081c2e2db 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -48,7 +48,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const cleanErrorStack = require('./lib/cleanErrorStack') const DiagnosticsReporter = require('./lib/diagnostics-reporter') const log = require('loglevel') -const TrezorKeyring = require("./lib/trezorKeyring"); +const TrezorKeyring = require('./lib/trezorKeyring') module.exports = class MetamaskController extends EventEmitter { @@ -540,9 +540,10 @@ module.exports = class MetamaskController extends EventEmitter { * @returns [] accounts */ async connectHardware (deviceName, page) { + const keyringController = this.keyringController const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware Keyring' + 'Trezor Hardware' )[0] if (!keyring) { throw new Error('MetamaskController - No Trezor Hardware Keyring found') @@ -562,7 +563,7 @@ module.exports = class MetamaskController extends EventEmitter { async unlockTrezorAccount (index) { const keyringController = this.keyringController const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware Keyring' + 'Trezor Hardware' )[0] if (!keyring) { throw new Error('MetamaskController - No Trezor Hardware Keyring found') -- cgit v1.2.3 From f6b27fa9eb542c1ac3fabdad9285e1a50baee7dc Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 19:02:54 -0400 Subject: add account working --- app/scripts/lib/trezorKeyring.js | 19 +++++++++++-------- app/scripts/metamask-controller.js | 2 ++ 2 files changed, 13 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js index 83787f3d2..cf7436a44 100644 --- a/app/scripts/lib/trezorKeyring.js +++ b/app/scripts/lib/trezorKeyring.js @@ -8,7 +8,7 @@ const keyringType = 'Trezor Hardware' const TrezorConnect = require('./trezor-connect.js') const HDKey = require('hdkey') const TREZOR_FIRMWARE_VERSION = '1.4.0' -const log = require('loglevel') +//const log = require('loglevel') class TrezorKeyring extends EventEmitter { constructor (opts = {}) { @@ -19,10 +19,11 @@ class TrezorKeyring extends EventEmitter { this.deserialize(opts) this.page = 0 this.perPage = 5 + this.accountToUnlock = 0 } serialize () { - return Promise.resolve({ + return Promise.resolve({ hdPath: this.hdPath, accounts: this.accounts, page: this.page, @@ -44,8 +45,6 @@ class TrezorKeyring extends EventEmitter { TrezorConnect.getXPubKey( this.hdPath, response => { - log.debug('TREZOR CONNECT RESPONSE: ') - log.debug(response) if (response.success) { this.hdk.publicKey = new Buffer(response.publicKey, 'hex') this.hdk.chainCode = new Buffer(response.chainCode, 'hex') @@ -59,14 +58,18 @@ class TrezorKeyring extends EventEmitter { }) } + setAccountToUnlock (index) { + this.accountToUnlock = parseInt(index, 10) + } + addAccounts (n = 1) { return new Promise((resolve, reject) => { return this.unlock() .then(_ => { const pathBase = 'm' - const from = n - const to = n + 1 + const from = this.accountToUnlock + const to = from + 1 this.accounts = [] @@ -133,7 +136,7 @@ class TrezorKeyring extends EventEmitter { // tx is an instance of the ethereumjs-transaction class. async signTransaction (address, tx) { throw new Error('Not supported on this device') - /* + /* await this.lock.acquire() try { @@ -200,7 +203,7 @@ class TrezorKeyring extends EventEmitter { } return signature - + } finally { await this.lock.release() } */ diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 081c2e2db..3cb77b35a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -569,6 +569,8 @@ module.exports = class MetamaskController extends EventEmitter { throw new Error('MetamaskController - No Trezor Hardware Keyring found') } + keyring.setAccountToUnlock(index) + const oldAccounts = await keyringController.getAccounts() const keyState = await keyringController.addNewAccount(keyring) const newAccounts = await keyringController.getAccounts() -- cgit v1.2.3 From d1880073f678dbdc52e07e62ec66c39eea5062a6 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 21:10:22 -0400 Subject: balances working --- app/scripts/lib/trezorKeyring.js | 3 ++- app/scripts/metamask-controller.js | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js index cf7436a44..fb029f82a 100644 --- a/app/scripts/lib/trezorKeyring.js +++ b/app/scripts/lib/trezorKeyring.js @@ -8,7 +8,7 @@ const keyringType = 'Trezor Hardware' const TrezorConnect = require('./trezor-connect.js') const HDKey = require('hdkey') const TREZOR_FIRMWARE_VERSION = '1.4.0' -//const log = require('loglevel') +const log = require('loglevel') class TrezorKeyring extends EventEmitter { constructor (opts = {}) { @@ -111,6 +111,7 @@ class TrezorKeyring extends EventEmitter { index: i, }) } + log.debug(accounts) resolve(accounts) }) .catch(e => { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3cb77b35a..daab5baa5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -549,10 +549,10 @@ module.exports = class MetamaskController extends EventEmitter { throw new Error('MetamaskController - No Trezor Hardware Keyring found') } - const accounts = page === -1 ? await keyring.getPrevAccountSet() : await keyring.getNextAccountSet() + const accounts = page === -1 ? await keyring.getPrevAccountSet(this.provider) : await keyring.getNextAccountSet(this.provider) + this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) return accounts - } /** @@ -570,7 +570,6 @@ module.exports = class MetamaskController extends EventEmitter { } keyring.setAccountToUnlock(index) - const oldAccounts = await keyringController.getAccounts() const keyState = await keyringController.addNewAccount(keyring) const newAccounts = await keyringController.getAccounts() -- cgit v1.2.3 From 68d97211ff468b137965df2a30c6b295a3ab5679 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 11 Jun 2018 01:52:41 -0400 Subject: sign transactions is pretty close --- app/scripts/lib/trezorKeyring.js | 126 ++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 49 deletions(-) (limited to 'app') diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js index fb029f82a..fa5d6070c 100644 --- a/app/scripts/lib/trezorKeyring.js +++ b/app/scripts/lib/trezorKeyring.js @@ -4,10 +4,11 @@ const ethUtil = require('ethereumjs-util') const hdPathString = `m/44'/60'/0'/0` const keyringType = 'Trezor Hardware' - +const Transaction = require('ethereumjs-tx') +const pathBase = 'm' const TrezorConnect = require('./trezor-connect.js') const HDKey = require('hdkey') -const TREZOR_FIRMWARE_VERSION = '1.4.0' +const TREZOR_MIN_FIRMWARE_VERSION = '1.5.2' const log = require('loglevel') class TrezorKeyring extends EventEmitter { @@ -19,7 +20,7 @@ class TrezorKeyring extends EventEmitter { this.deserialize(opts) this.page = 0 this.perPage = 5 - this.accountToUnlock = 0 + this.unlockedAccount = 0 } serialize () { @@ -53,13 +54,13 @@ class TrezorKeyring extends EventEmitter { reject(response.error || 'Unknown error') } }, - TREZOR_FIRMWARE_VERSION + TREZOR_MIN_FIRMWARE_VERSION ) }) } setAccountToUnlock (index) { - this.accountToUnlock = parseInt(index, 10) + this.unlockedAccount = parseInt(index, 10) } addAccounts (n = 1) { @@ -67,18 +68,13 @@ class TrezorKeyring extends EventEmitter { return new Promise((resolve, reject) => { return this.unlock() .then(_ => { - const pathBase = 'm' - const from = this.accountToUnlock + const from = this.unlockedAccount const to = from + 1 - this.accounts = [] for (let i = from; i < to; i++) { - const dkey = this.hdk.derive(`${pathBase}/${i}`) - const address = ethUtil - .publicToAddress(dkey.publicKey, true) - .toString('hex') - this.accounts.push(ethUtil.toChecksumAddress(address)) + + this.accounts.push(this.getEthAddress(pathBase, i)) this.page = 0 } resolve(this.accounts) @@ -94,19 +90,16 @@ class TrezorKeyring extends EventEmitter { return new Promise((resolve, reject) => { return this.unlock() .then(_ => { - const pathBase = 'm' + const from = this.page === 0 ? 0 : (this.page - 1) * this.perPage const to = from + this.perPage const accounts = [] for (let i = from; i < to; i++) { - const dkey = this.hdk.derive(`${pathBase}/${i}`) - const address = ethUtil - .publicToAddress(dkey.publicKey, true) - .toString('hex') + accounts.push({ - address: ethUtil.toChecksumAddress(address), + address: this.getEthAddress(pathBase, i), balance: 0, index: i, }) @@ -134,40 +127,75 @@ class TrezorKeyring extends EventEmitter { return Promise.resolve(this.accounts.slice()) } - // tx is an instance of the ethereumjs-transaction class. - async signTransaction (address, tx) { - throw new Error('Not supported on this device') - /* - await this.lock.acquire() - try { - - // Look before we leap - await this._checkCorrectTrezorAttached() + padLeftEven (hex) { + return hex.length % 2 !== 0 ? `0${hex}` : hex + } - let accountId = await this._findAddressId(address) - let eth = await this._getEth() - tx.v = tx._chainId - let TrezorSig = await eth.signTransaction( - this._derivePath(accountId), - tx.serialize().toString('hex') - ) - tx.v = parseInt(TrezorSig.v, 16) - tx.r = '0x' + TrezorSig.r - tx.s = '0x' + TrezorSig.s + cleanData (buf) { + return this.padLeftEven(ethUtil.bufferToHex(buf).substring(2).toLowerCase()) + } - // Since look before we leap check is racy, also check that signature is for account expected - let addressSignedWith = ethUtil.bufferToHex(tx.getSenderAddress()) - if (addressSignedWith.toLowerCase() !== address.toLowerCase()) { - throw new Error( - `Signature is for ${addressSignedWith} but expected ${address} - is the correct Trezor device attached?` - ) - } + getEthAddress (pathBase, i) { + const dkey = this.hdk.derive(`${pathBase}/${i}`) + const address = ethUtil + .publicToAddress(dkey.publicKey, true) + .toString('hex') + return ethUtil.toChecksumAddress(address) + } - return tx + // tx is an instance of the ethereumjs-transaction class. + async signTransaction (address, tx) { - } finally { - await this.lock.release() - }*/ + return new Promise((resolve, reject) => { + log.debug('sign transaction ', address, tx) + const account = `m/44'/60'/0'/${this.unlockedAccount}` + + const txData = { + account, + nonce: this.cleanData(tx.nonce), + gasPrice: this.cleanData(tx.gasPrice), + gasLimit: this.cleanData(tx.gasLimit), + to: this.cleanData(tx.to), + value: this.cleanData(tx.value), + data: this.cleanData(tx.data), + chainId: tx._chainId, + } + + TrezorConnect.ethereumSignTx( + txData.account, + txData.nonce, + txData.gasPrice, + txData.gasLimit, + txData.to, + txData.value, + txData.data === '' ? null : txData.data, + txData.chainId, + response => { + if (response.success) { + tx.v = `0x${response.v.toString(16)}` + tx.r = `0x${response.r}` + tx.s = `0x${response.s}` + log.debug('about to create new tx with data', tx) + + const signedTx = new Transaction(tx) + + log.debug('signature is valid?', signedTx.verifySignature()) + + const addressSignedWith = ethUtil.toChecksumAddress(`0x${signedTx.from.toString('hex')}`) + const correctAddress = ethUtil.toChecksumAddress(address) + if (addressSignedWith !== correctAddress) { + // throw new Error('signature doesnt match the right address') + log.error('signature doesnt match the right address', addressSignedWith, correctAddress) + } + + resolve(signedTx) + + } else { + throw new Error(response.error || 'Unknown error') + } + }, + TREZOR_MIN_FIRMWARE_VERSION) + }) } async signMessage (withAccount, data) { -- cgit v1.2.3 From 999b6bd24a85068209a3213a4c9a83bf67854456 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 11 Jun 2018 01:58:19 -0400 Subject: clean up --- app/scripts/lib/trezorKeyring.js | 113 ++++++++------------------------------- 1 file changed, 21 insertions(+), 92 deletions(-) (limited to 'app') diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js index fa5d6070c..b0e5d31da 100644 --- a/app/scripts/lib/trezorKeyring.js +++ b/app/scripts/lib/trezorKeyring.js @@ -74,7 +74,7 @@ class TrezorKeyring extends EventEmitter { for (let i = from; i < to; i++) { - this.accounts.push(this.getEthAddress(pathBase, i)) + this.accounts.push(this._addressFromId(pathBase, i)) this.page = 0 } resolve(this.accounts) @@ -85,7 +85,7 @@ class TrezorKeyring extends EventEmitter { }) } - async getPage () { + getPage () { return new Promise((resolve, reject) => { return this.unlock() @@ -99,7 +99,7 @@ class TrezorKeyring extends EventEmitter { for (let i = from; i < to; i++) { accounts.push({ - address: this.getEthAddress(pathBase, i), + address: this._addressFromId(pathBase, i), balance: 0, index: i, }) @@ -127,22 +127,6 @@ class TrezorKeyring extends EventEmitter { return Promise.resolve(this.accounts.slice()) } - padLeftEven (hex) { - return hex.length % 2 !== 0 ? `0${hex}` : hex - } - - cleanData (buf) { - return this.padLeftEven(ethUtil.bufferToHex(buf).substring(2).toLowerCase()) - } - - getEthAddress (pathBase, i) { - const dkey = this.hdk.derive(`${pathBase}/${i}`) - const address = ethUtil - .publicToAddress(dkey.publicKey, true) - .toString('hex') - return ethUtil.toChecksumAddress(address) - } - // tx is an instance of the ethereumjs-transaction class. async signTransaction (address, tx) { @@ -152,12 +136,12 @@ class TrezorKeyring extends EventEmitter { const txData = { account, - nonce: this.cleanData(tx.nonce), - gasPrice: this.cleanData(tx.gasPrice), - gasLimit: this.cleanData(tx.gasLimit), - to: this.cleanData(tx.to), - value: this.cleanData(tx.value), - data: this.cleanData(tx.data), + nonce: this._cleanData(tx.nonce), + gasPrice: this._cleanData(tx.gasPrice), + gasLimit: this._cleanData(tx.gasLimit), + to: this._cleanData(tx.to), + value: this._cleanData(tx.value), + data: this._cleanData(tx.data), chainId: tx._chainId, } @@ -204,41 +188,12 @@ class TrezorKeyring extends EventEmitter { // For personal_sign, we need to prefix the message: async signPersonalMessage (withAccount, message) { + // Waiting on trezor to enable this throw new Error('Not supported on this device') - /* - await this.lock.acquire() - try { - // Look before we leap - await this._checkCorrectTrezorAttached() - - let accountId = await this._findAddressId(withAccount) - let eth = await this._getEth() - let msgHex = ethUtil.stripHexPrefix(message) - let TrezorSig = await eth.signPersonalMessage( - this._derivePath(accountId), - msgHex - ) - let signature = this._personalToRawSig(TrezorSig) - - // Since look before we leap check is racy, also check that signature is for account expected - let addressSignedWith = sigUtil.recoverPersonalSignature({ - data: message, - sig: signature, - }) - if (addressSignedWith.toLowerCase() !== withAccount.toLowerCase()) { - throw new Error( - `Signature is for ${addressSignedWith} but expected ${withAccount} - is the correct Trezor device attached?` - ) - } - - return signature - - } finally { - await this.lock.release() - } */ } async signTypedData (withAccount, typedData) { + // Waiting on trezor to enable this throw new Error('Not supported on this device') } @@ -246,46 +201,20 @@ class TrezorKeyring extends EventEmitter { throw new Error('Not supported on this device') } - async _findAddressId (addr) { - const result = this.accounts.indexOf(addr) - if (result === -1) throw new Error('Unknown address') - else return result - } - - async _addressFromId (i) { - /* Must be called with lock acquired - const eth = await this._getEth() - return (await eth.getAddress(this._derivePath(i))).address*/ - const result = this.accounts[i] - if (!result) throw new Error('Unknown address') - else return result - } - - async _checkCorrectTrezorAttached () { - return true - /* Must be called with lock acquired - if (this.accounts.length > 0) { - const expectedFirstAccount = this.accounts[0] - let actualFirstAccount = await this._addressFromId(0) - if (expectedFirstAccount !== actualFirstAccount) { - throw new Error( - `Incorrect Trezor device attached - expected device containg account ${expectedFirstAccount}, but found ${actualFirstAccount}` - ) - } - }*/ + _padLeftEven (hex) { + return hex.length % 2 !== 0 ? `0${hex}` : hex } - _derivePath (i) { - return this.hdPath + '/' + i + _cleanData (buf) { + return this._padLeftEven(ethUtil.bufferToHex(buf).substring(2).toLowerCase()) } - _personalToRawSig (TrezorSig) { - var v = TrezorSig['v'] - 27 - v = v.toString(16) - if (v.length < 2) { - v = '0' + v - } - return '0x' + TrezorSig['r'] + TrezorSig['s'] + v + _addressFromId(pathBase, i) { + const dkey = this.hdk.derive(`${pathBase}/${i}`) + const address = ethUtil + .publicToAddress(dkey.publicKey, true) + .toString('hex') + return ethUtil.toChecksumAddress(address) } } -- cgit v1.2.3 From d4201ae1cc990ba6b69e84586caabc0848c2c38e Mon Sep 17 00:00:00 2001 From: Bruno Date: Wed, 13 Jun 2018 00:22:04 -0400 Subject: added support for signPersonalMessage --- app/scripts/lib/trezorKeyring.js | 73 +++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 28 deletions(-) (limited to 'app') diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js index b0e5d31da..6a483fdcd 100644 --- a/app/scripts/lib/trezorKeyring.js +++ b/app/scripts/lib/trezorKeyring.js @@ -1,6 +1,6 @@ const { EventEmitter } = require('events') const ethUtil = require('ethereumjs-util') -// const sigUtil = require('eth-sig-util') +const sigUtil = require('eth-sig-util') const hdPathString = `m/44'/60'/0'/0` const keyringType = 'Trezor Hardware' @@ -131,31 +131,21 @@ class TrezorKeyring extends EventEmitter { async signTransaction (address, tx) { return new Promise((resolve, reject) => { + log.debug('sign transaction ', address, tx) - const account = `m/44'/60'/0'/${this.unlockedAccount}` - - const txData = { - account, - nonce: this._cleanData(tx.nonce), - gasPrice: this._cleanData(tx.gasPrice), - gasLimit: this._cleanData(tx.gasLimit), - to: this._cleanData(tx.to), - value: this._cleanData(tx.value), - data: this._cleanData(tx.data), - chainId: tx._chainId, - } TrezorConnect.ethereumSignTx( - txData.account, - txData.nonce, - txData.gasPrice, - txData.gasLimit, - txData.to, - txData.value, - txData.data === '' ? null : txData.data, - txData.chainId, + this._getUnlockedAccount(), + this._normalize(tx.nonce), + this._normalize(tx.gasPrice), + this._normalize(tx.gasLimit), + this._normalize(tx.to), + this._normalize(tx.value), + this._normalize(tx.data), + tx._chainId, response => { if (response.success) { + tx.v = `0x${response.v.toString(16)}` tx.r = `0x${response.r}` tx.s = `0x${response.s}` @@ -163,13 +153,11 @@ class TrezorKeyring extends EventEmitter { const signedTx = new Transaction(tx) - log.debug('signature is valid?', signedTx.verifySignature()) - const addressSignedWith = ethUtil.toChecksumAddress(`0x${signedTx.from.toString('hex')}`) const correctAddress = ethUtil.toChecksumAddress(address) if (addressSignedWith !== correctAddress) { - // throw new Error('signature doesnt match the right address') log.error('signature doesnt match the right address', addressSignedWith, correctAddress) + throw new Error('signature doesnt match the right address') } resolve(signedTx) @@ -188,8 +176,24 @@ class TrezorKeyring extends EventEmitter { // For personal_sign, we need to prefix the message: async signPersonalMessage (withAccount, message) { - // Waiting on trezor to enable this - throw new Error('Not supported on this device') + + TrezorConnect.ethereumSignMessage(this._getUnlockedAccount(), message, response => { + if (response.success) { + + const signature = this._personalToRawSig(response.signature) + const addressSignedWith = sigUtil.recoverPersonalSignature({data: message, sig: signature}) + const correctAddress = ethUtil.toChecksumAddress(withAccount) + if (addressSignedWith !== correctAddress) { + log.error('signature doesnt match the right address', addressSignedWith, correctAddress) + throw new Error('signature doesnt match the right address') + } + return signature + + } else { + throw new Error(response.error || 'Unknown error') + } + + }, TREZOR_MIN_FIRMWARE_VERSION) } async signTypedData (withAccount, typedData) { @@ -205,17 +209,30 @@ class TrezorKeyring extends EventEmitter { return hex.length % 2 !== 0 ? `0${hex}` : hex } - _cleanData (buf) { + _normalize (buf) { return this._padLeftEven(ethUtil.bufferToHex(buf).substring(2).toLowerCase()) } - _addressFromId(pathBase, i) { + _addressFromId (pathBase, i) { const dkey = this.hdk.derive(`${pathBase}/${i}`) const address = ethUtil .publicToAddress(dkey.publicKey, true) .toString('hex') return ethUtil.toChecksumAddress(address) } + + _getUnlockedAccount () { + return `${this.hdPath}/${this.unlockedAccount}` + } + + _personalToRawSig (signature) { + var v = signature['v'] - 27 + v = v.toString(16) + if (v.length < 2) { + v = '0' + v + } + return '0x' + signature['r'] + signature['s'] + v + } } TrezorKeyring.type = keyringType -- cgit v1.2.3 From 8763ea898e7838d08315063b0e2181405a2ae3d5 Mon Sep 17 00:00:00 2001 From: Bruno Date: Wed, 13 Jun 2018 01:32:13 -0400 Subject: move TrezorKeyring to its own package --- app/scripts/lib/trezor-connect.js | 1138 ------------------------------------ app/scripts/lib/trezorKeyring.js | 239 -------- app/scripts/metamask-controller.js | 5 +- 3 files changed, 3 insertions(+), 1379 deletions(-) delete mode 100644 app/scripts/lib/trezor-connect.js delete mode 100644 app/scripts/lib/trezorKeyring.js (limited to 'app') diff --git a/app/scripts/lib/trezor-connect.js b/app/scripts/lib/trezor-connect.js deleted file mode 100644 index 574e88104..000000000 --- a/app/scripts/lib/trezor-connect.js +++ /dev/null @@ -1,1138 +0,0 @@ -/* eslint-disable */ -/* prettier-ignore */ - -/** - * (C) 2017 SatoshiLabs - * - * GPLv3 - */ -var TREZOR_CONNECT_VERSION = 4; - -if (!Array.isArray) { - Array.isArray = function(arg) { - return Object.prototype.toString.call(arg) === "[object Array]"; - }; -} - -var HD_HARDENED = 0x80000000; - -// react sometimes adds some other parameters that should not be there -function _fwStrFix(obj, fw) { - if (typeof fw === "string") { - obj.requiredFirmware = fw; - } - return obj; -} - -("use strict"); - -var chrome = window.chrome; -var IS_CHROME_APP = chrome && chrome.app && chrome.app.window; - -var ERR_TIMED_OUT = "Loading timed out"; -var ERR_WINDOW_CLOSED = "Window closed"; -var ERR_WINDOW_BLOCKED = "Window blocked"; -var ERR_ALREADY_WAITING = "Already waiting for a response"; -var ERR_CHROME_NOT_CONNECTED = "Internal Chrome popup is not responding."; - -var DISABLE_LOGIN_BUTTONS = window.TREZOR_DISABLE_LOGIN_BUTTONS || false; -var CHROME_URL = window.TREZOR_CHROME_URL || "./chrome/wrapper.html"; -var POPUP_ORIGIN = window.TREZOR_POPUP_ORIGIN || "https://connect.trezor.io"; -var POPUP_PATH = - window.TREZOR_POPUP_PATH || POPUP_ORIGIN + "/" + TREZOR_CONNECT_VERSION; -var POPUP_URL = - window.TREZOR_POPUP_URL || - POPUP_PATH + "/popup/popup.html?v=" + new Date().getTime(); - -var POPUP_INIT_TIMEOUT = 15000; - -/** - * Public API. - */ -function TrezorConnect() { - var manager = new PopupManager(); - - /** - * Popup errors. - */ - this.ERR_TIMED_OUT = ERR_TIMED_OUT; - this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED; - this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED; - this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING; - this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED; - - /** - * Open the popup for further communication. All API functions open the - * popup automatically, but if you need to generate some parameters - * asynchronously, use `open` first to avoid popup blockers. - * @param {function(?Error)} callback - */ - this.open = function(callback) { - var onchannel = function(result) { - if (result instanceof Error) { - callback(result); - } else { - callback(); - } - }; - manager.waitForChannel(onchannel); - }; - - /** - * Close the opened popup, if any. - */ - this.close = function() { - manager.close(); - }; - - /** - * Enable or disable closing the opened popup after a successful call. - * @param {boolean} value - */ - this.closeAfterSuccess = function(value) { - manager.closeAfterSuccess = value; - }; - - /** - * Enable or disable closing the opened popup after a failed call. - * @param {boolean} value - */ - this.closeAfterFailure = function(value) { - manager.closeAfterFailure = value; - }; - - /** - * Set bitcore server - * @param {string|Array} value - */ - this.setBitcoreURLS = function(value) { - if (typeof value === "string") { - manager.bitcoreURLS = [value]; - } else if (value instanceof Array) { - manager.bitcoreURLS = value; - } - }; - - /** - * Set currency. Human readable coin name - * @param {string|Array} value - */ - this.setCurrency = function(value) { - if (typeof value === "string") { - manager.currency = value; - } - }; - - /** - * Set currency units (mBTC, BTC) - * @param {string|Array} value - */ - this.setCurrencyUnits = function(value) { - if (typeof value === "string") { - manager.currencyUnits = value; - } - }; - - /** - * Set coin info json url - * @param {string|Array} value - */ - this.setCoinInfoURL = function(value) { - if (typeof value === "string") { - manager.coinInfoURL = value; - } - }; - - /** - * Set max. limit for account discovery - * @param {number} value - */ - this.setAccountDiscoveryLimit = function(value) { - if (!isNaN(value)) manager.accountDiscoveryLimit = value; - }; - - /** - * Set max. gap for account discovery - * @param {number} value - */ - this.setAccountDiscoveryGapLength = function(value) { - if (!isNaN(value)) manager.accountDiscoveryGapLength = value; - }; - - /** - * Set discovery BIP44 coin type - * @param {number} value - */ - this.setAccountDiscoveryBip44CoinType = function(value) { - if (!isNaN(value)) manager.accountDiscoveryBip44CoinType = value; - }; - - /** - * @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 - * @param {?(string|array)} requiredFirmware - */ - this.getXPubKey = function(path, callback, requiredFirmware) { - if (typeof path === "string") { - path = parseHDPath(path); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "xpubkey", - path: path - }, - requiredFirmware - ), - callback - ); - }; - - this.getFreshAddress = function(callback, requiredFirmware) { - var wrapperCallback = function(result) { - if (result.success) { - callback({ success: true, address: result.freshAddress }); - } else { - callback(result); - } - }; - - manager.sendWithChannel( - _fwStrFix( - { - type: "accountinfo" - }, - requiredFirmware - ), - wrapperCallback - ); - }; - - this.getAccountInfo = function(input, callback, requiredFirmware) { - try { - manager.sendWithChannel( - _fwStrFix( - { - type: "accountinfo", - description: input - }, - requiredFirmware - ), - callback - ); - } catch (e) { - callback({ success: false, error: e }); - } - }; - - this.getAllAccountsInfo = function(callback, requiredFirmware) { - try { - manager.sendWithChannel( - _fwStrFix( - { - type: "allaccountsinfo", - description: "all" - }, - requiredFirmware - ), - callback - ); - } catch (e) { - callback({ success: false, error: e }); - } - }; - - this.getBalance = function(callback, requiredFirmware) { - manager.sendWithChannel( - _fwStrFix( - { - type: "accountinfo" - }, - requiredFirmware - ), - callback - ); - }; - - /** - * @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 - * @param {?(string|array)} requiredFirmware - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto - */ - this.signTx = function(inputs, outputs, callback, requiredFirmware, coin) { - manager.sendWithChannel( - _fwStrFix( - { - type: "signtx", - inputs: inputs, - outputs: outputs, - coin: coin - }, - requiredFirmware - ), - callback - ); - }; - - // new implementation with ethereum at beginnig - this.ethereumSignTx = function() { - this.signEthereumTx.apply(this, arguments); - }; - - // old fallback - this.signEthereumTx = function( - address_n, - nonce, - gas_price, - gas_limit, - to, - value, - data, - chain_id, - callback, - requiredFirmware - ) { - if (requiredFirmware == null) { - requiredFirmware = "1.4.0"; // first firmware that supports ethereum - } - if (typeof address_n === "string") { - address_n = parseHDPath(address_n); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "signethtx", - address_n: address_n, - nonce: nonce, - gas_price: gas_price, - gas_limit: gas_limit, - to: to, - value: value, - data: data, - chain_id: chain_id - }, - requiredFirmware - ), - callback - ); - }; - - /** - * @typedef TxRecipient - * @param {number} amount the amount to send, in satoshis - * @param {string} address the address of the recipient - */ - - /** - * Compose a transaction by doing BIP-0044 discovery, letting the user - * select an account, and picking UTXO by internal preferences. - * Transaction is then signed and returned in the same format as - * `signTx`. Only supports BIP-0044 accounts (single-signature). - * - * @param {array} recipients - * @param {function(SignTxResult)} callback - * @param {?(string|array)} requiredFirmware - */ - this.composeAndSignTx = function(recipients, callback, requiredFirmware) { - manager.sendWithChannel( - _fwStrFix( - { - type: "composetx", - recipients: recipients - }, - requiredFirmware - ), - callback - ); - }; - - /** - * @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 - * @param {?(string|array)} requiredFirmware - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto - */ - this.requestLogin = function( - hosticon, - challenge_hidden, - challenge_visual, - callback, - requiredFirmware - ) { - 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( - _fwStrFix( - { - type: "login", - icon: hosticon, - challenge_hidden: challenge_hidden, - challenge_visual: challenge_visual - }, - requiredFirmware - ), - callback - ); - }; - - /** - * @typedef SignMessageResult - * @param {boolean} success - * @param {?string} error - * @param {?string} address address (in base58check) - * @param {?string} signature signature, in base64 - */ - - /** - * Sign a message - * - * @param {string|array} path - * @param {string} message to sign (ascii) - * @param {string|function(SignMessageResult)} callback - * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) - * @param {?(string|array)} requiredFirmware - * - */ - this.signMessage = function( - path, - message, - callback, - opt_coin, - requiredFirmware - ) { - if (typeof path === "string") { - path = parseHDPath(path); - } - if (!opt_coin) { - opt_coin = "Bitcoin"; - } - if (!callback) { - throw new TypeError("TrezorConnect: callback not found"); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "signmsg", - path: path, - message: message, - coin: opt_coin - }, - requiredFirmware - ), - callback - ); - }; - - /** - * Sign an Ethereum message - * - * @param {string|array} path - * @param {string} message to sign (ascii) - * @param {string|function(SignMessageResult)} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumSignMessage = function( - path, - message, - callback, - requiredFirmware - ) { - if (typeof path === "string") { - path = parseHDPath(path); - } - if (!callback) { - throw new TypeError("TrezorConnect: callback not found"); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "signethmsg", - path: path, - message: message - }, - requiredFirmware - ), - callback - ); - }; - - /** - * Verify message - * - * @param {string} address - * @param {string} signature (base64) - * @param {string} message (string) - * @param {string|function()} callback - * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) - * @param {?(string|array)} requiredFirmware - * - */ - this.verifyMessage = function( - address, - signature, - message, - callback, - opt_coin, - requiredFirmware - ) { - if (!opt_coin) { - opt_coin = "Bitcoin"; - } - if (!callback) { - throw new TypeError("TrezorConnect: callback not found"); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "verifymsg", - address: address, - signature: signature, - message: message, - coin: { coin_name: opt_coin } - }, - requiredFirmware - ), - callback - ); - }; - - /** - * Verify ethereum message - * - * @param {string} address - * @param {string} signature (base64) - * @param {string} message (string) - * @param {string|function()} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumVerifyMessage = function( - address, - signature, - message, - callback, - requiredFirmware - ) { - if (!callback) { - throw new TypeError("TrezorConnect: callback not found"); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "verifyethmsg", - address: address, - signature: signature, - message: message - }, - requiredFirmware - ), - callback - ); - }; - - /** - * Symmetric key-value encryption - * - * @param {string|array} path - * @param {string} key to show on device display - * @param {string} value hexadecimal value, length a multiple of 16 bytes - * @param {boolean} encrypt / decrypt direction - * @param {boolean} ask_on_encrypt (should user confirm on encrypt?) - * @param {boolean} ask_on_decrypt (should user confirm on decrypt?) - * @param {string|function()} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.cipherKeyValue = function( - path, - key, - value, - encrypt, - ask_on_encrypt, - ask_on_decrypt, - callback, - requiredFirmware - ) { - if (typeof path === "string") { - path = parseHDPath(path); - } - if (typeof value !== "string") { - throw new TypeError("TrezorConnect: Value must be a string"); - } - if (!/^[0-9A-Fa-f]*$/.test(value)) { - throw new TypeError("TrezorConnect: Value must be hexadecimal"); - } - if (value.length % 32 !== 0) { - // 1 byte == 2 hex strings - throw new TypeError( - "TrezorConnect: Value length must be multiple of 16 bytes" - ); - } - if (!callback) { - throw new TypeError("TrezorConnect: callback not found"); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "cipherkeyvalue", - path: path, - key: key, - value: value, - encrypt: !!encrypt, - ask_on_encrypt: !!ask_on_encrypt, - ask_on_decrypt: !!ask_on_decrypt - }, - requiredFirmware - ), - callback - ); - }; - - this.nemGetAddress = function( - address_n, - network, - callback, - requiredFirmware - ) { - if (requiredFirmware == null) { - requiredFirmware = "1.6.0"; // first firmware that supports NEM - } - if (typeof address_n === "string") { - address_n = parseHDPath(address_n); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "nemGetAddress", - address_n: address_n, - network: network - }, - requiredFirmware - ), - callback - ); - }; - - this.nemSignTx = function( - address_n, - transaction, - callback, - requiredFirmware - ) { - if (requiredFirmware == null) { - requiredFirmware = "1.6.0"; // first firmware that supports NEM - } - if (typeof address_n === "string") { - address_n = parseHDPath(address_n); - } - manager.sendWithChannel( - _fwStrFix( - { - type: "nemSignTx", - address_n: address_n, - transaction: transaction - }, - requiredFirmware - ), - callback - ); - }; - - this.pushTransaction = function(rawTx, callback) { - if (!/^[0-9A-Fa-f]*$/.test(rawTx)) { - throw new TypeError("TrezorConnect: Transaction must be hexadecimal"); - } - if (!callback) { - throw new TypeError("TrezorConnect: callback not found"); - } - - manager.sendWithChannel( - { - type: "pushtx", - rawTx: rawTx - }, - callback - ); - }; - - /** - * Display address on device - * - * @param {array} address - * @param {string} coin - * @param {boolean} segwit - * @param {?(string|array)} requiredFirmware - * - */ - this.getAddress = function( - address, - coin, - segwit, - callback, - requiredFirmware - ) { - if (typeof address === "string") { - address = parseHDPath(address); - } - - manager.sendWithChannel( - _fwStrFix( - { - type: "getaddress", - address_n: address, - coin: coin, - segwit: segwit - }, - requiredFirmware - ), - callback - ); - }; - - /** - * Display ethereum address on device - * - * @param {array} address - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumGetAddress = function(address, callback, requiredFirmware) { - if (typeof address === "string") { - address = parseHDPath(address); - } - - manager.sendWithChannel( - _fwStrFix( - { - type: "ethgetaddress", - address_n: address - }, - requiredFirmware - ), - callback - ); - }; - - 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.outerHTML = (LOGIN_CSS + LOGIN_HTML) - .replace("@text@", text) - .replace("@callback@", callback) - .replace("@hosticon@", hosticon) - .replace("@challenge_hidden@", challenge_hidden) - .replace("@challenge_visual@", challenge_visual) - .replace("@connect_path@", POPUP_PATH); - } - }; -} - -/* - * `getXPubKey()` - */ - -function parseHDPath(string) { - return string - .toLowerCase() - .split("/") - .filter(function(p) { - return p !== "m"; - }) - .map(function(p) { - var hardened = false; - if (p[p.length - 1] === "'") { - hardened = true; - p = p.substr(0, p.length - 1); - } - if (isNaN(p)) { - throw new Error("Not a valid path."); - } - var n = parseInt(p); - if (hardened) { - // hardened index - n = (n | 0x80000000) >>> 0; - } - return n; - }); -} - -/* - * Popup management - */ - -function ChromePopup(url, name, width, height) { - var left = (screen.width - width) / 2; - var top = (screen.height - height) / 2; - var opts = { - id: name, - innerBounds: { - width: width, - height: height, - left: left, - top: top - } - }; - - var closed = function() { - if (this.onclose) { - this.onclose(false); // never report as blocked - } - }.bind(this); - - var opened = function(w) { - this.window = w; - this.window.onClosed.addListener(closed); - }.bind(this); - - chrome.app.window.create(url, opts, opened); - - this.name = name; - this.window = null; - this.onclose = null; -} - -function ChromeChannel(popup, waiting) { - var port = null; - - var respond = function(data) { - if (waiting) { - var w = waiting; - waiting = null; - w(data); - } - }; - - var setup = function(p) { - if (p.name === popup.name) { - port = p; - port.onMessage.addListener(respond); - chrome.runtime.onConnect.removeListener(setup); - } - }; - - chrome.runtime.onConnect.addListener(setup); - - this.respond = respond; - - this.close = function() { - chrome.runtime.onConnect.removeListener(setup); - port.onMessage.removeListener(respond); - port.disconnect(); - port = null; - }; - - this.send = function(value, callback) { - if (waiting === null) { - waiting = callback; - - if (port) { - port.postMessage(value); - } else { - throw new Error(ERR_CHROME_NOT_CONNECTED); - } - } else { - throw new Error(ERR_ALREADY_WAITING); - } - }; -} - -function Popup(url, origin, name, width, height) { - var left = (screen.width - width) / 2; - var top = (screen.height - height) / 2; - var opts = - "width=" + - width + - ",height=" + - height + - ",left=" + - left + - ",top=" + - top + - ",menubar=no" + - ",toolbar=no" + - ",location=no" + - ",personalbar=no" + - ",status=no"; - var w = window.open(url, name, opts); - - var interval; - var blocked = w.closed; - var iterate = function() { - if (w.closed) { - clearInterval(interval); - if (this.onclose) { - this.onclose(blocked); - } - } - }.bind(this); - interval = setInterval(iterate, 100); - - this.window = w; - this.origin = origin; - this.onclose = null; -} - -function Channel(popup, waiting) { - var respond = function(data) { - if (waiting) { - var w = waiting; - waiting = null; - w(data); - } - }; - - var receive = function(event) { - var org1 = event.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; - var org2 = popup.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; - //if (event.source === popup.window && event.origin === popup.origin) { - if (event.source === popup.window && org1 === org2) { - respond(event.data); - } - }; - - window.addEventListener("message", receive); - - this.respond = respond; - - this.close = function() { - window.removeEventListener("message", receive); - }; - - this.send = function(value, callback) { - if (waiting === null) { - waiting = callback; - popup.window.postMessage(value, popup.origin); - } else { - throw new Error(ERR_ALREADY_WAITING); - } - }; -} - -function ConnectedChannel(p) { - var ready = function() { - clearTimeout(this.timeout); - this.popup.onclose = null; - this.ready = true; - this.onready(); - }.bind(this); - - var closed = function(blocked) { - clearTimeout(this.timeout); - this.channel.close(); - if (blocked) { - this.onerror(new Error(ERR_WINDOW_BLOCKED)); - } else { - this.onerror(new Error(ERR_WINDOW_CLOSED)); - } - }.bind(this); - - var timedout = function() { - this.popup.onclose = null; - if (this.popup.window) { - this.popup.window.close(); - } - this.channel.close(); - this.onerror(new Error(ERR_TIMED_OUT)); - }.bind(this); - - if (IS_CHROME_APP) { - this.popup = new ChromePopup(p.chromeUrl, p.name, p.width, p.height); - this.channel = new ChromeChannel(this.popup, ready); - } else { - this.popup = new Popup(p.url, p.origin, p.name, p.width, p.height); - this.channel = new Channel(this.popup, ready); - } - - this.timeout = setTimeout(timedout, POPUP_INIT_TIMEOUT); - - this.popup.onclose = closed; - - this.ready = false; - this.onready = null; - this.onerror = null; -} - -function PopupManager() { - 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({ - name: "trezor-connect", - width: 600, - height: 500, - origin: POPUP_ORIGIN, - path: POPUP_PATH, - url: POPUP_URL, - chromeUrl: CHROME_URL - }); - cc.onready = function() { - cc.popup.onclose = closed; - callback(cc.channel); - }; - cc.onerror = function(error) { - cc = null; - callback(error); - }; - }.bind(this); - - this.closeAfterSuccess = true; - this.closeAfterFailure = true; - - this.close = function() { - if (cc && cc.popup.window) { - cc.popup.window.close(); - } - }; - - this.waitForChannel = function(callback) { - if (cc) { - if (cc.ready) { - callback(cc.channel); - } else { - callback(new Error(ERR_ALREADY_WAITING)); - } - } else { - try { - open(callback); - } catch (e) { - callback(new Error(ERR_WINDOW_BLOCKED)); - } - } - }; - - this.sendWithChannel = function(message, callback) { - message.bitcoreURLS = this.bitcoreURLS || null; - message.currency = this.currency || null; - message.currencyUnits = this.currencyUnits || null; - message.coinInfoURL = this.coinInfoURL || null; - message.accountDiscoveryLimit = this.accountDiscoveryLimit || null; - message.accountDiscoveryGapLength = this.accountDiscoveryGapLength || null; - message.accountDiscoveryBip44CoinType = - this.accountDiscoveryBip44CoinType || null; - - var respond = function(response) { - var succ = response.success && this.closeAfterSuccess; - var fail = !response.success && this.closeAfterFailure; - if (succ || fail) { - this.close(); - } - callback(response); - }.bind(this); - - var onresponse = function(response) { - if (response instanceof Error) { - var error = response; - respond({ success: false, error: error.message }); - } else { - respond(response); - } - }; - - var onchannel = function(channel) { - if (channel instanceof Error) { - var error = channel; - respond({ success: false, error: error.message }); - } else { - channel.send(message, onresponse); - } - }; - - this.waitForChannel(onchannel); - }; -} - -const connect = new TrezorConnect(); -module.exports = connect; \ No newline at end of file diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js deleted file mode 100644 index 6a483fdcd..000000000 --- a/app/scripts/lib/trezorKeyring.js +++ /dev/null @@ -1,239 +0,0 @@ -const { EventEmitter } = require('events') -const ethUtil = require('ethereumjs-util') -const sigUtil = require('eth-sig-util') - -const hdPathString = `m/44'/60'/0'/0` -const keyringType = 'Trezor Hardware' -const Transaction = require('ethereumjs-tx') -const pathBase = 'm' -const TrezorConnect = require('./trezor-connect.js') -const HDKey = require('hdkey') -const TREZOR_MIN_FIRMWARE_VERSION = '1.5.2' -const log = require('loglevel') - -class TrezorKeyring extends EventEmitter { - constructor (opts = {}) { - super() - this.type = keyringType - this.accounts = [] - this.hdk = new HDKey() - this.deserialize(opts) - this.page = 0 - this.perPage = 5 - this.unlockedAccount = 0 - } - - serialize () { - return Promise.resolve({ - hdPath: this.hdPath, - accounts: this.accounts, - page: this.page, - }) - } - - deserialize (opts = {}) { - this.hdPath = opts.hdPath || hdPathString - this.accounts = opts.accounts || [] - this.page = opts.page || 0 - return Promise.resolve() - } - - unlock () { - - if (this.hdk.publicKey) return Promise.resolve() - - return new Promise((resolve, reject) => { - TrezorConnect.getXPubKey( - this.hdPath, - response => { - if (response.success) { - this.hdk.publicKey = new Buffer(response.publicKey, 'hex') - this.hdk.chainCode = new Buffer(response.chainCode, 'hex') - resolve() - } else { - reject(response.error || 'Unknown error') - } - }, - TREZOR_MIN_FIRMWARE_VERSION - ) - }) - } - - setAccountToUnlock (index) { - this.unlockedAccount = parseInt(index, 10) - } - - addAccounts (n = 1) { - - return new Promise((resolve, reject) => { - return this.unlock() - .then(_ => { - const from = this.unlockedAccount - const to = from + 1 - this.accounts = [] - - for (let i = from; i < to; i++) { - - this.accounts.push(this._addressFromId(pathBase, i)) - this.page = 0 - } - resolve(this.accounts) - }) - .catch(e => { - reject(e) - }) - }) - } - - getPage () { - - return new Promise((resolve, reject) => { - return this.unlock() - .then(_ => { - - const from = this.page === 0 ? 0 : (this.page - 1) * this.perPage - const to = from + this.perPage - - const accounts = [] - - for (let i = from; i < to; i++) { - - accounts.push({ - address: this._addressFromId(pathBase, i), - balance: 0, - index: i, - }) - } - log.debug(accounts) - resolve(accounts) - }) - .catch(e => { - reject(e) - }) - }) - } - - async getPrevAccountSet () { - this.page-- - return await this.getPage() - } - - async getNextAccountSet () { - this.page++ - return await this.getPage() - } - - getAccounts () { - return Promise.resolve(this.accounts.slice()) - } - - // tx is an instance of the ethereumjs-transaction class. - async signTransaction (address, tx) { - - return new Promise((resolve, reject) => { - - log.debug('sign transaction ', address, tx) - - TrezorConnect.ethereumSignTx( - this._getUnlockedAccount(), - this._normalize(tx.nonce), - this._normalize(tx.gasPrice), - this._normalize(tx.gasLimit), - this._normalize(tx.to), - this._normalize(tx.value), - this._normalize(tx.data), - tx._chainId, - response => { - if (response.success) { - - tx.v = `0x${response.v.toString(16)}` - tx.r = `0x${response.r}` - tx.s = `0x${response.s}` - log.debug('about to create new tx with data', tx) - - const signedTx = new Transaction(tx) - - const addressSignedWith = ethUtil.toChecksumAddress(`0x${signedTx.from.toString('hex')}`) - const correctAddress = ethUtil.toChecksumAddress(address) - if (addressSignedWith !== correctAddress) { - log.error('signature doesnt match the right address', addressSignedWith, correctAddress) - throw new Error('signature doesnt match the right address') - } - - resolve(signedTx) - - } else { - throw new Error(response.error || 'Unknown error') - } - }, - TREZOR_MIN_FIRMWARE_VERSION) - }) - } - - async signMessage (withAccount, data) { - throw new Error('Not supported on this device') - } - - // For personal_sign, we need to prefix the message: - async signPersonalMessage (withAccount, message) { - - TrezorConnect.ethereumSignMessage(this._getUnlockedAccount(), message, response => { - if (response.success) { - - const signature = this._personalToRawSig(response.signature) - const addressSignedWith = sigUtil.recoverPersonalSignature({data: message, sig: signature}) - const correctAddress = ethUtil.toChecksumAddress(withAccount) - if (addressSignedWith !== correctAddress) { - log.error('signature doesnt match the right address', addressSignedWith, correctAddress) - throw new Error('signature doesnt match the right address') - } - return signature - - } else { - throw new Error(response.error || 'Unknown error') - } - - }, TREZOR_MIN_FIRMWARE_VERSION) - } - - async signTypedData (withAccount, typedData) { - // Waiting on trezor to enable this - throw new Error('Not supported on this device') - } - - async exportAccount (address) { - throw new Error('Not supported on this device') - } - - _padLeftEven (hex) { - return hex.length % 2 !== 0 ? `0${hex}` : hex - } - - _normalize (buf) { - return this._padLeftEven(ethUtil.bufferToHex(buf).substring(2).toLowerCase()) - } - - _addressFromId (pathBase, i) { - const dkey = this.hdk.derive(`${pathBase}/${i}`) - const address = ethUtil - .publicToAddress(dkey.publicKey, true) - .toString('hex') - return ethUtil.toChecksumAddress(address) - } - - _getUnlockedAccount () { - return `${this.hdPath}/${this.unlockedAccount}` - } - - _personalToRawSig (signature) { - var v = signature['v'] - 27 - v = v.toString(16) - if (v.length < 2) { - v = '0' + v - } - return '0x' + signature['r'] + signature['s'] + v - } -} - -TrezorKeyring.type = keyringType -module.exports = TrezorKeyring diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index daab5baa5..abe7ff8a2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -48,7 +48,7 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const cleanErrorStack = require('./lib/cleanErrorStack') const DiagnosticsReporter = require('./lib/diagnostics-reporter') const log = require('loglevel') -const TrezorKeyring = require('./lib/trezorKeyring') +const TrezorKeyring = require('eth-trezor-keyring') module.exports = class MetamaskController extends EventEmitter { @@ -549,7 +549,8 @@ module.exports = class MetamaskController extends EventEmitter { throw new Error('MetamaskController - No Trezor Hardware Keyring found') } - const accounts = page === -1 ? await keyring.getPrevAccountSet(this.provider) : await keyring.getNextAccountSet(this.provider) + const accounts = await keyring.getPage(page) + this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) return accounts -- cgit v1.2.3 From 704e2a21f8a3fc5f3d6245c5a924cd2df0cfd36e Mon Sep 17 00:00:00 2001 From: Bruno Date: Wed, 13 Jun 2018 02:09:25 -0400 Subject: clean up --- app/scripts/metamask-controller.js | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 6c380fd71..c57b643bb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -131,7 +131,7 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, }) - + // key mgmt const additionalKeyrings = [TrezorKeyring] this.keyringController = new KeyringController({ @@ -423,7 +423,6 @@ module.exports = class MetamaskController extends EventEmitter { } - //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= @@ -537,19 +536,23 @@ module.exports = class MetamaskController extends EventEmitter { */ async connectHardware (deviceName, page) { - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController - No Trezor Hardware Keyring found') - } - - const accounts = await keyring.getPage(page) + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + throw new Error('MetamaskController - No Trezor Hardware Keyring found') + } - this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) + const accounts = await keyring.getPage(page) + this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) + return accounts - return accounts + default: + throw new Error('MetamaskController - Unknown device') + } } /** @@ -581,7 +584,7 @@ module.exports = class MetamaskController extends EventEmitter { const { identities } = this.preferencesController.store.getState() return { ...keyState, identities } } - + // // Account Management @@ -1037,7 +1040,7 @@ module.exports = class MetamaskController extends EventEmitter { * Allows a user to begin the seed phrase recovery process. * @param {Function} cb - A callback function called when complete. */ - markPasswordForgotten(cb) { + markPasswordForgotten (cb) { this.configManager.setPasswordForgotten(true) this.sendUpdate() cb() @@ -1047,7 +1050,7 @@ module.exports = class MetamaskController extends EventEmitter { * Allows a user to end the seed phrase recovery process. * @param {Function} cb - A callback function called when complete. */ - unMarkPasswordForgotten(cb) { + unMarkPasswordForgotten (cb) { this.configManager.setPasswordForgotten(false) this.sendUpdate() cb() -- cgit v1.2.3 From 87dfca07676f7a4745f68d2331a78f3ae53c558f Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Sat, 23 Jun 2018 02:52:11 -0400 Subject: fixes --- app/scripts/metamask-controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c57b643bb..943904e4c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -539,14 +539,14 @@ module.exports = class MetamaskController extends EventEmitter { switch (deviceName) { case 'trezor': const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( + let keyring = await keyringController.getKeyringsByType( 'Trezor Hardware' )[0] if (!keyring) { - throw new Error('MetamaskController - No Trezor Hardware Keyring found') + keyring = await this.keyringController.addNewKeyring('Trezor Hardware') } - const accounts = await keyring.getPage(page) + const accounts = page === 1 ? await keyring.getNextPage() : await keyring.getPreviousPage() this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) return accounts -- cgit v1.2.3 From 451c05bcbb2a9612cf242caa52c034c0056807c8 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 2 Jul 2018 15:14:05 -0400 Subject: fix environment detection regex --- app/scripts/lib/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 431d1e59c..51e9036cc 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -28,7 +28,7 @@ function getStack () { * */ const getEnvironmentType = (url = window.location.href) => { - if (url.match(/popup.html(?:\?.+)*$/)) { + if (url.match(/popup.html(?:#.*)*$/)) { return ENVIRONMENT_TYPE_POPUP } else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) { return ENVIRONMENT_TYPE_FULLSCREEN -- cgit v1.2.3 From 317c3084df9d81d372c3326aa8db1e1e6f0255e3 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 2 Jul 2018 15:14:31 -0400 Subject: allow to open specific route in fullscreen mode --- app/scripts/platforms/extension.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index f5cc255d1..f8dd767dc 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -17,8 +17,11 @@ class ExtensionPlatform { return extension.runtime.getManifest().version } - openExtensionInBrowser () { - const extensionURL = extension.runtime.getURL('home.html') + openExtensionInBrowser (route = null) { + let extensionURL = extension.runtime.getURL('home.html') + if (route) { + extensionURL += `#${route}` + } this.openWindow({ url: extensionURL }) } -- cgit v1.2.3 From f19ffaf08d49f33c395a25faf3eeb6b08d5285a4 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 2 Jul 2018 15:16:05 -0400 Subject: move hardcoded strings to localization file --- app/_locales/en/messages.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 37189ab7f..f10ba01f3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -11,6 +11,9 @@ "accountName": { "message": "Account Name" }, + "accountSelectionRequired": { + "message": "You need to select an account!" + }, "address": { "message": "Address" }, @@ -125,6 +128,12 @@ "connect": { "message": "Connect" }, + "connecting": { + "message": "Connecting..." + }, + "connectToTrezor": { + "message": "Connect to Trezor" + }, "continue": { "message": "Continue" }, @@ -618,6 +627,9 @@ "popularTokens": { "message": "Popular Tokens" }, + "prev": { + "message": "Prev" + }, "privacyMsg": { "message": "Privacy Policy" }, @@ -793,6 +805,9 @@ "searchTokens": { "message": "Search Tokens" }, + "selectAnAddress": { + "message": "Select an Address" + }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" }, -- cgit v1.2.3 From 9d3f2435e58e2454506ea1a5f7b85452a10edffa Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 3 Jul 2018 15:46:15 -0400 Subject: lint fix --- app/scripts/metamask-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 6e743d030..962611758 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -532,10 +532,10 @@ module.exports = class MetamaskController extends EventEmitter { 'Trezor Hardware' )[0] if (!keyring) { - keyring = await this.keyringController.addNewKeyring('Trezor Hardware') + keyring = await this.keyringController.addNewKeyring('Trezor Hardware') } - const accounts = page === 1 ? await keyring.getNextPage() : await keyring.getPreviousPage() + const accounts = page === 1 ? await keyring.getNextPage() : await keyring.getPreviousPage() this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) return accounts -- cgit v1.2.3 From ba5cde0995f956fb22825d604fe7d664677abaaa Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 5 Jul 2018 17:04:36 -0400 Subject: move strings to localization file --- app/_locales/en/messages.json | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f10ba01f3..9c3e43803 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -80,6 +80,9 @@ "borrowDharma": { "message": "Borrow With Dharma (Beta)" }, + "browserNotSupported": { + "message": "Bummer! Your Browser is not supported..." + }, "builtInCalifornia": { "message": "MetaMask is designed and built in California." }, @@ -107,6 +110,9 @@ "close": { "message": "Close" }, + "chromeRequiredForTrezor":{ + "message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device." + }, "confirm": { "message": "Confirm" }, @@ -259,6 +265,9 @@ "done": { "message": "Done" }, + "downloadGoogleChrome": { + "message": "Download Google Chrome" + }, "downloadStateLogs": { "message": "Download State Logs" }, -- cgit v1.2.3 From 6b2511f94f436a30c6c683f9da2c3142d9a6461c Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 5 Jul 2018 20:59:31 -0400 Subject: UI refactor --- app/scripts/metamask-controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 962611758..1246629be 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -534,8 +534,10 @@ module.exports = class MetamaskController extends EventEmitter { if (!keyring) { keyring = await this.keyringController.addNewKeyring('Trezor Hardware') } - - const accounts = page === 1 ? await keyring.getNextPage() : await keyring.getPreviousPage() + if (page === 0) { + keyring.page = 0 + } + const accounts = page === -1 ? await keyring.getPreviousPage() : await keyring.getNextPage() this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) return accounts -- cgit v1.2.3 From dddbb4250b30b7263eb97ddc2e23791166bcc98e Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 6 Jul 2018 20:04:20 -0400 Subject: update connect harwdware screen --- app/_locales/en/messages.json | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index e1f321c68..658a77e77 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -140,6 +140,12 @@ "connectToTrezor": { "message": "Connect to Trezor" }, + "connectToTrezorHelp": { + "message": "Metamask is able to access your TREZOR ethereum accounts. First make sure your device is connected and unlocked." + }, + "connectToTrezorTrouble": { + "message": "If you are having trouble, make sure you are using the latest version of the TREZOR firmware." + }, "continue": { "message": "Continue" }, @@ -944,6 +950,9 @@ "transfers": { "message": "Transfers" }, + "trezorHardwareWallet": { + "message": "TREZOR Hardware Wallet" + }, "troubleTokenBalances": { "message": "We had trouble loading your token balances. You can view them ", "description": "Followed by a link (here) to view token balances" -- cgit v1.2.3 From 512760154528c47213cc8ff75475c21e3e674a23 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 6 Jul 2018 20:37:08 -0400 Subject: copy updated --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 658a77e77..4598d14a5 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -144,7 +144,7 @@ "message": "Metamask is able to access your TREZOR ethereum accounts. First make sure your device is connected and unlocked." }, "connectToTrezorTrouble": { - "message": "If you are having trouble, make sure you are using the latest version of the TREZOR firmware." + "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware." }, "continue": { "message": "Continue" -- cgit v1.2.3 From 7cca7ace2ea4cd4b9d3a242067c9a7c344406aba Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 9 Jul 2018 17:24:52 -0400 Subject: fix all the account related bugs --- app/scripts/metamask-controller.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d70bac1c3..8104374bc 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -529,17 +529,28 @@ module.exports = class MetamaskController extends EventEmitter { switch (deviceName) { case 'trezor': const keyringController = this.keyringController + const oldAccounts = await keyringController.getAccounts() let keyring = await keyringController.getKeyringsByType( 'Trezor Hardware' )[0] if (!keyring) { keyring = await this.keyringController.addNewKeyring('Trezor Hardware') } - if (page === 0) { - keyring.page = 0 + let accounts = [] + + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() } - const accounts = page === -1 ? await keyring.getPreviousPage() : await keyring.getNextPage() - this.accountTracker.syncWithAddresses(accounts.map(a => a.address)) + + // Merge with existing accounts + this.accountTracker.syncWithAddresses(oldAccounts.concat(accounts.map(a => a.address))) return accounts default: -- cgit v1.2.3 From 2de3039b6b21ca05ef185c078b67815448864c72 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 9 Jul 2018 17:55:37 -0400 Subject: fix account duplication --- app/scripts/metamask-controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8104374bc..08b75e839 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -550,7 +550,9 @@ module.exports = class MetamaskController extends EventEmitter { } // Merge with existing accounts - this.accountTracker.syncWithAddresses(oldAccounts.concat(accounts.map(a => a.address))) + // and make sure addresses are not repeated + const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] + this.accountTracker.syncWithAddresses(accountsToTrack) return accounts default: -- cgit v1.2.3 From d3f793a44a94274c73e0ce770f34bb2e22cdbd5b Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 9 Jul 2018 19:04:30 -0400 Subject: added label for trezor accounts --- app/scripts/metamask-controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 08b75e839..71a22f6ec 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -578,7 +578,8 @@ module.exports = class MetamaskController extends EventEmitter { const oldAccounts = await keyringController.getAccounts() const keyState = await keyringController.addNewAccount(keyring) const newAccounts = await keyringController.getAccounts() - + // Assuming the trezor account is the last one + const trezorAccount = newAccounts[newAccounts.length -1] this.preferencesController.setAddresses(newAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { @@ -586,6 +587,7 @@ module.exports = class MetamaskController extends EventEmitter { } }) + this.preferencesController.setAccountLabel(trezorAccount, `TREZOR #${index + 1}`) const { identities } = this.preferencesController.store.getState() return { ...keyState, identities } } -- cgit v1.2.3 From 85a4e39b052b8e0c9d277766c79d1a2b5459d934 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 9 Jul 2018 20:54:47 -0400 Subject: fix trezor label --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 71a22f6ec..bec02c3ed 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -579,7 +579,7 @@ module.exports = class MetamaskController extends EventEmitter { const keyState = await keyringController.addNewAccount(keyring) const newAccounts = await keyringController.getAccounts() // Assuming the trezor account is the last one - const trezorAccount = newAccounts[newAccounts.length -1] + const trezorAccount = newAccounts[newAccounts.length - 1] this.preferencesController.setAddresses(newAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { -- cgit v1.2.3 From 9b81180ab10cf8ca59666104e862c0331e953591 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 10 Jul 2018 00:20:00 -0400 Subject: added ui to remove accounts --- app/_locales/en/messages.json | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 4598d14a5..303a612f1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -393,6 +393,9 @@ "message": "must be greater than or equal to $1.", "description": "helper for inputting hex as decimal input" }, + "hardware": { + "message": "hardware" + }, "here": { "message": "here", "description": "as in -click here- for more information (goes with troubleTokenBalances)" -- cgit v1.2.3 From b9c2994d24e688305d63aaefd7fac88d88773ad9 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 10 Jul 2018 19:19:29 -0400 Subject: finish warning modal UI --- app/_locales/en/messages.json | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 303a612f1..c4f78d121 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -733,6 +733,15 @@ "revert": { "message": "Revert" }, + "remove": { + "message": "remove" + }, + "removeAccount": { + "message": "Remove account?" + }, + "removeAccountDescription": { + "message": "This account will be removed from your wallet. Please make sure you have the original seed phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. " + }, "rinkeby": { "message": "Rinkeby Test Network" }, -- cgit v1.2.3 From 523cf9ad33d88719520ae5e7293329d133b64d4d Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 11 Jul 2018 00:20:40 -0400 Subject: account removal is working --- app/scripts/controllers/preferences.js | 24 ++++++++++++++++++++++++ app/scripts/metamask-controller.js | 16 +++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index b314745f5..f6250dc16 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -85,6 +85,30 @@ class PreferencesController { this.store.updateState({ identities }) } + /** + * Removes an address from state + * + * @param {string} address A hex address + * @returns {string} the address that was removed + */ + removeAddress (address) { + const identities = this.store.getState().identities + if (!identities[address]) { + throw new Error(`${address} can't be deleted cause it was not found`) + } + delete identities[address] + this.store.updateState({ identities }) + + // If the selected account is no longer valid, + // select an arbitrary other account: + if (address === this.getSelectedAddress()) { + const selected = Object.keys(identities)[0] + this.setSelectedAddress(selected) + } + return address + } + + /** * Adds addresses to the identities object without removing identities * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bec02c3ed..e8f0eba90 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -354,6 +354,7 @@ module.exports = class MetamaskController extends EventEmitter { verifySeedPhrase: nodeify(this.verifySeedPhrase, this), clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), + removeAccount: nodeify(this.removeAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), // trezor @@ -587,7 +588,8 @@ module.exports = class MetamaskController extends EventEmitter { } }) - this.preferencesController.setAccountLabel(trezorAccount, `TREZOR #${index + 1}`) + this.preferencesController.setAccountLabel(trezorAccount, `TREZOR #${parseInt(index, 10) + 1}`) + this.preferencesController.setSelectedAddress(trezorAccount) const { identities } = this.preferencesController.store.getState() return { ...keyState, identities } } @@ -705,6 +707,18 @@ module.exports = class MetamaskController extends EventEmitter { return selectedAddress } + /** + * Removes a "Loose" account from state. + * + * @param {string[]} address A hex address + * + */ + async removeAccount (address) { + this.preferencesController.removeAddress(address) + return address + } + + /** * Imports an account with the specified import strategy. * These are defined in app/scripts/account-import-strategies -- cgit v1.2.3 From 89cc48789af2bb6f0925384abe4d4a53179a3956 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 11 Jul 2018 20:01:44 -0400 Subject: update to temp dependencies --- app/scripts/metamask-controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e8f0eba90..cd6fdcc37 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -708,13 +708,18 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Removes a "Loose" account from state. + * Removes an account from state / storage. * * @param {string[]} address A hex address * */ async removeAccount (address) { + // Remove account from the preferences controller this.preferencesController.removeAddress(address) + // Remove account from the account tracker controller + this.accountTracker.removeAccount(address) + // Remove account from the keyring + await this.keyringController.removeAccount(address) return address } -- cgit v1.2.3 From 80e875308b4447ed38d7e0f677570d73956dd9de Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 11 Jul 2018 21:21:36 -0400 Subject: forget device and autiload account features added --- app/_locales/en/messages.json | 3 +++ app/scripts/metamask-controller.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c4f78d121..8e119d3e4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -339,6 +339,9 @@ "followTwitter": { "message": "Follow us on Twitter" }, + "forgetDevice": { + "message": "Forget this device" + }, "from": { "message": "From" }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index cd6fdcc37..b8b7c38e4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -357,8 +357,12 @@ module.exports = class MetamaskController extends EventEmitter { removeAccount: nodeify(this.removeAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), - // trezor + // hardware wallets connectHardware: nodeify(this.connectHardware, this), + forgetDevice: nodeify(this.forgetDevice, this), + checkHardwareStatus: nodeify(this.checkHardwareStatus, this), + + // TREZOR unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), // vault management @@ -561,6 +565,37 @@ module.exports = class MetamaskController extends EventEmitter { } } + async checkHardwareStatus (deviceName) { + + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + return false + } + return keyring.isUnlocked() + } + } + + async forgetDevice (deviceName) { + + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + return false + } + keyring.forgetDevice() + return true + } + } + /** * Imports an account from a trezor device. * -- cgit v1.2.3 From 2a0a7853249284cb27831890f3b62847ea27eb83 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 12 Jul 2018 00:23:08 -0400 Subject: added tooltip --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 8e119d3e4..14d2f923a 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -740,7 +740,7 @@ "message": "remove" }, "removeAccount": { - "message": "Remove account?" + "message": "Remove account" }, "removeAccountDescription": { "message": "This account will be removed from your wallet. Please make sure you have the original seed phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. " -- cgit v1.2.3 From 53995463883c062157a3d725e7cb8fe54486badb Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 13 Jul 2018 13:49:20 -0400 Subject: added affiliate link to trezor --- app/_locales/en/messages.json | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c40a8c3b6..b8b9d9c1c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -407,6 +407,9 @@ "message": "Get Ether from a faucet for the $1", "description": "Displays network name for Ether faucet" }, + "getYourTrezor": { + "message": "Don't have a TREZOR hardware wallet? Order yours now!" + }, "greaterThanMin": { "message": "must be greater than or equal to $1.", "description": "helper for inputting hex as decimal input" -- cgit v1.2.3 From 3ae5b4e77261035b7cfcec25c9186f6743b66026 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 13 Jul 2018 19:47:45 -0400 Subject: update label --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b8b9d9c1c..2b212a522 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -135,7 +135,7 @@ "message": "Confirm Transaction" }, "connectHardware": { - "message": "Connect Hardware" + "message": "Connect Hardware Wallet" }, "connect": { "message": "Connect" -- cgit v1.2.3 From e5512c306ded1d2a521a0ba0d2c3cdd5878e53bb Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 16 Jul 2018 19:36:08 -0400 Subject: added unit tests for metamaskcontroller --- app/scripts/metamask-controller.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b8b7c38e4..2f114e9f0 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -561,10 +561,15 @@ module.exports = class MetamaskController extends EventEmitter { return accounts default: - throw new Error('MetamaskController - Unknown device') + throw new Error('MetamaskController:connectHardware - Unknown device') } } + /** + * Check if the device is unlocked + * + * @returns {Promise} + */ async checkHardwareStatus (deviceName) { switch (deviceName) { @@ -574,12 +579,19 @@ module.exports = class MetamaskController extends EventEmitter { 'Trezor Hardware' )[0] if (!keyring) { - return false + throw new Error('MetamaskController:checkHardwareStatus - Trezor Hardware keyring not found') } return keyring.isUnlocked() + default: + throw new Error('MetamaskController:checkHardwareStatus - Unknown device') } } + /** + * Clear + * + * @returns {Promise} + */ async forgetDevice (deviceName) { switch (deviceName) { @@ -589,10 +601,12 @@ module.exports = class MetamaskController extends EventEmitter { 'Trezor Hardware' )[0] if (!keyring) { - return false + throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found') } keyring.forgetDevice() return true + default: + throw new Error('MetamaskController:forgetDevice - Unknown device') } } -- cgit v1.2.3 From de4265c629f8e68d882c2ded0e20417327cf4d2f Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 01:17:18 -0400 Subject: added more unit tests --- app/scripts/metamask-controller.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2f114e9f0..7d3f4c2a8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -628,17 +628,16 @@ module.exports = class MetamaskController extends EventEmitter { const oldAccounts = await keyringController.getAccounts() const keyState = await keyringController.addNewAccount(keyring) const newAccounts = await keyringController.getAccounts() - // Assuming the trezor account is the last one - const trezorAccount = newAccounts[newAccounts.length - 1] this.preferencesController.setAddresses(newAccounts) + console.log('new vs old', newAccounts, oldAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { + console.log('new address found', address) + this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) this.preferencesController.setSelectedAddress(address) } }) - this.preferencesController.setAccountLabel(trezorAccount, `TREZOR #${parseInt(index, 10) + 1}`) - this.preferencesController.setSelectedAddress(trezorAccount) const { identities } = this.preferencesController.store.getState() return { ...keyState, identities } } -- cgit v1.2.3 From e89350b19fdac56968303e5c48806a4605fb4b22 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 01:44:28 -0400 Subject: added tests for removeAccount --- app/scripts/metamask-controller.js | 2 -- 1 file changed, 2 deletions(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7d3f4c2a8..575c591fa 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -629,10 +629,8 @@ module.exports = class MetamaskController extends EventEmitter { const keyState = await keyringController.addNewAccount(keyring) const newAccounts = await keyringController.getAccounts() this.preferencesController.setAddresses(newAccounts) - console.log('new vs old', newAccounts, oldAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { - console.log('new address found', address) this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) this.preferencesController.setSelectedAddress(address) } -- cgit v1.2.3 From cb97517b26a7732cbb7c4a9f30f85b5fa596e608 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 18:53:37 -0400 Subject: updated account list based on new designs --- app/_locales/en/messages.json | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 564c12f86..f0927af9c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -877,6 +877,12 @@ "selectAnAddress": { "message": "Select an Address" }, + "selectAnAccount": { + "message": "Select an Account" + }, + "selectAnAccountHelp": { + "message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask." + }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" }, -- cgit v1.2.3 From aa5a987765677b4945e9eefe03cae8dcc93318cd Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 21:54:04 -0400 Subject: added some e2e tests --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 575c591fa..dc5c24b1b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -579,7 +579,7 @@ module.exports = class MetamaskController extends EventEmitter { 'Trezor Hardware' )[0] if (!keyring) { - throw new Error('MetamaskController:checkHardwareStatus - Trezor Hardware keyring not found') + return false } return keyring.isUnlocked() default: -- cgit v1.2.3 From cbb14f1d5e50c10865838a98452ecfb4b6cb8d6a Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 21:57:19 -0400 Subject: fix browser not supported screen --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 8619d6716..069db400d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -84,7 +84,7 @@ "message": "Borrow With Dharma (Beta)" }, "browserNotSupported": { - "message": "Bummer! Your Browser is not supported..." + "message": "Your Browser is not supported..." }, "builtInCalifornia": { "message": "MetaMask is designed and built in California." -- cgit v1.2.3 From 49d1bdea8a47139cc814d3c49aa97bf2d542d3d5 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 18 Jul 2018 22:57:47 -0400 Subject: design done --- app/_locales/en/messages.json | 44 +++++++++++++++++-- app/images/hardware-wallet-step-1.svg | 44 +++++++++++++++++++ app/images/hardware-wallet-step-2.svg | 81 +++++++++++++++++++++++++++++++++++ app/images/hardware-wallet-step-3.svg | 42 ++++++++++++++++++ 4 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 app/images/hardware-wallet-step-1.svg create mode 100644 app/images/hardware-wallet-step-2.svg create mode 100644 app/images/hardware-wallet-step-3.svg (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 069db400d..f9f01f040 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -134,7 +134,7 @@ "confirmTransaction": { "message": "Confirm Transaction" }, - "connectHardware": { + "connectHardwareWallet": { "message": "Connect Hardware Wallet" }, "connect": { @@ -286,6 +286,9 @@ "downloadStateLogs": { "message": "Download State Logs" }, + "dontHaveATrezorWallet": { + "message": "Don't have a TREZOR hardware wallet?" + }, "dropped": { "message": "Dropped" }, @@ -407,8 +410,8 @@ "message": "Get Ether from a faucet for the $1", "description": "Displays network name for Ether faucet" }, - "getYourTrezor": { - "message": "Don't have a TREZOR hardware wallet? Order yours now!" + "getHelp": { + "message": "Get Help." }, "greaterThanMin": { "message": "must be greater than or equal to $1.", @@ -417,6 +420,15 @@ "hardware": { "message": "hardware" }, + "hardwareSupport": { + "message": "Hardware Support" + }, + "hardwareSupportMsg": { + "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works." + }, + "havingTroubleConnecting": { + "message": "Having trouble connecting?" + }, "here": { "message": "here", "description": "as in -click here- for more information (goes with troubleTokenBalances)" @@ -515,7 +527,7 @@ "message": "Max" }, "learnMore": { - "message": "Learn more." + "message": "Learn more" }, "lessThanMax": { "message": "must be less than or equal to $1.", @@ -778,6 +790,9 @@ "removeAccountDescription": { "message": "This account will be removed from your wallet. Please make sure you have the original seed phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. " }, + "readyToConnect": { + "message": "Ready to Connect?" + }, "rinkeby": { "message": "Rinkeby Test Network" }, @@ -874,6 +889,9 @@ "message": "Only send $1 to an Ethereum account address.", "description": "displays token symbol" }, + "orderOneHere": { + "message": "Order one here." + }, "searchTokens": { "message": "Search Tokens" }, @@ -892,6 +910,24 @@ "settings": { "message": "Settings" }, + "step1HardwareWallet": { + "message": "1. Connect Hardware Wallet" + }, + "step1HardwareWalletMsg": { + "message": "Connect your hardware wallet directly to your computer." + }, + "step2HardwareWallet": { + "message": "2. Select an Account" + }, + "step2HardwareWalletMsg": { + "message": "Select the account you want to view. You can only choose one at a time." + }, + "step3HardwareWallet": { + "message": "3. Start using dApps and more!" + }, + "step3HardwareWalletMsg": { + "message": "Use your hardware account like you would with any Ethereum account. Log in to dApps, send Eth, buy and store ERC20 tokens and Non-Fungible tokens like CryptoKitties." + }, "info": { "message": "Info" }, diff --git a/app/images/hardware-wallet-step-1.svg b/app/images/hardware-wallet-step-1.svg new file mode 100644 index 000000000..2b6596a43 --- /dev/null +++ b/app/images/hardware-wallet-step-1.svg @@ -0,0 +1,44 @@ + + + + 2981A924-C7CB-4957-87AD-8C680802DAD7 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/hardware-wallet-step-2.svg b/app/images/hardware-wallet-step-2.svg new file mode 100644 index 000000000..9fff05b7e --- /dev/null +++ b/app/images/hardware-wallet-step-2.svg @@ -0,0 +1,81 @@ + + + + 27B850D0-B3BA-4F98-8BB4-B542D8BFDE3B + Created with sketchtool. + + + + + + + + + + + + + + + + + + 3 + + + OXz3…T3A4 + + + 0.020000 ETH + + + + + + + 1 + + + OXa4…s0a2 + + + 0.01500 ETH + + + + + + + 4 + + + OXd2…D0V4 + + + 0.030000 ETH + + + + + + + + + + + 2 + + + OXe7…B0a1 + + + 0.041000 ETH + + + + + + + + + \ No newline at end of file diff --git a/app/images/hardware-wallet-step-3.svg b/app/images/hardware-wallet-step-3.svg new file mode 100644 index 000000000..4a7655b3b --- /dev/null +++ b/app/images/hardware-wallet-step-3.svg @@ -0,0 +1,42 @@ + + + + CEB55C41-7BCE-405E-83CD-834B388B495F + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LOGIN WITH + METAMASK + + + + + + \ No newline at end of file -- cgit v1.2.3 From df19163bf9611d75aaf8ea6da52651dbba9a5e00 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 19 Jul 2018 02:31:13 -0400 Subject: last css fixes --- app/_locales/en/messages.json | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app') diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f9f01f040..08d1d33ae 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -420,6 +420,9 @@ "hardware": { "message": "hardware" }, + "hardwareWalletConnected": { + "message": "Hardware wallet connected" + }, "hardwareSupport": { "message": "Hardware Support" }, -- cgit v1.2.3 From cb045fd8feec88bd631329ab9b3285aeed0f2e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 20 Jul 2018 12:36:24 -0400 Subject: Auto-detect tokens #3034 (#4683) * detect tokens polling * network store to detect token * tests for spec * passtest-lint * fix lint * improve tests * detect tokens through infura * detect tokens when submit password and new account selected * keyring unlocked detect and unit tests * add changelog --- app/scripts/controllers/detect-tokens.js | 123 +++++++++++++++++++++++++++++++ app/scripts/metamask-controller.js | 12 ++- 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 app/scripts/controllers/detect-tokens.js (limited to 'app') diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js new file mode 100644 index 000000000..f1810cfa1 --- /dev/null +++ b/app/scripts/controllers/detect-tokens.js @@ -0,0 +1,123 @@ +const Web3 = require('web3') +const contracts = require('eth-contract-metadata') +const { warn } = require('loglevel') +const { MAINNET } = require('./network/enums') +// By default, poll every 3 minutes +const DEFAULT_INTERVAL = 180 * 1000 +const ERC20_ABI = [{'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'type': 'function'}] + +/** + * A controller that polls for token exchange + * rates based on a user's current token list + */ +class DetectTokensController { + /** + * Creates a DetectTokensController + * + * @param {Object} [config] - Options to configure controller + */ + constructor ({ interval = DEFAULT_INTERVAL, preferences, network, keyringMemStore } = {}) { + this.preferences = preferences + this.interval = interval + this.network = network + this.keyringMemStore = keyringMemStore + } + + /** + * For each token in eth-contract-metada, find check selectedAddress balance. + * + */ + async detectNewTokens () { + if (!this.isActive) { return } + if (this._network.store.getState().provider.type !== MAINNET) { return } + this.web3.setProvider(this._network._provider) + for (const contractAddress in contracts) { + if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) { + this.detectTokenBalance(contractAddress) + } + } + } + + /** + * Find if selectedAddress has tokens with contract in contractAddress. + * + * @param {string} contractAddress Hex address of the token contract to explore. + * @returns {boolean} If balance is detected, token is added. + * + */ + async detectTokenBalance (contractAddress) { + const ethContract = this.web3.eth.contract(ERC20_ABI).at(contractAddress) + ethContract.balanceOf(this.selectedAddress, (error, result) => { + if (!error) { + if (!result.isZero()) { + this._preferences.addToken(contractAddress, contracts[contractAddress].symbol, contracts[contractAddress].decimals) + } + } else { + warn(`MetaMask - DetectTokensController balance fetch failed for ${contractAddress}.`, error) + } + }) + } + + /** + * Restart token detection polling period and call detectNewTokens + * in case of address change or user session initialization. + * + */ + restartTokenDetection () { + if (this.isActive && this.selectedAddress) { + this.detectNewTokens() + this.interval = DEFAULT_INTERVAL + } + } + + /** + * @type {Number} + */ + set interval (interval) { + this._handle && clearInterval(this._handle) + if (!interval) { return } + this._handle = setInterval(() => { this.detectNewTokens() }, interval) + } + + /** + * In setter when selectedAddress is changed, detectNewTokens and restart polling + * @type {Object} + */ + set preferences (preferences) { + if (!preferences) { return } + this._preferences = preferences + preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) }) + preferences.store.subscribe(({ selectedAddress }) => { + if (this.selectedAddress !== selectedAddress) { + this.selectedAddress = selectedAddress + this.restartTokenDetection() + } + }) + } + + /** + * @type {Object} + */ + set network (network) { + if (!network) { return } + this._network = network + this.web3 = new Web3(network._provider) + } + + /** + * In setter when isUnlocked is updated to true, detectNewTokens and restart polling + * @type {Object} + */ + set keyringMemStore (keyringMemStore) { + if (!keyringMemStore) { return } + this._keyringMemStore = keyringMemStore + this._keyringMemStore.subscribe(({ isUnlocked }) => { + if (this.isUnlocked !== isUnlocked) { + if (isUnlocked) { this.restartTokenDetection() } + this.isUnlocked = isUnlocked + } + }) + } +} + +module.exports = DetectTokensController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index dc5c24b1b..6f5908414 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -35,6 +35,7 @@ const TypedMessageManager = require('./lib/typed-message-manager') const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') const TokenRatesController = require('./controllers/token-rates') +const DetectTokensController = require('./controllers/detect-tokens') const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') @@ -147,6 +148,13 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker.syncWithAddresses(addresses) }) + // detect tokens controller + this.detectTokensController = new DetectTokensController({ + preferences: this.preferencesController, + network: this.networkController, + keyringMemStore: this.keyringController.memStore, + }) + // address book controller this.addressBookController = new AddressBookController({ initState: initState.AddressBookController, @@ -1420,11 +1428,13 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * A method for activating the retrieval of price data, which should only be fetched when the UI is visible. + * A method for activating the retrieval of price data and auto detect tokens, + * which should only be fetched when the UI is visible. * @private * @param {boolean} active - True if price data should be getting fetched. */ set isClientOpenAndUnlocked (active) { this.tokenRatesController.isActive = active + this.detectTokensController.isActive = active } } -- cgit v1.2.3