From 6f3125d2ddf0913311903120a37396f2af3b14f1 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 10 Nov 2025 21:13:45 +0100 Subject: [PATCH 01/11] fix: remove sei from initialNetworkControllerState --- app/core/Engine/controllers/network-controller-init.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/core/Engine/controllers/network-controller-init.ts b/app/core/Engine/controllers/network-controller-init.ts index e6a22323702a..d9f6315ee7ca 100644 --- a/app/core/Engine/controllers/network-controller-init.ts +++ b/app/core/Engine/controllers/network-controller-init.ts @@ -90,9 +90,12 @@ export function getInitialNetworkControllerState(persistedState: { initialNetworkControllerState.networkConfigurationsByChainId[ ChainId['polygon-mainnet'] ].name = 'Polygon'; - initialNetworkControllerState.networkConfigurationsByChainId[ + + // Remove Sei from initial state so it appears in Additional Networks section + // Users can add it manually, and it will be available in FEATURED_RPCS + delete initialNetworkControllerState.networkConfigurationsByChainId[ ChainId['sei-mainnet'] - ].name = 'Sei'; + ]; } return initialNetworkControllerState; From bca5a5c77febc14ca5953f37fe8e88b2a627e2d1 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 10 Nov 2025 21:14:47 +0100 Subject: [PATCH 02/11] chore: add fallback rpc quicknode to sei --- app/util/networks/customNetworks.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/util/networks/customNetworks.tsx b/app/util/networks/customNetworks.tsx index 4f4eef579bb9..13b5f33681b3 100644 --- a/app/util/networks/customNetworks.tsx +++ b/app/util/networks/customNetworks.tsx @@ -25,6 +25,7 @@ export const QUICKNODE_ENDPOINT_URLS_BY_INFURA_NETWORK_NAME = { 'polygon-mainnet': () => process.env.QUICKNODE_POLYGON_URL, 'base-mainnet': () => process.env.QUICKNODE_BASE_URL, 'bsc-mainnet': () => process.env.QUICKNODE_BSC_URL, + 'sei-mainnet': () => process.env.QUICKNODE_SEI_URL, }; export function getFailoverUrlsForInfuraNetwork( @@ -139,7 +140,7 @@ export const PopularList = [ chainId: toHex('1329'), nickname: 'Sei', rpcUrl: `https://sei-mainnet.infura.io/v3/${infuraProjectId}`, - failoverRpcUrls: [], + failoverRpcUrls: getFailoverUrlsForInfuraNetwork('sei-mainnet'), ticker: 'SEI', warning: true, rpcPrefs: { From e63f0703cd628535603b9c072c7dc4106120ea2a Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 11 Nov 2025 00:59:21 +0100 Subject: [PATCH 03/11] remove sei from initial background state --- app/util/test/initial-background-state.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 7fced8f964d7..50e5b3edc214 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -128,21 +128,6 @@ } ] }, - "0x531": { - "blockExplorerUrls": [], - "chainId": "0x531", - "defaultRpcEndpointIndex": 0, - "name": "Sei", - "nativeCurrency": "SEI", - "rpcEndpoints": [ - { - "failoverUrls": [], - "networkClientId": "sei-mainnet", - "type": "infura", - "url": "https://sei-mainnet.infura.io/v3/{infuraProjectId}" - } - ] - }, "0x89": { "blockExplorerUrls": [], "chainId": "0x89", From 47ab64a4d1359c26ad0dfafe7905caf49c0057a7 Mon Sep 17 00:00:00 2001 From: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:10:10 +0800 Subject: [PATCH 04/11] chore: add migration --- app/store/migrations/107.test.ts | 345 +++++++++++++++++++++++++++++++ app/store/migrations/107.ts | 108 ++++++++++ app/store/migrations/index.ts | 2 + 3 files changed, 455 insertions(+) create mode 100644 app/store/migrations/107.test.ts create mode 100644 app/store/migrations/107.ts diff --git a/app/store/migrations/107.test.ts b/app/store/migrations/107.test.ts new file mode 100644 index 000000000000..164d57d53444 --- /dev/null +++ b/app/store/migrations/107.test.ts @@ -0,0 +1,345 @@ +import { captureException } from '@sentry/react-native'; +import { cloneDeep } from 'lodash'; + +import { ensureValidState } from './util'; +import migrate from './107'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); + +jest.mock('./util', () => ({ + ensureValidState: jest.fn(), +})); + +const mockedCaptureException = jest.mocked(captureException); +const mockedEnsureValidState = jest.mocked(ensureValidState); + +const migrationVersion = 107; +const QUICKNODE_SEI_URL = 'https://failover.com'; + +describe(`migration #${migrationVersion}`, () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + + originalEnv = { ...process.env }; + }); + + afterEach(() => { + for (const key of new Set([ + ...Object.keys(originalEnv), + ...Object.keys(process.env), + ])) { + if (originalEnv[key]) { + process.env[key] = originalEnv[key]; + } else { + delete process.env[key]; + } + } + }); + + it('returns state unchanged if ensureValidState fails', () => { + const state = { some: 'state' }; + mockedEnsureValidState.mockReturnValue(false); + + const migratedState = migrate(state); + + expect(migratedState).toStrictEqual({ some: 'state' }); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); + + it.each([ + { + state: { + engine: {}, + }, + test: 'empty engine state', + }, + { + state: { + engine: { + backgroundState: {}, + }, + }, + test: 'empty backgroundState', + }, + { + state: { + engine: { + backgroundState: { + NetworkController: 'invalid', + }, + }, + }, + test: 'invalid NetworkController state', + }, + { + state: { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: 'invalid', + }, + }, + }, + }, + test: 'invalid networkConfigurationsByChainId state', + }, + { + state: { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + }, + }, + }, + }, + }, + }, + test: 'SEI network is not found', + }, + { + state: { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x531': { + chainId: '0x531', + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://seitrace.com'], + defaultBlockExplorerUrlIndex: 0, + name: 'Custom Sei Network', + nativeCurrency: 'SEI', + }, + }, + }, + }, + }, + }, + test: 'rpcEndpoints is not an array in SEI network configuration', + }, + ])('does not modify state if the state is invalid - $test', ({ state }) => { + const orgState = cloneDeep(state); + mockedEnsureValidState.mockReturnValue(true); + + const migratedState = migrate(state); + + // State should be unchanged + expect(migratedState).toStrictEqual(orgState); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); + + it('does not add failover URL if there is already a failover URL', async () => { + const oldState = { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + networksMetadata: {}, + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + }, + '0x531': { + chainId: '0x531', + rpcEndpoints: [ + { + networkClientId: 'sei-network', + url: 'https://sei-mainnet.infura.io/v3/{infuraProjectId}', + type: 'custom', + name: 'Sei Network', + failoverUrls: ['https://failover.com'], + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://seitrace.com'], + defaultBlockExplorerUrlIndex: 0, + name: 'Sei Network', + nativeCurrency: 'SEI', + }, + }, + }, + }, + }, + }; + + mockedEnsureValidState.mockReturnValue(true); + + const migratedState = await migrate(oldState); + expect(migratedState).toStrictEqual(oldState); + }); + + it('does not add failover URL if QUICKNODE_SEI_URL env variable is not set', async () => { + const oldState = { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + networksMetadata: {}, + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + }, + '0x531': { + chainId: '0x531', + rpcEndpoints: [ + { + networkClientId: 'sei-network', + url: 'https://sei-mainnet.infura.io/v3/{infuraProjectId}', + type: 'custom', + name: 'Sei Network', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://seitrace.com'], + defaultBlockExplorerUrlIndex: 0, + name: 'Sei Network', + nativeCurrency: 'SEI', + }, + }, + }, + }, + }, + }; + + mockedEnsureValidState.mockReturnValue(true); + + const migratedState = await migrate(oldState); + expect(migratedState).toStrictEqual(oldState); + }); + + it('adds QuickNode failover URL to all SEI RPC endpoints when no failover URLs exist', async () => { + process.env.QUICKNODE_SEI_URL = QUICKNODE_SEI_URL; + const oldState = { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + networksMetadata: {}, + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + }, + '0x531': { + chainId: '0x531', + rpcEndpoints: [ + { + networkClientId: 'sei-network', + url: 'https://sei-mainnet.infura.io/v3/{infuraProjectId}', + type: 'custom', + name: 'Sei Network', + }, + { + networkClientId: 'sei-network-2', + url: 'http://some-sei-rpc.com', + type: 'custom', + name: 'Sei Network', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://seitrace.com'], + defaultBlockExplorerUrlIndex: 0, + name: 'Sei Network', + nativeCurrency: 'SEI', + }, + }, + }, + }, + }, + }; + + mockedEnsureValidState.mockReturnValue(true); + + const expectedData = { + engine: { + backgroundState: { + NetworkController: { + ...oldState.engine.backgroundState.NetworkController, + networkConfigurationsByChainId: { + ...oldState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + '0x531': { + ...oldState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId['0x531'], + rpcEndpoints: [ + { + networkClientId: 'sei-network', + url: 'https://sei-mainnet.infura.io/v3/{infuraProjectId}', + type: 'custom', + name: 'Sei Network', + failoverUrls: [QUICKNODE_SEI_URL], + }, + { + networkClientId: 'sei-network-2', + url: 'http://some-sei-rpc.com', + type: 'custom', + name: 'Sei Network', + failoverUrls: [QUICKNODE_SEI_URL], + }, + ], + }, + }, + }, + }, + }, + }; + + const migratedState = await migrate(oldState); + expect(migratedState).toStrictEqual(expectedData); + }); +}); diff --git a/app/store/migrations/107.ts b/app/store/migrations/107.ts new file mode 100644 index 000000000000..809e0692d9bd --- /dev/null +++ b/app/store/migrations/107.ts @@ -0,0 +1,108 @@ +import { captureException } from '@sentry/react-native'; +import { hasProperty } from '@metamask/utils'; +import { isObject } from 'lodash'; + +import { ensureValidState } from './util'; + +const seiChainId = '0x531'; +const migrationVersion = 107; +/** + * Migration 107: Add failoverUrls to SEI network configuration + * + * This migration adds failoverUrls to the SEI network configuration + * to ensure that the app can connect to the SEI network even if the + * primary RPC endpoint is down. + */ +export default function migrate(state: unknown) { + try { + if (!ensureValidState(state, migrationVersion)) { + return state; + } + + // Validate if the NetworkController state exists and has the expected structure. + if ( + !( + hasProperty(state, 'engine') && + hasProperty(state.engine, 'backgroundState') && + hasProperty(state.engine.backgroundState, 'NetworkController') && + isObject(state.engine.backgroundState.NetworkController) && + hasProperty( + state.engine.backgroundState.NetworkController, + 'networkConfigurationsByChainId', + ) && + isObject( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + ) && + hasProperty( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + seiChainId, + ) && + isObject( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[seiChainId], + ) && + hasProperty( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[seiChainId], + 'rpcEndpoints', + ) && + Array.isArray( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[seiChainId].rpcEndpoints, + ) + ) + ) { + return state; + } + + // Update RPC endpoints to add failover URL if needed + state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[ + seiChainId + ].rpcEndpoints = + state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[ + seiChainId + ].rpcEndpoints.map((rpcEndpoint) => { + // Skip if endpoint is not an object or doesn't have a url property + if ( + !isObject(rpcEndpoint) || + !hasProperty(rpcEndpoint, 'url') || + typeof rpcEndpoint.url !== 'string' + ) { + return rpcEndpoint; + } + + // Skip if endpoint already has failover URLs + if ( + hasProperty(rpcEndpoint, 'failoverUrls') && + Array.isArray(rpcEndpoint.failoverUrls) && + rpcEndpoint.failoverUrls.length > 0 + ) { + return rpcEndpoint; + } + + // Add QuickNode failover URL + const quickNodeUrl = process.env.QUICKNODE_SEI_URL; + + if (quickNodeUrl) { + return { + ...rpcEndpoint, + failoverUrls: [quickNodeUrl], + }; + } + + return rpcEndpoint; + }); + + return state; + } catch (error) { + captureException( + new Error( + `Migration ${migrationVersion}: Failed to add failoverUrls to SEI network configuration: ${error}`, + ), + ); + } + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index e63084cb88c3..cf9c47d4633b 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -107,6 +107,7 @@ import migration103 from './103'; import migration104 from './104'; import migration105 from './105'; import migration106 from './106'; +import migration107 from './107'; // Add migrations above this line import { ControllerStorage } from '../persistConfig'; @@ -230,6 +231,7 @@ export const migrationList: MigrationsList = { 104: migration104, 105: migration105, 106: migration106, + 107: migration107, }; // Enable both synchronous and asynchronous migrations From dab4d2c8ee0016ca636403553ca81a37352060a9 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 11 Nov 2025 11:52:50 +0100 Subject: [PATCH 05/11] chore: test + lint --- .../AddressSelector/AddressSelector.test.tsx | 2 - .../AddressSelector.test.tsx.snap | 191 +----------------- 2 files changed, 5 insertions(+), 188 deletions(-) diff --git a/app/components/Views/AddressSelector/AddressSelector.test.tsx b/app/components/Views/AddressSelector/AddressSelector.test.tsx index b2ad4cc64e5c..6aff0617f4fb 100644 --- a/app/components/Views/AddressSelector/AddressSelector.test.tsx +++ b/app/components/Views/AddressSelector/AddressSelector.test.tsx @@ -19,7 +19,6 @@ import { MAINNET_DISPLAY_NAME, OPTIMISM_DISPLAY_NAME, POLYGON_DISPLAY_NAME, - SEI_DISPLAY_NAME, } from '../../../core/Engine/constants'; jest.mock('../../../core/Engine', () => ({ @@ -138,7 +137,6 @@ describe('AccountSelector', () => { expect(networkNames).toEqual([ MAINNET_DISPLAY_NAME, BNB_DISPLAY_NAME, - SEI_DISPLAY_NAME, POLYGON_DISPLAY_NAME, OPTIMISM_DISPLAY_NAME, ARBITRUM_DISPLAY_NAME, diff --git a/app/components/Views/AddressSelector/__snapshots__/AddressSelector.test.tsx.snap b/app/components/Views/AddressSelector/__snapshots__/AddressSelector.test.tsx.snap index 3fde75c8127a..e1c145701835 100644 --- a/app/components/Views/AddressSelector/__snapshots__/AddressSelector.test.tsx.snap +++ b/app/components/Views/AddressSelector/__snapshots__/AddressSelector.test.tsx.snap @@ -1075,187 +1075,6 @@ exports[`AccountSelector renders correctly and matches snapshot 1`] = ` "flexDirection": "row", } } - > - - - - - - - - Sei - - - 0x4FeC2...fdcB5 - - - - - - - - - - - - Date: Wed, 12 Nov 2025 20:47:21 +0100 Subject: [PATCH 06/11] chore: update snapshot --- .../logs/__snapshots__/index.test.ts.snap | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index 161ffede2979..e15333f863d7 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -306,21 +306,6 @@ exports[`logs :: generateStateLogs Sanitized SeedlessOnboardingController State }, ], }, - "0x531": { - "blockExplorerUrls": [], - "chainId": "0x531", - "defaultRpcEndpointIndex": 0, - "name": "Sei", - "nativeCurrency": "SEI", - "rpcEndpoints": [ - { - "failoverUrls": [], - "networkClientId": "sei-mainnet", - "type": "infura", - "url": "https://sei-mainnet.infura.io/v3/{infuraProjectId}", - }, - ], - }, "0x89": { "blockExplorerUrls": [], "chainId": "0x89", @@ -1065,21 +1050,6 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` }, ], }, - "0x531": { - "blockExplorerUrls": [], - "chainId": "0x531", - "defaultRpcEndpointIndex": 0, - "name": "Sei", - "nativeCurrency": "SEI", - "rpcEndpoints": [ - { - "failoverUrls": [], - "networkClientId": "sei-mainnet", - "type": "infura", - "url": "https://sei-mainnet.infura.io/v3/{infuraProjectId}", - }, - ], - }, "0x89": { "blockExplorerUrls": [], "chainId": "0x89", From d99561003225fd4a2510ad921631a5316f244d5f Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 12 Nov 2025 21:05:36 +0100 Subject: [PATCH 07/11] chore: fix test --- app/core/Engine/controllers/network-controller/utils.test.ts | 1 + app/util/test/initial-background-state.json | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/core/Engine/controllers/network-controller/utils.test.ts b/app/core/Engine/controllers/network-controller/utils.test.ts index 7f1aa43843c8..867216f79814 100644 --- a/app/core/Engine/controllers/network-controller/utils.test.ts +++ b/app/core/Engine/controllers/network-controller/utils.test.ts @@ -388,6 +388,7 @@ function setQuicknodeEnvironmentVariables() { process.env.QUICKNODE_POLYGON_URL = 'https://example.quicknode.com/polygon'; process.env.QUICKNODE_BASE_URL = 'https://example.quicknode.com/base'; process.env.QUICKNODE_BSC_URL = 'https://example.quicknode.com/bsc'; + process.env.QUICKNODE_SEI_URL = 'https://example.quicknode.com/sei'; } /** diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 50e5b3edc214..6124a8e1f456 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -250,7 +250,6 @@ "0x2105": true, "0x279f": false, "0x38": true, - "0x531": true, "0x89": true, "0xa": true, "0xa4b1": true, @@ -319,7 +318,6 @@ "0x507": true, "0x505": true, "0x64": true, - "0x531": true, "0x8f": true }, "isIpfsGatewayEnabled": true, From 0475a5efe6bf477a9e03129dfef377d90026fa0d Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 12 Nov 2025 21:23:16 +0100 Subject: [PATCH 08/11] chore: fix test --- app/util/logs/__snapshots__/index.test.ts.snap | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index e15333f863d7..29eda6a4d2c2 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -420,7 +420,6 @@ exports[`logs :: generateStateLogs Sanitized SeedlessOnboardingController State "0x2105": true, "0x279f": false, "0x38": true, - "0x531": true, "0x89": true, "0xa": true, "0xa4b1": true, @@ -515,7 +514,6 @@ exports[`logs :: generateStateLogs Sanitized SeedlessOnboardingController State "0x504": true, "0x505": true, "0x507": true, - "0x531": true, "0x61": true, "0x64": true, "0x89": true, @@ -1164,7 +1162,6 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "0x2105": true, "0x279f": false, "0x38": true, - "0x531": true, "0x89": true, "0xa": true, "0xa4b1": true, @@ -1259,7 +1256,6 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "0x504": true, "0x505": true, "0x507": true, - "0x531": true, "0x61": true, "0x64": true, "0x89": true, From 76894df62f126bd9c07a60bd21cc0cfd0bf50988 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 13 Nov 2025 09:59:06 +0100 Subject: [PATCH 09/11] chore: update initial-background-state --- app/util/test/initial-background-state.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 6124a8e1f456..dca677791fbb 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -250,6 +250,7 @@ "0x2105": true, "0x279f": false, "0x38": true, + "0x531": true, "0x89": true, "0xa": true, "0xa4b1": true, @@ -316,9 +317,9 @@ "0xe708": true, "0x504": true, "0x507": true, + "0x531": true, "0x505": true, - "0x64": true, - "0x8f": true + "0x64": true }, "isIpfsGatewayEnabled": true, "smartTransactionsOptInStatus": true, From e6fecc7b90af73aaa5c297e91fc5e3f8b79325d6 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 13 Nov 2025 10:20:54 +0100 Subject: [PATCH 10/11] chore: update snapshot --- app/util/logs/__snapshots__/index.test.ts.snap | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index 29eda6a4d2c2..e15333f863d7 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -420,6 +420,7 @@ exports[`logs :: generateStateLogs Sanitized SeedlessOnboardingController State "0x2105": true, "0x279f": false, "0x38": true, + "0x531": true, "0x89": true, "0xa": true, "0xa4b1": true, @@ -514,6 +515,7 @@ exports[`logs :: generateStateLogs Sanitized SeedlessOnboardingController State "0x504": true, "0x505": true, "0x507": true, + "0x531": true, "0x61": true, "0x64": true, "0x89": true, @@ -1162,6 +1164,7 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "0x2105": true, "0x279f": false, "0x38": true, + "0x531": true, "0x89": true, "0xa": true, "0xa4b1": true, @@ -1256,6 +1259,7 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "0x504": true, "0x505": true, "0x507": true, + "0x531": true, "0x61": true, "0x64": true, "0x89": true, From 124b5e745daeee61c885604d944fea06d34d271f Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 13 Nov 2025 13:51:41 +0100 Subject: [PATCH 11/11] chore: handle comments --- app/store/migrations/107.test.ts | 110 ++++++++++++++--- app/store/migrations/107.ts | 129 +++++++++++++++----- app/util/test/initial-background-state.json | 4 +- 3 files changed, 190 insertions(+), 53 deletions(-) diff --git a/app/store/migrations/107.test.ts b/app/store/migrations/107.test.ts index 164d57d53444..c7ac15f880d2 100644 --- a/app/store/migrations/107.test.ts +++ b/app/store/migrations/107.test.ts @@ -51,12 +51,13 @@ describe(`migration #${migrationVersion}`, () => { expect(mockedCaptureException).not.toHaveBeenCalled(); }); - it.each([ + const invalidStates = [ { state: { engine: {}, }, - test: 'empty engine state', + errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`, + scenario: 'empty engine state', }, { state: { @@ -64,7 +65,8 @@ describe(`migration #${migrationVersion}`, () => { backgroundState: {}, }, }, - test: 'empty backgroundState', + errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`, + scenario: 'empty backgroundState', }, { state: { @@ -74,7 +76,19 @@ describe(`migration #${migrationVersion}`, () => { }, }, }, - test: 'invalid NetworkController state', + errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state: 'string'`, + scenario: 'invalid NetworkController state', + }, + { + state: { + engine: { + backgroundState: { + NetworkController: {}, + }, + }, + }, + errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state: missing networkConfigurationsByChainId property`, + scenario: 'missing networkConfigurationsByChainId property', }, { state: { @@ -86,7 +100,8 @@ describe(`migration #${migrationVersion}`, () => { }, }, }, - test: 'invalid networkConfigurationsByChainId state', + errorMessage: `Migration ${migrationVersion}: Invalid NetworkController networkConfigurationsByChainId: 'string'`, + scenario: 'invalid networkConfigurationsByChainId state', }, { state: { @@ -94,27 +109,36 @@ describe(`migration #${migrationVersion}`, () => { backgroundState: { NetworkController: { networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - rpcEndpoints: [ - { - networkClientId: 'mainnet', - url: 'https://mainnet.infura.io/v3/{infuraProjectId}', - type: 'infura', - }, - ], + '0x531': 'invalid', + }, + }, + }, + }, + }, + errorMessage: `Migration ${migrationVersion}: Invalid SEI network configuration: 'string'`, + scenario: 'invalid SEI network configuration', + }, + { + state: { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x531': { + chainId: '0x531', defaultRpcEndpointIndex: 0, - blockExplorerUrls: ['https://etherscan.io'], + blockExplorerUrls: ['https://seitrace.com'], defaultBlockExplorerUrlIndex: 0, - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', + name: 'Custom Sei Network', + nativeCurrency: 'SEI', }, }, }, }, }, }, - test: 'SEI network is not found', + errorMessage: `Migration ${migrationVersion}: Invalid SEI network configuration: missing rpcEndpoints property`, + scenario: 'missing rpcEndpoints property in SEI network configuration', }, { state: { @@ -124,6 +148,7 @@ describe(`migration #${migrationVersion}`, () => { networkConfigurationsByChainId: { '0x531': { chainId: '0x531', + rpcEndpoints: 'not-an-array', defaultRpcEndpointIndex: 0, blockExplorerUrls: ['https://seitrace.com'], defaultBlockExplorerUrlIndex: 0, @@ -135,9 +160,54 @@ describe(`migration #${migrationVersion}`, () => { }, }, }, - test: 'rpcEndpoints is not an array in SEI network configuration', + errorMessage: `Migration ${migrationVersion}: Invalid SEI network rpcEndpoints: expected array, got 'string'`, + scenario: 'rpcEndpoints is not an array in SEI network configuration', + }, + ]; + + it.each(invalidStates)( + 'should capture exception if $scenario', + ({ errorMessage, state }) => { + const orgState = cloneDeep(state); + mockedEnsureValidState.mockReturnValue(true); + + const migratedState = migrate(state); + + // State should be unchanged + expect(migratedState).toStrictEqual(orgState); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); }, - ])('does not modify state if the state is invalid - $test', ({ state }) => { + ); + + it('does not modify state and does not capture exception if SEI network is not found', () => { + const state = { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + }, + ], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + }, + }, + }, + }, + }, + }; const orgState = cloneDeep(state); mockedEnsureValidState.mockReturnValue(true); diff --git a/app/store/migrations/107.ts b/app/store/migrations/107.ts index 809e0692d9bd..66af8f935882 100644 --- a/app/store/migrations/107.ts +++ b/app/store/migrations/107.ts @@ -21,39 +21,106 @@ export default function migrate(state: unknown) { // Validate if the NetworkController state exists and has the expected structure. if ( - !( - hasProperty(state, 'engine') && - hasProperty(state.engine, 'backgroundState') && - hasProperty(state.engine.backgroundState, 'NetworkController') && - isObject(state.engine.backgroundState.NetworkController) && - hasProperty( - state.engine.backgroundState.NetworkController, - 'networkConfigurationsByChainId', - ) && - isObject( - state.engine.backgroundState.NetworkController - .networkConfigurationsByChainId, - ) && - hasProperty( - state.engine.backgroundState.NetworkController - .networkConfigurationsByChainId, - seiChainId, - ) && - isObject( - state.engine.backgroundState.NetworkController - .networkConfigurationsByChainId[seiChainId], - ) && - hasProperty( - state.engine.backgroundState.NetworkController - .networkConfigurationsByChainId[seiChainId], - 'rpcEndpoints', - ) && - Array.isArray( - state.engine.backgroundState.NetworkController - .networkConfigurationsByChainId[seiChainId].rpcEndpoints, - ) + !hasProperty(state, 'engine') || + !hasProperty(state.engine, 'backgroundState') || + !hasProperty(state.engine.backgroundState, 'NetworkController') + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`, + ), + ); + return state; + } + + if (!isObject(state.engine.backgroundState.NetworkController)) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid NetworkController state: '${typeof state.engine.backgroundState.NetworkController}'`, + ), + ); + return state; + } + + if ( + !hasProperty( + state.engine.backgroundState.NetworkController, + 'networkConfigurationsByChainId', + ) + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid NetworkController state: missing networkConfigurationsByChainId property`, + ), + ); + return state; + } + + if ( + !isObject( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + ) + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid NetworkController networkConfigurationsByChainId: '${typeof state.engine.backgroundState.NetworkController.networkConfigurationsByChainId}'`, + ), + ); + return state; + } + + if ( + !hasProperty( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId, + seiChainId, + ) + ) { + // SEI network not configured, no migration needed + return state; + } + + if ( + !isObject( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[seiChainId], + ) + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid SEI network configuration: '${typeof state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[seiChainId]}'`, + ), + ); + return state; + } + + if ( + !hasProperty( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[seiChainId], + 'rpcEndpoints', + ) + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid SEI network configuration: missing rpcEndpoints property`, + ), + ); + return state; + } + + if ( + !Array.isArray( + state.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[seiChainId].rpcEndpoints, ) ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid SEI network rpcEndpoints: expected array, got '${typeof state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[seiChainId].rpcEndpoints}'`, + ), + ); return state; } diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index dca677791fbb..3b8786c70bae 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -317,9 +317,9 @@ "0xe708": true, "0x504": true, "0x507": true, - "0x531": true, "0x505": true, - "0x64": true + "0x64": true, + "0x531": true }, "isIpfsGatewayEnabled": true, "smartTransactionsOptInStatus": true,