1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-26 04:20:53 +01:00

Connect Ledger via WebHID (#12411)

* Connect ledger via webhid if that option is available

* Explicitly setting preference for webhid

* Use ledgerTransportType enum instead of booleans for ledger live and webhid preferences

* Use single setLEdgerTransport preference methods and property

* Temp

* Lint fix

* Unit test fix

* Remove async keyword from setLedgerTransportPreference function definition in preferences controller

* Fix ledgelive setting toggle logic

* Migrate useLedgerLive preference property to ledgerTransportType

* Use shared constants for ledger transport type enums

* Use constant for ledger usb vendor id

* Use correct property to check if ledgerLive preference is set when deciding whether to ask for webhid connection

* Update eth-ledger-bridge-keyring to v0.9.0

* Only show ledger live transaction helper messages if using ledger live

* Only show ledger live part of tutorial if ledger live setting is on

* Fix ledger related prop type errors

* Explicitly use u2f enum instead of empty string as a transport type; default transport type to webhid if available; use constants for u2f and webhid

* Cleanup

* Wrap ledger webhid device request in try/catch

* Clean up

* Lint fix

* Ensure user can easily connect their ledger wallet when they need to.

* Fix locales

* Fix/improve locales changes

* Remove unused isFirefox property from confirm-transaction-base.container.js

* Disable transaction and message signing confirmation if ledger webhid requires connection

* Ensure translation keys for ledger connection options in settings dropdown can be properly detected by verify-locales

* Drop .component from ledger-instruction-field file name

* Move renderLedgerLiveStep to module scope

* Remove ledgerLive from function and message names in ledger-instruction-field

* Wrap ledger connection logic in ledger-instruction-field in try catch

* Clean up signature-request.component.js

* Check whether the signing address, and not the selected address, is a ledger account in singature-request.container

* Ensure ledger instructions and webhid connection button are shown on signature-request-original signatures

* Improve webhid selection handling in select-ledger-transport-type onChange handler

* Move metamask redux focused ledger selectors to metamask duck

* Lint fix

* Use async await in checkWebHidStatusRef.current

* Remove unnecessary use of ref in ledger-instruction-field.js

* Lint fix

* Remove unnecessary try/catch in ledger-instruction-field.js

* Check if from address, not selected address, is from a ledger account in confirm-approve

* Move findKeyringForAddress to metamask duck

* Fix typo in function name

* Ensure isEqualCaseInsensitive handles possible differences in address casing

* Fix Learn More link size in advanced settings tab

* Update app/scripts/migrations/066.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Update ui/pages/settings/advanced-tab/advanced-tab.component.test.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Add jsdoc comments for new selectors

* Use jest.spyOn for mocking navigator in ledger webhid migration tests

* Use LEDGER_TRANSPORT_TYPES values to set proptype of ledgerTransportType

* Use LEDGER_TRANSPORT_TYPES values to set proptype of ledgerTransportType

* Fix font size of link in ledger connection description in advanced settings

* Fix return type in setLedgerTransportPreference comment

* Clean up connectHardware code for webhid connection in actions.js

* Update app/scripts/migrations/066.test.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Update ui/ducks/metamask/metamask.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Add migration test for when useLedgerLive is true in a browser that supports webhid

* Lint fix

* Fix inline-link size

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
Dan J Miller 2021-10-21 16:47:03 -02:30 committed by ryanml
parent bd83f43ae1
commit 904dad256f
44 changed files with 773 additions and 214 deletions

View File

@ -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,42 @@
"ledgerAccountRestriction": {
"message": "You need to make use your last account before you can add a new one."
},
"ledgerLiveAdvancedSetting": {
"message": "Use Ledger Live"
"ledgerConnectionInstructionHeader": {
"message": "Prior to clicking confirm:"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "The new Ledger Live bridge allows you to more easily use your Ledger. Only available in Chrome."
"ledgerConnectionInstructionStepFour": {
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device"
},
"ledgerConnectionInstructionStepOne": {
"message": "Enable Use Ledger Live under Settings > Advanced"
},
"ledgerConnectionInstructionStepThree": {
"message": "Plug in your Ledger device and select the Ethereum app"
},
"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."
},
"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"
},
"ledgerLiveDialogHeader": {
"message": "Prior to clicking confirm:"
},
"ledgerLiveDialogStepFour": {
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device"
},
"ledgerLiveDialogStepOne": {
"message": "Enable Use Ledger Live under Settings > Advanced"
},
"ledgerLiveDialogStepThree": {
"message": "Plug in your Ledger device and select the Ethereum app"
},
"ledgerLiveDialogStepTwo": {
"message": "Open and unlock 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."
},
"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, lets get set up!"
},
@ -1710,6 +1720,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 +1786,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 +2835,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 +2980,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"
},

View File

@ -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"
},

View File

@ -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"
},

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": {
"message": "नया खाता जोड़ने से पहले आपको अपने अंतिम खाते का उपयोग करना होगा।"
},
"ledgerLiveAdvancedSetting": {
"message": "Ledger Live का उपयोग करें"
},
"ledgerLiveAdvancedSettingDescription": {
"message": "नया Ledger Live ब्रिज आपको अपने लेजर का अधिक आसानी से उपयोग करने की अनुमति देता है। केवल Chrome में उपलब्ध है।"
},
"ledgerLiveApp": {
"message": "Ledger Live ऐप"
},

View File

@ -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"
},

View File

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

View File

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

View File

@ -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"
},

View File

@ -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"
},

View File

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

View File

@ -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"
},

View File

@ -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;
}
/**

View File

@ -841,7 +841,10 @@ export default class MetamaskController extends EventEmitter {
this.unlockHardwareWalletAccount,
this,
),
setLedgerLivePreference: nodeify(this.setLedgerLivePreference, this),
setLedgerTransportPreference: nodeify(
this.setLedgerTransportPreference,
this,
),
// mobile
fetchInfoToSync: nodeify(this.fetchInfoToSync, this),
@ -1480,9 +1483,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();
}
@ -2984,16 +2987,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;
});
}

View File

@ -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;
}

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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.9.0",
"@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0",
"@metamask/jazzicon": "^2.0.0",

View File

@ -7,3 +7,20 @@ 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',
};

View File

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

View File

@ -0,0 +1,153 @@
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,
} from '../../../../shared/constants/hardware-wallets';
import {
PLATFORM_FIREFOX,
ENVIRONMENT_TYPE_FULLSCREEN,
} from '../../../../shared/constants/app';
import {
setLedgerWebHidConnectedStatus,
getLedgerWebHidConnectedStatus,
} 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';
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 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,
),
);
}
};
initialConnectedDeviceCheck();
}, [dispatch, ledgerTransportType, webHidConnectedStatus]);
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) {
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,
};

View File

@ -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>
);

View File

@ -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),

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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,
};
}

View File

@ -1,3 +1,4 @@
import { WEBHID_CONNECTED_STATUSES } from '../../../shared/constants/hardware-wallets';
import * as actionConstants from '../../store/actionConstants';
// actionConstants
@ -48,6 +49,7 @@ export default function reduceApp(state = {}, action) {
testKey: null,
},
gasLoadingAnimationIsShowing: false,
ledgerWebHidConnectedStatus: WEBHID_CONNECTED_STATUSES.UNKNOWN,
...state,
};
@ -340,6 +342,12 @@ export default function reduceApp(state = {}, action) {
gasLoadingAnimationIsShowing: action.value,
};
case actionConstants.SET_WEBHID_CONNECTED_STATUS:
return {
...appState,
ledgerWebHidConnectedStatus: action.value,
};
default:
return appState;
}
@ -363,6 +371,10 @@ export function toggleGasLoadingAnimation(value) {
return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value };
}
export function setLedgerWebHidConnectedStatus(value) {
return { type: actionConstants.SET_WEBHID_CONNECTED_STATUS, value };
}
// Selectors
export function getQrCodeData(state) {
return state.appState.qrCodeData;
@ -371,3 +383,7 @@ export function getQrCodeData(state) {
export function getGasLoadingAnimationIsShowing(state) {
return state.appState.gasLoadingAnimationIsShowing;
}
export function getLedgerWebHidConnectedStatus(state) {
return state.appState.ledgerWebHidConnectedStatus;
}

View File

@ -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;
});
}

View File

@ -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">

View File

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

View File

@ -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

View File

@ -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()}

View File

@ -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,
};
};

View File

@ -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));

View File

@ -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', [

View File

@ -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}
/>
);
};

View File

@ -6,9 +6,12 @@ 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 { 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 = {
@ -36,10 +39,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 = {
@ -393,24 +397,77 @@ 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) => {
setLedgerLivePreference(transportType);
if (
transportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
userHasALedgerAccount
) {
await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
}
}}
/>
</div>
</div>

View File

@ -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}

View File

@ -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,
};
};

View File

@ -233,6 +233,13 @@
margin-left: 1.875rem;
}
&__inline-link {
@include H6;
display: initial;
padding: 0;
}
&--selected {
.settings-page {
&__content {

View File

@ -1,4 +1,3 @@
import { stripHexPrefix } from 'ethereumjs-util';
import { createSelector } from 'reselect';
import { addHexPrefix } from '../../app/scripts/lib/util';
import {
@ -8,7 +7,11 @@ import {
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,
} from '../../shared/constants/hardware-wallets';
import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
@ -36,7 +39,11 @@ import {
getConversionRate,
isNotEIP1559Network,
isEIP1559Network,
getLedgerTransportType,
isAddressLedger,
findKeyringForAddress,
} from '../ducks/metamask/metamask';
import { getLedgerWebHidConnectedStatus } from '../ducks/app/app';
/**
* One of the only remaining valid uses of selecting the network subkey of the
@ -77,14 +84,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;
}
@ -646,3 +646,16 @@ 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;
return (
addressIsLedger && transportTypePreferenceIsWebHID && webHidIsNotConnected
);
}

View File

@ -77,6 +77,10 @@ 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';
// Network
export const SET_PENDING_TOKENS = 'SET_PENDING_TOKENS';
export const CLEAR_PENDING_TOKENS = 'CLEAR_PENDING_TOKENS';

View File

@ -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,31 @@ 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) => {
dispatch(
showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`),
);
let accounts;
try {
const { ledgerTransportType } = getState().metamask;
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,
@ -2745,7 +2765,7 @@ export function getCurrentWindowTab() {
export function setLedgerLivePreference(value) {
return async (dispatch) => {
dispatch(showLoadingIndication());
await promisifiedBackground.setLedgerLivePreference(value);
await promisifiedBackground.setLedgerTransportPreference(value);
dispatch(hideLoadingIndication());
};
}

View File

@ -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.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.9.0.tgz#42e98e7dfeaaa08e7c9ceff261facddd7320df80"
integrity sha512-EuNKvodbdJxQPzr+zAE5TE1iKUzuIRWKeVaYoYwpi18RjjtSQMKmZcb3VXY8hmQu+Fj4Ld/ujj22qSYjYAjtPg==
dependencies:
"@ethereumjs/tx" "^3.2.0"
eth-sig-util "^2.0.0"