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

fix: display new network popup only for accounts that are compatible. #28535

Merged
merged 8 commits into from
Nov 25, 2024
26 changes: 25 additions & 1 deletion shared/lib/multichain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { CaipNamespace, KnownCaipNamespace } from '@metamask/utils';
import {
CaipNamespace,
isCaipChainId,
KnownCaipNamespace,
parseCaipChainId,
} from '@metamask/utils';
import { validate, Network } from 'bitcoin-address-validation';
import { isAddress } from '@solana/addresses';
import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api';

/**
* Returns whether an address is on the Bitcoin mainnet.
Expand Down Expand Up @@ -59,3 +65,21 @@ export function getCaipNamespaceFromAddress(address: string): CaipNamespace {
// Defaults to "Ethereum" for all other cases for now.
return KnownCaipNamespace.Eip155;
}

export function isCurrentChainCompatibleWithAccount(
chainId: string,
account: InternalAccount,
): boolean {
if (!chainId) {
return false;
}

gantunesr marked this conversation as resolved.
Show resolved Hide resolved
if (isCaipChainId(chainId)) {
const { namespace } = parseCaipChainId(chainId);
return namespace === getCaipNamespaceFromAddress(account.address);
}

// For EVM accounts, we do not check the chain ID format, but we just expect it
// to be defined.
return isEvmAccountType(account.type);
}
montelaidev marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 6 additions & 1 deletion ui/pages/routes/routes.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ import NetworkConfirmationPopover from '../../components/multichain/network-list
import NftFullImage from '../../components/app/assets/nfts/nft-details/nft-full-image';
import CrossChainSwap from '../bridge';
import { ToastMaster } from '../../components/app/toast-master/toast-master';
import { InternalAccountPropType } from '../../selectors/multichain';
import { isCurrentChainCompatibleWithAccount } from '../../../shared/lib/multichain';
import {
isCorrectDeveloperTransactionType,
isCorrectSignatureApprovalType,
Expand All @@ -130,6 +132,7 @@ export default class Routes extends Component {
static propTypes = {
currentCurrency: PropTypes.string,
activeTabOrigin: PropTypes.string,
account: InternalAccountPropType,
setCurrentCurrencyToUSD: PropTypes.func,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
Expand Down Expand Up @@ -410,6 +413,7 @@ export default class Routes extends Component {
isNetworkUsed,
allAccountsOnNetworkAreEmpty,
isTestNet,
account,
currentChainId,
shouldShowSeedPhraseReminder,
isCurrentProviderCustom,
Expand Down Expand Up @@ -455,7 +459,8 @@ export default class Routes extends Component {
});
const shouldShowNetworkInfo =
isUnlocked &&
currentChainId &&
account &&
isCurrentChainCompatibleWithAccount(currentChainId, account) &&
montelaidev marked this conversation as resolved.
Show resolved Hide resolved
!isTestNet &&
!isSendRoute &&
!isNetworkUsed &&
Expand Down
109 changes: 108 additions & 1 deletion ui/pages/routes/routes.component.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { act } from '@testing-library/react';
import { act, waitFor } from '@testing-library/react';
import thunk from 'redux-thunk';
import { BtcAccountType } from '@metamask/keyring-api';
import { SEND_STAGES } from '../../ducks/send';
Expand All @@ -15,13 +15,18 @@ import { useIsOriginalNativeTokenSymbol } from '../../hooks/useIsOriginalNativeT
import { createMockInternalAccount } from '../../../test/jest/mocks';
import { CHAIN_IDS } from '../../../shared/constants/network';
import { mockNetworkState } from '../../../test/stub/networks';
import {
MOCK_ACCOUNT_BIP122_P2WPKH,
MOCK_ACCOUNT_EOA,
} from '../../../test/data/mock-accounts';
import useMultiPolling from '../../hooks/useMultiPolling';
import Routes from '.';

const middlewares = [thunk];

const mockShowNetworkDropdown = jest.fn();
const mockHideNetworkDropdown = jest.fn();
const mockFetchWithCache = jest.fn();

jest.mock('webextension-polyfill', () => ({
runtime: {
Expand All @@ -34,6 +39,7 @@ jest.mock('webextension-polyfill', () => ({
}));

jest.mock('../../store/actions', () => ({
...jest.requireActual('../../store/actions'),
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
gasFeeStartPollingByNetworkClientId: jest
.fn()
Expand Down Expand Up @@ -92,6 +98,11 @@ jest.mock(
'../../components/app/metamask-template-renderer/safe-component-list',
);

jest.mock(
'../../../shared/lib/fetch-with-cache',
() => () => mockFetchWithCache,
);

jest.mock('../../hooks/useMultiPolling', () => ({
__esModule: true,
default: jest.fn(),
Expand Down Expand Up @@ -180,6 +191,102 @@ describe('Routes Component', () => {
expect(getByTestId('account-menu-icon')).not.toBeDisabled();
});
});

describe('new network popup', () => {
const mockBtcAccount = MOCK_ACCOUNT_BIP122_P2WPKH;
const mockEvmAccount = MOCK_ACCOUNT_EOA;

const mockNewlyAddedNetwork = {
chainId: CHAIN_IDS.BASE,
name: 'Base',
nativeCurrency: 'ETH',
defaultRpcEndpointIndex: 0,
rpcEndpoints: [
{
type: 'custom',
url: 'https://base.com',
networkClientId: CHAIN_IDS.BASE,
},
],
};

const renderPopup = async (account) => {
// This popup does not show up for tests, so we have to disable this:
process.env.IN_TEST = '';
montelaidev marked this conversation as resolved.
Show resolved Hide resolved
const state = {
...mockSendState,
metamask: {
...mockState.metamask,
completedOnboarding: true,
selectedNetworkClientId: mockNewlyAddedNetwork.chainId,
internalAccounts: {
accounts: {
[account.id]: account,
},
selectedAccount: account.id,
},
usedNetworks: {
'0x1': true,
'0x5': true,
'0x539': true,
[mockNewlyAddedNetwork.chainId]: false,
},
networkConfigurationsByChainId: {
...mockState.metamask.networkConfigurationsByChainId,
[mockNewlyAddedNetwork.chainId]: mockNewlyAddedNetwork,
},
networksMetadata: {
...mockState.metamask.networksMetadata,
[mockNewlyAddedNetwork.chainId]: {
EIPS: {
1559: true,
},
status: 'available',
},
},
tokens: [],
swapsState: { swapsFeatureIsLive: false },
announcements: {},
pendingApprovals: {},
termsOfUseLastAgreed: new Date('2999-03-25'),
shouldShowSeedPhraseReminder: false,
useExternalServices: true,
},
send: {
...mockSendState.send,
stage: SEND_STAGES.INACTIVE,
currentTransactionUUID: null,
draftTransactions: {},
},
appState: {
...mockSendState.appState,
showWhatsNewPopup: false,
onboardedInThisUISession: false,
},
};
return await render(['/'], state);
};

it('displays new EVM network popup for EVM accounts', async () => {
const { getAllByText, getByTestId } = await renderPopup(mockEvmAccount);

const networkInfo = getByTestId('new-network-info__bullet-paragraph');

await waitFor(() => {
expect(getAllByText(mockNewlyAddedNetwork.name).length).toBeGreaterThan(
0,
);
expect(networkInfo).toBeInTheDocument();
});
});

it('does not display new EVM network popup for non-EVM accounts', async () => {
const { queryByTestId } = await renderPopup(mockBtcAccount);

const networkInfo = queryByTestId('new-network-info__bullet-paragraph');
expect(networkInfo).not.toBeInTheDocument();
});
});
});

describe('toast display', () => {
Expand Down
4 changes: 2 additions & 2 deletions ui/pages/routes/routes.container.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import {
getUnapprovedConfirmations,
///: END:ONLY_INCLUDE_IF
getShowExtensionInFullSizeView,
getSelectedAccount,
getSwitchedNetworkDetails,
getNetworkToAutomaticallySwitchTo,
getNumberOfAllUnapprovedTransactionsAndMessages,
getUseRequestQueue,
getCurrentNetwork,
getSelectedInternalAccount,
oldestPendingConfirmationSelector,
getUnapprovedTransactions,
getPendingApprovals,
Expand Down Expand Up @@ -64,7 +64,7 @@ function mapStateToProps(state) {

// If there is more than one connected account to activeTabOrigin,
// *BUT* the current account is not one of them, show the banner
const account = getSelectedAccount(state);
const account = getSelectedInternalAccount(state);
const activeTabOrigin = activeTab?.origin;
const currentNetwork = getCurrentNetwork(state);

Expand Down
Loading