Skip to content

Commit 7b5c8ec

Browse files
committed
feat(snaps): Add support for custom network per Snap (#26389)
Like with websites, we want to support a custom network per Snap rather than relying on the globally selected network. For Snaps, the permission for the network to switch to is automatically granted. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26389?quickstart=1) 1. Copy the build files for `@metamask/selected-network-controller` from [this PR](MetaMask/core#4602). 2. Copy the build files for `@metamask/snaps-controllers` and `@metamask/snaps-execution-environments` from [this PR](MetaMask/snaps#2634). 3. Run `test-snaps` for the PR above (`yarn start` in `packages/test-snaps`). 4. Install the Ethereum Provider Example Snap. 5. Switch the chain ID using the `test-snaps` button for the Ethereum Provider example. 6. The Snap should now use the new network, without any permission requests. Step 1 and 2 are no longer necessary once these PRs are merged and released. <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> <!-- [screenshots/recordings] --> <!-- [screenshots/recordings] --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.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/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [ ] 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.
1 parent bb53fe9 commit 7b5c8ec

File tree

10 files changed

+707
-52
lines changed

10 files changed

+707
-52
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { providerErrors } from '@metamask/rpc-errors';
2+
import { isSnapId } from '@metamask/snaps-utils';
23

34
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
45
import {
@@ -80,6 +81,7 @@ async function switchEthereumChainHandler(
8081
return switchChain(res, end, chainId, networkClientIdToSwitchTo, {
8182
origin,
8283
isSwitchFlow: true,
84+
autoApprove: isSnapId(origin),
8385
setActiveNetwork,
8486
getCaveat,
8587
requestPermittedChainsPermissionIncrementalForOrigin,

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ describe('switchEthereumChainHandler', () => {
168168
'0xdeadbeef',
169169
'mainnet',
170170
{
171+
autoApprove: false,
171172
setActiveNetwork: mocks.setActiveNetwork,
172173
fromNetworkConfiguration: {
173174
chainId: '0xe708',
@@ -198,4 +199,31 @@ describe('switchEthereumChainHandler', () => {
198199
},
199200
);
200201
});
202+
203+
it('calls `switchChain` with `autoApprove: true` if the origin is a Snap', async () => {
204+
const { mocks } = createMockedHandler();
205+
206+
const switchEthereumChainHandler = switchEthereumChain.implementation;
207+
await switchEthereumChainHandler(
208+
{
209+
origin: 'npm:foo-snap',
210+
params: [{ chainId: CHAIN_IDS.MAINNET }],
211+
},
212+
{},
213+
jest.fn(),
214+
jest.fn(),
215+
mocks,
216+
);
217+
218+
expect(EthChainUtils.switchChain).toHaveBeenCalledTimes(1);
219+
expect(EthChainUtils.switchChain).toHaveBeenCalledWith(
220+
{},
221+
expect.any(Function),
222+
CHAIN_IDS.MAINNET,
223+
NETWORK_TYPES.MAINNET,
224+
expect.objectContaining({
225+
autoApprove: true,
226+
}),
227+
);
228+
});
201229
});

app/scripts/metamask-controller.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ import {
130130
TransactionType,
131131
} from '@metamask/transaction-controller';
132132

133-
import { isSnapId } from '@metamask/snaps-utils';
134-
135133
import { Interface } from '@ethersproject/abi';
136134
import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis';
137135
import {
@@ -5894,12 +5892,6 @@ export default class MetamaskController extends EventEmitter {
58945892
autoApprove,
58955893
metadata,
58965894
}) {
5897-
if (isSnapId(origin)) {
5898-
throw new Error(
5899-
`Cannot request permittedChains permission for Snaps with origin "${origin}"`,
5900-
);
5901-
}
5902-
59035895
const caveatValueWithChains = setPermittedEthChainIds(
59045896
{
59055897
requiredScopes: {},
@@ -5951,11 +5943,11 @@ export default class MetamaskController extends EventEmitter {
59515943
* Requests user approval for the CAIP-25 permission for the specified origin
59525944
* and returns a granted permissions object.
59535945
*
5954-
* @param {string} origin - The origin to request approval for.
5946+
* @param {string} _origin - The origin to request approval for.
59555947
* @param requestedPermissions - The legacy permissions to request approval for.
59565948
* @returns the approved permissions object.
59575949
*/
5958-
getCaip25PermissionFromLegacyPermissions(origin, requestedPermissions = {}) {
5950+
getCaip25PermissionFromLegacyPermissions(_origin, requestedPermissions = {}) {
59595951
const permissions = pick(requestedPermissions, [
59605952
RestrictedMethods.eth_accounts,
59615953
PermissionNames.permittedChains,
@@ -5969,10 +5961,6 @@ export default class MetamaskController extends EventEmitter {
59695961
permissions[PermissionNames.permittedChains] = {};
59705962
}
59715963

5972-
if (isSnapId(origin)) {
5973-
delete permissions[PermissionNames.permittedChains];
5974-
}
5975-
59765964
const requestedAccounts =
59775965
permissions[RestrictedMethods.eth_accounts]?.caveats?.find(
59785966
(caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts,
@@ -5996,7 +5984,7 @@ export default class MetamaskController extends EventEmitter {
59965984

59975985
const caveatValueWithChains = setPermittedEthChainIds(
59985986
newCaveatValue,
5999-
isSnapId(origin) ? [] : requestedChains,
5987+
requestedChains,
60005988
);
60015989

60025990
const caveatValueWithAccountsAndChains = setEthAccounts(

app/scripts/metamask-controller.test.js

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,7 @@ describe('MetaMaskController', () => {
12931293
);
12941294
});
12951295

1296-
it('returns approval from the PermissionsController for only eth_accounts when only permittedChains is specified in params and origin is snapId', async () => {
1296+
it('returns approval from the PermissionsController for both eth_accounts and permittedChains when only permittedChains is specified in params and origin is snapId', async () => {
12971297
const permissions =
12981298
await metamaskController.getCaip25PermissionFromLegacyPermissions(
12991299
'npm:snap',
@@ -1318,6 +1318,9 @@ describe('MetaMaskController', () => {
13181318
value: {
13191319
requiredScopes: {},
13201320
optionalScopes: {
1321+
'eip155:100': {
1322+
accounts: [],
1323+
},
13211324
'wallet:eip155': {
13221325
accounts: [],
13231326
},
@@ -1332,7 +1335,7 @@ describe('MetaMaskController', () => {
13321335
);
13331336
});
13341337

1335-
it('returns approval from the PermissionsController for only eth_accounts when both eth_accounts and permittedChains are specified in params and origin is snapId', async () => {
1338+
it('returns approval from the PermissionsController for both eth_accounts and permittedChains when both eth_accounts and permittedChains are specified in params and origin is snapId', async () => {
13361339
const permissions =
13371340
await metamaskController.getCaip25PermissionFromLegacyPermissions(
13381341
'npm:snap',
@@ -1365,6 +1368,9 @@ describe('MetaMaskController', () => {
13651368
value: {
13661369
requiredScopes: {},
13671370
optionalScopes: {
1371+
'eip155:100': {
1372+
accounts: ['eip155:100:foo'],
1373+
},
13681374
'wallet:eip155': {
13691375
accounts: ['wallet:eip155:foo'],
13701376
},
@@ -1432,31 +1438,28 @@ describe('MetaMaskController', () => {
14321438
);
14331439
});
14341440

1435-
it('returns CAIP-25 approval with approved accounts for the `wallet:eip155` scope (and no approved chainIds) with isMultichainOrigin: false if origin is snapId', async () => {
1441+
it('returns CAIP-25 approval with approved accounts and chain IDs with isMultichainOrigin: false if origin is snapId', async () => {
14361442
const origin = 'npm:snap';
14371443

14381444
const permissions =
1439-
await metamaskController.getCaip25PermissionFromLegacyPermissions(
1440-
origin,
1441-
{
1442-
[RestrictedEthMethods.eth_accounts]: {
1443-
caveats: [
1444-
{
1445-
type: 'restrictReturnedAccounts',
1446-
value: ['0xdeadbeef'],
1447-
},
1448-
],
1449-
},
1450-
[EndowmentTypes.permittedChains]: {
1451-
caveats: [
1452-
{
1453-
type: 'restrictNetworkSwitching',
1454-
value: ['0x1', '0x5'],
1455-
},
1456-
],
1457-
},
1445+
metamaskController.getCaip25PermissionFromLegacyPermissions(origin, {
1446+
[RestrictedEthMethods.eth_accounts]: {
1447+
caveats: [
1448+
{
1449+
type: 'restrictReturnedAccounts',
1450+
value: ['0xdeadbeef'],
1451+
},
1452+
],
14581453
},
1459-
);
1454+
[EndowmentTypes.permittedChains]: {
1455+
caveats: [
1456+
{
1457+
type: 'restrictNetworkSwitching',
1458+
value: ['0x1', '0x5'],
1459+
},
1460+
],
1461+
},
1462+
});
14601463

14611464
expect(permissions).toStrictEqual(
14621465
expect.objectContaining({
@@ -1467,6 +1470,12 @@ describe('MetaMaskController', () => {
14671470
value: {
14681471
requiredScopes: {},
14691472
optionalScopes: {
1473+
'eip155:1': {
1474+
accounts: ['eip155:1:0xdeadbeef'],
1475+
},
1476+
'eip155:5': {
1477+
accounts: ['eip155:5:0xdeadbeef'],
1478+
},
14701479
'wallet:eip155': {
14711480
accounts: ['wallet:eip155:0xdeadbeef'],
14721481
},
@@ -1540,19 +1549,6 @@ describe('MetaMaskController', () => {
15401549
});
15411550

15421551
describe('requestPermittedChainsPermissionIncremental', () => {
1543-
it('throws if the origin is snapId', async () => {
1544-
await expect(() =>
1545-
metamaskController.requestPermittedChainsPermissionIncremental({
1546-
origin: 'npm:snap',
1547-
chainId: '0x1',
1548-
}),
1549-
).rejects.toThrow(
1550-
new Error(
1551-
'Cannot request permittedChains permission for Snaps with origin "npm:snap"',
1552-
),
1553-
);
1554-
});
1555-
15561552
it('requests permittedChains approval if autoApprove: false', async () => {
15571553
const expectedCaip25Permission = {
15581554
[Caip25EndowmentPermissionName]: {

0 commit comments

Comments
 (0)