Skip to content

feat(snaps): Add support for custom network per Snap #26389

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

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { providerErrors } from '@metamask/rpc-errors';
import { isSnapId } from '@metamask/snaps-utils';

import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
import {
Expand Down Expand Up @@ -78,6 +79,7 @@ async function switchEthereumChainHandler(
return switchChain(res, end, chainId, networkClientIdToSwitchTo, {
origin,
isSwitchFlow: true,
autoApprove: isSnapId(origin),
setActiveNetwork,
getCaveat,
requestPermittedChainsPermissionIncrementalForOrigin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ describe('switchEthereumChainHandler', () => {
'0xdeadbeef',
'mainnet',
{
autoApprove: false,
setActiveNetwork: mocks.setActiveNetwork,
fromNetworkConfiguration: {
chainId: '0xe708',
Expand Down Expand Up @@ -198,4 +199,31 @@ describe('switchEthereumChainHandler', () => {
},
);
});

it('calls `switchChain` with `autoApprove: true` if the origin is a Snap', async () => {
const { mocks } = createMockedHandler();

const switchEthereumChainHandler = switchEthereumChain.implementation;
await switchEthereumChainHandler(
{
origin: 'npm:foo-snap',
params: [{ chainId: CHAIN_IDS.MAINNET }],
},
{},
jest.fn(),
jest.fn(),
mocks,
);

expect(EthChainUtils.switchChain).toHaveBeenCalledTimes(1);
expect(EthChainUtils.switchChain).toHaveBeenCalledWith(
{},
expect.any(Function),
CHAIN_IDS.MAINNET,
NETWORK_TYPES.MAINNET,
expect.objectContaining({
autoApprove: true,
}),
);
});
});
18 changes: 3 additions & 15 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ import {
TransactionType,
} from '@metamask/transaction-controller';

import { isSnapId } from '@metamask/snaps-utils';

import { Interface } from '@ethersproject/abi';
import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis';
import { isEvmAccountType } from '@metamask/keyring-api';
Expand Down Expand Up @@ -5533,12 +5531,6 @@ export default class MetamaskController extends EventEmitter {
autoApprove,
metadata,
}) {
if (isSnapId(origin)) {
throw new Error(
`Cannot request permittedChains permission for Snaps with origin "${origin}"`,
);
}

const caveatValueWithChains = setPermittedEthChainIds(
{
requiredScopes: {},
Expand Down Expand Up @@ -5589,11 +5581,11 @@ export default class MetamaskController extends EventEmitter {
* Requests user approval for the CAIP-25 permission for the specified origin
* and returns a granted permissions object.
*
* @param {string} origin - The origin to request approval for.
* @param {string} _origin - The origin to request approval for.
* @param requestedPermissions - The legacy permissions to request approval for.
* @returns the approved permissions object.
*/
getCaip25PermissionFromLegacyPermissions(origin, requestedPermissions = {}) {
getCaip25PermissionFromLegacyPermissions(_origin, requestedPermissions = {}) {
const permissions = pick(requestedPermissions, [
RestrictedMethods.eth_accounts,
PermissionNames.permittedChains,
Expand All @@ -5607,10 +5599,6 @@ export default class MetamaskController extends EventEmitter {
permissions[PermissionNames.permittedChains] = {};
}

if (isSnapId(origin)) {
delete permissions[PermissionNames.permittedChains];
}

const requestedAccounts =
permissions[RestrictedMethods.eth_accounts]?.caveats?.find(
(caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts,
Expand All @@ -5633,7 +5621,7 @@ export default class MetamaskController extends EventEmitter {

const caveatValueWithChains = setPermittedEthChainIds(
newCaveatValue,
isSnapId(origin) ? [] : requestedChains,
requestedChains,
);

const caveatValueWithAccountsAndChains = setEthAccounts(
Expand Down
145 changes: 0 additions & 145 deletions app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1243,90 +1243,6 @@ describe('MetaMaskController', () => {
);
});

it('returns approval from the PermissionsController for only eth_accounts when only permittedChains is specified in params and origin is snapId', async () => {
const permissions =
await metamaskController.getCaip25PermissionFromLegacyPermissions(
'npm:snap',
{
[PermissionNames.permittedChains]: {
caveats: [
{
type: CaveatTypes.restrictNetworkSwitching,
value: ['0x64'],
},
],
},
},
);

expect(permissions).toStrictEqual(
expect.objectContaining({
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {
'wallet:eip155': {
accounts: [],
},
},
isMultichainOrigin: false,
},
},
],
},
}),
);
});

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 () => {
const permissions =
await metamaskController.getCaip25PermissionFromLegacyPermissions(
'npm:snap',
{
[PermissionNames.eth_accounts]: {
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['foo'],
},
],
},
[PermissionNames.permittedChains]: {
caveats: [
{
type: CaveatTypes.restrictNetworkSwitching,
value: ['0x64'],
},
],
},
},
);

expect(permissions).toStrictEqual(
expect.objectContaining({
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {
'wallet:eip155': {
accounts: ['wallet:eip155:foo'],
},
},
isMultichainOrigin: false,
},
},
],
},
}),
);
});

it('returns CAIP-25 approval with accounts and chainIds specified from `eth_accounts` and `endowment:permittedChains` permissions caveats, and isMultichainOrigin: false if origin is not snapId', async () => {
const permissions =
await metamaskController.getCaip25PermissionFromLegacyPermissions(
Expand Down Expand Up @@ -1378,54 +1294,6 @@ describe('MetaMaskController', () => {
}),
);
});

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 () => {
const origin = 'npm:snap';

const permissions =
await metamaskController.getCaip25PermissionFromLegacyPermissions(
origin,
{
[RestrictedEthMethods.eth_accounts]: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: ['0xdeadbeef'],
},
],
},
[EndowmentTypes.permittedChains]: {
caveats: [
{
type: 'restrictNetworkSwitching',
value: ['0x1', '0x5'],
},
],
},
},
);

expect(permissions).toStrictEqual(
expect.objectContaining({
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {
'wallet:eip155': {
accounts: ['wallet:eip155:0xdeadbeef'],
},
},
isMultichainOrigin: false,
},
},
],
},
}),
);
});
});

describe('requestApprovalPermittedChainsPermission', () => {
Expand Down Expand Up @@ -1485,19 +1353,6 @@ describe('MetaMaskController', () => {
});

describe('requestPermittedChainsPermissionIncremental', () => {
it('throws if the origin is snapId', async () => {
await expect(() =>
metamaskController.requestPermittedChainsPermissionIncremental({
origin: 'npm:snap',
chainId: '0x1',
}),
).rejects.toThrow(
new Error(
'Cannot request permittedChains permission for Snaps with origin "npm:snap"',
),
);
});

it('requests permittedChains approval if autoApprove: false', async () => {
const expectedCaip25Permission = {
[Caip25EndowmentPermissionName]: {
Expand Down
Loading
Loading