Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chain permissions - Next Steps explainer/PoC #11868

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const NetworkSelectorList = ({
onSelectNetwork,
networks = [],
isLoading = false,
selectedNetworkIds,
selectedChainIds,
isMultiSelect = true,
renderRightAccessory,
isSelectionDisabled,
Expand All @@ -33,7 +33,6 @@ const NetworkSelectorList = ({
}: NetworkConnectMultiSelectorProps) => {
const networksLengthRef = useRef<number>(0);
const { styles } = useStyles(styleSheet, {});

/**
* Ref for the FlatList component.
* The type of the ref is not explicitly defined.
Expand All @@ -51,8 +50,8 @@ const NetworkSelectorList = ({
? CellVariant.MultiSelect
: CellVariant.Select;
let isSelectedNetwork = isSelected;
if (selectedNetworkIds) {
isSelectedNetwork = selectedNetworkIds.includes(id);
if (selectedChainIds) {
isSelectedNetwork = selectedChainIds.includes(id);
}

return (
Expand All @@ -76,7 +75,7 @@ const NetworkSelectorList = ({
},
[
isLoading,
selectedNetworkIds,
selectedChainIds,
renderRightAccessory,
isSelectionDisabled,
onSelectNetwork,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface NetworkConnectMultiSelectorProps {
onSelectNetwork?: (id: string, isSelected: boolean) => void;
networks?: Network[];
isLoading?: boolean;
selectedNetworkIds?: string[];
selectedChainIds?: string[];
isMultiSelect?: boolean;
renderRightAccessory?: (id: string, name: string) => React.ReactNode;
isSelectionDisabled?: boolean;
Expand Down
14 changes: 11 additions & 3 deletions app/components/Views/AccountPermissions/AccountPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ const AccountPermissions = (props: AccountPermissionsProps) => {
const activeAddress: string = permittedAccountsByHostname[0];

const [userIntent, setUserIntent] = useState(USER_INTENT.None);
const [networkSelectorUserIntent, setNetworkSelectorUserIntent] = useState(
USER_INTENT.None,
);

const hideSheet = useCallback(
(callback?: () => void) =>
Expand Down Expand Up @@ -371,8 +374,13 @@ const AccountPermissions = (props: AccountPermissionsProps) => {
]);

useEffect(() => {
if (userIntent === USER_INTENT.None) return;
if (networkSelectorUserIntent === USER_INTENT.Confirm) {
hideSheet();
}
}, [networkSelectorUserIntent, hideSheet]);

useEffect(() => {
if (userIntent === USER_INTENT.None) return;
const handleUserActions = (action: USER_INTENT) => {
switch (action) {
case USER_INTENT.Confirm: {
Expand Down Expand Up @@ -586,7 +594,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => {
<NetworkConnectMultiSelector
onSelectNetworkIds={setSelectedAddresses}
isLoading={isLoading}
onUserAction={setUserIntent}
onUserAction={setNetworkSelectorUserIntent}
urlWithProtocol={urlWithProtocol}
hostname={hostname}
onBack={() =>
Expand All @@ -597,7 +605,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => {
),
[
isLoading,
setUserIntent,
setNetworkSelectorUserIntent,
urlWithProtocol,
hostname,
isRenderedAsBottomSheet,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Third party dependencies.
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import { Platform, SafeAreaView, View } from 'react-native';
import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { NetworkConfiguration } from '@metamask/network-controller';

// External dependencies.
import { strings } from '../../../../../locales/i18n';
Expand All @@ -10,14 +13,12 @@ import Button, {
ButtonVariants,
} from '../../../../component-library/components/Buttons/Button';
import SheetHeader from '../../../../component-library/components/Sheet/SheetHeader';
import { useNavigation } from '@react-navigation/native';

import { useStyles } from '../../../../component-library/hooks';
import { USER_INTENT } from '../../../../constants/permissions';
import HelpText, {
HelpTextSeverity,
} from '../../../../component-library/components/Form/HelpText';
import { Network } from '../../../../components/UI/NetworkSelectorList/NetworkSelectorList.types';

// Internal dependencies.
import ConnectNetworkModalSelectorsIDs from '../../../../../e2e/selectors/Modals/ConnectNetworkModal.selectors';
Expand All @@ -26,7 +27,10 @@ import { NetworkConnectMultiSelectorProps } from './NetworkConnectMultiSelector.
import Routes from '../../../../constants/navigation/Routes';
import Checkbox from '../../../../component-library/components/Checkbox';
import NetworkSelectorList from '../../../UI/NetworkSelectorList/NetworkSelectorList';
import { PopularList } from '../../../../util/networks/customNetworks';
import { selectNetworkConfigurations } from '../../../../selectors/networkController';
import Engine from '../../../../core/Engine';
import { PermissionKeys } from '../../../../core/Permissions/specifications';
import { CaveatTypes } from '../../../../core/Permissions/constants';

const NetworkConnectMultiSelector = ({
isLoading,
Expand All @@ -38,34 +42,95 @@ const NetworkConnectMultiSelector = ({
}: NetworkConnectMultiSelectorProps) => {
const { styles } = useStyles(styleSheet, { isRenderedAsBottomSheet });
const { navigate } = useNavigation();
const [selectedNetworkIds, setSelectedNetworkIds] = useState<string[]>([]);
const [selectedChainIds, setSelectedChainIds] = useState<string[]>([]);
const networkConfigurations = useSelector(selectNetworkConfigurations);

Copy link
Contributor

@EtherWizard33 EtherWizard33 Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adonesky1 Engine.context.PermissionController.getCaveat came from this POC PR originally

useEffect(() => {
let currentlyPermittedChains;
try {
currentlyPermittedChains = Engine.context.PermissionController.getCaveat(
hostname,
PermissionKeys.permittedChains,
CaveatTypes.restrictNetworkSwitching,
);
} catch (e) {
// noop
}

const mockNetworks: Network[] = PopularList.map((network) => ({
id: network.chainId,
name: network.nickname,
rpcUrl: network.rpcUrl,
isSelected: false,
imageSource: network.rpcPrefs.imageSource,
}));
setSelectedChainIds(currentlyPermittedChains?.value || []);
}, [hostname]);

const handleUpdateNetworkPermissions = useCallback(async () => {
let hasPermittedChains = false;
try {
hasPermittedChains = Engine.context.PermissionController.hasCaveat(
hostname,
PermissionKeys.permittedChains,
CaveatTypes.restrictNetworkSwitching,
);
} catch {
// noop
}
if (hasPermittedChains) {
Engine.context.PermissionController.updateCaveat(
hostname,
PermissionKeys.permittedChains,
CaveatTypes.restrictNetworkSwitching,
selectedChainIds,
);
} else {
Engine.context.PermissionController.grantPermissionsIncremental({
subject: {
origin: hostname,
},
approvedPermissions: {
[PermissionKeys.permittedChains]: {
caveats: [
{
type: CaveatTypes.restrictNetworkSwitching,
value: selectedChainIds,
},
],
},
},
});
}
onUserAction(USER_INTENT.Confirm);
}, [selectedChainIds, hostname, onUserAction]);
const networks = Object.entries(networkConfigurations).map(
([key, network]: [string, NetworkConfiguration]) => ({
id: key,
name: network.name,
rpcUrl: network.rpcEndpoints[network.defaultRpcEndpointIndex].url,
isSelected: false,
imageSource: '', // TODO not sure where to get image sources
}),
);

const onSelectNetwork = useCallback(
(clickedNetworkId) => {
const selectedAddressIndex = selectedNetworkIds.indexOf(clickedNetworkId);
(clickedChainId) => {
const selectedAddressIndex = selectedChainIds.indexOf(clickedChainId);
// Reconstruct selected network ids.
const newNetworkList = mockNetworks.reduce((acc, { id }) => {
if (clickedNetworkId === id) {
const newNetworkList = networks.reduce((acc, { id }) => {
if (clickedChainId === id) {
selectedAddressIndex === -1 && acc.push(id);
} else if (selectedNetworkIds.includes(id)) {
} else if (selectedChainIds.includes(id)) {
acc.push(id);
}
return acc;
}, [] as string[]);
setSelectedNetworkIds(newNetworkList);
setSelectedChainIds(newNetworkList);
},
[mockNetworks, selectedNetworkIds],
[networks, selectedChainIds],
);

const toggleRevokeAllNetworkPermissionsModal = useCallback(() => {
// not sure if we want to do this here or on the sub modal
// which provides the extra warning that it will fully disconnect you
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct

Engine.context.PermissionController.revokePermissions({
[hostname]: [PermissionKeys.permittedChains, PermissionKeys.eth_accounts],
});

navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS,
params: {
Expand All @@ -76,28 +141,28 @@ const NetworkConnectMultiSelector = ({
},
},
});
}, [navigate, urlWithProtocol]);
}, [navigate, urlWithProtocol, hostname]);

const areAllNetworksSelected = mockNetworks
const areAllNetworksSelected = networks
.map(({ id }) => id)
.every((id) => selectedNetworkIds.includes(id));
.every((id) => selectedChainIds?.includes(id));

const areAnyNetworksSelected = selectedNetworkIds?.length !== 0;
const areNoNetworksSelected = selectedNetworkIds?.length === 0;
const areAnyNetworksSelected = selectedChainIds?.length !== 0;
const areNoNetworksSelected = selectedChainIds?.length === 0;

const renderSelectAllCheckbox = useCallback((): React.JSX.Element | null => {
const areSomeNetworksSelectedButNotAll =
areAnyNetworksSelected && !areAllNetworksSelected;

const selectAll = () => {
if (isLoading) return;
const allSelectedNetworkIds = mockNetworks.map(({ id }) => id);
setSelectedNetworkIds(allSelectedNetworkIds);
const allSelectedChainIds = networks.map(({ id }) => id);
setSelectedChainIds(allSelectedChainIds);
};

const unselectAll = () => {
if (isLoading) return;
setSelectedNetworkIds([]);
setSelectedChainIds([]);
};

const onPress = () => {
Expand All @@ -118,14 +183,14 @@ const NetworkConnectMultiSelector = ({
}, [
areAllNetworksSelected,
areAnyNetworksSelected,
mockNetworks,
networks,
isLoading,
setSelectedNetworkIds,
setSelectedChainIds,
styles.selectAllContainer,
]);

const renderCtaButtons = useCallback(() => {
const isConnectDisabled = Boolean(!selectedNetworkIds.length) || isLoading;
const isConnectDisabled = Boolean(!selectedChainIds.length) || isLoading;

return (
<View style={styles.buttonsContainer}>
Expand All @@ -134,7 +199,7 @@ const NetworkConnectMultiSelector = ({
<Button
variant={ButtonVariants.Primary}
label={strings('networks.update')}
onPress={() => onUserAction(USER_INTENT.Confirm)}
onPress={handleUpdateNetworkPermissions}
size={ButtonSize.Lg}
style={{
...styles.buttonPositioning,
Expand Down Expand Up @@ -174,10 +239,10 @@ const NetworkConnectMultiSelector = ({
</View>
);
}, [
handleUpdateNetworkPermissions,
areAnyNetworksSelected,
isLoading,
onUserAction,
selectedNetworkIds,
selectedChainIds,
styles,
areNoNetworksSelected,
hostname,
Expand All @@ -194,19 +259,19 @@ const NetworkConnectMultiSelector = ({
/>
<View style={styles.bodyContainer}>{renderSelectAllCheckbox()}</View>
<NetworkSelectorList
networks={mockNetworks}
selectedNetworkIds={selectedNetworkIds}
networks={networks}
selectedChainIds={selectedChainIds}
onSelectNetwork={onSelectNetwork}
></NetworkSelectorList>
<View style={styles.bodyContainer}>{renderCtaButtons()}</View>
</View>
</SafeAreaView>
),
[
mockNetworks,
networks,
onSelectNetwork,
renderCtaButtons,
selectedNetworkIds,
selectedChainIds,
styles.bodyContainer,
styles.bottomSheetContainer,
onBack,
Expand Down
2 changes: 1 addition & 1 deletion app/core/RPCMethods/lib/ethereum-chain-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export async function switchToNetwork({
symbol: networkConfiguration?.ticker || 'ETH',
...analytics,
};

console.log('ALEX LOGGING: networkConfiguration', requestData);
// for some reason this extra step is necessary for accessing the env variable in test environment
const chainPermissionsFeatureEnabled =
{ ...process.env }?.NODE_ENV === 'test'
Expand Down
1 change: 0 additions & 1 deletion app/core/RPCMethods/wallet_switchEthereumChain.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ const wallet_switchEthereumChain = async ({
res.result = null;
return;
}

const analyticsParams = await switchToNetwork({
network: existingNetwork,
chainId: _chainId,
Expand Down
5 changes: 2 additions & 3 deletions app/selectors/networkController.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { createSelector } from 'reselect';
import { RootState } from '../reducers';
import { InfuraNetworkType } from '@metamask/controller-utils';
import {
BuiltInNetworkClientId,
CustomNetworkClientId,
NetworkConfiguration,
NetworkState,
RpcEndpointType,
} from '@metamask/network-controller';
import { RootState } from '../reducers';
import { createDeepEqualSelector } from './util';
import { NETWORKS_CHAIN_ID } from '../constants/network';
import { InfuraNetworkType } from '@metamask/controller-utils';

interface InfuraRpcEndpoint {
name?: string;
Expand Down Expand Up @@ -156,4 +156,3 @@ export const selectNetworkClientId = createSelector(
(networkControllerState: NetworkState) =>
networkControllerState.selectedNetworkClientId,
);

Loading