diff options
author | Dan Finlay <542863+danfinlay@users.noreply.github.com> | 2018-08-18 02:26:38 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-08-18 02:26:38 +0800 |
commit | c7f5436143a87369ab29c019a1c988c20a98d810 (patch) | |
tree | a92c329bc53f99f88fdd0a72e522dc94f4dfb83e | |
parent | 755369e4e48e71b04b4dbb9cb702353c09b66e28 (diff) | |
parent | bd904c86f6f27eb50e0633f46af6831869b99893 (diff) | |
download | tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.tar tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.tar.gz tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.tar.bz2 tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.tar.lz tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.tar.xz tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.tar.zst tangerine-wallet-browser-c7f5436143a87369ab29c019a1c988c20a98d810.zip |
Merge pull request #5050 from MetaMask/ledger-support
Add Ledger hardware wallet support
-rw-r--r-- | app/_locales/en/messages.json | 38 | ||||
-rw-r--r-- | app/images/ledger-logo.svg | 1 | ||||
-rw-r--r-- | app/images/trezor-logo.svg | 1 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 138 | ||||
-rw-r--r-- | package-lock.json | 104 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | test/e2e/beta/from-import-beta-ui.spec.js | 5 | ||||
-rw-r--r-- | test/unit/app/controllers/metamask-controller-test.js | 36 | ||||
-rw-r--r-- | ui/app/actions.js | 29 | ||||
-rw-r--r-- | ui/app/components/account-menu/index.js | 1 | ||||
-rw-r--r-- | ui/app/components/modals/account-details-modal.js | 16 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/connect-hardware/account-list.js | 73 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/connect-hardware/connect-screen.js | 87 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/connect-hardware/index.js | 91 | ||||
-rw-r--r-- | ui/app/components/wallet-view.js | 16 | ||||
-rw-r--r-- | ui/app/css/itcss/components/new-account.scss | 105 | ||||
-rw-r--r-- | ui/app/reducers/app.js | 13 | ||||
-rw-r--r-- | ui/app/selectors/confirm-transaction.js | 2 |
18 files changed, 559 insertions, 198 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ddfcf6f12..a25a2bd59 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -119,8 +119,8 @@ "close": { "message": "Close" }, - "chromeRequiredForTrezor":{ - "message": "You need to use MetaMask on Google Chrome in order to connect to your TREZOR device." + "chromeRequiredForHardwareWallets":{ + "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, "confirm": { "message": "Confirm" @@ -146,15 +146,12 @@ "connecting": { "message": "Connecting..." }, + "connectToLedger": { + "message": "Connect to Ledger" + }, "connectToTrezor": { "message": "Connect to Trezor" }, - "connectToTrezorHelp": { - "message": "MetaMask is able to access your TREZOR Ethereum accounts. First make sure your device is connected and unlocked." - }, - "connectToTrezorTrouble": { - "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware." - }, "continue": { "message": "Continue" }, @@ -289,8 +286,8 @@ "downloadStateLogs": { "message": "Download State Logs" }, - "dontHaveATrezorWallet": { - "message": "Don't have a TREZOR hardware wallet?" + "dontHaveAHardwareWallet": { + "message": "Don’t have a hardware wallet?" }, "dropped": { "message": "Dropped" @@ -426,11 +423,11 @@ "hardwareWalletConnected": { "message": "Hardware wallet connected" }, - "hardwareSupport": { - "message": "Hardware Support" + "hardwareWallets": { + "message": "Connect a hardware wallet" }, - "hardwareSupportMsg": { - "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works." + "hardwareWalletsMsg": { + "message": "Select a hardware wallet you'd like to use with MetaMask" }, "havingTroubleConnecting": { "message": "Having trouble connecting?" @@ -538,6 +535,9 @@ "learnMore": { "message": "Learn more" }, + "ledgerAccountRestriction": { + "message": "You need to make use your last account before you can add a new one." + }, "lessThanMax": { "message": "must be less than or equal to $1.", "description": "helper for inputting hex as decimal input" @@ -908,7 +908,7 @@ "description": "displays token symbol" }, "orderOneHere": { - "message": "Order one here." + "message": "Order a Trezor or Ledger and keep your funds in cold storage" }, "searchTokens": { "message": "Search Tokens" @@ -920,7 +920,13 @@ "message": "Select an Account" }, "selectAnAccountHelp": { - "message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask." + "message": "Select the account to view in MetaMask" + }, + "selectHdPath": { + "message": "Select HD Path" + }, + "selectPathHelp": { + "message": "If you don't see your existing Ledger accounts below, try switching paths to \"Legacy (MEW / MyCrypto)\"" }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" diff --git a/app/images/ledger-logo.svg b/app/images/ledger-logo.svg new file mode 100644 index 000000000..21b99d0e5 --- /dev/null +++ b/app/images/ledger-logo.svg @@ -0,0 +1 @@ +<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1916.3 516.8" width="2500" height="674"><style>.st0{fill:#333745}</style><g id="squares_1_"><path class="st0" d="M578.2 392.7V24.3h25.6v344.1h175.3v24.3H578.2zm327.5 5.1c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5H804.6c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.3 2.6-28.9 3.9-43.5 3.8zM898 135.6c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm238-21.8c19.2 0 37.1 3.8 51.2 10.2 14.1 7.7 26.9 19.2 38.4 37.1h1.3c-1.3-21.7-1.3-42.2-1.3-62.7V0h24.3v392.7h-16.6l-6.4-42.2c-20.5 30.7-51.2 47.3-89.6 47.3s-66.5-11.5-87-35.8c-20.5-23-29.4-57.6-29.4-102.3 0-47.3 10.2-83.2 29.4-108.7 19.2-25.6 48.6-37.2 85.7-37.2zm0 21.8c-29.4 0-52.4 10.2-67.8 32-15.3 20.5-23 51.2-23 92.1 0 78 30.7 116.4 90.8 116.4 30.7 0 53.7-9 67.8-26.9 14.1-17.9 21.7-47.3 21.7-89.6v-3.8c0-42.2-7.7-72.9-21.7-90.8-12.8-20.5-35.8-29.4-67.8-29.4zm379.9-16.6v17.9l-56.3 3.8c15.3 19.2 23 39.7 23 61.4 0 26.9-9 47.3-26.9 64-17.9 16.6-40.9 24.3-70.4 24.3-12.8 0-21.7 0-25.6-1.3-10.2 5.1-17.9 11.5-23 17.9-5.1 7.7-7.7 14.1-7.7 23s3.8 15.3 10.2 19.2c6.4 3.8 17.9 6.4 33.3 6.4h47.3c29.4 0 52.4 6.4 67.8 17.9s24.3 29.4 24.3 53.7c0 29.4-11.5 51.2-34.5 66.5-23 15.3-56.3 23-99.8 23-34.5 0-61.4-6.4-80.6-20.5-19.2-12.8-28.1-32-28.1-55 0-19.2 6.4-34.5 17.9-47.3s28.1-20.5 47.3-25.6c-7.7-3.8-15.3-9-19.2-15.3-5-6.2-7.7-13.8-7.7-21.7 0-17.9 11.5-34.5 34.5-48.6-15.3-6.4-28.1-16.6-37.1-30.7-9-14.1-12.8-30.7-12.8-48.6 0-26.9 9-49.9 25.6-66.5 17.9-16.6 40.9-24.3 70.4-24.3 17.9 0 32 1.3 42.2 5.1h85.7v1.3h.2zm-222.6 319.8c0 37.1 28.1 56.3 84.4 56.3 71.6 0 107.5-23 107.5-69.1 0-16.6-5.1-28.1-16.6-35.8-11.5-7.7-29.4-11.5-55-11.5h-44.8c-49.9 1.2-75.5 20.4-75.5 60.1zm21.8-235.4c0 21.7 6.4 37.1 19.2 49.9 12.8 11.5 29.4 17.9 51.2 17.9 23 0 40.9-6.4 52.4-17.9 12.8-11.5 17.9-28.1 17.9-49.9 0-23-6.4-40.9-19.2-52.4-12.8-11.5-29.4-17.9-52.4-17.9-21.7 0-39.7 6.4-51.2 19.2-12.8 11.4-17.9 29.3-17.9 51.1z"/><path class="st0" d="M1640 397.8c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5h-197c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.1 2.6-28.2 3.8-44.8 3.8zm-6.4-262.2c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm245.6-21.8c11.5 0 24.3 1.3 37.1 3.8l-5.1 24.3c-11.8-2.6-23.8-3.9-35.8-3.8-23 0-42.2 10.2-57.6 29.4-15.3 20.5-23 44.8-23 75.5v149.7h-25.6V119h21.7l2.6 49.9h1.3c11.5-20.5 23-34.5 35.8-42.2 15.4-9 30.7-12.9 48.6-12.9zM333.9 12.8h-183v245.6h245.6V76.7c.1-34.5-28.1-63.9-62.6-63.9zm-239.2 0H64c-34.5 0-64 28.1-64 64v30.7h94.7V12.8zM0 165h94.7v94.7H0V165zm301.9 245.6h30.7c34.5 0 64-28.1 64-64V316h-94.7v94.6zm-151-94.6h94.7v94.7h-94.7V316zM0 316v30.7c0 34.5 28.1 64 64 64h30.7V316H0z"/></g></svg>
\ No newline at end of file diff --git a/app/images/trezor-logo.svg b/app/images/trezor-logo.svg new file mode 100644 index 000000000..b8d85e3af --- /dev/null +++ b/app/images/trezor-logo.svg @@ -0,0 +1 @@ +<svg id="trezor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2567.5 722.3" width="2567.5" height="722.3"><style>.st0{display:none;fill:#fff;stroke:#000}</style><path id="rect25" class="st0" d="M1186 2932.6h46.2v147H1186v-147z"/><path id="path7" d="M249 0C149.9 0 69.7 80.2 69.7 179.3v67.2C34.9 252.8 0 261.2 0 272.1v350.7s0 9.7 10.9 14.3c39.5 16 194.9 71 230.6 83.6 4.6 1.7 5.9 1.7 7.1 1.7 1.7 0 2.5 0 7.1-1.7 35.7-12.6 191.5-67.6 231-83.6 10.1-4.2 10.5-13.9 10.5-13.9V272.1c0-10.9-34.4-19.7-69.3-25.6v-67.2C428.4 80.2 347.7 0 249 0zm0 85.7c58.4 0 93.7 35.3 93.7 93.7v58.4c-65.5-4.6-121.4-4.6-187.3 0v-58.4c0-58.5 35.3-93.7 93.6-93.7zm-.4 238.1c81.5 0 149.9 6.3 149.9 17.6v218.8c0 3.4-.4 3.8-3.4 5-2.9 1.3-139 50.4-139 50.4s-5.5 1.7-7.1 1.7c-1.7 0-7.1-2.1-7.1-2.1s-136.1-49.1-139-50.4-3.4-1.7-3.4-5V341c-.8-11.3 67.6-17.2 149.1-17.2z"/><g id="g3222" transform="translate(91.363 -287.434) scale(.95575)"><path id="path13" d="M666.6 890V639.3H575v-89.9h285.6v89.9h-90.7V890H666.6z"/><path id="path15" d="M1092 890l-47-107.1h-37.4V890H904.3V549.4h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5H1092zm12.2-223.9c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.2-.4 33.6-8.4 33.6-27.3z"/><path id="path17" d="M1262.9 890V549.4h258.3v89.9h-155.4v33.6h151.6v89.9h-151.6v37.4h155.4V890h-258.3z"/><path id="path19" d="M1574.9 890.4v-81.9l129.8-168.8h-129.8v-89.9h265.8v81.1l-130.2 169.7h134v89.9l-269.6-.1z"/><path id="path21" d="M1869.7 720.3c0-104.6 81.1-176.4 186.5-176.4 105 0 186.5 71.4 186.5 176.4 0 104.6-81.1 176-186.5 176s-186.5-71.4-186.5-176zm268 0c0-47.5-32.3-85.3-81.9-85.3-49.6 0-81.9 37.8-81.9 85.3s32.3 85.3 81.9 85.3c50 0 81.9-37.8 81.9-85.3z"/><path id="path23" d="M2473.6 890.4l-47-107.1h-37.4v107.1h-103.3V549.8h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5h-117.1zm12.6-224.3c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.3-.4 33.6-8.4 33.6-27.3z"/></g></svg>
\ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 166be720f..2d7d2c671 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -48,6 +48,7 @@ const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') +const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') const EthQuery = require('eth-query') module.exports = class MetamaskController extends EventEmitter { @@ -128,7 +129,7 @@ module.exports = class MetamaskController extends EventEmitter { }) // key mgmt - const additionalKeyrings = [TrezorKeyring] + const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, initState: initState.KeyringController, @@ -372,9 +373,7 @@ module.exports = class MetamaskController extends EventEmitter { connectHardware: nodeify(this.connectHardware, this), forgetDevice: nodeify(this.forgetDevice, this), checkHardwareStatus: nodeify(this.checkHardwareStatus, this), - - // TREZOR - unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this), // vault management submitPassword: nodeify(this.submitPassword, this), @@ -579,45 +578,57 @@ module.exports = class MetamaskController extends EventEmitter { // Hardware // + async getKeyringForDevice (deviceName, hdPath = null) { + let keyringName = null + switch (deviceName) { + case 'trezor': + keyringName = TrezorKeyring.type + break + case 'ledger': + keyringName = LedgerBridgeKeyring.type + break + default: + throw new Error('MetamaskController:getKeyringForDevice - Unknown device') + } + let keyring = await this.keyringController.getKeyringsByType(keyringName)[0] + if (!keyring) { + keyring = await this.keyringController.addNewKeyring(keyringName) + } + if (hdPath && keyring.setHdPath) { + keyring.setHdPath(hdPath) + } + + keyring.network = this.networkController.getProviderConfig().type + + return keyring + + } + /** * Fetch account list from a trezor device. * * @returns [] accounts */ - async connectHardware (deviceName, page) { - - 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') - } - let accounts = [] - - switch (page) { - case -1: - accounts = await keyring.getPreviousPage() - break - case 1: - accounts = await keyring.getNextPage() - break - default: - accounts = await keyring.getFirstPage() - } - - // Merge with existing accounts - // 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: - throw new Error('MetamaskController:connectHardware - Unknown device') + async connectHardware (deviceName, page, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + let accounts = [] + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() } + + // Merge with existing accounts + // and make sure addresses are not repeated + const oldAccounts = await this.keyringController.getAccounts() + const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] + this.accountTracker.syncWithAddresses(accountsToTrack) + return accounts } /** @@ -625,21 +636,9 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {Promise<boolean>} */ - 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() - default: - throw new Error('MetamaskController:checkHardwareStatus - Unknown device') - } + async checkHardwareStatus (deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + return keyring.isUnlocked() } /** @@ -649,20 +648,9 @@ module.exports = class MetamaskController extends EventEmitter { */ async forgetDevice (deviceName) { - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found') - } - keyring.forgetDevice() - return true - default: - throw new Error('MetamaskController:forgetDevice - Unknown device') - } + const keyring = await this.getKeyringForDevice(deviceName) + keyring.forgetDevice() + return true } /** @@ -670,23 +658,17 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {} keyState */ - async unlockTrezorAccount (index) { - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController - No Trezor Hardware Keyring found') - } + async unlockHardwareWalletAccount (index, deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) keyring.setAccountToUnlock(index) - const oldAccounts = await keyringController.getAccounts() - const keyState = await keyringController.addNewAccount(keyring) - const newAccounts = await keyringController.getAccounts() + const oldAccounts = await this.keyringController.getAccounts() + const keyState = await this.keyringController.addNewAccount(keyring) + const newAccounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(newAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { - this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) + this.preferencesController.setAccountLabel(address, `${deviceName.toUpperCase()} ${parseInt(index, 10) + 1}`) this.preferencesController.setSelectedAddress(address) } }) diff --git a/package-lock.json b/package-lock.json index f6d25d750..ec0192500 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1585,9 +1585,9 @@ } }, "@zxing/library": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.7.0.tgz", - "integrity": "sha512-VJ1cJaCWVF8MspnuyaZKGKlrSQLqQ5usgSap8uuCAvWGQ6W6OwN1NeGvnjhT+9hmnwkHK8XjaflvzaDBC7nKnw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.8.0.tgz", + "integrity": "sha512-D7oopukr7cJ0Va01Er2zXiSPXvmvc6D1PpOq/THRvd/57yEsBs+setRsiDo7tSRnYHcw7FrRZSZ7rwyzNSLJeA==", "requires": { "text-encoding": "^0.6.4", "ts-custom-error": "^2.2.1" @@ -8372,6 +8372,63 @@ } } }, + "eth-ledger-bridge-keyring": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.1.0.tgz", + "integrity": "sha512-fZQry1rxA23swq7Qw9JolFltRePwIbKXCn9Vo6Qfr122cqqA3MBzV3WSI+ABQvwf3obQrMpbtqP5tiRxpX/0Vg==", + "requires": { + "eth-sig-util": "^1.4.2", + "ethereumjs-tx": "^1.3.4", + "ethereumjs-util": "^5.1.5", + "events": "^2.0.0", + "hdkey": "0.8.0" + }, + "dependencies": { + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + }, + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + } + }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==" + }, + "hdkey": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-0.8.0.tgz", + "integrity": "sha512-oYsdlK22eobT68N5faWI3776f6tOLyqxLLYwxMx+TP0rkWzuCs0oiOm2VbLWcxdpHFP4LtiRR8udaIX8VkEaZQ==", + "requires": { + "coinstring": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, "eth-lib": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.27.tgz", @@ -8511,13 +8568,12 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -8556,8 +8612,17 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereumjs-abi": { @@ -8868,6 +8933,20 @@ } } }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, "ethereumjs-vm": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.3.4.tgz", @@ -29504,7 +29583,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, "requires": { "is-typedarray": "^1.0.0" } @@ -30494,7 +30572,6 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz", "integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=", "requires": { - "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2": "*", @@ -30503,7 +30580,7 @@ "dependencies": { "bignumber.js": { "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" } } }, @@ -30901,8 +30978,7 @@ "dev": true, "requires": { "underscore": "1.8.3", - "web3-core-helpers": "1.0.0-beta.34", - "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" + "web3-core-helpers": "1.0.0-beta.34" }, "dependencies": { "underscore": { @@ -30913,8 +30989,7 @@ }, "websocket": { "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", - "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible", - "dev": true, + "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", "requires": { "debug": "^2.2.0", "nan": "^2.3.3", @@ -31503,8 +31578,7 @@ "yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", - "dev": true + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" }, "yallist": { "version": "2.1.2", diff --git a/package.json b/package.json index 517f40791..1e0215a8d 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "eth-hd-keyring": "^1.2.2", "eth-json-rpc-filters": "^2.1.1", "eth-json-rpc-infura": "^3.0.0", + "eth-ledger-bridge-keyring": "^0.1.0", "eth-method-registry": "^1.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index e14ee2361..1261b6f95 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -366,7 +366,10 @@ describe('Using MetaMask with an existing account', function () { }) it('should open the TREZOR Connect popup', async () => { - const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect to Trezor')]`)) + const trezorButton = await findElements(driver, By.css('.hw-connect__btn')) + await trezorButton[1].click() + await delay(regularDelayMs) + const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) await connectButtons[0].click() await delay(regularDelayMs) const allWindows = await driver.getAllWindowHandles() diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index e51cd66aa..1fc604c9c 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -286,9 +286,9 @@ describe('MetaMaskController', function () { it('should throw if it receives an unknown device name', async function () { try { - await metamaskController.connectHardware('Some random device name', 0) + await metamaskController.connectHardware('Some random device name', 0, `m/44/0'/0'`) } catch (e) { - assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device') + assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device') } }) @@ -302,14 +302,24 @@ describe('MetaMaskController', function () { assert.equal(keyrings.length, 1) }) + it('should add the Ledger Hardware keyring', async function () { + sinon.spy(metamaskController.keyringController, 'addNewKeyring') + await metamaskController.connectHardware('ledger', 0).catch((e) => null) + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Ledger Hardware' + ) + assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Ledger Hardware') + assert.equal(keyrings.length, 1) + }) + }) describe('checkHardwareStatus', function () { it('should throw if it receives an unknown device name', async function () { try { - await metamaskController.checkHardwareStatus('Some random device name') + await metamaskController.checkHardwareStatus('Some random device name', `m/44/0'/0'`) } catch (e) { - assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device') + assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device') } }) @@ -325,7 +335,7 @@ describe('MetaMaskController', function () { try { await metamaskController.forgetDevice('Some random device name') } catch (e) { - assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device') + assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device') } }) @@ -342,7 +352,7 @@ describe('MetaMaskController', function () { }) }) - describe('unlockTrezorAccount', function () { + describe('unlockHardwareWalletAccount', function () { let accountToUnlock let windowOpenStub let addNewAccountStub @@ -365,16 +375,20 @@ describe('MetaMaskController', function () { sinon.spy(metamaskController.preferencesController, 'setAddresses') sinon.spy(metamaskController.preferencesController, 'setSelectedAddress') sinon.spy(metamaskController.preferencesController, 'setAccountLabel') - await metamaskController.connectHardware('trezor', 0).catch((e) => null) - await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null) + await metamaskController.connectHardware('trezor', 0, `m/44/0'/0'`).catch((e) => null) + await metamaskController.unlockHardwareWalletAccount(accountToUnlock, 'trezor', `m/44/0'/0'`) }) afterEach(function () { - metamaskController.keyringController.addNewAccount.restore() window.open.restore() + metamaskController.keyringController.addNewAccount.restore() + metamaskController.keyringController.getAccounts.restore() + metamaskController.preferencesController.setAddresses.restore() + metamaskController.preferencesController.setSelectedAddress.restore() + metamaskController.preferencesController.setAccountLabel.restore() }) - it('should set accountToUnlock in the keyring', async function () { + it('should set unlockedAccount in the keyring', async function () { const keyrings = await metamaskController.keyringController.getKeyringsByType( 'Trezor Hardware' ) @@ -382,7 +396,7 @@ describe('MetaMaskController', function () { }) - it('should call keyringController.addNewAccount', async function () { + it('should call keyringController.addNewAccount', async function () { assert(metamaskController.keyringController.addNewAccount.calledOnce) }) diff --git a/ui/app/actions.js b/ui/app/actions.js index bd5d25327..6bcc64e17 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -91,7 +91,7 @@ var actions = { connectHardware, checkHardwareStatus, forgetDevice, - unlockTrezorAccount, + unlockHardwareWalletAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, @@ -235,6 +235,8 @@ var actions = { UPDATE_TOKENS: 'UPDATE_TOKENS', setRpcTarget: setRpcTarget, setProviderType: setProviderType, + SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', + setHardwareWalletDefaultHdPath, updateProviderType, // loading overlay SHOW_LOADING: 'SHOW_LOADING_INDICATION', @@ -639,12 +641,12 @@ function addNewAccount () { } } -function checkHardwareStatus (deviceName) { - log.debug(`background.checkHardwareStatus`, deviceName) +function checkHardwareStatus (deviceName, hdPath) { + log.debug(`background.checkHardwareStatus`, deviceName, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.checkHardwareStatus(deviceName, (err, unlocked) => { + background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -681,12 +683,12 @@ function forgetDevice (deviceName) { } } -function connectHardware (deviceName, page) { - log.debug(`background.connectHardware`, deviceName, page) +function connectHardware (deviceName, page, hdPath) { + log.debug(`background.connectHardware`, deviceName, page, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.connectHardware(deviceName, page, (err, accounts) => { + background.connectHardware(deviceName, page, hdPath, (err, accounts) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -702,12 +704,12 @@ function connectHardware (deviceName, page) { } } -function unlockTrezorAccount (index) { - log.debug(`background.unlockTrezorAccount`, index) +function unlockHardwareWalletAccount (index, deviceName, hdPath) { + log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.unlockTrezorAccount(index, (err, accounts) => { + background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -1854,6 +1856,13 @@ function showLoadingIndication (message) { } } +function setHardwareWalletDefaultHdPath ({ device, path }) { + return { + type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH, + value: {device, path}, + } +} + function hideLoadingIndication () { return { type: actions.HIDE_LOADING, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 9c063d31e..bcada41e3 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -229,6 +229,7 @@ AccountMenu.prototype.renderKeyringType = function (keyring) { let label switch (type) { case 'Trezor Hardware': + case 'Ledger Hardware': label = this.context.t('hardware') break case 'Simple Key Pair': diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index 5607cf051..cc90cf578 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -14,6 +14,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), + keyrings: state.metamask.keyrings, } } @@ -50,9 +51,20 @@ AccountDetailsModal.prototype.render = function () { network, showExportPrivateKeyModal, setAccountLabel, + keyrings, } = this.props const { name, address } = selectedIdentity + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(address) + }) + + let exportPrivateKeyFeatureEnabled = true + // This feature is disabled for hardware wallets + if (keyring.type.search('Hardware') !== -1) { + exportPrivateKeyFeatureEnabled = false + } + return h(AccountModalContainer, {}, [ h(EditableLabel, { className: 'account-modal__name', @@ -73,9 +85,9 @@ AccountDetailsModal.prototype.render = function () { }, this.context.t('etherscanView')), // Holding on redesign for Export Private Key functionality - h('button.btn-primary.account-modal__button', { + exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', { onClick: () => showExportPrivateKeyModal(), - }, this.context.t('exportPrivateKey')), + }, this.context.t('exportPrivateKey')) : null, ]) } diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index c722d1f55..488a189ea 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -2,16 +2,75 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const genAccountLink = require('../../../../../lib/account-link.js') +const Select = require('react-select').default class AccountList extends Component { constructor (props, context) { super(props) } + getHdPaths () { + return [ + { + label: `Ledger Live`, + value: `m/44'/60'/0'/0/0`, + }, + { + label: `Legacy (MEW / MyCrypto)`, + value: `m/44'/60'/0'`, + }, + ] + } + + goToNextPage = () => { + // If we have < 5 accounts, it's restricted by BIP-44 + if (this.props.accounts.length === 5) { + this.props.getPage(this.props.device, 1, this.props.selectedPath) + } else { + this.props.onAccountRestriction() + } + } + + goToPreviousPage = () => { + this.props.getPage(this.props.device, -1, this.props.selectedPath) + } + + renderHdPathSelector () { + const { onPathChange, selectedPath } = this.props + + const options = this.getHdPaths() + return h('div', [ + h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')), + h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')), + h('div.hw-connect__hdPath', [ + h(Select, { + className: 'hw-connect__hdPath__select', + name: 'hd-path-select', + clearable: false, + value: selectedPath, + options, + onChange: (opt) => { + onPathChange(opt.value) + }, + }), + ]), + ]) + } + + capitalizeDevice (device) { + return device.slice(0, 1).toUpperCase() + device.slice(1) + } + renderHeader () { + const { device } = this.props return ( h('div.hw-connect', [ - h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')), + + h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`), + + device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null, + + h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')), h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')), ]) ) @@ -61,7 +120,7 @@ class AccountList extends Component { h( 'button.hw-list-pagination__button', { - onClick: () => this.props.getPage(-1), + onClick: this.goToPreviousPage, }, `< ${this.context.t('prev')}` ), @@ -69,7 +128,7 @@ class AccountList extends Component { h( 'button.hw-list-pagination__button', { - onClick: () => this.props.getPage(1), + onClick: this.goToNextPage, }, `${this.context.t('next')} >` ), @@ -95,7 +154,7 @@ class AccountList extends Component { h( `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`, { - onClick: this.props.onUnlockAccount.bind(this), + onClick: this.props.onUnlockAccount.bind(this, this.props.device), ...buttonProps, }, [this.context.t('unlock')] @@ -106,7 +165,7 @@ class AccountList extends Component { renderForgetDevice () { return h('div.hw-forget-device-container', {}, [ h('a', { - onClick: this.props.onForgetDevice.bind(this), + onClick: this.props.onForgetDevice.bind(this, this.props.device), }, this.context.t('forgetDevice')), ]) } @@ -125,6 +184,9 @@ class AccountList extends Component { AccountList.propTypes = { + onPathChange: PropTypes.func.isRequired, + selectedPath: PropTypes.string.isRequired, + device: PropTypes.string.isRequired, accounts: PropTypes.array.isRequired, onAccountChange: PropTypes.func.isRequired, onForgetDevice: PropTypes.func.isRequired, @@ -134,6 +196,7 @@ AccountList.propTypes = { history: PropTypes.object, onUnlockAccount: PropTypes.func, onCancel: PropTypes.func, + onAccountRestriction: PropTypes.func, } AccountList.contextTypes = { diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index cb2b86595..b3dfa4ee2 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -5,6 +5,52 @@ const h = require('react-hyperscript') class ConnectScreen extends Component { constructor (props, context) { super(props) + this.state = { + selectedDevice: null, + } + } + + connect = () => { + if (this.state.selectedDevice) { + this.props.connectToHardwareWallet(this.state.selectedDevice) + } + return null + } + + renderConnectToTrezorButton () { + return h( + `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`, + { onClick: _ => this.setState({selectedDevice: 'trezor'}) }, + h('img.hw-connect__btn__img', { + src: 'images/trezor-logo.svg', + }) + ) + } + + renderConnectToLedgerButton () { + return h( + `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`, + { onClick: _ => this.setState({selectedDevice: 'ledger'}) }, + h('img.hw-connect__btn__img', { + src: 'images/ledger-logo.svg', + }) + ) + } + + renderButtons () { + return ( + h('div', {}, [ + h('div.hw-connect__btn-wrapper', {}, [ + this.renderConnectToLedgerButton(), + this.renderConnectToTrezorButton(), + ]), + h( + `button.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`, + { onClick: this.connect }, + this.context.t('connect') + ), + ]) + ) } renderUnsupportedBrowser () { @@ -12,7 +58,7 @@ class ConnectScreen extends Component { h('div.new-account-connect-form.unsupported-browser', {}, [ h('div.hw-connect', [ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), - h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')), + h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')), ]), h( 'button.btn-primary.btn--large', @@ -30,29 +76,31 @@ class ConnectScreen extends Component { renderHeader () { return ( h('div.hw-connect__header', {}, [ - h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)), - h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)), + h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)), + h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)), ]) ) } + getAffiliateLinks () { + const links = { + trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`, + ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`, + } + + const text = this.context.t('orderOneHere') + const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger) + + return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }}) + } + renderTrezorAffiliateLink () { - return h('div.hw-connect__get-trezor', {}, [ - h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)), - h('a.hw-connect__get-trezor__link', { - href: 'https://shop.trezor.io/?a=metamask', - target: '_blank', - }, this.context.t('orderOneHere')), + return h('div.hw-connect__get-hw', {}, [ + h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)), + this.getAffiliateLinks(), ]) } - renderConnectToTrezorButton () { - return h( - 'button.btn-primary.btn--large', - { onClick: this.props.connectToTrezor.bind(this) }, - this.props.btnText - ) - } scrollToTutorial = (e) => { if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) @@ -102,7 +150,7 @@ class ConnectScreen extends Component { return ( h('div.hw-connect__footer', {}, [ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)), - this.renderConnectToTrezorButton(), + this.renderButtons(), h('p.hw-connect__footer__msg', {}, [ this.context.t(`havingTroubleConnecting`), h('a.hw-connect__footer__link', { @@ -118,8 +166,8 @@ class ConnectScreen extends Component { return ( h('div.new-account-connect-form', {}, [ this.renderHeader(), + this.renderButtons(), this.renderTrezorAffiliateLink(), - this.renderConnectToTrezorButton(), this.renderLearnMore(), this.renderTutorialSteps(), this.renderFooter(), @@ -136,8 +184,7 @@ class ConnectScreen extends Component { } ConnectScreen.propTypes = { - connectToTrezor: PropTypes.func.isRequired, - btnText: PropTypes.string.isRequired, + connectToHardwareWallet: PropTypes.func.isRequired, browserSupported: PropTypes.bool.isRequired, } diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 3f66e7098..547df5223 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -7,17 +7,19 @@ const ConnectScreen = require('./connect-screen') const AccountList = require('./account-list') const { DEFAULT_ROUTE } = require('../../../../routes') const { formatBalance } = require('../../../../util') +const { getPlatform } = require('../../../../../../app/scripts/lib/util') +const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums') class ConnectHardwareForm extends Component { constructor (props, context) { super(props) this.state = { error: null, - btnText: context.t('connectToTrezor'), selectedAccount: null, accounts: [], browserSupported: true, unlocked: false, + device: null, } } @@ -38,25 +40,44 @@ class ConnectHardwareForm extends Component { } async checkIfUnlocked () { - const unlocked = await this.props.checkHardwareStatus('trezor') - if (unlocked) { - this.setState({unlocked: true}) - this.getPage(0) - } + ['trezor', 'ledger'].forEach(async device => { + const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device]) + if (unlocked) { + this.setState({unlocked: true}) + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + }) } - connectToTrezor = () => { + connectToHardwareWallet = (device) => { + // None of the hardware wallets are supported + // At least for now + if (getPlatform() === PLATFORM_FIREFOX) { + this.setState({ browserSupported: false, error: null}) + return null + } + if (this.state.accounts.length) { return null } - this.setState({ btnText: this.context.t('connecting')}) - this.getPage(0) + + // Default values + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + + onPathChange = (path) => { + this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path}) + this.getPage(this.state.device, 0, path) } onAccountChange = (account) => { this.setState({selectedAccount: account.toString(), error: null}) } + onAccountRestriction = () => { + this.setState({error: this.context.t('ledgerAccountRestriction') }) + } + showTemporaryAlert () { this.props.showAlert(this.context.t('hardwareWalletConnected')) // Autohide the alert after 5 seconds @@ -65,9 +86,9 @@ class ConnectHardwareForm extends Component { }, 5000) } - getPage = (page) => { + getPage = (device, page, hdPath) => { this.props - .connectHardware('trezor', page) + .connectHardware(device, page, hdPath) .then(accounts => { if (accounts.length) { @@ -77,7 +98,7 @@ class ConnectHardwareForm extends Component { this.showTemporaryAlert() } - const newState = { unlocked: true } + const newState = { unlocked: true, device, error: null } // Default to the first account if (this.state.selectedAccount === null) { accounts.forEach((a, i) => { @@ -104,18 +125,18 @@ class ConnectHardwareForm extends Component { }) .catch(e => { if (e === 'Window blocked') { - this.setState({ browserSupported: false }) + this.setState({ browserSupported: false, error: null}) + } else if (e !== 'Window closed') { + this.setState({ error: e.toString() }) } - this.setState({ btnText: this.context.t('connectToTrezor') }) }) } - onForgetDevice = () => { - this.props.forgetDevice('trezor') + onForgetDevice = (device) => { + this.props.forgetDevice(device) .then(_ => { this.setState({ error: null, - btnText: this.context.t('connectToTrezor'), selectedAccount: null, accounts: [], unlocked: false, @@ -125,13 +146,13 @@ class ConnectHardwareForm extends Component { }) } - onUnlockAccount = () => { + onUnlockAccount = (device) => { if (this.state.selectedAccount === null) { this.setState({ error: this.context.t('accountSelectionRequired') }) } - this.props.unlockTrezorAccount(this.state.selectedAccount) + this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device) .then(_ => { this.props.history.push(DEFAULT_ROUTE) }).catch(e => { @@ -145,20 +166,22 @@ class ConnectHardwareForm extends Component { renderError () { return this.state.error - ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) + ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error) : null } renderContent () { if (!this.state.accounts.length) { return h(ConnectScreen, { - connectToTrezor: this.connectToTrezor, - btnText: this.state.btnText, + connectToHardwareWallet: this.connectToHardwareWallet, browserSupported: this.state.browserSupported, }) } return h(AccountList, { + onPathChange: this.onPathChange, + selectedPath: this.props.defaultHdPaths[this.state.device], + device: this.state.device, accounts: this.state.accounts, selectedAccount: this.state.selectedAccount, onAccountChange: this.onAccountChange, @@ -168,6 +191,7 @@ class ConnectHardwareForm extends Component { onUnlockAccount: this.onUnlockAccount, onForgetDevice: this.onForgetDevice, onCancel: this.onCancel, + onAccountRestriction: this.onAccountRestriction, }) } @@ -188,13 +212,15 @@ ConnectHardwareForm.propTypes = { forgetDevice: PropTypes.func, showAlert: PropTypes.func, hideAlert: PropTypes.func, - unlockTrezorAccount: PropTypes.func, + unlockHardwareWalletAccount: PropTypes.func, + setHardwareWalletDefaultHdPath: PropTypes.func, numberOfExistingAccounts: PropTypes.number, history: PropTypes.object, t: PropTypes.func, network: PropTypes.string, accounts: PropTypes.object, address: PropTypes.string, + defaultHdPaths: PropTypes.object, } const mapStateToProps = state => { @@ -202,28 +228,35 @@ const mapStateToProps = state => { metamask: { network, selectedAddress, identities = {}, accounts = [] }, } = state const numberOfExistingAccounts = Object.keys(identities).length + const { + appState: { defaultHdPaths }, + } = state return { network, accounts, address: selectedAddress, numberOfExistingAccounts, + defaultHdPaths, } } const mapDispatchToProps = dispatch => { return { - connectHardware: (deviceName, page) => { - return dispatch(actions.connectHardware(deviceName, page)) + setHardwareWalletDefaultHdPath: ({device, path}) => { + return dispatch(actions.setHardwareWalletDefaultHdPath({device, path})) + }, + connectHardware: (deviceName, page, hdPath) => { + return dispatch(actions.connectHardware(deviceName, page, hdPath)) }, - checkHardwareStatus: (deviceName) => { - return dispatch(actions.checkHardwareStatus(deviceName)) + checkHardwareStatus: (deviceName, hdPath) => { + return dispatch(actions.checkHardwareStatus(deviceName, hdPath)) }, forgetDevice: (deviceName) => { return dispatch(actions.forgetDevice(deviceName)) }, - unlockTrezorAccount: index => { - return dispatch(actions.unlockTrezorAccount(index)) + unlockHardwareWalletAccount: (index, deviceName, hdPath) => { + return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath)) }, showImportPage: () => dispatch(actions.showImportPage()), showConnectPage: () => dispatch(actions.showConnectPage()), diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 20c2be0f1..8e092364c 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -118,8 +118,18 @@ WalletView.prototype.render = function () { return kr.accounts.includes(selectedAddress) }) - const type = keyring.type - const isLoose = type !== 'HD Key Tree' + let label = '' + let type + if (keyring) { + type = keyring.type + if (type !== 'HD Key Tree') { + if (type.toLowerCase().search('hardware') !== -1) { + label = this.context.t('hardware') + } else { + label = this.context.t('imported') + } + } + } return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { style: {}, @@ -133,7 +143,7 @@ WalletView.prototype.render = function () { onClick: hideSidebar, }), - h('div.wallet-view__keyring-label.allcaps', isLoose ? this.context.t('imported') : ''), + h('div.wallet-view__keyring-label.allcaps', label), h('div.flex-column.flex-center.wallet-view__name-container', { style: { margin: '0 auto' }, diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index b12afb124..e4c7a4e0d 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -162,19 +162,99 @@ } .hw-connect { + width: 100%; + &__header { &__title { margin-top: 5px; margin-bottom: 15px; font-size: 22px; - text-align: center; } &__msg { font-size: 14px; color: #9b9b9b; margin-top: 10px; - margin-bottom: 0px; + margin-bottom: 20px; + } + } + + &__btn-wrapper { + flex: 1; + flex-direction: row; + display: flex; + } + + &__connect-btn { + background-color: #259De5; + color: #fff; + border: none; + width: 315px; + min-height: 54px; + font-weight: 300; + font-size: 14px; + margin-bottom: 20px; + margin-top: 20px; + border-radius: 5px; + display: flex; + flex: 1; + margin-left: 20px; + margin-right: 20px; + justify-content: center; + text-transform: uppercase; + } + + &__connect-btn.disabled { + cursor: not-allowed; + opacity: .5; + } + + &__btn { + background: #fbfbfb; + border: 1px solid #e5e5e5; + height: 100px; + width: 150px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + + &__img { + width: 95px; + } + } + + &__btn.selected { + border: 2px solid #00a8ee; + width: 149px; + } + + &__btn:first-child { + margin-right: 15px; + margin-left: 20px; + } + + &__btn:last-child { + margin-right: 20px; + } + + &__hdPath { + display: flex; + flex-direction: row; + margin-top: 15px; + margin-bottom: 30px; + font-size: 14px; + + &__title { + display: flex; + margin-top: 10px; + margin-right: 15px; + } + + &__select { + display: flex; + flex: 1; } } @@ -201,6 +281,13 @@ font-size: 18px; } + &__unlock-title { + padding-top: 10px; + font-weight: 400; + font-size: 22px; + margin-bottom: 15px; + } + &__msg { font-size: 14px; color: #9b9b9b; @@ -213,8 +300,6 @@ } &__footer { - width: 100%; - &__title { padding-top: 15px; padding-bottom: 12px; @@ -228,6 +313,9 @@ color: #9b9b9b; margin-top: 12px; margin-bottom: 27px; + width: 100%; + display: block; + margin-left: 20px; } &__link { @@ -236,10 +324,10 @@ } } - &__get-trezor { + &__get-hw { width: 100%; - padding-bottom: 20px; - padding-top: 20px; + padding-bottom: 10px; + padding-top: 10px; &__msg { font-size: 14px; @@ -390,6 +478,8 @@ &.account-list { height: auto; + padding-left: 20px; + padding-right: 20px; } &__buttons { @@ -412,6 +502,7 @@ min-height: 54px; font-weight: 300; font-size: 14px; + margin-bottom: 20px } &__button.unlock { diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 98d467163..c246e7904 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -67,6 +67,10 @@ function reduceApp (state, action) { isMouseUser: false, gasIsLoading: false, networkNonce: null, + defaultHdPaths: { + trezor: `m/44'/60'/0'/0`, + ledger: `m/44'/60'/0'/0/0`, + }, }, state.appState) switch (action.type) { @@ -525,6 +529,15 @@ function reduceApp (state, action) { warning: '', }) + case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH: + const { device, path } = action.value + const newDefaults = {...appState.defaultHdPaths} + newDefaults[device] = path + + return extend(appState, { + defaultHdPaths: newDefaults, + }) + case actions.SHOW_LOADING: return extend(appState, { isLoading: true, diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index aa1fc5404..6e760c429 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -159,7 +159,7 @@ export const approveTokenAmountAndToAddressSelector = createSelector( if (tokenDecimals) { tokenAmount = calcTokenAmount(value, tokenDecimals) } - + tokenAmount = roundExponential(tokenAmount) } |