diff --git a/app/scripts/constants/snaps.ts b/app/scripts/constants/snaps.ts index 1b79418ad463..581d81eb8f0b 100644 --- a/app/scripts/constants/snaps.ts +++ b/app/scripts/constants/snaps.ts @@ -1,6 +1,5 @@ // Needed for webpack to analyze the preinstalled snaps export const PREINSTALLED_SNAPS_URLS = [ - ///: BEGIN:ONLY_INCLUDE_IF(gator-permissions) new URL( '@metamask/permissions-kernel-snap/dist/preinstalled-snap.json', // @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS @@ -11,7 +10,6 @@ export const PREINSTALLED_SNAPS_URLS = [ // @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS import.meta.url, ), - ///: END:ONLY_INCLUDE_IF new URL( '@metamask/message-signing-snap/dist/preinstalled-snap.json', // @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS diff --git a/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.test.ts b/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.test.ts index bb6ca42f13dc..a242ede6b580 100644 --- a/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.test.ts +++ b/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.test.ts @@ -1,7 +1,7 @@ import { GatorPermissionsController } from '@metamask/gator-permissions-controller'; import { buildControllerInitRequestMock } from '../test/utils'; import type { ControllerInitRequest } from '../types'; -import { isGatorPermissionsFeatureEnabled } from '../../../../shared/modules/environment'; +import { getEnabledAdvancedPermissions } from '../../../../shared/modules/environment'; import { getGatorPermissionsControllerMessenger, GatorPermissionsControllerMessenger, @@ -36,7 +36,9 @@ describe('GatorPermissionsControllerInit', () => { beforeEach(() => { jest.resetAllMocks(); - jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(true); + jest + .mocked(getEnabledAdvancedPermissions) + .mockReturnValue(['native-token-stream']); process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID = MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID; }); @@ -59,7 +61,9 @@ describe('GatorPermissionsControllerInit', () => { it('initializes with correct messenger and state(gator permissions feature enabled)', () => { const requestMock = buildInitRequestMock(); - jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(true); + jest + .mocked(getEnabledAdvancedPermissions) + .mockReturnValue(['native-token-stream']); GatorPermissionsControllerInit(requestMock); expect(GatorPermissionsControllerClassMock).toHaveBeenCalledWith({ @@ -74,7 +78,7 @@ describe('GatorPermissionsControllerInit', () => { it('initializes with correct messenger and state(gator permissions feature disabled)', () => { const requestMock = buildInitRequestMock(); - jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(false); + jest.mocked(getEnabledAdvancedPermissions).mockReturnValue([]); GatorPermissionsControllerInit(requestMock); expect(GatorPermissionsControllerClassMock).toHaveBeenCalledWith({ @@ -101,7 +105,9 @@ describe('GatorPermissionsControllerInit', () => { it('handles undefined persistedState.GatorPermissionsController', () => { const requestMock = buildInitRequestMock(); requestMock.persistedState.GatorPermissionsController = undefined; - jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(true); + jest + .mocked(getEnabledAdvancedPermissions) + .mockReturnValue(['native-token-stream']); GatorPermissionsControllerInit(requestMock); diff --git a/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.ts b/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.ts index 789c6aac6044..edf4fc3383a1 100644 --- a/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.ts +++ b/app/scripts/controller-init/gator-permissions/gator-permissions-controller-init.ts @@ -4,7 +4,7 @@ import { } from '@metamask/gator-permissions-controller'; import { assertIsValidSnapId } from '@metamask/snaps-utils'; import { ControllerInitFunction } from '../types'; -import { isGatorPermissionsFeatureEnabled } from '../../../../shared/modules/environment'; +import { getEnabledAdvancedPermissions } from '../../../../shared/modules/environment'; import { GatorPermissionsControllerMessenger } from '../messengers/gator-permissions'; const generateDefaultGatorPermissionsControllerState = @@ -26,7 +26,8 @@ const generateDefaultGatorPermissionsControllerState = } } - const isGatorPermissionsEnabled = isGatorPermissionsFeatureEnabled(); + const isGatorPermissionsEnabled = + getEnabledAdvancedPermissions().length > 0; const state: Partial = { isGatorPermissionsEnabled, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 40c369a0f9aa..71b4ca81e597 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -215,7 +215,7 @@ import { FirstTimeFlowType } from '../../shared/constants/onboarding'; import { updateCurrentLocale } from '../../shared/lib/translate'; import { getIsSeedlessOnboardingFeatureEnabled, - isGatorPermissionsFeatureEnabled, + getEnabledAdvancedPermissions, } from '../../shared/modules/environment'; import { isSnapPreinstalled } from '../../shared/lib/snaps/snaps'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; @@ -1144,12 +1144,31 @@ export default class MetamaskController extends EventEmitter { (meta) => meta.hash === hash && meta.status === TransactionStatus.submitted, ), - processRequestExecutionPermissions: isGatorPermissionsFeatureEnabled() - ? forwardRequestToSnap.bind(null, { + processRequestExecutionPermissions: async (params, req) => { + const enabledTypes = getEnabledAdvancedPermissions(); + + if (!params || params.length === 0) { + throw rpcErrors.methodNotSupported('No permission type provided'); + } + + for (const param of params) { + const permissionType = param?.permission?.type; + if (!enabledTypes.includes(permissionType)) { + throw rpcErrors.methodNotSupported( + `Permission type '${permissionType ?? 'unknown'}' is not enabled`, + ); + } + } + + return forwardRequestToSnap( + { snapId: process.env.PERMISSIONS_KERNEL_SNAP_ID, handleRequest: this.handleSnapRequest.bind(this), - }) - : undefined, + }, + params, + req, + ); + }, }); // ensure isClientOpenAndUnlocked is updated when memState updates diff --git a/builds.yml b/builds.yml index cb45775b68ae..f737c9298172 100644 --- a/builds.yml +++ b/builds.yml @@ -126,7 +126,6 @@ buildTypes: - tron - multi-srp - multichain - - gator-permissions env: - INFURA_FLASK_PROJECT_ID - SEGMENT_FLASK_WRITE_KEY @@ -147,7 +146,7 @@ buildTypes: - APPLE_FLASK_CLIENT_ID - GOOGLE_CLIENT_ID_REF: GOOGLE_FLASK_CLIENT_ID - APPLE_CLIENT_ID_REF: APPLE_FLASK_CLIENT_ID - - GATOR_PERMISSIONS_ENABLED: true + - GATOR_ENABLED_PERMISSION_TYPES: 'native-token-stream,native-token-periodic,erc20-token-stream,erc20-token-periodic' - GATOR_PERMISSIONS_REVOCATION_ENABLED: true isPrerelease: true @@ -190,6 +189,10 @@ features: dest: snaps/institutional-wallet-snap.json - src: ./node_modules/@metamask/solana-wallet-snap/dist/preinstalled-snap.json dest: snaps/solana-wallet-snap.json + - src: ./node_modules/@metamask/permissions-kernel-snap/dist/preinstalled-snap.json + dest: snaps/permissions-kernel-snap.json + - src: ./node_modules/@metamask/gator-permissions-snap/dist/preinstalled-snap.json + dest: snaps/gator-permissions-snap.json - ./{app,shared,ui}/**/keyring-snaps/** bitcoin: assets: @@ -208,12 +211,6 @@ features: ocap-kernel: assets: - ./{app,shared,ui}/**/ocap-kernel/** - gator-permissions: - assets: - - src: ./node_modules/@metamask/permissions-kernel-snap/dist/preinstalled-snap.json - dest: snaps/permissions-kernel-snap.json - - src: ./node_modules/@metamask/gator-permissions-snap/dist/preinstalled-snap.json - dest: snaps/gator-permissions-snap.json build-experimental: assets: - src: ./node_modules/@metamask/account-watcher/dist/preinstalled-snap.json @@ -389,7 +386,7 @@ env: - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io # EIP-7715 Readable Permissions - - GATOR_PERMISSIONS_ENABLED: false + - GATOR_ENABLED_PERMISSION_TYPES: '' - PERMISSIONS_KERNEL_SNAP_ID: 'npm:@metamask/permissions-kernel-snap' - GATOR_PERMISSIONS_PROVIDER_SNAP_ID: 'npm:@metamask/gator-permissions-snap' - GATOR_PERMISSIONS_REVOCATION_ENABLED: false diff --git a/shared/lib/snaps/snaps.ts b/shared/lib/snaps/snaps.ts index c8a7a0744cec..277edbf5d888 100644 --- a/shared/lib/snaps/snaps.ts +++ b/shared/lib/snaps/snaps.ts @@ -22,10 +22,8 @@ export const PREINSTALLED_SNAPS = [ ///: BEGIN:ONLY_INCLUDE_IF(tron) 'npm:@metamask/tron-wallet-snap', ///: END:ONLY_INCLUDE_IF - ///: BEGIN:ONLY_INCLUDE_IF(gator-permissions) 'npm:@metamask/permissions-kernel-snap', 'npm:@metamask/gator-permissions-snap', - ///: END:ONLY_INCLUDE_IF ]; /** diff --git a/shared/modules/environment.test.ts b/shared/modules/environment.test.ts index 866683fd6ffe..a44a892c80a8 100644 --- a/shared/modules/environment.test.ts +++ b/shared/modules/environment.test.ts @@ -1,6 +1,6 @@ import { ENVIRONMENT } from '../../development/build/constants'; import { - isGatorPermissionsFeatureEnabled, + getEnabledAdvancedPermissions, isProduction, isGatorPermissionsRevocationFeatureEnabled, } from './environment'; @@ -32,20 +32,53 @@ describe('isProduction', () => { }); }); -describe('isGatorPermissionsFeatureEnabled', () => { - it('should return true when GATOR_PERMISSIONS_ENABLED is "true"', () => { - process.env.GATOR_PERMISSIONS_ENABLED = 'true'; - expect(isGatorPermissionsFeatureEnabled()).toBe(true); +describe('getEnabledAdvancedPermissions', () => { + let originalGatorEnabledPermissionTypes: string | undefined; + + beforeAll(() => { + originalGatorEnabledPermissionTypes = + process.env.GATOR_ENABLED_PERMISSION_TYPES; + }); + + afterAll(() => { + process.env.GATOR_ENABLED_PERMISSION_TYPES = + originalGatorEnabledPermissionTypes; + }); + + it('should return an empty array when GATOR_ENABLED_PERMISSION_TYPES is not set', () => { + delete process.env.GATOR_ENABLED_PERMISSION_TYPES; + expect(getEnabledAdvancedPermissions()).toStrictEqual([]); + }); + + it('should return an empty array when GATOR_ENABLED_PERMISSION_TYPES is an empty string', () => { + process.env.GATOR_ENABLED_PERMISSION_TYPES = ''; + expect(getEnabledAdvancedPermissions()).toStrictEqual([]); + }); + + it('should parse comma-separated values correctly', () => { + process.env.GATOR_ENABLED_PERMISSION_TYPES = + 'native-token-stream,native-token-periodic,erc20-token-stream'; + expect(getEnabledAdvancedPermissions()).toStrictEqual([ + 'native-token-stream', + 'native-token-periodic', + 'erc20-token-stream', + ]); }); - it('should return false when GATOR_PERMISSIONS_ENABLED is "false"', () => { - process.env.GATOR_PERMISSIONS_ENABLED = 'false'; - expect(isGatorPermissionsFeatureEnabled()).toBe(false); + it('should filter out empty strings from the result', () => { + process.env.GATOR_ENABLED_PERMISSION_TYPES = + 'native-token-stream,,erc20-token-stream'; + expect(getEnabledAdvancedPermissions()).toStrictEqual([ + 'native-token-stream', + 'erc20-token-stream', + ]); }); - it('should return false when GATOR_PERMISSIONS_ENABLED is undefined', () => { - delete process.env.GATOR_PERMISSIONS_ENABLED; - expect(isGatorPermissionsFeatureEnabled()).toBe(false); + it('should handle a single permission type', () => { + process.env.GATOR_ENABLED_PERMISSION_TYPES = 'native-token-stream'; + expect(getEnabledAdvancedPermissions()).toStrictEqual([ + 'native-token-stream', + ]); }); }); diff --git a/shared/modules/environment.ts b/shared/modules/environment.ts index f1cf111aa5da..1ab61fa95951 100644 --- a/shared/modules/environment.ts +++ b/shared/modules/environment.ts @@ -19,8 +19,18 @@ export const getIsSettingsPageDevOptionsEnabled = (): boolean => { return process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS?.toString() === 'true'; }; -export const isGatorPermissionsFeatureEnabled = (): boolean => { - return process.env.GATOR_PERMISSIONS_ENABLED?.toString() === 'true'; +/** + * Returns the list of enabled Gator permission types from the environment configuration. + * These permission types control which advanced permissions (e.g., token streams, + * periodic transfers) are available in the current build. + * + * @returns An array of enabled permission type strings (e.g., 'native-token-stream', + * 'erc20-token-periodic'), or an empty array if none are configured. + */ +export const getEnabledAdvancedPermissions = (): string[] => { + const enabled = + process.env.GATOR_ENABLED_PERMISSION_TYPES?.toString().trim() || ''; + return enabled.split(',').filter(Boolean); }; export const isGatorPermissionsRevocationFeatureEnabled = (): boolean => { diff --git a/test/e2e/tests/settings/state-logs.json b/test/e2e/tests/settings/state-logs.json index 588b1c773bdf..8e2924940b31 100644 --- a/test/e2e/tests/settings/state-logs.json +++ b/test/e2e/tests/settings/state-logs.json @@ -887,6 +887,24 @@ "svgIcon": "string", "version": "string" }, + "npm:@metamask/permissions-kernel-snap": { + "extensionId": "null", + "iconUrl": "null", + "name": "string", + "origin": "string", + "subjectType": "string", + "svgIcon": "string", + "version": "string" + }, + "npm:@metamask/gator-permissions-snap": { + "extensionId": "null", + "iconUrl": "null", + "name": "string", + "origin": "string", + "subjectType": "string", + "svgIcon": "string", + "version": "string" + }, "npm:@metamask/solana-wallet-snap": { "extensionId": "null", "iconUrl": "null", diff --git a/ui/pages/confirmations/components/confirm/info/info.test.tsx b/ui/pages/confirmations/components/confirm/info/info.test.tsx index a5dda92e7016..cae46cafa89e 100644 --- a/ui/pages/confirmations/components/confirm/info/info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.test.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { useAssetDetails } from '../../../hooks/useAssetDetails'; -import { isGatorPermissionsFeatureEnabled } from '../../../../../../shared/modules/environment'; +import { getEnabledAdvancedPermissions } from '../../../../../../shared/modules/environment'; import { DEFAULT_ROUTE } from '../../../../../helpers/constants/routes'; import Info from './info'; @@ -55,7 +55,9 @@ jest.mock('../../../hooks/useTransactionFocusEffect', () => ({ jest.mock('../../../../../../shared/modules/environment', () => ({ ...jest.requireActual('../../../../../../shared/modules/environment'), - isGatorPermissionsFeatureEnabled: jest.fn().mockReturnValue(true), + getEnabledAdvancedPermissions: jest + .fn() + .mockReturnValue(['native-token-stream']), })); jest.mock('react-router-dom-v5-compat', () => ({ @@ -99,7 +101,7 @@ describe('Info', () => { }); it('throws an error if gator permissions feature is not enabled', () => { - jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(false); + jest.mocked(getEnabledAdvancedPermissions).mockReturnValue([]); const state = getMockTypedSignPermissionConfirmState(); const mockStore = configureMockStore([])(state); diff --git a/ui/pages/confirmations/components/confirm/info/info.tsx b/ui/pages/confirmations/components/confirm/info/info.tsx index ad1796d3a1c0..5fe9b693c4a5 100644 --- a/ui/pages/confirmations/components/confirm/info/info.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.tsx @@ -1,7 +1,7 @@ import { TransactionType } from '@metamask/transaction-controller'; import { ApprovalType } from '@metamask/controller-utils'; import React, { useMemo } from 'react'; -import { isGatorPermissionsFeatureEnabled } from '../../../../../../shared/modules/environment'; +import { getEnabledAdvancedPermissions } from '../../../../../../shared/modules/environment'; import { useTrustSignalMetrics } from '../../../../trust-signals/hooks/useTrustSignalMetrics'; import { useConfirmContext } from '../../../context/confirm'; import { useSmartTransactionFeatureFlags } from '../../../hooks/useSmartTransactionFeatureFlags'; @@ -47,7 +47,7 @@ const Info = () => { return TypedSignV1Info; } if (signatureRequest?.decodedPermission) { - if (!isGatorPermissionsFeatureEnabled()) { + if (getEnabledAdvancedPermissions().length === 0) { throw new Error('Gator permissions feature is not enabled'); }