Skip to content

Commit 3d81cde

Browse files
authored
feat: add monad failover rpc (MetaMask#23479)
<!-- 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** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Adds QuickNode Failover for Monad. This needs a migration as Monad is already in prod. ## **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: added QuickNode fallback RPC for Monad. ## **Related issues** Fixes: ## **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** - [x] 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 - [x] 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] > Adds migration to set QuickNode failover URLs for Monad, refactors SEI migration to a shared util, updates configs, and expands tests. > > - **Migrations**: > - **109 (Monad)**: Adds `failoverUrls` to `NetworkController.networkConfigurationsByChainId['0x8f']` RPC endpoints using `process.env.QUICKNODE_MONAD_URL`. > - **107 (SEI)**: Refactored to use shared helper. > - Registered `109` in `migrationList` and async pipeline. > - **Migration Utility**: > - New `addFailoverUrlToNetworkConfiguration(state, chainId, version, name, envVar)` with validation and Sentry error handling; reused by 107/109. > - **Networks config**: > - Added `'monad-mainnet'` QuickNode mapping and enabled failover URLs for Monad in `PopularList`. > - **Tests**: > - New `109.test.ts` covering happy path, invalid states, and env-var behavior. > - Updated `107.test.ts` to spy `ensureValidState` and align expectations. > - Extended `util/index.test.ts` for the new helper. > - Added `QUICKNODE_MONAD_URL` to network-controller utils test env setup. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7388fc9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 16a9c88 commit 3d81cde

9 files changed

Lines changed: 808 additions & 175 deletions

File tree

app/core/Engine/controllers/network-controller/utils.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ function setQuicknodeEnvironmentVariables() {
389389
process.env.QUICKNODE_BASE_URL = 'https://example.quicknode.com/base';
390390
process.env.QUICKNODE_BSC_URL = 'https://example.quicknode.com/bsc';
391391
process.env.QUICKNODE_SEI_URL = 'https://example.quicknode.com/sei';
392+
process.env.QUICKNODE_MONAD_URL = 'https://example.quicknode.com/monad';
392393
}
393394

394395
/**

app/store/migrations/107.test.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { captureException } from '@sentry/react-native';
22
import { cloneDeep } from 'lodash';
33

4-
import { ensureValidState } from './util';
54
import migrate from './107';
65

76
jest.mock('@sentry/react-native', () => ({
87
captureException: jest.fn(),
98
}));
109

11-
jest.mock('./util', () => ({
12-
ensureValidState: jest.fn(),
13-
}));
14-
1510
const mockedCaptureException = jest.mocked(captureException);
16-
const mockedEnsureValidState = jest.mocked(ensureValidState);
11+
const mockedEnsureValidState = jest.spyOn(
12+
jest.requireActual('./util'),
13+
'ensureValidState',
14+
);
1715

1816
const migrationVersion = 107;
1917
const QUICKNODE_SEI_URL = 'https://failover.com';
@@ -48,25 +46,37 @@ describe(`migration #${migrationVersion}`, () => {
4846
const migratedState = migrate(state);
4947

5048
expect(migratedState).toStrictEqual({ some: 'state' });
51-
expect(mockedCaptureException).not.toHaveBeenCalled();
49+
// ensureValidState may call captureException for invalid states
50+
// but the migration should still return the state unchanged
5251
});
5352

5453
const invalidStates = [
5554
{
5655
state: {
57-
engine: {},
56+
engine: {
57+
backgroundState: {
58+
settings: {},
59+
},
60+
},
61+
settings: {},
62+
security: {},
5863
},
5964
errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`,
60-
scenario: 'empty engine state',
65+
scenario: 'missing NetworkController',
6166
},
6267
{
6368
state: {
6469
engine: {
65-
backgroundState: {},
70+
backgroundState: {
71+
NetworkController: 'invalid',
72+
settings: {},
73+
},
6674
},
75+
settings: {},
76+
security: {},
6777
},
68-
errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`,
69-
scenario: 'empty backgroundState',
78+
errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state: 'string'`,
79+
scenario: 'invalid NetworkController type',
7080
},
7181
{
7282
state: {
@@ -166,7 +176,7 @@ describe(`migration #${migrationVersion}`, () => {
166176
];
167177

168178
it.each(invalidStates)(
169-
'should capture exception if $scenario',
179+
'captures exception if $scenario',
170180
({ errorMessage, state }) => {
171181
const orgState = cloneDeep(state);
172182
mockedEnsureValidState.mockReturnValue(true);

app/store/migrations/107.ts

Lines changed: 9 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import { captureException } from '@sentry/react-native';
2-
import { hasProperty } from '@metamask/utils';
3-
import { isObject } from 'lodash';
4-
5-
import { ensureValidState } from './util';
1+
import { ensureValidState, addFailoverUrlToNetworkConfiguration } from './util';
62

73
const seiChainId = '0x531';
84
const migrationVersion = 107;
@@ -14,162 +10,15 @@ const migrationVersion = 107;
1410
* primary RPC endpoint is down.
1511
*/
1612
export default function migrate(state: unknown) {
17-
try {
18-
if (!ensureValidState(state, migrationVersion)) {
19-
return state;
20-
}
21-
22-
// Validate if the NetworkController state exists and has the expected structure.
23-
if (
24-
!hasProperty(state, 'engine') ||
25-
!hasProperty(state.engine, 'backgroundState') ||
26-
!hasProperty(state.engine.backgroundState, 'NetworkController')
27-
) {
28-
captureException(
29-
new Error(
30-
`Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`,
31-
),
32-
);
33-
return state;
34-
}
35-
36-
if (!isObject(state.engine.backgroundState.NetworkController)) {
37-
captureException(
38-
new Error(
39-
`Migration ${migrationVersion}: Invalid NetworkController state: '${typeof state.engine.backgroundState.NetworkController}'`,
40-
),
41-
);
42-
return state;
43-
}
44-
45-
if (
46-
!hasProperty(
47-
state.engine.backgroundState.NetworkController,
48-
'networkConfigurationsByChainId',
49-
)
50-
) {
51-
captureException(
52-
new Error(
53-
`Migration ${migrationVersion}: Invalid NetworkController state: missing networkConfigurationsByChainId property`,
54-
),
55-
);
56-
return state;
57-
}
58-
59-
if (
60-
!isObject(
61-
state.engine.backgroundState.NetworkController
62-
.networkConfigurationsByChainId,
63-
)
64-
) {
65-
captureException(
66-
new Error(
67-
`Migration ${migrationVersion}: Invalid NetworkController networkConfigurationsByChainId: '${typeof state.engine.backgroundState.NetworkController.networkConfigurationsByChainId}'`,
68-
),
69-
);
70-
return state;
71-
}
72-
73-
if (
74-
!hasProperty(
75-
state.engine.backgroundState.NetworkController
76-
.networkConfigurationsByChainId,
77-
seiChainId,
78-
)
79-
) {
80-
// SEI network not configured, no migration needed
81-
return state;
82-
}
83-
84-
if (
85-
!isObject(
86-
state.engine.backgroundState.NetworkController
87-
.networkConfigurationsByChainId[seiChainId],
88-
)
89-
) {
90-
captureException(
91-
new Error(
92-
`Migration ${migrationVersion}: Invalid SEI network configuration: '${typeof state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[seiChainId]}'`,
93-
),
94-
);
95-
return state;
96-
}
97-
98-
if (
99-
!hasProperty(
100-
state.engine.backgroundState.NetworkController
101-
.networkConfigurationsByChainId[seiChainId],
102-
'rpcEndpoints',
103-
)
104-
) {
105-
captureException(
106-
new Error(
107-
`Migration ${migrationVersion}: Invalid SEI network configuration: missing rpcEndpoints property`,
108-
),
109-
);
110-
return state;
111-
}
112-
113-
if (
114-
!Array.isArray(
115-
state.engine.backgroundState.NetworkController
116-
.networkConfigurationsByChainId[seiChainId].rpcEndpoints,
117-
)
118-
) {
119-
captureException(
120-
new Error(
121-
`Migration ${migrationVersion}: Invalid SEI network rpcEndpoints: expected array, got '${typeof state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[seiChainId].rpcEndpoints}'`,
122-
),
123-
);
124-
return state;
125-
}
126-
127-
// Update RPC endpoints to add failover URL if needed
128-
state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[
129-
seiChainId
130-
].rpcEndpoints =
131-
state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[
132-
seiChainId
133-
].rpcEndpoints.map((rpcEndpoint) => {
134-
// Skip if endpoint is not an object or doesn't have a url property
135-
if (
136-
!isObject(rpcEndpoint) ||
137-
!hasProperty(rpcEndpoint, 'url') ||
138-
typeof rpcEndpoint.url !== 'string'
139-
) {
140-
return rpcEndpoint;
141-
}
142-
143-
// Skip if endpoint already has failover URLs
144-
if (
145-
hasProperty(rpcEndpoint, 'failoverUrls') &&
146-
Array.isArray(rpcEndpoint.failoverUrls) &&
147-
rpcEndpoint.failoverUrls.length > 0
148-
) {
149-
return rpcEndpoint;
150-
}
151-
152-
// Add QuickNode failover URL
153-
const quickNodeUrl = process.env.QUICKNODE_SEI_URL;
154-
155-
if (quickNodeUrl) {
156-
return {
157-
...rpcEndpoint,
158-
failoverUrls: [quickNodeUrl],
159-
};
160-
}
161-
162-
return rpcEndpoint;
163-
});
164-
13+
if (!ensureValidState(state, migrationVersion)) {
16514
return state;
166-
} catch (error) {
167-
captureException(
168-
new Error(
169-
`Migration ${migrationVersion}: Failed to add failoverUrls to SEI network configuration: ${error}`,
170-
),
171-
);
17215
}
17316

174-
return state;
17+
return addFailoverUrlToNetworkConfiguration(
18+
state,
19+
seiChainId,
20+
migrationVersion,
21+
'SEI',
22+
'QUICKNODE_SEI_URL',
23+
);
17524
}

0 commit comments

Comments
 (0)