Skip to content

Commit 9dae762

Browse files
jiexiadonesky1
andauthored
fix: fix granted permissions when adding an existing network via wallet_addEthereumChain (#29837)
<!-- 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** Fixes bug with the automatically permittedChains permission grant when triggered via wallet_addEthereumChain in certain scenarios. See ticket for details. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29837?quickstart=1) ## **Related issues** Fixes: MetaMask/MetaMask-planning#3814 ## **Manual testing steps** 1. Add a new network to the dapp via [wallet_addEthereumChain](https://docs.metamask.io/wallet/reference/json-rpc-methods/wallet_addethereumchain/) 2. After approval, the dapp should also have `endowment:permitted-chains` permission for the recently added chain via `wallet_getPermissions` 3. switch the dapp to a different chain via the wallet UI 4. revoke dapp permissions via the wallet UI 6. Add a new network to the dapp via [wallet_addEthereumChain](https://docs.metamask.io/wallet/reference/json-rpc-methods/wallet_addethereumchain/) using a DIFFERENT rpc endpoint, but for the same chainId added in step 1 7. After approval, the dapp should also have `endowment:permitted-chains` permission for the recently added chain via `wallet_getPermissions` 8. switch the dapp to a different chain via the wallet UI 9. revoke dapp permissions via the wallet UI 10. Add a new network to the dapp via [wallet_addEthereumChain](https://docs.metamask.io/wallet/reference/json-rpc-methods/wallet_addethereumchain/) using the SAME rpc endpoint in step 1 (i.e. repeat step 1 exactly) 11. You should get an approval for chain switch, NOT network adding. Approve it. 12. After approval, the dapp should also have `endowment:permitted-chains` permission for the recently added chain via `wallet_getPermissions` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. --------- Co-authored-by: Alex Donesky <[email protected]>
1 parent 94d7958 commit 9dae762

File tree

5 files changed

+539
-82
lines changed

5 files changed

+539
-82
lines changed

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

+14-18
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,14 @@ async function addEthereumChainHandler(
8888
: undefined;
8989

9090
// If there's something to add or update
91-
if (
91+
92+
const shouldAddOrUpdateNetwork =
9293
!existingNetwork ||
9394
rpcIndex !== existingNetwork.defaultRpcEndpointIndex ||
9495
(firstValidBlockExplorerUrl &&
95-
blockExplorerIndex !== existingNetwork.defaultBlockExplorerUrlIndex)
96-
) {
96+
blockExplorerIndex !== existingNetwork.defaultBlockExplorerUrlIndex);
97+
98+
if (shouldAddOrUpdateNetwork) {
9799
try {
98100
await requestUserApproval({
99101
origin,
@@ -180,20 +182,14 @@ async function addEthereumChainHandler(
180182
}
181183
}
182184

183-
// If the added or updated network is not the current chain, prompt the user to switch
184-
if (chainId !== currentChainIdForDomain) {
185-
const { networkClientId } =
186-
updatedNetwork.rpcEndpoints[updatedNetwork.defaultRpcEndpointIndex];
187-
188-
return switchChain(res, end, chainId, networkClientId, {
189-
isAddFlow: true,
190-
setActiveNetwork,
191-
getCaveat,
192-
requestPermittedChainsPermissionForOrigin,
193-
requestPermittedChainsPermissionIncrementalForOrigin,
194-
});
195-
}
185+
const { networkClientId } =
186+
updatedNetwork.rpcEndpoints[updatedNetwork.defaultRpcEndpointIndex];
196187

197-
res.result = null;
198-
return end();
188+
return switchChain(res, end, chainId, networkClientId, {
189+
autoApprove: shouldAddOrUpdateNetwork,
190+
setActiveNetwork,
191+
getCaveat,
192+
requestPermittedChainsPermissionForOrigin,
193+
requestPermittedChainsPermissionIncrementalForOrigin,
194+
});
199195
}

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

+4-30
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe('addEthereumChainHandler', () => {
184184
NON_INFURA_CHAIN_ID,
185185
123,
186186
{
187-
isAddFlow: true,
187+
autoApprove: true,
188188
getCaveat: mocks.getCaveat,
189189
setActiveNetwork: mocks.setActiveNetwork,
190190
requestPermittedChainsPermissionForOrigin:
@@ -251,7 +251,7 @@ describe('addEthereumChainHandler', () => {
251251
'0x1',
252252
123,
253253
{
254-
isAddFlow: true,
254+
autoApprove: true,
255255
getCaveat: mocks.getCaveat,
256256
setActiveNetwork: mocks.setActiveNetwork,
257257
requestPermittedChainsPermissionForOrigin:
@@ -264,7 +264,7 @@ describe('addEthereumChainHandler', () => {
264264
});
265265

266266
describe('if the proposed networkConfiguration does not have a different rpcUrl from the one already in state', () => {
267-
it('should only switch to the existing networkConfiguration if one already exists for the given chain id', async () => {
267+
it('should only switch to the existing networkConfiguration if one already exists for the given chain id without auto approving the chain permission', async () => {
268268
const { mocks, end, handler } = createMockedHandler();
269269
mocks.getCurrentChainIdForDomain.mockReturnValue(CHAIN_IDS.MAINNET);
270270
mocks.getNetworkConfigurationByChainId.mockReturnValue(
@@ -298,7 +298,7 @@ describe('addEthereumChainHandler', () => {
298298
'0xa',
299299
createMockOptimismConfiguration().rpcEndpoints[0].networkClientId,
300300
{
301-
isAddFlow: true,
301+
autoApprove: false,
302302
getCaveat: mocks.getCaveat,
303303
setActiveNetwork: mocks.setActiveNetwork,
304304
requestPermittedChainsPermissionForOrigin:
@@ -338,30 +338,4 @@ describe('addEthereumChainHandler', () => {
338338
}),
339339
);
340340
});
341-
342-
it('should add result set to null to response object if the requested rpcUrl (and chainId) is currently selected', async () => {
343-
const CURRENT_RPC_CONFIG = createMockNonInfuraConfiguration();
344-
345-
const { mocks, response, handler } = createMockedHandler();
346-
mocks.getCurrentChainIdForDomain.mockReturnValue(
347-
CURRENT_RPC_CONFIG.chainId,
348-
);
349-
mocks.getNetworkConfigurationByChainId.mockReturnValue(CURRENT_RPC_CONFIG);
350-
await handler({
351-
origin: 'example.com',
352-
params: [
353-
{
354-
chainId: CURRENT_RPC_CONFIG.chainId,
355-
chainName: 'Custom Network',
356-
rpcUrls: [CURRENT_RPC_CONFIG.rpcEndpoints[0].url],
357-
nativeCurrency: {
358-
symbol: CURRENT_RPC_CONFIG.nativeCurrency,
359-
decimals: 18,
360-
},
361-
blockExplorerUrls: ['https://custom.blockexplorer'],
362-
},
363-
],
364-
});
365-
expect(response.result).toBeNull();
366-
});
367341
});

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,20 @@ export function validateAddEthereumChainParams(params) {
165165
* @param {string} chainId - The chainId being switched to.
166166
* @param {string} networkClientId - The network client being switched to.
167167
* @param {object} hooks - The hooks object.
168-
* @param {boolean} hooks.isAddFlow - The boolean determining if this call originates from wallet_addEthereumChain.
168+
* @param {boolean} [hooks.autoApprove] - A boolean indicating whether the request should prompt the user or be automatically approved.
169169
* @param {Function} hooks.setActiveNetwork - The callback to change the current network for the origin.
170170
* @param {Function} hooks.getCaveat - The callback to get the CAIP-25 caveat for the origin.
171171
* @param {Function} hooks.requestPermittedChainsPermissionForOrigin - The callback to request a new permittedChains-equivalent CAIP-25 permission.
172172
* @param {Function} hooks.requestPermittedChainsPermissionIncrementalForOrigin - The callback to add a new chain to the permittedChains-equivalent CAIP-25 permission.
173-
* @returns a null response on success or an error if user rejects an approval when isAddFlow is false or on unexpected errors.
173+
* @returns a null response on success or an error if user rejects an approval when autoApprove is false or on unexpected errors.
174174
*/
175175
export async function switchChain(
176176
response,
177177
end,
178178
chainId,
179179
networkClientId,
180180
{
181-
isAddFlow,
181+
autoApprove,
182182
setActiveNetwork,
183183
getCaveat,
184184
requestPermittedChainsPermissionForOrigin,
@@ -197,13 +197,13 @@ export async function switchChain(
197197
if (!ethChainIds.includes(chainId)) {
198198
await requestPermittedChainsPermissionIncrementalForOrigin({
199199
chainId,
200-
autoApprove: isAddFlow,
200+
autoApprove,
201201
});
202202
}
203203
} else {
204204
await requestPermittedChainsPermissionForOrigin({
205205
chainId,
206-
autoApprove: isAddFlow,
206+
autoApprove,
207207
});
208208
}
209209

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

+18-29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { rpcErrors } from '@metamask/rpc-errors';
1+
import { errorCodes, rpcErrors } from '@metamask/rpc-errors';
22
import {
33
Caip25CaveatType,
44
Caip25EndowmentPermissionName,
@@ -10,7 +10,7 @@ describe('Ethereum Chain Utils', () => {
1010
const createMockedSwitchChain = () => {
1111
const end = jest.fn();
1212
const mocks = {
13-
isAddFlow: false,
13+
autoApprove: false,
1414
setActiveNetwork: jest.fn(),
1515
getCaveat: jest.fn(),
1616
requestPermittedChainsPermissionForOrigin: jest.fn(),
@@ -39,21 +39,10 @@ describe('Ethereum Chain Utils', () => {
3939
});
4040
});
4141

42-
it('passes through unexpected errors', async () => {
43-
const { mocks, end, switchChain } = createMockedSwitchChain();
44-
mocks.requestPermittedChainsPermissionForOrigin.mockRejectedValueOnce(
45-
new Error('unexpected error'),
46-
);
47-
48-
await switchChain('0x1', 'mainnet');
49-
50-
expect(end).toHaveBeenCalledWith(new Error('unexpected error'));
51-
});
52-
5342
describe('with no existing CAIP-25 permission', () => {
54-
it('requests a switch chain approval without autoApprove if isAddFlow: false', async () => {
43+
it('requests a switch chain approval without autoApprove if autoApprove: false', async () => {
5544
const { mocks, switchChain } = createMockedSwitchChain();
56-
mocks.isAddFlow = false;
45+
mocks.autoApprove = false;
5746
await switchChain('0x1', 'mainnet');
5847

5948
expect(
@@ -68,28 +57,28 @@ describe('Ethereum Chain Utils', () => {
6857
expect(mocks.setActiveNetwork).toHaveBeenCalledWith('mainnet');
6958
});
7059

71-
it('should handle errors if the switch chain grant fails', async () => {
60+
it('should throw an error if the switch chain approval is rejected', async () => {
7261
const { mocks, end, switchChain } = createMockedSwitchChain();
73-
mocks.requestPermittedChainsPermissionForOrigin.mockRejectedValueOnce(
74-
new Error('failed to grant permittedChains'),
75-
);
62+
mocks.requestPermittedChainsPermissionForOrigin.mockRejectedValueOnce({
63+
code: errorCodes.provider.userRejectedRequest,
64+
});
7665

7766
await switchChain('0x1', 'mainnet');
7867

7968
expect(
8069
mocks.requestPermittedChainsPermissionForOrigin,
8170
).toHaveBeenCalled();
8271
expect(mocks.setActiveNetwork).not.toHaveBeenCalled();
83-
expect(end).toHaveBeenCalledWith(
84-
new Error('failed to grant permittedChains'),
85-
);
72+
expect(end).toHaveBeenCalledWith({
73+
code: errorCodes.provider.userRejectedRequest,
74+
});
8675
});
8776
});
8877

8978
describe('with an existing CAIP-25 permission granted from the legacy flow (isMultichainOrigin: false) and the chainId is not already permissioned', () => {
90-
it('requests a switch chain approval with autoApprove and switches to it if isAddFlow: true', async () => {
79+
it('requests a switch chain approval with autoApprove and switches to it if autoApprove: true', async () => {
9180
const { mocks, switchChain } = createMockedSwitchChain();
92-
mocks.isAddFlow = true;
81+
mocks.autoApprove = true;
9382
mocks.getCaveat.mockReturnValue({
9483
value: {
9584
requiredScopes: {},
@@ -105,9 +94,9 @@ describe('Ethereum Chain Utils', () => {
10594
expect(mocks.setActiveNetwork).toHaveBeenCalledWith('mainnet');
10695
});
10796

108-
it('requests permittedChains approval without autoApprove then switches to it if isAddFlow: false', async () => {
97+
it('requests permittedChains approval without autoApprove then switches to it if autoApprove: false', async () => {
10998
const { mocks, switchChain } = createMockedSwitchChain();
110-
mocks.isAddFlow = false;
99+
mocks.autoApprove = false;
111100
mocks.getCaveat.mockReturnValue({
112101
value: {
113102
requiredScopes: {},
@@ -123,10 +112,10 @@ describe('Ethereum Chain Utils', () => {
123112
expect(mocks.setActiveNetwork).toHaveBeenCalledWith('mainnet');
124113
});
125114

126-
it('should handle errors if the permittedChains grant fails', async () => {
115+
it('should throw errors if the permittedChains grant fails', async () => {
127116
const { mocks, end, switchChain } = createMockedSwitchChain();
128117
mocks.requestPermittedChainsPermissionIncrementalForOrigin.mockRejectedValueOnce(
129-
new Error('failed to grant permittedChains'),
118+
new Error('failed to auto grant permittedChains'),
130119
);
131120
mocks.getCaveat.mockReturnValue({
132121
value: {
@@ -142,7 +131,7 @@ describe('Ethereum Chain Utils', () => {
142131
).toHaveBeenCalled();
143132
expect(mocks.setActiveNetwork).not.toHaveBeenCalled();
144133
expect(end).toHaveBeenCalledWith(
145-
new Error('failed to grant permittedChains'),
134+
new Error('failed to auto grant permittedChains'),
146135
);
147136
});
148137
});

0 commit comments

Comments
 (0)