1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +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": { "chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." "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": { "clickToRevealSeed": {
"message": "Click here to reveal secret words" "message": "Click here to reveal secret words"
}, },
@ -1251,36 +1255,42 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "You need to make use your last account before you can add a new one." "message": "You need to make use your last account before you can add a new one."
}, },
"ledgerLiveAdvancedSetting": { "ledgerConnectionInstructionHeader": {
"message": "Use Ledger Live" "message": "Prior to clicking confirm:"
}, },
"ledgerLiveAdvancedSettingDescription": { "ledgerConnectionInstructionStepFour": {
"message": "The new Ledger Live bridge allows you to more easily use your Ledger. Only available in Chrome." "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": { "ledgerLiveApp": {
"message": "Ledger Live App" "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": { "ledgerLocked": {
"message": "Cannot connect to Ledger device. Please make sure your device is unlocked and Ethereum app is opened." "message": "Cannot connect to Ledger device. Please make sure your device is unlocked and Ethereum app is opened."
}, },
"ledgerTimeout": { "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." "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": { "letsGoSetUp": {
"message": "Yes, lets get set up!" "message": "Yes, lets get set up!"
}, },
@ -1710,6 +1720,10 @@
"onlyConnectTrust": { "onlyConnectTrust": {
"message": "Only connect with sites you trust." "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": { "optional": {
"message": "Optional" "message": "Optional"
}, },
@ -1772,6 +1786,10 @@
"message": "+ $1 more", "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" "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": { "prev": {
"message": "Prev" "message": "Prev"
}, },
@ -2817,6 +2835,10 @@
"typePassword": { "typePassword": {
"message": "Type your MetaMask password" "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": { "unapproved": {
"message": "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.", "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." "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": { "welcome": {
"message": "Welcome to MetaMask" "message": "Welcome to MetaMask"
}, },

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Debe usar su última cuenta antes de poder agregar una nueva." "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": { "ledgerLiveApp": {
"message": "Aplicación de Ledger Live" "message": "Aplicación de Ledger Live"
}, },

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Debe usar su última cuenta antes de poder agregar una nueva." "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": { "ledgerLiveApp": {
"message": "Aplicación de Ledger Live" "message": "Aplicación de Ledger Live"
}, },

View File

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

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Anda perlu memanfaatkan akun terakhir Anda sebelum menambahkan yang baru." "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": { "ledgerLiveApp": {
"message": "Aplikasi Ledger Live" "message": "Aplikasi Ledger Live"
}, },

View File

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

View File

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

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Kailangan mong gamitin ang huli mong account bago ka magdagdag ng panibago." "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": { "ledgerLiveApp": {
"message": "Ledger Live App" "message": "Ledger Live App"
}, },

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "ledgerAccountRestriction": {
"message": "Você precisa usar sua última conta antes de adicionar uma nova." "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": { "ledgerLiveApp": {
"message": "Aplicativo Ledger Live" "message": "Aplicativo Ledger Live"
}, },

View File

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

View File

@ -1004,12 +1004,6 @@
"ledgerAccountRestriction": { "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." "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": { "ledgerLiveApp": {
"message": "Ứng dụng Ledger Live" "message": "Ứng dụng Ledger Live"
}, },

View File

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

View File

@ -841,7 +841,10 @@ export default class MetamaskController extends EventEmitter {
this.unlockHardwareWalletAccount, this.unlockHardwareWalletAccount,
this, this,
), ),
setLedgerLivePreference: nodeify(this.setLedgerLivePreference, this), setLedgerTransportPreference: nodeify(
this.setLedgerTransportPreference,
this,
),
// mobile // mobile
fetchInfoToSync: nodeify(this.fetchInfoToSync, this), fetchInfoToSync: nodeify(this.fetchInfoToSync, this),
@ -1480,9 +1483,9 @@ export default class MetamaskController extends EventEmitter {
// keyring's iframe and have the setting initialized properly // keyring's iframe and have the setting initialized properly
// Optimistically called to not block Metamask login due to // Optimistically called to not block Metamask login due to
// Ledger Keyring GitHub downtime // Ledger Keyring GitHub downtime
this.setLedgerLivePreference( const transportPreference = this.preferencesController.getLedgerTransportPreference();
this.preferencesController.getLedgerLivePreference(),
); this.setLedgerTransportPreference(transportPreference);
return this.keyringController.fullUpdate(); 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 * 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 * @param {bool} bool - the value representing if the users wants to use Ledger Live
*/ */
async setLedgerLivePreference(bool) { async setLedgerTransportPreference(transportType) {
const currentValue = this.preferencesController.getLedgerLivePreference(); const currentValue = this.preferencesController.getLedgerTransportPreference();
this.preferencesController.setLedgerLivePreference(bool); const newValue = this.preferencesController.setLedgerTransportPreference(
transportType,
);
const keyring = await this.getKeyringForDevice('ledger'); const keyring = await this.getKeyringForDevice('ledger');
if (keyring?.updateTransportMethod) { 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 // If there was an error updating the transport, we should
// fall back to the original value // fall back to the original value
this.preferencesController.setLedgerLivePreference(currentValue); this.preferencesController.setLedgerTransportPreference(currentValue);
throw e; 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 m063 from './063';
import m064 from './064'; import m064 from './064';
import m065 from './065'; import m065 from './065';
import m066 from './066';
const migrations = [ const migrations = [
m002, m002,
@ -135,6 +136,7 @@ const migrations = [
m063, m063,
m064, m064,
m065, m065,
m066,
]; ];
export default migrations; export default migrations;

View File

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

View File

@ -105,7 +105,7 @@
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.28.0", "@metamask/contract-metadata": "^1.28.0",
"@metamask/controllers": "^17.0.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/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0", "@metamask/etherscan-link": "^2.1.0",
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",

View File

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

View File

@ -8,18 +8,32 @@ import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
conversionRateSelector, conversionRateSelector,
getDomainMetadata, getDomainMetadata,
doesAddressRequireLedgerHidConnection,
} from '../../../selectors'; } from '../../../selectors';
import { getAccountByAddress } from '../../../helpers/utils/util'; import { getAccountByAddress } from '../../../helpers/utils/util';
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import SignatureRequestOriginal from './signature-request-original.component'; 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 { return {
requester: null, requester: null,
requesterAddress: null, requesterAddress: null,
conversionRate: conversionRateSelector(state), conversionRate: conversionRateSelector(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
hardwareWalletRequiresConnection,
isLedgerWallet,
// not passed to component // not passed to component
allAccounts: accountsWithSendEtherInfoSelector(state), allAccounts: accountsWithSendEtherInfoSelector(state),
domainMetadata: getDomainMetadata(state), domainMetadata: getDomainMetadata(state),

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { WEBHID_CONNECTED_STATUSES } from '../../../shared/constants/hardware-wallets';
import * as actionConstants from '../../store/actionConstants'; import * as actionConstants from '../../store/actionConstants';
// actionConstants // actionConstants
@ -48,6 +49,7 @@ export default function reduceApp(state = {}, action) {
testKey: null, testKey: null,
}, },
gasLoadingAnimationIsShowing: false, gasLoadingAnimationIsShowing: false,
ledgerWebHidConnectedStatus: WEBHID_CONNECTED_STATUSES.UNKNOWN,
...state, ...state,
}; };
@ -340,6 +342,12 @@ export default function reduceApp(state = {}, action) {
gasLoadingAnimationIsShowing: action.value, gasLoadingAnimationIsShowing: action.value,
}; };
case actionConstants.SET_WEBHID_CONNECTED_STATUS:
return {
...appState,
ledgerWebHidConnectedStatus: action.value,
};
default: default:
return appState; return appState;
} }
@ -363,6 +371,10 @@ export function toggleGasLoadingAnimation(value) {
return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value }; return { type: actionConstants.TOGGLE_GAS_LOADING_ANIMATION, value };
} }
export function setLedgerWebHidConnectedStatus(value) {
return { type: actionConstants.SET_WEBHID_CONNECTED_STATUS, value };
}
// Selectors // Selectors
export function getQrCodeData(state) { export function getQrCodeData(state) {
return state.appState.qrCodeData; return state.appState.qrCodeData;
@ -371,3 +383,7 @@ export function getQrCodeData(state) {
export function getGasLoadingAnimationIsShowing(state) { export function getGasLoadingAnimationIsShowing(state) {
return state.appState.gasLoadingAnimationIsShowing; 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 * as actionConstants from '../../store/actionConstants';
import { ALERT_TYPES } from '../../../shared/constants/alerts'; import { ALERT_TYPES } from '../../../shared/constants/alerts';
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; import { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
@ -10,7 +10,9 @@ import {
import { updateTransaction } from '../../store/actions'; import { updateTransaction } from '../../store/actions';
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'; import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas'; import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
export default function reduceMetamask(state = {}, action) { export default function reduceMetamask(state = {}, action) {
const metamaskState = { const metamaskState = {
@ -340,3 +342,59 @@ export function getIsUnlocked(state) {
export function getSeedPhraseBackedUp(state) { export function getSeedPhraseBackedUp(state) {
return state.metamask.seedPhraseBackedUp; 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'; } from '../../../helpers/constants/design-system';
import Box from '../../../components/ui/box'; import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import LedgerInstructionField from '../../../components/app/ledger-instruction-field';
export default class ConfirmApproveContent extends Component { export default class ConfirmApproveContent extends Component {
static contextTypes = { static contextTypes = {
@ -44,6 +45,8 @@ export default class ConfirmApproveContent extends Component {
nextNonce: PropTypes.number, nextNonce: PropTypes.number,
showCustomizeNonceModal: PropTypes.func, showCustomizeNonceModal: PropTypes.func,
warning: PropTypes.string, warning: PropTypes.string,
txData: PropTypes.object,
ledgerWalletRequiredHidConnection: PropTypes.bool,
}; };
state = { state = {
@ -238,6 +241,8 @@ export default class ConfirmApproveContent extends Component {
tokenBalance, tokenBalance,
useNonceField, useNonceField,
warning, warning,
txData,
ledgerWalletRequiredHidConnection,
} = this.props; } = this.props;
const { showFullTxDetails } = this.state; const { showFullTxDetails } = this.state;
@ -346,6 +351,14 @@ export default class ConfirmApproveContent extends Component {
})} })}
</div> </div>
{ledgerWalletRequiredHidConnection ? (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField
showDataInstruction={Boolean(txData.txParams?.data)}
/>
</div>
) : null}
{showFullTxDetails ? ( {showFullTxDetails ? (
<div className="confirm-approve-content__full-tx-content"> <div className="confirm-approve-content__full-tx-content">
<div className="confirm-approve-content__permission"> <div className="confirm-approve-content__permission">

View File

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

View File

@ -24,6 +24,7 @@ import {
getUseNonceField, getUseNonceField,
getCustomNonceValue, getCustomNonceValue,
getNextSuggestedNonce, getNextSuggestedNonce,
doesAddressRequireLedgerHidConnection,
} from '../../selectors'; } from '../../selectors';
import { useApproveTransaction } from '../../hooks/useApproveTransaction'; import { useApproveTransaction } from '../../hooks/useApproveTransaction';
@ -35,12 +36,18 @@ import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { getCustomTxParamsData } from './confirm-approve.util'; import { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content'; import ConfirmApproveContent from './confirm-approve-content';
const doesAddressRequireLedgerHidConnectionByFromAddress = (address) => (
state,
) => {
return doesAddressRequireLedgerHidConnection(state, address);
};
export default function ConfirmApprove() { export default function ConfirmApprove() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { id: paramsTransactionId } = useParams(); const { id: paramsTransactionId } = useParams();
const { const {
id: transactionId, id: transactionId,
txParams: { to: tokenAddress, data } = {}, txParams: { to: tokenAddress, data, from } = {},
} = useSelector(txDataSelector); } = useSelector(txDataSelector);
const currentCurrency = useSelector(getCurrentCurrency); const currentCurrency = useSelector(getCurrentCurrency);
@ -52,6 +59,10 @@ export default function ConfirmApprove() {
const nextNonce = useSelector(getNextSuggestedNonce); const nextNonce = useSelector(getNextSuggestedNonce);
const customNonceValue = useSelector(getCustomNonceValue); const customNonceValue = useSelector(getCustomNonceValue);
const ledgerWalletRequiredHidConnection = useSelector(
doesAddressRequireLedgerHidConnectionByFromAddress(from),
);
const transaction = const transaction =
currentNetworkTxList.find( currentNetworkTxList.find(
({ id }) => id === (Number(paramsTransactionId) || transactionId), ({ id }) => id === (Number(paramsTransactionId) || transactionId),
@ -207,6 +218,10 @@ export default function ConfirmApprove() {
) )
} }
warning={submitWarning} warning={submitWarning}
txData={transaction}
ledgerWalletRequiredHidConnection={
ledgerWalletRequiredHidConnection
}
/> />
{showCustomizeGasPopover && ( {showCustomizeGasPopover && (
<EditGasPopover <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 InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../components/ui/loading-heartbeat'; import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
import GasTiming from '../../components/app/gas-timing/gas-timing.component'; 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 { import {
COLORS, COLORS,
FONT_STYLE, FONT_STYLE,
FONT_WEIGHT,
TYPOGRAPHY, TYPOGRAPHY,
} from '../../helpers/constants/design-system'; } from '../../helpers/constants/design-system';
import { import {
@ -127,9 +126,9 @@ export default class ConfirmTransactionBase extends Component {
isMainnet: PropTypes.bool, isMainnet: PropTypes.bool,
gasFeeIsCustom: PropTypes.bool, gasFeeIsCustom: PropTypes.bool,
showLedgerSteps: PropTypes.bool.isRequired, showLedgerSteps: PropTypes.bool.isRequired,
isFirefox: PropTypes.bool.isRequired,
nativeCurrency: PropTypes.string, nativeCurrency: PropTypes.string,
supportsEIP1559: PropTypes.bool, supportsEIP1559: PropTypes.bool,
hardwareWalletRequiresConnection: PropTypes.bool,
}; };
state = { state = {
@ -310,7 +309,6 @@ export default class ConfirmTransactionBase extends Component {
maxPriorityFeePerGas, maxPriorityFeePerGas,
isMainnet, isMainnet,
showLedgerSteps, showLedgerSteps,
isFirefox,
supportsEIP1559, supportsEIP1559,
} = this.props; } = this.props;
const { t } = this.context; const { t } = this.context;
@ -405,46 +403,6 @@ export default class ConfirmTransactionBase extends Component {
</div> </div>
) : null; ) : 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 ( return (
<div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__details">
<TransactionDetail <TransactionDetail
@ -574,7 +532,11 @@ export default class ConfirmTransactionBase extends Component {
]} ]}
/> />
{nonceField} {nonceField}
{ledgerInstructionField} {showLedgerSteps ? (
<LedgerInstructionField
showDataInstruction={Boolean(txData.txParams?.data)}
/>
) : null}
</div> </div>
); );
} }
@ -916,6 +878,7 @@ export default class ConfirmTransactionBase extends Component {
gasIsLoading, gasIsLoading,
gasFeeIsCustom, gasFeeIsCustom,
nativeCurrency, nativeCurrency,
hardwareWalletRequiresConnection,
} = this.props; } = this.props;
const { const {
submitting, submitting,
@ -981,7 +944,12 @@ export default class ConfirmTransactionBase extends Component {
lastTx={lastTx} lastTx={lastTx}
ofText={ofText} ofText={ofText}
requestsWaitingText={requestsWaitingText} requestsWaitingText={requestsWaitingText}
disabled={!valid || submitting || (gasIsLoading && !gasFeeIsCustom)} disabled={
!valid ||
submitting ||
hardwareWalletRequiresConnection ||
(gasIsLoading && !gasFeeIsCustom)
}
onEdit={() => this.handleEdit()} onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()} onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()} onCancel={() => this.handleCancel()}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
import SelectHardware from './select-hardware'; import SelectHardware from './select-hardware';
export default { export default {
@ -14,7 +15,7 @@ export const SelectHardwareComponent = () => {
connectToHardwareWallet={(selectedDevice) => connectToHardwareWallet={(selectedDevice) =>
action(`Continue connect to ${selectedDevice}`)() action(`Continue connect to ${selectedDevice}`)()
} }
useLedgerLive ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
/> />
); );
}; };
@ -23,7 +24,7 @@ export const BrowserNotSupported = () => {
<SelectHardware <SelectHardware
browserSupported={false} browserSupported={false}
connectToHardwareWallet={() => undefined} 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 TextField from '../../../components/ui/text-field';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes'; import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes';
import Dropdown from '../../../components/ui/dropdown';
import { getPlatform } from '../../../../app/scripts/lib/util'; import {
import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
} from '../../../../shared/constants/hardware-wallets';
export default class AdvancedTab extends PureComponent { export default class AdvancedTab extends PureComponent {
static contextTypes = { static contextTypes = {
@ -36,10 +39,11 @@ export default class AdvancedTab extends PureComponent {
threeBoxDisabled: PropTypes.bool.isRequired, threeBoxDisabled: PropTypes.bool.isRequired,
setIpfsGateway: PropTypes.func.isRequired, setIpfsGateway: PropTypes.func.isRequired,
ipfsGateway: PropTypes.string.isRequired, ipfsGateway: PropTypes.string.isRequired,
useLedgerLive: PropTypes.bool.isRequired, ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
setLedgerLivePreference: PropTypes.func.isRequired, setLedgerLivePreference: PropTypes.func.isRequired,
setDismissSeedBackUpReminder: PropTypes.func.isRequired, setDismissSeedBackUpReminder: PropTypes.func.isRequired,
dismissSeedBackUpReminder: PropTypes.bool.isRequired, dismissSeedBackUpReminder: PropTypes.bool.isRequired,
userHasALedgerAccount: PropTypes.bool.isRequired,
}; };
state = { state = {
@ -393,24 +397,77 @@ export default class AdvancedTab extends PureComponent {
renderLedgerLiveControl() { renderLedgerLiveControl() {
const { t } = this.context; 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 ( return (
<div className="settings-page__content-row"> <div className="settings-page__content-row">
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<span>{t('ledgerLiveAdvancedSetting')}</span> <span>{t('preferredLedgerConnectionType')}</span>
<div className="settings-page__content-description"> <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> </div>
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<div className="settings-page__content-item-col"> <div className="settings-page__content-item-col">
<ToggleButton <Dropdown
value={useLedgerLive} id="select-ledger-transport-type"
onToggle={(value) => setLedgerLivePreference(!value)} options={transportTypeOptions}
offLabel={t('off')} selectedOption={ledgerTransportType}
onLabel={t('on')} onChange={async (transportType) => {
disabled={getPlatform() === PLATFORM_FIREFOX} setLedgerLivePreference(transportType);
if (
transportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
userHasALedgerAccount
) {
await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
}
}}
/> />
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { stripHexPrefix } from 'ethereumjs-util';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addHexPrefix } from '../../app/scripts/lib/util'; import { addHexPrefix } from '../../app/scripts/lib/util';
import { import {
@ -8,7 +7,11 @@ import {
NETWORK_TYPE_RPC, NETWORK_TYPE_RPC,
NATIVE_CURRENCY_TOKEN_IMAGE_MAP, NATIVE_CURRENCY_TOKEN_IMAGE_MAP,
} from '../../shared/constants/network'; } 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 { import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP, SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
@ -36,7 +39,11 @@ import {
getConversionRate, getConversionRate,
isNotEIP1559Network, isNotEIP1559Network,
isEIP1559Network, isEIP1559Network,
getLedgerTransportType,
isAddressLedger,
findKeyringForAddress,
} from '../ducks/metamask/metamask'; } from '../ducks/metamask/metamask';
import { getLedgerWebHidConnectedStatus } from '../ducks/app/app';
/** /**
* One of the only remaining valid uses of selecting the network subkey of the * One of the only remaining valid uses of selecting the network subkey of the
@ -77,14 +84,7 @@ export function getCurrentKeyring(state) {
return null; return null;
} }
const simpleAddress = stripHexPrefix(identity.address).toLowerCase(); const keyring = findKeyringForAddress(state, identity.address);
const keyring = state.metamask.keyrings.find((kr) => {
return (
kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
);
});
return keyring; return keyring;
} }
@ -646,3 +646,16 @@ export function getUseTokenDetection(state) {
export function getTokenList(state) { export function getTokenList(state) {
return state.metamask.tokenList; 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'; export const SET_MOUSE_USER_STATE = 'SET_MOUSE_USER_STATE';
// Ledger
export const SET_WEBHID_CONNECTED_STATUS = 'SET_WEBHID_CONNECTED_STATUS';
// Network // Network
export const SET_PENDING_TOKENS = 'SET_PENDING_TOKENS'; export const SET_PENDING_TOKENS = 'SET_PENDING_TOKENS';
export const CLEAR_PENDING_TOKENS = 'CLEAR_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 { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; 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'; import * as actionConstants from './actionConstants';
let background = null; 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); log.debug(`background.connectHardware`, deviceName, page, hdPath);
return async (dispatch) => { return async (dispatch, getState) => {
dispatch( dispatch(
showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`), showLoadingIndication(`Looking for your ${capitalize(deviceName)}...`),
); );
let accounts; let accounts;
try { 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( accounts = await promisifiedBackground.connectHardware(
deviceName, deviceName,
page, page,
@ -2745,7 +2765,7 @@ export function getCurrentWindowTab() {
export function setLedgerLivePreference(value) { export function setLedgerLivePreference(value) {
return async (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());
await promisifiedBackground.setLedgerLivePreference(value); await promisifiedBackground.setLedgerTransportPreference(value);
dispatch(hideLoadingIndication()); dispatch(hideLoadingIndication());
}; };
} }

View File

@ -2821,10 +2821,10 @@
resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-6.0.0.tgz#ec53e8ab278073e882411ed89705bc7d06b78c81" resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-6.0.0.tgz#ec53e8ab278073e882411ed89705bc7d06b78c81"
integrity sha512-LyakGYGwM8UQOGhwWa+5erAI1hXuiTgf/y7USzOomX6H9KiuY09IAUYnPh7ToPG2sedD2F48UF1bUm8yvCoZOw== integrity sha512-LyakGYGwM8UQOGhwWa+5erAI1hXuiTgf/y7USzOomX6H9KiuY09IAUYnPh7ToPG2sedD2F48UF1bUm8yvCoZOw==
"@metamask/eth-ledger-bridge-keyring@^0.7.0": "@metamask/eth-ledger-bridge-keyring@^0.9.0":
version "0.7.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.7.0.tgz#7d80e1e3dfab91ba2b6a1a2a5e352320e948b568" resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.9.0.tgz#42e98e7dfeaaa08e7c9ceff261facddd7320df80"
integrity sha512-0UOEb/c3/fkatDK+se3gOHaGQ0RTRLbG5DqsoeowZ/JcO4wcMxBhOiIgOY4domOqUTekKKVPNC7Pc0mHpM9sAQ== integrity sha512-EuNKvodbdJxQPzr+zAE5TE1iKUzuIRWKeVaYoYwpi18RjjtSQMKmZcb3VXY8hmQu+Fj4Ld/ujj22qSYjYAjtPg==
dependencies: dependencies:
"@ethereumjs/tx" "^3.2.0" "@ethereumjs/tx" "^3.2.0"
eth-sig-util "^2.0.0" eth-sig-util "^2.0.0"