diff --git a/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap
index a45b7d0da40..fd3bd4b0038 100644
--- a/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap
@@ -903,6 +903,71 @@ exports[`NetworksSettings should render correctly 1`] = `
+
+
+
+
+
+ Monad Testnet
+
+
+
+
+
+
+
({
+ captureException: jest.fn(),
+}));
+
+jest.mock('./util', () => ({
+ ensureValidState: jest.fn(),
+}));
+
+const mockedCaptureException = jest.mocked(captureException);
+const mockedEnsureValidState = jest.mocked(ensureValidState);
+
+const createTestState = () => ({
+ 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',
+ },
+ '0xaa36a7': {
+ chainId: '0xaa36a7',
+ rpcEndpoints: [
+ {
+ networkClientId: 'sepolia',
+ url: 'https://sepolia.infura.io/v3/{infuraProjectId}',
+ type: 'infura',
+ },
+ ],
+ defaultRpcEndpointIndex: 0,
+ blockExplorerUrls: ['https://sepolia.etherscan.io'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Sepolia',
+ nativeCurrency: 'SepoliaETH',
+ },
+ '0xe705': {
+ chainId: '0xe705',
+ rpcEndpoints: [
+ {
+ networkClientId: 'linea-sepolia',
+ url: 'https://linea-sepolia.infura.io/v3/{infuraProjectId}',
+ type: 'infura',
+ },
+ ],
+ defaultRpcEndpointIndex: 0,
+ blockExplorerUrls: ['https://sepolia.lineascan.build'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Linea Sepolia',
+ nativeCurrency: 'LineaETH',
+ },
+ '0xe708': {
+ chainId: '0xe708',
+ rpcEndpoints: [
+ {
+ networkClientId: 'linea-mainnet',
+ url: 'https://linea-mainnet.infura.io/v3/{infuraProjectId}',
+ type: 'infura',
+ },
+ ],
+ defaultRpcEndpointIndex: 0,
+ blockExplorerUrls: ['https://lineascan.build'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Linea Mainnet',
+ nativeCurrency: 'ETH',
+ },
+ '0x18c6': {
+ chainId: '0x18c6',
+ rpcEndpoints: [
+ {
+ networkClientId: 'megaeth-testnet',
+ url: 'https://carrot.megaeth.com/rpc',
+ type: RpcEndpointType.Custom,
+ failoverUrls: [],
+ },
+ ],
+ defaultRpcEndpointIndex: 0,
+ blockExplorerUrls: ['https://megaexplorer.xyz'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Mega Testnet',
+ nativeCurrency: 'MegaETH',
+ },
+ },
+ },
+ }
+ }
+});
+
+const createMonadTestnetConfiguration = (): NetworkConfiguration => ({
+ chainId: '0x279f',
+ rpcEndpoints: [
+ {
+ networkClientId: 'monad-testnet',
+ url: 'https://testnet-rpc.monad.xyz',
+ type: RpcEndpointType.Custom,
+ failoverUrls: [],
+ },
+ ],
+ defaultRpcEndpointIndex: 0,
+ blockExplorerUrls: ['https://testnet.monadexplorer.com'],
+ defaultBlockExplorerUrlIndex: 0,
+ name: 'Monad Testnet',
+ nativeCurrency: 'MON',
+});
+
+describe('Migration 77: Add `Monad Testnet`', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ 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('adds `Monad Testnet` as default network to state', () => {
+ const monadTestnetConfiguration = createMonadTestnetConfiguration();
+ const oldState = createTestState();
+ mockedEnsureValidState.mockReturnValue(true);
+
+ const expectedData = {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ ...oldState.engine.backgroundState.NetworkController,
+ networkConfigurationsByChainId: {
+ ...oldState.engine.backgroundState.NetworkController.networkConfigurationsByChainId,
+ [monadTestnetConfiguration.chainId]: monadTestnetConfiguration
+ },
+ },
+ }
+ }
+ };
+
+ const migratedState = migrate(oldState);
+
+ expect(migratedState).toStrictEqual(expectedData);
+ expect(mockedCaptureException).not.toHaveBeenCalled();
+ });
+
+ it('replaces `Monad Testnet` NetworkConfiguration if there is one', () => {
+ const monadTestnetConfiguration = createMonadTestnetConfiguration();
+ const oldState = createTestState();
+ const networkConfigurationsByChainId = oldState.engine.backgroundState.NetworkController.networkConfigurationsByChainId as Record;
+ networkConfigurationsByChainId[monadTestnetConfiguration.chainId] = {
+ ...monadTestnetConfiguration,
+ rpcEndpoints: [
+ {
+ networkClientId: 'some-client-id',
+ url: 'https://some-url.com/rpc',
+ type: RpcEndpointType.Custom,
+ },
+ ],
+ };
+ mockedEnsureValidState.mockReturnValue(true);
+
+ const expectedData = {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ ...oldState.engine.backgroundState.NetworkController,
+ networkConfigurationsByChainId: {
+ ...oldState.engine.backgroundState.NetworkController.networkConfigurationsByChainId,
+ [monadTestnetConfiguration.chainId]: monadTestnetConfiguration
+ },
+ },
+ }
+ }
+ };
+
+ const migratedState = migrate(oldState);
+
+ expect(migratedState).toStrictEqual(expectedData);
+ 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'
+ }
+ ])('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).toHaveBeenCalledWith(
+ new Error('Migration 77: NetworkController or networkConfigurationsByChainId not found in state'),
+ );
+ });
+});
diff --git a/app/store/migrations/077.ts b/app/store/migrations/077.ts
new file mode 100644
index 00000000000..c0acb765d6b
--- /dev/null
+++ b/app/store/migrations/077.ts
@@ -0,0 +1,84 @@
+import { captureException } from '@sentry/react-native';
+import { hasProperty, isObject } from '@metamask/utils';
+import { type NetworkConfiguration, RpcEndpointType } from '@metamask/network-controller';
+import {
+ ChainId,
+ BuiltInNetworkName,
+ NetworkNickname,
+ BUILT_IN_CUSTOM_NETWORKS_RPC,
+ NetworksTicker,
+ BlockExplorerUrl
+} from '@metamask/controller-utils';
+
+import { ensureValidState } from './util';
+
+/**
+ * Migration 77: Add 'Monad Testnet'
+ *
+ * This migration add Monad Testnet to the network controller
+ * as a default Testnet.
+ */
+const migration = (state: unknown): unknown => {
+ const migrationVersion = 77;
+
+ // Ensure the state is valid for migration
+ if (!ensureValidState(state, migrationVersion)) {
+ return state;
+ }
+
+ try {
+ if (
+ hasProperty(state, 'engine') &&
+ hasProperty(state.engine, 'backgroundState') &&
+ hasProperty(state.engine.backgroundState, 'NetworkController') &&
+ isObject(state.engine.backgroundState.NetworkController) &&
+ isObject(
+ state.engine.backgroundState.NetworkController
+ .networkConfigurationsByChainId,
+ )
+ ) {
+ // It is possible to get the Monad Network configuration by `getDefaultNetworkConfigurationsByChainId()`,
+ // But we choose to re-define it here to prevent the need to change this file,
+ // when `getDefaultNetworkConfigurationsByChainId()` has some breaking changes in the future.
+ const networkClientId = BuiltInNetworkName.MonadTestnet;
+ const chainId = ChainId[networkClientId];
+ const monadTestnetConfiguration: NetworkConfiguration = {
+ blockExplorerUrls: [BlockExplorerUrl[networkClientId]],
+ chainId,
+ defaultRpcEndpointIndex: 0,
+ defaultBlockExplorerUrlIndex: 0,
+ name: NetworkNickname[networkClientId],
+ nativeCurrency: NetworksTicker[networkClientId],
+ rpcEndpoints: [
+ {
+ failoverUrls: [],
+ networkClientId,
+ type: RpcEndpointType.Custom,
+ url: BUILT_IN_CUSTOM_NETWORKS_RPC['monad-testnet'],
+ },
+ ],
+ };
+
+ // Regardless if the network already exists, we will overwrite it with the default configuration.
+ state.engine.backgroundState.NetworkController.networkConfigurationsByChainId[
+ chainId
+ ] = monadTestnetConfiguration;
+ } else {
+ captureException(
+ new Error(
+ `Migration ${migrationVersion}: NetworkController or networkConfigurationsByChainId not found in state`,
+ ),
+ );
+ }
+ return state;
+ } catch (error) {
+ captureException(
+ new Error(
+ `Migration ${migrationVersion}: Adding Monad Testnet failed with error: ${error}`,
+ ),
+ );
+ return state;
+ }
+};
+
+export default migration;
diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts
index 41a997f82d2..ab362f953b1 100644
--- a/app/store/migrations/index.ts
+++ b/app/store/migrations/index.ts
@@ -76,6 +76,7 @@ import migration72 from './072';
import migration74 from './074';
import migration75 from './075';
import migration76 from './076';
+import migration77 from './077';
// Add migrations above this line
import { validatePostMigrationState } from '../validateMigration/validateMigration';
import { RootState } from '../../reducers';
@@ -167,6 +168,7 @@ export const migrationList: MigrationsList = {
74: migration74,
75: migration75,
76: migration76,
+ 77: migration77,
};
// 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 b85e849d4e8..5350772fb68 100644
--- a/app/util/logs/__snapshots__/index.test.ts.snap
+++ b/app/util/logs/__snapshots__/index.test.ts.snap
@@ -224,6 +224,24 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = `
},
],
},
+ "0x279f": {
+ "blockExplorerUrls": [
+ "https://testnet.monadexplorer.com",
+ ],
+ "chainId": "0x279f",
+ "defaultBlockExplorerUrlIndex": 0,
+ "defaultRpcEndpointIndex": 0,
+ "name": "Monad Testnet",
+ "nativeCurrency": "MON",
+ "rpcEndpoints": [
+ {
+ "failoverUrls": [],
+ "networkClientId": "monad-testnet",
+ "type": "custom",
+ "url": "https://testnet-rpc.monad.xyz",
+ },
+ ],
+ },
"0xaa36a7": {
"blockExplorerUrls": [],
"chainId": "0xaa36a7",
diff --git a/app/util/networks/index.js b/app/util/networks/index.js
index 743c4b6f44f..84e6456e38d 100644
--- a/app/util/networks/index.js
+++ b/app/util/networks/index.js
@@ -9,6 +9,7 @@ import {
LINEA_MAINNET,
LINEA_SEPOLIA,
MEGAETH_TESTNET,
+ MONAD_TESTNET,
} from '../../../app/constants/network';
import { NetworkSwitchErrorType } from '../../../app/constants/error';
import {
@@ -28,6 +29,7 @@ const sepoliaLogo = require('../../images/sepolia-logo-dark.png');
const lineaTestnetLogo = require('../../images/linea-testnet-logo.png');
const lineaMainnetLogo = require('../../images/linea-mainnet-logo.png');
const megaEthTestnetLogo = require('../../images/megaeth-testnet-logo.png');
+const monadTestnetLogo = require('../../images/monad-testnet-logo.png');
/* eslint-enable */
import {
@@ -134,6 +136,19 @@ export const NetworkList = {
imageSource: megaEthTestnetLogo,
blockExplorerUrl: BlockExplorerUrl['megaeth-testnet'],
},
+ [MONAD_TESTNET]: {
+ name: 'Monad Testnet',
+ shortName: 'Monad Testnet',
+ networkId: 10143,
+ chainId: toHex('10143'),
+ ticker: 'MON',
+ // Third party color
+ // eslint-disable-next-line @metamask/design-tokens/color-no-hex
+ color: '#61dfff',
+ networkType: 'monad-testnet',
+ imageSource: monadTestnetLogo,
+ blockExplorerUrl: BlockExplorerUrl['monad-testnet'],
+ },
[RPC]: {
name: 'Private Network',
shortName: 'Private',
@@ -246,6 +261,9 @@ export const getTestNetImageByChainId = (chainId) => {
if (NETWORKS_CHAIN_ID.MEGAETH_TESTNET === chainId) {
return networksWithImages?.['MEGAETH-TESTNET'];
}
+ if (NETWORKS_CHAIN_ID.MONAD_TESTNET === chainId) {
+ return networksWithImages?.['MONAD-TESTNET'];
+ }
};
/**
@@ -257,6 +275,7 @@ export const TESTNET_CHAIN_IDS = [
ChainId[NetworkType['linea-goerli']],
ChainId[NetworkType['linea-sepolia']],
ChainId[NetworkType['megaeth-testnet']],
+ ChainId[NetworkType['monad-testnet']],
];
/**
@@ -607,6 +626,7 @@ export const WHILELIST_NETWORK_NAME = {
[ChainId.mainnet]: 'Mainnet',
[ChainId['linea-mainnet']]: 'Linea Mainnet',
[ChainId['megaeth-testnet']]: 'Mega Testnet',
+ [ChainId['monad-testnet']]: 'Monad Testnet',
};
/**
diff --git a/app/util/networks/index.test.ts b/app/util/networks/index.test.ts
index 98da9020e4c..24465503d9a 100644
--- a/app/util/networks/index.test.ts
+++ b/app/util/networks/index.test.ts
@@ -29,7 +29,8 @@ import {
LINEA_GOERLI,
LINEA_MAINNET,
LINEA_SEPOLIA,
- MEGAETH_TESTNET
+ MEGAETH_TESTNET,
+ MONAD_TESTNET,
} from '../../../app/constants/network';
import { NetworkSwitchErrorType } from '../../../app/constants/error';
import Engine from './../../core/Engine';
@@ -117,7 +118,12 @@ describe('network-utils', () => {
const allNetworks = getAllNetworks();
it('should get all networks', () => {
expect(allNetworks).toStrictEqual([
- MAINNET,LINEA_MAINNET, SEPOLIA, LINEA_SEPOLIA, MEGAETH_TESTNET
+ MAINNET,
+ LINEA_MAINNET,
+ SEPOLIA,
+ LINEA_SEPOLIA,
+ MEGAETH_TESTNET,
+ MONAD_TESTNET,
]);
});
@@ -763,6 +769,10 @@ describe('network-utils', () => {
{
chainId: ChainId['megaeth-testnet'],
expectedImage: networksWithImages?.['MEGAETH-TESTNET'],
+ },
+ {
+ chainId: ChainId['monad-testnet'],
+ expectedImage: networksWithImages?.['MONAD-TESTNET'],
}
]
)('returns corresponding image for the testnet - $.chainId', ({
@@ -794,6 +804,11 @@ describe('network-utils', () => {
name: 'MegaETH Testnet',
nickname: WHILELIST_NETWORK_NAME[ChainId['megaeth-testnet']],
},
+ {
+ chainId: ChainId['monad-testnet'],
+ name: 'Monad Testnet',
+ nickname: WHILELIST_NETWORK_NAME[ChainId['monad-testnet']],
+ },
])('returns true if the chainId is %.chainId and network nickname is the same with the whilelisted name', ({
chainId, name, nickname
}) =>{
diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json
index 5a5d3bb4c20..b3a57f14e2c 100644
--- a/app/util/test/initial-background-state.json
+++ b/app/util/test/initial-background-state.json
@@ -90,6 +90,24 @@
}
]
},
+ "0x279f": {
+ "blockExplorerUrls": [
+ "https://testnet.monadexplorer.com"
+ ],
+ "chainId": "0x279f",
+ "defaultBlockExplorerUrlIndex": 0,
+ "defaultRpcEndpointIndex": 0,
+ "name": "Monad Testnet",
+ "nativeCurrency": "MON",
+ "rpcEndpoints": [
+ {
+ "failoverUrls": [],
+ "networkClientId": "monad-testnet",
+ "type": "custom",
+ "url": "https://testnet-rpc.monad.xyz"
+ }
+ ]
+ },
"0xaa36a7": {
"blockExplorerUrls": [],
"chainId": "0xaa36a7",
diff --git a/package.json b/package.json
index 632992760c2..9fa04ba38f6 100644
--- a/package.json
+++ b/package.json
@@ -170,7 +170,7 @@
"@metamask/bridge-status-controller": "^18.0.0",
"@metamask/chain-agnostic-permission": "^0.3.0",
"@metamask/composable-controller": "^11.0.0",
- "@metamask/controller-utils": "^11.7.0",
+ "@metamask/controller-utils": "^11.8.0",
"@metamask/design-tokens": "^7.0.0",
"@metamask/earn-controller": "^0.13.0",
"@metamask/eip1193-permission-middleware": "^0.1.0",
@@ -198,7 +198,7 @@
"@metamask/metamask-eth-abis": "3.1.1",
"@metamask/multichain-network-controller": "^0.4.0",
"@metamask/multichain-transactions-controller": "^0.10.0",
- "@metamask/network-controller": "^23.2.0",
+ "@metamask/network-controller": "^23.4.0",
"@metamask/notification-services-controller": "^7.0.0",
"@metamask/permission-controller": "^11.0.6",
"@metamask/phishing-controller": "^12.4.1",
diff --git a/yarn.lock b/yarn.lock
index 0ab68b84bc9..1dead2a69d9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4866,10 +4866,10 @@
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-2.5.0.tgz#33921fa9c15eb1863f55dcd5f75467ae15614ebb"
integrity sha512-+j7jEcp0P1OUMEpa/OIwfJs/ahBC/akwgWxaRTSWX2SWABvlUKBVRMtslfL94Qj2wN2xw8xjaUy5nSHqrznqDA==
-"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.3.0", "@metamask/controller-utils@^11.5.0", "@metamask/controller-utils@^11.6.0", "@metamask/controller-utils@^11.7.0":
- version "11.7.0"
- resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.7.0.tgz#1186daecffff9dec6846f64f9da7a319dd9c9a83"
- integrity sha512-Q2TPmTJT6L/ixBk5TEb+mJ1NRyFAGe+VjFuulNQMVu6AanEBeGSoxsuBnAzWQlIgbt3/EHGP7o31ep1H5gr5Gw==
+"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.3.0", "@metamask/controller-utils@^11.5.0", "@metamask/controller-utils@^11.6.0", "@metamask/controller-utils@^11.7.0", "@metamask/controller-utils@^11.8.0":
+ version "11.8.0"
+ resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.8.0.tgz#7d573db8a2ab0ce594f92753b0bda02d18330142"
+ integrity sha512-FqApJXW0mnHWwnKC4HOQwf2P9fPxfiQmlLJdGWwK6hAYKQ+t7ADDw9Wf8RwVquFPzJGQbXnOcbXVBY2zAyoU5w==
dependencies:
"@ethereumjs/util" "^9.1.0"
"@metamask/eth-query" "^4.0.0"
@@ -5381,13 +5381,13 @@
uri-js "^4.4.1"
uuid "^8.3.2"
-"@metamask/network-controller@^23.1.0", "@metamask/network-controller@^23.2.0":
- version "23.2.0"
- resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-23.2.0.tgz#02b0641bcf4e8ca35005f945bc4943a3baf949e3"
- integrity sha512-DnsMUtBxDQm4tk+2+bMbEgbUEgWS2xGmN6JqA47AXGBjzpnUUKQolcyJuPX0jJA+qUSnMG9jQjuMJBf1RHSR5Q==
+"@metamask/network-controller@^23.1.0", "@metamask/network-controller@^23.4.0":
+ version "23.4.0"
+ resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-23.4.0.tgz#17236ad4c06b5d1e1bc982aef4a03db6fddfd559"
+ integrity sha512-RQCbpNqWAJKLH4TLGRbS0Oj/R01cNsa+B8UF47hTVuRJXJMH/4ArU6kzUBbVyHM/gULcKU8eIrf1iEVqdkrWuw==
dependencies:
- "@metamask/base-controller" "^8.0.0"
- "@metamask/controller-utils" "^11.7.0"
+ "@metamask/base-controller" "^8.0.1"
+ "@metamask/controller-utils" "^11.8.0"
"@metamask/eth-block-tracker" "^11.0.3"
"@metamask/eth-json-rpc-infura" "^10.1.1"
"@metamask/eth-json-rpc-middleware" "^16.0.1"