aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorBruno <brunobar79@gmail.com>2018-06-10 15:52:32 +0800
committerBruno <brunobar79@gmail.com>2018-06-10 15:52:32 +0800
commit8c4d58aa4508e3d54c3f69847347e78d09c63b97 (patch)
treee9206c28499d7f4d66d7d407851c5efc05f615de /app
parentc0d2dab28b4083ee3ef65b6b561e28c811c6773d (diff)
downloadtangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.tar
tangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.tar.gz
tangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.tar.bz2
tangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.tar.lz
tangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.tar.xz
tangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.tar.zst
tangerine-wallet-browser-8c4d58aa4508e3d54c3f69847347e78d09c63b97.zip
initial trezor support
Diffstat (limited to 'app')
-rw-r--r--app/_locales/en/messages.json9
-rw-r--r--app/images/connect-icon.svg11
-rw-r--r--app/scripts/lib/trezor-connect.js1138
-rw-r--r--app/scripts/lib/trezorKeyring.js255
-rw-r--r--app/scripts/metamask-controller.js63
5 files changed, 1475 insertions, 1 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 457c3c3b1..339dd8da2 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -119,6 +119,12 @@
"confirmTransaction": {
"message": "Confirm Transaction"
},
+ "connectHardware": {
+ "message": "Connect Hardware"
+ },
+ "connect": {
+ "message": "Connect"
+ },
"continue": {
"message": "Continue"
},
@@ -930,6 +936,9 @@
"unknownNetworkId": {
"message": "Unknown network ID"
},
+ "unlock": {
+ "message": "Unlock"
+ },
"unlockMessage": {
"message": "The decentralized web awaits"
},
diff --git a/app/images/connect-icon.svg b/app/images/connect-icon.svg
new file mode 100644
index 000000000..84540999a
--- /dev/null
+++ b/app/images/connect-icon.svg
@@ -0,0 +1,11 @@
+<svg width="288" height="288" xmlns="http://www.w3.org/2000/svg">
+
+ <g>
+ <title>background</title>
+ <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
+ </g>
+ <g>
+ <title>Layer 1</title>
+ <path fill="#ffffff" id="svg_1" d="m122,25l15,-21c4,-5 10,-5 14,0l16,22c4,5 2,10 -5,10l-12,0l0,118c0,3 3,3 5,1l25,-25c4,-4 6,-10 6,-16l0,-24c-7,0 -12,-5 -12,-12l0,-12c0,-6 5,-12 12,-12l12,0c7,0 12,5 12,12l0,12c0,7 -5,12 -12,12l0,24c0,10 -3,18 -10,25l-31,31c-4,4 -7,6 -7,16l0,49c12,3 21,13 21,26c0,15 -12,27 -27,27s-27,-12 -27,-27c0,-13 9,-23 21,-26l0,-13c0,-10 -3,-13 -7,-17l-31,-31c-6,-6 -10,-14 -10,-24l0,-25c-7,-2 -12,-9 -12,-17c0,-10 8,-18 18,-18s18,8 18,18c0,8 -5,15 -12,17l0,25c0,7 3,12 7,16l25,25c2,2 4,2 4,-1l0,-154l-12,0c-7,0 -9,-5 -4,-11z"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/scripts/lib/trezor-connect.js b/app/scripts/lib/trezor-connect.js
new file mode 100644
index 000000000..574e88104
--- /dev/null
+++ b/app/scripts/lib/trezor-connect.js
@@ -0,0 +1,1138 @@
+/* eslint-disable */
+/* prettier-ignore */
+
+/**
+ * (C) 2017 SatoshiLabs
+ *
+ * GPLv3
+ */
+var TREZOR_CONNECT_VERSION = 4;
+
+if (!Array.isArray) {
+ Array.isArray = function(arg) {
+ return Object.prototype.toString.call(arg) === "[object Array]";
+ };
+}
+
+var HD_HARDENED = 0x80000000;
+
+// react sometimes adds some other parameters that should not be there
+function _fwStrFix(obj, fw) {
+ if (typeof fw === "string") {
+ obj.requiredFirmware = fw;
+ }
+ return obj;
+}
+
+("use strict");
+
+var chrome = window.chrome;
+var IS_CHROME_APP = chrome && chrome.app && chrome.app.window;
+
+var ERR_TIMED_OUT = "Loading timed out";
+var ERR_WINDOW_CLOSED = "Window closed";
+var ERR_WINDOW_BLOCKED = "Window blocked";
+var ERR_ALREADY_WAITING = "Already waiting for a response";
+var ERR_CHROME_NOT_CONNECTED = "Internal Chrome popup is not responding.";
+
+var DISABLE_LOGIN_BUTTONS = window.TREZOR_DISABLE_LOGIN_BUTTONS || false;
+var CHROME_URL = window.TREZOR_CHROME_URL || "./chrome/wrapper.html";
+var POPUP_ORIGIN = window.TREZOR_POPUP_ORIGIN || "https://connect.trezor.io";
+var POPUP_PATH =
+ window.TREZOR_POPUP_PATH || POPUP_ORIGIN + "/" + TREZOR_CONNECT_VERSION;
+var POPUP_URL =
+ window.TREZOR_POPUP_URL ||
+ POPUP_PATH + "/popup/popup.html?v=" + new Date().getTime();
+
+var POPUP_INIT_TIMEOUT = 15000;
+
+/**
+ * Public API.
+ */
+function TrezorConnect() {
+ var manager = new PopupManager();
+
+ /**
+ * Popup errors.
+ */
+ this.ERR_TIMED_OUT = ERR_TIMED_OUT;
+ this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED;
+ this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED;
+ this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING;
+ this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED;
+
+ /**
+ * Open the popup for further communication. All API functions open the
+ * popup automatically, but if you need to generate some parameters
+ * asynchronously, use `open` first to avoid popup blockers.
+ * @param {function(?Error)} callback
+ */
+ this.open = function(callback) {
+ var onchannel = function(result) {
+ if (result instanceof Error) {
+ callback(result);
+ } else {
+ callback();
+ }
+ };
+ manager.waitForChannel(onchannel);
+ };
+
+ /**
+ * Close the opened popup, if any.
+ */
+ this.close = function() {
+ manager.close();
+ };
+
+ /**
+ * Enable or disable closing the opened popup after a successful call.
+ * @param {boolean} value
+ */
+ this.closeAfterSuccess = function(value) {
+ manager.closeAfterSuccess = value;
+ };
+
+ /**
+ * Enable or disable closing the opened popup after a failed call.
+ * @param {boolean} value
+ */
+ this.closeAfterFailure = function(value) {
+ manager.closeAfterFailure = value;
+ };
+
+ /**
+ * Set bitcore server
+ * @param {string|Array<string>} 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<string>} value
+ */
+ this.setCurrency = function(value) {
+ if (typeof value === "string") {
+ manager.currency = value;
+ }
+ };
+
+ /**
+ * Set currency units (mBTC, BTC)
+ * @param {string|Array<string>} value
+ */
+ this.setCurrencyUnits = function(value) {
+ if (typeof value === "string") {
+ manager.currencyUnits = value;
+ }
+ };
+
+ /**
+ * Set coin info json url
+ * @param {string|Array<string>} 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<number>)} path
+ * @param {function(XPubKeyResult)} callback
+ * @param {?(string|array<number>)} 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<string>} signatures array of input signatures, in hex
+ */
+
+ /**
+ * Sign a transaction in the device and return both serialized
+ * transaction and the signatures.
+ *
+ * @param {array<TxInputType>} inputs
+ * @param {array<TxOutputType>} outputs
+ * @param {function(SignTxResult)} callback
+ * @param {?(string|array<number>)} 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<TxRecipient>} recipients
+ * @param {function(SignTxResult)} callback
+ * @param {?(string|array<number>)} 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<number>)} 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 <trezor:login> 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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<number>)} 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 =
+ '<style>@import url("@connect_path@/login_buttons.css")</style>';
+
+ var LOGIN_ONCLICK =
+ "TrezorConnect.requestLogin(" +
+ "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" +
+ ")";
+
+ var LOGIN_HTML =
+ '<div id="trezorconnect-wrapper">' +
+ ' <a id="trezorconnect-button" onclick="' +
+ LOGIN_ONCLICK +
+ '">' +
+ ' <span id="trezorconnect-icon"></span>' +
+ ' <span id="trezorconnect-text">@text@</span>' +
+ " </a>" +
+ ' <span id="trezorconnect-info">' +
+ ' <a id="trezorconnect-infolink" href="https://www.buytrezor.com/"' +
+ ' target="_blank">What is TREZOR?</a>' +
+ " </span>" +
+ "</div>";
+
+ /**
+ * Find <trezor:login> 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", "<strong>TREZOR</strong>");
+ 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),
@@ -524,6 +531,60 @@ module.exports = class MetamaskController extends EventEmitter {
}
//
+ // 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
//