Skip to content

Commit b6a61e2

Browse files
Mrtenzweitingsun
authored andcommitted
refactor: Refactor network controller to use messenger and init pattern (#20741)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This refactors the network controller to use modular init. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: #12876. ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Replaces inline `NetworkController` setup with a modular init using new messengers, wires it into Engine, and adds focused tests. > > - **Engine**: > - Replace inline `NetworkController` construction with modular `networkControllerInit` via `initModularizedControllers`; access via `controllersByName.NetworkController`. > - Remove direct imports/logic for default network state, failover URLs, and event subscriptions; rely on the new init module. > - Rewire dependent controllers (e.g., `AssetsContractController`, `TokenListController`, `RemoteFeatureFlagController`, `AccountTrackerController`, etc.) to use the initialized `NetworkController` messenger/state. > - **New Init Module** (`app/core/Engine/controllers/network-controller-init.ts`): > - Defines `ADDITIONAL_DEFAULT_NETWORKS`, computes initial state (including Infura failovers and network names), configures block tracker/RPC policies (incl. Quicknode handling), subscribes to `rpcEndpointUnavailable`/`rpcEndpointDegraded`, and calls `initializeProvider()`. > - **Messengers**: > - Add `getNetworkControllerMessenger` and `getNetworkControllerInitMessenger` in `messengers/network-controller-messenger.{ts,test.ts}` and register in `messengers/index.ts`. > - **Types/Plumbing**: > - Add `NetworkController` to `ControllersToInitialize` and related types in `types.ts`; update tests/mocks to include `networkControllerInit`. > - **Tests**: > - Add `network-controller-init.test.ts`; update `utils.test.ts` to mock/use the new controller init and messenger. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit be157c2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5a3afaf commit b6a61e2

File tree

8 files changed

+493
-275
lines changed

8 files changed

+493
-275
lines changed

app/core/Engine/Engine.ts

Lines changed: 131 additions & 275 deletions
Large diffs are not rendered by default.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { buildControllerInitRequestMock } from '../utils/test-utils';
2+
import { ExtendedControllerMessenger } from '../../ExtendedControllerMessenger';
3+
import {
4+
getNetworkControllerInitMessenger,
5+
getNetworkControllerMessenger,
6+
NetworkControllerInitMessenger,
7+
type NetworkControllerMessenger,
8+
} from '../messengers/network-controller-messenger';
9+
import { ControllerInitRequest } from '../types';
10+
import {
11+
ADDITIONAL_DEFAULT_NETWORKS,
12+
getInitialNetworkControllerState,
13+
networkControllerInit,
14+
} from './network-controller-init';
15+
import {
16+
getDefaultNetworkControllerState,
17+
NetworkController,
18+
} from '@metamask/network-controller';
19+
20+
jest.mock('@metamask/network-controller');
21+
22+
function getInitRequestMock(): jest.Mocked<
23+
ControllerInitRequest<
24+
NetworkControllerMessenger,
25+
NetworkControllerInitMessenger
26+
>
27+
> {
28+
const baseMessenger = new ExtendedControllerMessenger<never, never>();
29+
30+
const requestMock = {
31+
...buildControllerInitRequestMock(baseMessenger),
32+
controllerMessenger: getNetworkControllerMessenger(baseMessenger),
33+
initMessenger: getNetworkControllerInitMessenger(baseMessenger),
34+
};
35+
36+
return requestMock;
37+
}
38+
39+
describe('networkControllerInit', () => {
40+
jest
41+
.mocked(getDefaultNetworkControllerState)
42+
.mockImplementation((additionalNetworks) =>
43+
jest
44+
.requireActual('@metamask/network-controller')
45+
.getDefaultNetworkControllerState(additionalNetworks),
46+
);
47+
48+
it('initializes the controller', () => {
49+
const { controller } = networkControllerInit(getInitRequestMock());
50+
expect(controller).toBeInstanceOf(NetworkController);
51+
});
52+
53+
it('passes the proper arguments to the controller', () => {
54+
networkControllerInit(getInitRequestMock());
55+
56+
const controllerMock = jest.mocked(NetworkController);
57+
expect(controllerMock).toHaveBeenCalledWith({
58+
messenger: expect.any(Object),
59+
state: getInitialNetworkControllerState({}),
60+
additionalDefaultNetworks: ADDITIONAL_DEFAULT_NETWORKS,
61+
getBlockTrackerOptions: expect.any(Function),
62+
getRpcServiceOptions: expect.any(Function),
63+
infuraProjectId: 'NON_EMPTY',
64+
});
65+
});
66+
});
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { ControllerInitFunction } from '../types';
2+
import {
3+
getDefaultNetworkControllerState,
4+
NetworkController,
5+
NetworkState,
6+
} from '@metamask/network-controller';
7+
import {
8+
NetworkControllerInitMessenger,
9+
NetworkControllerMessenger,
10+
} from '../messengers/network-controller-messenger';
11+
import { ChainId } from '@metamask/controller-utils';
12+
import { getFailoverUrlsForInfuraNetwork } from '../../../util/networks/customNetworks';
13+
import { INFURA_PROJECT_ID } from '../../../constants/network';
14+
import { SECOND } from '../../../constants/time';
15+
import { getIsQuicknodeEndpointUrl } from './network-controller/utils';
16+
import {
17+
onRpcEndpointDegraded,
18+
onRpcEndpointUnavailable,
19+
} from './network-controller/messenger-action-handlers';
20+
import { MetricsEventBuilder } from '../../Analytics/MetricsEventBuilder';
21+
import { MetaMetrics } from '../../Analytics';
22+
import { Hex } from '@metamask/utils';
23+
24+
const NON_EMPTY = 'NON_EMPTY';
25+
26+
export const ADDITIONAL_DEFAULT_NETWORKS = [
27+
ChainId['megaeth-testnet'],
28+
ChainId['monad-testnet'],
29+
];
30+
31+
export function getInitialNetworkControllerState(persistedState: {
32+
NetworkController?: Partial<NetworkState>;
33+
}) {
34+
let initialNetworkControllerState =
35+
persistedState.NetworkController as NetworkState;
36+
37+
if (!initialNetworkControllerState) {
38+
initialNetworkControllerState = getDefaultNetworkControllerState(
39+
ADDITIONAL_DEFAULT_NETWORKS,
40+
);
41+
42+
// Add failovers for default Infura RPC endpoints
43+
initialNetworkControllerState.networkConfigurationsByChainId[
44+
ChainId.mainnet
45+
].rpcEndpoints[0].failoverUrls =
46+
getFailoverUrlsForInfuraNetwork('ethereum-mainnet');
47+
initialNetworkControllerState.networkConfigurationsByChainId[
48+
ChainId['linea-mainnet']
49+
].rpcEndpoints[0].failoverUrls =
50+
getFailoverUrlsForInfuraNetwork('linea-mainnet');
51+
initialNetworkControllerState.networkConfigurationsByChainId[
52+
ChainId['base-mainnet']
53+
].rpcEndpoints[0].failoverUrls =
54+
getFailoverUrlsForInfuraNetwork('base-mainnet');
55+
56+
// Update default popular network names
57+
initialNetworkControllerState.networkConfigurationsByChainId[
58+
ChainId.mainnet
59+
].name = 'Ethereum';
60+
initialNetworkControllerState.networkConfigurationsByChainId[
61+
ChainId['linea-mainnet']
62+
].name = 'Linea';
63+
initialNetworkControllerState.networkConfigurationsByChainId[
64+
ChainId['base-mainnet']
65+
].name = 'Base';
66+
}
67+
68+
return initialNetworkControllerState;
69+
}
70+
71+
/**
72+
* Initialize the network controller.
73+
*
74+
* @param request - The request object.
75+
* @param request.controllerMessenger - The messenger to use for the controller.
76+
* @returns The initialized controller.
77+
*/
78+
export const networkControllerInit: ControllerInitFunction<
79+
NetworkController,
80+
NetworkControllerMessenger,
81+
NetworkControllerInitMessenger
82+
> = ({ controllerMessenger, initMessenger, persistedState }) => {
83+
const infuraProjectId = INFURA_PROJECT_ID || NON_EMPTY;
84+
85+
const controller = new NetworkController({
86+
infuraProjectId,
87+
state: getInitialNetworkControllerState(persistedState),
88+
messenger: controllerMessenger,
89+
getBlockTrackerOptions: () =>
90+
process.env.IN_TEST
91+
? {}
92+
: {
93+
pollingInterval: 20 * SECOND,
94+
// The retry timeout is pretty short by default, and if the endpoint is
95+
// down, it will end up exhausting the max number of consecutive
96+
// failures quickly.
97+
retryTimeout: 20 * SECOND,
98+
},
99+
100+
getRpcServiceOptions: (rpcEndpointUrl: string) => {
101+
const maxRetries = 4;
102+
const commonOptions = {
103+
fetch: globalThis.fetch.bind(globalThis),
104+
btoa: globalThis.btoa.bind(globalThis),
105+
};
106+
107+
if (getIsQuicknodeEndpointUrl(rpcEndpointUrl)) {
108+
return {
109+
...commonOptions,
110+
policyOptions: {
111+
maxRetries,
112+
// When we fail over to Quicknode, we expect it to be down at
113+
// first while it is being automatically activated. If an endpoint
114+
// is down, the failover logic enters a "cooldown period" of 30
115+
// minutes. We'd really rather not enter that for Quicknode, so
116+
// keep retrying longer.
117+
maxConsecutiveFailures: (maxRetries + 1) * 14,
118+
},
119+
};
120+
}
121+
122+
return {
123+
...commonOptions,
124+
policyOptions: {
125+
maxRetries,
126+
// Ensure that the circuit does not break too quickly.
127+
maxConsecutiveFailures: (maxRetries + 1) * 7,
128+
},
129+
};
130+
},
131+
additionalDefaultNetworks: ADDITIONAL_DEFAULT_NETWORKS,
132+
});
133+
134+
initMessenger.subscribe(
135+
'NetworkController:rpcEndpointUnavailable',
136+
async ({
137+
chainId,
138+
endpointUrl,
139+
error,
140+
}: {
141+
chainId: Hex;
142+
endpointUrl: string;
143+
error: unknown;
144+
}) => {
145+
onRpcEndpointUnavailable({
146+
chainId,
147+
endpointUrl,
148+
infuraProjectId,
149+
error,
150+
trackEvent: ({ event, properties }) => {
151+
const metricsEvent = MetricsEventBuilder.createEventBuilder(event)
152+
.addProperties(properties)
153+
.build();
154+
MetaMetrics.getInstance().trackEvent(metricsEvent);
155+
},
156+
metaMetricsId: await MetaMetrics.getInstance().getMetaMetricsId(),
157+
});
158+
},
159+
);
160+
161+
initMessenger.subscribe(
162+
'NetworkController:rpcEndpointDegraded',
163+
async ({
164+
chainId,
165+
endpointUrl,
166+
error,
167+
}: {
168+
chainId: Hex;
169+
endpointUrl: string;
170+
error: unknown;
171+
}) => {
172+
onRpcEndpointDegraded({
173+
chainId,
174+
endpointUrl,
175+
error,
176+
infuraProjectId,
177+
trackEvent: ({ event, properties }) => {
178+
const metricsEvent = MetricsEventBuilder.createEventBuilder(event)
179+
.addProperties(properties)
180+
.build();
181+
MetaMetrics.getInstance().trackEvent(metricsEvent);
182+
},
183+
metaMetricsId: await MetaMetrics.getInstance().getMetaMetricsId(),
184+
});
185+
},
186+
);
187+
188+
controller.initializeProvider();
189+
190+
return {
191+
controller,
192+
};
193+
};

app/core/Engine/messengers/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ import {
5656
getSnapKeyringBuilderMessenger,
5757
} from './snap-keyring-builder-messenger';
5858
import { getKeyringControllerMessenger } from './keyring-controller-messenger';
59+
import {
60+
getNetworkControllerInitMessenger,
61+
getNetworkControllerMessenger,
62+
} from './network-controller-messenger';
5963

6064
/**
6165
* The messengers for the controllers that have been.
@@ -93,6 +97,10 @@ export const CONTROLLER_MESSENGERS = {
9397
getMessenger: getKeyringControllerMessenger,
9498
getInitMessenger: noop,
9599
},
100+
NetworkController: {
101+
getMessenger: getNetworkControllerMessenger,
102+
getInitMessenger: getNetworkControllerInitMessenger,
103+
},
96104
AppMetadataController: {
97105
getMessenger: getAppMetadataControllerMessenger,
98106
getInitMessenger: noop,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Messenger, RestrictedMessenger } from '@metamask/base-controller';
2+
import {
3+
getNetworkControllerMessenger,
4+
getNetworkControllerInitMessenger,
5+
} from './network-controller-messenger';
6+
7+
describe('getNetworkControllerMessenger', () => {
8+
it('returns a restricted messenger', () => {
9+
const messenger = new Messenger<never, never>();
10+
const networkControllerMessenger = getNetworkControllerMessenger(messenger);
11+
12+
expect(networkControllerMessenger).toBeInstanceOf(RestrictedMessenger);
13+
});
14+
});
15+
16+
describe('getNetworkControllerInitMessenger', () => {
17+
it('returns a restricted messenger', () => {
18+
const messenger = new Messenger<never, never>();
19+
const networkControllerInitMessenger =
20+
getNetworkControllerInitMessenger(messenger);
21+
22+
expect(networkControllerInitMessenger).toBeInstanceOf(RestrictedMessenger);
23+
});
24+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Messenger } from '@metamask/base-controller';
2+
import { ErrorReportingServiceCaptureExceptionAction } from '@metamask/error-reporting-service';
3+
import {
4+
NetworkControllerRpcEndpointDegradedEvent,
5+
NetworkControllerRpcEndpointUnavailableEvent,
6+
} from '@metamask/network-controller';
7+
8+
type AllowedActions = ErrorReportingServiceCaptureExceptionAction;
9+
10+
export type NetworkControllerMessenger = ReturnType<
11+
typeof getNetworkControllerMessenger
12+
>;
13+
14+
/**
15+
* Get a restricted messenger for the network controller. This is scoped to the
16+
* actions and events that the network controller is allowed to handle.
17+
*
18+
* @param messenger - The messenger to restrict.
19+
* @returns The restricted messenger.
20+
*/
21+
export function getNetworkControllerMessenger(
22+
messenger: Messenger<AllowedActions, never>,
23+
) {
24+
return messenger.getRestricted({
25+
name: 'NetworkController',
26+
allowedActions: ['ErrorReportingService:captureException'],
27+
allowedEvents: [],
28+
});
29+
}
30+
31+
type AllowedInitializationActions = never;
32+
33+
type AllowedInitializationEvents =
34+
| NetworkControllerRpcEndpointDegradedEvent
35+
| NetworkControllerRpcEndpointUnavailableEvent;
36+
37+
export type NetworkControllerInitMessenger = ReturnType<
38+
typeof getNetworkControllerInitMessenger
39+
>;
40+
41+
/**
42+
* Get a restricted messenger for the network controller. This is scoped to the
43+
* actions and events that the network controller is allowed to handle during
44+
* initialization.
45+
*
46+
* @param messenger - The messenger to restrict.
47+
* @returns The restricted messenger.
48+
*/
49+
export function getNetworkControllerInitMessenger(
50+
messenger: Messenger<
51+
AllowedInitializationActions,
52+
AllowedInitializationEvents
53+
>,
54+
) {
55+
return messenger.getRestricted({
56+
name: 'NetworkControllerInit',
57+
allowedActions: [],
58+
allowedEvents: [
59+
'NetworkController:rpcEndpointDegraded',
60+
'NetworkController:rpcEndpointUnavailable',
61+
],
62+
});
63+
}

app/core/Engine/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,7 @@ export type ControllersToInitialize =
693693
| 'MultichainAccountService'
694694
| 'SnapKeyringBuilder'
695695
///: END:ONLY_INCLUDE_IF
696+
| 'NetworkController'
696697
| 'AccountTreeController'
697698
| 'AccountsController'
698699
| 'ApprovalController'

0 commit comments

Comments
 (0)