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

[composable-controller] Fix incorrect behavior and improve type-level safeguards #4467

Merged
merged 20 commits into from
Aug 20, 2024

Conversation

MajorLift
Copy link
Contributor

@MajorLift MajorLift commented Jun 27, 2024

Overview

This commit fixes issues with the ComposableController class's interface, and its logic for validating V1 and V2 controllers.

These changes will enable ComposableController to function correctly downstream in the Mobile Engine, and eventually the Wallet Framework POC.

Explanation

The previous approach of generating mock controller classes from the ComposableControllerState input using the GetChildControllers was flawed, because the mock controllers would always be incomplete, complicating attempts at validation.

Instead, we now rely on the downstream consumer to provide both a composed type of state schemas (ComposableControllerState) and a type union of the child controller instances (ChildControllers). For example, in mobile, we can use (with some adjustments) EngineState for the former, and Controllers[keyof Controllers] for the latter.

The validation logic for V1 controllers has also been updated. Due to breaking changes made to the private properties of BaseControllerV1 (#3959), mobile V1 controllers relying on versions prior to these changes were introduced were incompatible with the up-to-date BaseControllerV1 version that the composable-controller package references.

In this commit, the validator type BaseControllerV1Instance filters out the problematic private properties by using the PublicInterface type. Because the public API of BaseControllerV1 has been relatively constant, this removes the type errors that previously occurred in mobile when passing V1 controllers into ComposableController.

References

Changelog

@metamask/composable-controller (major)

Changed

  • BREAKING: Add two required generic parameters to the ComposableController class: ComposedControllerState (constrained by LegacyComposableControllerStateConstraint) and ChildControllers (constrained by ControllerInstance) (#4467).
  • BREAKING: The type guard isBaseController now validates that the input has an object-type property named metadata in addition to its existing checks.
  • BREAKING: The type guard isBaseControllerV1 now validates that the input has object-type properties config, state, and function-type property subscribe, in addition to its existing checks.
  • BREAKING: Narrow LegacyControllerStateConstraint type from BaseState | StateConstraint to BaseState & object | StateConstraint.
  • Add an optional generic parameter ControllerName to the RestrictedControllerMessengerConstraint type, which extends string and defaults to string.

Fixed

  • BREAKING: The ComposableController class raises a type error if a non-controller with no state property is passed into the ChildControllers generic parameter or the controllers constructor option.
    • Previously, a runtime error was thrown at class instantiation with no type-level enforcement.
  • When the ComposableController class is instantiated, its messenger now attempts to subscribe to all child controller stateChange events that are included in the messenger's events allowlist.
    • This was always the expected behavior, but a bug introduced in @metamask/composable-controller@6.0.0 caused stateChange event subscriptions to fail.
  • isBaseController and isBaseControllerV1 no longer return false negatives.
    • The instanceof operator is no longer used to validate that the input is a subclass of BaseController or BaseControllerV1.
  • The ChildControllerStateChangeEvents type checks that the child controller's state extends from the StateConstraintV1 type instead of from Record<string, unknown>. (#4467)
    • V1 controllers define their state types using the interface keyword, which are incompatible with Record<string, unknown> by default. This resulted in ChildControllerStateChangeEvents failing to generate stateChange events for V1 controllers and returning never.

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

@MajorLift MajorLift self-assigned this Jun 27, 2024
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch 4 times, most recently from 6457a28 to 1a452ef Compare July 2, 2024 15:45
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch from 1a452ef to 7359c4b Compare July 6, 2024 03:43
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch from 7359c4b to 63e3837 Compare July 17, 2024 01:48
@MajorLift MajorLift requested review from a team July 17, 2024 02:07
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch 2 times, most recently from a65140e to 604c2e9 Compare July 17, 2024 02:21
@MajorLift MajorLift changed the base branch from main to bump-utils-to-9.1.0 July 17, 2024 02:41
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch 2 times, most recently from d5343e5 to a850f90 Compare July 17, 2024 02:45
@MajorLift MajorLift marked this pull request as ready for review July 17, 2024 02:51
@MajorLift MajorLift requested a review from a team as a code owner July 17, 2024 02:51
Base automatically changed from bump-utils-to-9.1.0 to main July 17, 2024 15:09
@MajorLift MajorLift requested review from a team as code owners July 17, 2024 15:09
@MajorLift MajorLift removed request for a team July 18, 2024 13:31
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch 3 times, most recently from a32d745 to ea7e75d Compare July 26, 2024 15:52
@MetaMask MetaMask deleted a comment from github-actions bot Jul 26, 2024
@MajorLift

This comment was marked as resolved.

This comment was marked as outdated.

@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch from c081c50 to f59de5e Compare July 31, 2024 23:13
@MajorLift

This comment was marked as resolved.

This comment was marked as resolved.

@MajorLift

This comment was marked as resolved.

This comment was marked as resolved.

@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch from 1f29151 to 36f9cf4 Compare August 1, 2024 00:06
@MajorLift MajorLift marked this pull request as ready for review August 1, 2024 02:25
@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch from edc11c3 to 7804ce6 Compare August 1, 2024 16:38
Copy link
Contributor Author

@MajorLift MajorLift left a comment

Choose a reason for hiding this comment

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

Notes for reviewers:

*/
export class ComposableController<
ComposableControllerState extends LegacyComposableControllerStateConstraint,
ChildControllers extends ControllerInstance = GetChildControllers<ComposableControllerState>,
ChildControllers extends ControllerInstance,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous approach of generating mock controller classes from the ComposableControllerState input was flawed, because the mock controllers would always be incomplete, complicating attempts at validation.

Instead, we now rely on the downstream consumer to provide both a composed type of state schemas (ComposableControllerState) and a type union of the child controller instances (ChildControllers).

For example, in mobile, we can use (with some adjustments) EngineState for the former, and Controllers[keyof Controllers] for the latter.

Comment on lines +195 to +345
try {
this.messagingSystem.subscribe(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// False negative. `name` is a string type.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${name}:stateChange`,
(childState: Record<string, unknown>) => {
(childState: LegacyControllerStateConstraint) => {
this.update((state) => {
Object.assign(state, { [name]: childState });
});
},
);
} else if (isBaseControllerV1(controller)) {
controller.subscribe((childState) => {
} catch (error: unknown) {
// False negative. `name` is a string type.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.error(`${name} - ${String(error)}`);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To handle edge cases due to mobile using older BaseControllerV1 versions, this method no longer attempts to limit the stateChange event subscription to controllers that are defined with a messenger. Instead, it relies on the events allowlist of the ComposableController messenger to ensure that only valid subscriptions are made.

The subscribe call throws an error when invoked with a stateChange event that is not in the allowlist. This prevents any incorrect subscriptions to child controllers without a messenger or a stateChange event.

The catch block is intended to allow the logic to continue without terminating prematurely. We can remove or replace the console.error call if it causes noisy messages downstream.

Comment on lines +211 to +347
if (isBaseControllerV1(controller)) {
controller.subscribe((childState: StateConstraintV1) => {
Copy link
Contributor Author

@MajorLift MajorLift Aug 1, 2024

Choose a reason for hiding this comment

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

This subscription applies to all V1 child controllers. This may result in duplicate state updates for V1 child controllers with messengers, but the performance impact should be minimal, and it shouldn't affect the correctness of state.

Also, this issue will be automatically resolved as we phase out V1 controllers in our clients.

@MajorLift

This comment was marked as outdated.

This comment was marked as outdated.

@MajorLift MajorLift force-pushed the 240620-fix-ComposableController-validator-types branch from b42590f to 2ca9038 Compare August 1, 2024 19:32
@MajorLift
Copy link
Contributor Author

@metamaskbot publish-preview

Copy link
Contributor

github-actions bot commented Aug 1, 2024

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/accounts-controller": "18.0.0-preview-2ca9038",
  "@metamask-previews/address-book-controller": "5.0.0-preview-2ca9038",
  "@metamask-previews/announcement-controller": "7.0.0-preview-2ca9038",
  "@metamask-previews/approval-controller": "7.0.2-preview-2ca9038",
  "@metamask-previews/assets-controllers": "37.0.0-preview-2ca9038",
  "@metamask-previews/base-controller": "6.0.2-preview-2ca9038",
  "@metamask-previews/build-utils": "3.0.0-preview-2ca9038",
  "@metamask-previews/chain-controller": "0.1.1-preview-2ca9038",
  "@metamask-previews/composable-controller": "7.0.0-preview-2ca9038",
  "@metamask-previews/controller-utils": "11.0.2-preview-2ca9038",
  "@metamask-previews/ens-controller": "13.0.1-preview-2ca9038",
  "@metamask-previews/eth-json-rpc-provider": "4.1.2-preview-2ca9038",
  "@metamask-previews/gas-fee-controller": "19.0.1-preview-2ca9038",
  "@metamask-previews/json-rpc-engine": "9.0.2-preview-2ca9038",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.2-preview-2ca9038",
  "@metamask-previews/keyring-controller": "17.1.2-preview-2ca9038",
  "@metamask-previews/logging-controller": "5.0.0-preview-2ca9038",
  "@metamask-previews/message-manager": "10.0.2-preview-2ca9038",
  "@metamask-previews/name-controller": "8.0.0-preview-2ca9038",
  "@metamask-previews/network-controller": "20.1.0-preview-2ca9038",
  "@metamask-previews/notification-controller": "6.0.0-preview-2ca9038",
  "@metamask-previews/notification-services-controller": "0.2.0-preview-2ca9038",
  "@metamask-previews/permission-controller": "11.0.0-preview-2ca9038",
  "@metamask-previews/permission-log-controller": "3.0.0-preview-2ca9038",
  "@metamask-previews/phishing-controller": "10.1.1-preview-2ca9038",
  "@metamask-previews/polling-controller": "9.0.1-preview-2ca9038",
  "@metamask-previews/preferences-controller": "13.0.1-preview-2ca9038",
  "@metamask-previews/profile-sync-controller": "0.2.0-preview-2ca9038",
  "@metamask-previews/queued-request-controller": "4.0.0-preview-2ca9038",
  "@metamask-previews/rate-limit-controller": "6.0.0-preview-2ca9038",
  "@metamask-previews/selected-network-controller": "17.0.0-preview-2ca9038",
  "@metamask-previews/signature-controller": "18.0.1-preview-2ca9038",
  "@metamask-previews/transaction-controller": "35.1.0-preview-2ca9038",
  "@metamask-previews/user-operation-controller": "14.0.1-preview-2ca9038"
}

@MajorLift MajorLift changed the title [composable-controller] Fix validator types [composable-controller] Fix incorrect behavior and improve type-level safeguards Aug 2, 2024
Copy link
Member

@mikesposito mikesposito 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! just one question

@MajorLift MajorLift merged commit c522a4d into main Aug 20, 2024
116 checks passed
@MajorLift MajorLift deleted the 240620-fix-ComposableController-validator-types branch August 20, 2024 17:04
MajorLift added a commit that referenced this pull request Aug 21, 2024
## Explanation

This commit moves `BaseController`-related types and functions in
`@metamask/composable-controller` to `@metamask/base-controller`.

Because applying these changes requires a concurrent major update of
`@metamask/base-controller`, this commit will be excluded from
`@metamask/composable-controller@8.0.0`
(#4467), so that complications can
be avoided while applying `8.0.0` to mobile.

## References

- Blocked by #4467
- Blocked by MetaMask/metamask-mobile#10441

## Changelog

### `@metamask/base-controller` (minor)

### Added

- Migrate from `@metamask/composable-controller@8.0.0` into
`@metamask/base-controller`: types `LegacyControllerStateConstraint`,
`RestrictedControllerMessengerConstraint` and type guard functions
`isBaseController`, `isBaseControllerV1`
([#4581](#4581))
- Add and export types `ControllerInstance`, `BaseControllerInstance`,
`StateDeriverConstraint`, `StateMetadataConstraint`,
`StatePropertyMetadataConstraint`, `BaseControllerV1Instance`,
`ConfigConstraintV1`, `StateConstraintV1`
([#4581](#4581))

### `@metamask/composable-controller` (major)

### Removed

- **BREAKING:** Remove exports for types
`LegacyControllerStateConstraint`,
`RestrictedControllerMessengerConstraint`, and type guard functions
`isBaseController`, `isBaseControllerV1`
([#4467](#4467))
  - These have been migrated to `@metamask/base-controller@6.2.0`.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[composable-controller] Fix BaseControllerV1Instance type and make ChildControllers a required type parameter
3 participants