-
-
Notifications
You must be signed in to change notification settings - Fork 242
chore: create network enablement controller #6028
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
base: main
Are you sure you want to change the base?
Conversation
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/tests/NetworkEnablementController.test.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.ts
Outdated
Show resolved
Hide resolved
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.
Bug: Network Controller Errors: Type Handling & Nested Updates
The NetworkEnablementController
has two bugs:
- TypeError when disabling networks: The
#toggleNetwork
method can throw aTypeError
when attempting to disable a network. This occurs becauseselectAllEnabledNetworks(this.state)[namespace]
can returnundefined
if the namespace has no enabled networks, leading toObject.values(undefined)
. Additionally, the logic for counting enabled networks is unnecessarily complex and redundant, asselectAllEnabledNetworks(...)[namespace]
already returns a string array whoselength
should be used directly. - Nested state updates: The
#toggleNetwork
method callsthis.#ensureNetworkEntry(chainId)
within itsthis.update()
callback. Since#ensureNetworkEntry
also callsthis.update()
, this creates nested state updates, which can lead to state inconsistencies or unexpected behavior.
packages/network-enablement-controller/src/NetworkEnablementController.ts#L455-L466
core/packages/network-enablement-controller/src/NetworkEnablementController.ts
Lines 455 to 466 in 303ac2a
// Don't disable the last remaining enabled network | |
if ( | |
!enable && | |
Object.values(selectAllEnabledNetworks(this.state)[namespace]).flat() | |
.length <= 1 | |
) { | |
return; | |
} | |
this.update((s) => { | |
// Ensure entry exists first | |
this.#ensureNetworkEntry(chainId); |
Was this report helpful? Give feedback by reacting with 👍 or 👎
controller.setEnabledNetwork('0x1'); // Hex format for EVM | ||
controller.setEnabledNetwork('eip155:1'); // CAIP-2 format for EVM | ||
controller.setEnabledNetwork('solana:mainnet'); // CAIP-2 format for Solana |
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.
Nice! Should we allow an array, to allow enabling multiple networks. Just considering for the "select all" networks flow.
We can have this in a future version if performance is a concern.
const hasEvmNetworks = selectHasEnabledNetworksForNamespace('eip155')(state); | ||
``` | ||
|
||
## API Reference |
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.
Hmmm, open to discussion, but I don't think we should include a manually written API reference.
I'm not against this - it is really great for open source devs to view. But I am wondering if the full API spec can be auto-generated through types (we do include a typedoc file).
E.g. our README would look like:
## Usage
... our useage section for controller/selectors (amazing!)
## API Reference
For full API reference, look at [path to auto generated type doc].
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.
Unfortunately typedoc
generation is currently broken due to the way that we build the repo 😞 I've been slowly working on fixing this (#1845 (comment)) but I'm not sure when that will be complete. Whatever path you want to take here is fine with me, but I just wanted to let you know about that.
Having some kind of usage examples at the very least would be great though, we don't do that often enough.
"dependencies": { | ||
"@metamask/base-controller": "^8.0.1", | ||
"@metamask/controller-utils": "^11.11.0", | ||
"@metamask/keyring-api": "^19.0.0", |
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.
I was wondering why we needed this dep. Looks like it is to provide us the SolScope
property. Shame, it would be nice if this was provided through other utils.
import { SolScope } from '@metamask/keyring-api';
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.
If you don't want the dependency, could you copy the type? Sometimes we've done that before. Just a thought.
packages/network-enablement-controller/src/NetworkEnablementController.test.ts
Show resolved
Hide resolved
packages/network-enablement-controller/src/NetworkEnablementController.test.ts
Show resolved
Hide resolved
import { POPULAR_NETWORKS } from './constants'; | ||
import { selectAllEnabledNetworks } from './selectors'; | ||
|
||
// Unique name for the controller |
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.
We don't really need this comment do we?
[ChainId[BuiltInNetworkName.Mainnet]]: true, | ||
[ChainId[BuiltInNetworkName.LineaMainnet]]: true, | ||
[ChainId[BuiltInNetworkName.BaseMainnet]]: true, |
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.
I think it would be good to align the default networks with what is done in the NetworkController?
Just to avoid having multiple places to update.
Only mentioning since we will soon add Arbitrum as another default network.
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.
So that you don't have to maintain a list here as well, what are your thoughts on adding an init
method to this controller that intelligently populates the initial state from whatever network configurations are present in NetworkController state if no initial state was passed to the constructor? That is, the state of this controller would be set to {}
initially but would be populated in init
.
packages/network-enablement-controller/src/NetworkEnablementController.ts
Show resolved
Hide resolved
/** | ||
* A map of enabled networks by namespace and chain id. | ||
*/ | ||
type EnabledMap = Record<CaipNamespace, Record<string, boolean>>; |
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.
Because the CaipNamespace
can be deleted, can we make the value optional? Also thoughts on expanding this type so we can insert some JSDoc comments?
interface EnabledMap {
/**
* caipNamespace
* @example eip155 or solana
* NOTE - a namespace may be deleted, hence indexing can result in undefined.
*/
[caipNamespace: string]: {
/**
* chainId, provided as hex or CAIP-2
* @example 0x1 or solana:mainnet
*/
[chainId: string]: boolean
} | undefined
}
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.
I know what you mean, however, this type says that all keys in this object must be a CaipNamespace
and all values must be a Record<string, boolean>
. If you delete a key, wouldn't that still hold true?
Regardless, should chain IDs be CAIP chain IDs?
type EnabledMap = Record<CaipNamespace, Record<string, boolean>>; | |
type EnabledMap = Record<CaipNamespace, Record<CaipChainId, boolean>>; |
if (this.#hasOneEnabled(this.state, namespace, chainId)) { | ||
return; | ||
} |
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.
Q - will this hasOneEnabled
prevent a enabledNetworkMap entry from being deleted?
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.
Took a pass at this. Had a lot of comments so I decided to stop. :) I will do another pass later.
In general it seems like there is more complexity in this controller than I would expect even though we are tracking whether networks are enabled or disabled and organizing them by group. It seems that we are constantly having to convert CAIP chain IDs so I'm curious if there is a more succinct way we can represent them so we don't have to do that. I will take a closer look later.
this.messagingSystem = messenger; | ||
|
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 property is already set in BaseController
, do we need this here?
this.messagingSystem = messenger; |
* - A CAIP-2 chain ID (e.g., 'eip155:1' for Ethereum mainnet, 'solana:mainnet' for Solana) | ||
* @param enable - Whether to enable (true) or disable (false) the network. Defaults to true. | ||
*/ | ||
setEnabledNetwork(chainId: Hex | CaipChainId, enable = true): void { |
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 method seems to either set a network to enabled or disabled. Yet there is a method that explicitly sets a network to disabled below. That means there are two ways to disable a network using this controller. You can also set a network to disabled despite the name being setEnabledNetwork
.
To make the API more consistent and reduce the number of code paths, what are your thoughts on making this method only enable a network? Then, what are your thoughts on calling this enableNetwork
rather than setEnabledNetwork
? I think that it would be more clear what this method does.
* - A CAIP-2 chain ID (e.g., 'eip155:1' for Ethereum mainnet, 'solana:mainnet' for Solana) | ||
* @returns True if the network is enabled, false otherwise. | ||
*/ | ||
isNetworkEnabled(chainId: Hex | CaipChainId): boolean { |
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.
It looks like you already have an isNetworkEnabled
selector. To make this API lighter what are your thoughts on removing this method and having consumers always use the selector instead?
* @returns An object containing namespace, storageKey, and caipId | ||
* @throws Error if the chain ID cannot be parsed | ||
*/ | ||
#deriveKeys(chainId: Hex | CaipChainId) { |
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.
Does this need to be an instance method or can it be a function outside of the class? This way the selectors can use it.
this.#ensureNetworkEntry(chainId, false); | ||
this.#toggleNetwork(chainId, true); |
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.
It looks like each of these lines makes a state update. It's generally good practice to batch state updates together in the same operation. What are your thoughts on refactoring this so there is only one state update?
Object.values(selectAllEnabledNetworks(this.state)[namespace]).flat() | ||
.length <= 1 | ||
) { | ||
return; |
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.
Similar here. If this is something that's not allowed, should we throw an error so that we push the responsibility to prevent this from happening on to the UI layer in the client?
* This method is used to prevent unnecessary state updates when trying to enable | ||
* a network that is already the only enabled network in its namespace. |
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.
We added something a while back to BaseController that prevents redundant updates: c580f85. So I am not sure that using this method to prevent unnecessary state updates would be necessary. However, I can see how this might prevent the last network in a namespace from being disabled. Perhaps we should update this wording?
* This method is used to prevent unnecessary state updates when trying to enable | |
* a network that is already the only enabled network in its namespace. | |
* This method is used to prevent the last network in a namespace from being removed. |
} else { | ||
storageKey = caipId; | ||
} | ||
return { namespace, storageKey, caipId }; |
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.
What are your thoughts on having this method/function return reference
as well? It seems that this would prevent us from needing to re-parse caipId
any time we want the reference
(which we are doing a lot in this file).
this.update((s) => { | ||
// Ensure entry exists first | ||
this.#ensureNetworkEntry(chainId); | ||
|
||
if (this.#hasOneEnabled(s, namespace, chainId)) { | ||
return; | ||
} | ||
|
||
// If enabling a non-popular network, disable all networks in the same namespace | ||
if (enable && !this.#isPopularNetwork(caipId)) { | ||
Object.keys(s.enabledNetworkMap[namespace]).forEach((key) => { | ||
s.enabledNetworkMap[namespace][key] = false; | ||
}); | ||
} | ||
|
||
s.enabledNetworkMap[namespace][storageKey] = enable; | ||
}); |
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.
Do we need to do anything if #hasOneEnabled
returns true
?
this.update((s) => { | |
// Ensure entry exists first | |
this.#ensureNetworkEntry(chainId); | |
if (this.#hasOneEnabled(s, namespace, chainId)) { | |
return; | |
} | |
// If enabling a non-popular network, disable all networks in the same namespace | |
if (enable && !this.#isPopularNetwork(caipId)) { | |
Object.keys(s.enabledNetworkMap[namespace]).forEach((key) => { | |
s.enabledNetworkMap[namespace][key] = false; | |
}); | |
} | |
s.enabledNetworkMap[namespace][storageKey] = enable; | |
}); | |
this.update((s) => { | |
// Ensure entry exists first | |
this.#ensureNetworkEntry(chainId); | |
// If enabling a non-popular network, disable all networks in the same namespace | |
if (enable && !this.#isPopularNetwork(caipId)) { | |
Object.keys(s.enabledNetworkMap[namespace]).forEach((key) => { | |
s.enabledNetworkMap[namespace][key] = false; | |
}); | |
} | |
s.enabledNetworkMap[namespace][storageKey] = enable; | |
}); |
return; | ||
} | ||
|
||
// Don't disable the last remaining enabled network |
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.
Should this say:
// Don't disable the last remaining enabled network | |
// Don't update the last remaining enabled network |
But also, do we need this? Is this covered already by the #hasOneEnabled
check below?
Explanation
The
NetworkEnablementController
is a core implementation of network enablement functionality that was previously only available in the extension. This PR moves and expands this functionality from the extension's NetworkOrder controller to core, enabling network visibility features across both extension and mobile platforms. It also renames it toNetworkEnablementController
to more accurately represent what it does. Maybe in the future this can be expanded to include other UX enhancements that don't need to be tightly coupled with the NetworkControllerKey motivations for this change:
Network Enablement Feature: This work is part of the broader Network Enablement initiative (#5737). The controller has been expanded to include enabledNetworkMap state, allowing us to:
Track which networks are enabled/disabled
Support the new network enablement feature
Code Quality Improvements: The move to core has provided an opportunity to:
Improve test coverage of the existing network ordering logic
The controller now handles three key aspects:
Network enablement (new functionality. recently introduced on extension)
References
Changelog
Checklist