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

Merge EthKeyringController into KeyringController #3830

Merged
merged 15 commits into from
Jan 30, 2024

Conversation

mikesposito
Copy link
Member

@mikesposito mikesposito commented Jan 24, 2024

Explanation

This PR moves the logic implemented in EthKeyringController to KeyringController, leaving KeyringController's API unchanged

References

Changelog

@metamask/keyring-controller

  • ADDED: Added keyringBuilderFactory util
  • ADDED: Added GenericEncryptor, ExportableKeyEncryptor and SerializedKeyring types
  • CHANGED: Removed @metamask/eth-keyring-controller dep
  • CHANGED: Added dependencies from @metamask/eth-keyring-controller
    • @metamask/eth-hd-keyring
    • @metamask/eth-simple-keyring
    • @metamask/eth-sig-util
    • @metamask/browser-passworder

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've highlighted breaking changes using the "BREAKING" category above as appropriate

@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch from 49d7b61 to bd80fe1 Compare January 24, 2024 12:30
@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch 2 times, most recently from e087e79 to d11a19a Compare January 24, 2024 16:48
Comment on lines 1751 to 1746
/**
* Update the controller state with its current keyrings.
*/
async #updateKeyringsInState(): Promise<void> {
const keyrings = await Promise.all(this.#keyrings.map(displayForKeyring));
this.update((state) => {
state.keyrings = keyrings;
});
}
Copy link
Member Author

@mikesposito mikesposito Jan 24, 2024

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps #updateStateKeyrings would reflect the old name better, but I'm not sure what would be the best naming for this

Suggested change
/**
* Update the controller state with its current keyrings.
*/
async #updateKeyringsInState(): Promise<void> {
const keyrings = await Promise.all(this.#keyrings.map(displayForKeyring));
this.update((state) => {
state.keyrings = keyrings;
});
}
/**
* Update the controller state with its current keyrings.
*/
async #updateStateKeyrings(): Promise<void> {
const keyrings = await Promise.all(this.#keyrings.map(displayForKeyring));
this.update((state) => {
state.keyrings = keyrings;
});
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I like updateKeyringsInState over updateStateKeyrings as it reads better. However it might be nice to convey that it's not taking a set of keyrings but rather it's persisting the keyrings that have already been saved. So perhaps persistKeyrings? I don't know if we already use "persist" elsewhere to mean "write to state", but it seems to fit.

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately we already have a method called persistAllKeyrings, which saves the contents of the #keyrings array to the encrypted (and persisted) storage - the vault

This function instead, syncs the Controller state with the contents of the #keyrings array, so we are not persisting keyrings anywhere, but we are updating their representation in the controller state

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, gotcha. Okay, updateKeyringsInState seems fine then.

@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch 2 times, most recently from 88ab32e to a5ed982 Compare January 24, 2024 20:26
@@ -35,13 +34,13 @@ export default class MockEncryptor implements ExportableKeyEncryptor {
throw new Error(INVALID_PASSWORD_ERROR);
}

return cacheVal ?? {};
return JSON.parse(cacheVal) ?? {};
Copy link
Member Author

@mikesposito mikesposito Jan 25, 2024

Choose a reason for hiding this comment

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

This is due to an interesting discovery regarding the QRKeyring. Apparently, the serialize method of the keyring does not return an object compatible with what we internally call "serializable" (i.e. Json type), as it contains keys with undefined as value

The only reason why this does not break production is that the encryptor calls JSON.stringify, which removes any key with undefined as value.

Without this, the unlock operation would be broken after adding the QRKeyring, as the isSerializedKeyringArray assertion would fail

Copy link
Member Author

@mikesposito mikesposito Jan 25, 2024

Choose a reason for hiding this comment

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

Due to this, I'm also realising that we make sure that what is unlocked from the vault is a correct serializable object, but we don't do that when locking and saving the keyrings to the vault (the other way around): this means that:

  • we are not sure that we are saving a proper object in the vault, but we expect it to be when reading from it.
  • we are effectively relying on the fact that the object that we retrieve from the vault is not the same that we saved in it

Perhaps we could add the same check in persistAllKeyrings, even if it is not required type-wise. This would ensure that each keyring implements serialize properly (given that this is our requirement)

* Checks for duplicate keypairs, using the the first account in the given
* array. Rejects if a duplicate is found.
*
* Only supports 'Simple Key Pair'.
Copy link
Member Author

@mikesposito mikesposito Jan 25, 2024

Choose a reason for hiding this comment

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

Uhmm, we probably want to avoid account duplication with any keyring type, but I'm wondering if we want to change this behavior in this PR

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah perhaps not, but good eye. I wonder if this was done intentionally for simplicity or was added before the other keyring types were added and hasn't been updated since.

Comment on lines +760 to +761
if (type === KeyringTypes.hd && (!isObject(opts) || !opts.mnemonic)) {
if (!keyring.generateRandomMnemonic) {
throw new Error(
KeyringControllerError.UnsupportedGenerateRandomMnemonic,
);
}

keyring.generateRandomMnemonic();
await keyring.addAccounts(1);
}
Copy link
Member Author

@mikesposito mikesposito Jan 25, 2024

Choose a reason for hiding this comment

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

Most likely unrelated to this PR: I feel like this should be the responsibility of a hypothetical init method to be added on HDKeyring

mikesposito added a commit that referenced this pull request Jan 26, 2024
## Explanation

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

This PR refactors tests related to user operations methods, to align
them with the structure used for other methods.
It also adds test coverage for keyrings that do not support user
operations

## References

<!--
Are there any issues that this pull request is tied to? Are there other
links that reviewers should consult to understand these changes better?

For example:

* Fixes #12345
* Related to #67890
-->

* Related to #3830 

## Changelog

<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

No functional changes

## Checklist

- [ ] I've updated the test suite for new or updated code as appropriate
- [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [ ] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
mikesposito added a commit that referenced this pull request Jan 26, 2024
## Explanation

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

This PR adds additional tests for edge cases, useful for #3830.
Some `istanbul ignore if` statements are also being removed

## References

<!--
Are there any issues that this pull request is tied to? Are there other
links that reviewers should consult to understand these changes better?

For example:

* Fixes #12345
* Related to #67890
-->

* Fixes #3767 
* See #3830 

## Changelog

<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

No functional changes

## Checklist

- [ ] I've updated the test suite for new or updated code as appropriate
- [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [ ] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch 2 times, most recently from 18c9f9a to 84d9cf4 Compare January 26, 2024 10:42
@mikesposito mikesposito marked this pull request as ready for review January 26, 2024 10:47
@mikesposito mikesposito requested a review from a team as a code owner January 26, 2024 10:47
@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch from c26aba4 to 83a94cd Compare January 26, 2024 12:28
Comment on lines 1834 to 1850
/**
* Create keyring.
* Create a new keyring, ensuring that the first account is
* also created.
*
* - Creates a new vault.
* - Creates a new keyring with at least one account.
* - Makes the first account the selected account.
* @param type - Keyring type to instantiate.
* @param opts - Optional parameters required to instantiate the keyring.
* @returns A promise that resolves if the operation was successful.
* @returns A promise that resolves if the operation is successful.
*/
async #createKeyring(type: string, opts?: unknown) {
async #createKeyringWithFirstAccount(type: string, opts?: unknown) {
const keyring = (await this.addNewKeyring(type, opts)) as EthKeyring<Json>;

const [firstAccount] = await keyring.getAccounts();
Copy link
Member Author

Choose a reason for hiding this comment

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

This method has been renamed to better match what it is used for

Copy link
Member Author

@mikesposito mikesposito Jan 26, 2024

Choose a reason for hiding this comment

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

I'm now realizing the new naming might also be incorrect since the first account is not explicitly created here, but only expected to be created - and this is a case only for HD keyring because we explicitly check for that in the addNewAccount method.

I think there might be a better way to do all this (see also my previous comment: #3830 (comment))

Comment on lines 754 to 756
if (!keyring) {
throw new Error(KeyringControllerError.NoKeyring);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This condition is not possible, even in case the keyring builder is somehow broken and returns undefined, as #newKeyring would throw an error.

@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch from b2f6d4e to a9d1f1a Compare January 26, 2024 14:21
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

I checked every method in eth-keyring-controller and everything seems to be represented here. Nice! All of my comments are either thoughts or minor suggestions.

packages/keyring-controller/src/KeyringController.ts Outdated Show resolved Hide resolved
@@ -0,0 +1 @@
declare module '@metamask/eth-hd-keyring';
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that adding these files will make it possible to build @metamask/keyring-controller. But these type augmentations won't get included in the compiled version of the package, so in order to use the package, consumers may have to add these to their project as well.

However... I see that we copied these from eth-keyring-controller. And obviously we haven't had the problem I described above, because otherwise we would have had to handle it already. So maybe we don't need to do anything right now, but we should probably make a note to add type definition files to these packages (even if they're empty) so we don't have to do this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Uhm good point. We should tackle the typescript migration for these keyring packages, and that might be part of the effort to align them all with the Keyring API - but in the meantime, adding blank type definition files to these packages sounds like a good short-term solution.

return res.concat(arr);
}, []);

// Cast to `Hex[]` here is safe here because `addresses` has no nullish
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a thought for later: it would be nice if we could add an overload to normalize so it returns Hex if given something non-nullish, so we don't have to cast here.

Comment on lines 1751 to 1746
/**
* Update the controller state with its current keyrings.
*/
async #updateKeyringsInState(): Promise<void> {
const keyrings = await Promise.all(this.#keyrings.map(displayForKeyring));
this.update((state) => {
state.keyrings = keyrings;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I like updateKeyringsInState over updateStateKeyrings as it reads better. However it might be nice to convey that it's not taking a set of keyrings but rather it's persisting the keyrings that have already been saved. So perhaps persistKeyrings? I don't know if we already use "persist" elsewhere to mean "write to state", but it seems to fit.

* Checks for duplicate keypairs, using the the first account in the given
* array. Rejects if a duplicate is found.
*
* Only supports 'Simple Key Pair'.
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah perhaps not, but good eye. I wonder if this was done intentionally for simplicity or was added before the other keyring types were added and hasn't been updated since.

*
* @param error - Error message or Error object to throw.
*/
export function throwError(error: string | Error): never {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like such a simple function that I'm curious if we need it, but if it was copied from somewhere else, then I suppose that's fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

We copied this one from eth-keyring-controller, but it is used in only three occurrences, and all of them just pass a string - so I think this is just adding cognitive overload.

removed in a9712ca

@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch from a9712ca to 9985f69 Compare January 29, 2024 13:21
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

Looks good to me!

@mikesposito mikesposito force-pushed the refactor/merge-keyring-controllers branch from 9985f69 to dd28464 Compare January 29, 2024 19:18
@mikesposito mikesposito merged commit 50ee1ee into main Jan 30, 2024
136 checks passed
@mikesposito mikesposito deleted the refactor/merge-keyring-controllers branch January 30, 2024 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Merge EthKeyringController into KeyringController
2 participants