From 22153396e4ff1abbbd47fe53f3dc1d7556ac670e Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Thu, 9 Jan 2025 17:47:01 +0100 Subject: [PATCH] fix: remove goerli infura from network state (#12838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** core PR: https://github.com/MetaMask/core/pull/5094 ## **Related issues** Fixes: #12837 ## **Manual testing steps** 1. Run the app locally 2. you should not see the verbose logs , or any request sent to Goerli ## **Screenshots/Recordings** ### **Before** ### **After** ## **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. --- app/store/migrations/065.test.ts | 177 ++++++++++++++++++ app/store/migrations/065.ts | 83 ++++++++ app/store/migrations/index.ts | 2 + .../logs/__snapshots__/index.test.ts.snap | 28 --- app/util/test/initial-background-state.json | 28 --- .../@metamask+network-controller+22.1.0.patch | 59 ++++++ 6 files changed, 321 insertions(+), 56 deletions(-) create mode 100644 app/store/migrations/065.test.ts create mode 100644 app/store/migrations/065.ts create mode 100644 patches/@metamask+network-controller+22.1.0.patch diff --git a/app/store/migrations/065.test.ts b/app/store/migrations/065.test.ts new file mode 100644 index 00000000000..ad80dbfd647 --- /dev/null +++ b/app/store/migrations/065.test.ts @@ -0,0 +1,177 @@ +import migrate from './065'; +import { merge } from 'lodash'; +import { captureException } from '@sentry/react-native'; +import initialRootState from '../../util/test/initial-root-state'; +import { RootState } from '../../components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +describe('Migration: Remove Goerli and Linea Goerli if Infura type', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "Migration: Invalid root state: 'object'", + scenario: 'state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: null, + }), + errorMessage: "Migration: Invalid root engine state: 'object'", + scenario: 'engine state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: null, + }, + }), + errorMessage: "Migration: Invalid root engine backgroundState: 'object'", + scenario: 'backgroundState is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { NetworkController: null }, + }, + }), + errorMessage: "Migration: Invalid NetworkController state: 'object'", + scenario: 'NetworkController is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`captures exception if ${scenario}`, async () => { + const newState = await migrate(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('removes Goerli and Linea Goerli configurations with Infura type', async () => { + const mockState = { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x5': { + blockExplorerUrls: [], + chainId: '0x5', + rpcEndpoints: [ + { + networkClientId: 'goerli', + type: 'infura', + }, + ], + }, + '0xe704': { + blockExplorerUrls: [], + chainId: '0xe704', + rpcEndpoints: [ + { + networkClientId: 'linea-goerli', + type: 'infura', + }, + ], + }, + }, + }, + }, + }, + }; + + const migratedState = (await migrate(mockState)) as RootState; + + expect( + migratedState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + ).not.toHaveProperty('0x5'); + expect( + migratedState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + ).not.toHaveProperty('0xe704'); + }); + + it('retains configurations that are not Infura type', async () => { + const mockState = { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x5': { + blockExplorerUrls: [], + chainId: '0x5', + rpcEndpoints: [ + { + networkClientId: 'goerli', + type: 'custom', + }, + ], + }, + '0xe704': { + blockExplorerUrls: [], + chainId: '0xe704', + rpcEndpoints: [ + { + networkClientId: 'linea-goerli', + type: 'custom', + }, + ], + }, + }, + }, + }, + }, + }; + + const migratedState = (await migrate(mockState)) as RootState; + + expect( + migratedState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + ).toHaveProperty('0x5'); + expect( + migratedState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + ).toHaveProperty('0xe704'); + }); + + it('does not modify state if no matching configurations exist', async () => { + const mockState = { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + blockExplorerUrls: [], + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'infura', + }, + ], + }, + }, + }, + }, + }, + }; + + const migratedState = await migrate(mockState); + + expect(migratedState).toEqual(mockState); + }); +}); diff --git a/app/store/migrations/065.ts b/app/store/migrations/065.ts new file mode 100644 index 00000000000..8b020d31f7c --- /dev/null +++ b/app/store/migrations/065.ts @@ -0,0 +1,83 @@ +import { captureException } from '@sentry/react-native'; +import { isObject, hasProperty } from '@metamask/utils'; +import { NetworkState, RpcEndpointType } from '@metamask/network-controller'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; + +/** + * Migration to delete Goerli and Linea Goerli configurations if they are Infura types. + * @param {unknown} stateAsync - Promise Redux state. + * @returns Migrated Redux state. + */ +export default async function migrate(stateAsync: unknown) { + const state = await stateAsync; + + if (!isObject(state)) { + captureException( + new Error(`Migration: Invalid root state: '${typeof state}'`), + ); + return state; + } + + if (!isObject(state.engine)) { + captureException( + new Error( + `Migration: Invalid root engine state: '${typeof state.engine}'`, + ), + ); + return state; + } + + if (!isObject(state.engine.backgroundState)) { + captureException( + new Error( + `Migration: Invalid root engine backgroundState: '${typeof state.engine + .backgroundState}'`, + ), + ); + return state; + } + + const networkControllerState = state.engine.backgroundState + .NetworkController as NetworkState; + if (!isObject(networkControllerState)) { + captureException( + new Error( + `Migration: Invalid NetworkController state: '${typeof networkControllerState}'`, + ), + ); + return state; + } + + const networkConfigurationsByChainId = + networkControllerState.networkConfigurationsByChainId; + + if (!isObject(networkConfigurationsByChainId)) { + captureException( + new Error( + `Migration: Invalid networkConfigurationsByChainId: '${typeof networkConfigurationsByChainId}'`, + ), + ); + return state; + } + + // Chain IDs to remove + const chainIdsToRemove = [CHAIN_IDS.GOERLI, CHAIN_IDS.LINEA_GOERLI]; + + // Filter out Goerli and Linea Goerli configurations with Infura type + chainIdsToRemove.forEach((chainId) => { + if (hasProperty(networkConfigurationsByChainId, chainId)) { + const config = networkConfigurationsByChainId[chainId]; + + if ( + Array.isArray(config.rpcEndpoints) && + config.rpcEndpoints.some( + (endpoint) => endpoint.type === RpcEndpointType.Infura, + ) + ) { + delete networkConfigurationsByChainId[chainId]; + } + } + }); + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 44ef0d4f7d2..abc15ca9bb0 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -65,6 +65,7 @@ import migration61 from './061'; import migration62 from './062'; import migration63 from './063'; import migration64 from './064'; +import migration65 from './065'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -142,6 +143,7 @@ export const migrationList: MigrationsList = { 62: migration62, 63: migration63, 64: migration64, + 65: migration65, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index 6ee35eccd2d..27c9ea11dca 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -71,20 +71,6 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` }, ], }, - "0x5": { - "blockExplorerUrls": [], - "chainId": "0x5", - "defaultRpcEndpointIndex": 0, - "name": "Goerli", - "nativeCurrency": "GoerliETH", - "rpcEndpoints": [ - { - "networkClientId": "goerli", - "type": "infura", - "url": "https://goerli.infura.io/v3/{infuraProjectId}", - }, - ], - }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7", @@ -99,20 +85,6 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` }, ], }, - "0xe704": { - "blockExplorerUrls": [], - "chainId": "0xe704", - "defaultRpcEndpointIndex": 0, - "name": "Linea Goerli", - "nativeCurrency": "LineaETH", - "rpcEndpoints": [ - { - "networkClientId": "linea-goerli", - "type": "infura", - "url": "https://linea-goerli.infura.io/v3/{infuraProjectId}", - }, - ], - }, "0xe705": { "blockExplorerUrls": [], "chainId": "0xe705", diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index b55fbd59d02..953fb97a48b 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -71,20 +71,6 @@ } ] }, - "0x5": { - "blockExplorerUrls": [], - "chainId": "0x5", - "defaultRpcEndpointIndex": 0, - "name": "Goerli", - "nativeCurrency": "GoerliETH", - "rpcEndpoints": [ - { - "networkClientId": "goerli", - "type": "infura", - "url": "https://goerli.infura.io/v3/{infuraProjectId}" - } - ] - }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7", @@ -99,20 +85,6 @@ } ] }, - "0xe704": { - "blockExplorerUrls": [], - "chainId": "0xe704", - "defaultRpcEndpointIndex": 0, - "name": "Linea Goerli", - "nativeCurrency": "LineaETH", - "rpcEndpoints": [ - { - "networkClientId": "linea-goerli", - "type": "infura", - "url": "https://linea-goerli.infura.io/v3/{infuraProjectId}" - } - ] - }, "0xe705": { "blockExplorerUrls": [], "chainId": "0xe705", diff --git a/patches/@metamask+network-controller+22.1.0.patch b/patches/@metamask+network-controller+22.1.0.patch new file mode 100644 index 00000000000..773995c39f9 --- /dev/null +++ b/patches/@metamask+network-controller+22.1.0.patch @@ -0,0 +1,59 @@ +diff --git a/node_modules/@metamask/network-controller/dist/NetworkController.cjs b/node_modules/@metamask/network-controller/dist/NetworkController.cjs +index cc9793f..59bd31f 100644 +--- a/node_modules/@metamask/network-controller/dist/NetworkController.cjs ++++ b/node_modules/@metamask/network-controller/dist/NetworkController.cjs +@@ -108,28 +108,32 @@ const controllerName = 'NetworkController'; + * @returns The default value for `networkConfigurationsByChainId`. + */ + function getDefaultNetworkConfigurationsByChainId() { +- return Object.values(controller_utils_1.InfuraNetworkType).reduce((obj, infuraNetworkType) => { +- const chainId = controller_utils_1.ChainId[infuraNetworkType]; +- const rpcEndpointUrl = +- // This ESLint rule mistakenly produces an error. +- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions +- `https://${infuraNetworkType}.infura.io/v3/{infuraProjectId}`; +- const networkConfiguration = { +- blockExplorerUrls: [], +- chainId, +- defaultRpcEndpointIndex: 0, +- name: controller_utils_1.NetworkNickname[infuraNetworkType], +- nativeCurrency: controller_utils_1.NetworksTicker[infuraNetworkType], +- rpcEndpoints: [ +- { +- networkClientId: infuraNetworkType, +- type: RpcEndpointType.Infura, +- url: rpcEndpointUrl, +- }, +- ], +- }; +- return { ...obj, [chainId]: networkConfiguration }; +- }, {}); ++ return Object.values(controller_utils_1.InfuraNetworkType).reduce((obj, infuraNetworkType) => { ++ if (infuraNetworkType === controller_utils_1.InfuraNetworkType.goerli || ++ infuraNetworkType === controller_utils_1.InfuraNetworkType['linea-goerli']) { ++ return obj; ++ } ++ const chainId = controller_utils_1.ChainId[infuraNetworkType]; ++ const rpcEndpointUrl = ++ // This ESLint rule mistakenly produces an error. ++ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions ++ `https://${infuraNetworkType}.infura.io/v3/{infuraProjectId}`; ++ const networkConfiguration = { ++ blockExplorerUrls: [], ++ chainId, ++ defaultRpcEndpointIndex: 0, ++ name: controller_utils_1.NetworkNickname[infuraNetworkType], ++ nativeCurrency: controller_utils_1.NetworksTicker[infuraNetworkType], ++ rpcEndpoints: [ ++ { ++ networkClientId: infuraNetworkType, ++ type: RpcEndpointType.Infura, ++ url: rpcEndpointUrl, ++ }, ++ ], ++ }; ++ return { ...obj, [chainId]: networkConfiguration }; ++ }, {}); + } + /** + * Constructs properties for the NetworkController state whose values will be