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
-
-
-
-
-
-
-
-
-
-
-
-
({
+ 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();
+ });
+
+ const invalidStates = [
+ {
+ state: {
+ engine: {},
+ },
+ errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`,
+ scenario: 'empty engine state',
+ },
+ {
+ state: {
+ engine: {
+ backgroundState: {},
+ },
+ },
+ errorMessage: `Migration ${migrationVersion}: Invalid NetworkController state structure: missing required properties`,
+ scenario: 'empty backgroundState',
+ },
+ {
+ state: {
+ engine: {
+ backgroundState: {
+ NetworkController: 'invalid',
+ },
+ },
+ },
+ 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: {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ networkConfigurationsByChainId: 'invalid',
+ },
+ },
+ },
+ },
+ errorMessage: `Migration ${migrationVersion}: Invalid NetworkController networkConfigurationsByChainId: 'string'`,
+ scenario: 'invalid networkConfigurationsByChainId state',
+ },
+ {
+ state: {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ networkConfigurationsByChainId: {
+ '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://seitrace.com'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Custom Sei Network',
+ nativeCurrency: 'SEI',
+ },
+ },
+ },
+ },
+ },
+ },
+ errorMessage: `Migration ${migrationVersion}: Invalid SEI network configuration: missing rpcEndpoints property`,
+ scenario: 'missing rpcEndpoints property in SEI network configuration',
+ },
+ {
+ state: {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ networkConfigurationsByChainId: {
+ '0x531': {
+ chainId: '0x531',
+ rpcEndpoints: 'not-an-array',
+ defaultRpcEndpointIndex: 0,
+ blockExplorerUrls: ['https://seitrace.com'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Custom Sei Network',
+ nativeCurrency: 'SEI',
+ },
+ },
+ },
+ },
+ },
+ },
+ 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,
+ );
+ },
+ );
+
+ 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);
+
+ 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..66af8f935882
--- /dev/null
+++ b/app/store/migrations/107.ts
@@ -0,0 +1,175 @@
+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')
+ ) {
+ 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;
+ }
+
+ // 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
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",
diff --git a/app/util/networks/customNetworks.tsx b/app/util/networks/customNetworks.tsx
index c7683d2dc3fa..c402631b7f7d 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(
@@ -152,7 +153,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: {
diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json
index 7fced8f964d7..3b8786c70bae 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",
@@ -334,8 +319,7 @@
"0x507": true,
"0x505": true,
"0x64": true,
- "0x531": true,
- "0x8f": true
+ "0x531": true
},
"isIpfsGatewayEnabled": true,
"smartTransactionsOptInStatus": true,