Skip to content

Commit

Permalink
feat: add dangerouslySetNetworkConfiguration method to override netwo…
Browse files Browse the repository at this point in the history
…rkConfiguration

this is used for network syncing to override local state with the remove state.
  • Loading branch information
Prithpal-Sooriya committed Nov 19, 2024
1 parent ad4ec39 commit 6e65961
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 1 deletion.
138 changes: 137 additions & 1 deletion packages/network-controller/src/NetworkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,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 @@ -486,7 +491,8 @@ export type NetworkControllerActions =
| NetworkControllerSetActiveNetworkAction
| NetworkControllerSetProviderTypeAction
| NetworkControllerGetNetworkConfigurationByChainId
| NetworkControllerGetNetworkConfigurationByNetworkClientId;
| NetworkControllerGetNetworkConfigurationByNetworkClientId
| NetworkControllerDangerouslySetNetworkConfigurationAction;

export type NetworkControllerMessenger = RestrictedControllerMessenger<
typeof controllerName,
Expand Down Expand Up @@ -957,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 @@ -1945,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(
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
199 changes: 199 additions & 0 deletions packages/network-controller/tests/NetworkController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11436,6 +11436,205 @@ describe('NetworkController', () => {
});
});

describe('dangerouslySetNetworkConfiguration', () => {
const TEST_CHAIN_ID = '0x1337';
const ORIGINAL_NETWORK_CLIENT_ID = '1111';

const arrangeTestUtils = (props: { newNetworkClientIds: string[] }) => {
mockCreateNetworkClient().mockReturnValue(buildFakeClient());

const originalNetwork = buildCustomNetworkConfiguration({
chainId: TEST_CHAIN_ID,
rpcEndpoints: [
buildCustomRpcEndpoint({
networkClientId: ORIGINAL_NETWORK_CLIENT_ID,
}),
],
});

const overrideNetwork = buildCustomNetworkConfiguration({
chainId: TEST_CHAIN_ID,
rpcEndpoints: props.newNetworkClientIds.map((id) =>
buildCustomRpcEndpoint({ networkClientId: id }),
),
});

const controllerState =
buildNetworkControllerStateWithDefaultSelectedNetworkClientId({
networkConfigurationsByChainId: {
[originalNetwork.chainId]: originalNetwork,
},
networksMetadata: {
[ORIGINAL_NETWORK_CLIENT_ID]: {
EIPS: {
'1559': true,
},
status: NetworkStatus.Available,
},
},
});

return { originalNetwork, overrideNetwork, controllerState };
};

const actTest = async (
props: Pick<
ReturnType<typeof arrangeTestUtils>,
'controllerState' | 'overrideNetwork'
>,
) => {
const result = await withController(
{ state: props.controllerState },
async ({ controller }) => {
await controller.dangerouslySetNetworkConfiguration(
props.overrideNetwork,
);
return {
state: controller.state,
controller,
};
},
);

return result;
};

const arrangeActTest = async (props: { newNetworkClientIds: string[] }) => {
const arrange = arrangeTestUtils(props);

// Act
const result = await actTest({
controllerState: arrange.controllerState,
overrideNetwork: arrange.overrideNetwork,
});
return { ...arrange, result };
};

const assertStateHasBeenUpdated = (props: {
state: NetworkState;
newNetworkConfiguration: NetworkConfiguration;
networkClientIdsRemoved: string[];
}) => {
const { state, newNetworkConfiguration, networkClientIdsRemoved } = props;

// Assert - new network config has been set
expect(state.networkConfigurationsByChainId[TEST_CHAIN_ID]).toStrictEqual(
newNetworkConfiguration,
);

// Assert - networks metadata removed some endpoints (from original network)
networkClientIdsRemoved.forEach((id) => {
expect(state.networksMetadata[id]).toBeUndefined();
});

// Assert - networks metadata has been set
newNetworkConfiguration.rpcEndpoints.forEach((r) => {
expect(state.networksMetadata[r.networkClientId]).toBeDefined();
});
};

const assertNetworkRegistryHasBeenUpdated = (props: {
controller: NetworkController;
newNetworkConfiguration: NetworkConfiguration;
networkClientIdsRemoved: string[];
}) => {
const { controller, newNetworkConfiguration, networkClientIdsRemoved } =
props;
const registry = controller.getNetworkClientRegistry();

// Assert - networks that were removed (from original network that was overwritten)
networkClientIdsRemoved.forEach((id) => {
expect(registry[id]).toBeUndefined();
});

// Assert - new network config RPCs has been updated in the network registry
newNetworkConfiguration.rpcEndpoints.forEach((r) => {
expect(registry[r.networkClientId]).toBeDefined();
});
};

const assertNetworkConfigurationsByIdCacheHasBeenUpdated = (props: {
controller: NetworkController;
newNetworkConfiguration: NetworkConfiguration;
}) => {
const { controller, newNetworkConfiguration } = props;
expect(
controller.getNetworkConfigurationByChainId(TEST_CHAIN_ID),
).toStrictEqual(newNetworkConfiguration);
};

it('overrides a set network configuration', async () => {
const { result, overrideNetwork } = await arrangeActTest({
newNetworkClientIds: ['2222', '3333'],
});

assertStateHasBeenUpdated({
state: result.state,
newNetworkConfiguration: overrideNetwork,
networkClientIdsRemoved: [ORIGINAL_NETWORK_CLIENT_ID],
});
assertNetworkRegistryHasBeenUpdated({
controller: result.controller,
newNetworkConfiguration: overrideNetwork,
networkClientIdsRemoved: [ORIGINAL_NETWORK_CLIENT_ID],
});
assertNetworkConfigurationsByIdCacheHasBeenUpdated({
controller: result.controller,
newNetworkConfiguration: overrideNetwork,
});

// Selected network has changed (since original was removed)
// We will select next available RPC for the given chain
expect(result.state.selectedNetworkClientId).toBe('2222');
});

it('overrides network config, but keeps same selected network', async () => {
const { result, overrideNetwork } = await arrangeActTest({
newNetworkClientIds: [ORIGINAL_NETWORK_CLIENT_ID, '2222'],
});

assertStateHasBeenUpdated({
state: result.state,
newNetworkConfiguration: overrideNetwork,
// no networks were removed, as the new network config contains the original network client id
networkClientIdsRemoved: [],
});
assertNetworkRegistryHasBeenUpdated({
controller: result.controller,
newNetworkConfiguration: overrideNetwork,
// no networks were removed, as the new network config contains the original network client id
networkClientIdsRemoved: [],
});
assertNetworkConfigurationsByIdCacheHasBeenUpdated({
controller: result.controller,
newNetworkConfiguration: overrideNetwork,
});

// selected RPC has not changed, as it was not removed
expect(result.state.selectedNetworkClientId).toBe(
ORIGINAL_NETWORK_CLIENT_ID,
);
});

it('does nothing if there is no network to override', async () => {
const { controllerState, overrideNetwork } = arrangeTestUtils({
newNetworkClientIds: ['2222'],
});

// shim/mock the controller state to not contain a network we are overriding
controllerState.networkConfigurationsByChainId['0xDiffChain'] =
controllerState.networkConfigurationsByChainId[TEST_CHAIN_ID];
delete controllerState.networkConfigurationsByChainId[TEST_CHAIN_ID];
controllerState.networkConfigurationsByChainId['0xDiffChain'].chainId =
'0xDiffChain';

const result = await actTest({ controllerState, overrideNetwork });

// No changes should have occurred
expect(result.state).toStrictEqual(controllerState);
});
});

describe('rollbackToPreviousProvider', () => {
describe('when called not following any network switches', () => {
for (const infuraNetworkType of Object.values(InfuraNetworkType)) {
Expand Down

0 comments on commit 6e65961

Please sign in to comment.