diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 25e33479019..406fc6929ca 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Refresh gator permissions map after revocation state change ([#7235](https://github.com/MetaMask/core/pull/7235)) - Add `submitDirectRevocation` method for already-disabled delegations that don't require an on-chain transaction ([#7244](https://github.com/MetaMask/core/pull/7244)) ### Changed diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts index 60883b7e565..a07124977a6 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts @@ -40,6 +40,7 @@ import type { PermissionTypesWithCustom, RevocationParams, } from './types'; +import { flushPromises } from '../../../tests/helpers'; const MOCK_CHAIN_ID_1: Hex = '0xaa36a7'; const MOCK_CHAIN_ID_2: Hex = '0x1'; @@ -1111,9 +1112,9 @@ describe('GatorPermissionsController', () => { id: txId, } as TransactionMeta); - // Wait for async operations - await Promise.resolve(); + await flushPromises(); + // Verify submitRevocation was called expect(mockHandleRequestHandler).toHaveBeenCalledWith({ snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, origin: 'metamask', @@ -1124,6 +1125,18 @@ describe('GatorPermissionsController', () => { params: { permissionContext }, }, }); + + // Verify that permissions are refreshed after revocation (getGrantedPermissions is called) + expect(mockHandleRequestHandler).toHaveBeenCalledWith({ + snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + origin: 'metamask', + handler: 'onRpcRequest', + request: { + jsonrpc: '2.0', + method: 'permissionsProvider_getGrantedPermissions', + params: { isRevoked: false }, + }, + }); }); it('should cleanup without adding to state when transaction is rejected by user', async () => { @@ -1164,7 +1177,7 @@ describe('GatorPermissionsController', () => { expect(controller.pendingRevocations).toStrictEqual([]); }); - it('should cleanup without submitting revocation when transaction fails', async () => { + it('should cleanup and refresh permissions without submitting revocation when transaction fails', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); const rootMessenger = getRootMessenger({ snapControllerHandleRequestActionHandler: mockHandleRequestHandler, @@ -1194,11 +1207,23 @@ describe('GatorPermissionsController', () => { // Wait for async operations await Promise.resolve(); - // Should not call submitRevocation - expect(mockHandleRequestHandler).not.toHaveBeenCalled(); + // Should refresh permissions with isRevoked: false + expect(mockHandleRequestHandler).toHaveBeenCalledWith({ + handler: 'onRpcRequest', + origin: 'metamask', + request: { + jsonrpc: '2.0', + method: 'permissionsProvider_getGrantedPermissions', + params: { isRevoked: false }, + }, + snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }); + + // Should not be in pending revocations + expect(controller.pendingRevocations).toStrictEqual([]); }); - it('should cleanup without submitting revocation when transaction is dropped', async () => { + it('should cleanup and refresh permissions without submitting revocation when transaction is dropped', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); const rootMessenger = getRootMessenger({ snapControllerHandleRequestActionHandler: mockHandleRequestHandler, @@ -1227,8 +1252,97 @@ describe('GatorPermissionsController', () => { // Wait for async operations await Promise.resolve(); - // Should not call submitRevocation - expect(mockHandleRequestHandler).not.toHaveBeenCalled(); + // Should refresh permissions with isRevoked: false + expect(mockHandleRequestHandler).toHaveBeenCalledWith({ + handler: 'onRpcRequest', + origin: 'metamask', + request: { + jsonrpc: '2.0', + method: 'permissionsProvider_getGrantedPermissions', + params: { isRevoked: false }, + }, + snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }); + + // Should not be in pending revocations + expect(controller.pendingRevocations).toStrictEqual([]); + }); + + it('should handle error when refreshing permissions after transaction fails', async () => { + const mockError = new Error('Failed to fetch permissions'); + const mockHandleRequestHandler = jest.fn().mockRejectedValue(mockError); + 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 and catch blocks to execute + await Promise.resolve(); + await Promise.resolve(); + + // Should have attempted to refresh permissions + expect(mockHandleRequestHandler).toHaveBeenCalled(); + + // Should not be in pending revocations + expect(controller.pendingRevocations).toStrictEqual([]); + }); + + it('should handle error when refreshing permissions after transaction is dropped', async () => { + const mockError = new Error('Failed to fetch permissions'); + const mockHandleRequestHandler = jest.fn().mockRejectedValue(mockError); + 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 and catch blocks to execute + await Promise.resolve(); + await Promise.resolve(); + + // Should have attempted to refresh permissions + expect(mockHandleRequestHandler).toHaveBeenCalled(); + + // Should not be in pending revocations + expect(controller.pendingRevocations).toStrictEqual([]); }); it('should cleanup without submitting revocation when timeout is reached', async () => { diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 55255d8053d..f91607f1f45 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -874,6 +874,19 @@ export default class GatorPermissionsController extends BaseController< timeoutId: undefined, }; + // Helper to refresh permissions after transaction state change + const refreshPermissions = (context: string) => { + this.fetchAndUpdateGatorPermissions({ isRevoked: false }).catch( + (error) => { + controllerLog(`Failed to refresh permissions after ${context}`, { + txId, + permissionContext, + error, + }); + }, + ); + }; + // Helper to unsubscribe from approval/rejection events after decision is made const cleanupApprovalHandlers = () => { if (handlers.approved) { @@ -962,16 +975,18 @@ export default class GatorPermissionsController extends BaseController< permissionContext, }); - this.submitRevocation({ permissionContext }).catch((error) => { - controllerLog( - 'Failed to submit revocation after transaction confirmed', - { - txId, - permissionContext, - error, - }, - ); - }); + this.submitRevocation({ permissionContext }) + .catch((error) => { + controllerLog( + 'Failed to submit revocation after transaction confirmed', + { + txId, + permissionContext, + error, + }, + ); + }) + .finally(() => refreshPermissions('transaction confirmed')); cleanup(transactionMeta.id); } @@ -987,6 +1002,8 @@ export default class GatorPermissionsController extends BaseController< }); cleanup(payload.transactionMeta.id); + + refreshPermissions('transaction failed'); } }; @@ -999,6 +1016,8 @@ export default class GatorPermissionsController extends BaseController< }); cleanup(payload.transactionMeta.id); + + refreshPermissions('transaction dropped'); } };