Skip to content

Commit

Permalink
feat: Transition from Multiple Networks with Same ChainID to Unique N…
Browse files Browse the repository at this point in the history
…etworks with Distinct ChainIDs and Multiple RPC URLs (#11705)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
This PR refactors our network configuration to eliminate the use of
multiple networks with the same ChainID but different RPC URLs. Instead,
we are moving towards a setup where each network is uniquely identified
by a distinct ChainID and can have multiple RPC URLs associated with it.

This PR includes three merge commits. The first primarily addresses the
Network Controller upgrade, as outlined in issue
#[11229](#11229). The
second commit contains the script for migrating the state to v21, and
the third commit includes all the UI changes along with the fix for the
e2e tests.

For more details, please refer to
[this](https://github.com/orgs/MetaMask/projects/120/views/1) .

related PRs:

- #11292
- #11622
- #11436

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Related issues**

Fixes:
#[11229](#11229)
#[11232](#11232)
#[11234](#11234)
#11233

## **Manual testing steps**

1. Go to add network flow and test

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**


https://drive.google.com/drive/folders/149Xji42k5of5Vl8nBlI0pFYFgPnWqILH?usp=drive_link

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
  • Loading branch information
salimtb authored Oct 11, 2024
1 parent 5c66cee commit 87b82d9
Show file tree
Hide file tree
Showing 91 changed files with 6,923 additions and 4,050 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] =
"alignItems": "center",
"backgroundColor": "#ffffff",
"flexDirection": "row",
"paddingRight": 20,
"width": "100%",
}
}
>
<TouchableOpacity
disabled={false}
style={
{
"flex": 1,
"opacity": 1,
"padding": 16,
"position": "relative",
"width": "90%",
"zIndex": 1,
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ListItemMultiSelectButtonProps } from './ListItemMultiSelectButton.type
// Defaults
export const DEFAULT_LISTITEMMULTISELECT_GAP = 16;
export const BUTTON_TEST_ID = 'button-menu-select-test-id';
export const BUTTON_TEXT_TEST_ID = 'button-text-select-test-id';

// Sample consts
export const SAMPLE_LISTITEMMULTISELECT_PROPS: ListItemMultiSelectButtonProps =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ const styleSheet = (params: {
return StyleSheet.create({
base: Object.assign(
{
flex: 1,
position: 'relative',
opacity: isDisabled ? 0.5 : 1,
padding: 16,
width: '90%',
zIndex: 1,
} as ViewStyle,
style,
Expand Down Expand Up @@ -71,10 +71,8 @@ const styleSheet = (params: {
backgroundColor: isSelected
? colors.primary.muted
: colors.background.default,
paddingRight: 20,
flexDirection: 'row',
alignItems: 'center',
width: '100%',
},
itemColumn: {
display: 'flex',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@ exports[`ListItemMultiSelectButton should render correctly with default props 1`
"alignItems": "center",
"backgroundColor": "#ffffff",
"flexDirection": "row",
"paddingRight": 20,
"width": "100%",
}
}
>
<TouchableOpacity
disabled={false}
style={
{
"flex": 1,
"opacity": 1,
"padding": 16,
"position": "relative",
"width": "90%",
"zIndex": 1,
}
}
Expand Down
1 change: 0 additions & 1 deletion app/components/Nav/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,6 @@ const App = (props) => {
component={MultiRpcModal}
/>
) : null}

<Stack.Screen
name={Routes.SHEET.SHOW_TOKEN_ID}
component={ShowTokenIdSheet}
Expand Down
1 change: 0 additions & 1 deletion app/components/Nav/Main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,6 @@ const Main = (props) => {
networkImageSource: networkImage,
});
}

previousNetworkConfigurations.current = networkConfigurations;
}, [networkConfigurations, networkName, networkImage, toastRef]);

Expand Down
23 changes: 9 additions & 14 deletions app/components/UI/AccountInfoCard/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
MOCK_ADDRESS_1,
} from '../../../util/test/accountsControllerTestUtils';
import { RootState } from '../../../reducers';
import { RpcEndpointType } from '@metamask/network-controller';
import { mockNetworkState } from '../../../util/test/network';

jest.mock('../../../core/Engine', () => ({
resetState: jest.fn(),
Expand Down Expand Up @@ -48,20 +50,13 @@ const mockInitialState: DeepPartial<RootState> = {
},
},
NetworkController: {
selectedNetworkClientId: 'sepolia',
networksMetadata: {},
networkConfigurations: {
sepolia: {
id: 'sepolia',
rpcUrl: 'http://localhost/v3/',
chainId: '0xaa36a7',
ticker: 'ETH',
nickname: 'sepolia',
rpcPrefs: {
blockExplorerUrl: 'https://etherscan.com',
},
},
},
...mockNetworkState({
chainId: '0xaa36a7',
id: 'mainnet',
nickname: 'Sepolia',
ticker: 'SepoliaETH',
type: RpcEndpointType.Infura,
}),
},
TokenBalancesController: {
contractBalances: {},
Expand Down
5 changes: 4 additions & 1 deletion app/components/UI/NetworkModal/NetworkAdded/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const createStyles = (colors: any) =>
flexDirection: 'row',
paddingVertical: 16,
},
base: {
padding: 16,
},
button: {
flex: 1,
},
Expand Down Expand Up @@ -41,7 +44,7 @@ const NetworkAdded = (props: NetworkAddedProps) => {
const styles = createStyles(colors);

return (
<View>
<View style={styles.base}>
<Text centered bold black big>
{strings('networks.new_network')}
</Text>
Expand Down
178 changes: 145 additions & 33 deletions app/components/UI/NetworkModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import { useMetrics } from '../../../components/hooks/useMetrics';
import { toHex } from '@metamask/controller-utils';
import { rpcIdentifierUtility } from '../../../components/hooks/useSafeChains';
import Logger from '../../../util/Logger';
import { selectNetworkConfigurations } from '../../../selectors/networkController';
import {
NetworkConfiguration,
RpcEndpointType,
AddNetworkFields,
} from '@metamask/network-controller';

export interface SafeChain {
chainId: string;
Expand Down Expand Up @@ -162,6 +168,10 @@ const NetworkModals = (props: NetworkProps) => {
selectUseSafeChainsListValidation,
);

const networkConfigurationByChainId = useSelector(
selectNetworkConfigurations,
);

const customNetworkInformation = {
chainId,
blockExplorerUrl,
Expand Down Expand Up @@ -189,52 +199,154 @@ const NetworkModals = (props: NetworkProps) => {
checkNetwork();
}, [checkNetwork]);

const closeModal = () => {
const closeModal = async () => {
const { NetworkController } = Engine.context;
const url = new URLPARSE(rpcUrl);
!isPrivateConnection(url.hostname) && url.set('protocol', 'https:');
NetworkController.upsertNetworkConfiguration(
{
rpcUrl: url.href,

const existingNetwork = networkConfigurationByChainId[chainId];
let networkClientId;

if (existingNetwork) {
const updatedNetwork = await NetworkController.updateNetwork(
existingNetwork.chainId,
existingNetwork,
existingNetwork.chainId === chainId
? {
replacementSelectedRpcEndpointIndex:
existingNetwork.defaultRpcEndpointIndex,
}
: undefined,
);

networkClientId =
updatedNetwork?.rpcEndpoints?.[updatedNetwork.defaultRpcEndpointIndex]
?.networkClientId;
} else {
const addedNetwork = await NetworkController.addNetwork({
chainId,
ticker,
nickname,
rpcPrefs: { blockExplorerUrl },
},
{
// Metrics-related properties required, but the metric event is a no-op
// TODO: Use events for controller metric events
referrer: 'ignored',
source: 'ignored',
},
);
blockExplorerUrls: [blockExplorerUrl],
defaultRpcEndpointIndex: 0,
defaultBlockExplorerUrlIndex: 0,
name: nickname,
nativeCurrency: ticker,
rpcEndpoints: [
{
url: rpcUrl,
name: nickname,
type: RpcEndpointType.Custom,
},
],
});

networkClientId =
addedNetwork?.rpcEndpoints?.[addedNetwork.defaultRpcEndpointIndex]
?.networkClientId;
}

if (networkClientId) {
await NetworkController.setActiveNetwork(networkClientId);
}

onClose();
};

const switchNetwork = () => {
const handleExistingNetwork = async (
existingNetwork: NetworkConfiguration,
networkId: string,
) => {
const { NetworkController } = Engine.context;
const updatedNetwork = await NetworkController.updateNetwork(
existingNetwork.chainId,
existingNetwork,
existingNetwork.chainId === networkId
? {
replacementSelectedRpcEndpointIndex:
existingNetwork.defaultRpcEndpointIndex,
}
: undefined,
);

const { networkClientId } =
updatedNetwork?.rpcEndpoints?.[updatedNetwork.defaultRpcEndpointIndex] ??
{};

await NetworkController.setActiveNetwork(networkClientId);
};

const handleNewNetwork = async (
networkId: `0x${string}`,
networkRpcUrl: string,
name: string,
nativeCurrency: string,
networkBlockExplorerUrl: string,
) => {
const { NetworkController } = Engine.context;
const networkConfig = {
chainId: networkId,
blockExplorerUrls: networkBlockExplorerUrl
? [networkBlockExplorerUrl]
: [],
defaultRpcEndpointIndex: 0,
defaultBlockExplorerUrlIndex: blockExplorerUrl ? 0 : undefined,
name,
nativeCurrency,
rpcEndpoints: [
{
url: networkRpcUrl,
name,
type: RpcEndpointType.Custom,
},
],
} as AddNetworkFields;

return NetworkController.addNetwork(networkConfig);
};

const handleNavigation = (
onSwitchNetwork: () => void,
networkSwitchPopToWallet: boolean,
) => {
if (onSwitchNetwork) {
onSwitchNetwork();
} else {
networkSwitchPopToWallet
? navigation.navigate('WalletView')
: navigation.goBack();
}
};

const switchNetwork = async () => {
const { NetworkController, CurrencyRateController } = Engine.context;
const url = new URLPARSE(rpcUrl);
const existingNetwork = networkConfigurationByChainId[chainId];

CurrencyRateController.updateExchangeRate(ticker);
!isPrivateConnection(url.hostname) && url.set('protocol', 'https:');
NetworkController.upsertNetworkConfiguration(
{
rpcUrl: url.href,

if (!isPrivateConnection(url.hostname)) {
url.set('protocol', 'https:');
}

if (existingNetwork) {
await handleExistingNetwork(existingNetwork, chainId);
} else {
const addedNetwork = await handleNewNetwork(
chainId,
ticker,
rpcUrl,
nickname,
rpcPrefs: { blockExplorerUrl },
},
{
setActive: true,
// Metrics-related properties required, but the metric event is a no-op
// TODO: Use events for controller metric events
referrer: 'ignored',
source: 'ignored',
},
);
closeModal();
ticker,
blockExplorerUrl,
);
const { networkClientId } =
addedNetwork?.rpcEndpoints?.[addedNetwork.defaultRpcEndpointIndex] ??
{};

NetworkController.setActiveNetwork(networkClientId);
}
onClose();

if (onNetworkSwitch) {
onNetworkSwitch();
handleNavigation(onNetworkSwitch, shouldNetworkSwitchPopToWallet);
} else {
shouldNetworkSwitchPopToWallet
? navigation.navigate('WalletView')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function render(Component: React.ComponentType, chainId?: `0x${string}`) {
chainId: '0x89',
id: 'networkId1',
nickname: 'Polygon Mainnet',
ticker: 'MATIC',
ticker: 'POL',
},
),
},
Expand Down
18 changes: 12 additions & 6 deletions app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,22 @@ function NetworkSwitcher() {
const switchNetwork = useCallback(
(networkConfiguration) => {
const { CurrencyRateController, NetworkController } = Engine.context;
const entry = Object.entries(networkConfigurations).find(
([_a, { chainId }]) => chainId === networkConfiguration.chainId,
const config = Object.values(networkConfigurations).find(
({ chainId }) => chainId === networkConfiguration.chainId,
);

if (entry) {
const [networkConfigurationId] = entry;
const { ticker } = networkConfiguration;
if (config) {
const {
nativeCurrency: ticker,
rpcEndpoints,
defaultRpcEndpointIndex,
} = config;

const { networkClientId } =
rpcEndpoints?.[defaultRpcEndpointIndex] ?? {};

CurrencyRateController.updateExchangeRate(ticker);
NetworkController.setActiveNetwork(networkConfigurationId);
NetworkController.setActiveNetwork(networkClientId);
navigateToGetStarted();
}
},
Expand Down
Loading

0 comments on commit 87b82d9

Please sign in to comment.