Skip to content

Commit

Permalink
refactor: refactor network picker
Browse files Browse the repository at this point in the history
  • Loading branch information
gantunesr committed Feb 19, 2025
1 parent c7e077e commit 2a456cc
Show file tree
Hide file tree
Showing 14 changed files with 426 additions and 136 deletions.
21 changes: 16 additions & 5 deletions app/scripts/controllers/network-order.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { BtcScope, SolScope } from '@metamask/keyring-api';
import { BaseController, RestrictedMessenger } from '@metamask/base-controller';
import {
NetworkControllerStateChangeEvent,
NetworkState,
} from '@metamask/network-controller';
import { Hex } from '@metamask/utils';
import { toEvmCaipChainId } from '@metamask/multichain-network-controller';
import type { CaipChainId, Hex } from '@metamask/utils';
import type { Patch } from 'immer';
import { TEST_CHAINS } from '../../../shared/constants/network';

Expand All @@ -14,7 +16,7 @@ const controllerName = 'NetworkOrderController';
* Information about an ordered network.
*/
export type NetworksInfo = {
networkId: Hex; // The network's chain id
networkId: CaipChainId; // The network's chain id
};

// State shape for NetworkOrderController
Expand Down Expand Up @@ -112,10 +114,11 @@ export class NetworkOrderController extends BaseController<
}: NetworkState) {
this.update((state) => {
// Filter out testnets, which are in the state but not orderable
const chainIds = Object.keys(networkConfigurationsByChainId).filter(
const hexChainIds = Object.keys(networkConfigurationsByChainId).filter(
(chainId) =>
!TEST_CHAINS.includes(chainId as (typeof TEST_CHAINS)[number]),
) as Hex[];
const chainIds: CaipChainId[] = hexChainIds.map(toEvmCaipChainId);

const newNetworks = chainIds
.filter(
Expand All @@ -128,7 +131,15 @@ export class NetworkOrderController extends BaseController<

state.orderedNetworkList = state.orderedNetworkList
// Filter out deleted networks
.filter(({ networkId }) => chainIds.includes(networkId))
.filter(
({ networkId }) =>
chainIds.includes(networkId) ||
// Since Bitcoin and Solana are not part of the @metamask/network-controller, we have
// to add a second check to make sure it is not filtered out.
// TO DO: Update this logic to @metamask/multichain-network-controller once all networks are migrated.
// @ts-expect-error - BtcScope.Mainnet and SolScope.Mainnet are of type '`${string}:${string}`'
[BtcScope.Mainnet, SolScope.Mainnet].includes(networkId),
)
// Append new networks to the end
.concat(newNetworks);
});
Expand All @@ -140,7 +151,7 @@ export class NetworkOrderController extends BaseController<
* @param networkList - The list of networks to update in the state.
*/

updateNetworksList(chainIds: Hex[]) {
updateNetworksList(chainIds: CaipChainId[]) {
this.update((state) => {
state.orderedNetworkList = chainIds.map((chainId) => ({
networkId: chainId,
Expand Down
6 changes: 4 additions & 2 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3287,8 +3287,10 @@ export default class MetamaskController extends EventEmitter {
verifyPassword: this.verifyPassword.bind(this),

// network management
setActiveNetwork: (networkConfigurationId) => {
return this.networkController.setActiveNetwork(networkConfigurationId);
setActiveNetwork: (id) => {
// The multichain network controller will proxy the call to the network controller
// in the case that the ID is an EVM network client ID.
return this.multichainNetworkController.setActiveNetwork(id);
},
// Avoids returning the promise so that initial call to switch network
// doesn't block on the network lookup step
Expand Down
2 changes: 1 addition & 1 deletion shared/constants/multichain/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP = {
'https://explorer.solana.com/?cluster=testnet',
} as const;

export const MULTICHAIN_TOKEN_IMAGE_MAP = {
export const MULTICHAIN_TOKEN_IMAGE_MAP: Record<CaipChainId, string> = {
[MultichainNetworks.BITCOIN]: BITCOIN_TOKEN_IMAGE_URL,
[MultichainNetworks.SOLANA]: SOLANA_TOKEN_IMAGE_URL,
} as const;
Expand Down
96 changes: 96 additions & 0 deletions shared/modules/network.utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { SolScope, BtcScope } from '@metamask/keyring-api';
import { MAX_SAFE_CHAIN_ID } from '../constants/network';
import {
isSafeChainId,
isPrefixedFormattedHexString,
isTokenDetectionEnabledForNetwork,
convertNetworkId,
convertCaipToHexChainId,
sortNetworks,
} from './network.utils';

describe('network utils', () => {
Expand Down Expand Up @@ -132,4 +135,97 @@ describe('network utils', () => {
expect(convertNetworkId('1.1')).toStrictEqual(null);
});
});

describe('convertCaipToHexChainId', () => {
it('converts a CAIP chain ID to a hex chain ID', () => {
expect(convertCaipToHexChainId('eip155:1')).toBe('0x1');
expect(convertCaipToHexChainId('eip155:56')).toBe('0x38');
expect(convertCaipToHexChainId('eip155:80094')).toBe('0x138de');
expect(convertCaipToHexChainId('eip155:8453')).toBe('0x2105');
});

it('throws an error given a CAIP chain ID with an unsupported namespace', () => {
expect(() =>
convertCaipToHexChainId('bip122:000000000019d6689c085ae165831e93'),
).toThrow(
'Unsupported CAIP chain ID namespace: bip122. Only eip155 is supported.',
);
expect(() =>
convertCaipToHexChainId('solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'),
).toThrow(
'Unsupported CAIP chain ID namespace: solana. Only eip155 is supported.',
);
});
});

describe('sortNetworks', () => {
it('sorts a list of networks based on the order of their chain IDs', () => {
const networks = {
[SolScope.Mainnet]: {
chainId: SolScope.Mainnet,
name: 'Solana Mainnet',
nativeCurrency: `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`,
isEvm: false,
},
'eip155:1': {
chainId: 'eip155:1',
name: 'Ethereum Mainnet',
nativeCurrency: 'ETH',
blockExplorerUrls: ['https://etherscan.io'],
defaultBlockExplorerUrlIndex: 0,
isEvm: true,
},
'eip155:11155111': {
chainId: 'eip155:11155111',
name: 'Sepolia',
nativeCurrency: 'SepoliaETH',
blockExplorerUrls: ['https://sepolia.etherscan.io'],
defaultBlockExplorerUrlIndex: 0,
isEvm: true,
},
[BtcScope.Mainnet]: {
chainId: BtcScope.Mainnet,
name: 'Bitcoin Mainnet',
nativeCurreny: `${BtcScope.Mainnet}/slip44:0`,
isEvm: false,
},
};
const sortedChainIds = [
{ networkId: SolScope.Mainnet },
{ networkId: BtcScope.Mainnet },
{ networkId: 'eip155:1' },
{ networkId: 'eip155:11155111' },
];
expect(sortNetworks(networks, sortedChainIds)).toStrictEqual([

Check failure on line 199 in shared/modules/network.utils.test.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Argument of type '{ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": { chainId: SolScope; name: string; nativeCurrency: string; isEvm: boolean; }; 'eip155:1': { chainId: string; name: string; nativeCurrency: string; blockExplorerUrls: string[]; defaultBlockExplorerUrlIndex: number; isEvm: boolean; }; 'eip155:11155111': { ...; }; "bip122:00...' is not assignable to parameter of type 'Record<string, MultichainNetworkConfiguration>'.
{
chainId: SolScope.Mainnet,
name: 'Solana Mainnet',
nativeCurrency: `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`,
isEvm: false,
},
{
chainId: BtcScope.Mainnet,
name: 'Bitcoin Mainnet',
nativeCurreny: `${BtcScope.Mainnet}/slip44:0`,
isEvm: false,
},
{
chainId: 'eip155:1',
name: 'Ethereum Mainnet',
nativeCurrency: 'ETH',
blockExplorerUrls: ['https://etherscan.io'],
defaultBlockExplorerUrlIndex: 0,
isEvm: true,
},
{
chainId: 'eip155:11155111',
name: 'Sepolia',
nativeCurrency: 'SepoliaETH',
blockExplorerUrls: ['https://sepolia.etherscan.io'],
defaultBlockExplorerUrlIndex: 0,
isEvm: true,
},
]);
});
});
});
43 changes: 42 additions & 1 deletion shared/modules/network.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { isStrictHexString } from '@metamask/utils';
import {
type Hex,
type CaipChainId,
KnownCaipNamespace,
isStrictHexString,
parseCaipChainId,
} from '@metamask/utils';
import { convertHexToDecimal } from '@metamask/controller-utils';
import type { MultichainNetworkConfiguration } from '@metamask/multichain-network-controller';
import { CHAIN_IDS, MAX_SAFE_CHAIN_ID } from '../constants/network';

/**
Expand Down Expand Up @@ -91,3 +98,37 @@ export function convertNetworkId(value: unknown): string | null {
}
return null;
}

/**
* Convert an eip155 CAIP chain ID to a hex chain ID.
*
* @param id - The CAIP chain ID to convert.
* @returns The hex chain ID.
*/
export function convertCaipToHexChainId(id: CaipChainId): Hex {
const { namespace, reference } = parseCaipChainId(id);
if (namespace === KnownCaipNamespace.Eip155) {
return `0x${parseInt(reference, 10).toString(16)}`;
}

throw new Error(
`Unsupported CAIP chain ID namespace: ${namespace}. Only eip155 is supported.`,
);
}

/**
* Sorts a list of networks based on the order of their chain IDs.
*
* @param networks - The networks to sort.
* @param sortedChainIds - The chain IDs to sort by.
* @returns The sorted list of networks.
*/
export const sortNetworks = (
networks: Record<string, MultichainNetworkConfiguration>,
sortedChainIds: { networkId: string }[],
): MultichainNetworkConfiguration[] =>
Object.values(networks).sort(
(a, b) =>
sortedChainIds.findIndex(({ networkId }) => networkId === a.chainId) -
sortedChainIds.findIndex(({ networkId }) => networkId === b.chainId),
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type MultichainNetworkConfiguration } from '@metamask/multichain-network-controller';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
Expand All @@ -6,16 +7,17 @@ import MetafoxLogo from '../../ui/metafox-logo';
import { PickerNetwork } from '../../component-library';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { getTestNetworkBackgroundColor } from '../../../selectors';
import { MultichainNetwork } from '../../../selectors/multichain';

type AppHeaderLockedContentProps = {
currentNetwork: MultichainNetwork;
currentNetwork: MultichainNetworkConfiguration;
networkIconSrc: string;
networkOpenCallback: () => void;
};

export const AppHeaderLockedContent = ({
currentNetwork,
networkOpenCallback,
networkIconSrc,
}: AppHeaderLockedContentProps) => {
const t = useI18nContext();
const history = useHistory();
Expand All @@ -29,11 +31,11 @@ export const AppHeaderLockedContent = ({
avatarNetworkProps={{
backgroundColor: testNetworkBackgroundColor,
role: 'img',
name: currentNetwork?.nickname ?? '',
name: currentNetwork.name,
}}
aria-label={`${t('networkMenu')} ${currentNetwork?.nickname}`}
label={currentNetwork?.nickname ?? ''}
src={currentNetwork?.network?.rpcPrefs?.imageUrl}
aria-label={`${t('networkMenu')} ${currentNetwork.name}`}
label={currentNetwork.name}
src={networkIconSrc}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
e.preventDefault();
Expand Down
24 changes: 13 additions & 11 deletions ui/components/multichain/app-header/app-header-unlocked-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useContext, useState } from 'react';
import browser from 'webextension-polyfill';

import { InternalAccount } from '@metamask/keyring-internal-api';
import { type MultichainNetworkConfiguration } from '@metamask/multichain-network-controller';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import {
Expand Down Expand Up @@ -56,12 +57,12 @@ import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
import { MINUTE } from '../../../../shared/constants/time';
import { NotificationsTagCounter } from '../notifications-tag-counter';
import { REVIEW_PERMISSIONS } from '../../../helpers/constants/routes';
import { MultichainNetwork } from '../../../selectors/multichain';

type AppHeaderUnlockedContentProps = {
popupStatus: boolean;
isEvmNetwork: boolean;
currentNetwork: MultichainNetwork;
currentNetwork: MultichainNetworkConfiguration;
networkIconSrc: string;
networkOpenCallback: () => void;
disableNetworkPicker: boolean;
disableAccountPicker: boolean;
Expand All @@ -73,6 +74,7 @@ export const AppHeaderUnlockedContent = ({
popupStatus,
isEvmNetwork,
currentNetwork,
networkIconSrc,
networkOpenCallback,
disableNetworkPicker,
disableAccountPicker,
Expand Down Expand Up @@ -126,19 +128,19 @@ export const AppHeaderUnlockedContent = ({
<>
{popupStatus ? (
<Box className="multichain-app-header__contents__container">
<Tooltip title={currentNetwork?.nickname} position="right">
<Tooltip title={currentNetwork.name} position="right">
<PickerNetwork
avatarNetworkProps={{
backgroundColor: testNetworkBackgroundColor,
role: 'img',
name: currentNetwork?.nickname ?? '',
name: currentNetwork.name ?? '',
}}
className="multichain-app-header__contents--avatar-network"
ref={menuRef}
as="button"
src={currentNetwork?.network?.rpcPrefs?.imageUrl ?? ''}
label={currentNetwork?.nickname ?? ''}
aria-label={`${t('networkMenu')} ${currentNetwork?.nickname}`}
src={networkIconSrc}
label={currentNetwork.name}
aria-label={`${t('networkMenu')} ${currentNetwork.name}`}
labelProps={{
display: Display.None,
}}
Expand All @@ -158,12 +160,12 @@ export const AppHeaderUnlockedContent = ({
avatarNetworkProps={{
backgroundColor: testNetworkBackgroundColor,
role: 'img',
name: currentNetwork?.nickname ?? '',
name: currentNetwork.name,
}}
margin={2}
aria-label={`${t('networkMenu')} ${currentNetwork?.nickname}`}
label={currentNetwork?.nickname ?? ''}
src={currentNetwork?.network?.rpcPrefs?.imageUrl}
aria-label={`${t('networkMenu')} ${currentNetwork.name}`}
label={currentNetwork.name}
src={networkIconSrc}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
e.preventDefault();
Expand Down
Loading

0 comments on commit 2a456cc

Please sign in to comment.