Skip to content

Commit 2be8ec5

Browse files
authored
fix: multichain support on dapp connected site popover cp-13.5.0 (#36881)
<!-- 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 fixes issue with the DaPP permissions icon showing wrong network for Solana only DaPP. <!-- 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? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Fixed a bug with the DaPP permissions icon showing wrong network for Solana only DaPP ## **Related issues** Fixes: #36825 ## **Manual testing steps** 1. Navigate to SOL only dapp (Orca, Jupiter) 2. Connect to DaPP 3. Notice Badge icon ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <img width="398" height="596" alt="Screenshot 2025-10-15 at 16 37 41" src="https://github.com/user-attachments/assets/350edb96-5c1d-4904-8601-c1f04f1a5c8c" /> <!-- [screenshots/recordings] --> ### **After** <img width="398" height="598" alt="Screenshot 2025-10-15 at 16 35 33" src="https://github.com/user-attachments/assets/c212b838-246d-4bb3-8e96-99c4ddeadf88" /> <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/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-extension/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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Refactors active network detection to be multichain-aware (EVM and Solana) via a selector, updates the connected site popover to use it, adds safeguards, and adjusts/extends tests and fixtures. > > - **Selectors**: > - `getDappActiveNetwork`: Derives active network from ordered connected accounts; supports EVM and non‑EVM (Solana) via scopes; returns `{ ...network, isEvm }`; uses `getMultichainNetworkConfigurationsByChainId`. > - Adds use of `getOrderedConnectedAccountsForActiveTab`; updates tests for new logic. > - `getMetaMaskAccountsOrdered`: Safeguards spreading when `address` missing. > - **UI**: > - `ConnectedSitePopover`: Replaces manual domain-based lookup with `getDappActiveNetwork`; removes `useMemo` and unused selectors. > - **Tests**: > - New/updated tests for selector (`ui/selectors/dapp.test.ts`) covering EVM, Solana, and null paths. > - Updates to popover/menu/routes tests to include required state (`domains`, `selectedNetworkClientId`, `keyrings.metadata`, `internalAccounts`, `permissionHistory`, multichain configs/accounts). > - **Test data**: > - Extend fixtures with additional accounts/addresses and keyring metadata in `mock-send-state.json` and onboarding data. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 98c125e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 281f7b7 commit 2be8ec5

File tree

9 files changed

+267
-71
lines changed

9 files changed

+267
-71
lines changed

test/data/mock-send-state.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,22 @@
407407
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
408408
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823",
409409
"balance": "0x0"
410+
},
411+
"0xd5e099c71b797516c10ed0f0d895f429c2781111": {
412+
"address": "0xd5e099c71b797516c10ed0f0d895f429c2781111",
413+
"balance": "0x0"
414+
},
415+
"0x1234567890123456789012345678901234567890": {
416+
"address": "0x1234567890123456789012345678901234567890",
417+
"balance": "0x0"
418+
},
419+
"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq": {
420+
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
421+
"balance": "0x0"
422+
},
423+
"2byhg1jregmqQx2VfLGLn7hb5mStJw2iVVU8sfM5xTYj": {
424+
"address": "2byhg1jregmqQx2VfLGLn7hb5mStJw2iVVU8sfM5xTYj",
425+
"balance": "0x0"
410426
}
411427
},
412428
"tokens": [

test/integration/data/onboarding-completion-route.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,10 @@
189189
"keyrings": [
190190
{
191191
"type": "HD Key Tree",
192-
"accounts": ["0x03cf1158b58ccdfc04dd518f11f85c3ee7fa0189"]
192+
"accounts": ["0x03cf1158b58ccdfc04dd518f11f85c3ee7fa0189"],
193+
"metadata": {
194+
"id": "test-keyring-id"
195+
}
193196
}
194197
],
195198
"knownMethodData": {},

ui/components/multichain/connected-site-menu/connected-site-menu.test.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,31 @@ describe('Connected Site Menu', () => {
142142
},
143143
},
144144
},
145-
domains: {},
145+
domains: {
146+
'https://uniswap.org/': 'mainnet-infura',
147+
},
146148
networkConfigurationsByChainId: defaultNetworkConfigurations,
149+
selectedNetworkClientId: 'mainnet-infura',
150+
keyrings: [
151+
{
152+
type: 'HD Key Tree',
153+
accounts: [mockAccount1.address, mockAccount2.address],
154+
metadata: {
155+
id: 'test-keyring-id',
156+
},
157+
},
158+
],
159+
// Add multichain network state
160+
selectedMultichainNetworkChainId: 'eip155:1',
161+
isEvmSelected: true,
162+
// Add permission history for the selector
163+
permissionHistory: {
164+
'https://uniswap.org/': {
165+
eth_accounts: {
166+
accounts: [`eip155:1:${mockAccount1.address}`],
167+
},
168+
},
169+
},
147170
...customState.metamask,
148171
};
149172

ui/components/multichain/connected-site-popover/connected-site-popover.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const render = () => {
4141
// Add multichain network state
4242
selectedMultichainNetworkChainId: 'eip155:5',
4343
isEvmSelected: true,
44+
selectedNetworkClientId: 'goerli-test-client',
4445
multichainNetworkConfigurationsByChainId: {
4546
...mockState.metamask.multichainNetworkConfigurationsByChainId,
4647
'eip155:5': {
@@ -50,6 +51,41 @@ const render = () => {
5051
isEvm: true,
5152
},
5253
},
54+
// Add internal accounts for the new selector
55+
internalAccounts: {
56+
accounts: {
57+
'eip155:5:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
58+
id: 'eip155:5:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
59+
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
60+
type: 'eip155:eoa',
61+
metadata: {
62+
name: 'Test Account',
63+
lastSelected: Date.now(),
64+
},
65+
scopes: ['eip155:5'],
66+
methods: [],
67+
options: {},
68+
},
69+
},
70+
selectedAccount: 'eip155:5:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
71+
},
72+
// Add accounts for compatibility
73+
accounts: {
74+
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
75+
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
76+
balance: '0x0',
77+
},
78+
},
79+
// Add keyrings for compatibility
80+
keyrings: [
81+
{
82+
type: 'HD Key Tree',
83+
accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
84+
metadata: {
85+
id: 'test-keyring-id',
86+
},
87+
},
88+
],
5389
// Add permissions for the test dapp
5490
subjects: {
5591
'https://metamask.github.io': {

ui/components/multichain/connected-site-popover/connected-site-popover.tsx

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, RefObject, useMemo } from 'react';
1+
import React, { useContext, RefObject } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33
import { parseCaipChainId } from '@metamask/utils';
44
import {
@@ -20,11 +20,11 @@ import {
2020
TextVariant,
2121
} from '../../../helpers/constants/design-system';
2222
import { I18nContext } from '../../../contexts/i18n';
23-
import { getOriginOfCurrentTab, getAllDomains } from '../../../selectors';
24-
import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks';
23+
import { getOriginOfCurrentTab } from '../../../selectors';
2524
import { getURLHost } from '../../../helpers/utils/util';
2625
import { getImageForChainId } from '../../../selectors/multichain';
2726
import { toggleNetworkMenu } from '../../../store/actions';
27+
import { getDappActiveNetwork } from '../../../selectors/dapp';
2828

2929
type ConnectedSitePopoverProps = {
3030
referenceElement: RefObject<HTMLElement>;
@@ -43,37 +43,10 @@ export const ConnectedSitePopover: React.FC<ConnectedSitePopoverProps> = ({
4343
}) => {
4444
const t = useContext(I18nContext);
4545
const activeTabOrigin = useSelector(getOriginOfCurrentTab);
46+
const dappActiveNetwork = useSelector(getDappActiveNetwork);
4647
const siteName = getURLHost(activeTabOrigin);
47-
const allDomains = useSelector(getAllDomains);
48-
const networkConfigurationsByChainId = useSelector(
49-
getNetworkConfigurationsByChainId,
50-
);
5148
const dispatch = useDispatch();
5249

53-
// Get the network that this dapp is actually connected to using domain mapping
54-
const dappActiveNetwork = useMemo(() => {
55-
if (!activeTabOrigin || !allDomains) {
56-
return null;
57-
}
58-
59-
// Get the networkClientId for this domain
60-
const networkClientId = allDomains[activeTabOrigin];
61-
if (!networkClientId) {
62-
return null;
63-
}
64-
65-
// Find the network configuration that has this networkClientId
66-
const networkConfiguration = Object.values(
67-
networkConfigurationsByChainId,
68-
).find((network) => {
69-
return network.rpcEndpoints.some(
70-
(rpcEndpoint) => rpcEndpoint.networkClientId === networkClientId,
71-
);
72-
});
73-
74-
return networkConfiguration || null;
75-
}, [activeTabOrigin, allDomains, networkConfigurationsByChainId]);
76-
7750
const getChainIdForImage = (chainId: `${string}:${string}`): string => {
7851
const { namespace, reference } = parseCaipChainId(chainId);
7952
return namespace === 'eip155'

ui/pages/routes/routes.component.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ describe('Routes Component', () => {
172172
tokenBalances: {
173173
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': '0x176270e2b862e4ed3',
174174
},
175+
permissionHistory: {
176+
'https://metamask.github.io': {
177+
eth_accounts: {
178+
accounts: [
179+
'eip155:1:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
180+
],
181+
},
182+
},
183+
},
175184
},
176185
send: {
177186
...mockSendState.send,
@@ -312,6 +321,25 @@ describe('toast display', () => {
312321
},
313322
selectedAccount: selectedAccountId ?? mockAccount.id,
314323
},
324+
accounts: {
325+
...mockState.metamask.accounts,
326+
[mockAccount.address]: {
327+
balance: '0x0',
328+
address: mockAccount.address,
329+
},
330+
[mockAccount2.address]: {
331+
balance: '0x0',
332+
address: mockAccount2.address,
333+
},
334+
[mockNonEvmAccount.address]: {
335+
balance: '0x0',
336+
address: mockNonEvmAccount.address,
337+
},
338+
[mockSolanaAccount.address]: {
339+
balance: '0x0',
340+
address: mockSolanaAccount.address,
341+
},
342+
},
315343
accountsAssets: {
316344
[selectedAccountId ?? mockAccount.id]: [],
317345
},

ui/selectors/dapp.test.ts

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import {
44
} from '@metamask/network-controller';
55
import { getNetworkConfigurationsByChainId } from '../../shared/modules/selectors/networks';
66
import { getDappActiveNetwork } from './dapp';
7-
import { getAllDomains, getOriginOfCurrentTab } from './selectors';
7+
import {
8+
getOrderedConnectedAccountsForActiveTab,
9+
getOriginOfCurrentTab,
10+
getAllDomains,
11+
} from './selectors';
12+
import { getMultichainNetworkConfigurationsByChainId } from './multichain';
813

14+
// Mock the selectors that the new getDappActiveNetwork uses
915
jest.mock('./selectors', () => ({
16+
getOrderedConnectedAccountsForActiveTab: jest.fn(),
1017
getOriginOfCurrentTab: jest.fn(),
1118
getAllDomains: jest.fn(),
1219
}));
@@ -15,11 +22,21 @@ jest.mock('../../shared/modules/selectors/networks', () => ({
1522
getNetworkConfigurationsByChainId: jest.fn(),
1623
}));
1724

25+
jest.mock('./multichain', () => ({
26+
getMultichainNetworkConfigurationsByChainId: jest.fn(),
27+
}));
28+
29+
const mockGetOrderedConnectedAccountsForActiveTab = jest.mocked(
30+
getOrderedConnectedAccountsForActiveTab,
31+
);
1832
const mockGetOriginOfCurrentTab = jest.mocked(getOriginOfCurrentTab);
1933
const mockGetAllDomains = jest.mocked(getAllDomains);
2034
const mockGetNetworkConfigurationsByChainId = jest.mocked(
2135
getNetworkConfigurationsByChainId,
2236
);
37+
const mockGetMultichainNetworkConfigurationsByChainId = jest.mocked(
38+
getMultichainNetworkConfigurationsByChainId,
39+
);
2340

2441
describe('getDappActiveNetwork selector', () => {
2542
beforeEach(() => {
@@ -43,57 +60,119 @@ describe('getDappActiveNetwork selector', () => {
4360
nativeCurrency: '',
4461
};
4562

63+
const mockEvmAccount = {
64+
id: 'eip155:1:0x1234567890123456789012345678901234567890',
65+
address: '0x1234567890123456789012345678901234567890',
66+
type: 'eip155:eoa',
67+
metadata: {
68+
name: 'Test Account',
69+
lastSelected: Date.now(),
70+
},
71+
scopes: ['eip155:1'],
72+
methods: [],
73+
options: {},
74+
};
75+
76+
const mockSolanaAccount = {
77+
id: 'solana:mainnet:0x1234567890123456789012345678901234567890',
78+
address: '0x1234567890123456789012345678901234567890',
79+
type: 'solana:data-account',
80+
metadata: {
81+
name: 'Test Solana Account',
82+
lastSelected: Date.now(),
83+
},
84+
scopes: ['solana:mainnet'],
85+
methods: [],
86+
options: {},
87+
};
88+
89+
const mockMultichainNetworkConfig: NetworkConfiguration = {
90+
chainId: '0x1' as `0x${string}`,
91+
name: 'Solana Mainnet',
92+
nativeCurrency: 'SOL',
93+
blockExplorerUrls: [],
94+
defaultRpcEndpointIndex: 0,
95+
rpcEndpoints: [
96+
{
97+
networkClientId: 'solana-mainnet',
98+
type: RpcEndpointType.Custom,
99+
url: '',
100+
},
101+
],
102+
};
103+
46104
const arrangeMocks = () => {
105+
mockGetOrderedConnectedAccountsForActiveTab.mockReturnValue([
106+
mockEvmAccount,
107+
]);
47108
mockGetOriginOfCurrentTab.mockReturnValue(mockOrigin);
48-
49109
mockGetAllDomains.mockReturnValue({
50110
[mockOrigin]: mockNetworkClientId,
51111
});
52-
53112
mockGetNetworkConfigurationsByChainId.mockReturnValue({
54113
'0x1': mockNetworkConfig,
55114
});
115+
mockGetMultichainNetworkConfigurationsByChainId.mockReturnValue({});
56116

57117
return {
58118
mockOrigin,
59119
mockNetworkClientId,
60-
mockGetAllDomains,
120+
mockGetOrderedConnectedAccountsForActiveTab,
61121
mockGetOriginOfCurrentTab,
122+
mockGetAllDomains,
62123
mockGetNetworkConfigurationsByChainId,
124+
mockGetMultichainNetworkConfigurationsByChainId,
63125
mockState: {},
64126
};
65127
};
66128

67-
it('returns correct network configuration when all data is available', () => {
129+
it('returns correct EVM network configuration when all data is available', () => {
130+
const mocks = arrangeMocks();
131+
const result = getDappActiveNetwork(mocks.mockState);
132+
expect(result).toEqual({ ...mockNetworkConfig, isEvm: true });
133+
});
134+
135+
it('returns correct non-EVM network configuration for Solana account', () => {
68136
const mocks = arrangeMocks();
137+
mocks.mockGetOrderedConnectedAccountsForActiveTab.mockReturnValue([
138+
mockSolanaAccount,
139+
]);
140+
mocks.mockGetMultichainNetworkConfigurationsByChainId.mockReturnValue({
141+
'solana:mainnet': mockMultichainNetworkConfig,
142+
});
143+
69144
const result = getDappActiveNetwork(mocks.mockState);
70-
expect(result).toEqual(mockNetworkConfig);
145+
expect(result).toEqual({ ...mockMultichainNetworkConfig, isEvm: false });
71146
});
72147

73-
it('returns null when activeTabOrigin is null', () => {
148+
it('returns null when no connected accounts', () => {
74149
const mocks = arrangeMocks();
75-
mocks.mockGetOriginOfCurrentTab.mockReturnValue(null);
150+
mocks.mockGetOrderedConnectedAccountsForActiveTab.mockReturnValue([]);
76151
const result = getDappActiveNetwork(mocks.mockState);
77152
expect(result).toBeNull();
78153
});
79154

80-
it('returns null when allDomains is null', () => {
155+
it('returns null when orderedConnectedAccounts is null', () => {
81156
const mocks = arrangeMocks();
82-
mocks.mockGetAllDomains.mockReturnValue(null);
157+
mocks.mockGetOrderedConnectedAccountsForActiveTab.mockReturnValue(null);
83158
const result = getDappActiveNetwork(mocks.mockState);
84159
expect(result).toBeNull();
85160
});
86161

87-
it('returns null when networkClientId not found for origin', () => {
162+
it('returns null when no matching EVM network configuration exists', () => {
88163
const mocks = arrangeMocks();
89-
mocks.mockGetAllDomains.mockReturnValue({});
164+
mocks.mockGetNetworkConfigurationsByChainId.mockReturnValue({});
90165
const result = getDappActiveNetwork(mocks.mockState);
91166
expect(result).toBeNull();
92167
});
93168

94-
it('returns null when no matching network configuration exists', () => {
169+
it('returns null when no matching non-EVM network configuration exists', () => {
95170
const mocks = arrangeMocks();
96-
mocks.mockGetNetworkConfigurationsByChainId.mockReturnValue({});
171+
mocks.mockGetOrderedConnectedAccountsForActiveTab.mockReturnValue([
172+
mockSolanaAccount,
173+
]);
174+
mocks.mockGetMultichainNetworkConfigurationsByChainId.mockReturnValue({});
175+
97176
const result = getDappActiveNetwork(mocks.mockState);
98177
expect(result).toBeNull();
99178
});

0 commit comments

Comments
 (0)