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/scripts/lib/trezor-connect.js | 1138 ++++++++++++++++++++++++++++++++++++ app/scripts/lib/trezorKeyring.js | 255 ++++++++ app/scripts/metamask-controller.js | 63 +- 3 files changed, 1455 insertions(+), 1 deletion(-) create mode 100644 app/scripts/lib/trezor-connect.js create mode 100644 app/scripts/lib/trezorKeyring.js (limited to 'app/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts/metamask-controller.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'app/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 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/scripts') 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 From cb045fd8feec88bd631329ab9b3285aeed0f2e97 Mon Sep 17 00:00:00 2001 From: Esteban Miño 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/scripts') 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