Merge pull request #12584 from MetaMask/Version-v10.5.0

Version v10.5.0 RC
feature/default_network_editable
ryanml 3 years ago committed by GitHub
commit 263e80da5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 150
      .circleci/config.yml
  2. 11
      CHANGELOG.md
  3. 77
      app/_locales/en/messages.json
  4. 6
      app/_locales/es/messages.json
  5. 6
      app/_locales/es_419/messages.json
  6. 6
      app/_locales/hi/messages.json
  7. 6
      app/_locales/id/messages.json
  8. 6
      app/_locales/ja/messages.json
  9. 6
      app/_locales/ko/messages.json
  10. 6
      app/_locales/ph/messages.json
  11. 6
      app/_locales/pt_BR/messages.json
  12. 6
      app/_locales/ru/messages.json
  13. 6
      app/_locales/vi/messages.json
  14. 25
      app/scripts/controllers/preferences.js
  15. 49
      app/scripts/metamask-controller.js
  16. 37
      app/scripts/migrations/066.js
  17. 116
      app/scripts/migrations/066.test.js
  18. 2
      app/scripts/migrations/index.js
  19. 11
      app/scripts/platforms/extension.js
  20. 14
      development/metamaskbot-build-announce.js
  21. 4
      package.json
  22. 24
      shared/constants/hardware-wallets.js
  23. 16
      shared/notifications/index.js
  24. 3
      test/e2e/fixtures/address-entry/state.json
  25. 3
      test/e2e/fixtures/connected-state/state.json
  26. 3
      test/e2e/fixtures/custom-rpc/state.json
  27. 6
      test/e2e/fixtures/custom-token/state.json
  28. 3
      test/e2e/fixtures/import-ui/state.json
  29. 3
      test/e2e/fixtures/imported-account/state.json
  30. 3
      test/e2e/fixtures/localization/state.json
  31. 3
      test/e2e/fixtures/metrics-enabled/state.json
  32. 3
      test/e2e/fixtures/navigate-transactions/state.json
  33. 3
      test/e2e/fixtures/send-edit/state.json
  34. 3
      test/e2e/fixtures/threebox-enabled/state.json
  35. 18
      test/e2e/metamask-ui.spec.js
  36. 2
      test/e2e/tests/add-hide-token.spec.js
  37. 5
      test/e2e/tests/from-import-ui.spec.js
  38. 7
      test/e2e/tests/incremental-security.spec.js
  39. 1
      ui/components/app/ledger-instruction-field/index.js
  40. 212
      ui/components/app/ledger-instruction-field/ledger-instruction-field.js
  41. 10
      ui/components/app/signature-request-original/signature-request-original.component.js
  42. 16
      ui/components/app/signature-request-original/signature-request-original.container.js
  43. 5
      ui/components/app/signature-request/signature-request-footer/signature-request-footer.component.js
  44. 17
      ui/components/app/signature-request/signature-request.component.js
  45. 28
      ui/components/app/signature-request/signature-request.container.js
  46. 9
      ui/components/app/whats-new-popup/whats-new-popup.js
  47. 34
      ui/ducks/app/app.js
  48. 60
      ui/ducks/metamask/metamask.js
  49. 13
      ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
  50. 5
      ui/pages/confirm-approve/confirm-approve-content/index.scss
  51. 17
      ui/pages/confirm-approve/confirm-approve.js
  52. 60
      ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
  53. 30
      ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
  54. 17
      ui/pages/create-account/connect-hardware/index.js
  55. 5
      ui/pages/create-account/connect-hardware/select-hardware.js
  56. 5
      ui/pages/create-account/connect-hardware/select-hardware.stories.js
  57. 100
      ui/pages/settings/advanced-tab/advanced-tab.component.js
  58. 5
      ui/pages/settings/advanced-tab/advanced-tab.component.test.js
  59. 8
      ui/pages/settings/advanced-tab/advanced-tab.container.js
  60. 11
      ui/pages/settings/index.scss
  61. 61
      ui/selectors/selectors.js
  62. 5
      ui/store/actionConstants.js
  63. 47
      ui/store/actions.js
  64. 4
      ui/store/actions.test.js
  65. 26
      yarn.lock

@ -39,6 +39,12 @@ workflows:
- prep-build:
requires:
- prep-deps
- prep-build-beta:
requires:
- prep-deps
- prep-build-flask:
requires:
- prep-deps
- prep-build-test:
requires:
- prep-deps
@ -79,10 +85,24 @@ workflows:
- validate-source-maps:
requires:
- prep-build
- validate-source-maps-beta:
requires:
- prep-build-beta
- validate-source-maps-flask:
requires:
- prep-build-flask
- test-mozilla-lint:
requires:
- prep-deps
- prep-build
- test-mozilla-lint-beta:
requires:
- prep-deps
- prep-build-beta
- test-mozilla-lint-flask:
requires:
- prep-deps
- prep-build-flask
- all-tests-pass:
requires:
- validate-lavamoat-config
@ -93,7 +113,11 @@ workflows:
- test-unit
- test-unit-global
- validate-source-maps
- validate-source-maps-beta
- validate-source-maps-flask
- test-mozilla-lint
- test-mozilla-lint-beta
- test-mozilla-lint-flask
- test-e2e-chrome
- test-e2e-firefox
- test-e2e-chrome-metrics
@ -105,6 +129,8 @@ workflows:
requires:
- prep-deps
- prep-build
- prep-build-beta
- prep-build-flask
- prep-build-storybook
- benchmark
- all-tests-pass
@ -201,6 +227,54 @@ jobs:
- dist
- builds
prep-build-beta:
executor: node-browsers-medium-plus
steps:
- checkout
- attach_workspace:
at: .
- run:
name: build:dist
command: yarn build --build-type beta prod
- run:
name: build:debug
command: find dist/ -type f -exec md5sum {} \; | sort -k 2
- run:
name: Move beta build to 'dist-beta' to avoid conflict with production build
command: mv ./dist ./dist-beta
- run:
name: Move beta zips to 'builds-beta' to avoid conflict with production build
command: mv ./builds ./builds-beta
- persist_to_workspace:
root: .
paths:
- dist-beta
- builds-beta
prep-build-flask:
executor: node-browsers-medium-plus
steps:
- checkout
- attach_workspace:
at: .
- run:
name: build:dist
command: yarn build --build-type flask prod
- run:
name: build:debug
command: find dist/ -type f -exec md5sum {} \; | sort -k 2
- run:
name: Move flask build to 'dist-flask' to avoid conflict with production build
command: mv ./dist ./dist-flask
- run:
name: Move flask zips to 'builds-flask' to avoid conflict with production build
command: mv ./builds ./builds-flask
- persist_to_workspace:
root: .
paths:
- dist-flask
- builds-flask
prep-build-test:
executor: node-browsers-medium-plus
steps:
@ -482,9 +556,21 @@ jobs:
- store_artifacts:
path: dist/sourcemaps
destination: builds/sourcemaps
- store_artifacts:
path: dist-beta/sourcemaps
destination: builds-beta/sourcemaps
- store_artifacts:
path: dist-flask/sourcemaps
destination: builds-flask/sourcemaps
- store_artifacts:
path: builds
destination: builds
- store_artifacts:
path: builds-beta
destination: builds-beta
- store_artifacts:
path: builds-flask
destination: builds-flask
- store_artifacts:
path: coverage
destination: coverage
@ -577,6 +663,38 @@ jobs:
name: Validate source maps
command: yarn validate-source-maps
validate-source-maps-beta:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Move beta build to dist
command: mv ./dist-beta ./dist
- run:
name: Move beta zips to builds
command: mv ./builds-beta ./builds
- run:
name: Validate source maps
command: yarn validate-source-maps
validate-source-maps-flask:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Move flask build to dist
command: mv ./dist-flask ./dist
- run:
name: Move flask zips to builds
command: mv ./builds-flask ./builds
- run:
name: Validate source maps
command: yarn validate-source-maps
test-mozilla-lint:
executor: node-browsers
steps:
@ -587,6 +705,38 @@ jobs:
name: test:mozilla-lint
command: NODE_OPTIONS=--max_old_space_size=3072 yarn mozilla-lint
test-mozilla-lint-beta:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Move beta build to dist
command: mv ./dist-beta ./dist
- run:
name: Move beta zips to builds
command: mv ./builds-beta ./builds
- run:
name: test:mozilla-lint
command: NODE_OPTIONS=--max_old_space_size=3072 yarn mozilla-lint
test-mozilla-lint-flask:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Move flask build to dist
command: mv ./dist-flask ./dist
- run:
name: Move flask zips to builds
command: mv ./builds-flask ./builds
- run:
name: test:mozilla-lint
command: NODE_OPTIONS=--max_old_space_size=3072 yarn mozilla-lint
all-tests-pass:
executor: node-browsers
steps:

@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [10.5.0]
### Added
- [#12411](https://github.com/MetaMask/metamask-extension/pull/12411): Add support for connecting Ledger devices to MetaMask via WebHID
- [#12501](https://github.com/MetaMask/metamask-extension/pull/12501): Add "What's New" notification regarding Ledger WebHID support
### Removed
- [#12500](https://github.com/MetaMask/metamask-extension/pull/12500): Remove all notifications prior to Ledger WebHID announcement
## [10.4.1]
### Changed
- [#12515](https://github.com/MetaMask/metamask-extension/pull/12515): Updating 'Learn more' link location in dapp connection flow
@ -2538,7 +2546,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.4.1...HEAD
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.5.0...HEAD
[10.5.0]: https://github.com/MetaMask/metamask-extension/compare/v10.4.1...v10.5.0
[10.4.1]: https://github.com/MetaMask/metamask-extension/compare/v10.4.0...v10.4.1
[10.4.0]: https://github.com/MetaMask/metamask-extension/compare/v10.3.0...v10.4.0
[10.3.0]: https://github.com/MetaMask/metamask-extension/compare/v10.2.2...v10.3.0

@ -345,6 +345,10 @@
"chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
},
"clickToConnectLedgerViaWebHID": {
"message": "Click here to connect your Ledger via WebHID",
"description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid"
},
"clickToRevealSeed": {
"message": "Click here to reveal secret words"
},
@ -1251,36 +1255,51 @@
"ledgerAccountRestriction": {
"message": "You need to make use your last account before you can add a new one."
},
"ledgerLiveAdvancedSetting": {
"message": "Use Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "The new Ledger Live bridge allows you to more easily use your Ledger. Only available in Chrome."
},
"ledgerLiveApp": {
"message": "Ledger Live App"
"ledgerConnectionInstructionCloseOtherApps": {
"message": "Close any other software connected to your device and then click here to refresh."
},
"ledgerLiveDialogHeader": {
"ledgerConnectionInstructionHeader": {
"message": "Prior to clicking confirm:"
},
"ledgerLiveDialogStepFour": {
"ledgerConnectionInstructionStepFour": {
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device"
},
"ledgerLiveDialogStepOne": {
"ledgerConnectionInstructionStepOne": {
"message": "Enable Use Ledger Live under Settings > Advanced"
},
"ledgerLiveDialogStepThree": {
"ledgerConnectionInstructionStepThree": {
"message": "Plug in your Ledger device and select the Ethereum app"
},
"ledgerLiveDialogStepTwo": {
"ledgerConnectionInstructionStepTwo": {
"message": "Open and unlock Ledger Live App"
},
"ledgerConnectionPreferenceDescription": {
"message": "Customize how you connect your Ledger to MetaMask. $1 is recommended, but other options are available. Read more here: $2",
"description": "A description that appears above a dropdown where users can select between up to three options - Ledger Live, U2F or WebHID - depending on what is supported in their browser. $1 is the recommended browser option, it will be either WebHID or U2f. $2 is a link to an article where users can learn more, but will be the translation of the learnMore message."
},
"ledgerDeviceOpenFailureMessage": {
"message": "The Ledger device failed to open. Your Ledger might be connected to other software. Please close Ledger Live or other applications connected to your Ledger device, and try to connect again."
},
"ledgerLive": {
"message": "Ledger Live",
"description": "The name of a desktop app that can be used with your ledger device. We can also use it to connect a users Ledger device to MetaMask."
},
"ledgerLiveApp": {
"message": "Ledger Live App"
},
"ledgerLocked": {
"message": "Cannot connect to Ledger device. Please make sure your device is unlocked and Ethereum app is opened."
},
"ledgerTimeout": {
"message": "Ledger Live is taking too long to respond or connection timeout. Make sure Ledger Live app is opened and your device is unlocked."
},
"ledgerTransportChangeWarning": {
"message": "If your Ledger Live app is open, please disconnect any open Ledger Live connection and close the Ledger Live app."
},
"ledgerWebHIDNotConnectedErrorMessage": {
"message": "The ledger device was not connected. If you wish to connect your Ledger, please click 'Continue' again and approve HID connection",
"description": "An error message shown to the user during the hardware connect flow."
},
"letsGoSetUp": {
"message": "Yes, let’s get set up!"
},
@ -1635,6 +1654,22 @@
"message": "Ledger firmware update",
"description": "Title for a notification in the 'See What's New' popup. Notifies ledger users of the need to update firmware."
},
"notifications8ActionText": {
"message": "Go to Advanced Settings",
"description": "Description on an action button that appears in the What's New popup. Tells the user that if they click it, they will go to our Advanced Settings page."
},
"notifications8DescriptionOne": {
"message": "As of MetaMask v10.4.0, you no longer need Ledger Live to connect your Ledger device to MetaMask.",
"description": "Description of a notification in the 'See What's New' popup. Describes changes for how Ledger Live is no longer needed to connect the device."
},
"notifications8DescriptionTwo": {
"message": "For an easier and more stable ledger experience, go to the Advanced tab of settings and switch the 'Preferred Ledger Connection Type' to 'WebHID'.",
"description": "Description of a notification in the 'See What's New' popup. Describes how the user can turn off the Ledger Live setting."
},
"notifications8Title": {
"message": "Ledger connection improvement",
"description": "Title for a notification in the 'See What's New' popup. Notifies ledger users that there is an improvement in how they can connect their device."
},
"ofTextNofM": {
"message": "of"
},
@ -1710,6 +1745,10 @@
"onlyConnectTrust": {
"message": "Only connect with sites you trust."
},
"openFullScreenForLedgerWebHid": {
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."
},
"optional": {
"message": "Optional"
},
@ -1772,6 +1811,10 @@
"message": "+ $1 more",
"description": "$1 is a number of additional but unshown items in a list- this message will be shown in place of those items"
},
"preferredLedgerConnectionType": {
"message": "Preferred Ledger Connection Type",
"description": "A header for a dropdown in the advanced section of settings. Appears above the ledgerConnectionPreferenceDescription message"
},
"prev": {
"message": "Prev"
},
@ -2817,6 +2860,10 @@
"typePassword": {
"message": "Type your MetaMask password"
},
"u2f": {
"message": "U2F",
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."
},
"unapproved": {
"message": "Unapproved"
},
@ -2958,6 +3005,10 @@
"message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.",
"description": "$1 is a clickable link."
},
"webhid": {
"message": "WebHID",
"description": "Refers to a interface for connecting external devices to the browser. Used for connecting ledger to the browser. Read more here https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API"
},
"welcome": {
"message": "Welcome to MetaMask"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Debe usar su última cuenta antes de poder agregar una nueva."
},
"ledgerLiveAdvancedSetting": {
"message": "Utilizar Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "El nuevo puente Ledger Live le permite utilizar su Ledger de forma más sencilla. Disponible solo en Google Chrome."
},
"ledgerLiveApp": {
"message": "Aplicación de Ledger Live"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Debe usar su última cuenta antes de poder agregar una nueva."
},
"ledgerLiveAdvancedSetting": {
"message": "Utilizar Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "El nuevo puente Ledger Live le permite utilizar su Ledger de forma más sencilla. Disponible solo en Google Chrome."
},
"ledgerLiveApp": {
"message": "Aplicación de Ledger Live"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "नय पहल आपक अपनिम ख उपयग करन।"
},
"ledgerLiveAdvancedSetting": {
"message": "Ledger Live क उपयग कर"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "नय Ledger Live बिज आपक अपनजर क अधिक आस उपयग करन अनमति। कवल Chrome म उपलबध ह।"
},
"ledgerLiveApp": {
"message": "Ledger Live ऐप"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Anda perlu memanfaatkan akun terakhir Anda sebelum menambahkan yang baru."
},
"ledgerLiveAdvancedSetting": {
"message": "Gunakan Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "Jembatan Ledger Live baru memungkinkan Anda untuk menggunakan Ledger Anda dengan lebih mudah. Hanya tersedia di Chrome."
},
"ledgerLiveApp": {
"message": "Aplikasi Ledger Live"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "新しいアカウントを追加するには、その前に最後のアカウントを使用する必要があります。"
},
"ledgerLiveAdvancedSetting": {
"message": "レジャー ライブを使用"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "新しいレジャー ライブのブリッジを使用すると、レジャーをより簡単に使用できます。Chrome でのみ利用可能。"
},
"ledgerLiveApp": {
"message": "レジャー ライブのアプリ"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "새 계정을 추가하려면 먼저 마지막 계정을 사용해야 합니다."
},
"ledgerLiveAdvancedSetting": {
"message": "Ledger Live 사용하기"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "새로운 Ledger Live 브리지를 통해 Ledger를 더 쉽게 사용할 수 있습니다. Chrome에서만 사용 가능합니다."
},
"ledgerLiveApp": {
"message": "Ledger Live 앱"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Kailangan mong gamitin ang huli mong account bago ka magdagdag ng panibago."
},
"ledgerLiveAdvancedSetting": {
"message": "Gamitin ang Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "Binibigyang-daan ka ng bagong Ledger Live bridge na mas madaling magamit ang iyong Ledger. Available lang sa Chrome."
},
"ledgerLiveApp": {
"message": "Ledger Live App"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Você precisa usar sua última conta antes de adicionar uma nova."
},
"ledgerLiveAdvancedSetting": {
"message": "Usar Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "A nova ponte do Ledger Live permite utilizar seu Ledger mais facilmente. Disponível somente no Chrome."
},
"ledgerLiveApp": {
"message": "Aplicativo Ledger Live"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Вам необходимо использовать свой последний счет, прежде чем вы сможете добавить новый."
},
"ledgerLiveAdvancedSetting": {
"message": "Использовать Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "Новое решение Ledger Live Bridge упрощает использование Ledger. Доступно только в Chrome."
},
"ledgerLiveApp": {
"message": "Приложение Ledger Live"
},

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "Bạn cần sử dụng tài khoản gần đây nhất thì mới có thể thêm một tài khoản mới."
},
"ledgerLiveAdvancedSetting": {
"message": "Dùng Ledger Live"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "Cầu Ledger Live mới cho phép bạn dùng Ledger dễ dàng hơn. Chỉ có trong Chrome."
},
"ledgerLiveApp": {
"message": "Ứng dụng Ledger Live"
},

@ -5,6 +5,7 @@ import { ethers } from 'ethers';
import log from 'loglevel';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
import { NETWORK_EVENTS } from './network';
export default class PreferencesController {
@ -58,7 +59,9 @@ export default class PreferencesController {
// ENS decentralized website resolution
ipfsGateway: 'dweb.link',
infuraBlocked: null,
useLedgerLive: false,
ledgerTransportType: window.navigator.hid
? LEDGER_TRANSPORT_TYPES.WEBHID
: LEDGER_TRANSPORT_TYPES.U2F,
...opts.initState,
};
@ -516,21 +519,21 @@ export default class PreferencesController {
}
/**
* A setter for the `useLedgerLive` property
* @param {bool} useLedgerLive - Value for ledger live support
* @returns {Promise<string>} A promise of the update to useLedgerLive
* A setter for the `useWebHid` property
* @param {string} ledgerTransportType - Either 'ledgerLive', 'webhid' or 'u2f'
* @returns {string} The transport type that was set.
*/
async setLedgerLivePreference(useLedgerLive) {
this.store.updateState({ useLedgerLive });
return useLedgerLive;
setLedgerTransportPreference(ledgerTransportType) {
this.store.updateState({ ledgerTransportType });
return ledgerTransportType;
}
/**
* A getter for the `useLedgerLive` property
* @returns {boolean} User preference of using Ledger Live
* A getter for the `ledgerTransportType` property
* @returns {boolean} User preference of using WebHid to connect Ledger
*/
getLedgerLivePreference() {
return this.store.getState().useLedgerLive;
getLedgerTransportPreference() {
return this.store.getState().ledgerTransportType;
}
/**

@ -841,7 +841,18 @@ export default class MetamaskController extends EventEmitter {
this.unlockHardwareWalletAccount,
this,
),
setLedgerLivePreference: nodeify(this.setLedgerLivePreference, this),
setLedgerTransportPreference: nodeify(
this.setLedgerTransportPreference,
this,
),
attemptLedgerTransportCreation: nodeify(
this.attemptLedgerTransportCreation,
this,
),
establishLedgerTransportPreference: nodeify(
this.establishLedgerTransportPreference,
this,
),
// mobile
fetchInfoToSync: nodeify(this.fetchInfoToSync, this),
@ -1245,6 +1256,7 @@ export default class MetamaskController extends EventEmitter {
this.preferencesController.setAddresses(addresses);
this.selectFirstIdentity();
}
return vault;
} finally {
releaseLock();
@ -1314,6 +1326,13 @@ export default class MetamaskController extends EventEmitter {
accounts = await keyringController.getAccounts();
}
// This must be set as soon as possible to communicate to the
// keyring's iframe and have the setting initialized properly
// Optimistically called to not block Metamask login due to
// Ledger Keyring GitHub downtime
const transportPreference = this.preferencesController.getLedgerTransportPreference();
this.setLedgerTransportPreference(transportPreference);
// set new identities
this.preferencesController.setAddresses(accounts);
this.selectFirstIdentity();
@ -1480,9 +1499,9 @@ export default class MetamaskController extends EventEmitter {
// keyring's iframe and have the setting initialized properly
// Optimistically called to not block Metamask login due to
// Ledger Keyring GitHub downtime
this.setLedgerLivePreference(
this.preferencesController.getLedgerLivePreference(),
);
const transportPreference = this.preferencesController.getLedgerTransportPreference();
this.setLedgerTransportPreference(transportPreference);
return this.keyringController.fullUpdate();
}
@ -1546,6 +1565,16 @@ export default class MetamaskController extends EventEmitter {
return keyring;
}
async attemptLedgerTransportCreation() {
const keyring = await this.getKeyringForDevice('ledger');
return await keyring.attemptMakeApp();
}
async establishLedgerTransportPreference() {
const transportPreference = this.preferencesController.getLedgerTransportPreference();
return await this.setLedgerTransportPreference(transportPreference);
}
/**
* Fetch account list from a trezor device.
*
@ -2984,16 +3013,18 @@ export default class MetamaskController extends EventEmitter {
* Sets the Ledger Live preference to use for Ledger hardware wallet support
* @param {bool} bool - the value representing if the users wants to use Ledger Live
*/
async setLedgerLivePreference(bool) {
const currentValue = this.preferencesController.getLedgerLivePreference();
this.preferencesController.setLedgerLivePreference(bool);
async setLedgerTransportPreference(transportType) {
const currentValue = this.preferencesController.getLedgerTransportPreference();
const newValue = this.preferencesController.setLedgerTransportPreference(
transportType,
);
const keyring = await this.getKeyringForDevice('ledger');
if (keyring?.updateTransportMethod) {
return keyring.updateTransportMethod(bool).catch((e) => {
return keyring.updateTransportMethod(newValue).catch((e) => {
// If there was an error updating the transport, we should
// fall back to the original value
this.preferencesController.setLedgerLivePreference(currentValue);
this.preferencesController.setLedgerTransportPreference(currentValue);
throw e;
});
}

@ -0,0 +1,37 @@
import { cloneDeep } from 'lodash';
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
const version = 66;
/**
* Changes the useLedgerLive boolean property to the ledgerTransportType enum
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
const newState = transformState(state);
versionedData.data = newState;
return versionedData;
},
};
function transformState(state) {
const defaultTransportType = window.navigator.hid
? LEDGER_TRANSPORT_TYPES.WEBHID
: LEDGER_TRANSPORT_TYPES.U2F;
const useLedgerLive = Boolean(state.PreferencesController?.useLedgerLive);
const newState = {
...state,
PreferencesController: {
...state?.PreferencesController,
ledgerTransportType: useLedgerLive
? LEDGER_TRANSPORT_TYPES.LIVE
: defaultTransportType,
},
};
delete newState.PreferencesController.useLedgerLive;
return newState;
}

@ -0,0 +1,116 @@
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
import migration66 from './066';
describe('migration #66', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 65,
},
data: {},
};
const newStorage = await migration66.migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version: 66,
});
});
it('should set ledgerTransportType to `u2f` if no preferences controller exists and webhid is not available', async () => {
const oldStorage = {
meta: {},
data: {},
};
const newStorage = await migration66.migrate(oldStorage);
expect(
newStorage.data.PreferencesController.ledgerTransportType,
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F);
});
it('should set ledgerTransportType to `u2f` if no useLedgerLive property exists and webhid is not available', async () => {
const oldStorage = {
meta: {},
data: {
PreferencesController: {},
},
};
const newStorage = await migration66.migrate(oldStorage);
expect(
newStorage.data.PreferencesController.ledgerTransportType,
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F);
});
it('should set ledgerTransportType to `u2f` if useLedgerLive is false and webhid is not available', async () => {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
useLedgerLive: false,
},
},
};
const newStorage = await migration66.migrate(oldStorage);
expect(
newStorage.data.PreferencesController.ledgerTransportType,
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F);
});
it('should set ledgerTransportType to `webhid` if useLedgerLive is false and webhid is available', async () => {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
useLedgerLive: false,
},
},
};
jest
.spyOn(window, 'navigator', 'get')
.mockImplementation(() => ({ hid: true }));
const newStorage = await migration66.migrate(oldStorage);
expect(
newStorage.data.PreferencesController.ledgerTransportType,
).toStrictEqual(LEDGER_TRANSPORT_TYPES.WEBHID);
});
it('should set ledgerTransportType to `ledgerLive` if useLedgerLive is true', async () => {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
useLedgerLive: true,
},
},
};
const newStorage = await migration66.migrate(oldStorage);
expect(
newStorage.data.PreferencesController.ledgerTransportType,
).toStrictEqual('ledgerLive');
});
it('should not change ledgerTransportType if useLedgerLive is true and webhid is available', async () => {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
useLedgerLive: true,
},
},
};
jest
.spyOn(window, 'navigator', 'get')
.mockImplementation(() => ({ hid: true }));
const newStorage = await migration66.migrate(oldStorage);
expect(
newStorage.data.PreferencesController.ledgerTransportType,
).toStrictEqual(LEDGER_TRANSPORT_TYPES.LIVE);
});
});

@ -69,6 +69,7 @@ import m062 from './062';
import m063 from './063';
import m064 from './064';
import m065 from './065';
import m066 from './066';
const migrations = [
m002,
@ -135,6 +136,7 @@ const migrations = [
m063,
m064,
m065,
m066,
];
export default migrations;

@ -111,7 +111,11 @@ export default class ExtensionPlatform {
return version;
}
openExtensionInBrowser(route = null, queryString = null) {
openExtensionInBrowser(
route = null,
queryString = null,
keepWindowOpen = false,
) {
let extensionURL = extension.runtime.getURL('home.html');
if (queryString) {
@ -122,7 +126,10 @@ export default class ExtensionPlatform {
extensionURL += `#${route}`;
}
this.openTab({ url: extensionURL });
if (getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND) {
if (
getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND &&
!keepWindowOpen
) {
window.close();
}
}

@ -39,6 +39,18 @@ async function start() {
return `<a href="${url}">${platform}</a>`;
})
.join(', ');
const betaBuildLinks = platforms
.map((platform) => {
const url = `${BUILD_LINK_BASE}/builds-beta/metamask-beta-${platform}-${VERSION}.zip`;
return `<a href="${url}">${platform}</a>`;
})
.join(', ');
const flaskBuildLinks = platforms
.map((platform) => {
const url = `${BUILD_LINK_BASE}/builds-flask/metamask-flask-${platform}-${VERSION}.zip`;
return `<a href="${url}">${platform}</a>`;
})
.join(', ');
// links to bundle browser builds
const bundles = {};
@ -86,6 +98,8 @@ async function start() {
const contentRows = [
`builds: ${buildLinks}`,
`builds (beta): ${betaBuildLinks}`,
`builds (flask): ${flaskBuildLinks}`,
`build viz: ${depVizLink}`,
`code coverage: ${coverageLink}`,
`storybook: ${storybookLink}`,

@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "10.4.1",
"version": "10.5.0",
"private": true,
"repository": {
"type": "git",
@ -105,7 +105,7 @@
"@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.28.0",
"@metamask/controllers": "^17.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.7.0",
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
"@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0",
"@metamask/jazzicon": "^2.0.0",

@ -7,3 +7,27 @@ export const KEYRING_TYPES = {
LEDGER: 'Ledger Hardware',
TREZOR: 'Trezor Hardware',
};
/**
* Used for setting the users preference for ledger transport type
*/
export const LEDGER_TRANSPORT_TYPES = {
LIVE: 'ledgerLive',
WEBHID: 'webhid',
U2F: 'u2f',
};
export const LEDGER_USB_VENDOR_ID = '0x2c97';
export const WEBHID_CONNECTED_STATUSES = {
CONNECTED: 'connected',
NOT_CONNECTED: 'notConnected',
UNKNOWN: 'unknown',
};
export const TRANSPORT_STATES = {
NONE: 'NONE',
VERIFIED: 'VERIFIED',
DEVICE_OPEN_FAILURE: 'DEVICE_OPEN_FAILURE',
UNKNOWN_FAILURE: 'UNKNOWN_FAILURE',
};

@ -34,6 +34,10 @@ export const UI_NOTIFICATIONS = {
id: 7,
date: '2021-09-17',
},
8: {
id: 8,
date: '2021-11-01',
},
};
export const getTranslatedUINoficiations = (t, locale) => {
@ -97,5 +101,17 @@ export const getTranslatedUINoficiations = (t, locale) => {
new Date(UI_NOTIFICATIONS[7].date),
),
},
8: {
...UI_NOTIFICATIONS[8],
title: t('notifications8Title'),
description: [
t('notifications8DescriptionOne'),
t('notifications8DescriptionTwo'),
],
date: new Intl.DateTimeFormat(formattedLocale).format(
new Date(UI_NOTIFICATIONS[8].date),
),
actionText: t('notifications8ActionText'),
},
};
};

@ -63,6 +63,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -53,6 +53,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -49,6 +49,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -67,7 +67,11 @@
}
},
"NotificationController": {
"notifications": {}
"notifications": {
"8": {
"isShown": true
}
}
},
"OnboardingController": {
"onboardingTabs": {},

@ -104,6 +104,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -49,6 +49,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -49,6 +49,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -53,6 +53,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -49,6 +49,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -50,6 +50,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -60,6 +60,9 @@
},
"6": {
"isShown": true
},
"8": {
"isShown": true
}
}
},

@ -176,24 +176,6 @@ describe('MetaMask', function () {
});
});
describe("Close the what's new popup", function () {
it("should show the what's new popover", async function () {
const popoverTitle = await driver.findElement(
'.popover-header__title h2',
);
assert.equal(await popoverTitle.getText(), "What's new");
});
it("should close the what's new popup", async function () {
const popover = await driver.findElement('.popover-container');
await driver.clickElement('[data-testid="popover-close"]');
await popover.waitForElementState('hidden');
});
});
describe('Import Secret Recovery Phrase', function () {
it('logs out of the vault', async function () {
await driver.clickElement('.account-menu__icon');

@ -27,7 +27,6 @@ describe('Hide token', function () {
css: '.asset-list-item__token-button',
text: '0 TST',
});
await driver.clickElement('.popover-header__button');
let assets = await driver.findElements('.asset-list-item');
assert.equal(assets.length, 2);
@ -39,7 +38,6 @@ describe('Hide token', function () {
await driver.clickElement('[data-testid="asset-options__button"]');
await driver.clickElement('[data-testid="asset-options__hide"]');
// wait for confirm hide modal to be visible
const confirmHideModal = await driver.findVisibleElement('span .modal');

@ -60,11 +60,6 @@ describe('Metamask Import UI', function () {
tag: 'button',
});
// close the what's new popup
const popover = await driver.findElement('.popover-container');
await driver.clickElement('[data-testid="popover-close"]');
await popover.waitForElementState('hidden');
// Show account information
await driver.clickElement(
'[data-testid="account-options-menu-button"]',

@ -65,13 +65,6 @@ describe('Incremental Security', function () {
tag: 'button',
});
// closes the what's new popup
const popover = await driver.findElement('.popover-container');
await driver.clickElement('[data-testid="popover-close"]');
await popover.waitForElementState('hidden');
await driver.clickElement(
'[data-testid="account-options-menu-button"]',
);

@ -0,0 +1 @@
export { default } from './ledger-instruction-field';

@ -0,0 +1,212 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
WEBHID_CONNECTED_STATUSES,
TRANSPORT_STATES,
} from '../../../../shared/constants/hardware-wallets';
import {
PLATFORM_FIREFOX,
ENVIRONMENT_TYPE_FULLSCREEN,
} from '../../../../shared/constants/app';
import {
setLedgerWebHidConnectedStatus,
getLedgerWebHidConnectedStatus,
setLedgerTransportStatus,
getLedgerTransportStatus,
} from '../../../ducks/app/app';
import Typography from '../../ui/typography/typography';
import Button from '../../ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
COLORS,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
import Dialog from '../../ui/dialog';
import {
getPlatform,
getEnvironmentType,
} from '../../../../app/scripts/lib/util';
import { getLedgerTransportType } from '../../../ducks/metamask/metamask';
import { attemptLedgerTransportCreation } from '../../../store/actions';
const renderInstructionStep = (text, show = true, color = COLORS.PRIMARY3) => {
return (
show && (
<Typography
boxProps={{ margin: 0 }}
color={color}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H7}
>
{text}
</Typography>
)
);
};
export default function LedgerInstructionField({ showDataInstruction }) {
const t = useI18nContext();
const dispatch = useDispatch();
const webHidConnectedStatus = useSelector(getLedgerWebHidConnectedStatus);
const ledgerTransportType = useSelector(getLedgerTransportType);
const transportStatus = useSelector(getLedgerTransportStatus);
const environmentType = getEnvironmentType();
const environmentTypeIsFullScreen =
environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
useEffect(() => {
const initialConnectedDeviceCheck = async () => {
if (
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
webHidConnectedStatus !== WEBHID_CONNECTED_STATUSES.CONNECTED
) {
const devices = await window.navigator.hid.getDevices();
const webHidIsConnected = devices.some(
(device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID),
);
dispatch(
setLedgerWebHidConnectedStatus(
webHidIsConnected
? WEBHID_CONNECTED_STATUSES.CONNECTED
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED,
),
);
}
};
const determineTransportStatus = async () => {
if (
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
webHidConnectedStatus === WEBHID_CONNECTED_STATUSES.CONNECTED &&
transportStatus === TRANSPORT_STATES.NONE
) {
try {
const transportedCreated = await attemptLedgerTransportCreation();
dispatch(
setLedgerTransportStatus(
transportedCreated
? TRANSPORT_STATES.VERIFIED
: TRANSPORT_STATES.UNKNOWN_FAILURE,
),
);
} catch (e) {
if (e.message.match('Failed to open the device')) {
dispatch(
setLedgerTransportStatus(TRANSPORT_STATES.DEVICE_OPEN_FAILURE),
);
} else if (e.message.match('the device is already open')) {
dispatch(setLedgerTransportStatus(TRANSPORT_STATES.VERIFIED));
} else {
dispatch(
setLedgerTransportStatus(TRANSPORT_STATES.UNKNOWN_FAILURE),
);
}
}
}
};
determineTransportStatus();
initialConnectedDeviceCheck();
}, [dispatch, ledgerTransportType, webHidConnectedStatus, transportStatus]);
useEffect(() => {
return () => {
dispatch(setLedgerTransportStatus(TRANSPORT_STATES.NONE));
};
}, [dispatch]);
const usingLedgerLive = ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE;
const usingWebHID = ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID;
const isFirefox = getPlatform() === PLATFORM_FIREFOX;
return (
<div>
<div className="confirm-detail-row">
<Dialog type="message">
<div className="ledger-live-dialog">
{renderInstructionStep(t('ledgerConnectionInstructionHeader'))}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepOne')}`,
!isFirefox && usingLedgerLive,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepTwo')}`,
!isFirefox && usingLedgerLive,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepThree')}`,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepFour')}`,
showDataInstruction,
)}
{renderInstructionStep(
<span>
<Button
type="link"
onClick={async () => {
if (environmentTypeIsFullScreen) {
window.location.reload();
} else {
global.platform.openExtensionInBrowser(null, null, true);
}
}}
>
{t('ledgerConnectionInstructionCloseOtherApps')}
</Button>
</span>,
transportStatus === TRANSPORT_STATES.DEVICE_OPEN_FAILURE,
)}
{renderInstructionStep(
<span>
<Button
type="link"
onClick={async () => {
if (environmentTypeIsFullScreen) {
const connectedDevices = await window.navigator.hid.requestDevice(
{
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
},
);
const webHidIsConnected = connectedDevices.some(
(device) =>
device.vendorId === Number(LEDGER_USB_VENDOR_ID),
);
dispatch(
setLedgerWebHidConnectedStatus({
webHidConnectedStatus: webHidIsConnected
? WEBHID_CONNECTED_STATUSES.CONNECTED
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED,
}),
);
} else {
global.platform.openExtensionInBrowser(null, null, true);
}
}}
>
{environmentTypeIsFullScreen
? t('clickToConnectLedgerViaWebHID')
: t('openFullScreenForLedgerWebHid')}
</Button>
</span>,
usingWebHID &&
webHidConnectedStatus ===
WEBHID_CONNECTED_STATUSES.NOT_CONNECTED,
COLORS.SECONDARY1,
)}
</div>
</Dialog>
</div>
</div>
);
}
LedgerInstructionField.propTypes = {
showDataInstruction: PropTypes.bool,
};

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { stripHexPrefix } from 'ethereumjs-util';
import classnames from 'classnames';
import { ObjectInspector } from 'react-inspector';
import LedgerInstructionField from '../ledger-instruction-field';
import {
ENVIRONMENT_TYPE_NOTIFICATION,
@ -36,6 +37,8 @@ export default class SignatureRequestOriginal extends Component {
sign: PropTypes.func.isRequired,
txData: PropTypes.object.isRequired,
domainMetadata: PropTypes.object,
hardwareWalletRequiresConnection: PropTypes.bool,
isLedgerWallet: PropTypes.bool,
};
state = {
@ -286,6 +289,7 @@ export default class SignatureRequestOriginal extends Component {
mostRecentOverviewPage,
sign,
txData: { type },
hardwareWalletRequiresConnection,
} = this.props;
const { metricsEvent, t } = this.context;
@ -319,6 +323,7 @@ export default class SignatureRequestOriginal extends Component {
type="primary"
large
className="request-signature__footer__sign-button"
disabled={hardwareWalletRequiresConnection}
onClick={async (event) => {
this._removeBeforeUnload();
await sign(event);
@ -347,6 +352,11 @@ export default class SignatureRequestOriginal extends Component {
<div className="request-signature__container">
{this.renderHeader()}
{this.renderBody()}
{this.props.isLedgerWallet ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField showDataInstruction />
</div>
) : null}
{this.renderFooter()}
</div>
);

@ -8,18 +8,32 @@ import {
accountsWithSendEtherInfoSelector,
conversionRateSelector,
getDomainMetadata,
doesAddressRequireLedgerHidConnection,
} from '../../../selectors';
import { getAccountByAddress } from '../../../helpers/utils/util';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import SignatureRequestOriginal from './signature-request-original.component';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {
msgParams: { from },
} = ownProps.txData;
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
state,
from,
);
const isLedgerWallet = isAddressLedger(state, from);
return {
requester: null,
requesterAddress: null,
conversionRate: conversionRateSelector(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state),
hardwareWalletRequiresConnection,
isLedgerWallet,
// not passed to component
allAccounts: accountsWithSendEtherInfoSelector(state),
domainMetadata: getDomainMetadata(state),

@ -6,6 +6,7 @@ export default class SignatureRequestFooter extends PureComponent {
static propTypes = {
cancelAction: PropTypes.func.isRequired,
signAction: PropTypes.func.isRequired,
disabled: PropTypes.boolean,
};
static contextTypes = {
@ -13,13 +14,13 @@ export default class SignatureRequestFooter extends PureComponent {
};
render() {
const { cancelAction, signAction } = this.props;
const { cancelAction, signAction, disabled = false } = this.props;
return (
<div className="signature-request-footer">
<Button onClick={cancelAction} type="secondary" large>
{this.context.t('cancel')}
</Button>
<Button onClick={signAction} type="primary" large>
<Button onClick={signAction} type="primary" disabled={disabled} large>
{this.context.t('sign')}
</Button>
</div>

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import Identicon from '../../ui/identicon';
import LedgerInstructionField from '../ledger-instruction-field';
import Header from './signature-request-header';
import Footer from './signature-request-footer';
import Message from './signature-request-message';
@ -15,10 +16,11 @@ export default class SignatureRequest extends PureComponent {
balance: PropTypes.string,
name: PropTypes.string,
}).isRequired,
isLedgerWallet: PropTypes.bool,
clearConfirmTransaction: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
sign: PropTypes.func.isRequired,
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
};
static contextTypes = {
@ -69,6 +71,8 @@ export default class SignatureRequest extends PureComponent {
},
cancel,
sign,
isLedgerWallet,
hardwareWalletRequiresConnection,
} = this.props;
const { address: fromAddress } = fromAccount;
const { message, domain = {} } = JSON.parse(data);
@ -128,8 +132,17 @@ export default class SignatureRequest extends PureComponent {
{this.formatWallet(fromAddress)}
</div>
</div>
{isLedgerWallet ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField showDataInstruction />
</div>
) : null}
<Message data={message} />
<Footer cancelAction={onCancel} signAction={onSign} />
<Footer
cancelAction={onCancel}
signAction={onSign}
disabled={hardwareWalletRequiresConnection}
/>
</div>
);
}

@ -1,12 +1,28 @@
import { connect } from 'react-redux';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { accountsWithSendEtherInfoSelector } from '../../../selectors';
import {
accountsWithSendEtherInfoSelector,
doesAddressRequireLedgerHidConnection,
} from '../../../selectors';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import { getAccountByAddress } from '../../../helpers/utils/util';
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import SignatureRequest from './signature-request.component';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const { txData } = ownProps;
const {
msgParams: { from },
} = txData;
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
state,
from,
);
const isLedgerWallet = isAddressLedger(state, from);
return {
isLedgerWallet,
hardwareWalletRequiresConnection,
// not forwarded to component
allAccounts: accountsWithSendEtherInfoSelector(state),
};
@ -19,7 +35,11 @@ function mapDispatchToProps(dispatch) {
}
function mergeProps(stateProps, dispatchProps, ownProps) {
const { allAccounts } = stateProps;
const {
allAccounts,
isLedgerWallet,
hardwareWalletRequiresConnection,
} = stateProps;
const {
signPersonalMessage,
signTypedMessage,
@ -58,6 +78,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
txData,
cancel,
sign,
isLedgerWallet,
hardwareWalletRequiresConnection,
};
}

@ -12,7 +12,10 @@ import Typography from '../../ui/typography';
import { updateViewedNotifications } from '../../../store/actions';
import { getTranslatedUINoficiations } from '../../../../shared/notifications';
import { getSortedNotificationsToShow } from '../../../selectors';
import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes';
import {
BUILD_QUOTE_ROUTE,
ADVANCED_ROUTE,
} from '../../../helpers/constants/routes';
import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
function getActionFunctionById(id, history) {
@ -38,6 +41,10 @@ function getActionFunctionById(id, history) {
url: 'https://metamask.zendesk.com/hc/en-us/articles/360060826432',
});
},
8: () => {
updateViewedNotifications({ 8: true });
history.push(ADVANCED_ROUTE);
},
};
return actionFunctions[id];

@ -1,3 +1,7 @@
import {
WEBHID_CONNECTED_STATUSES,
TRANSPORT_STATES,
} from '../../../shared/constants/hardware-wallets';
import * as actionConstants from '../../store/actionConstants';
// actionConstants
@ -48,6 +52,8 @@ export default function reduceApp(state = {}, action) {
testKey: null,
},
gasLoadingAnimationIsShowing: false,
ledgerWebHidConnectedStatus: WEBHID_CONNECTED_STATUSES.UNKNOWN,
ledgerTransportStatus: TRANSPORT_STATES.NONE,
...state,
};
@ -340,6 +346,18 @@ export default function reduceApp(state = {}, action) {
gasLoadingAnimationIsShowing: action.value,
};
case actionConstants.SET_WEBHID_CONNECTED_STATUS:
return {
...appState,
ledgerWebHidConnectedStatus: action.value,
};
case actionConstants.SET_LEDGER_TRANSPORT_STATUS:
return {
...appState,
ledgerTransportStatus: action.value,
};
default:
return appState;
}
@ -363,6 +381,14 @@ export function toggleGasLoadingAnimation(value) {
return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value };
}
export function setLedgerWebHidConnectedStatus(value) {
return { type: actionConstants.SET_WEBHID_CONNECTED_STATUS, value };
}
export function setLedgerTransportStatus(value) {
return { type: actionConstants.SET_LEDGER_TRANSPORT_STATUS, value };
}
// Selectors
export function getQrCodeData(state) {
return state.appState.qrCodeData;
@ -371,3 +397,11 @@ export function getQrCodeData(state) {
export function getGasLoadingAnimationIsShowing(state) {
return state.appState.gasLoadingAnimationIsShowing;
}
export function getLedgerWebHidConnectedStatus(state) {
return state.appState.ledgerWebHidConnectedStatus;
}
export function getLedgerTransportStatus(state) {
return state.appState.ledgerTransportStatus;
}

@ -1,4 +1,4 @@
import { addHexPrefix, isHexString } from 'ethereumjs-util';
import { addHexPrefix, isHexString, stripHexPrefix } from 'ethereumjs-util';
import * as actionConstants from '../../store/actionConstants';
import { ALERT_TYPES } from '../../../shared/constants/alerts';
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
@ -10,7 +10,9 @@ import {
import { updateTransaction } from '../../store/actions';
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
export default function reduceMetamask(state = {}, action) {
const metamaskState = {
@ -340,3 +342,59 @@ export function getIsUnlocked(state) {
export function getSeedPhraseBackedUp(state) {
return state.metamask.seedPhraseBackedUp;
}
/**
* Given the redux state object and an address, finds a keyring that contains that address, if one exists
*
* @param {Object} state - the redux state object
* @param {string} address - the address to search for among the keyring addresses
* @returns {Object|undefined} The keyring which contains the passed address, or undefined
*/
export function findKeyringForAddress(state, address) {
const keyring = state.metamask.keyrings.find((kr) => {
return kr.accounts.some((account) => {
return (
isEqualCaseInsensitive(account, addHexPrefix(address)) ||
isEqualCaseInsensitive(account, stripHexPrefix(address))
);
});
});
return keyring;
}
/**
* Given the redux state object, returns the users preferred ledger transport type
*
* @param {Object} state - the redux state object
* @returns {string} The users preferred ledger transport type. One of'ledgerLive', 'webhid' or 'u2f'
*/
export function getLedgerTransportType(state) {
return state.metamask.ledgerTransportType;
}
/**
* Given the redux state object and an address, returns a boolean indicating whether the passed address is part of a Ledger keyring
*
* @param {Object} state - the redux state object
* @param {string} address - the address to search for among all keyring addresses
* @returns {boolean} true if the passed address is part of a ledger keyring, and false otherwise
*/
export function isAddressLedger(state, address) {
const keyring = findKeyringForAddress(state, address);
return keyring?.type === KEYRING_TYPES.LEDGER;
}
/**
* Given the redux state object, returns a boolean indicating whether the user has any Ledger accounts added to MetaMask (i.e. Ledger keyrings
* in state)
*
* @param {Object} state - the redux state object
* @returns {boolean} true if the user has a Ledger account and false otherwise
*/
export function doesUserHaveALedgerAccount(state) {
return state.metamask.keyrings.some((kr) => {
return kr.type === KEYRING_TYPES.LEDGER;
});
}

@ -14,6 +14,7 @@ import {
} from '../../../helpers/constants/design-system';
import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button';
import LedgerInstructionField from '../../../components/app/ledger-instruction-field';
export default class ConfirmApproveContent extends Component {
static contextTypes = {
@ -44,6 +45,8 @@ export default class ConfirmApproveContent extends Component {
nextNonce: PropTypes.number,
showCustomizeNonceModal: PropTypes.func,
warning: PropTypes.string,
txData: PropTypes.object,
ledgerWalletRequiredHidConnection: PropTypes.bool,
};
state = {
@ -238,6 +241,8 @@ export default class ConfirmApproveContent extends Component {
tokenBalance,
useNonceField,
warning,
txData,
ledgerWalletRequiredHidConnection,
} = this.props;
const { showFullTxDetails } = this.state;
@ -346,6 +351,14 @@ export default class ConfirmApproveContent extends Component {
})}
</div>
{ledgerWalletRequiredHidConnection ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField
showDataInstruction={Boolean(txData.txParams?.data)}
/>
</div>
) : null}
{showFullTxDetails ? (
<div className="confirm-approve-content__full-tx-content">
<div className="confirm-approve-content__permission">

@ -161,6 +161,11 @@
}
}
&__ledger-instruction-wrapper {
padding-left: 10px;
padding-right: 10px;
}
&__transaction-details-content {
display: flex;
flex-flow: row;

@ -24,6 +24,7 @@ import {
getUseNonceField,
getCustomNonceValue,
getNextSuggestedNonce,
doesAddressRequireLedgerHidConnection,
} from '../../selectors';
import { useApproveTransaction } from '../../hooks/useApproveTransaction';
@ -35,12 +36,18 @@ import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content';
const doesAddressRequireLedgerHidConnectionByFromAddress = (address) => (
state,
) => {
return doesAddressRequireLedgerHidConnection(state, address);
};
export default function ConfirmApprove() {
const dispatch = useDispatch();
const { id: paramsTransactionId } = useParams();
const {
id: transactionId,
txParams: { to: tokenAddress, data } = {},
txParams: { to: tokenAddress, data, from } = {},
} = useSelector(txDataSelector);
const currentCurrency = useSelector(getCurrentCurrency);
@ -52,6 +59,10 @@ export default function ConfirmApprove() {
const nextNonce = useSelector(getNextSuggestedNonce);
const customNonceValue = useSelector(getCustomNonceValue);
const ledgerWalletRequiredHidConnection = useSelector(
doesAddressRequireLedgerHidConnectionByFromAddress(from),
);
const transaction =
currentNetworkTxList.find(
({ id }) => id === (Number(paramsTransactionId) || transactionId),
@ -207,6 +218,10 @@ export default function ConfirmApprove() {
)
}
warning={submitWarning}
txData={transaction}
ledgerWalletRequiredHidConnection={
ledgerWalletRequiredHidConnection
}
/>
{showCustomizeGasPopover && (
<EditGasPopover

@ -35,12 +35,11 @@ import TransactionDetailItem from '../../components/app/transaction-detail-item/
import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import Dialog from '../../components/ui/dialog';
import LedgerInstructionField from '../../components/app/ledger-instruction-field';
import {
COLORS,
FONT_STYLE,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../helpers/constants/design-system';
import {
@ -127,9 +126,9 @@ export default class ConfirmTransactionBase extends Component {
isMainnet: PropTypes.bool,
gasFeeIsCustom: PropTypes.bool,
showLedgerSteps: PropTypes.bool.isRequired,
isFirefox: PropTypes.bool.isRequired,
nativeCurrency: PropTypes.string,
supportsEIP1559: PropTypes.bool,
hardwareWalletRequiresConnection: PropTypes.bool,
};
state = {
@ -310,7 +309,6 @@ export default class ConfirmTransactionBase extends Component {
maxPriorityFeePerGas,
isMainnet,
showLedgerSteps,
isFirefox,
supportsEIP1559,
} = this.props;
const { t } = this.context;
@ -405,46 +403,6 @@ export default class ConfirmTransactionBase extends Component {
</div>
) : null;
const renderLedgerLiveStep = (text, show = true) => {
return (
show && (
<Typography
boxProps={{ margin: 0 }}
color={COLORS.PRIMARY3}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H7}
>
{text}
</Typography>
)
);
};
const ledgerInstructionField = showLedgerSteps ? (
<div>
<div className="confirm-detail-row">
<Dialog type="message">
<div className="ledger-live-dialog">
{renderLedgerLiveStep(t('ledgerLiveDialogHeader'))}
{renderLedgerLiveStep(
`- ${t('ledgerLiveDialogStepOne')}`,
!isFirefox,
)}
{renderLedgerLiveStep(
`- ${t('ledgerLiveDialogStepTwo')}`,
!isFirefox,
)}
{renderLedgerLiveStep(`- ${t('ledgerLiveDialogStepThree')}`)}
{renderLedgerLiveStep(
`- ${t('ledgerLiveDialogStepFour')}`,
Boolean(txData.txParams?.data),
)}
</div>
</Dialog>
</div>
</div>
) : null;
return (
<div className="confirm-page-container-content__details">
<TransactionDetail
@ -574,7 +532,11 @@ export default class ConfirmTransactionBase extends Component {
]}
/>
{nonceField}
{ledgerInstructionField}
{showLedgerSteps ? (
<LedgerInstructionField
showDataInstruction={Boolean(txData.txParams?.data)}
/>
) : null}
</div>
);
}
@ -916,6 +878,7 @@ export default class ConfirmTransactionBase extends Component {
gasIsLoading,
gasFeeIsCustom,
nativeCurrency,
hardwareWalletRequiresConnection,
} = this.props;
const {
submitting,
@ -981,7 +944,12 @@ export default class ConfirmTransactionBase extends Component {
lastTx={lastTx}
ofText={ofText}
requestsWaitingText={requestsWaitingText}
disabled={!valid || submitting || (gasIsLoading && !gasFeeIsCustom)}
disabled={
!valid ||
submitting ||
hardwareWalletRequiresConnection ||
(gasIsLoading && !gasFeeIsCustom)
}
onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()}

@ -28,24 +28,24 @@ import {
getShouldShowFiat,
checkNetworkAndAccountSupports1559,
getPreferences,
getHardwareWalletType,
doesAddressRequireLedgerHidConnection,
getUseTokenDetection,
getTokenList,
} from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import {
transactionMatchesNetwork,
txParamsAreDappSuggested,
} from '../../../shared/modules/transaction.utils';
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
import { getPlatform } from '../../../app/scripts/lib/util';
import { PLATFORM_FIREFOX } from '../../../shared/constants/app';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import {
isAddressLedger,
updateTransactionGasFees,
getIsGasEstimatesLoading,
getNativeCurrency,
} from '../../ducks/metamask/metamask';
import {
transactionMatchesNetwork,
txParamsAreDappSuggested,
} from '../../../shared/modules/transaction.utils';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import ConfirmTransactionBase from './confirm-transaction-base.component';
@ -170,10 +170,14 @@ const mapStateToProps = (state, ownProps) => {
const gasFeeIsCustom =
fullTxData.userFeeLevel === 'custom' ||
txParamsAreDappSuggested(fullTxData);
const showLedgerSteps = getHardwareWalletType(state) === KEYRING_TYPES.LEDGER;
const isFirefox = getPlatform() === PLATFORM_FIREFOX;
const fromAddressIsLedger = isAddressLedger(state, fromAddress);
const nativeCurrency = getNativeCurrency(state);
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
state,
fromAddress,
);
return {
balance,
fromAddress,
@ -219,9 +223,9 @@ const mapStateToProps = (state, ownProps) => {
maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas,
baseFeePerGas: gasEstimationObject.baseFeePerGas,
gasFeeIsCustom,
showLedgerSteps,
isFirefox,
showLedgerSteps: fromAddressIsLedger,
nativeCurrency,
hardwareWalletRequiresConnection,
};
};

@ -11,6 +11,7 @@ import {
import { formatBalance } from '../../../helpers/utils/util';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { SECOND } from '../../../../shared/constants/time';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import SelectHardware from './select-hardware';
import AccountList from './account-list';
@ -26,6 +27,10 @@ const HD_PATHS = [
];
class ConnectHardwareForm extends Component {
static contextTypes = {
t: PropTypes.func,
};
state = {
error: null,
selectedAccounts: [],
@ -106,7 +111,7 @@ class ConnectHardwareForm extends Component {
getPage = (device, page, hdPath) => {
this.props
.connectHardware(device, page, hdPath)
.connectHardware(device, page, hdPath, this.context.t)
.then((accounts) => {
if (accounts.length) {
// If we just loaded the accounts for the first time
@ -262,7 +267,7 @@ class ConnectHardwareForm extends Component {
<SelectHardware
connectToHardwareWallet={this.connectToHardwareWallet}
browserSupported={this.state.browserSupported}
useLedgerLive={this.props.useLedgerLive}
ledgerTransportType={this.props.ledgerTransportType}
/>
);
}
@ -313,7 +318,7 @@ ConnectHardwareForm.propTypes = {
connectedAccounts: PropTypes.array.isRequired,
defaultHdPaths: PropTypes.object,
mostRecentOverviewPage: PropTypes.string.isRequired,
useLedgerLive: PropTypes.bool.isRequired,
ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
};
const mapStateToProps = (state) => ({
@ -323,7 +328,7 @@ const mapStateToProps = (state) => ({
connectedAccounts: getMetaMaskAccountsConnected(state),
defaultHdPaths: state.appState.defaultHdPaths,
mostRecentOverviewPage: getMostRecentOverviewPage(state),
useLedgerLive: state.metamask.useLedgerLive,
ledgerTransportType: state.metamask.ledgerTransportType,
});
const mapDispatchToProps = (dispatch) => {
@ -331,8 +336,8 @@ const mapDispatchToProps = (dispatch) => {
setHardwareWalletDefaultHdPath: ({ device, path }) => {
return dispatch(actions.setHardwareWalletDefaultHdPath({ device, path }));
},
connectHardware: (deviceName, page, hdPath) => {
return dispatch(actions.connectHardware(deviceName, page, hdPath));
connectHardware: (deviceName, page, hdPath, t) => {
return dispatch(actions.connectHardware(deviceName, page, hdPath, t));
},
checkHardwareStatus: (deviceName, hdPath) => {
return dispatch(actions.checkHardwareStatus(deviceName, hdPath));

@ -2,6 +2,7 @@ import classnames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from '../../../components/ui/button';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
export default class SelectHardware extends Component {
static contextTypes = {
@ -11,7 +12,7 @@ export default class SelectHardware extends Component {
static propTypes = {
connectToHardwareWallet: PropTypes.func.isRequired,
browserSupported: PropTypes.bool.isRequired,
useLedgerLive: PropTypes.bool.isRequired,
ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
};
state = {
@ -136,7 +137,7 @@ export default class SelectHardware extends Component {
renderLedgerTutorialSteps() {
const steps = [];
if (this.props.useLedgerLive) {
if (this.props.ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE) {
steps.push({
title: this.context.t('step1LedgerWallet'),
message: this.context.t('step1LedgerWalletMsg', [

@ -1,5 +1,6 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import SelectHardware from './select-hardware';
export default {
@ -14,7 +15,7 @@ export const SelectHardwareComponent = () => {
connectToHardwareWallet={(selectedDevice) =>
action(`Continue connect to ${selectedDevice}`)()
}
useLedgerLive
ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
/>
);
};
@ -23,7 +24,7 @@ export const BrowserNotSupported = () => {
<SelectHardware
browserSupported={false}
connectToHardwareWallet={() => undefined}
useLedgerLive
ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
/>
);
};

@ -6,10 +6,18 @@ import ToggleButton from '../../../components/ui/toggle-button';
import TextField from '../../../components/ui/text-field';
import Button from '../../../components/ui/button';
import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes';
import Dropdown from '../../../components/ui/dropdown';
import Dialog from '../../../components/ui/dialog';
import { getPlatform } from '../../../../app/scripts/lib/util';
import { PLATFORM_FIREFOX } from '../../../../shared/constants/app';
import {
LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
} from '../../../../shared/constants/hardware-wallets';
export default class AdvancedTab extends PureComponent {
static contextTypes = {
t: PropTypes.func,
@ -36,10 +44,11 @@ export default class AdvancedTab extends PureComponent {
threeBoxDisabled: PropTypes.bool.isRequired,
setIpfsGateway: PropTypes.func.isRequired,
ipfsGateway: PropTypes.string.isRequired,
useLedgerLive: PropTypes.bool.isRequired,
ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
setLedgerLivePreference: PropTypes.func.isRequired,
setDismissSeedBackUpReminder: PropTypes.func.isRequired,
dismissSeedBackUpReminder: PropTypes.bool.isRequired,
userHasALedgerAccount: PropTypes.bool.isRequired,
};
state = {
@ -47,6 +56,7 @@ export default class AdvancedTab extends PureComponent {
lockTimeError: '',
ipfsGateway: this.props.ipfsGateway,
ipfsGatewayError: '',
showLedgerTransportWarning: false,
};
renderMobileSync() {
@ -393,25 +403,91 @@ export default class AdvancedTab extends PureComponent {
renderLedgerLiveControl() {
const { t } = this.context;
const { useLedgerLive, setLedgerLivePreference } = this.props;
const {
ledgerTransportType,
setLedgerLivePreference,
userHasALedgerAccount,
} = this.props;
const LEDGER_TRANSPORT_NAMES = {
LIVE: t('ledgerLive'),
WEBHID: t('webhid'),
U2F: t('u2f'),
};
const transportTypeOptions = [
{
name: LEDGER_TRANSPORT_NAMES.LIVE,
value: LEDGER_TRANSPORT_TYPES.LIVE,
},
{
name: LEDGER_TRANSPORT_NAMES.U2F,
value: LEDGER_TRANSPORT_TYPES.U2F,
},
];
if (window.navigator.hid) {
transportTypeOptions.push({
name: LEDGER_TRANSPORT_NAMES.WEBHID,
value: LEDGER_TRANSPORT_TYPES.WEBHID,
});
}
const recommendedLedgerOption = window.navigator.hid
? LEDGER_TRANSPORT_NAMES.WEBHID
: LEDGER_TRANSPORT_NAMES.U2F;
return (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{t('ledgerLiveAdvancedSetting')}</span>
<span>{t('preferredLedgerConnectionType')}</span>
<div className="settings-page__content-description">
{t('ledgerLiveAdvancedSettingDescription')}
{t('ledgerConnectionPreferenceDescription', [
recommendedLedgerOption,
<Button
key="ledger-connection-settings-learn-more"
type="link"
href="https://metamask.zendesk.com/hc/en-us/articles/360020394612-How-to-connect-a-Trezor-or-Ledger-Hardware-Wallet"
target="_blank"
rel="noopener noreferrer"
className="settings-page__inline-link"
>
{t('learnMore')}
</Button>,
])}
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={useLedgerLive}
onToggle={(value) => setLedgerLivePreference(!value)}
offLabel={t('off')}
onLabel={t('on')}
disabled={getPlatform() === PLATFORM_FIREFOX}
<Dropdown
id="select-ledger-transport-type"
options={transportTypeOptions}
selectedOption={ledgerTransportType}
onChange={async (transportType) => {
if (
ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE &&
transportType === LEDGER_TRANSPORT_TYPES.WEBHID
) {
this.setState({ showLedgerTransportWarning: true });
}
setLedgerLivePreference(transportType);
if (
transportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
userHasALedgerAccount
) {
await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
}
}}
/>
{this.state.showLedgerTransportWarning ? (
<Dialog type="message">
<div className="settings-page__content-item-dialog">
{t('ledgerTransportChangeWarning')}
</div>
</Dialog>
) : null}
</div>
</div>
</div>
@ -531,6 +607,8 @@ export default class AdvancedTab extends PureComponent {
render() {
const { warning } = this.props;
const notUsingFirefox = getPlatform() !== PLATFORM_FIREFOX;
return (
<div className="settings-page__body">
{warning ? <div className="settings-tab__error">{warning}</div> : null}
@ -544,7 +622,7 @@ export default class AdvancedTab extends PureComponent {
{this.renderAutoLockTimeLimit()}
{this.renderThreeBoxControl()}
{this.renderIpfsGatewayControl()}
{this.renderLedgerLiveControl()}
{notUsingFirefox ? this.renderLedgerLiveControl() : null}
{this.renderDismissSeedBackupReminderControl()}
</div>
);

@ -2,6 +2,7 @@ import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import TextField from '../../../components/ui/text-field';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import AdvancedTab from './advanced-tab.component';
describe('AdvancedTab Component', () => {
@ -15,7 +16,7 @@ describe('AdvancedTab Component', () => {
setThreeBoxSyncingPermission={() => undefined}
threeBoxDisabled
threeBoxSyncingAllowed={false}
useLedgerLive={false}
ledgerTransportType={LEDGER_TRANSPORT_TYPES.U2F}
setLedgerLivePreference={() => undefined}
setDismissSeedBackUpReminder={() => undefined}
dismissSeedBackUpReminder={false}
@ -41,7 +42,7 @@ describe('AdvancedTab Component', () => {
setThreeBoxSyncingPermission={() => undefined}
threeBoxDisabled
threeBoxSyncingAllowed={false}
useLedgerLive={false}
ledgerTransportType={LEDGER_TRANSPORT_TYPES.U2F}
setLedgerLivePreference={() => undefined}
setDismissSeedBackUpReminder={() => undefined}
dismissSeedBackUpReminder={false}

@ -15,6 +15,7 @@ import {
setDismissSeedBackUpReminder,
} from '../../../store/actions';
import { getPreferences } from '../../../selectors';
import { doesUserHaveALedgerAccount } from '../../../ducks/metamask/metamask';
import AdvancedTab from './advanced-tab.component';
export const mapStateToProps = (state) => {
@ -28,11 +29,13 @@ export const mapStateToProps = (state) => {
threeBoxDisabled,
useNonceField,
ipfsGateway,
useLedgerLive,
ledgerTransportType,
dismissSeedBackUpReminder,
} = metamask;
const { showFiatInTestnets, autoLockTimeLimit } = getPreferences(state);
const userHasALedgerAccount = doesUserHaveALedgerAccount(state);
return {
warning,
sendHexData,
@ -43,8 +46,9 @@ export const mapStateToProps = (state) => {
threeBoxDisabled,
useNonceField,
ipfsGateway,
useLedgerLive,
ledgerTransportType,
dismissSeedBackUpReminder,
userHasALedgerAccount,
};
};

@ -190,6 +190,10 @@
cursor: not-allowed;
opacity: 0.5;
}
& .dialog {
margin-top: 10px;
}
}
&__content-label {
@ -233,6 +237,13 @@
margin-left: 1.875rem;
}
&__inline-link {
@include H6;
display: initial;
padding: 0;
}
&--selected {
.settings-page {
&__content {

@ -1,14 +1,17 @@
import { stripHexPrefix } from 'ethereumjs-util';
import { createSelector } from 'reselect';
import { addHexPrefix } from '../../app/scripts/lib/util';
import {
MAINNET_CHAIN_ID,
BSC_CHAIN_ID,
TEST_CHAINS,
NETWORK_TYPE_RPC,
NATIVE_CURRENCY_TOKEN_IMAGE_MAP,
} from '../../shared/constants/network';
import { KEYRING_TYPES } from '../../shared/constants/hardware-wallets';
import {
KEYRING_TYPES,
WEBHID_CONNECTED_STATUSES,
LEDGER_TRANSPORT_TYPES,
TRANSPORT_STATES,
} from '../../shared/constants/hardware-wallets';
import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
@ -36,7 +39,14 @@ import {
getConversionRate,
isNotEIP1559Network,
isEIP1559Network,
getLedgerTransportType,
isAddressLedger,
findKeyringForAddress,
} from '../ducks/metamask/metamask';
import {
getLedgerWebHidConnectedStatus,
getLedgerTransportStatus,
} from '../ducks/app/app';
/**
* One of the only remaining valid uses of selecting the network subkey of the
@ -77,14 +87,7 @@ export function getCurrentKeyring(state) {
return null;
}
const simpleAddress = stripHexPrefix(identity.address).toLowerCase();
const keyring = state.metamask.keyrings.find((kr) => {
return (
kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
);
});
const keyring = findKeyringForAddress(state, identity.address);
return keyring;
}
@ -574,15 +577,19 @@ export function getShowWhatsNewPopup(state) {
function getAllowedNotificationIds(state) {
const currentKeyring = getCurrentKeyring(state);
const currentKeyringIsLedger = currentKeyring?.type === KEYRING_TYPES.LEDGER;
const supportsWebHid = window.navigator.hid !== undefined;
const currentlyUsingLedgerLive =
getLedgerTransportType(state) === LEDGER_TRANSPORT_TYPES.LIVE;
return {
1: true,
2: true,
3: true,
4: getCurrentChainId(state) === BSC_CHAIN_ID,
5: true,
6: currentKeyringIsLedger,
7: currentKeyringIsLedger,
1: false,
2: false,
3: false,
4: false,
5: false,
6: false,
7: false,
8: supportsWebHid && currentKeyringIsLedger && currentlyUsingLedgerLive,
};
}
@ -646,3 +653,21 @@ export function getUseTokenDetection(state) {
export function getTokenList(state) {
return state.metamask.tokenList;
}
export function doesAddressRequireLedgerHidConnection(state, address) {
const addressIsLedger = isAddressLedger(state, address);
const transportTypePreferenceIsWebHID =
getLedgerTransportType(state) === LEDGER_TRANSPORT_TYPES.WEBHID;
const webHidIsNotConnected =
getLedgerWebHidConnectedStatus(state) !==
WEBHID_CONNECTED_STATUSES.CONNECTED;
const ledgerTransportStatus = getLedgerTransportStatus(state);
const transportIsNotSuccessfullyCreated =
ledgerTransportStatus !== TRANSPORT_STATES.VERIFIED;
return (
addressIsLedger &&
transportTypePreferenceIsWebHID &&
(webHidIsNotConnected || transportIsNotSuccessfullyCreated)
);
}

@ -77,6 +77,11 @@ export const COMPLETE_ONBOARDING = 'COMPLETE_ONBOARDING';
export const SET_MOUSE_USER_STATE = 'SET_MOUSE_USER_STATE';
// Ledger
export const SET_WEBHID_CONNECTED_STATUS = 'SET_WEBHID_CONNECTED_STATUS';
export const SET_LEDGER_TRANSPORT_STATUS = 'SET_LEDGER_TRANSPORT_STATUS';
// Network
export const SET_PENDING_TOKENS = 'SET_PENDING_TOKENS';
export const CLEAR_PENDING_TOKENS = 'CLEAR_PENDING_TOKENS';

@ -28,6 +28,10 @@ import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import {
LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
} from '../../shared/constants/hardware-wallets';
import * as actionConstants from './actionConstants';
let background = null;
@ -395,15 +399,35 @@ export function forgetDevice(deviceName) {
};
}
export function connectHardware(deviceName, page, hdPath) {
export function connectHardware(deviceName, page, hdPath, t) {
log.debug(`background.connectHardware`, deviceName, page, hdPath);
return async (dispatch) => {
return async (dispatch, getState) => {
const { ledgerTransportType } = getState().metamask;
dispatch(
showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`),
);
let accounts;
try {
if (deviceName === 'ledger') {
await promisifiedBackground.establishLedgerTransportPreference();
}
if (
deviceName === 'ledger' &&
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID
) {
const connectedDevices = await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
const userApprovedWebHidConnection = connectedDevices.some(
(device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID),
);
if (!userApprovedWebHidConnection) {
throw new Error(t('ledgerWebHIDNotConnectedErrorMessage'));
}
}
accounts = await promisifiedBackground.connectHardware(
deviceName,
page,
@ -411,8 +435,17 @@ export function connectHardware(deviceName, page, hdPath) {
);
} catch (error) {
log.error(error);
dispatch(displayWarning(error.message));
throw error;
if (
deviceName === 'ledger' &&
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
error.message.match('Failed to open the device')
) {
dispatch(displayWarning(t('ledgerDeviceOpenFailureMessage')));
throw new Error(t('ledgerDeviceOpenFailureMessage'));
} else {
dispatch(displayWarning(error.message));
throw error;
}
} finally {
dispatch(hideLoadingIndication());
}
@ -2745,11 +2778,15 @@ export function getCurrentWindowTab() {
export function setLedgerLivePreference(value) {
return async (dispatch) => {
dispatch(showLoadingIndication());
await promisifiedBackground.setLedgerLivePreference(value);
await promisifiedBackground.setLedgerTransportPreference(value);
dispatch(hideLoadingIndication());
};
}
export async function attemptLedgerTransportCreation() {
return await promisifiedBackground.attemptLedgerTransportCreation();
}
export function captureSingleException(error) {
return async (dispatch, getState) => {
const { singleExceptions } = getState().appState;

@ -548,6 +548,8 @@ describe('Actions', () => {
(_, __, ___, cb) => cb(),
);
background.establishLedgerTransportPreference.callsFake((cb) => cb());
actions._setBackgroundConnection(background);
await store.dispatch(
@ -563,6 +565,8 @@ describe('Actions', () => {
cb(new Error('error')),
);
background.establishLedgerTransportPreference.callsFake((cb) => cb());
actions._setBackgroundConnection(background);
const expectedActions = [

@ -2821,10 +2821,10 @@
resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-6.0.0.tgz#ec53e8ab278073e882411ed89705bc7d06b78c81"
integrity sha512-LyakGYGwM8UQOGhwWa+5erAI1hXuiTgf/y7USzOomX6H9KiuY09IAUYnPh7ToPG2sedD2F48UF1bUm8yvCoZOw==
"@metamask/eth-ledger-bridge-keyring@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.7.0.tgz#7d80e1e3dfab91ba2b6a1a2a5e352320e948b568"
integrity sha512-0UOEb/c3/fkatDK+se3gOHaGQ0RTRLbG5DqsoeowZ/JcO4wcMxBhOiIgOY4domOqUTekKKVPNC7Pc0mHpM9sAQ==
"@metamask/eth-ledger-bridge-keyring@^0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.10.0.tgz#9d5103be22221f4ef71393a2e11f24b788e343a5"
integrity sha512-ewcnEFmIL2lkUta811yQeJVWhTjll9U62GdbuauvxdQ0c6VBGZnf02GU3gcxyMOcEvZBnlU+d5LWpURQA8iNZQ==
dependencies:
"@ethereumjs/tx" "^3.2.0"
eth-sig-util "^2.0.0"
@ -17221,9 +17221,9 @@ keygrip@~1.1.0:
tsscmp "1.0.6"
keypair@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/keypair/-/keypair-1.0.1.tgz#7603719270afb6564ed38a22087a06fc9aa4ea1b"
integrity sha1-dgNxknCvtlZO04oiCHoG/Jqk6hs=
version "1.0.4"
resolved "https://registry.yarnpkg.com/keypair/-/keypair-1.0.4.tgz#a749a45f388593f3950f18b3757d32a93bd8ce83"
integrity sha512-zwhgOhhniaL7oxMgUMKKw5219PWWABMO+dgMnzJOQ2/5L3XJtTJGhW2PEXlxXj9zaccdReZJZ83+4NPhVfNVDg==
keyv@^3.0.0:
version "3.1.0"
@ -26667,9 +26667,9 @@ tmp@^0.0.33:
os-tmpdir "~1.0.2"
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
to-absolute-glob@^2.0.0:
version "2.0.2"
@ -27829,9 +27829,9 @@ vm-browserify@^1.0.0, vm-browserify@^1.0.1:
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vm2@^3.9.3:
version "3.9.3"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.3.tgz#29917f6cc081cc43a3f580c26c5b553fd3c91f40"
integrity sha512-smLS+18RjXYMl9joyJxMNI9l4w7biW8ilSDaVRvFBDwOH8P0BK1ognFQTpg0wyQ6wIKLTblHJvROW692L/E53Q==
version "3.9.5"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496"
integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==
w3c-hr-time@^1.0.2:
version "1.0.2"

Loading…
Cancel
Save