Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
26 changes: 0 additions & 26 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1091,32 +1091,6 @@
"count": 1
}
},
"packages/gator-permissions-controller/src/GatorPermissionsController.test.ts": {
"id-denylist": {
"count": 2
}
},
"packages/gator-permissions-controller/src/GatorPermissionsController.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 8
}
},
"packages/gator-permissions-controller/src/decodePermission/utils.test.ts": {
"id-length": {
"count": 1
}
},
"packages/gator-permissions-controller/src/decodePermission/utils.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 2
},
"@typescript-eslint/naming-convention": {
"count": 1
},
"id-length": {
"count": 1
}
},
"packages/logging-controller/src/LoggingController.test.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 1
Expand Down
4 changes: 4 additions & 0 deletions packages/gator-permissions-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **BREAKING:** Now passes the hash of the transaction that revoked the permission, when invoking the Gator Permissions Snap to mark the stored permission as revoked. ([#7503](https://github.com/MetaMask/core/pull/7503))

### Changed

- Bump `@metamask/transaction-controller` from `^62.9.1` to `^62.9.2` ([#7642](https://github.com/MetaMask/core/pull/7642))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import type { TransactionMeta } from '@metamask/transaction-controller';
import { TransactionStatus } from '@metamask/transaction-controller';
import { hexToBigInt, numberToHex } from '@metamask/utils';
import type { Hex } from '@metamask/utils';

Expand Down Expand Up @@ -196,7 +197,7 @@ describe('GatorPermissionsController', () => {
isAdjustmentAllowed: false,
data: {
target: '0x1234567890123456789012345678901234567890',
sig: '0xabcd',
signature: '0xabcd',
expiry: 1735689600, // Example expiry timestamp
},
},
Expand Down Expand Up @@ -274,7 +275,7 @@ describe('GatorPermissionsController', () => {
isAdjustmentAllowed: false,
data: {
target: '0x1234567890123456789012345678901234567890',
sig: '0xabcd',
signature: '0xabcd',
expiry: 1735689600,
},
},
Expand Down Expand Up @@ -841,6 +842,7 @@ describe('GatorPermissionsController', () => {

const revocationParams: RevocationParams = {
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
txHash: undefined,
};

await controller.submitRevocation(revocationParams);
Expand Down Expand Up @@ -869,6 +871,7 @@ describe('GatorPermissionsController', () => {

const revocationParams: RevocationParams = {
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
txHash: undefined,
};

await expect(
Expand Down Expand Up @@ -897,6 +900,7 @@ describe('GatorPermissionsController', () => {

const revocationParams: RevocationParams = {
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
txHash: undefined,
};

await expect(
Expand Down Expand Up @@ -941,6 +945,7 @@ describe('GatorPermissionsController', () => {

const revocationParams: RevocationParams = {
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
txHash: undefined,
};

// Should throw GatorPermissionsFetchError (not GatorPermissionsProviderError)
Expand Down Expand Up @@ -981,6 +986,7 @@ describe('GatorPermissionsController', () => {

const revocationParams: RevocationParams = {
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
txHash: undefined,
};

await controller.submitDirectRevocation(revocationParams);
Expand Down Expand Up @@ -1022,6 +1028,7 @@ describe('GatorPermissionsController', () => {
'0x1234567890abcdef1234567890abcdef12345678' as Hex;
const revocationParams: RevocationParams = {
permissionContext,
txHash: undefined,
};

// Spy on submitRevocation to check pending state before it's called
Expand All @@ -1046,6 +1053,7 @@ describe('GatorPermissionsController', () => {

const revocationParams: RevocationParams = {
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
txHash: undefined,
};

await expect(
Expand Down Expand Up @@ -1076,6 +1084,7 @@ describe('GatorPermissionsController', () => {
'0x1234567890abcdef1234567890abcdef12345678' as Hex;
const revocationParams: RevocationParams = {
permissionContext,
txHash: undefined,
};

await expect(
Expand Down Expand Up @@ -1200,6 +1209,7 @@ describe('GatorPermissionsController', () => {
// Emit transaction confirmed event
rootMessenger.publish('TransactionController:transactionConfirmed', {
id: txId,
status: TransactionStatus.confirmed,
} as TransactionMeta);

await flushPromises();
Expand All @@ -1212,7 +1222,124 @@ describe('GatorPermissionsController', () => {
request: {
jsonrpc: '2.0',
method: 'permissionsProvider_submitRevocation',
params: { permissionContext },
params: { permissionContext, txHash: undefined },
},
});

// 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 throw and error if the 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 approved event (user confirms)
rootMessenger.publish('TransactionController:transactionApproved', {
transactionMeta: { id: txId } as TransactionMeta,
});

// Emit transaction confirmed event
rootMessenger.publish('TransactionController:transactionConfirmed', {
id: txId,
status: TransactionStatus.failed,
} as TransactionMeta);

await flushPromises();

// Should not call submitRevocation
expect(mockHandleRequestHandler).toHaveBeenCalledTimes(1);

// 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 },
},
});

// Should not be in pending revocations
expect(controller.pendingRevocations).toStrictEqual([]);
});

it('should submit revocation metadata 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';
const hash = '0x-mock-hash';

await controller.addPendingRevocation({ txId, permissionContext });

// Emit transaction approved event (user confirms)
rootMessenger.publish('TransactionController:transactionApproved', {
transactionMeta: { id: txId } as TransactionMeta,
});

// Emit transaction confirmed event
rootMessenger.publish('TransactionController:transactionConfirmed', {
id: txId,
status: TransactionStatus.confirmed,
hash,
} as TransactionMeta);

await flushPromises();

// Verify submitRevocation was called
expect(mockHandleRequestHandler).toHaveBeenCalledWith({
snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
origin: 'metamask',
handler: 'onRpcRequest',
request: {
jsonrpc: '2.0',
method: 'permissionsProvider_submitRevocation',
params: {
permissionContext,
txHash: hash,
},
},
});

Expand Down Expand Up @@ -1530,6 +1657,7 @@ describe('GatorPermissionsController', () => {
// Emit transaction confirmed event for different transaction
rootMessenger.publish('TransactionController:transactionConfirmed', {
id: 'different-tx-id',
status: TransactionStatus.confirmed,
} as TransactionMeta);

// Wait for async operations
Expand Down Expand Up @@ -1570,6 +1698,7 @@ describe('GatorPermissionsController', () => {
// Emit transaction confirmed event
rootMessenger.publish('TransactionController:transactionConfirmed', {
id: txId,
status: TransactionStatus.confirmed,
} as TransactionMeta);

// Wait for async operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 { TransactionStatus } from '@metamask/transaction-controller';
import type {
TransactionControllerTransactionApprovedEvent,
TransactionControllerTransactionConfirmedEvent,
Expand Down Expand Up @@ -846,7 +847,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Helper to refresh permissions after transaction state change
const refreshPermissions = (context: string) => {
const refreshPermissions = (context: string): void => {
this.fetchAndUpdateGatorPermissions({ isRevoked: false }).catch(
(error) => {
controllerLog(`Failed to refresh permissions after ${context}`, {
Expand All @@ -859,7 +860,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Helper to unsubscribe from approval/rejection events after decision is made
const cleanupApprovalHandlers = () => {
const cleanupApprovalHandlers = (): void => {
if (handlers.approved) {
this.messenger.unsubscribe(
'TransactionController:transactionApproved',
Expand All @@ -877,7 +878,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Cleanup function to unsubscribe from all events and clear timeout
const cleanup = (txIdToRemove: string, removeFromState = true) => {
const cleanup = (txIdToRemove: string, removeFromState = true): void => {
cleanupApprovalHandlers();
if (handlers.confirmed) {
this.messenger.unsubscribe(
Expand Down Expand Up @@ -908,7 +909,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Handle approved transaction - add to pending revocations state
handlers.approved = (payload) => {
handlers.approved = (payload): void => {
if (payload.transactionMeta.id === txId) {
controllerLog(
'Transaction approved by user, adding to pending revocations',
Expand All @@ -926,7 +927,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Handle rejected transaction - cleanup without adding to state
handlers.rejected = (payload) => {
handlers.rejected = (payload): void => {
if (payload.transactionMeta.id === txId) {
controllerLog('Transaction rejected by user, cleaning up listeners', {
txId,
Expand All @@ -939,14 +940,41 @@ export default class GatorPermissionsController extends BaseController<
};

// Handle confirmed transaction - submit revocation
handlers.confirmed = (transactionMeta) => {
handlers.confirmed = (transactionMeta): void => {
if (transactionMeta.id === txId) {
controllerLog('Transaction confirmed, submitting revocation', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not strictly related to this PR - but do we need to check transactionMeta.status here?

When a transaction is confirmed, do we explicitly guard against failed transactions?

txId,
permissionContext,
txHash: transactionMeta.hash,
});

this.submitRevocation({ permissionContext })
if (transactionMeta.status !== TransactionStatus.confirmed) {
controllerLog('Transaction not confirmed, skipping revocation', {
txId,
permissionContext,
status: transactionMeta.status,
});
cleanup(transactionMeta.id);
refreshPermissions('transaction not confirmed');
return;
}

const txHash = transactionMeta.hash as Hex | undefined;

if (txHash === undefined) {
controllerLog(
'Failed to resolve transaction hash after revocation transaction confirmed',
{
txId,
permissionContext,
error: new Error(
'Confirmed transaction is missing transaction hash',
),
},
);
}

this.submitRevocation({ permissionContext, txHash })
.catch((error) => {
controllerLog(
'Failed to submit revocation after transaction confirmed',
Expand All @@ -964,7 +992,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Handle failed transaction - cleanup without submitting revocation
handlers.failed = (payload) => {
handlers.failed = (payload): void => {
if (payload.transactionMeta.id === txId) {
controllerLog('Transaction failed, cleaning up revocation listener', {
txId,
Expand All @@ -979,7 +1007,7 @@ export default class GatorPermissionsController extends BaseController<
};

// Handle dropped transaction - cleanup without submitting revocation
handlers.dropped = (payload) => {
handlers.dropped = (payload): void => {
if (payload.transactionMeta.id === txId) {
controllerLog('Transaction dropped, cleaning up revocation listener', {
txId,
Expand Down
Loading
Loading