Skip to content

Commit 5739a10

Browse files
committed
When switching network alert the user and get approval if there are pending requests from origin.
1 parent bb8e799 commit 5739a10

15 files changed

+203
-13
lines changed

app/_locales/en/messages.json

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/_locales/en_GB/messages.json

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js

+1
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ async function addEthereumChainHandler(
188188
updatedNetwork.rpcEndpoints[updatedNetwork.defaultRpcEndpointIndex];
189189

190190
return switchChain(res, end, chainId, networkClientId, {
191+
isAddFlow: true,
191192
autoApprove: shouldAddOrUpdateNetwork,
192193
setActiveNetwork,
193194
getCaveat,

app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const createMockedHandler = () => {
6363
const next = jest.fn();
6464
const end = jest.fn();
6565
const mocks = {
66+
isAddFlow: true,
6667
getCurrentChainIdForDomain: jest.fn().mockReturnValue(NON_INFURA_CHAIN_ID),
6768
setNetworkClientIdForDomain: jest.fn(),
6869
getNetworkConfigurationByChainId: jest.fn(),
@@ -187,6 +188,7 @@ describe('addEthereumChainHandler', () => {
187188
{
188189
autoApprove: true,
189190
getCaveat: mocks.getCaveat,
191+
isAddFlow: true,
190192
setActiveNetwork: mocks.setActiveNetwork,
191193
requestPermittedChainsPermissionForOrigin:
192194
mocks.requestPermittedChainsPermissionForOrigin,
@@ -255,6 +257,7 @@ describe('addEthereumChainHandler', () => {
255257
{
256258
autoApprove: true,
257259
getCaveat: mocks.getCaveat,
260+
isAddFlow: true,
258261
setActiveNetwork: mocks.setActiveNetwork,
259262
requestPermittedChainsPermissionForOrigin:
260263
mocks.requestPermittedChainsPermissionForOrigin,
@@ -304,6 +307,7 @@ describe('addEthereumChainHandler', () => {
304307
{
305308
autoApprove: false,
306309
getCaveat: mocks.getCaveat,
310+
isAddFlow: true,
307311
setActiveNetwork: mocks.setActiveNetwork,
308312
requestPermittedChainsPermissionForOrigin:
309313
mocks.requestPermittedChainsPermissionForOrigin,

app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js

+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ApprovalType } from '@metamask/controller-utils';
12
import { rpcErrors } from '@metamask/rpc-errors';
23
import {
34
Caip25CaveatType,
@@ -165,13 +166,19 @@ export function validateAddEthereumChainParams(params) {
165166
* @param {string} chainId - The chainId being switched to.
166167
* @param {string} networkClientId - The network client being switched to.
167168
* @param {object} hooks - The hooks object.
169+
* @param {string} hooks.origin - The origin sending this request.
170+
* @param {boolean} hooks.isAddFlow - Variable to check if its add flow.
168171
* @param {boolean} [hooks.autoApprove] - A boolean indicating whether the request should prompt the user or be automatically approved.
169172
* @param {Function} hooks.setActiveNetwork - The callback to change the current network for the origin.
170173
* @param {Function} hooks.getCaveat - The callback to get the CAIP-25 caveat for the origin.
171174
* @param {Function} hooks.requestPermittedChainsPermissionForOrigin - The callback to request a new permittedChains-equivalent CAIP-25 permission.
172175
* @param {Function} hooks.requestPermittedChainsPermissionIncrementalForOrigin - The callback to add a new chain to the permittedChains-equivalent CAIP-25 permission.
173176
* @param {Function} hooks.setTokenNetworkFilter - The callback to set the token network filter.
174177
* @param {Function} hooks.rejectApprovalRequestsForOrigin - The callback to reject all pending approval requests for the origin.
178+
* @param {Function} hooks.requestUserApproval - The callback to trigger user approval flow.
179+
* @param {Function} hooks.hasApprovalRequestsForOrigin - Function to check if there are pending approval requests from the origin.
180+
* @param {object} hooks.toNetworkConfiguration - Network configutation of network switching to.
181+
* @param {object} hooks.fromNetworkConfiguration - Network configutation of network switching from.
175182
* @returns a null response on success or an error if user rejects an approval when autoApprove is false or on unexpected errors.
176183
*/
177184
export async function switchChain(
@@ -180,13 +187,19 @@ export async function switchChain(
180187
chainId,
181188
networkClientId,
182189
{
190+
origin,
191+
isAddFlow,
183192
autoApprove,
184193
setActiveNetwork,
185194
getCaveat,
186195
requestPermittedChainsPermissionForOrigin,
187196
requestPermittedChainsPermissionIncrementalForOrigin,
188197
setTokenNetworkFilter,
189198
rejectApprovalRequestsForOrigin,
199+
requestUserApproval,
200+
hasApprovalRequestsForOrigin,
201+
toNetworkConfiguration,
202+
fromNetworkConfiguration,
190203
},
191204
) {
192205
try {
@@ -203,6 +216,19 @@ export async function switchChain(
203216
chainId,
204217
autoApprove,
205218
});
219+
} else if (
220+
hasApprovalRequestsForOrigin &&
221+
hasApprovalRequestsForOrigin() &&
222+
!isAddFlow
223+
) {
224+
await requestUserApproval({
225+
origin,
226+
type: ApprovalType.SwitchEthereumChain,
227+
requestData: {
228+
toNetworkConfiguration,
229+
fromNetworkConfiguration,
230+
},
231+
});
206232
}
207233
} else {
208234
await requestPermittedChainsPermissionForOrigin({

app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Multichain from '@metamask/multichain';
12
import { errorCodes, rpcErrors } from '@metamask/rpc-errors';
23
import {
34
Caip25CaveatType,
@@ -10,13 +11,19 @@ describe('Ethereum Chain Utils', () => {
1011
const createMockedSwitchChain = () => {
1112
const end = jest.fn();
1213
const mocks = {
14+
origin: 'www.test.com',
15+
isAddFlow: false,
1316
autoApprove: false,
1417
setActiveNetwork: jest.fn(),
1518
getCaveat: jest.fn(),
1619
requestPermittedChainsPermissionForOrigin: jest.fn(),
1720
requestPermittedChainsPermissionIncrementalForOrigin: jest.fn(),
1821
setTokenNetworkFilter: jest.fn(),
1922
rejectApprovalRequestsForOrigin: jest.fn(),
23+
requestUserApproval: jest.fn(),
24+
hasApprovalRequestsForOrigin: jest.fn(),
25+
toNetworkConfiguration: {},
26+
fromNetworkConfiguration: {},
2027
};
2128
const response: { result?: true } = {};
2229
const switchChain = (chainId: Hex, networkClientId: string) =>
@@ -124,6 +131,25 @@ describe('Ethereum Chain Utils', () => {
124131
expect(mocks.setTokenNetworkFilter).toHaveBeenCalledWith('0x1');
125132
});
126133

134+
it('check for user approval is user already has access on the chain', async () => {
135+
jest
136+
.spyOn(Multichain, 'getPermittedEthChainIds')
137+
.mockReturnValue(['0x1']);
138+
const { mocks, switchChain } = createMockedSwitchChain();
139+
mocks.hasApprovalRequestsForOrigin.mockReturnValue(true);
140+
mocks.autoApprove = false;
141+
mocks.getCaveat.mockReturnValue({
142+
value: {
143+
requiredScopes: {},
144+
optionalScopes: {},
145+
isMultichainOrigin: false,
146+
},
147+
});
148+
await switchChain('0x1', 'testnet');
149+
150+
expect(mocks.requestUserApproval).toHaveBeenCalledTimes(1);
151+
});
152+
127153
it('should throw errors if the permittedChains grant fails', async () => {
128154
const { mocks, end, switchChain } = createMockedSwitchChain();
129155
mocks.requestPermittedChainsPermissionIncrementalForOrigin.mockRejectedValueOnce(

app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js

+16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { providerErrors } from '@metamask/rpc-errors';
2+
23
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
34
import {
45
validateSwitchEthereumChainParams,
@@ -11,11 +12,13 @@ const switchEthereumChain = {
1112
hookNames: {
1213
getNetworkConfigurationByChainId: true,
1314
setActiveNetwork: true,
15+
requestUserApproval: true,
1416
getCaveat: true,
1517
getCurrentChainIdForDomain: true,
1618
requestPermittedChainsPermissionForOrigin: true,
1719
requestPermittedChainsPermissionIncrementalForOrigin: true,
1820
setTokenNetworkFilter: true,
21+
hasApprovalRequestsForOrigin: true,
1922
},
2023
};
2124

@@ -29,11 +32,13 @@ async function switchEthereumChainHandler(
2932
{
3033
getNetworkConfigurationByChainId,
3134
setActiveNetwork,
35+
requestUserApproval,
3236
getCaveat,
3337
getCurrentChainIdForDomain,
3438
requestPermittedChainsPermissionForOrigin,
3539
requestPermittedChainsPermissionIncrementalForOrigin,
3640
setTokenNetworkFilter,
41+
hasApprovalRequestsForOrigin,
3742
},
3843
) {
3944
let chainId;
@@ -66,11 +71,22 @@ async function switchEthereumChainHandler(
6671
);
6772
}
6873

74+
const fromNetworkConfiguration = getNetworkConfigurationByChainId(
75+
currentChainIdForOrigin,
76+
);
77+
78+
const toNetworkConfiguration = getNetworkConfigurationByChainId(chainId);
79+
6980
return switchChain(res, end, chainId, networkClientIdToSwitchTo, {
81+
origin,
7082
setActiveNetwork,
7183
getCaveat,
7284
requestPermittedChainsPermissionForOrigin,
7385
requestPermittedChainsPermissionIncrementalForOrigin,
7486
setTokenNetworkFilter,
87+
requestUserApproval,
88+
hasApprovalRequestsForOrigin,
89+
toNetworkConfiguration,
90+
fromNetworkConfiguration,
7591
});
7692
}

app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js

+23
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const createMockedHandler = () => {
3939
const next = jest.fn();
4040
const end = jest.fn();
4141
const mocks = {
42+
hasApprovalRequestsForOrigin: () => false,
4243
getNetworkConfigurationByChainId: jest
4344
.fn()
4445
.mockReturnValue(createMockMainnetConfiguration()),
@@ -48,6 +49,7 @@ const createMockedHandler = () => {
4849
requestPermittedChainsPermissionForOrigin: jest.fn(),
4950
requestPermittedChainsPermissionIncrementalForOrigin: jest.fn(),
5051
setTokenNetworkFilter: jest.fn(),
52+
requestUserApproval: jest.fn(),
5153
};
5254
const response = {};
5355
const handler = (request) =>
@@ -168,12 +170,33 @@ describe('switchEthereumChainHandler', () => {
168170
'mainnet',
169171
{
170172
setActiveNetwork: mocks.setActiveNetwork,
173+
fromNetworkConfiguration: {
174+
chainId: '0xe708',
175+
defaultRpcEndpointIndex: 0,
176+
rpcEndpoints: [
177+
{
178+
networkClientId: 'linea-mainnet',
179+
},
180+
],
181+
},
171182
getCaveat: mocks.getCaveat,
183+
hasApprovalRequestsForOrigin: mocks.hasApprovalRequestsForOrigin,
184+
origin: 'example.com',
172185
requestPermittedChainsPermissionForOrigin:
173186
mocks.requestPermittedChainsPermissionForOrigin,
174187
requestPermittedChainsPermissionIncrementalForOrigin:
175188
mocks.requestPermittedChainsPermissionIncrementalForOrigin,
189+
requestUserApproval: mocks.requestUserApproval,
176190
setTokenNetworkFilter: mocks.setTokenNetworkFilter,
191+
toNetworkConfiguration: {
192+
chainId: '0x1',
193+
defaultRpcEndpointIndex: 0,
194+
rpcEndpoints: [
195+
{
196+
networkClientId: 'mainnet',
197+
},
198+
],
199+
},
177200
},
178201
);
179202
});

app/scripts/metamask-controller.js

+2
Original file line numberDiff line numberDiff line change
@@ -6356,6 +6356,8 @@ export default class MetamaskController extends EventEmitter {
63566356
this.permissionController,
63576357
origin,
63586358
),
6359+
hasApprovalRequestsForOrigin: () =>
6360+
this.approvalController.has({ origin }),
63596361
rejectApprovalRequestsForOrigin: () =>
63606362
this.rejectOriginPendingApprovals(origin),
63616363

ui/pages/confirmations/confirmation/alerts/useTemplateConfirmationAlerts.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux';
44
import mockState from '../../../../../test/data/mock-state.json';
55
import { renderHookWithProvider } from '../../../../../test/lib/render-helpers';
66
import * as AlertActions from '../../../../ducks/confirm-alerts/confirm-alerts';
7-
import * as AddEthereumChainAlerts from './useAddEthereumChainAlerts';
7+
import * as UpdateEthereumChainAlerts from './useUpdateEthereumChainAlerts';
88

99
import { useTemplateConfirmationAlerts } from './useTemplateConfirmationAlerts';
1010

@@ -34,7 +34,7 @@ describe('updateConfirmationAlerts', () => {
3434
const mockDispatch = jest.fn();
3535
(useDispatch as jest.Mock).mockReturnValue(mockDispatch);
3636
jest
37-
.spyOn(AddEthereumChainAlerts, 'useAddEthereumChainAlerts')
37+
.spyOn(UpdateEthereumChainAlerts, 'useUpdateEthereumChainAlerts')
3838
.mockReturnValue(MOCK_ADD_ETH_CHAIN_ALERT);
3939
const mockUpdateAlerts = jest.spyOn(AlertActions, 'updateAlerts');
4040

ui/pages/confirmations/confirmation/alerts/useTemplateConfirmationAlerts.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import {
77
clearAlerts,
88
updateAlerts,
99
} from '../../../../ducks/confirm-alerts/confirm-alerts';
10-
import { useAddEthereumChainAlerts } from './useAddEthereumChainAlerts';
10+
import { useUpdateEthereumChainAlerts } from './useUpdateEthereumChainAlerts';
1111

1212
export const useTemplateConfirmationAlerts = (
1313
pendingConfirmation: ApprovalRequest<{ id: string }>,
1414
) => {
1515
const dispatch = useDispatch();
16-
const addEthereumChainAlerts = useAddEthereumChainAlerts(pendingConfirmation);
16+
const addEthereumChainAlerts =
17+
useUpdateEthereumChainAlerts(pendingConfirmation);
1718
const alerts: Alert[] = useMemo(
1819
() => addEthereumChainAlerts,
1920
[addEthereumChainAlerts],

ui/pages/confirmations/confirmation/alerts/useAddEthereumChainAlerts.test.ts renamed to ui/pages/confirmations/confirmation/alerts/useUpdateEthereumChainAlerts.test.ts

+37-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getMockPersonalSignConfirmState } from '../../../../../test/data/confir
66
import { renderHookWithProvider } from '../../../../../test/lib/render-helpers';
77
import * as AlertActions from '../../../../ducks/confirm-alerts/confirm-alerts';
88

9-
import { useAddEthereumChainAlerts } from './useAddEthereumChainAlerts';
9+
import { useUpdateEthereumChainAlerts } from './useUpdateEthereumChainAlerts';
1010

1111
jest.mock('react-redux', () => ({
1212
...jest.requireActual('react-redux'),
@@ -34,18 +34,50 @@ const ADD_ETH_CHAIN_ALERT = [
3434
},
3535
] as AlertActions.Alert[];
3636

37-
describe('useAddEthereumChainAlerts', () => {
38-
it('returns alert if there are pending confirmations', () => {
37+
const SWITCH_ETH_CHAIN_ALERT = [
38+
{
39+
...ADD_ETH_CHAIN_ALERT[0],
40+
message:
41+
'Switching network will cancel 1 pending transactions from this site.',
42+
},
43+
] as AlertActions.Alert[];
44+
45+
describe('useUpdateEthereumChainAlerts', () => {
46+
it('returns alert for approval type addEthereumChain if there are pending confirmations', () => {
3947
const { result } = renderHookWithProvider(
40-
() => useAddEthereumChainAlerts(PENDING_APPROVAL_MOCK),
48+
() => useUpdateEthereumChainAlerts(PENDING_APPROVAL_MOCK),
4149
getMockPersonalSignConfirmState(),
4250
);
4351
expect(result.current).toStrictEqual(ADD_ETH_CHAIN_ALERT);
4452
});
4553

54+
it('returns alert for approval type switchEthereumChain if there are pending confirmations', () => {
55+
const { result } = renderHookWithProvider(
56+
() =>
57+
useUpdateEthereumChainAlerts({
58+
...PENDING_APPROVAL_MOCK,
59+
type: ApprovalType.SwitchEthereumChain,
60+
}),
61+
getMockPersonalSignConfirmState(),
62+
);
63+
expect(result.current).toStrictEqual(SWITCH_ETH_CHAIN_ALERT);
64+
});
65+
4666
it('does not returns alert if there are no pending confirmations', () => {
4767
const { result } = renderHookWithProvider(
48-
() => useAddEthereumChainAlerts(PENDING_APPROVAL_MOCK),
68+
() => useUpdateEthereumChainAlerts(PENDING_APPROVAL_MOCK),
69+
mockState,
70+
);
71+
expect(result.current).toStrictEqual([]);
72+
});
73+
74+
it('does not returns alert for un-supported pending confirmation type', () => {
75+
const { result } = renderHookWithProvider(
76+
() =>
77+
useUpdateEthereumChainAlerts({
78+
...PENDING_APPROVAL_MOCK,
79+
type: ApprovalType.PersonalSign,
80+
}),
4981
mockState,
5082
);
5183
expect(result.current).toStrictEqual([]);

0 commit comments

Comments
 (0)