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

feat: NetworkController changes to support Network Syncing #4939

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
146 changes: 143 additions & 3 deletions packages/network-controller/src/NetworkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,11 @@ export type NetworkConfiguration = {
* Custom RPC endpoints do not need a `networkClientId` property because it is
* assumed that they have not already been added and therefore network clients
* do not exist for them yet (and hence IDs need to be generated).
*
* However Custom RPC endpoints, that are synchronized between devices,
* can contain a `networkClientId` set on both devices.
Comment on lines +216 to +217
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For custom RPCs, the networkClientId is a random UUID.
To make it easier for syncing, if a device is adding new networks from remote, we can reuse the UUID.

This was there is less chance of having some weird state where 2 devices use the same networks + RPCs, but they are different UUIDs.

Copy link
Contributor Author

@Prithpal-Sooriya Prithpal-Sooriya Nov 15, 2024

Choose a reason for hiding this comment

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

This is mostly optional (to keep uuid eventual consistency) across devices.

*/
export type AddNetworkCustomRpcEndpointFields = Omit<
export type AddNetworkCustomRpcEndpointFields = Partialize<
CustomRpcEndpoint,
'networkClientId'
>;
Expand Down Expand Up @@ -473,6 +476,11 @@ export type NetworkControllerGetNetworkConfigurationByNetworkClientId = {
handler: NetworkController['getNetworkConfigurationByNetworkClientId'];
};

export type NetworkControllerDangerouslySetNetworkConfigurationAction = {
type: 'NetworkController:dangerouslySetNetworkConfiguration';
handler: NetworkController['dangerouslySetNetworkConfiguration'];
};

export type NetworkControllerActions =
| NetworkControllerGetStateAction
| NetworkControllerGetEthQueryAction
Expand All @@ -483,7 +491,8 @@ export type NetworkControllerActions =
| NetworkControllerSetActiveNetworkAction
| NetworkControllerSetProviderTypeAction
| NetworkControllerGetNetworkConfigurationByChainId
| NetworkControllerGetNetworkConfigurationByNetworkClientId;
| NetworkControllerGetNetworkConfigurationByNetworkClientId
| NetworkControllerDangerouslySetNetworkConfigurationAction;

export type NetworkControllerMessenger = RestrictedControllerMessenger<
typeof controllerName,
Expand Down Expand Up @@ -954,6 +963,13 @@ export class NetworkController extends BaseController<
`${this.name}:getSelectedNetworkClient`,
this.getSelectedNetworkClient.bind(this),
);

this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:dangerouslySetNetworkConfiguration`,
this.dangerouslySetNetworkConfiguration.bind(this),
);
}

/**
Expand Down Expand Up @@ -1557,7 +1573,8 @@ export class NetworkController extends BaseController<
defaultOrCustomRpcEndpointFields.type === RpcEndpointType.Custom
? {
...defaultOrCustomRpcEndpointFields,
networkClientId: uuidV4(),
networkClientId:
defaultOrCustomRpcEndpointFields.networkClientId ?? uuidV4(),
}
: defaultOrCustomRpcEndpointFields;
return {
Expand Down Expand Up @@ -1941,6 +1958,129 @@ export class NetworkController extends BaseController<
);
}

/**
* This is used to override an existing network configuration.
* This is only meant for internal use only and not to be exposed via the UI.
* It is used as part of "Network Syncing", to sync networks, RPCs and block explorers cross devices.
*
* This will subsequently update the network client registry; state.networksMetadata, and state.selectedNetworkClientId
* @param networkConfiguration - the network configuration to override
*/
async dangerouslySetNetworkConfiguration(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I understand that this is a lot of logic here. I've ensured that we correctly update controller state; as well as internal data structures used inside the controller.

Fundamentally we need this method as we are unable to sync/override networks due to the random (uuidV4()) networkClientIds assigned to different devices (hence we were not able to use the existing updatedNetwork method).

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 name for this is a bit of a throwback to Reacts dangerouslySetInnerHTML. I want to signify to developers that we should not use this method, and we need to be very cautious when using this method.

networkConfiguration: NetworkConfiguration,
) {
const prevNetworkConfig: NetworkConfiguration | undefined =
networkConfiguration.chainId in this.state.networkConfigurationsByChainId
? this.state.networkConfigurationsByChainId[
networkConfiguration.chainId
]
: undefined;

if (!prevNetworkConfig) {
// We only want to perform overrides, not add new network configurations
return;
}

// Update Registry (remove old and add new)
const updateRegistry = () => {
// Unregister old networks we want to override
const autoManagedNetworkClientRegistry =
this.#ensureAutoManagedNetworkClientRegistryPopulated();
const networkClientRemoveOperations = prevNetworkConfig.rpcEndpoints.map(
(rpcEndpoint) => {
return {
type: 'remove' as const,
rpcEndpoint,
};
},
);
this.#unregisterNetworkClientsAsNeeded({
networkClientOperations: networkClientRemoveOperations,
autoManagedNetworkClientRegistry,
});

// Register new networks we want to override
const networkClientAddOperations = networkConfiguration.rpcEndpoints.map(
(rpcEndpoint) => {
return {
type: 'add' as const,
rpcEndpoint,
};
},
);
this.#registerNetworkClientsAsNeeded({
networkFields: networkConfiguration,
networkClientOperations: networkClientAddOperations,
autoManagedNetworkClientRegistry,
});
};

// Replace the networkConfiguration with our new networkConfiguration
// This is a full replace (no merging)
const replaceNetworkConfiguration = () => {
// Update State
this.update((state) => {
state.networkConfigurationsByChainId[networkConfiguration.chainId] =
networkConfiguration;
});

// Update Cache
this.#networkConfigurationsByNetworkClientId =
buildNetworkConfigurationsByNetworkClientId(
this.state.networkConfigurationsByChainId,
);
};

// Updates the NetworksMetadata State
const updateNetworksMetadata = async () => {
// Remove old metadata state
this.update((state) => {
prevNetworkConfig.rpcEndpoints.forEach((r) => {
if (state.networksMetadata?.[r.networkClientId]) {
delete state.networksMetadata[r.networkClientId];
}
});
});

// Add new metadata state
for (const r of networkConfiguration.rpcEndpoints) {
await this.lookupNetwork(r.networkClientId);
}
};

// Update selectedNetworkId State
// Will try to keep the same OR will select a new RPC from new network OR any network (edge case)
const updateSelectedNetworkId = async () => {
const selectedClientId = this.state.selectedNetworkClientId;
const wasClientIdReplaced = prevNetworkConfig.rpcEndpoints.some(
(r) => r.networkClientId === selectedClientId,
);
const doesExistInNewNetwork = networkConfiguration.rpcEndpoints.some(
(r) => r.networkClientId === selectedClientId,
);

const shouldUpdateSelectedNetworkId =
wasClientIdReplaced && !doesExistInNewNetwork;
if (shouldUpdateSelectedNetworkId) {
// Update the clientId to "something" that exists
const newRPCClientId = networkConfiguration.rpcEndpoints.find(
(r) => r.networkClientId in this.state.networksMetadata,
)?.networkClientId;
const anyRPCClientId = Object.keys(this.state.networksMetadata)[0];
/* istanbul ignore next: anyRPCClientId and selectedClientId are fallbacks and should be impossible to reach */
const newlySelectedNetwork =
newRPCClientId ?? anyRPCClientId ?? selectedClientId;
await this.#refreshNetwork(newlySelectedNetwork);
}
};

// Execute Set Network Config
updateRegistry();
replaceNetworkConfiguration();
await updateNetworksMetadata();
await updateSelectedNetworkId();
}

/**
* Assuming that the network has been previously switched, switches to this
* new network.
Expand Down
1 change: 1 addition & 0 deletions packages/network-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type {
NetworkControllerFindNetworkClientIdByChainIdAction,
NetworkControllerSetProviderTypeAction,
NetworkControllerSetActiveNetworkAction,
NetworkControllerDangerouslySetNetworkConfigurationAction,
NetworkControllerGetNetworkConfigurationByNetworkClientId,
NetworkControllerActions,
NetworkControllerMessenger,
Expand Down
Loading
Loading