Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/gator-permissions-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -987,6 +1002,8 @@ export default class GatorPermissionsController extends BaseController<
});

cleanup(payload.transactionMeta.id);

refreshPermissions('transaction failed');
}
};

Expand All @@ -999,6 +1016,8 @@ export default class GatorPermissionsController extends BaseController<
});

cleanup(payload.transactionMeta.id);

refreshPermissions('transaction dropped');
}
};

Expand Down