Skip to content
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

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

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 {
validateSwitchEthereumChainParams,
Expand Down Expand Up @@ -67,6 +68,7 @@ async function switchEthereumChainHandler(
}

return switchChain(res, end, chainId, networkClientIdToSwitchTo, {
autoApprove: isSnapId(origin),
setActiveNetwork,
getCaveat,
requestPermittedChainsPermissionForOrigin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,41 @@ describe('switchEthereumChainHandler', () => {
'0xdeadbeef',
'mainnet',
{
autoApprove: false,
setActiveNetwork: mocks.setActiveNetwork,
getCaveat: mocks.getCaveat,
requestPermittedChainsPermissionForOrigin:
mocks.requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin:
mocks.requestPermittedChainsPermissionIncrementalForOrigin,
setTokenNetworkFilter: mocks.setTokenNetworkFilter,
},
);
});

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,
{
autoApprove: true,
setActiveNetwork: mocks.setActiveNetwork,
getCaveat: mocks.getCaveat,
requestPermittedChainsPermissionForOrigin:
Expand Down
19 changes: 1 addition & 18 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ import {
getLocalizedSnapManifest,
stripSnapPrefix,
///: END:ONLY_INCLUDE_IF
isSnapId,
} from '@metamask/snaps-utils';

import { Interface } from '@ethersproject/abi';
Expand Down Expand Up @@ -5073,12 +5072,6 @@ export default class MetamaskController extends EventEmitter {
* @param {boolean} options.autoApprove - If the chain should be granted without prompting for user approval.
*/
async requestPermittedChainsPermission({ origin, chainId, autoApprove }) {
if (isSnapId(origin)) {
throw new Error(
`Cannot request permittedChains permission for Snaps with origin "${origin}"`,
);
}

if (!autoApprove) {
await this.requestApprovalPermittedChainsPermission(origin, chainId);
}
Expand Down Expand Up @@ -5123,12 +5116,6 @@ export default class MetamaskController extends EventEmitter {
chainId,
autoApprove,
}) {
if (isSnapId(origin)) {
throw new Error(
`Cannot request permittedChains permission for Snaps with origin "${origin}"`,
);
}

if (!autoApprove) {
await this.requestApprovalPermittedChainsPermission(origin, chainId);
}
Expand Down Expand Up @@ -5207,10 +5194,6 @@ export default class MetamaskController extends EventEmitter {
permissions[PermissionNames.permittedChains] = {};
}

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

const newCaveatValue = {
requiredScopes: {},
optionalScopes: {
Expand All @@ -5223,7 +5206,7 @@ export default class MetamaskController extends EventEmitter {

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

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

it('requests approval from the ApprovalController for only eth_accounts when only permittedChains is specified in params and origin is snapId', async () => {
jest
.spyOn(
metamaskController.approvalController,
'addAndShowApprovalRequest',
)
.mockResolvedValue({
permissions: {},
});
jest
.spyOn(metamaskController.permissionController, 'grantPermissions')
.mockReturnValue({
[Caip25EndowmentPermissionName]: {
foo: 'bar',
},
});

await metamaskController.requestCaip25Approval('npm:snap', {
[PermissionNames.permittedChains]: {
caveats: [
{
type: CaveatTypes.restrictNetworkSwitching,
value: ['0x64'],
},
],
},
});

expect(
metamaskController.approvalController.addAndShowApprovalRequest,
).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.stringMatching(/.{21}/u),
origin: 'npm:snap',
requestData: {
metadata: {
id: expect.stringMatching(/.{21}/u),
origin: 'npm:snap',
},
permissions: {
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {
'wallet:eip155': {
accounts: [],
},
},
isMultichainOrigin: false,
},
},
],
},
},
},
type: 'wallet_requestPermissions',
}),
);
});

it('requests approval from the ApprovalController for only eth_accounts when both eth_accounts and permittedChains are specified in params and origin is snapId', async () => {
jest
.spyOn(
metamaskController.approvalController,
'addAndShowApprovalRequest',
)
.mockResolvedValue({
permissions: {},
});
jest
.spyOn(metamaskController.permissionController, 'grantPermissions')
.mockReturnValue({
[Caip25EndowmentPermissionName]: {
foo: 'bar',
},
});

await metamaskController.requestCaip25Approval('npm:snap', {
[PermissionNames.eth_accounts]: {
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['foo'],
},
],
},
[PermissionNames.permittedChains]: {
caveats: [
{
type: CaveatTypes.restrictNetworkSwitching,
value: ['0x64'],
},
],
},
});

expect(
metamaskController.approvalController.addAndShowApprovalRequest,
).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.stringMatching(/.{21}/u),
origin: 'npm:snap',
requestData: {
metadata: {
id: expect.stringMatching(/.{21}/u),
origin: 'npm:snap',
},
permissions: {
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {
'wallet:eip155': {
accounts: ['wallet:eip155:foo'],
},
},
isMultichainOrigin: false,
},
},
],
},
},
},
type: 'wallet_requestPermissions',
}),
);
});

it('throws an error if the eth_accounts and permittedChains approval is rejected', async () => {
jest
.spyOn(
Expand Down Expand Up @@ -1511,92 +1377,6 @@ describe('MetaMaskController', () => {
);
});

it('requests 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';
jest
.spyOn(
metamaskController.approvalController,
'addAndShowApprovalRequest',
)
.mockResolvedValue({
permissions: {
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {},
isMultichainOrigin: false,
},
},
],
},
},
});

jest
.spyOn(metamaskController.permissionController, 'grantPermissions')
.mockReturnValue({
[Caip25EndowmentPermissionName]: {
foo: 'bar',
},
});

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

expect(
metamaskController.approvalController.addAndShowApprovalRequest,
).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.stringMatching(/.{21}/u),
origin,
requestData: expect.objectContaining({
metadata: {
id: expect.stringMatching(/.{21}/u),
origin,
},
permissions: {
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
requiredScopes: {},
optionalScopes: {
'wallet:eip155': {
accounts: ['wallet:eip155:0xdeadbeef'],
},
},
isMultichainOrigin: false,
},
},
],
},
},
}),
type: 'wallet_requestPermissions',
}),
);
});

it('should return sessions scopes returned from calling ApprovalController.addAndShowApprovalRequest', async () => {
const expectedPermissions = {
[Caip25EndowmentPermissionName]: {
Expand Down Expand Up @@ -1782,19 +1562,6 @@ describe('MetaMaskController', () => {
});

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

it('requests approval for permittedChains permissions from the ApprovalController if autoApprove: false', async () => {
jest
.spyOn(metamaskController, 'requestApprovalPermittedChainsPermission')
Expand Down Expand Up @@ -1905,19 +1672,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('gets the CAIP-25 caveat', async () => {
jest
.spyOn(metamaskController.permissionController, 'getCaveat')
Expand Down
Loading
Loading