Skip to content
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

Merged
merged 24 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
309d61e
refactor: use `withKeyring` in MetamaskController
mikesposito Jun 20, 2024
4ff2c60
fix: `unlockHardwareWalletAccount` account label assignment
mikesposito Jun 20, 2024
b8139cf
fix: no-shadow var
mikesposito Jun 20, 2024
6ce4941
test: remove unapplicable test case
mikesposito Jun 20, 2024
7953eff
test: change `getKeyringForDevice` to `withKeyringForDevice`
mikesposito Jun 20, 2024
5f83a21
fix: shadow var
mikesposito Jun 24, 2024
77723b5
test: remove outdated cases
mikesposito Jun 24, 2024
ac50918
fix: `unlockHardwareWalletAccount` should return normalized address
mikesposito Jun 24, 2024
c896f29
refactor: leave `setLedgerTransportPreference` promise dangling
mikesposito Jun 24, 2024
a25b945
fix: deadlock on account removal
mikesposito Jun 24, 2024
10fc798
test: remove outdated cases
mikesposito Jun 24, 2024
710cdd9
fix: `forgetDevice` should remove keyring through `this.removeAccount`
mikesposito Jun 24, 2024
b3bc905
refactor: extract side effects from `removeAccount`
mikesposito Jun 24, 2024
4e9bd9f
fix: `removeAccount` should return the removed address
mikesposito Jun 24, 2024
45be5c4
Merge branch 'develop' into use-with-keyring
mikesposito Jul 8, 2024
7958f65
Merge branch 'develop' into use-with-keyring
mikesposito Jul 10, 2024
2baee85
Merge branch 'develop' into use-with-keyring
mikesposito Jul 12, 2024
bbf5af9
Merge branch 'develop' into use-with-keyring
mikesposito Jul 15, 2024
a7397d9
refactor: remove `isKeyringAvailable`
mikesposito Jul 16, 2024
01ca220
refactor: normalize `unlockedAccount` address
mikesposito Jul 16, 2024
ee14fba
refactor: rename `deviceSelector` argument
mikesposito Jul 16, 2024
c09d281
Merge branch 'develop' into use-with-keyring
mikesposito Jul 16, 2024
10e3236
Merge branch 'develop' into use-with-keyring
mikesposito Jul 18, 2024
b93dd74
Merge branch 'develop' into use-with-keyring
mikesposito Jul 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 153 additions & 103 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,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 {
methodsRequiringNetworkSwitch,
methodsWithConfirmation,
Expand Down Expand Up @@ -4015,7 +4016,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();
Expand Down Expand Up @@ -4145,7 +4149,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),
);
mikesposito marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -4249,50 +4256,74 @@ 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) {
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 = this.networkController.state.providerConfig.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);
}

return keyring;
keyring.network = this.networkController.state.providerConfig.type;

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

/**
Expand All @@ -4304,35 +4335,38 @@ export default class MetamaskController extends EventEmitter {
* @returns [] accounts
*/
async connectHardware(deviceName, page, hdPath) {
const keyring = await this.getKeyringForDevice(deviceName, hdPath);
return this.withKeyringForDevice(
{ name: deviceName, hdPath },
async (keyring) => {
if (deviceName === HardwareDeviceNames.ledger) {
await this.setLedgerTransportPreference(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;
},
);
}

/**
Expand All @@ -4343,8 +4377,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();
},
);
}

/**
Expand All @@ -4354,14 +4392,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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By extracting side effects from removeAccount we can execute side effects here without ending up in a deadlock.

The account is removed from KeyringController anyway when calling keyring.forgetDevice()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. These side effects are a little strange (permissions should be automatically removed when an account is deleted, for instance). But I see that this is similar to how it already works today. Seems like a reasonable solution to me

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Also, ideally, side effects should be executed after the account removal instead of before since an error could occur (that now would also trigger a rollback)

}

for (const address of keyring.accounts) {
await this.removeAccount(address);
}
keyring.forgetDevice();

keyring.forgetDevice();
return true;
return true;
});
}

/**
Expand Down Expand Up @@ -4399,21 +4438,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;
}
});
}

/**
Expand Down Expand Up @@ -4443,16 +4483,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
Expand Down Expand Up @@ -4628,20 +4676,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?.();
Gudahtt marked this conversation as resolved.
Show resolved Hide resolved
}

return address;
}
Expand Down Expand Up @@ -5854,6 +5890,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) {
// Remove all associated permissions
this.removeAllAccountPermissions(address);

///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
this.custodyController.removeAccount(address);
///: END:ONLY_INCLUDE_IF(build-mmi)
}

// misc

/**
Expand Down Expand Up @@ -6124,16 +6175,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;
Expand Down
Loading