diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 73057b0f3ad..519a826b130 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.3.0] +### Added + +- Add `submitRevocation` action to submit permission revocations through the gator permissions provider snap +- Add `addPendingRevocation` action to queue revocations until transaction confirmation + ### Changed - **BREAKING:** Use new `Messenger` from `@metamask/messenger` ([#6461](https://github.com/MetaMask/core/pull/6461)) diff --git a/packages/gator-permissions-controller/README.md b/packages/gator-permissions-controller/README.md index 104a52043e0..a694e389c36 100644 --- a/packages/gator-permissions-controller/README.md +++ b/packages/gator-permissions-controller/README.md @@ -29,8 +29,16 @@ gatorPermissionsController.enableGatorPermissions(); ### Fetch from Profile Sync ```typescript +// Fetch all permissions const permissions = await gatorPermissionsController.fetchAndUpdateGatorPermissions(); + +// Fetch permissions with optional filter params +const filteredPermissions = + await gatorPermissionsController.fetchAndUpdateGatorPermissions({ + origin: 'https://example.com', + chainId: '0x1', + }); ``` ## Contributing diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index 6e074dffa9f..b536eae38e1 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -62,6 +62,7 @@ "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/snaps-controllers": "^14.0.1", + "@metamask/transaction-controller": "^61.1.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts index 682ec8971ae..fcbb51267c5 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts @@ -18,6 +18,7 @@ import { } from '@metamask/messenger'; import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; +import type { TransactionMeta } from '@metamask/transaction-controller'; import { hexToBigInt, numberToHex, type Hex } from '@metamask/utils'; import type { GatorPermissionsControllerMessenger } from './GatorPermissionsController'; @@ -36,6 +37,7 @@ import type { GatorPermissionsMap, StoredGatorPermission, PermissionTypesWithCustom, + RevocationParams, } from './types'; const MOCK_CHAIN_ID_1: Hex = '0xaa36a7'; @@ -281,6 +283,32 @@ describe('GatorPermissionsController', () => { }); }); + it('fetches gator permissions with optional params', async () => { + const mockHandleRequestHandler = jest + .fn() + .mockResolvedValue(MOCK_GATOR_PERMISSIONS_STORAGE_ENTRIES); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + + const controller = new GatorPermissionsController({ + messenger: getMessenger(rootMessenger), + }); + + await controller.enableGatorPermissions(); + + const params = { origin: 'https://example.com', chainId: '0x1' }; + await controller.fetchAndUpdateGatorPermissions(params); + + expect(mockHandleRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + request: expect.objectContaining({ + params, + }), + }), + ); + }); + it('handles error during fetch and update', async () => { const rootMessenger = getRootMessenger({ snapControllerHandleRequestActionHandler: async () => { @@ -691,6 +719,325 @@ describe('GatorPermissionsController', () => { ).toThrow('Failed to decode permission'); }); }); + + describe('submitRevocation', () => { + it('should successfully submit a revocation when gator permissions are enabled', async () => { + const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); + const messenger = getMessenger( + getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }), + ); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const revocationParams: RevocationParams = { + permissionContext: '0x1234567890abcdef1234567890abcdef12345678', + }; + + await controller.submitRevocation(revocationParams); + + expect(mockHandleRequestHandler).toHaveBeenCalledWith({ + snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + origin: 'metamask', + handler: 'onRpcRequest', + request: { + jsonrpc: '2.0', + method: 'permissionsProvider_submitRevocation', + params: revocationParams, + }, + }); + }); + + it('should throw GatorPermissionsNotEnabledError when gator permissions are disabled', async () => { + const messenger = getMessenger(); + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: false, + }, + }); + + const revocationParams: RevocationParams = { + permissionContext: '0x1234567890abcdef1234567890abcdef12345678', + }; + + await expect( + controller.submitRevocation(revocationParams), + ).rejects.toThrow('Gator permissions are not enabled'); + }); + + it('should throw GatorPermissionsProviderError when snap request fails', async () => { + const mockHandleRequestHandler = jest + .fn() + .mockRejectedValue(new Error('Snap request failed')); + const messenger = getMessenger( + getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }), + ); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const revocationParams: RevocationParams = { + permissionContext: '0x1234567890abcdef1234567890abcdef12345678', + }; + + await expect( + controller.submitRevocation(revocationParams), + ).rejects.toThrow( + 'Failed to handle snap request to gator permissions provider for method permissionsProvider_submitRevocation', + ); + }); + }); + + describe('addPendingRevocation', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should submit revocation when transaction is confirmed', async () => { + const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await controller.addPendingRevocation({ txId, permissionContext }); + + // Emit transaction confirmed event + rootMessenger.publish('TransactionController:transactionConfirmed', { + id: txId, + } as TransactionMeta); + + // Wait for async operations + await Promise.resolve(); + + expect(mockHandleRequestHandler).toHaveBeenCalledWith({ + snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + origin: 'metamask', + handler: 'onRpcRequest', + request: { + jsonrpc: '2.0', + method: 'permissionsProvider_submitRevocation', + params: { permissionContext }, + }, + }); + }); + + it('should cleanup without submitting revocation when transaction fails', async () => { + const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await controller.addPendingRevocation({ txId, permissionContext }); + + // Emit transaction failed event + rootMessenger.publish('TransactionController:transactionFailed', { + transactionMeta: { id: txId } as TransactionMeta, + error: 'Transaction failed', + }); + + // Wait for async operations + await Promise.resolve(); + + // Should not call submitRevocation + expect(mockHandleRequestHandler).not.toHaveBeenCalled(); + }); + + it('should cleanup without submitting revocation when transaction is dropped', async () => { + const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await controller.addPendingRevocation({ txId, permissionContext }); + + // Emit transaction dropped event + rootMessenger.publish('TransactionController:transactionDropped', { + transactionMeta: { id: txId } as TransactionMeta, + }); + + // Wait for async operations + await Promise.resolve(); + + // Should not call submitRevocation + expect(mockHandleRequestHandler).not.toHaveBeenCalled(); + }); + + it('should cleanup without submitting revocation when timeout is reached', async () => { + const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await controller.addPendingRevocation({ txId, permissionContext }); + + // Fast-forward time by 2 hours + jest.advanceTimersByTime(2 * 60 * 60 * 1000); + + // Wait for async operations + await Promise.resolve(); + + // Should not call submitRevocation + expect(mockHandleRequestHandler).not.toHaveBeenCalled(); + }); + + it('should not submit revocation for different transaction IDs', async () => { + const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await controller.addPendingRevocation({ txId, permissionContext }); + + // Emit transaction confirmed event for different transaction + rootMessenger.publish('TransactionController:transactionConfirmed', { + id: 'different-tx-id', + } as TransactionMeta); + + // Wait for async operations + await Promise.resolve(); + + // Should not call submitRevocation for different transaction + expect(mockHandleRequestHandler).not.toHaveBeenCalled(); + }); + + it('should handle revocation submission errors gracefully', async () => { + const mockHandleRequestHandler = jest + .fn() + .mockRejectedValue(new Error('Revocation submission failed')); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: true, + gatorPermissionsProviderSnapId: + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await controller.addPendingRevocation({ txId, permissionContext }); + + // Emit transaction confirmed event + rootMessenger.publish('TransactionController:transactionConfirmed', { + id: txId, + } as TransactionMeta); + + // Wait for async operations + await Promise.resolve(); + + // Should have attempted to call submitRevocation even though it failed + expect(mockHandleRequestHandler).toHaveBeenCalled(); + }); + + it('should throw GatorPermissionsNotEnabledError when gator permissions are disabled', async () => { + const messenger = getMessenger(); + const controller = new GatorPermissionsController({ + messenger, + state: { + isGatorPermissionsEnabled: false, + }, + }); + + const txId = 'test-tx-id'; + const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; + + await expect( + controller.addPendingRevocation({ txId, permissionContext }), + ).rejects.toThrow('Gator permissions are not enabled'); + }); + }); }); /** @@ -774,6 +1121,23 @@ function getGatorPermissionsControllerMessenger( rootMessenger.delegate({ messenger: gatorPermissionsControllerMessenger, actions: ['SnapController:handleRequest', 'SnapController:has'], + events: [ + 'TransactionController:transactionConfirmed', + 'TransactionController:transactionFailed', + 'TransactionController:transactionDropped', + ], }); return gatorPermissionsControllerMessenger; } + +/** + * Shorthand alias for getGatorPermissionsControllerMessenger. + * + * @param rootMessenger - The root messenger to restrict. + * @returns The controller messenger. + */ +function getMessenger( + rootMessenger = getRootMessenger(), +): GatorPermissionsControllerMessenger { + return getGatorPermissionsControllerMessenger(rootMessenger); +} diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 59f63dd66ae..77cec13cdde 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -10,6 +10,12 @@ import type { Messenger } from '@metamask/messenger'; import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; +import type { + TransactionControllerTransactionConfirmedEvent, + TransactionControllerTransactionDroppedEvent, + TransactionControllerTransactionFailedEvent, +} from '@metamask/transaction-controller'; +import type { Json } from '@metamask/utils'; import type { DecodedPermission } from './decodePermission'; import { @@ -32,6 +38,8 @@ import { type PermissionTypesWithCustom, type StoredGatorPermission, type DelegationDetails, + type RevocationParams, + type PendingRevocationParams, } from './types'; import { deserializeGatorPermissionsMap, @@ -61,6 +69,12 @@ const defaultGatorPermissionsMap: GatorPermissionsMap = { */ export const DELEGATION_FRAMEWORK_VERSION = '1.3.0'; +/** + * Timeout duration for pending revocations (2 hours in milliseconds). + * After this time, event listeners will be cleaned up to prevent memory leaks. + */ +const PENDING_REVOCATION_TIMEOUT = 2 * 60 * 60 * 1000; + const contractsByChainId = DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION]; // === STATE === @@ -180,6 +194,22 @@ export type GatorPermissionsControllerDecodePermissionFromPermissionContextForOr handler: GatorPermissionsController['decodePermissionFromPermissionContextForOrigin']; }; +/** + * The action which can be used to submit a revocation. + */ +export type GatorPermissionsControllerSubmitRevocationAction = { + type: `${typeof controllerName}:submitRevocation`; + handler: GatorPermissionsController['submitRevocation']; +}; + +/** + * The action which can be used to add a pending revocation. + */ +export type GatorPermissionsControllerAddPendingRevocationAction = { + type: `${typeof controllerName}:addPendingRevocation`; + handler: GatorPermissionsController['addPendingRevocation']; +}; + /** * All actions that {@link GatorPermissionsController} registers, to be called * externally. @@ -189,7 +219,9 @@ export type GatorPermissionsControllerActions = | GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction | GatorPermissionsControllerEnableGatorPermissionsAction | GatorPermissionsControllerDisableGatorPermissionsAction - | GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction; + | GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction + | GatorPermissionsControllerSubmitRevocationAction + | GatorPermissionsControllerAddPendingRevocationAction; /** * All actions that {@link GatorPermissionsController} calls internally. @@ -218,7 +250,11 @@ export type GatorPermissionsControllerEvents = /** * Events that {@link GatorPermissionsController} is allowed to subscribe to internally. */ -type AllowedEvents = GatorPermissionsControllerStateChangeEvent; +type AllowedEvents = + | GatorPermissionsControllerStateChangeEvent + | TransactionControllerTransactionConfirmedEvent + | TransactionControllerTransactionFailedEvent + | TransactionControllerTransactionDroppedEvent; /** * Messenger type for the GatorPermissionsController. @@ -297,6 +333,18 @@ export default class GatorPermissionsController extends BaseController< `${controllerName}:decodePermissionFromPermissionContextForOrigin`, this.decodePermissionFromPermissionContextForOrigin.bind(this), ); + + const submitRevocationAction = `${controllerName}:submitRevocation`; + + this.messenger.registerActionHandler( + submitRevocationAction, + this.submitRevocation.bind(this), + ); + + this.messenger.registerActionHandler( + `${controllerName}:addPendingRevocation`, + this.addPendingRevocation.bind(this), + ); } /** @@ -315,12 +363,15 @@ export default class GatorPermissionsController extends BaseController< * * @param args - The request parameters. * @param args.snapId - The ID of the Snap of the gator permissions provider snap. + * @param args.params - Optional parameters to pass to the snap method. * @returns A promise that resolves with the gator permissions. */ async #handleSnapRequestToGatorPermissionsProvider({ snapId, + params, }: { snapId: SnapId; + params?: Json; }): Promise< StoredGatorPermission[] | null > { @@ -335,6 +386,7 @@ export default class GatorPermissionsController extends BaseController< jsonrpc: '2.0', method: GatorPermissionsSnapRpcMethod.PermissionProviderGetGrantedPermissions, + ...(params !== undefined && { params }), }, }, )) as StoredGatorPermission[] | null; @@ -487,10 +539,13 @@ export default class GatorPermissionsController extends BaseController< /** * Fetches the gator permissions from profile sync and updates the state. * + * @param params - Optional parameters to pass to the snap's getGrantedPermissions method. * @returns A promise that resolves to the gator permissions map. * @throws {GatorPermissionsFetchError} If the gator permissions fetch fails. */ - public async fetchAndUpdateGatorPermissions(): Promise { + public async fetchAndUpdateGatorPermissions( + params?: Json, + ): Promise { try { this.#setIsFetchingGatorPermissions(true); this.#assertGatorPermissionsEnabled(); @@ -498,6 +553,7 @@ export default class GatorPermissionsController extends BaseController< const permissionsData = await this.#handleSnapRequestToGatorPermissionsProvider({ snapId: this.state.gatorPermissionsProviderSnapId, + params, }); const gatorPermissionsMap = @@ -597,4 +653,197 @@ export default class GatorPermissionsController extends BaseController< }); } } + + /** + * Submits a revocation to the gator permissions provider snap. + * + * @param revocationParams - The revocation parameters containing the permission context. + * @returns A promise that resolves when the revocation is submitted successfully. + * @throws {GatorPermissionsNotEnabledError} If the gator permissions are not enabled. + * @throws {GatorPermissionsProviderError} If the snap request fails. + */ + public async submitRevocation( + revocationParams: RevocationParams, + ): Promise { + controllerLog('submitRevocation method called', { + permissionContext: revocationParams.permissionContext, + }); + + this.#assertGatorPermissionsEnabled(); + + try { + const snapRequest = { + snapId: this.state.gatorPermissionsProviderSnapId, + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + jsonrpc: '2.0', + method: + GatorPermissionsSnapRpcMethod.PermissionProviderSubmitRevocation, + params: revocationParams, + }, + }; + + const result = await this.messenger.call( + 'SnapController:handleRequest', + snapRequest, + ); + + controllerLog('Successfully submitted revocation', { + permissionContext: revocationParams.permissionContext, + result, + }); + } catch (error) { + controllerLog('Failed to submit revocation', { + error, + permissionContext: revocationParams.permissionContext, + }); + + throw new GatorPermissionsProviderError({ + method: + GatorPermissionsSnapRpcMethod.PermissionProviderSubmitRevocation, + cause: error as Error, + }); + } + } + + /** + * Adds a pending revocation that will be submitted once the transaction is confirmed. + * + * This method sets up listeners for terminal transaction states (confirmed, failed, dropped) + * and includes a timeout safety net to prevent memory leaks if the transaction never + * reaches a terminal state. + * + * @param params - The pending revocation parameters. + * @returns A promise that resolves when the listener is set up. + */ + public async addPendingRevocation( + params: PendingRevocationParams, + ): Promise { + const { txId, permissionContext } = params; + + controllerLog('addPendingRevocation method called', { + txId, + permissionContext, + }); + + this.#assertGatorPermissionsEnabled(); + + type PendingRevocationHandlers = { + confirmed?: ( + ...args: TransactionControllerTransactionConfirmedEvent['payload'] + ) => void; + failed?: ( + ...args: TransactionControllerTransactionFailedEvent['payload'] + ) => void; + dropped?: ( + ...args: TransactionControllerTransactionDroppedEvent['payload'] + ) => void; + timeoutId?: ReturnType; + }; + + // Track handlers and timeout for cleanup + const handlers: PendingRevocationHandlers = { + confirmed: undefined, + failed: undefined, + dropped: undefined, + timeoutId: undefined, + }; + + // Cleanup function to unsubscribe from all events and clear timeout + const cleanup = () => { + if (handlers.confirmed) { + this.messenger.unsubscribe( + 'TransactionController:transactionConfirmed', + handlers.confirmed, + ); + } + if (handlers.failed) { + this.messenger.unsubscribe( + 'TransactionController:transactionFailed', + handlers.failed, + ); + } + if (handlers.dropped) { + this.messenger.unsubscribe( + 'TransactionController:transactionDropped', + handlers.dropped, + ); + } + if (handlers.timeoutId !== undefined) { + clearTimeout(handlers.timeoutId); + } + }; + + // Handle confirmed transaction - submit revocation + handlers.confirmed = (transactionMeta) => { + if (transactionMeta.id === txId) { + controllerLog('Transaction confirmed, submitting revocation', { + txId, + permissionContext, + }); + + cleanup(); + + this.submitRevocation({ permissionContext }).catch((error) => { + controllerLog( + 'Failed to submit revocation after transaction confirmed', + { + txId, + permissionContext, + error, + }, + ); + }); + } + }; + + // Handle failed transaction - cleanup without submitting revocation + handlers.failed = (payload) => { + if (payload.transactionMeta.id === txId) { + controllerLog('Transaction failed, cleaning up revocation listener', { + txId, + permissionContext, + error: payload.error, + }); + + cleanup(); + } + }; + + // Handle dropped transaction - cleanup without submitting revocation + handlers.dropped = (payload) => { + if (payload.transactionMeta.id === txId) { + controllerLog('Transaction dropped, cleaning up revocation listener', { + txId, + permissionContext, + }); + + cleanup(); + } + }; + + // Subscribe to terminal transaction events + this.messenger.subscribe( + 'TransactionController:transactionConfirmed', + handlers.confirmed, + ); + this.messenger.subscribe( + 'TransactionController:transactionFailed', + handlers.failed, + ); + this.messenger.subscribe( + 'TransactionController:transactionDropped', + handlers.dropped, + ); + + // Set timeout as safety net to prevent memory leaks + handlers.timeoutId = setTimeout(() => { + controllerLog('Pending revocation timed out, cleaning up listeners', { + txId, + permissionContext, + }); + cleanup(); + }, PENDING_REVOCATION_TIMEOUT); + } } diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index c2170783ff2..6fb66704515 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -11,6 +11,8 @@ export type { GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction, GatorPermissionsControllerEnableGatorPermissionsAction, GatorPermissionsControllerDisableGatorPermissionsAction, + GatorPermissionsControllerSubmitRevocationAction, + GatorPermissionsControllerAddPendingRevocationAction, GatorPermissionsControllerActions, GatorPermissionsControllerEvents, GatorPermissionsControllerStateChangeEvent, @@ -31,6 +33,7 @@ export type { GatorPermissionsMapByPermissionType, GatorPermissionsListByPermissionTypeAndChainId, DelegationDetails, + RevocationParams, } from './types'; export type { diff --git a/packages/gator-permissions-controller/src/types.ts b/packages/gator-permissions-controller/src/types.ts index 6d875cc37f6..b214b20935f 100644 --- a/packages/gator-permissions-controller/src/types.ts +++ b/packages/gator-permissions-controller/src/types.ts @@ -32,6 +32,10 @@ export enum GatorPermissionsSnapRpcMethod { * This method is used by the metamask to request a permissions provider to get granted permissions for all sites. */ PermissionProviderGetGrantedPermissions = 'permissionsProvider_getGrantedPermissions', + /** + * This method is used by the metamask to submit a revocation to the permissions provider. + */ + PermissionProviderSubmitRevocation = 'permissionsProvider_submitRevocation', } /** @@ -145,6 +149,10 @@ export type StoredGatorPermission< > = { permissionResponse: PermissionResponse; siteOrigin: string; + /** + * Flag indicating whether this permission has been revoked. + */ + isRevoked?: boolean; }; /** @@ -159,6 +167,10 @@ export type StoredGatorPermissionSanitized< > = { permissionResponse: PermissionResponseSanitized; siteOrigin: string; + /** + * Flag indicating whether this permission has been revoked. + */ + isRevoked?: boolean; }; /** @@ -220,3 +232,27 @@ export type DelegationDetails = Pick< Delegation, 'caveats' | 'delegator' | 'delegate' | 'authority' >; + +/** + * Represents the parameters for submitting a revocation. + */ +export type RevocationParams = { + /** + * The permission context as a hex string that identifies the permission to revoke. + */ + permissionContext: Hex; +}; + +/** + * Represents the parameters for adding a pending revocation. + */ +export type PendingRevocationParams = { + /** + * The transaction metadata ID to monitor. + */ + txId: string; + /** + * The permission context as a hex string that identifies the permission to revoke. + */ + permissionContext: Hex; +}; diff --git a/packages/gator-permissions-controller/tsconfig.build.json b/packages/gator-permissions-controller/tsconfig.build.json index 931c4d6594b..3ed87eae95d 100644 --- a/packages/gator-permissions-controller/tsconfig.build.json +++ b/packages/gator-permissions-controller/tsconfig.build.json @@ -7,7 +7,8 @@ }, "references": [ { "path": "../base-controller/tsconfig.build.json" }, - { "path": "../messenger/tsconfig.build.json" } + { "path": "../messenger/tsconfig.build.json" }, + { "path": "../transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/gator-permissions-controller/tsconfig.json b/packages/gator-permissions-controller/tsconfig.json index 68c3ddfc2cd..848c8340ed3 100644 --- a/packages/gator-permissions-controller/tsconfig.json +++ b/packages/gator-permissions-controller/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [{ "path": "../base-controller" }, { "path": "../messenger" }], + "references": [ + { "path": "../base-controller" }, + { "path": "../messenger" }, + { "path": "../transaction-controller" } + ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index 9708afc155e..af6dfe11497 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3901,6 +3901,7 @@ __metadata: "@metamask/snaps-controllers": "npm:^14.0.1" "@metamask/snaps-sdk": "npm:^9.0.0" "@metamask/snaps-utils": "npm:^11.0.0" + "@metamask/transaction-controller": "npm:^61.1.0" "@metamask/utils": "npm:^11.8.1" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^27.4.1"