diff --git a/app/scripts/controllers/permissions/background-api.js b/app/scripts/controllers/permissions/background-api.js index 62b32a072e68..bb4c50dcef38 100644 --- a/app/scripts/controllers/permissions/background-api.js +++ b/app/scripts/controllers/permissions/background-api.js @@ -12,8 +12,6 @@ import { setPermittedEthChainIds, } from '@metamask/multichain'; import { isSnapId } from '@metamask/snaps-utils'; -import { RestrictedMethods } from '../../../../shared/constants/permissions'; -import { PermissionNames } from './specifications'; export function getPermissionBackgroundApiMethods({ permissionController, @@ -102,14 +100,15 @@ export function getPermissionBackgroundApiMethods({ }; const requestAccountsAndChainPermissions = async (origin, id) => { - // Note that we are purposely requesting an approval from the ApprovalController - // and then manually forming the permission that is then granted via the - // PermissionController rather than calling the PermissionController.requestPermissions() - // directly because the Approval UI is still dependent on the notion of there - // being separate "eth_accounts" and "endowment:permitted-chains" permissions. - // After that depedency is refactored, we can move to requesting "endowment:caip25" - // directly from the PermissionController instead. - const legacyApproval = await approvalController.addAndShowApprovalRequest({ + /** + * Note that we are purposely requesting an approval from the ApprovalController + * and then manually forming the permission that is then granted via the + * PermissionController rather than calling the PermissionController.requestPermissions() + * directly because the CAIP-25 permission is missing the factory method implementation. + * After the factory method is added, we can move to requesting "endowment:caip25" + * directly from the PermissionController instead. + */ + const { permissions } = await approvalController.addAndShowApprovalRequest({ id, origin, requestData: { @@ -118,41 +117,26 @@ export function getPermissionBackgroundApiMethods({ origin, }, permissions: { - [RestrictedMethods.eth_accounts]: {}, - [PermissionNames.permittedChains]: {}, + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: false, + }, + }, + ], + }, }, }, type: MethodNames.RequestPermissions, }); - const newCaveatValue = { - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const caveatValueWithChains = setPermittedEthChainIds( - newCaveatValue, - legacyApproval.approvedChainIds, - ); - - const caveatValueWithAccounts = setEthAccounts( - caveatValueWithChains, - legacyApproval.approvedAccounts, - ); - permissionController.grantPermissions({ subject: { origin }, - approvedPermissions: { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: caveatValueWithAccounts, - }, - ], - }, - }, + approvedPermissions: permissions, }); }; diff --git a/app/scripts/controllers/permissions/background-api.test.js b/app/scripts/controllers/permissions/background-api.test.js index 74a357f35f52..b469ce120cb9 100644 --- a/app/scripts/controllers/permissions/background-api.test.js +++ b/app/scripts/controllers/permissions/background-api.test.js @@ -6,10 +6,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '@metamask/multichain'; -import { RestrictedMethods } from '../../../../shared/constants/permissions'; import { flushPromises } from '../../../../test/lib/timer-helpers'; import { getPermissionBackgroundApiMethods } from './background-api'; -import { PermissionNames } from './specifications'; describe('permission background API methods', () => { afterEach(() => { @@ -466,13 +464,35 @@ describe('permission background API methods', () => { }); describe('requestAccountsAndChainPermissionsWithId', () => { + const approvedPermissions = { + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdeadbeef'], + }, + 'eip155:5': { + accounts: ['eip155:5:0xdeadbeef'], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, + }; + it('requests eth_accounts and permittedChains approval and returns the request id', async () => { const approvalController = { addAndShowApprovalRequest: jest.fn().mockResolvedValue({ - approvedChainIds: ['0x1', '0x5'], - approvedAccounts: ['0xdeadbeef'], + permissions: approvedPermissions, }), }; + const permissionController = { grantPermissions: jest.fn(), }; @@ -496,8 +516,18 @@ describe('permission background API methods', () => { origin: 'foo.com', }, permissions: { - [RestrictedMethods.eth_accounts]: {}, - [PermissionNames.permittedChains]: {}, + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: false, + }, + }, + ], + }, }, }, type: MethodNames.RequestPermissions, @@ -508,10 +538,10 @@ describe('permission background API methods', () => { it('grants a legacy CAIP-25 permission (isMultichainOrigin: false) with the approved eip155 chainIds and accounts', async () => { const approvalController = { addAndShowApprovalRequest: jest.fn().mockResolvedValue({ - approvedChainIds: ['0x1', '0x5'], - approvedAccounts: ['0xdeadbeef'], + permissions: approvedPermissions, }), }; + const permissionController = { grantPermissions: jest.fn(), }; @@ -527,27 +557,7 @@ describe('permission background API methods', () => { subject: { origin: 'foo.com', }, - approvedPermissions: { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdeadbeef'], - }, - 'eip155:5': { - accounts: ['eip155:5:0xdeadbeef'], - }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }, + approvedPermissions, }); }); }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7e5f2763dcca..abb43e42d60f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -5318,6 +5318,15 @@ export default class MetamaskController extends EventEmitter { * @param {Hex} chainId - The chainId to add incrementally. */ async requestApprovalPermittedChainsPermission(origin, chainId) { + const caveatValueWithChains = setPermittedEthChainIds( + { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: false, + }, + [chainId], + ); + const id = nanoid(); await this.approvalController.addAndShowApprovalRequest({ id, @@ -5328,15 +5337,16 @@ export default class MetamaskController extends EventEmitter { origin, }, permissions: { - [PermissionNames.permittedChains]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictNetworkSwitching, - value: [chainId], + type: Caip25CaveatType, + value: caveatValueWithChains, }, ], }, }, + isLegacySwitchEthereumChain: true, }, type: MethodNames.RequestPermissions, }); @@ -5468,20 +5478,15 @@ export default class MetamaskController extends EventEmitter { delete permissions[PermissionNames.permittedChains]; } - const id = nanoid(); - const legacyApproval = - await this.approvalController.addAndShowApprovalRequest({ - id, - origin, - requestData: { - metadata: { - id, - origin, - }, - permissions, - }, - type: MethodNames.RequestPermissions, - }); + const requestedChains = + permissions[PermissionNames.permittedChains]?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value ?? []; + + const requestedAccounts = + permissions[PermissionNames.eth_accounts]?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, + )?.value ?? []; const newCaveatValue = { requiredScopes: {}, @@ -5495,26 +5500,41 @@ export default class MetamaskController extends EventEmitter { const caveatValueWithChains = setPermittedEthChainIds( newCaveatValue, - isSnapId(origin) ? [] : legacyApproval.approvedChainIds, + isSnapId(origin) ? [] : requestedChains, ); - const caveatValueWithAccounts = setEthAccounts( + const caveatValueWithAccountsAndChains = setEthAccounts( caveatValueWithChains, - legacyApproval.approvedAccounts, + requestedAccounts, ); - return { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: caveatValueWithAccounts, + const id = nanoid(); + + const { permissions: approvedPermissions } = + await this.approvalController.addAndShowApprovalRequest({ + id, + origin, + requestData: { + metadata: { + id, + origin, }, - ], - }, - }; - } + permissions: { + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: caveatValueWithAccountsAndChains, + }, + ], + }, + }, + }, + type: MethodNames.RequestPermissions, + }); + return approvedPermissions; + } // --------------------------------------------------------------------------- // Identity Management (signature operations) diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 464a4b017d7d..72435e8f6bd3 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -56,7 +56,8 @@ import { ENVIRONMENT } from '../../development/build/constants'; import { SECOND } from '../../shared/constants/time'; import { CaveatTypes, - RestrictedMethods, + EndowmentTypes, + RestrictedEthMethods, } from '../../shared/constants/permissions'; import { deferredPromise } from './lib/util'; import { METAMASK_COOKIE_HANDLER } from './constants/stream'; @@ -966,8 +967,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1008,8 +1008,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1042,15 +1041,22 @@ describe('MetaMaskController', () => { origin: 'test.com', }, permissions: { - [RestrictedMethods.eth_accounts]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictReturnedAccounts, - value: ['foo'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: ['wallet:eip155:foo'], + }, + }, + isMultichainOrigin: false, + }, }, ], }, - [PermissionNames.permittedChains]: {}, }, }, type: 'wallet_requestPermissions', @@ -1065,8 +1071,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1099,12 +1104,22 @@ describe('MetaMaskController', () => { origin: 'test.com', }, permissions: { - [RestrictedMethods.eth_accounts]: {}, - [PermissionNames.permittedChains]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictNetworkSwitching, - value: ['0x64'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [], + }, + 'eip155:100': { + accounts: [], + }, + }, + isMultichainOrigin: false, + }, }, ], }, @@ -1122,8 +1137,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1164,19 +1178,22 @@ describe('MetaMaskController', () => { origin: 'test.com', }, permissions: { - [PermissionNames.eth_accounts]: { - caveats: [ - { - type: CaveatTypes.restrictReturnedAccounts, - value: ['foo'], - }, - ], - }, - [PermissionNames.permittedChains]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictNetworkSwitching, - value: ['0x64'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: ['wallet:eip155:foo'], + }, + 'eip155:100': { + accounts: ['eip155:100:foo'], + }, + }, + isMultichainOrigin: false, + }, }, ], }, @@ -1194,8 +1211,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1228,11 +1244,19 @@ describe('MetaMaskController', () => { origin: 'npm:snap', }, permissions: { - [RestrictedMethods.eth_accounts]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictReturnedAccounts, - value: ['foo'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: ['wallet:eip155:foo'], + }, + }, + isMultichainOrigin: false, + }, }, ], }, @@ -1250,8 +1274,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1284,7 +1307,22 @@ describe('MetaMaskController', () => { origin: 'npm:snap', }, permissions: { - [PermissionNames.eth_accounts]: {}, + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, }, }, type: 'wallet_requestPermissions', @@ -1299,8 +1337,7 @@ describe('MetaMaskController', () => { 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedAccounts: [], - approvedChainIds: [], + permissions: {}, }); jest .spyOn(metamaskController.permissionController, 'grantPermissions') @@ -1341,11 +1378,19 @@ describe('MetaMaskController', () => { origin: 'npm:snap', }, permissions: { - [PermissionNames.eth_accounts]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictReturnedAccounts, - value: ['foo'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: ['wallet:eip155:foo'], + }, + }, + isMultichainOrigin: false, + }, }, ], }, @@ -1371,58 +1416,115 @@ describe('MetaMaskController', () => { ).rejects.toThrow(new Error('approval rejected')); }); - it('returns the CAIP-25 approval with eth accounts, chainIds, and isMultichainOrigin: false if origin is not snapId', async () => { + it('requests 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 origin = 'test.com'; + jest .spyOn( metamaskController.approvalController, 'addAndShowApprovalRequest', ) .mockResolvedValue({ - approvedChainIds: ['0x1', '0x5'], - approvedAccounts: ['0xdeadbeef'], + permissions: { + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: false, + }, + }, + ], + }, + }, }); - const result = await metamaskController.requestCaip25Approval( - 'test.com', - {}, - ); - - expect(result).toStrictEqual({ - [Caip25EndowmentPermissionName]: { + await metamaskController.requestCaip25Approval('test.com', { + [RestrictedEthMethods.eth_accounts]: { caveats: [ { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'wallet:eip155': { - accounts: ['wallet:eip155:0xdeadbeef'], - }, - 'eip155:1': { - accounts: ['eip155:1:0xdeadbeef'], - }, - 'eip155:5': { - accounts: ['eip155:5:0xdeadbeef'], - }, - }, - isMultichainOrigin: false, - }, + 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: { + metadata: { + id: expect.stringMatching(/.{21}/u), + origin, + }, + permissions: { + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: ['wallet:eip155:0xdeadbeef'], + }, + 'eip155:1': { + accounts: ['eip155:1:0xdeadbeef'], + }, + 'eip155:5': { + accounts: ['eip155:5:0xdeadbeef'], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, + }, + }, + type: 'wallet_requestPermissions', + }), + ); }); - it('returns the CAIP-25 approval with approved accounts for the `wallet:eip155` scope (and no approved chainIds) with isMultichainOrigin: false if origin is snapId', async () => { + 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({ - approvedChainIds: ['0x1', '0x5'], - approvedAccounts: ['0xdeadbeef'], + permissions: { + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: false, + }, + }, + ], + }, + }, }); + jest .spyOn(metamaskController.permissionController, 'grantPermissions') .mockReturnValue({ @@ -1431,12 +1533,62 @@ describe('MetaMaskController', () => { }, }); - const result = await metamaskController.requestCaip25Approval( - 'npm:snap', - {}, + 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', + }), ); + }); - expect(result).toStrictEqual({ + it('should return sessions scopes returned from calling ApprovalController.addAndShowApprovalRequest', async () => { + const expectedPermissions = { [Caip25EndowmentPermissionName]: { caveats: [ { @@ -1453,7 +1605,23 @@ describe('MetaMaskController', () => { }, ], }, - }); + }; + + jest + .spyOn( + metamaskController.approvalController, + 'addAndShowApprovalRequest', + ) + .mockResolvedValue({ + permissions: expectedPermissions, + }); + + const result = await metamaskController.requestCaip25Approval( + 'test.com', + {}, + ); + + expect(result).toStrictEqual(expectedPermissions); }); }); @@ -1483,11 +1651,19 @@ describe('MetaMaskController', () => { origin: 'test.com', }, permissions: { - [PermissionNames.permittedChains]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictNetworkSwitching, - value: ['0x1'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'eip155:1': { + accounts: [], + }, + }, + isMultichainOrigin: false, + }, }, ], }, diff --git a/ui/components/app/permission-page-container/permission-page-container.component.js b/ui/components/app/permission-page-container/permission-page-container.component.js index 6e0ff41cd461..253497359412 100644 --- a/ui/components/app/permission-page-container/permission-page-container.component.js +++ b/ui/components/app/permission-page-container/permission-page-container.component.js @@ -4,26 +4,30 @@ import { SnapCaveatType, WALLET_SNAP_PERMISSION_KEY, } from '@metamask/snaps-rpc-methods'; +import { + Caip25EndowmentPermissionName, + getPermittedEthChainIds, +} from '@metamask/multichain'; import { SubjectType } from '@metamask/permission-controller'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { PageContainerFooter } from '../../ui/page-container'; import PermissionsConnectFooter from '../permissions-connect-footer'; -import { - CaveatTypes, - RestrictedMethods, -} from '../../../../shared/constants/permissions'; +import { RestrictedMethods } from '../../../../shared/constants/permissions'; import SnapPrivacyWarning from '../snaps/snap-privacy-warning'; import { getDedupedSnaps } from '../../../helpers/utils/util'; -import { containsEthPermissionsAndNonEvmAccount } from '../../../helpers/utils/permissions'; + import { BackgroundColor, Display, FlexDirection, } from '../../../helpers/constants/design-system'; import { Box } from '../../component-library'; -// eslint-disable-next-line import/no-restricted-paths -import { PermissionNames } from '../../../../app/scripts/controllers/permissions'; +import { + getRequestedSessionScopes, + getCaip25PermissionsResponse, +} from '../../../pages/permissions-connect/connect-page/utils'; +import { containsEthPermissionsAndNonEvmAccount } from '../../../helpers/utils/permissions'; import { PermissionPageContainerContent } from '.'; export default class PermissionPageContainer extends Component { @@ -147,19 +151,17 @@ export default class PermissionPageContainer extends Component { (selectedAccount) => selectedAccount.address, ); - const permittedChainsPermission = - _request.permissions?.[PermissionNames.permittedChains]; - const approvedChainIds = permittedChainsPermission?.caveats?.find( - (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, - )?.value; + const requestedSessionsScopes = getRequestedSessionScopes( + _request.permission, + ); + const approvedChainIds = getPermittedEthChainIds(requestedSessionsScopes); const request = { ..._request, - permissions: { ..._request.permissions }, - ...(_request.permissions?.eth_accounts && { approvedAccounts }), - ...(_request.permissions?.[PermissionNames.permittedChains] && { - approvedChainIds, - }), + permissions: { + ..._request.permissions, + ...getCaip25PermissionsResponse(approvedAccounts, approvedChainIds), + }, }; if (Object.keys(request.permissions).length > 0) { @@ -171,7 +173,7 @@ export default class PermissionPageContainer extends Component { onLeftFooterClick = () => { const requestedPermissions = this.getRequestedPermissions(); - if (requestedPermissions[PermissionNames.permittedChains] === undefined) { + if (requestedPermissions[Caip25EndowmentPermissionName] === undefined) { this.goBack(); } else { this.onCancel(); @@ -201,7 +203,7 @@ export default class PermissionPageContainer extends Component { }; const footerLeftActionText = requestedPermissions[ - PermissionNames.permittedChains + Caip25EndowmentPermissionName ] ? this.context.t('cancel') : this.context.t('back'); diff --git a/ui/helpers/utils/permission.js b/ui/helpers/utils/permission.js index 576116bd9893..0cf6abf8b1cc 100644 --- a/ui/helpers/utils/permission.js +++ b/ui/helpers/utils/permission.js @@ -8,7 +8,10 @@ import { getSnapDerivationPathName, } from '@metamask/snaps-utils'; import { isNonEmptyArray } from '@metamask/controller-utils'; -import { Caip25EndowmentPermissionName } from '@metamask/multichain'; +import { + Caip25EndowmentPermissionName, + getEthAccounts, +} from '@metamask/multichain'; import { RestrictedMethods, EndowmentPermissions, @@ -30,6 +33,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { PermissionNames } from '../../../app/scripts/controllers/permissions'; +import { getRequestedSessionScopes } from '../../pages/permissions-connect/connect-page/utils'; import { getURLHost } from './util'; const UNKNOWN_PERMISSION = Symbol('unknown'); @@ -50,13 +54,28 @@ function getSnapNameComponent(snapName) { ); } -export const PERMISSION_DESCRIPTIONS = deepFreeze({ +const PERMISSION_DESCRIPTIONS = deepFreeze({ // "endowment:caip25" entry is needed for the Snaps Permissions Review UI - [Caip25EndowmentPermissionName]: ({ t }) => ({ - label: t('permission_ethereumAccounts'), - leftIcon: IconName.Eye, - weight: PermissionWeight.eth_accounts, - }), + [Caip25EndowmentPermissionName]: ({ t, permissionValue }) => { + const caveatValue = getRequestedSessionScopes({ + permissions: permissionValue, + }); + const requestedAccounts = getEthAccounts(caveatValue); + const isLegacySwitchChain = requestedAccounts.length === 0; + + if (isLegacySwitchChain) { + return { + label: t('permission_walletSwitchEthereumChain'), + leftIcon: IconName.Wifi, + weight: PermissionWeight.permittedChains, + }; + } + return { + label: t('permission_ethereumAccounts'), + leftIcon: IconName.Eye, + weight: PermissionWeight.eth_accounts, + }; + }, // "eth_accounts" entry is needed for the Snaps Permissions Grant UI [RestrictedMethods.eth_accounts]: ({ t }) => ({ label: t('permission_ethereumAccounts'), diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx index 86fa769c3206..e720bb4781fc 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -1,12 +1,11 @@ import React from 'react'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '@metamask/multichain'; import { renderWithProvider } from '../../../../test/jest/rendering'; import mockState from '../../../../test/data/mock-state.json'; import configureStore from '../../../store/store'; -import { - CaveatTypes, - EndowmentTypes, - RestrictedMethods, -} from '../../../../shared/constants/permissions'; import { overrideAccountsFromMockState } from '../../../../test/jest/mocks'; import { MOCK_ACCOUNT_BIP122_P2WPKH, @@ -95,19 +94,21 @@ describe('ConnectPage', () => { id: '1', origin: mockTestDappUrl, permissions: { - [RestrictedMethods.eth_accounts]: { - caveats: [ - { - type: CaveatTypes.restrictReturnedAccounts, - value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - [EndowmentTypes.permittedChains]: { + [Caip25EndowmentPermissionName]: { caveats: [ { - type: CaveatTypes.restrictNetworkSwitching, - value: ['0x1'], + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'eip155:1': { + accounts: [ + 'eip155:1:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + ], + }, + }, + isMultichainOrigin: false, + }, }, ], }, diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index 0baa9f393b77..2a0335363c2e 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -3,6 +3,8 @@ import { useSelector } from 'react-redux'; import { InternalAccount } from '@metamask/keyring-internal-api'; import { isEvmAccountType } from '@metamask/keyring-api'; import { NetworkConfiguration } from '@metamask/network-controller'; +import { getEthAccounts, getPermittedEthChainIds } from '@metamask/multichain'; +import { Hex } from '@metamask/utils'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getSelectedInternalAccount, @@ -32,20 +34,17 @@ import { } from '../../../helpers/constants/design-system'; import { TEST_CHAINS } from '../../../../shared/constants/network'; import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer'; -import { - CaveatTypes, - EndowmentTypes, - RestrictedMethods, -} from '../../../../shared/constants/permissions'; import { getMultichainNetwork } from '../../../selectors/multichain'; +import { + getRequestedSessionScopes, + getCaip25PermissionsResponse, + PermissionsRequest, +} from './utils'; export type ConnectPageRequest = { id: string; origin: string; - permissions?: Record< - string, - { caveats?: { type: string; value: string[] }[] } - >; + permissions?: PermissionsRequest; }; export type ConnectPageProps = { @@ -64,19 +63,11 @@ export const ConnectPage: React.FC = ({ }) => { const t = useI18nContext(); - const ethAccountsPermission = - request?.permissions?.[RestrictedMethods.eth_accounts]; - const requestedAccounts = - ethAccountsPermission?.caveats?.find( - (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, - )?.value || []; - - const permittedChainsPermission = - request?.permissions?.[EndowmentTypes.permittedChains]; - const requestedChainIds = - permittedChainsPermission?.caveats?.find( - (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, - )?.value || []; + const requestedSessionsScopes = getRequestedSessionScopes( + request.permissions, + ); + const requestedAccounts = getEthAccounts(requestedSessionsScopes); + const requestedChainIds = getPermittedEthChainIds(requestedSessionsScopes); const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const [nonTestNetworks, testNetworks] = useMemo( @@ -96,7 +87,7 @@ export const ConnectPage: React.FC = ({ const currentlySelectedNetwork = useSelector(getMultichainNetwork); const currentlySelectedNetworkChainId = currentlySelectedNetwork.network.chainId; - // If globally selected network is a test network, include that in the default selcted networks for connection request + // If globally selected network is a test network, include that in the default selected networks for connection request const selectedTestNetwork = testNetworks.find( (network: { chainId: string }) => network.chainId === currentlySelectedNetworkChainId, @@ -133,8 +124,13 @@ export const ConnectPage: React.FC = ({ const onConfirm = () => { const _request = { ...request, - approvedAccounts: selectedAccountAddresses, - approvedChainIds: selectedChainIds, + permissions: { + ...request.permissions, + ...getCaip25PermissionsResponse( + selectedAccountAddresses as Hex[], + selectedChainIds, + ), + }, }; approveConnection(_request); }; diff --git a/ui/pages/permissions-connect/connect-page/utils.test.ts b/ui/pages/permissions-connect/connect-page/utils.test.ts new file mode 100644 index 000000000000..fc494eb576cb --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/utils.test.ts @@ -0,0 +1,130 @@ +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '@metamask/multichain'; +import { Hex } from '@metamask/utils'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { getCaip25PermissionsResponse } from './utils'; + +describe('getCaip25PermissionsResponse', () => { + describe('No accountAddresses or chainIds requested', () => { + it(`should construct a valid ${Caip25EndowmentPermissionName} empty permission`, () => { + const result = getCaip25PermissionsResponse([], []); + + expect(result).toEqual({ + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, + }); + }); + }); + + describe('Request approval for chainIds', () => { + it(`should construct a valid ${Caip25EndowmentPermissionName} permission from the passed chainIds`, () => { + const hexChainIds: Hex[] = [CHAIN_IDS.ARBITRUM]; + const result = getCaip25PermissionsResponse([], hexChainIds); + + expect(result).toEqual({ + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [], + }, + 'eip155:42161': { + accounts: [], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, + }); + }); + }); + describe('Request approval for accountAddresses', () => { + it(`should construct a valid ${Caip25EndowmentPermissionName} permission from the passed accountAddresses`, () => { + const addresses: Hex[] = ['0x4c286da233db3d63d44dc2ec8adc8b6dfb595cb4']; + + const result = getCaip25PermissionsResponse(addresses, []); + + expect(result).toEqual({ + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [ + 'wallet:eip155:0x4c286da233db3d63d44dc2ec8adc8b6dfb595cb4', + ], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, + }); + }); + }); + describe('Request approval for accountAddresses and chainIds', () => { + it(`should construct a valid ${Caip25EndowmentPermissionName} permission from the passed accountAddresses and chainIds`, () => { + const addresses: Hex[] = ['0x4c286da233db3d63d44dc2ec8adc8b6dfb595cb4']; + const hexChainIds: Hex[] = [CHAIN_IDS.ARBITRUM, CHAIN_IDS.LINEA_MAINNET]; + + const result = getCaip25PermissionsResponse(addresses, hexChainIds); + + expect(result).toEqual({ + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [ + 'wallet:eip155:0x4c286da233db3d63d44dc2ec8adc8b6dfb595cb4', + ], + }, + 'eip155:42161': { + accounts: [ + 'eip155:42161:0x4c286da233db3d63d44dc2ec8adc8b6dfb595cb4', + ], + }, + 'eip155:59144': { + accounts: [ + 'eip155:59144:0x4c286da233db3d63d44dc2ec8adc8b6dfb595cb4', + ], + }, + }, + isMultichainOrigin: false, + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/ui/pages/permissions-connect/connect-page/utils.ts b/ui/pages/permissions-connect/connect-page/utils.ts new file mode 100644 index 000000000000..75b66d790f63 --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/utils.ts @@ -0,0 +1,79 @@ +import { Hex } from '@metamask/utils'; +import { + Caip25CaveatType, + Caip25CaveatValue, + Caip25EndowmentPermissionName, + setEthAccounts, + setPermittedEthChainIds, +} from '@metamask/multichain'; + +export type PermissionsRequest = Record< + string, + { caveats?: { type: string; value: Caip25CaveatValue }[] } +>; + +/** + * Takes in an incoming {@link PermissionsRequest} and attempts to return the {@link Caip25CaveatValue} with the Ethereum accounts set. + * + * @param permissions - The {@link PermissionsRequest} with the target name of the {@link Caip25EndowmentPermissionName}. + * @returns The {@link Caip25CaveatValue} with the Ethereum accounts set. + */ +export function getRequestedSessionScopes( + permissions?: PermissionsRequest, +): Pick { + return ( + permissions?.[Caip25EndowmentPermissionName]?.caveats?.find( + (caveat) => caveat.type === Caip25CaveatType, + )?.value ?? { + optionalScopes: {}, + requiredScopes: {}, + } + ); +} + +/** + * Parses the CAIP-25 authorized permissions object after UI confirmation. + * + * @param addresses - The list of permitted addresses. + * @param hexChainIds - The list of permitted chains. + * @returns The granted permissions with the target name of the {@link Caip25EndowmentPermissionName}. + */ +export function getCaip25PermissionsResponse( + addresses: Hex[], + hexChainIds: Hex[], +): { + [Caip25EndowmentPermissionName]: { + caveats: [{ type: string; value: Caip25CaveatValue }]; + }; +} { + const caveatValue: Caip25CaveatValue = { + requiredScopes: {}, + optionalScopes: { + 'wallet:eip155': { + accounts: [], + }, + }, + isMultichainOrigin: false, + }; + + const caveatValueWithChains = setPermittedEthChainIds( + caveatValue, + hexChainIds, + ); + + const caveatValueWithAccounts = setEthAccounts( + caveatValueWithChains, + addresses, + ); + + return { + [Caip25EndowmentPermissionName]: { + caveats: [ + { + type: Caip25CaveatType, + value: caveatValueWithAccounts, + }, + ], + }, + }; +} diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index e9f4f5f8f592..302346846fb4 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -4,6 +4,7 @@ import { Switch, Route } from 'react-router-dom'; import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; import { isSnapId } from '@metamask/snaps-utils'; +import { getEthAccounts, getPermittedEthChainIds } from '@metamask/multichain'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { isEthAddress } from '../../../app/scripts/lib/multichain/address'; @@ -13,13 +14,6 @@ import PermissionPageContainer from '../../components/app/permission-page-contai import { Box } from '../../components/component-library'; import SnapAuthorshipHeader from '../../components/app/snaps/snap-authorship-header/snap-authorship-header'; import PermissionConnectHeader from '../../components/app/permission-connect-header'; -import { - CaveatTypes, - RestrictedMethods, -} from '../../../shared/constants/permissions'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { PermissionNames } from '../../../app/scripts/controllers/permissions'; import ChooseAccount from './choose-account'; import PermissionsRedirect from './redirect'; import SnapsConnect from './snaps/snaps-connect'; @@ -27,17 +21,15 @@ import SnapInstall from './snaps/snap-install'; import SnapUpdate from './snaps/snap-update'; import SnapResult from './snaps/snap-result'; import { ConnectPage } from './connect-page/connect-page'; +import { getRequestedSessionScopes } from './connect-page/utils'; const APPROVE_TIMEOUT = MILLISECOND * 1200; -function getDefaultSelectedAccounts(currentAddress, permissionsRequest) { - const permission = - permissionsRequest?.permissions?.[RestrictedMethods.eth_accounts]; - const requestedAccounts = permission?.caveats?.find( - (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, - )?.value; +function getDefaultSelectedAccounts(currentAddress, permissions) { + const requestedSessionsScopes = getRequestedSessionScopes(permissions); + const requestedAccounts = getEthAccounts(requestedSessionsScopes); - if (requestedAccounts) { + if (requestedAccounts.length > 0) { return new Set( requestedAccounts .map((address) => address.toLowerCase()) @@ -50,9 +42,9 @@ function getDefaultSelectedAccounts(currentAddress, permissionsRequest) { return new Set(isEthAddress(currentAddress) ? [currentAddress] : []); } -function getRequestedChainIds(permissionsRequest) { - return permissionsRequest?.permissions?.[PermissionNames.permittedChains] - ?.caveats[0]?.value; +function getRequestedChainIds(permissions) { + const requestedSessionsScopes = getRequestedSessionScopes(permissions); + return getPermittedEthChainIds(requestedSessionsScopes); } export default class PermissionConnect extends Component { @@ -128,7 +120,7 @@ export default class PermissionConnect extends Component { redirecting: false, selectedAccountAddresses: getDefaultSelectedAccounts( this.props.currentAddress, - this.props.permissionsRequest, + this.props.permissionsRequest?.permissions, ), permissionsApproved: null, origin: this.props.origin, @@ -380,7 +372,7 @@ export default class PermissionConnect extends Component { this.cancelPermissionsRequest(requestId) } activeTabOrigin={this.state.origin} - request={permissionsRequest} + request={permissionsRequest || {}} permissionsRequestId={permissionsRequestId} approveConnection={this.approveConnection} /> @@ -403,7 +395,9 @@ export default class PermissionConnect extends Component { selectedAccounts={accounts.filter((account) => selectedAccountAddresses.has(account.address), )} - requestedChainIds={getRequestedChainIds(permissionsRequest)} + requestedChainIds={getRequestedChainIds( + permissionsRequest?.permissions, + )} targetSubjectMetadata={targetSubjectMetadata} history={history} connectPath={connectPath} diff --git a/ui/pages/permissions-connect/permissions-connect.container.js b/ui/pages/permissions-connect/permissions-connect.container.js index 075d14d172ef..d4c080c6be05 100644 --- a/ui/pages/permissions-connect/permissions-connect.container.js +++ b/ui/pages/permissions-connect/permissions-connect.container.js @@ -3,6 +3,7 @@ import { WALLET_SNAP_PERMISSION_KEY } from '@metamask/snaps-rpc-methods'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { isEvmAccountType } from '@metamask/keyring-api'; +import { Caip25EndowmentPermissionName } from '@metamask/multichain'; import { getAccountsWithLabels, getLastConnectedInfo, @@ -54,14 +55,16 @@ const mapStateToProps = (state, ownProps) => { (req) => req.metadata.id === permissionsRequestId, ); - const isRequestingAccounts = Boolean( - permissionsRequest?.permissions?.eth_accounts, - ); - - const { metadata = {} } = permissionsRequest || {}; + const { metadata = {}, isLegacySwitchEthereumChain } = + permissionsRequest || {}; const { origin } = metadata; const nativeCurrency = getNativeCurrency(state); + const isRequestingAccounts = Boolean( + permissionsRequest?.permissions?.[Caip25EndowmentPermissionName] && + !isLegacySwitchEthereumChain, + ); + const targetSubjectMetadata = getTargetSubjectMetadata(state, origin) ?? { name: getURLHostName(origin) || origin, origin,