diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index b04c11548697..c81df4fabd16 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -1712,6 +1712,9 @@
"editGasTooLow": {
"message": "Unknown processing time"
},
+ "editNetworkLink": {
+ "message": "edit the original network"
+ },
"editNonceField": {
"message": "Edit nonce"
},
@@ -1881,6 +1884,12 @@
"etherscanViewOn": {
"message": "View on Etherscan"
},
+ "existingChainId": {
+ "message": "The information you have entered is associated with an existing chain ID."
+ },
+ "existingRpcUrl": {
+ "message": "This URL is associated with another chain ID."
+ },
"expandView": {
"message": "Expand view"
},
@@ -1935,6 +1944,9 @@
"message": "File import not working? Click here!",
"description": "Helps user import their account from a JSON file"
},
+ "findTheRightChainId": {
+ "message": "Find the right one on:"
+ },
"flaskWelcomeUninstall": {
"message": "you should uninstall this extension",
"description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded."
@@ -6158,6 +6170,9 @@
"message": "U2F",
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."
},
+ "unMatchedChain": {
+ "message": "According to our records, this URL does not match a known provider for this chain ID."
+ },
"unapproved": {
"message": "Unapproved"
},
@@ -6209,6 +6224,9 @@
"update": {
"message": "Update"
},
+ "updateOrEditNetworkInformations": {
+ "message": "Update your information or"
+ },
"updateRequest": {
"message": "Update request"
},
@@ -6436,6 +6454,9 @@
"whatsThis": {
"message": "What's this?"
},
+ "wrongChainId": {
+ "message": "This chain ID doesn’t match the network name."
+ },
"wrongNetworkName": {
"message": "According to our records, the network name may not correctly match this chain ID."
},
diff --git a/test/e2e/playwright/shared/pageObjects/network-controller-page.ts b/test/e2e/playwright/shared/pageObjects/network-controller-page.ts
index 86a83d0312c7..aed0e7377b16 100644
--- a/test/e2e/playwright/shared/pageObjects/network-controller-page.ts
+++ b/test/e2e/playwright/shared/pageObjects/network-controller-page.ts
@@ -9,8 +9,6 @@ export class NetworkController {
readonly addNetworkManuallyButton: Locator;
- readonly networkTickerInput: Locator;
-
readonly approveBtn: Locator;
readonly saveBtn: Locator;
@@ -21,6 +19,14 @@ export class NetworkController {
readonly networkSearch: Locator;
+ readonly networkName: Locator;
+
+ readonly networkRpc: Locator;
+
+ readonly networkChainId: Locator;
+
+ readonly networkTicker: Locator;
+
constructor(page: Page) {
this.page = page;
this.networkDisplay = this.page.getByTestId('network-display');
@@ -28,9 +34,6 @@ export class NetworkController {
this.addNetworkManuallyButton = this.page.getByTestId(
'add-network-manually',
);
- this.networkTickerInput = this.page.getByTestId(
- 'network-form-ticker-input',
- );
this.saveBtn = this.page.getByRole('button', { name: 'Save' });
this.approveBtn = this.page.getByTestId('confirmation-submit-button');
this.switchToNetworkBtn = this.page.locator('button', {
@@ -38,6 +41,10 @@ export class NetworkController {
});
this.gotItBtn = this.page.getByRole('button', { name: 'Got it' });
this.networkSearch = this.page.locator('input[type="search"]');
+ this.networkName = this.page.getByTestId('network-form-network-name');
+ this.networkRpc = this.page.getByTestId('network-form-rpc-url');
+ this.networkChainId = this.page.getByTestId('network-form-chain-id');
+ this.networkTicker = this.page.getByTestId('network-form-ticker-input');
}
async addCustomNetwork(options: {
@@ -50,11 +57,11 @@ export class NetworkController {
await this.addNetworkButton.click();
await this.addNetworkManuallyButton.click();
- const formField = await this.page.$$('.form-field__input');
- await formField[0].fill(options.name);
- await formField[1].fill(options.url);
- await formField[2].fill(options.chainID);
- await this.networkTickerInput.fill(options.symbol);
+ await this.networkName.waitFor();
+ await this.networkName.fill(options.name);
+ await this.networkRpc.fill(options.url);
+ await this.networkChainId.fill(options.chainID);
+ await this.networkTicker.fill(options.symbol);
await this.saveBtn.click();
await this.switchToNetworkBtn.click();
}
diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js
index fd133fa47e47..0baab9c94bc5 100644
--- a/test/e2e/tests/network/add-custom-network.spec.js
+++ b/test/e2e/tests/network/add-custom-network.spec.js
@@ -79,11 +79,9 @@ const selectors = {
saveButton: { text: 'Save', tag: 'button' },
updatedNetworkDropDown: { tag: 'span', text: 'Update Network' },
errorMessageInvalidUrl: {
- tag: 'h6',
text: 'URLs require the appropriate HTTP/HTTPS prefix.',
},
warningSymbol: {
- tag: 'h6',
text: 'URLs require the appropriate HTTP/HTTPS prefix.',
},
suggestedTicker: '[data-testid="network-form-ticker-suggestion"]',
@@ -802,6 +800,8 @@ describe('Custom network', function () {
);
await driver.fill(selectors.chainIdInputField, '1');
await driver.fill(selectors.tickerInputField, 'TST');
+ // fix flaky test
+ await driver.delay(regularDelayMs);
await driver.fill(selectors.explorerInputField, 'https://test.com');
const suggestedTicker = await driver.isElementPresent(
@@ -849,6 +849,9 @@ describe('Custom network', function () {
inputData.networkName,
);
await driver.fill(selectors.rpcUrlInputField, inputData.rpcUrl);
+
+ // fix flaky test
+ await driver.delay(regularDelayMs);
await driver.fill(selectors.chainIdInputField, inputData.chainId);
await driver.fill(selectors.tickerInputField, inputData.ticker);
@@ -945,7 +948,6 @@ async function failCandidateNetworkValidation(driver) {
const chainIdValidationMessageRawLocator = {
text: 'Could not fetch chain ID. Is your RPC URL correct?',
- tag: 'h6',
};
await driver.waitForSelector(chainIdValidationMessageRawLocator);
await driver.waitForSelector('[data-testid="network-form-ticker-warning"]');
@@ -1047,6 +1049,8 @@ async function candidateNetworkIsNotValidated(driver) {
await driver.fill('[data-testid="network-form-ticker-input"]', 'cTH');
await blockExplorerURLInputEl.fill('https://block-explorer.url');
+ // fix flaky test
+ await driver.delay(regularDelayMs);
const saveButtonRawLocator = {
text: 'Save',
tag: 'button',
diff --git a/test/e2e/tests/network/custom-rpc-history.spec.js b/test/e2e/tests/network/custom-rpc-history.spec.js
index 7df8746a2e62..b2a45a968f49 100644
--- a/test/e2e/tests/network/custom-rpc-history.spec.js
+++ b/test/e2e/tests/network/custom-rpc-history.spec.js
@@ -102,7 +102,6 @@ describe('Custom RPC history', function () {
await rpcUrlInput.sendKeys(duplicateRpcUrl);
await driver.findElement({
text: 'This URL is currently used by the mainnet network.',
- tag: 'h6',
});
},
);
@@ -144,7 +143,6 @@ describe('Custom RPC history', function () {
await chainIdInput.sendKeys(duplicateChainId);
await driver.findElement({
text: 'This Chain ID is currently used by the mainnet network.',
- tag: 'h6',
});
await rpcUrlInput.clear();
@@ -160,7 +158,6 @@ describe('Custom RPC history', function () {
await driver.findElement({
text: 'Could not fetch chain ID. Is your RPC URL correct?',
- tag: 'h6',
});
},
);
@@ -294,7 +291,9 @@ describe('Custom RPC history', function () {
value: customNetworkName,
});
// delete custom network in a modal
- await driver.clickElement('.networks-tab__network-form .btn-danger');
+ await driver.clickElement(
+ '.networks-tab__network-form-footer .btn-danger',
+ );
await driver.findVisibleElement(
'[data-testid="confirm-delete-network-modal"]',
);
diff --git a/test/e2e/tests/network/update-network.spec.ts b/test/e2e/tests/network/update-network.spec.ts
index 1c09b88a621d..4242dfe1533f 100644
--- a/test/e2e/tests/network/update-network.spec.ts
+++ b/test/e2e/tests/network/update-network.spec.ts
@@ -22,7 +22,6 @@ const selectors = {
saveButton: { text: 'Save', tag: 'button' },
updatedNetworkDropDown: { tag: 'span', text: 'Update Network' },
errorMessageInvalidUrl: {
- tag: 'h6',
text: 'URLs require the appropriate HTTP/HTTPS prefix.',
},
networkNameInputField: '[data-testid="network-form-network-name"]',
diff --git a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js
index 51a77c851a72..32b648eebaab 100644
--- a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js
+++ b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
+ Box,
IconName,
ModalFocus,
Popover,
@@ -36,7 +37,7 @@ export const NetworkListItemMenu = ({
flip
>
-
+
{onEditClick ? (
) : null}
{onDeleteClick ? (
@@ -66,7 +67,7 @@ export const NetworkListItemMenu = ({
{t('delete')}
) : null}
-
+
);
diff --git a/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap b/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap
index df225a439aed..ede77cf934c3 100644
--- a/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap
+++ b/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap
@@ -3,7 +3,7 @@
exports[`NetworkListItem renders properly 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx
new file mode 100644
index 000000000000..65b8e889e94b
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import AddRpcUrlModal from './add-rpc-url-modal';
+
+jest.mock('../../../../hooks/useI18nContext', () => ({
+ useI18nContext: jest.fn(),
+}));
+
+describe('AddRpcUrlModal', () => {
+ const useI18nContextMock = useI18nContext as jest.Mock;
+
+ beforeEach(() => {
+ useI18nContextMock.mockReturnValue((key: string) => key);
+ jest.clearAllMocks();
+ });
+
+ it('should render correctly', () => {
+ const { container } = render(
);
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should render the input field with the correct label', () => {
+ render(
);
+ const inputLabel = screen.getByLabelText('additionalRpcUrl');
+ expect(inputLabel).toBeInTheDocument();
+ });
+
+ it('should render the "Add URL" button with correct text', () => {
+ render(
);
+ const addButton = screen.getByRole('button', { name: 'addUrl' });
+ expect(addButton).toBeInTheDocument();
+ });
+
+ it('should call the appropriate function when "Add URL" button is clicked', () => {
+ const mockAddUrl = jest.fn();
+ render(
);
+ const addButton = screen.getByRole('button', { name: 'addUrl' });
+ userEvent.click(addButton);
+ expect(mockAddUrl).not.toHaveBeenCalled();
+ });
+});
diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx
index 8e4269928bc8..8a9ff53a74b5 100644
--- a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx
+++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx
@@ -33,6 +33,7 @@ const AddRpcUrlModal = () => {
marginTop={8}
marginLeft={'auto'}
marginRight={'auto'}
+ onClick={() => ({})}
>
{t('addUrl')}
diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js
index 6f511b6a8bcd..8c44bf368132 100644
--- a/ui/components/multichain/network-list-menu/network-list-menu.js
+++ b/ui/components/multichain/network-list-menu/network-list-menu.js
@@ -55,6 +55,7 @@ import {
IconName,
ModalContent,
ModalHeader,
+ AvatarNetworkSize,
} from '../../component-library';
import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
@@ -74,7 +75,7 @@ import PopularNetworkList from './popular-network-list/popular-network-list';
import NetworkListSearch from './network-list-search/network-list-search';
import AddRpcUrlModal from './add-rpc-url-modal/add-rpc-url-modal';
-const ACTION_MODES = {
+export const ACTION_MODES = {
// Displays the search box and network list
LIST: 'list',
// Displays the Add form
@@ -121,6 +122,13 @@ export const NetworkListMenu = ({ onClose }) => {
const [actionMode, setActionMode] = useState(
editedNetwork ? ACTION_MODES.EDIT : ACTION_MODES.LIST,
);
+ const [networkFormInformation, setNetworkFormInformation] = useState({
+ networkNameForm: '',
+ networkChainIdForm: '',
+ networkTickerForm: '',
+ });
+
+ const [prevActionMode, setPrevActionMode] = useState(null);
const networkToEdit = useMemo(() => {
const network = [...nonTestNetworks, ...testNetworks].find(
@@ -167,6 +175,7 @@ export const NetworkListMenu = ({ onClose }) => {
useEffect(() => {
setActionMode(ACTION_MODES.LIST);
+ setPrevActionMode(null);
if (currentlyOnTestNetwork) {
dispatch(setShowTestNetworks(currentlyOnTestNetwork));
}
@@ -268,6 +277,7 @@ export const NetworkListMenu = ({ onClose }) => {
}),
);
setActionMode(ACTION_MODES.EDIT);
+ setPrevActionMode(ACTION_MODES.LIST);
};
const getOnEdit = (network) => {
@@ -283,6 +293,9 @@ export const NetworkListMenu = ({ onClose }) => {
{
}
};
+ const goToRpcFormEdit = () => {
+ setActionMode(ACTION_MODES.ADD_RPC);
+ setPrevActionMode(ACTION_MODES.EDIT);
+ };
+ const goToRpcFormAdd = () => {
+ setActionMode(ACTION_MODES.ADD_RPC);
+ setPrevActionMode(ACTION_MODES.ADD);
+ };
+
const renderListNetworks = () => {
if (actionMode === ACTION_MODES.LIST) {
return (
@@ -376,6 +398,7 @@ export const NetworkListMenu = ({ onClose }) => {
marginLeft={4}
marginRight={4}
marginBottom={4}
+ marginTop={2}
backgroundColor={BackgroundColor.backgroundAlternative}
startAccessory={
{
/>
) : null}
-
- {t('enabledNetworks')}
-
- {searchResults.length === 0 && focusSearch ? (
+ {searchResults.length > 0 ? (
+
+
+ {t('enabledNetworks')}
+
+
+ ) : null}
+
+ {searchResults.length === 0 &&
+ searchAddNetworkResults.length === 0 &&
+ searchTestNetworkResults.length === 0 &&
+ focusSearch ? (
{
data-testid="add-popular-network-view"
/>
) : null}
-
- {t('showTestnetNetworks')}
-
-
+
+ {searchTestNetworkResults.length > 0 ? (
+
+
+ {t('showTestnetNetworks')}
+
+
+
+ ) : null}
+
{showTestNetworks || currentlyOnTestNetwork ? (
{generateMenuItems(searchTestNetworkResults)}
@@ -512,9 +549,10 @@ export const NetworkListMenu = ({ onClose }) => {
category: MetaMetricsEventCategory.Network,
});
setActionMode(ACTION_MODES.ADD);
+ setPrevActionMode(ACTION_MODES.LIST);
}}
>
- {t('addNetwork')}
+ {networkMenuRedesign ? t('addCustomNetwork') : t('addNetwork')}
>
@@ -525,6 +563,10 @@ export const NetworkListMenu = ({ onClose }) => {
isNewNetworkFlow
addNewNetwork
getOnEditCallback={getOnEdit}
+ onRpcUrlAdd={goToRpcFormAdd}
+ prevActionMode={prevActionMode}
+ networkFormInformation={networkFormInformation}
+ setNetworkFormInformation={setNetworkFormInformation}
/>
);
} else if (actionMode === ACTION_MODES.EDIT) {
@@ -533,7 +575,7 @@ export const NetworkListMenu = ({ onClose }) => {
isNewNetworkFlow
addNewNetwork={false}
networkToEdit={networkToEdit}
- onRpcUrlAdd={() => setActionMode(ACTION_MODES.ADD_RPC)}
+ onRpcUrlAdd={goToRpcFormEdit}
/>
);
} else if (actionMode === ACTION_MODES.ADD_RPC) {
@@ -546,8 +588,16 @@ export const NetworkListMenu = ({ onClose }) => {
let onBack;
if (actionMode === ACTION_MODES.EDIT || actionMode === ACTION_MODES.ADD) {
onBack = () => setActionMode(ACTION_MODES.LIST);
- } else if (actionMode === ACTION_MODES.ADD_RPC) {
+ } else if (
+ actionMode === ACTION_MODES.ADD_RPC &&
+ prevActionMode === ACTION_MODES.EDIT
+ ) {
onBack = () => setActionMode(ACTION_MODES.EDIT);
+ } else if (
+ actionMode === ACTION_MODES.ADD_RPC &&
+ prevActionMode === ACTION_MODES.ADD
+ ) {
+ onBack = () => setActionMode(ACTION_MODES.ADD);
}
// Modal title
@@ -556,8 +606,10 @@ export const NetworkListMenu = ({ onClose }) => {
title = t('networkMenuHeading');
} else if (actionMode === ACTION_MODES.ADD) {
title = t('addCustomNetwork');
+ } else if (actionMode === ACTION_MODES.ADD_RPC) {
+ title = t('addRpcUrl');
} else {
- title = editedNetwork.nickname;
+ title = editedNetwork?.nickname ?? '';
}
return (
@@ -575,7 +627,6 @@ export const NetworkListMenu = ({ onClose }) => {
diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js
index 678745defa25..544b0880048e 100644
--- a/ui/components/multichain/network-list-menu/network-list-menu.test.js
+++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js
@@ -14,7 +14,6 @@ import { NetworkListMenu } from '.';
const mockSetShowTestNetworks = jest.fn();
const mockSetProviderType = jest.fn();
const mockToggleNetworkMenu = jest.fn();
-const mockNetworkMenuRedesignToggle = jest.fn();
const mockSetNetworkClientIdForDomain = jest.fn();
const mockSetActiveNetwork = jest.fn();
@@ -27,11 +26,6 @@ jest.mock('../../../store/actions.ts', () => ({
mockSetNetworkClientIdForDomain(network, id),
}));
-jest.mock('../../../helpers/utils/feature-flags', () => ({
- ...jest.requireActual('../../../helpers/utils/feature-flags'),
- getLocalNetworkMenuRedesignFeatureFlag: () => mockNetworkMenuRedesignToggle,
-}));
-
const MOCK_ORIGIN = 'https://portfolio.metamask.io';
const render = ({
@@ -66,7 +60,7 @@ const render = ({
describe('NetworkListMenu', () => {
beforeEach(() => {
- mockNetworkMenuRedesignToggle.mockReturnValue(false);
+ process.env.ENABLE_NETWORK_UI_REDESIGN = 'false';
});
it('renders properly', () => {
@@ -185,12 +179,16 @@ describe('NetworkListMenu', () => {
describe('NetworkListMenu with ENABLE_NETWORK_UI_REDESIGN', () => {
// Set the environment variable before tests run
beforeEach(() => {
- process.env.ENABLE_NETWORK_UI_REDESIGN = 'true';
+ window.metamaskFeatureFlags = {
+ networkMenuRedesign: true,
+ };
});
// Reset the environment variable after tests complete
afterEach(() => {
- delete process.env.ENABLE_NETWORK_UI_REDESIGN;
+ window.metamaskFeatureFlags = {
+ networkMenuRedesign: false,
+ };
});
it('should display "Arbitrum" when ENABLE_NETWORK_UI_REDESIGN is true', async () => {
diff --git a/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap b/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap
index 6b2597430661..ca09bf0c145a 100644
--- a/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap
+++ b/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap
@@ -3,10 +3,10 @@
exports[`NetworkListSearch renders search list component 1`] = `
+
diff --git a/ui/components/multichain/network-list-menu/popular-network-list/popular-network-list.tsx b/ui/components/multichain/network-list-menu/popular-network-list/popular-network-list.tsx
index cbe1a99fd69f..62560c1866bd 100644
--- a/ui/components/multichain/network-list-menu/popular-network-list/popular-network-list.tsx
+++ b/ui/components/multichain/network-list-menu/popular-network-list/popular-network-list.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import { ApprovalType } from '@metamask/controller-utils';
import { useDispatch } from 'react-redux';
import { useI18nContext } from '../../../../hooks/useI18nContext';
@@ -14,6 +14,8 @@ import {
IconSize,
ButtonLinkSize,
ButtonLink,
+ Popover,
+ PopoverPosition,
} from '../../../component-library';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
import {
@@ -32,9 +34,9 @@ import {
JustifyContent,
TextColor,
IconColor,
+ TextVariant,
} from '../../../../helpers/constants/design-system';
import { RPCDefinition } from '../../../../../shared/constants/network';
-import Tooltip from '../../../ui/tooltip';
import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
const PopularNetworkList = ({
@@ -45,6 +47,22 @@ const PopularNetworkList = ({
const t = useI18nContext();
const isPopUp = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP;
const dispatch = useDispatch();
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleMouseEnter = () => {
+ setIsOpen(true);
+ };
+
+ const handleMouseLeave = () => {
+ setIsOpen(false);
+ };
+
+ const [referenceElement, setReferenceElement] = useState();
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const setBoxRef = (ref: any) => {
+ setReferenceElement(ref);
+ };
return (
@@ -53,72 +71,78 @@ const PopularNetworkList = ({
marginBottom={1}
paddingLeft={4}
paddingRight={4}
+ ref={setBoxRef}
>
{Object.keys(searchAddNetworkResults).length === 0 ? null : (
-
+
{t('additionalNetworks')}
-
- {t('popularNetworkAddToolTip', [
-
- {
- global.platform.openTab({
- url: ZENDESK_URLS.UNKNOWN_NETWORK,
- });
- }}
- >
- {t('learnMoreUpperCase')}
-
- ,
- ,
- ])}
-
- }
- trigger="mouseenter"
- >
-
-
-
-
+
+
+
+ {t('popularNetworkAddToolTip', [
+
+ {
+ global.platform.openTab({
+ url: ZENDESK_URLS.UNKNOWN_NETWORK,
+ });
+ }}
+ >
+ {t('learnMoreUpperCase')}
+
+ ,
+ ])}
+
+
)}
-
{searchAddNetworkResults.map((item: RPCDefinition, index: number) => (
-
+
) : null}
- {newNetworkAddedConfigurationId && (
+ {newNetworkAddedConfigurationId && !networkMenuRedesign && (
clearNewNetworkAdded()}
diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js
index 07cfcbdff912..649d9f0577fe 100644
--- a/ui/pages/home/home.container.js
+++ b/ui/pages/home/home.container.js
@@ -100,6 +100,7 @@ import {
Web3ShimUsageAlertStates,
} from '../../../shared/constants/alerts';
import { hasTransactionPendingApprovals } from '../../selectors/transactions';
+import { getLocalNetworkMenuRedesignFeatureFlag } from '../../helpers/utils/feature-flags';
import Home from './home.component';
const mapStateToProps = (state) => {
@@ -124,6 +125,8 @@ const mapStateToProps = (state) => {
const pendingConfirmations = getUnapprovedTemplatedConfirmations(state);
const pendingConfirmationsPrioritized =
getPrioritizedUnapprovedTemplatedConfirmations(state);
+ const networkMenuRedesign = getLocalNetworkMenuRedesignFeatureFlag(state);
+
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
const institutionalConnectRequests = getInstitutionalConnectRequests(state);
///: END:ONLY_INCLUDE_IF
@@ -196,6 +199,7 @@ const mapStateToProps = (state) => {
shouldShowWeb3ShimUsageNotification,
pendingConfirmations,
pendingConfirmationsPrioritized,
+ networkMenuRedesign,
infuraBlocked: getInfuraBlocked(state),
announcementsToShow: getSortedAnnouncementsToShow(state).length > 0,
showWhatsNewPopup,
diff --git a/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap b/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap
index 05128f7c75ca..005027028e4a 100644
--- a/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap
+++ b/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap
@@ -12,204 +12,161 @@ exports[`Add Network Modal should render 1`] = `
-
diff --git a/ui/pages/onboarding-flow/add-network-modal/add-network-modal.test.js b/ui/pages/onboarding-flow/add-network-modal/add-network-modal.test.js
index 16909bc8010b..615c1790530e 100644
--- a/ui/pages/onboarding-flow/add-network-modal/add-network-modal.test.js
+++ b/ui/pages/onboarding-flow/add-network-modal/add-network-modal.test.js
@@ -23,7 +23,7 @@ describe('Add Network Modal', () => {
mockNetworkMenuRedesignToggle.mockImplementation(() => false);
const mockStore = configureMockStore([])({
- metamask: { useSafeChainsListValidation: true },
+ metamask: { useSafeChainsListValidation: true, orderedNetworkList: {} },
});
const { container } = renderWithProvider(
@@ -40,7 +40,7 @@ describe('Add Network Modal', () => {
mockNetworkMenuRedesignToggle.mockReturnValue(true);
const mockStore = configureMockStore([thunk])({
- metamask: { useSafeChainsListValidation: true },
+ metamask: { useSafeChainsListValidation: true, orderedNetworkList: {} },
});
const { queryByText } = renderWithProvider(
@@ -50,7 +50,7 @@ describe('Add Network Modal', () => {
await waitFor(() => {
expect(queryByText('Cancel')).not.toBeInTheDocument();
- expect(queryByText('Save')).toBeInTheDocument();
+ expect(queryByText('Next')).toBeInTheDocument();
});
});
});
diff --git a/ui/pages/onboarding-flow/add-network-modal/index.js b/ui/pages/onboarding-flow/add-network-modal/index.js
index b35785e4f2ac..911bb89eee04 100644
--- a/ui/pages/onboarding-flow/add-network-modal/index.js
+++ b/ui/pages/onboarding-flow/add-network-modal/index.js
@@ -16,10 +16,13 @@ import NetworksForm from '../../settings/networks-tab/networks-form/networks-for
export default function AddNetworkModal({
showHeader = false,
- isNewNetworkFlow = false,
+ onEditNetwork = null,
addNewNetwork = true,
networkToEdit = null,
onRpcUrlAdd,
+ prevActionMode = null,
+ networkFormInformation = {},
+ setNetworkFormInformation = () => null,
}) {
const dispatch = useDispatch();
const t = useI18nContext();
@@ -52,7 +55,10 @@ export default function AddNetworkModal({
cancelCallback={closeCallback}
submitCallback={closeCallback}
onRpcUrlAdd={onRpcUrlAdd}
- isNewNetworkFlow={isNewNetworkFlow}
+ onEditNetwork={onEditNetwork}
+ prevActionMode={prevActionMode}
+ networkFormInformation={networkFormInformation}
+ setNetworkFormInformation={setNetworkFormInformation}
{...additionalProps}
/>
>
@@ -63,13 +69,21 @@ AddNetworkModal.propTypes = {
showHeader: PropTypes.bool,
isNewNetworkFlow: PropTypes.bool,
addNewNetwork: PropTypes.bool,
+ onEditNetwork: PropTypes.func,
networkToEdit: PropTypes.object,
onRpcUrlAdd: PropTypes.func,
+ prevActionMode: PropTypes.string,
+ networkFormInformation: PropTypes.object,
+ setNetworkFormInformation: PropTypes.func,
};
AddNetworkModal.defaultProps = {
showHeader: false,
isNewNetworkFlow: false,
addNewNetwork: true,
+ onEditNetwork: null,
networkToEdit: null,
+ prevActionMode: null,
+ networkFormInformation: {},
+ setNetworkFormInformation: () => null,
};
diff --git a/ui/pages/settings/networks-tab/index.scss b/ui/pages/settings/networks-tab/index.scss
index 86e792d988c4..69f1cbb9d073 100644
--- a/ui/pages/settings/networks-tab/index.scss
+++ b/ui/pages/settings/networks-tab/index.scss
@@ -113,8 +113,13 @@
color: var(--color-text-muted);
}
+ &__scrollable {
+ overflow-y: auto;
+ }
+
&__network-form {
- padding: 16px 24px;
+ padding: 16px 16px;
+ width: 100%;
@include design-system.screen-sm-min {
padding: 16px;
@@ -262,23 +267,25 @@
}
&__add-network-form {
- margin-left: 16px;
- margin-right: 16px;
grid-column: span 2; // spread both columns of grid layout
@include design-system.screen-sm-min {
max-width: 400px; // but only expand to 400px
- padding: 16px;
+ padding-left: 16px;
+ padding-right: 16px;
+ padding-bottom: 16px;
}
&__alert {
- margin-top: 0;
+ margin-top: 4;
+ padding: 4;
}
}
&__restrict-height {
max-height: 578px;
overflow-y: auto;
+ width: 100%;
}
&__add-network-form-footer {
diff --git a/ui/pages/settings/networks-tab/networks-form/__snapshots__/rpc-url-editor.test.tsx.snap b/ui/pages/settings/networks-tab/networks-form/__snapshots__/rpc-url-editor.test.tsx.snap
new file mode 100644
index 000000000000..d79e1349e4a4
--- /dev/null
+++ b/ui/pages/settings/networks-tab/networks-form/__snapshots__/rpc-url-editor.test.tsx.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RpcUrlEditor should render correctly 1`] = `
+
+
+ defaultRpcUrl
+
+
+
+ https://current-rpc-url.com
+
+
+
+
+`;
diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js
index c5bdaeeb1fc5..8549b4597924 100644
--- a/ui/pages/settings/networks-tab/networks-form/networks-form.js
+++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js
@@ -21,10 +21,12 @@ import {
import {
BUILT_IN_NETWORKS,
CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP,
+ CHAIN_ID_TO_RPC_URL_MAP,
CHAIN_IDS,
CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION,
FEATURED_RPCS,
infuraProjectId,
+ NETWORK_TO_NAME_MAP,
} from '../../../../../shared/constants/network';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
import { decimalToHex } from '../../../../../shared/modules/conversion.utils';
@@ -40,7 +42,11 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics';
import { getNetworkLabelKey } from '../../../../helpers/utils/i18n-helper';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { usePrevious } from '../../../../hooks/usePrevious';
-import { useSafeChainsListValidationSelector } from '../../../../selectors';
+import {
+ getNonTestNetworks,
+ getOrderedNetworksList,
+ useSafeChainsListValidationSelector,
+} from '../../../../selectors';
import {
editAndSetNetworkConfiguration,
requestUserApproval,
@@ -59,6 +65,8 @@ import {
ButtonPrimarySize,
HelpText,
HelpTextSeverity,
+ IconName,
+ IconSize,
Text,
} from '../../../../components/component-library';
import { FormTextField } from '../../../../components/component-library/form-text-field/deprecated';
@@ -66,6 +74,8 @@ import {
AlignItems,
BackgroundColor,
BlockSize,
+ Display,
+ FlexDirection,
FontWeight,
TextAlign,
TextColor,
@@ -77,7 +87,8 @@ import {
getMatchedSymbols,
} from '../../../../helpers/utils/network-helper';
import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../../helpers/utils/feature-flags';
-import { RpcUrlEditor } from './rpc-url-editor';
+import { ACTION_MODES } from '../../../../components/multichain/network-list-menu/network-list-menu';
+import InfoTooltip from '../../../../components/ui/info-tooltip';
/**
* Attempts to convert the given chainId to a decimal string, for display
@@ -120,25 +131,53 @@ const NetworksForm = ({
selectedNetwork,
cancelCallback,
submitCallback,
- onRpcUrlAdd,
+ onEditNetwork,
+ prevActionMode,
+ networkFormInformation = {},
+ setNetworkFormInformation = () => null,
}) => {
const t = useI18nContext();
const dispatch = useDispatch();
const DEFAULT_SUGGESTED_TICKER = [];
const DEFAULT_SUGGESTED_NAME = [];
+ const CHAIN_LIST_URL = 'https://chainid.network/';
+ const BASE_HEX = 16;
+ const BASE_DECIMAL = 10;
+ const MAX_CHAIN_ID_LENGTH = 12;
+
const { label, labelKey, viewOnly, rpcPrefs } = selectedNetwork;
const selectedNetworkName =
label || (labelKey && t(getNetworkLabelKey(labelKey)));
- const [networkName, setNetworkName] = useState(selectedNetworkName || '');
+ const networkNameForm =
+ prevActionMode === ACTION_MODES.ADD
+ ? networkFormInformation.networkNameForm
+ : '';
+ const networkChainIdForm =
+ prevActionMode === ACTION_MODES.ADD
+ ? networkFormInformation.networkChainIdForm
+ : '';
+ const networkTickerForm =
+ prevActionMode === ACTION_MODES.ADD
+ ? networkFormInformation.networkTickerForm
+ : '';
+
+ const [networkName, setNetworkName] = useState(
+ selectedNetworkName || networkNameForm,
+ );
const [rpcUrl, setRpcUrl] = useState(selectedNetwork?.rpcUrl || '');
- const [chainId, setChainId] = useState(selectedNetwork?.chainId || '');
- const [ticker, setTicker] = useState(selectedNetwork?.ticker || '');
+ const [chainId, setChainId] = useState(
+ selectedNetwork?.chainId || networkChainIdForm,
+ );
+ const [ticker, setTicker] = useState(
+ selectedNetwork?.ticker || networkTickerForm,
+ );
const [suggestedTicker, setSuggestedTicker] = useState(
DEFAULT_SUGGESTED_TICKER,
);
const [blockExplorerUrl, setBlockExplorerUrl] = useState(
selectedNetwork?.blockExplorerUrl || '',
);
+
const [errors, setErrors] = useState({});
const [warnings, setWarnings] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -148,12 +187,16 @@ const NetworksForm = ({
const [isEditing, setIsEditing] = useState(Boolean(addNewNetwork));
const [previousNetwork, setPreviousNetwork] = useState(selectedNetwork);
const [suggestedNames, setSuggestedNames] = useState(DEFAULT_SUGGESTED_NAME);
+ const nonTestNetworks = useSelector(getNonTestNetworks);
const trackEvent = useContext(MetaMetricsContext);
const useSafeChainsListValidation = useSelector(
useSafeChainsListValidationSelector,
);
+
+ const orderedNetworksList = useSelector(getOrderedNetworksList);
+
const networkMenuRedesign = useSelector(
getLocalNetworkMenuRedesignFeatureFlag,
);
@@ -240,7 +283,11 @@ const NetworksForm = ({
const prevBlockExplorerUrl = useRef();
// This effect is used to reset the form when the user switches between networks
useEffect(() => {
- if (!prevAddNewNetwork.current && addNewNetwork) {
+ if (
+ !prevAddNewNetwork.current &&
+ addNewNetwork &&
+ prevActionMode !== ACTION_MODES.ADD
+ ) {
setNetworkName('');
setRpcUrl('');
setChainId('');
@@ -282,13 +329,52 @@ const NetworksForm = ({
previousNetwork,
resetForm,
isEditing,
+ prevActionMode,
]);
+ const newOrderNetworks = () => {
+ if (!orderedNetworksList || orderedNetworksList.length === 0) {
+ return nonTestNetworks;
+ }
+
+ // Create a mapping of chainId to index in orderedNetworksList
+ const orderedIndexMap = {};
+ orderedNetworksList.forEach((network, index) => {
+ orderedIndexMap[`${network.networkId}_${network.networkRpcUrl}`] = index;
+ });
+
+ // Sort nonTestNetworks based on the order in orderedNetworksList
+ const sortedNonTestNetworks = nonTestNetworks.sort((a, b) => {
+ const keyA = `${a.chainId}_${a.rpcUrl}`;
+ const keyB = `${b.chainId}_${b.rpcUrl}`;
+ return orderedIndexMap[keyA] - orderedIndexMap[keyB];
+ });
+
+ return sortedNonTestNetworks;
+ };
+
+ const handleEditNetworkClick = () => {
+ const networksList = networkMenuRedesign
+ ? nonTestNetworks
+ : newOrderNetworks();
+
+ const networkToEdit = Object.values(networksList).find(
+ (network) =>
+ getDisplayChainId(chainId) === getDisplayChainId(network.chainId),
+ );
+
+ if (networkToEdit) {
+ onEditNetwork(networkToEdit);
+ }
+ };
+
useEffect(() => {
return () => {
- setNetworkName('');
- setRpcUrl('');
- setChainId('');
+ if (prevActionMode !== ACTION_MODES.ADD) {
+ setNetworkName('');
+ setRpcUrl('');
+ setChainId('');
+ }
setTicker('');
setBlockExplorerUrl('');
setErrors({});
@@ -302,6 +388,7 @@ const NetworksForm = ({
setBlockExplorerUrl,
setErrors,
dispatch,
+ prevActionMode,
]);
const autoSuggestTicker = useCallback((formChainId) => {
@@ -369,6 +456,7 @@ const NetworksForm = ({
});
};
+ const networksList = Object.values(orderedNetworksList);
const validateBlockExplorerURL = useCallback(
(url) => {
if (url?.length > 0 && !isWebUrl(url)) {
@@ -413,6 +501,22 @@ const NetworksForm = ({
}
}
+ if (
+ addNewNetwork &&
+ networksList.some(
+ (network) =>
+ getDisplayChainId(chainArg) ===
+ parseInt(network.networkId, BASE_HEX).toString(BASE_DECIMAL) &&
+ rpcUrl === network.networkRpcUrl,
+ )
+ ) {
+ return {
+ error: {
+ key: 'existingChainId',
+ },
+ };
+ }
+
const [matchingChainId] = networksToRender.filter(
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
);
@@ -474,13 +578,18 @@ const NetworksForm = ({
}
errorKey = 'endpointReturnedDifferentChainId';
- errorMessage = t('endpointReturnedDifferentChainId', [
- endpointChainId.length <= 12
- ? endpointChainId
- : `${endpointChainId.slice(0, 9)}...`,
- ]);
+ if (networkMenuRedesign) {
+ errorMessage = t('wrongChainId');
+ } else {
+ errorMessage = t('endpointReturnedDifferentChainId', [
+ endpointChainId.length <= MAX_CHAIN_ID_LENGTH
+ ? endpointChainId
+ : `${endpointChainId.slice(0, 9)}...`,
+ ]);
+ }
}
}
+
if (errorKey) {
return {
error: {
@@ -501,7 +610,15 @@ const NetworksForm = ({
autoSuggestName(formChainId);
return null;
},
- [rpcUrl, networksToRender, t],
+ [
+ rpcUrl,
+ networksToRender,
+ t,
+ addNewNetwork,
+ autoSuggestName,
+ autoSuggestTicker,
+ orderedNetworksList,
+ ],
);
/**
@@ -567,7 +684,23 @@ const NetworksForm = ({
let warningMessage;
const decimalChainId = getDisplayChainId(formChainId);
- if (!decimalChainId || !formName) {
+ let hexChainId = formChainId;
+ if (!formChainId.startsWith('0x')) {
+ try {
+ hexChainId = `0x${decimalToHex(formChainId)}`;
+ } catch (err) {
+ return {
+ error: {
+ key: 'invalidHexNumber',
+ msg: t('invalidHexNumber'),
+ },
+ };
+ }
+ }
+
+ const isMatchedName = NETWORK_TO_NAME_MAP[hexChainId] === formName;
+
+ if (!decimalChainId || !formName || isMatchedName) {
setSuggestedNames([]);
return null;
}
@@ -613,7 +746,9 @@ const NetworksForm = ({
);
const validateRPCUrl = useCallback(
- (url) => {
+ async (url, formChainId) => {
+ const decimalChainId = getDisplayChainId(formChainId);
+
const [
{
rpcUrl: matchingRPCUrl = null,
@@ -623,6 +758,21 @@ const NetworksForm = ({
] = networksToRender.filter((e) => e.rpcUrl === url);
const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork;
+ if (
+ networksList.some((network) => url === network.networkRpcUrl) &&
+ addNewNetwork &&
+ networkMenuRedesign
+ ) {
+ return {
+ key: 'existingRpcUrl',
+ msg: t('existingRpcUrl'),
+ };
+ }
+
+ if (!url || (!decimalChainId && networkMenuRedesign)) {
+ return null;
+ }
+
if (url?.length > 0 && !isWebUrl(url)) {
if (isWebUrl(`https://${url}`)) {
return {
@@ -642,9 +792,29 @@ const NetworksForm = ({
]),
};
}
+
+ if (networkMenuRedesign) {
+ let endpointChainId;
+ let providerError;
+
+ try {
+ endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId');
+ } catch (err) {
+ log.warn('Failed to fetch the chainId from the endpoint.', err);
+ providerError = err;
+ }
+
+ if (providerError || typeof endpointChainId !== 'string') {
+ return {
+ key: 'failedToFetchChainId',
+ msg: t('unMatchedChain'),
+ };
+ }
+ }
+
return null;
},
- [selectedNetwork, networksToRender, t],
+ [selectedNetwork, networksToRender, t, rpcUrl],
);
// validation effect
@@ -652,6 +822,8 @@ const NetworksForm = ({
const previousChainId = usePrevious(chainId);
const previousTicker = usePrevious(ticker);
const previousBlockExplorerUrl = usePrevious(blockExplorerUrl);
+ const previousNetworkName = usePrevious(networkName);
+
useEffect(() => {
if (viewOnly) {
return;
@@ -661,7 +833,8 @@ const NetworksForm = ({
previousRpcUrl === rpcUrl &&
previousChainId === chainId &&
previousTicker === ticker &&
- previousBlockExplorerUrl === blockExplorerUrl
+ previousBlockExplorerUrl === blockExplorerUrl &&
+ previousNetworkName === networkName
) {
return;
}
@@ -671,7 +844,7 @@ const NetworksForm = ({
const tickerWarning = await validateTickerSymbol(chainId, ticker);
const nameWarning = await validateNetworkName(chainId, networkName);
const blockExplorerError = validateBlockExplorerURL(blockExplorerUrl);
- const rpcUrlError = validateRPCUrl(rpcUrl);
+ const rpcUrlError = await validateRPCUrl(rpcUrl, chainId);
setErrors({
...errors,
@@ -703,6 +876,7 @@ const NetworksForm = ({
previousChainId,
previousTicker,
previousBlockExplorerUrl,
+ previousNetworkName,
validateBlockExplorerURL,
validateChainId,
validateTickerSymbol,
@@ -846,6 +1020,34 @@ const NetworksForm = ({
}),
);
};
+
+ const isPopularNetwork = Object.values(FEATURED_RPCS).some(
+ (network) =>
+ getDisplayChainId(chainId) === getDisplayChainId(network.chainId) &&
+ rpcUrl === network.rpcUrl,
+ );
+
+ const isDefaultNetwork = (networkId, rpcUrlLink, targetChainId) =>
+ getDisplayChainId(networkId) === parseInt(targetChainId, 16).toString(10) &&
+ rpcUrlLink === CHAIN_ID_TO_RPC_URL_MAP[targetChainId];
+
+ const isDefaultMainnet = isDefaultNetwork(chainId, rpcUrl, CHAIN_IDS.MAINNET);
+ const isDefaultLineaMainnet = isDefaultNetwork(
+ chainId,
+ rpcUrl,
+ CHAIN_IDS.LINEA_MAINNET,
+ );
+ const isDefaultSepoliaTestnet = isDefaultNetwork(
+ chainId,
+ rpcUrl,
+ CHAIN_IDS.SEPOLIA,
+ );
+ const isDefaultLineaSepoliaTestnet = isDefaultNetwork(
+ chainId,
+ rpcUrl,
+ CHAIN_IDS.LINEA_SEPOLIA,
+ );
+
const deletable = !isCurrentRpcTarget && !viewOnly && !addNewNetwork;
const stateUnchanged = stateIsUnchanged();
const chainIdErrorOnFeaturedRpcDuringEdit =
@@ -858,6 +1060,7 @@ const NetworksForm = ({
!rpcUrl ||
!chainId ||
!ticker;
+
let displayRpcUrl = rpcUrl?.includes(`/v3/${infuraProjectId}`)
? rpcUrl.replace(`/v3/${infuraProjectId}`, '')
: rpcUrl;
@@ -865,189 +1068,380 @@ const NetworksForm = ({
displayRpcUrl = displayRpcUrl?.toLowerCase();
}
+ const disableEdit =
+ viewOnly ||
+ isDefaultMainnet ||
+ isDefaultLineaMainnet ||
+ isDefaultLineaSepoliaTestnet ||
+ isDefaultSepoliaTestnet;
+
return (
-
- {addNewNetwork ? (
-
- ) : null}
-
{
- setIsEditing(true);
- setNetworkName(value);
- }}
- titleText={t('networkName')}
- value={networkName}
- disabled={viewOnly}
- dataTestId="network-form-network-name"
- />
- {suggestedNames &&
- suggestedNames.length > 0 &&
- !suggestedNames.some(
- (nameSuggested) => nameSuggested === networkName,
- ) ? (
-
- {t('suggestedTokenName')}
- {suggestedNames.map((suggestedName, i) => (
- {
- setNetworkName(suggestedName);
- }}
- paddingLeft={1}
- paddingRight={1}
- style={{ verticalAlign: 'baseline' }}
- key={i}
- >
- {suggestedName}
-
- ))}
-
+ {addNewNetwork ? (
+
) : null}
-
- {networkMenuRedesign ? (
-
+ 0 &&
+ !suggestedNames.some(
+ (nameSuggested) => nameSuggested === networkName,
+ ) ? (
+
+ {t('suggestedTokenName')}
+ {suggestedNames.map((suggestedName, i) => (
+ {
+ setNetworkName(suggestedName);
+ setNetworkFormInformation((prevState) => ({
+ ...prevState,
+ networkNameForm: suggestedName,
+ }));
+ }}
+ paddingLeft={1}
+ paddingRight={1}
+ style={{ verticalAlign: 'baseline' }}
+ key={i}
+ >
+ {suggestedName}
+
+ ))}
+
+ ) : null
+ }
+ onChange={(e) => {
+ setIsEditing(true);
+ setNetworkName(e.target?.value);
+ setNetworkFormInformation((prevState) => ({
+ ...prevState,
+ networkNameForm: e.target?.value ?? '',
+ }));
+ }}
+ label={t('networkName')}
+ labelProps={{
+ variant: TextVariant.bodySm,
+ fontWeight: FontWeight.Bold,
+ paddingBottom: 1,
+ paddingTop: 1,
+ }}
+ inputProps={{
+ paddingLeft: 2,
+ variant: TextVariant.bodySm,
+ 'data-testid': 'network-form-network-name',
+ }}
+ value={networkName}
+ disabled={disableEdit && !addNewNetwork}
/>
- ) : (
+ {errors.networkName?.msg ? (
+
+ {errors.networkName.msg}
+
+ ) : null}
+ {warnings.networkName?.msg ? (
+
+ {warnings.networkName.msg}
+
+ ) : null}
+
{
setIsEditing(true);
setRpcUrl(value);
}}
titleText={t('rpcUrl')}
value={displayRpcUrl}
- disabled={viewOnly}
+ disabled={disableEdit && !addNewNetwork}
dataTestId="network-form-rpc-url"
/>
- )}
- {
- setIsEditing(true);
- setChainId(value);
- autoSuggestTicker(value);
- }}
- titleText={t('chainId')}
- value={chainId}
- disabled={viewOnly}
- tooltipText={viewOnly ? null : t('networkSettingsChainIdDescription')}
- dataTestId="network-form-chain-id"
- />
- 0 &&
- !suggestedTicker.some(
- (symbolSuggested) => symbolSuggested === ticker,
- ) ? (
-
+ {errors.rpcUrl.msg}
+
+ ) : null}
+ {
+ setIsEditing(true);
+ setChainId(e.target?.value);
+ autoSuggestTicker(e.target?.value);
+ autoSuggestName(e.target?.value);
+ setNetworkFormInformation((prevState) => ({
+ ...prevState,
+ networkChainIdForm: e.target?.value ?? '',
+ }));
+ }}
+ label={
+ viewOnly || networkMenuRedesign ? (
+ t('chainId')
+ ) : (
+ <>
+ {t('chainId')}
+
+
+
+ >
+ )
+ }
+ labelProps={{
+ variant: TextVariant.bodySm,
+ fontWeight: FontWeight.Bold,
+ paddingBottom: 1,
+ paddingTop: 1,
+ }}
+ inputProps={{
+ paddingLeft: 2,
+ variant: TextVariant.bodySm,
+ 'data-testid': 'network-form-chain-id',
+ }}
+ value={chainId}
+ disabled={(disableEdit || isPopularNetwork) && !addNewNetwork}
+ />
+
+ {warnings.chainId?.msg ? (
+
+ {warnings.chainId?.msg}
+
+ ) : null}
+ {errors.chainId?.msg ? (
+
+ {errors.chainId.msg}
+
+ ) : null}
+ {errors.chainId?.key === 'endpointReturnedDifferentChainId' &&
+ networkMenuRedesign ? (
+
+
- {t('suggestedTokenSymbol')}
- {suggestedTicker.map((suggestedSymbol, i) => (
- {
- setTicker(suggestedSymbol);
- }}
- paddingLeft={1}
- paddingRight={1}
- style={{ verticalAlign: 'baseline' }}
- key={i}
- >
- {suggestedSymbol}
-
- ))}
-
- ) : null
- }
- onChange={(e) => {
- setIsEditing(true);
- setTicker(e.target.value);
- }}
- label={t('currencySymbol')}
- labelProps={{
- variant: TextVariant.bodySm,
- fontWeight: FontWeight.Bold,
- paddingBottom: 1,
- paddingTop: 1,
- }}
- inputProps={{
- paddingLeft: 2,
- variant: TextVariant.bodySm,
- 'data-testid': 'network-form-ticker-input',
- }}
- value={ticker}
- disabled={viewOnly}
- />
- {warnings.ticker?.msg ? (
-
- {warnings.ticker.msg}
-
- ) : null}
- {
- setIsEditing(true);
- setBlockExplorerUrl(value);
- }}
- titleText={t('blockExplorerUrl')}
- titleUnit={t('optionalWithParanthesis')}
- value={blockExplorerUrl}
- disabled={viewOnly}
- autoFocus={window.location.hash.split('#')[2] === 'blockExplorerUrl'}
- dataTestId="network-form-block-explorer-url"
- />
+ {t('findTheRightChainId')}{' '}
+ {
+ global.platform.openTab({
+ url: CHAIN_LIST_URL,
+ });
+ }}
+ endIconName={IconName.Export}
+ endIconProps={{
+ size: IconSize.Xs,
+ }}
+ >
+ chainid.network
+
+
+
+ ) : null}
+ {errors.chainId?.key === 'existingChainId' ? (
+
+
+ {t('existingChainId')}
+
+
+ {t('updateOrEditNetworkInformations')}{' '}
+
+ {t('editNetworkLink')}
+
+
+
+ ) : null}
+ 0 &&
+ !suggestedTicker.some(
+ (symbolSuggested) => symbolSuggested === ticker,
+ ) ? (
+
+ {t('suggestedTokenSymbol')}
+ {suggestedTicker.map((suggestedSymbol, i) => (
+ {
+ setTicker(suggestedSymbol);
+ setNetworkFormInformation((prevState) => ({
+ ...prevState,
+ networkTickerForm: suggestedSymbol,
+ }));
+ }}
+ paddingLeft={1}
+ paddingRight={1}
+ style={{ verticalAlign: 'baseline' }}
+ key={i}
+ >
+ {suggestedSymbol}
+
+ ))}
+
+ ) : null
+ }
+ onChange={(e) => {
+ setIsEditing(true);
+ setTicker(e.target?.value);
+ setNetworkFormInformation((prevState) => ({
+ ...prevState,
+ networkTickerForm: e.target?.value ?? '',
+ }));
+ }}
+ label={t('currencySymbol')}
+ labelProps={{
+ variant: TextVariant.bodySm,
+ fontWeight: FontWeight.Bold,
+ paddingBottom: 1,
+ paddingTop: 1,
+ }}
+ inputProps={{
+ paddingLeft: 2,
+ variant: TextVariant.bodySm,
+ 'data-testid': 'network-form-ticker-input',
+ }}
+ value={ticker}
+ disabled={disableEdit && !addNewNetwork}
+ />
+ {warnings.ticker?.msg ? (
+
+ {warnings.ticker.msg}
+
+ ) : null}
+ {
+ setIsEditing(true);
+ setBlockExplorerUrl(e.target?.value);
+ }}
+ label={`${t('blockExplorerUrl')} ${t('optionalWithParanthesis')}`}
+ labelProps={{
+ variant: TextVariant.bodySm,
+ fontWeight: FontWeight.Bold,
+ paddingBottom: 1,
+ paddingTop: 1,
+ }}
+ inputProps={{
+ paddingLeft: 2,
+ variant: TextVariant.bodySm,
+ 'data-testid': 'network-form-block-explorer-url',
+ }}
+ value={blockExplorerUrl ?? ''}
+ disabled={disableEdit && !addNewNetwork}
+ autoFocus={
+ window.location.hash.split('#')[2] === 'blockExplorerUrl'
+ }
+ />
+ {errors.blockExplorerUrl?.msg ? (
+
+ {errors.blockExplorerUrl.msg}
+
+ ) : null}
+
-
{networkMenuRedesign ? (
- {t('save')}
+ {addNewNetwork ? t('next') : t('save')}
) : (
@@ -1091,6 +1485,7 @@ const NetworksForm = ({
type="primary"
disabled={isSubmitDisabled}
onClick={onSubmit}
+ dataTestId="network-form-network-save-button"
>
{t('save')}
@@ -1098,7 +1493,7 @@ const NetworksForm = ({
)}
)}
-
+
);
};
@@ -1111,7 +1506,10 @@ NetworksForm.propTypes = {
submitCallback: PropTypes.func,
restrictHeight: PropTypes.bool,
setActiveOnSubmit: PropTypes.bool,
- onRpcUrlAdd: PropTypes.func,
+ onEditNetwork: PropTypes.func,
+ prevActionMode: PropTypes.string,
+ networkFormInformation: PropTypes.object,
+ setNetworkFormInformation: PropTypes.func,
};
NetworksForm.defaultProps = {
diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.test.js b/ui/pages/settings/networks-tab/networks-form/networks-form.test.js
index e4b338a8a580..adaba2dce9af 100644
--- a/ui/pages/settings/networks-tab/networks-form/networks-form.test.js
+++ b/ui/pages/settings/networks-tab/networks-form/networks-form.test.js
@@ -13,11 +13,21 @@ import NetworksForm from '.';
const renderComponent = (props) => {
const store = configureMockStore([])({
- metamask: { useSafeChainsListValidation: true },
+ metamask: {
+ useSafeChainsListValidation: true,
+ orderedNetworkList: {
+ networkId: '0x1',
+ networkRpcUrl: 'https://mainnet.infura.io/v3/',
+ },
+ },
});
return renderWithProvider(, store);
};
+jest.mock('../../../../helpers/utils/feature-flags', () => ({
+ getLocalNetworkMenuRedesignFeatureFlag: jest.fn(() => false),
+}));
+
const defaultNetworks = defaultNetworksData.map((network) => ({
...network,
viewOnly: true,
@@ -43,11 +53,6 @@ const propNetworkDisplay = {
addNewNetwork: false,
};
-jest.mock('../../../../helpers/utils/feature-flags', () => ({
- ...jest.requireActual('../../../../helpers/utils/feature-flags'),
- getLocalNetworkMenuRedesignFeatureFlag: () => false,
-}));
-
describe('NetworkForm Component', () => {
beforeAll(() => {
nock.disableNetConnect();
@@ -100,7 +105,7 @@ describe('NetworkForm Component', () => {
});
it('should render add new network form correctly', async () => {
- const { queryByText, queryAllByText } = renderComponent(propNewNetwork);
+ const { queryByText, getByTestId } = renderComponent(propNewNetwork);
expect(
queryByText(
'A malicious network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust.',
@@ -110,14 +115,16 @@ describe('NetworkForm Component', () => {
expect(queryByText('New RPC URL')).toBeInTheDocument();
expect(queryByText('Chain ID')).toBeInTheDocument();
expect(queryByText('Currency symbol')).toBeInTheDocument();
- expect(queryByText('Block explorer URL')).toBeInTheDocument();
- expect(queryAllByText('(Optional)')).toHaveLength(1);
+ expect(queryByText('Block explorer URL (Optional)')).toBeInTheDocument();
expect(queryByText('Cancel')).toBeInTheDocument();
expect(queryByText('Save')).toBeInTheDocument();
- await fireEvent.change(screen.getByRole('textbox', { name: 'Chain ID' }), {
+ const chainIdField = getByTestId('network-form-chain-id');
+
+ fireEvent.change(chainIdField, {
target: { value: '1' },
});
+
expect(
await screen.findByText(
'This Chain ID is currently used by the mainnet network.',
@@ -144,7 +151,7 @@ describe('NetworkForm Component', () => {
expect(queryByText('New RPC URL')).toBeInTheDocument();
expect(queryByText('Chain ID')).toBeInTheDocument();
expect(queryByText('Currency symbol')).toBeInTheDocument();
- expect(queryByText('Block explorer URL')).toBeInTheDocument();
+ expect(queryByText('Block explorer URL (Optional)')).toBeInTheDocument();
expect(queryByText('Delete')).toBeInTheDocument();
expect(queryByText('Cancel')).toBeInTheDocument();
expect(queryByText('Save')).toBeInTheDocument();
@@ -167,12 +174,18 @@ describe('NetworkForm Component', () => {
});
it('should validate RPC URL field correctly', async () => {
- renderComponent(propNewNetwork);
+ const { getByTestId } = renderComponent(propNewNetwork);
const rpcUrlField = screen.getByRole('textbox', { name: 'New RPC URL' });
await fireEvent.change(rpcUrlField, {
target: { value: 'test' },
});
+
+ const chainIdField = getByTestId('network-form-chain-id');
+
+ fireEvent.change(chainIdField, {
+ target: { value: '1' },
+ });
expect(
await screen.findByText(
'URLs require the appropriate HTTP/HTTPS prefix.',
@@ -219,10 +232,11 @@ describe('NetworkForm Component', () => {
});
it('should validate chain id field correctly', async () => {
- renderComponent(propNewNetwork);
- const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
+ const { getByTestId } = renderComponent(propNewNetwork);
+ const chainIdField = getByTestId('network-form-chain-id');
+
const rpcUrlField = screen.getByRole('textbox', { name: 'New RPC URL' });
- const currencySymbolField = screen.getByTestId('network-form-ticker-input');
+ const currencySymbolField = getByTestId('network-form-ticker-input');
fireEvent.change(chainIdField, {
target: { value: '1' },
@@ -277,10 +291,10 @@ describe('NetworkForm Component', () => {
});
it('should validate currency symbol field correctly', async () => {
- renderComponent(propNewNetwork);
+ const { getByTestId } = renderComponent(propNewNetwork);
- const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
- const currencySymbolField = screen.getByTestId('network-form-ticker-input');
+ const chainIdField = getByTestId('network-form-chain-id');
+ const currencySymbolField = getByTestId('network-form-ticker-input');
fireEvent.change(chainIdField, {
target: { value: '1234' },
@@ -303,10 +317,12 @@ describe('NetworkForm Component', () => {
});
it('should validate block explorer URL field correctly', async () => {
- renderComponent(propNewNetwork);
- const blockExplorerUrlField = screen.getByRole('textbox', {
- name: 'Block explorer URL (Optional)',
- });
+ const { getByTestId } = renderComponent(propNewNetwork);
+
+ const blockExplorerUrlField = getByTestId(
+ 'network-form-block-explorer-url',
+ );
+
fireEvent.change(blockExplorerUrlField, {
target: { value: '1234' },
});
@@ -332,10 +348,10 @@ describe('NetworkForm Component', () => {
.spyOn(fetchWithCacheModule, 'default')
.mockResolvedValue(safeChainsList);
- renderComponent(propNewNetwork);
+ const { getByTestId } = renderComponent(propNewNetwork);
- const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
- const currencySymbolField = screen.getByTestId('network-form-ticker-input');
+ const chainIdField = getByTestId('network-form-chain-id');
+ const currencySymbolField = getByTestId('network-form-ticker-input');
fireEvent.change(chainIdField, {
target: { value: '42161' },
@@ -376,9 +392,9 @@ describe('NetworkForm Component', () => {
.spyOn(fetchWithCacheModule, 'default')
.mockResolvedValue(safeChainsList);
- renderComponent(propNewNetwork);
+ const { getByTestId } = renderComponent(propNewNetwork);
- const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
+ const chainIdField = getByTestId('network-form-chain-id');
const currencySymbolField = screen.getByTestId('network-form-ticker-input');
fireEvent.change(chainIdField, {
diff --git a/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.test.tsx b/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.test.tsx
new file mode 100644
index 000000000000..8adf0fcbc985
--- /dev/null
+++ b/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.test.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { useDispatch } from 'react-redux';
+import { showModal, toggleNetworkMenu } from '../../../../store/actions';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import { RpcUrlEditor } from './rpc-url-editor';
+
+// Mock useDispatch
+jest.mock('react-redux', () => ({
+ useDispatch: jest.fn(),
+}));
+
+jest.mock('../../../../hooks/useI18nContext', () => ({
+ useI18nContext: jest.fn(),
+}));
+
+describe('RpcUrlEditor', () => {
+ const useDispatchMock = useDispatch as jest.Mock;
+ const mockOnRpcUrlAdd = jest.fn();
+ const mockOnRpcSelected = jest.fn();
+ const useI18nContextMock = useI18nContext as jest.Mock;
+ const mockDispatch = jest.fn();
+
+ const defaultProps = {
+ currentRpcUrl: 'https://current-rpc-url.com',
+ onRpcUrlAdd: mockOnRpcUrlAdd,
+ onRpcSelected: mockOnRpcSelected,
+ dummyRpcUrls: [
+ { url: 'https://rpc-url-1.com', selected: false },
+ { url: 'https://rpc-url-2.com', selected: true },
+ ],
+ };
+
+ beforeEach(() => {
+ useDispatchMock.mockReturnValue(mockDispatch);
+ useI18nContextMock.mockReturnValue((key: string) => key);
+ jest.clearAllMocks();
+ });
+
+ it('should render correctly', () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should toggle the dropdown when clicked', () => {
+ render();
+
+ const dropdown = screen.getByText('https://current-rpc-url.com');
+ fireEvent.click(dropdown);
+ expect(screen.getByText('https://rpc-url-1.com')).toBeVisible();
+
+ fireEvent.click(dropdown);
+ expect(screen.queryByText('https://rpc-url-1.com')).not.toBeInTheDocument();
+ });
+
+ it('should call onRpcUrlAdd when "Add RPC URL" button is clicked', () => {
+ render();
+
+ const dropdown = screen.getByText('https://current-rpc-url.com');
+ fireEvent.click(dropdown);
+
+ const addButton = screen.getByText('addRpcUrl');
+ fireEvent.click(addButton);
+
+ expect(mockOnRpcUrlAdd).toHaveBeenCalled();
+ });
+
+ it('should dispatch actions when delete button is clicked', () => {
+ render();
+
+ const dropdown = screen.getByText('https://current-rpc-url.com');
+ fireEvent.click(dropdown);
+
+ const deleteButton = screen.getAllByLabelText('delete')[0];
+ fireEvent.click(deleteButton);
+
+ expect(mockDispatch).toHaveBeenCalledWith(toggleNetworkMenu());
+ expect(mockDispatch).toHaveBeenCalledWith(
+ showModal({ name: 'CONFIRM_DELETE_RPC_URL' }),
+ );
+ });
+});
diff --git a/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.tsx b/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.tsx
index 41ece0b7388d..093656855512 100644
--- a/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.tsx
+++ b/ui/pages/settings/networks-tab/networks-form/rpc-url-editor.tsx
@@ -30,23 +30,25 @@ import { showModal, toggleNetworkMenu } from '../../../../store/actions';
export const RpcUrlEditor = ({
currentRpcUrl,
onRpcUrlAdd,
+ onRpcSelected,
+ dummyRpcUrls = [],
}: {
currentRpcUrl: string;
onRpcUrlAdd: () => void;
+ onRpcSelected: (url: string) => void;
+ dummyRpcUrls: { url: string; selected: boolean }[];
}) => {
- // TODO: real endpoints
- const dummyRpcUrls = [
- currentRpcUrl,
- 'https://mainnet.public.blastapi.io',
- 'https://infura.foo.bar.baz/123456789',
- ];
-
const t = useI18nContext();
const dispatch = useDispatch();
const rpcDropdown = useRef(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [currentRpcEndpoint, setCurrentRpcEndpoint] = useState(currentRpcUrl);
+ const handleSelectRpc = (rpcEndpoint: string) => {
+ onRpcSelected(rpcEndpoint);
+ setCurrentRpcEndpoint(rpcEndpoint);
+ };
+
return (
<>
setIsDropdownOpen(!isDropdownOpen)}
>
- {dummyRpcUrls.map((rpcEndpoint) => (
+ {dummyRpcUrls.map(({ url }) => (
{
- setCurrentRpcEndpoint(rpcEndpoint);
+ handleSelectRpc(url);
setIsDropdownOpen(false);
}}
className={classnames('networks-tab__rpc-item', {
- 'networks-tab__rpc-item--selected':
- rpcEndpoint === currentRpcEndpoint,
+ 'networks-tab__rpc-item--selected': url === currentRpcEndpoint,
})}
>
- {rpcEndpoint === currentRpcEndpoint && (
+ {url === currentRpcEndpoint && (
- {rpcEndpoint}
+ {url}
({
+ getLocalNetworkMenuRedesignFeatureFlag: jest.fn(() => false),
+}));
+
const renderComponent = (props) => {
const store = configureMockStore([])(mockState);
return renderWithProvider(, store);
@@ -58,7 +66,7 @@ describe('NetworksTabContent Component', () => {
expect(queryByText('New RPC URL')).toBeInTheDocument();
expect(queryByText('Chain ID')).toBeInTheDocument();
expect(queryByText('Currency symbol')).toBeInTheDocument();
- expect(queryByText('Block explorer URL')).toBeInTheDocument();
+ expect(queryByText('Block explorer URL (Optional)')).toBeInTheDocument();
expect(queryByText('Cancel')).toBeInTheDocument();
expect(queryByText('Save')).toBeInTheDocument();
diff --git a/ui/pages/settings/networks-tab/networks-tab.test.js b/ui/pages/settings/networks-tab/networks-tab.test.js
index cead29634a70..e30430f53b2a 100644
--- a/ui/pages/settings/networks-tab/networks-tab.test.js
+++ b/ui/pages/settings/networks-tab/networks-tab.test.js
@@ -14,6 +14,10 @@ const mockState = {
type: 'localhost',
},
networkConfigurations: {},
+ orderedNetworkList: {
+ chainId: '0x539',
+ rpcUrl: 'http://localhost:8545',
+ },
},
appState: {
networksTabSelectedRpcUrl: 'http://localhost:8545',
@@ -48,7 +52,7 @@ describe('NetworksTab Component', () => {
expect(queryByText('New RPC URL')).toBeInTheDocument();
expect(queryByText('Chain ID')).toBeInTheDocument();
expect(queryByText('Currency symbol')).toBeInTheDocument();
- expect(queryByText('Block explorer URL')).toBeInTheDocument();
+ expect(queryByText('Block explorer URL (Optional)')).toBeInTheDocument();
expect(queryByText('Cancel')).toBeInTheDocument();
expect(queryByText('Save')).toBeInTheDocument();
});
diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js
index 9e8221aaf7ed..af6779509830 100644
--- a/ui/selectors/selectors.js
+++ b/ui/selectors/selectors.js
@@ -44,6 +44,7 @@ import {
POLYGON_ZKEVM_DISPLAY_NAME,
MOONBEAM_DISPLAY_NAME,
MOONRIVER_DISPLAY_NAME,
+ BUILT_IN_NETWORKS,
} from '../../shared/constants/network';
import {
WebHIDConnectedStatuses,
@@ -650,6 +651,8 @@ export const getNonTestNetworks = createDeepEqualSelector(
ticker: CURRENCY_SYMBOLS.ETH,
id: NETWORK_TYPES.MAINNET,
removable: false,
+ blockExplorerUrl:
+ BUILT_IN_NETWORKS[NETWORK_TYPES.MAINNET].blockExplorerUrl,
},
{
chainId: CHAIN_IDS.LINEA_MAINNET,
@@ -662,12 +665,15 @@ export const getNonTestNetworks = createDeepEqualSelector(
ticker: CURRENCY_SYMBOLS.ETH,
id: NETWORK_TYPES.LINEA_MAINNET,
removable: false,
+ blockExplorerUrl:
+ BUILT_IN_NETWORKS[NETWORK_TYPES.LINEA_MAINNET].blockExplorerUrl,
},
// Custom networks added by the user
...Object.values(networkConfigurations)
.filter(({ chainId }) => ![CHAIN_IDS.LOCALHOST].includes(chainId))
.map((network) => ({
...network,
+ blockExplorerUrl: network.rpcPrefs?.blockExplorerUrl,
rpcPrefs: {
...network.rpcPrefs,
// Provide an image based on chainID if a network