-
Notifications
You must be signed in to change notification settings - Fork 5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: use withKeyring
method (#25435)
#27025
base: main
Are you sure you want to change the base?
Changes from 10 commits
6a165c8
77d3dd2
11bc013
90c82b2
e9c33ce
c483c52
d5005d0
9ad5aad
32eeb35
5e3ff33
181a31f
d302d2c
1501ecc
8919f77
7ec4004
6f073da
d84e167
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -147,6 +147,7 @@ import { | |
import { Interface } from '@ethersproject/abi'; | ||
import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis'; | ||
import { isEvmAccountType } from '@metamask/keyring-api'; | ||
import { normalize } from '@metamask/eth-sig-util'; | ||
import { | ||
AuthenticationController, | ||
UserStorageController, | ||
|
@@ -4241,7 +4242,10 @@ 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.setLedgerTransportPreference(); | ||
this.withKeyringForDevice( | ||
{ name: HardwareDeviceNames.ledger }, | ||
async (keyring) => this.setLedgerTransportPreference(keyring), | ||
); | ||
} | ||
} finally { | ||
releaseLock(); | ||
|
@@ -4373,7 +4377,10 @@ export default class MetamaskController extends EventEmitter { | |
// Optimistically called to not block MetaMask login due to | ||
// Ledger Keyring GitHub downtime | ||
if (completedOnboarding) { | ||
this.setLedgerTransportPreference(); | ||
this.withKeyringForDevice( | ||
{ name: HardwareDeviceNames.ledger }, | ||
async (keyring) => this.setLedgerTransportPreference(keyring), | ||
); | ||
} | ||
} | ||
|
||
|
@@ -4477,52 +4484,76 @@ export default class MetamaskController extends EventEmitter { | |
// Hardware | ||
// | ||
|
||
async getKeyringForDevice(deviceName, hdPath = null) { | ||
/** | ||
* Select a hardware wallet device and execute a | ||
* callback with the keyring for that device. | ||
* | ||
* Note that KeyringController state is not updated before | ||
* the end of the callback execution, and calls to KeyringController | ||
* methods within the callback can lead to deadlocks. | ||
* | ||
* @param {object} options - The options for the device | ||
* @param {string} options.name - The device name to select | ||
* @param {string} options.hdPath - An optional hd path to be set on the device | ||
* keyring | ||
* @param {*} callback - The callback to execute with the keyring | ||
* @returns {*} The result of the callback | ||
*/ | ||
async withKeyringForDevice(options, callback) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: can we make this private I see it's being used only on this file (can we have a workaround for the test file without mocking a private function)? |
||
const keyringOverrides = this.opts.overrides?.keyrings; | ||
let keyringName = null; | ||
switch (deviceName) { | ||
let keyringType = null; | ||
switch (options.name) { | ||
case HardwareDeviceNames.trezor: | ||
keyringName = keyringOverrides?.trezor?.type || TrezorKeyring.type; | ||
keyringType = keyringOverrides?.trezor?.type || TrezorKeyring.type; | ||
break; | ||
case HardwareDeviceNames.ledger: | ||
keyringName = keyringOverrides?.ledger?.type || LedgerKeyring.type; | ||
keyringType = keyringOverrides?.ledger?.type || LedgerKeyring.type; | ||
break; | ||
case HardwareDeviceNames.qr: | ||
keyringName = QRHardwareKeyring.type; | ||
keyringType = QRHardwareKeyring.type; | ||
break; | ||
case HardwareDeviceNames.lattice: | ||
keyringName = keyringOverrides?.lattice?.type || LatticeKeyring.type; | ||
keyringType = keyringOverrides?.lattice?.type || LatticeKeyring.type; | ||
break; | ||
default: | ||
throw new Error( | ||
'MetamaskController:getKeyringForDevice - Unknown device', | ||
'MetamaskController:withKeyringForDevice - Unknown device', | ||
); | ||
} | ||
let [keyring] = await this.keyringController.getKeyringsByType(keyringName); | ||
if (!keyring) { | ||
keyring = await this.keyringController.addNewKeyring(keyringName); | ||
} | ||
if (hdPath && keyring.setHdPath) { | ||
keyring.setHdPath(hdPath); | ||
} | ||
if (deviceName === HardwareDeviceNames.lattice) { | ||
keyring.appName = 'MetaMask'; | ||
} | ||
if (deviceName === HardwareDeviceNames.trezor) { | ||
const model = keyring.getModel(); | ||
this.appStateController.setTrezorModel(model); | ||
} | ||
|
||
keyring.network = getProviderConfig({ | ||
metamask: this.networkController.state, | ||
}).type; | ||
return this.keyringController.withKeyring( | ||
{ type: keyringType }, | ||
async (keyring) => { | ||
if (options.hdPath && keyring.setHdPath) { | ||
keyring.setHdPath(options.hdPath); | ||
} | ||
|
||
if (options.name === HardwareDeviceNames.lattice) { | ||
keyring.appName = 'MetaMask'; | ||
} | ||
|
||
if (options.name === HardwareDeviceNames.trezor) { | ||
const model = keyring.getModel(); | ||
this.appStateController.setTrezorModel(model); | ||
} | ||
|
||
keyring.network = getProviderConfig({ | ||
metamask: this.networkController.state, | ||
}).type; | ||
|
||
return keyring; | ||
return await callback(keyring); | ||
}, | ||
{ | ||
createIfMissing: true, | ||
}, | ||
); | ||
} | ||
|
||
async attemptLedgerTransportCreation() { | ||
const keyring = await this.getKeyringForDevice(HardwareDeviceNames.ledger); | ||
return await keyring.attemptMakeApp(); | ||
return await this.withKeyringForDevice( | ||
HardwareDeviceNames.ledger, | ||
async (keyring) => keyring.attemptMakeApp(), | ||
); | ||
} | ||
|
||
/** | ||
|
@@ -4534,35 +4565,38 @@ export default class MetamaskController extends EventEmitter { | |
* @returns [] accounts | ||
*/ | ||
async connectHardware(deviceName, page, hdPath) { | ||
const keyring = await this.getKeyringForDevice(deviceName, hdPath); | ||
|
||
if (deviceName === HardwareDeviceNames.ledger) { | ||
await this.setLedgerTransportPreference(keyring); | ||
} | ||
return this.withKeyringForDevice( | ||
{ name: deviceName, hdPath }, | ||
async (keyring) => { | ||
if (deviceName === HardwareDeviceNames.ledger) { | ||
await this.setLedgerTransportPreference(keyring); | ||
} | ||
|
||
let accounts = []; | ||
switch (page) { | ||
case -1: | ||
accounts = await keyring.getPreviousPage(); | ||
break; | ||
case 1: | ||
accounts = await keyring.getNextPage(); | ||
break; | ||
default: | ||
accounts = await keyring.getFirstPage(); | ||
} | ||
let accounts = []; | ||
switch (page) { | ||
case -1: | ||
accounts = await keyring.getPreviousPage(); | ||
break; | ||
case 1: | ||
accounts = await keyring.getNextPage(); | ||
break; | ||
default: | ||
accounts = await keyring.getFirstPage(); | ||
} | ||
|
||
// Merge with existing accounts | ||
// and make sure addresses are not repeated | ||
const oldAccounts = await this.keyringController.getAccounts(); | ||
// Merge with existing accounts | ||
// and make sure addresses are not repeated | ||
const oldAccounts = await this.keyringController.getAccounts(); | ||
|
||
const accountsToTrack = [ | ||
...new Set( | ||
oldAccounts.concat(accounts.map((a) => a.address.toLowerCase())), | ||
), | ||
]; | ||
this.accountTracker.syncWithAddresses(accountsToTrack); | ||
return accounts; | ||
const accountsToTrack = [ | ||
...new Set( | ||
oldAccounts.concat(accounts.map((a) => a.address.toLowerCase())), | ||
), | ||
]; | ||
this.accountTracker.syncWithAddresses(accountsToTrack); | ||
return accounts; | ||
}, | ||
); | ||
} | ||
|
||
/** | ||
|
@@ -4573,8 +4607,12 @@ export default class MetamaskController extends EventEmitter { | |
* @returns {Promise<boolean>} | ||
*/ | ||
async checkHardwareStatus(deviceName, hdPath) { | ||
const keyring = await this.getKeyringForDevice(deviceName, hdPath); | ||
return keyring.isUnlocked(); | ||
return this.withKeyringForDevice( | ||
{ name: deviceName, hdPath }, | ||
async (keyring) => { | ||
return keyring.isUnlocked(); | ||
}, | ||
); | ||
} | ||
|
||
/** | ||
|
@@ -4584,14 +4622,15 @@ export default class MetamaskController extends EventEmitter { | |
* @returns {Promise<boolean>} | ||
*/ | ||
async forgetDevice(deviceName) { | ||
const keyring = await this.getKeyringForDevice(deviceName); | ||
return this.withKeyringForDevice({ name: deviceName }, async (keyring) => { | ||
for (const address of keyring.accounts) { | ||
await this._onAccountRemoved(address); | ||
} | ||
|
||
for (const address of keyring.accounts) { | ||
await this.removeAccount(address); | ||
} | ||
keyring.forgetDevice(); | ||
|
||
keyring.forgetDevice(); | ||
return true; | ||
return true; | ||
}); | ||
} | ||
|
||
/** | ||
|
@@ -4629,21 +4668,22 @@ export default class MetamaskController extends EventEmitter { | |
* @returns {'ledger' | 'lattice' | string | undefined} | ||
*/ | ||
async getDeviceModel(address) { | ||
const keyring = await this.keyringController.getKeyringForAccount(address); | ||
switch (keyring.type) { | ||
case KeyringType.trezor: | ||
return keyring.getModel(); | ||
case KeyringType.qr: | ||
return keyring.getName(); | ||
case KeyringType.ledger: | ||
// TODO: get model after ledger keyring exposes method | ||
return HardwareDeviceNames.ledger; | ||
case KeyringType.lattice: | ||
// TODO: get model after lattice keyring exposes method | ||
return HardwareDeviceNames.lattice; | ||
default: | ||
return undefined; | ||
} | ||
return this.keyringController.withKeyring({ address }, async (keyring) => { | ||
switch (keyring.type) { | ||
case KeyringType.trezor: | ||
return keyring.getModel(); | ||
case KeyringType.qr: | ||
return keyring.getName(); | ||
case KeyringType.ledger: | ||
// TODO: get model after ledger keyring exposes method | ||
return HardwareDeviceNames.ledger; | ||
case KeyringType.lattice: | ||
// TODO: get model after lattice keyring exposes method | ||
return HardwareDeviceNames.lattice; | ||
default: | ||
return undefined; | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
|
@@ -4673,16 +4713,24 @@ export default class MetamaskController extends EventEmitter { | |
hdPath, | ||
hdPathDescription, | ||
) { | ||
const keyring = await this.getKeyringForDevice(deviceName, hdPath); | ||
|
||
keyring.setAccountToUnlock(index); | ||
const unlockedAccount = | ||
await this.keyringController.addNewAccountForKeyring(keyring); | ||
const label = this.getAccountLabel( | ||
deviceName === HardwareDeviceNames.qr ? keyring.getName() : deviceName, | ||
index, | ||
hdPathDescription, | ||
const { address: unlockedAccount, label } = await this.withKeyringForDevice( | ||
{ name: deviceName, hdPath }, | ||
async (keyring) => { | ||
keyring.setAccountToUnlock(index); | ||
const [address] = await keyring.addAccounts(1); | ||
return { | ||
address: normalize(address), | ||
label: this.getAccountLabel( | ||
deviceName === HardwareDeviceNames.qr | ||
? keyring.getName() | ||
: deviceName, | ||
index, | ||
hdPathDescription, | ||
), | ||
}; | ||
}, | ||
); | ||
|
||
// Set the account label to Trezor 1 / Ledger 1 / QR Hardware 1, etc | ||
this.preferencesController.setAccountLabel(unlockedAccount, label); | ||
// Select the account | ||
|
@@ -4837,20 +4885,8 @@ export default class MetamaskController extends EventEmitter { | |
* @param {string[]} address - A hex address | ||
*/ | ||
async removeAccount(address) { | ||
// Remove all associated permissions | ||
this.removeAllAccountPermissions(address); | ||
|
||
///: BEGIN:ONLY_INCLUDE_IF(build-mmi) | ||
this.custodyController.removeAccount(address); | ||
///: END:ONLY_INCLUDE_IF(build-mmi) | ||
|
||
const keyring = await this.keyringController.getKeyringForAccount(address); | ||
// Remove account from the keyring | ||
await this._onAccountRemoved(address); | ||
await this.keyringController.removeAccount(address); | ||
const updatedKeyringAccounts = keyring ? await keyring.getAccounts() : {}; | ||
if (updatedKeyringAccounts?.length === 0) { | ||
keyring.destroy?.(); | ||
} | ||
Comment on lines
-5296
to
-5299
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
return address; | ||
} | ||
|
@@ -6151,6 +6187,21 @@ export default class MetamaskController extends EventEmitter { | |
this._notifyChainChange(); | ||
} | ||
|
||
/** | ||
* Execute side effects of a removed account. | ||
* | ||
* @param {string} address - The address of the account to remove. | ||
* @returns {Promise<void>} | ||
*/ | ||
async _onAccountRemoved(address) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this seems to me it can be just a void can we remove the async keyword ? and await when calling the function |
||
// Remove all associated permissions | ||
this.removeAllAccountPermissions(address); | ||
|
||
///: BEGIN:ONLY_INCLUDE_IF(build-mmi) | ||
this.custodyController.removeAccount(address); | ||
///: END:ONLY_INCLUDE_IF(build-mmi) | ||
} | ||
|
||
// misc | ||
|
||
/** | ||
|
@@ -6412,16 +6463,15 @@ export default class MetamaskController extends EventEmitter { | |
/** | ||
* Sets the Ledger Live preference to use for Ledger hardware wallet support | ||
* | ||
* @param _keyring | ||
* @param keyring | ||
* @deprecated This method is deprecated and will be removed in the future. | ||
* Only webhid connections are supported in chrome and u2f in firefox. | ||
*/ | ||
async setLedgerTransportPreference(_keyring) { | ||
async setLedgerTransportPreference(keyring) { | ||
const transportType = window.navigator.hid | ||
? LedgerTransportTypes.webhid | ||
: LedgerTransportTypes.u2f; | ||
const keyring = | ||
_keyring || (await this.getKeyringForDevice(HardwareDeviceNames.ledger)); | ||
|
||
if (keyring?.updateTransportMethod) { | ||
return keyring.updateTransportMethod(transportType).catch((e) => { | ||
throw e; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably the biggest change of this PR:
getKeyringForDevice
is nowwithKeyringForDevice
.Instead of being a getter for a Keyring instance related to a device, it is now a safe callback executor for hardware devices.
It uses
KeyringController.withKeyring
behind the scenes, so it also takes care of:KeyringController.state