mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +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:
parent
1a1fa3aac5
commit
9d70c60c22
@ -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, let’s 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"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Debe usar su última cuenta antes de poder agregar una nueva."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Utilizar Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "El nuevo puente Ledger Live le permite utilizar su Ledger de forma más sencilla. Disponible solo en Google Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Aplicación de Ledger Live"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Debe usar su última cuenta antes de poder agregar una nueva."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Utilizar Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "El nuevo puente Ledger Live le permite utilizar su Ledger de forma más sencilla. Disponible solo en Google Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Aplicación de Ledger Live"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "नया खाता जोड़ने से पहले आपको अपने अंतिम खाते का उपयोग करना होगा।"
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Ledger Live का उपयोग करें"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "नया Ledger Live ब्रिज आपको अपने लेजर का अधिक आसानी से उपयोग करने की अनुमति देता है। केवल Chrome में उपलब्ध है।"
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Ledger Live ऐप"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Anda perlu memanfaatkan akun terakhir Anda sebelum menambahkan yang baru."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Gunakan Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "Jembatan Ledger Live baru memungkinkan Anda untuk menggunakan Ledger Anda dengan lebih mudah. Hanya tersedia di Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Aplikasi Ledger Live"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "新しいアカウントを追加するには、その前に最後のアカウントを使用する必要があります。"
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "レジャー ライブを使用"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "新しいレジャー ライブのブリッジを使用すると、レジャーをより簡単に使用できます。Chrome でのみ利用可能。"
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "レジャー ライブのアプリ"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "새 계정을 추가하려면 먼저 마지막 계정을 사용해야 합니다."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Ledger Live 사용하기"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "새로운 Ledger Live 브리지를 통해 Ledger를 더 쉽게 사용할 수 있습니다. Chrome에서만 사용 가능합니다."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Ledger Live 앱"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Kailangan mong gamitin ang huli mong account bago ka magdagdag ng panibago."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Gamitin ang Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "Binibigyang-daan ka ng bagong Ledger Live bridge na mas madaling magamit ang iyong Ledger. Available lang sa Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Ledger Live App"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Você precisa usar sua última conta antes de adicionar uma nova."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Usar Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "A nova ponte do Ledger Live permite utilizar seu Ledger mais facilmente. Disponível somente no Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Aplicativo Ledger Live"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Вам необходимо использовать свой последний счет, прежде чем вы сможете добавить новый."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Использовать Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "Новое решение Ledger Live Bridge упрощает использование Ledger. Доступно только в Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Приложение Ledger Live"
|
||||
},
|
||||
|
@ -1004,12 +1004,6 @@
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "Bạn cần sử dụng tài khoản gần đây nhất thì mới có thể thêm một tài khoản mới."
|
||||
},
|
||||
"ledgerLiveAdvancedSetting": {
|
||||
"message": "Dùng Ledger Live"
|
||||
},
|
||||
"ledgerLiveAdvancedSettingDescription": {
|
||||
"message": "Cầu Ledger Live mới cho phép bạn dùng Ledger dễ dàng hơn. Chỉ có trong Chrome."
|
||||
},
|
||||
"ledgerLiveApp": {
|
||||
"message": "Ứng dụng Ledger Live"
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import { ethers } from 'ethers';
|
||||
import log from 'loglevel';
|
||||
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
|
||||
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
export default class PreferencesController {
|
||||
@ -58,7 +59,9 @@ export default class PreferencesController {
|
||||
// ENS decentralized website resolution
|
||||
ipfsGateway: 'dweb.link',
|
||||
infuraBlocked: null,
|
||||
useLedgerLive: false,
|
||||
ledgerTransportType: window.navigator.hid
|
||||
? LEDGER_TRANSPORT_TYPES.WEBHID
|
||||
: LEDGER_TRANSPORT_TYPES.U2F,
|
||||
...opts.initState,
|
||||
};
|
||||
|
||||
@ -516,21 +519,21 @@ export default class PreferencesController {
|
||||
}
|
||||
|
||||
/**
|
||||
* A setter for the `useLedgerLive` property
|
||||
* @param {bool} useLedgerLive - Value for ledger live support
|
||||
* @returns {Promise<string>} A promise of the update to useLedgerLive
|
||||
* A setter for the `useWebHid` property
|
||||
* @param {string} ledgerTransportType - Either 'ledgerLive', 'webhid' or 'u2f'
|
||||
* @returns {string} The transport type that was set.
|
||||
*/
|
||||
async setLedgerLivePreference(useLedgerLive) {
|
||||
this.store.updateState({ useLedgerLive });
|
||||
return useLedgerLive;
|
||||
setLedgerTransportPreference(ledgerTransportType) {
|
||||
this.store.updateState({ ledgerTransportType });
|
||||
return ledgerTransportType;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the `useLedgerLive` property
|
||||
* @returns {boolean} User preference of using Ledger Live
|
||||
* A getter for the `ledgerTransportType` property
|
||||
* @returns {boolean} User preference of using WebHid to connect Ledger
|
||||
*/
|
||||
getLedgerLivePreference() {
|
||||
return this.store.getState().useLedgerLive;
|
||||
getLedgerTransportPreference() {
|
||||
return this.store.getState().ledgerTransportType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -841,7 +841,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;
|
||||
});
|
||||
}
|
||||
|
37
app/scripts/migrations/066.js
Normal file
37
app/scripts/migrations/066.js
Normal 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;
|
||||
}
|
116
app/scripts/migrations/066.test.js
Normal file
116
app/scripts/migrations/066.test.js
Normal 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);
|
||||
});
|
||||
});
|
@ -69,6 +69,7 @@ import m062 from './062';
|
||||
import m063 from './063';
|
||||
import m064 from './064';
|
||||
import m065 from './065';
|
||||
import m066 from './066';
|
||||
|
||||
const migrations = [
|
||||
m002,
|
||||
@ -135,6 +136,7 @@ const migrations = [
|
||||
m063,
|
||||
m064,
|
||||
m065,
|
||||
m066,
|
||||
];
|
||||
|
||||
export default migrations;
|
||||
|
@ -111,7 +111,11 @@ export default class ExtensionPlatform {
|
||||
return version;
|
||||
}
|
||||
|
||||
openExtensionInBrowser(route = null, queryString = null) {
|
||||
openExtensionInBrowser(
|
||||
route = null,
|
||||
queryString = null,
|
||||
keepWindowOpen = false,
|
||||
) {
|
||||
let extensionURL = extension.runtime.getURL('home.html');
|
||||
|
||||
if (queryString) {
|
||||
@ -122,7 +126,10 @@ export default class ExtensionPlatform {
|
||||
extensionURL += `#${route}`;
|
||||
}
|
||||
this.openTab({ url: extensionURL });
|
||||
if (getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND) {
|
||||
if (
|
||||
getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND &&
|
||||
!keepWindowOpen
|
||||
) {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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',
|
||||
};
|
||||
|
1
ui/components/app/ledger-instruction-field/index.js
Normal file
1
ui/components/app/ledger-instruction-field/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ledger-instruction-field';
|
@ -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,
|
||||
};
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { stripHexPrefix } from 'ethereumjs-util';
|
||||
import classnames from 'classnames';
|
||||
import { ObjectInspector } from 'react-inspector';
|
||||
import LedgerInstructionField from '../ledger-instruction-field';
|
||||
|
||||
import {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
@ -36,6 +37,8 @@ export default class SignatureRequestOriginal extends Component {
|
||||
sign: PropTypes.func.isRequired,
|
||||
txData: PropTypes.object.isRequired,
|
||||
domainMetadata: PropTypes.object,
|
||||
hardwareWalletRequiresConnection: PropTypes.bool,
|
||||
isLedgerWallet: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -286,6 +289,7 @@ export default class SignatureRequestOriginal extends Component {
|
||||
mostRecentOverviewPage,
|
||||
sign,
|
||||
txData: { type },
|
||||
hardwareWalletRequiresConnection,
|
||||
} = this.props;
|
||||
const { metricsEvent, t } = this.context;
|
||||
|
||||
@ -319,6 +323,7 @@ export default class SignatureRequestOriginal extends Component {
|
||||
type="primary"
|
||||
large
|
||||
className="request-signature__footer__sign-button"
|
||||
disabled={hardwareWalletRequiresConnection}
|
||||
onClick={async (event) => {
|
||||
this._removeBeforeUnload();
|
||||
await sign(event);
|
||||
@ -347,6 +352,11 @@ export default class SignatureRequestOriginal extends Component {
|
||||
<div className="request-signature__container">
|
||||
{this.renderHeader()}
|
||||
{this.renderBody()}
|
||||
{this.props.isLedgerWallet ? (
|
||||
<div className="confirm-approve-content__ledger-instruction-wrapper">
|
||||
<LedgerInstructionField showDataInstruction />
|
||||
</div>
|
||||
) : null}
|
||||
{this.renderFooter()}
|
||||
</div>
|
||||
);
|
||||
|
@ -8,18 +8,32 @@ import {
|
||||
accountsWithSendEtherInfoSelector,
|
||||
conversionRateSelector,
|
||||
getDomainMetadata,
|
||||
doesAddressRequireLedgerHidConnection,
|
||||
} from '../../../selectors';
|
||||
import { getAccountByAddress } from '../../../helpers/utils/util';
|
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||
import { isAddressLedger } from '../../../ducks/metamask/metamask';
|
||||
import SignatureRequestOriginal from './signature-request-original.component';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {
|
||||
msgParams: { from },
|
||||
} = ownProps.txData;
|
||||
|
||||
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
|
||||
state,
|
||||
from,
|
||||
);
|
||||
const isLedgerWallet = isAddressLedger(state, from);
|
||||
|
||||
return {
|
||||
requester: null,
|
||||
requesterAddress: null,
|
||||
conversionRate: conversionRateSelector(state),
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
hardwareWalletRequiresConnection,
|
||||
isLedgerWallet,
|
||||
// not passed to component
|
||||
allAccounts: accountsWithSendEtherInfoSelector(state),
|
||||
domainMetadata: getDomainMetadata(state),
|
||||
|
@ -6,6 +6,7 @@ export default class SignatureRequestFooter extends PureComponent {
|
||||
static propTypes = {
|
||||
cancelAction: PropTypes.func.isRequired,
|
||||
signAction: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.boolean,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@ -13,13 +14,13 @@ export default class SignatureRequestFooter extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { cancelAction, signAction } = this.props;
|
||||
const { cancelAction, signAction, disabled = false } = this.props;
|
||||
return (
|
||||
<div className="signature-request-footer">
|
||||
<Button onClick={cancelAction} type="secondary" large>
|
||||
{this.context.t('cancel')}
|
||||
</Button>
|
||||
<Button onClick={signAction} type="primary" large>
|
||||
<Button onClick={signAction} type="primary" disabled={disabled} large>
|
||||
{this.context.t('sign')}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import Identicon from '../../ui/identicon';
|
||||
import LedgerInstructionField from '../ledger-instruction-field';
|
||||
import Header from './signature-request-header';
|
||||
import Footer from './signature-request-footer';
|
||||
import Message from './signature-request-message';
|
||||
@ -15,10 +16,11 @@ export default class SignatureRequest extends PureComponent {
|
||||
balance: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
|
||||
isLedgerWallet: PropTypes.bool,
|
||||
clearConfirmTransaction: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func.isRequired,
|
||||
sign: PropTypes.func.isRequired,
|
||||
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@ -69,6 +71,8 @@ export default class SignatureRequest extends PureComponent {
|
||||
},
|
||||
cancel,
|
||||
sign,
|
||||
isLedgerWallet,
|
||||
hardwareWalletRequiresConnection,
|
||||
} = this.props;
|
||||
const { address: fromAddress } = fromAccount;
|
||||
const { message, domain = {} } = JSON.parse(data);
|
||||
@ -128,8 +132,17 @@ export default class SignatureRequest extends PureComponent {
|
||||
{this.formatWallet(fromAddress)}
|
||||
</div>
|
||||
</div>
|
||||
{isLedgerWallet ? (
|
||||
<div className="confirm-approve-content__ledger-instruction-wrapper">
|
||||
<LedgerInstructionField showDataInstruction />
|
||||
</div>
|
||||
) : null}
|
||||
<Message data={message} />
|
||||
<Footer cancelAction={onCancel} signAction={onSign} />
|
||||
<Footer
|
||||
cancelAction={onCancel}
|
||||
signAction={onSign}
|
||||
disabled={hardwareWalletRequiresConnection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import { accountsWithSendEtherInfoSelector } from '../../../selectors';
|
||||
import {
|
||||
accountsWithSendEtherInfoSelector,
|
||||
doesAddressRequireLedgerHidConnection,
|
||||
} from '../../../selectors';
|
||||
import { isAddressLedger } from '../../../ducks/metamask/metamask';
|
||||
import { getAccountByAddress } from '../../../helpers/utils/util';
|
||||
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
|
||||
import SignatureRequest from './signature-request.component';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const { txData } = ownProps;
|
||||
const {
|
||||
msgParams: { from },
|
||||
} = txData;
|
||||
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
|
||||
state,
|
||||
from,
|
||||
);
|
||||
const isLedgerWallet = isAddressLedger(state, from);
|
||||
|
||||
return {
|
||||
isLedgerWallet,
|
||||
hardwareWalletRequiresConnection,
|
||||
// not forwarded to component
|
||||
allAccounts: accountsWithSendEtherInfoSelector(state),
|
||||
};
|
||||
@ -19,7 +35,11 @@ function mapDispatchToProps(dispatch) {
|
||||
}
|
||||
|
||||
function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
const { allAccounts } = stateProps;
|
||||
const {
|
||||
allAccounts,
|
||||
isLedgerWallet,
|
||||
hardwareWalletRequiresConnection,
|
||||
} = stateProps;
|
||||
const {
|
||||
signPersonalMessage,
|
||||
signTypedMessage,
|
||||
@ -58,6 +78,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
txData,
|
||||
cancel,
|
||||
sign,
|
||||
isLedgerWallet,
|
||||
hardwareWalletRequiresConnection,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { addHexPrefix, isHexString } from 'ethereumjs-util';
|
||||
import { addHexPrefix, isHexString, stripHexPrefix } from 'ethereumjs-util';
|
||||
import * as actionConstants from '../../store/actionConstants';
|
||||
import { ALERT_TYPES } from '../../../shared/constants/alerts';
|
||||
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
|
||||
@ -10,7 +10,9 @@ import {
|
||||
import { updateTransaction } from '../../store/actions';
|
||||
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
|
||||
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
|
||||
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
|
||||
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
|
||||
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
|
||||
|
||||
export default function reduceMetamask(state = {}, action) {
|
||||
const metamaskState = {
|
||||
@ -340,3 +342,59 @@ export function getIsUnlocked(state) {
|
||||
export function getSeedPhraseBackedUp(state) {
|
||||
return state.metamask.seedPhraseBackedUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the redux state object and an address, finds a keyring that contains that address, if one exists
|
||||
*
|
||||
* @param {Object} state - the redux state object
|
||||
* @param {string} address - the address to search for among the keyring addresses
|
||||
* @returns {Object|undefined} The keyring which contains the passed address, or undefined
|
||||
*/
|
||||
export function findKeyringForAddress(state, address) {
|
||||
const keyring = state.metamask.keyrings.find((kr) => {
|
||||
return kr.accounts.some((account) => {
|
||||
return (
|
||||
isEqualCaseInsensitive(account, addHexPrefix(address)) ||
|
||||
isEqualCaseInsensitive(account, stripHexPrefix(address))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return keyring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the redux state object, returns the users preferred ledger transport type
|
||||
*
|
||||
* @param {Object} state - the redux state object
|
||||
* @returns {string} The users preferred ledger transport type. One of'ledgerLive', 'webhid' or 'u2f'
|
||||
*/
|
||||
export function getLedgerTransportType(state) {
|
||||
return state.metamask.ledgerTransportType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the redux state object and an address, returns a boolean indicating whether the passed address is part of a Ledger keyring
|
||||
*
|
||||
* @param {Object} state - the redux state object
|
||||
* @param {string} address - the address to search for among all keyring addresses
|
||||
* @returns {boolean} true if the passed address is part of a ledger keyring, and false otherwise
|
||||
*/
|
||||
export function isAddressLedger(state, address) {
|
||||
const keyring = findKeyringForAddress(state, address);
|
||||
|
||||
return keyring?.type === KEYRING_TYPES.LEDGER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the redux state object, returns a boolean indicating whether the user has any Ledger accounts added to MetaMask (i.e. Ledger keyrings
|
||||
* in state)
|
||||
*
|
||||
* @param {Object} state - the redux state object
|
||||
* @returns {boolean} true if the user has a Ledger account and false otherwise
|
||||
*/
|
||||
export function doesUserHaveALedgerAccount(state) {
|
||||
return state.metamask.keyrings.some((kr) => {
|
||||
return kr.type === KEYRING_TYPES.LEDGER;
|
||||
});
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Box from '../../../components/ui/box';
|
||||
import Button from '../../../components/ui/button';
|
||||
import LedgerInstructionField from '../../../components/app/ledger-instruction-field';
|
||||
|
||||
export default class ConfirmApproveContent extends Component {
|
||||
static contextTypes = {
|
||||
@ -44,6 +45,8 @@ export default class ConfirmApproveContent extends Component {
|
||||
nextNonce: PropTypes.number,
|
||||
showCustomizeNonceModal: PropTypes.func,
|
||||
warning: PropTypes.string,
|
||||
txData: PropTypes.object,
|
||||
ledgerWalletRequiredHidConnection: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -238,6 +241,8 @@ export default class ConfirmApproveContent extends Component {
|
||||
tokenBalance,
|
||||
useNonceField,
|
||||
warning,
|
||||
txData,
|
||||
ledgerWalletRequiredHidConnection,
|
||||
} = this.props;
|
||||
const { showFullTxDetails } = this.state;
|
||||
|
||||
@ -346,6 +351,14 @@ export default class ConfirmApproveContent extends Component {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{ledgerWalletRequiredHidConnection ? (
|
||||
<div className="confirm-approve-content__ledger-instruction-wrapper">
|
||||
<LedgerInstructionField
|
||||
showDataInstruction={Boolean(txData.txParams?.data)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showFullTxDetails ? (
|
||||
<div className="confirm-approve-content__full-tx-content">
|
||||
<div className="confirm-approve-content__permission">
|
||||
|
@ -161,6 +161,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__ledger-instruction-wrapper {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&__transaction-details-content {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
getUseNonceField,
|
||||
getCustomNonceValue,
|
||||
getNextSuggestedNonce,
|
||||
doesAddressRequireLedgerHidConnection,
|
||||
} from '../../selectors';
|
||||
|
||||
import { useApproveTransaction } from '../../hooks/useApproveTransaction';
|
||||
@ -35,12 +36,18 @@ import { isEqualCaseInsensitive } from '../../helpers/utils/util';
|
||||
import { getCustomTxParamsData } from './confirm-approve.util';
|
||||
import ConfirmApproveContent from './confirm-approve-content';
|
||||
|
||||
const doesAddressRequireLedgerHidConnectionByFromAddress = (address) => (
|
||||
state,
|
||||
) => {
|
||||
return doesAddressRequireLedgerHidConnection(state, address);
|
||||
};
|
||||
|
||||
export default function ConfirmApprove() {
|
||||
const dispatch = useDispatch();
|
||||
const { id: paramsTransactionId } = useParams();
|
||||
const {
|
||||
id: transactionId,
|
||||
txParams: { to: tokenAddress, data } = {},
|
||||
txParams: { to: tokenAddress, data, from } = {},
|
||||
} = useSelector(txDataSelector);
|
||||
|
||||
const currentCurrency = useSelector(getCurrentCurrency);
|
||||
@ -52,6 +59,10 @@ export default function ConfirmApprove() {
|
||||
const nextNonce = useSelector(getNextSuggestedNonce);
|
||||
const customNonceValue = useSelector(getCustomNonceValue);
|
||||
|
||||
const ledgerWalletRequiredHidConnection = useSelector(
|
||||
doesAddressRequireLedgerHidConnectionByFromAddress(from),
|
||||
);
|
||||
|
||||
const transaction =
|
||||
currentNetworkTxList.find(
|
||||
({ id }) => id === (Number(paramsTransactionId) || transactionId),
|
||||
@ -207,6 +218,10 @@ export default function ConfirmApprove() {
|
||||
)
|
||||
}
|
||||
warning={submitWarning}
|
||||
txData={transaction}
|
||||
ledgerWalletRequiredHidConnection={
|
||||
ledgerWalletRequiredHidConnection
|
||||
}
|
||||
/>
|
||||
{showCustomizeGasPopover && (
|
||||
<EditGasPopover
|
||||
|
@ -35,12 +35,11 @@ import TransactionDetailItem from '../../components/app/transaction-detail-item/
|
||||
import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
|
||||
import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
|
||||
import GasTiming from '../../components/app/gas-timing/gas-timing.component';
|
||||
import Dialog from '../../components/ui/dialog';
|
||||
import LedgerInstructionField from '../../components/app/ledger-instruction-field';
|
||||
|
||||
import {
|
||||
COLORS,
|
||||
FONT_STYLE,
|
||||
FONT_WEIGHT,
|
||||
TYPOGRAPHY,
|
||||
} from '../../helpers/constants/design-system';
|
||||
import {
|
||||
@ -127,9 +126,9 @@ export default class ConfirmTransactionBase extends Component {
|
||||
isMainnet: PropTypes.bool,
|
||||
gasFeeIsCustom: PropTypes.bool,
|
||||
showLedgerSteps: PropTypes.bool.isRequired,
|
||||
isFirefox: PropTypes.bool.isRequired,
|
||||
nativeCurrency: PropTypes.string,
|
||||
supportsEIP1559: PropTypes.bool,
|
||||
hardwareWalletRequiresConnection: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -310,7 +309,6 @@ export default class ConfirmTransactionBase extends Component {
|
||||
maxPriorityFeePerGas,
|
||||
isMainnet,
|
||||
showLedgerSteps,
|
||||
isFirefox,
|
||||
supportsEIP1559,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
@ -405,46 +403,6 @@ export default class ConfirmTransactionBase extends Component {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const renderLedgerLiveStep = (text, show = true) => {
|
||||
return (
|
||||
show && (
|
||||
<Typography
|
||||
boxProps={{ margin: 0 }}
|
||||
color={COLORS.PRIMARY3}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
variant={TYPOGRAPHY.H7}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ledgerInstructionField = showLedgerSteps ? (
|
||||
<div>
|
||||
<div className="confirm-detail-row">
|
||||
<Dialog type="message">
|
||||
<div className="ledger-live-dialog">
|
||||
{renderLedgerLiveStep(t('ledgerLiveDialogHeader'))}
|
||||
{renderLedgerLiveStep(
|
||||
`- ${t('ledgerLiveDialogStepOne')}`,
|
||||
!isFirefox,
|
||||
)}
|
||||
{renderLedgerLiveStep(
|
||||
`- ${t('ledgerLiveDialogStepTwo')}`,
|
||||
!isFirefox,
|
||||
)}
|
||||
{renderLedgerLiveStep(`- ${t('ledgerLiveDialogStepThree')}`)}
|
||||
{renderLedgerLiveStep(
|
||||
`- ${t('ledgerLiveDialogStepFour')}`,
|
||||
Boolean(txData.txParams?.data),
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-content__details">
|
||||
<TransactionDetail
|
||||
@ -574,7 +532,11 @@ export default class ConfirmTransactionBase extends Component {
|
||||
]}
|
||||
/>
|
||||
{nonceField}
|
||||
{ledgerInstructionField}
|
||||
{showLedgerSteps ? (
|
||||
<LedgerInstructionField
|
||||
showDataInstruction={Boolean(txData.txParams?.data)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -916,6 +878,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
gasIsLoading,
|
||||
gasFeeIsCustom,
|
||||
nativeCurrency,
|
||||
hardwareWalletRequiresConnection,
|
||||
} = this.props;
|
||||
const {
|
||||
submitting,
|
||||
@ -981,7 +944,12 @@ export default class ConfirmTransactionBase extends Component {
|
||||
lastTx={lastTx}
|
||||
ofText={ofText}
|
||||
requestsWaitingText={requestsWaitingText}
|
||||
disabled={!valid || submitting || (gasIsLoading && !gasFeeIsCustom)}
|
||||
disabled={
|
||||
!valid ||
|
||||
submitting ||
|
||||
hardwareWalletRequiresConnection ||
|
||||
(gasIsLoading && !gasFeeIsCustom)
|
||||
}
|
||||
onEdit={() => this.handleEdit()}
|
||||
onCancelAll={() => this.handleCancelAll()}
|
||||
onCancel={() => this.handleCancel()}
|
||||
|
@ -28,24 +28,24 @@ import {
|
||||
getShouldShowFiat,
|
||||
checkNetworkAndAccountSupports1559,
|
||||
getPreferences,
|
||||
getHardwareWalletType,
|
||||
doesAddressRequireLedgerHidConnection,
|
||||
getUseTokenDetection,
|
||||
getTokenList,
|
||||
} from '../../selectors';
|
||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||
import {
|
||||
transactionMatchesNetwork,
|
||||
txParamsAreDappSuggested,
|
||||
} from '../../../shared/modules/transaction.utils';
|
||||
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
|
||||
import { getPlatform } from '../../../app/scripts/lib/util';
|
||||
import { PLATFORM_FIREFOX } from '../../../shared/constants/app';
|
||||
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
|
||||
import {
|
||||
isAddressLedger,
|
||||
updateTransactionGasFees,
|
||||
getIsGasEstimatesLoading,
|
||||
getNativeCurrency,
|
||||
} from '../../ducks/metamask/metamask';
|
||||
|
||||
import {
|
||||
transactionMatchesNetwork,
|
||||
txParamsAreDappSuggested,
|
||||
} from '../../../shared/modules/transaction.utils';
|
||||
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
|
||||
|
||||
import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app';
|
||||
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
|
||||
import ConfirmTransactionBase from './confirm-transaction-base.component';
|
||||
@ -170,10 +170,14 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const gasFeeIsCustom =
|
||||
fullTxData.userFeeLevel === 'custom' ||
|
||||
txParamsAreDappSuggested(fullTxData);
|
||||
const showLedgerSteps = getHardwareWalletType(state) === KEYRING_TYPES.LEDGER;
|
||||
const isFirefox = getPlatform() === PLATFORM_FIREFOX;
|
||||
const fromAddressIsLedger = isAddressLedger(state, fromAddress);
|
||||
const nativeCurrency = getNativeCurrency(state);
|
||||
|
||||
const hardwareWalletRequiresConnection = doesAddressRequireLedgerHidConnection(
|
||||
state,
|
||||
fromAddress,
|
||||
);
|
||||
|
||||
return {
|
||||
balance,
|
||||
fromAddress,
|
||||
@ -219,9 +223,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas,
|
||||
baseFeePerGas: gasEstimationObject.baseFeePerGas,
|
||||
gasFeeIsCustom,
|
||||
showLedgerSteps,
|
||||
isFirefox,
|
||||
showLedgerSteps: fromAddressIsLedger,
|
||||
nativeCurrency,
|
||||
hardwareWalletRequiresConnection,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { formatBalance } from '../../../helpers/utils/util';
|
||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||
import { SECOND } from '../../../../shared/constants/time';
|
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
|
||||
import SelectHardware from './select-hardware';
|
||||
import AccountList from './account-list';
|
||||
|
||||
@ -26,6 +27,10 @@ const HD_PATHS = [
|
||||
];
|
||||
|
||||
class ConnectHardwareForm extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
selectedAccounts: [],
|
||||
@ -106,7 +111,7 @@ class ConnectHardwareForm extends Component {
|
||||
|
||||
getPage = (device, page, hdPath) => {
|
||||
this.props
|
||||
.connectHardware(device, page, hdPath)
|
||||
.connectHardware(device, page, hdPath, this.context.t)
|
||||
.then((accounts) => {
|
||||
if (accounts.length) {
|
||||
// If we just loaded the accounts for the first time
|
||||
@ -262,7 +267,7 @@ class ConnectHardwareForm extends Component {
|
||||
<SelectHardware
|
||||
connectToHardwareWallet={this.connectToHardwareWallet}
|
||||
browserSupported={this.state.browserSupported}
|
||||
useLedgerLive={this.props.useLedgerLive}
|
||||
ledgerTransportType={this.props.ledgerTransportType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -313,7 +318,7 @@ ConnectHardwareForm.propTypes = {
|
||||
connectedAccounts: PropTypes.array.isRequired,
|
||||
defaultHdPaths: PropTypes.object,
|
||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||
useLedgerLive: PropTypes.bool.isRequired,
|
||||
ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
@ -323,7 +328,7 @@ const mapStateToProps = (state) => ({
|
||||
connectedAccounts: getMetaMaskAccountsConnected(state),
|
||||
defaultHdPaths: state.appState.defaultHdPaths,
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
useLedgerLive: state.metamask.useLedgerLive,
|
||||
ledgerTransportType: state.metamask.ledgerTransportType,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
@ -331,8 +336,8 @@ const mapDispatchToProps = (dispatch) => {
|
||||
setHardwareWalletDefaultHdPath: ({ device, path }) => {
|
||||
return dispatch(actions.setHardwareWalletDefaultHdPath({ device, path }));
|
||||
},
|
||||
connectHardware: (deviceName, page, hdPath) => {
|
||||
return dispatch(actions.connectHardware(deviceName, page, hdPath));
|
||||
connectHardware: (deviceName, page, hdPath, t) => {
|
||||
return dispatch(actions.connectHardware(deviceName, page, hdPath, t));
|
||||
},
|
||||
checkHardwareStatus: (deviceName, hdPath) => {
|
||||
return dispatch(actions.checkHardwareStatus(deviceName, hdPath));
|
||||
|
@ -2,6 +2,7 @@ import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from '../../../components/ui/button';
|
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
|
||||
|
||||
export default class SelectHardware extends Component {
|
||||
static contextTypes = {
|
||||
@ -11,7 +12,7 @@ export default class SelectHardware extends Component {
|
||||
static propTypes = {
|
||||
connectToHardwareWallet: PropTypes.func.isRequired,
|
||||
browserSupported: PropTypes.bool.isRequired,
|
||||
useLedgerLive: PropTypes.bool.isRequired,
|
||||
ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -136,7 +137,7 @@ export default class SelectHardware extends Component {
|
||||
|
||||
renderLedgerTutorialSteps() {
|
||||
const steps = [];
|
||||
if (this.props.useLedgerLive) {
|
||||
if (this.props.ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE) {
|
||||
steps.push({
|
||||
title: this.context.t('step1LedgerWallet'),
|
||||
message: this.context.t('step1LedgerWalletMsg', [
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
|
||||
import SelectHardware from './select-hardware';
|
||||
|
||||
export default {
|
||||
@ -14,7 +15,7 @@ export const SelectHardwareComponent = () => {
|
||||
connectToHardwareWallet={(selectedDevice) =>
|
||||
action(`Continue connect to ${selectedDevice}`)()
|
||||
}
|
||||
useLedgerLive
|
||||
ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -23,7 +24,7 @@ export const BrowserNotSupported = () => {
|
||||
<SelectHardware
|
||||
browserSupported={false}
|
||||
connectToHardwareWallet={() => undefined}
|
||||
useLedgerLive
|
||||
ledgerTransportType={LEDGER_TRANSPORT_TYPES.LIVE}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -6,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>
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import TextField from '../../../components/ui/text-field';
|
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets';
|
||||
import AdvancedTab from './advanced-tab.component';
|
||||
|
||||
describe('AdvancedTab Component', () => {
|
||||
@ -15,7 +16,7 @@ describe('AdvancedTab Component', () => {
|
||||
setThreeBoxSyncingPermission={() => undefined}
|
||||
threeBoxDisabled
|
||||
threeBoxSyncingAllowed={false}
|
||||
useLedgerLive={false}
|
||||
ledgerTransportType={LEDGER_TRANSPORT_TYPES.U2F}
|
||||
setLedgerLivePreference={() => undefined}
|
||||
setDismissSeedBackUpReminder={() => undefined}
|
||||
dismissSeedBackUpReminder={false}
|
||||
@ -41,7 +42,7 @@ describe('AdvancedTab Component', () => {
|
||||
setThreeBoxSyncingPermission={() => undefined}
|
||||
threeBoxDisabled
|
||||
threeBoxSyncingAllowed={false}
|
||||
useLedgerLive={false}
|
||||
ledgerTransportType={LEDGER_TRANSPORT_TYPES.U2F}
|
||||
setLedgerLivePreference={() => undefined}
|
||||
setDismissSeedBackUpReminder={() => undefined}
|
||||
dismissSeedBackUpReminder={false}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
setDismissSeedBackUpReminder,
|
||||
} from '../../../store/actions';
|
||||
import { getPreferences } from '../../../selectors';
|
||||
import { doesUserHaveALedgerAccount } from '../../../ducks/metamask/metamask';
|
||||
import AdvancedTab from './advanced-tab.component';
|
||||
|
||||
export const mapStateToProps = (state) => {
|
||||
@ -28,11 +29,13 @@ export const mapStateToProps = (state) => {
|
||||
threeBoxDisabled,
|
||||
useNonceField,
|
||||
ipfsGateway,
|
||||
useLedgerLive,
|
||||
ledgerTransportType,
|
||||
dismissSeedBackUpReminder,
|
||||
} = metamask;
|
||||
const { showFiatInTestnets, autoLockTimeLimit } = getPreferences(state);
|
||||
|
||||
const userHasALedgerAccount = doesUserHaveALedgerAccount(state);
|
||||
|
||||
return {
|
||||
warning,
|
||||
sendHexData,
|
||||
@ -43,8 +46,9 @@ export const mapStateToProps = (state) => {
|
||||
threeBoxDisabled,
|
||||
useNonceField,
|
||||
ipfsGateway,
|
||||
useLedgerLive,
|
||||
ledgerTransportType,
|
||||
dismissSeedBackUpReminder,
|
||||
userHasALedgerAccount,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -233,6 +233,13 @@
|
||||
margin-left: 1.875rem;
|
||||
}
|
||||
|
||||
&__inline-link {
|
||||
@include H6;
|
||||
|
||||
display: initial;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
.settings-page {
|
||||
&__content {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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());
|
||||
};
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user