Skip to content

Commit 9e7baf9

Browse files
authored
feat: Expose list of pending revocations in state (#7055)
## Explanation Expose the list of pending revocations in the state to allow MM clients to create custom selectors that filter on `pendingRevocations` to determine whether the revoke CTA should show the "pending" status. - A revocation is added to `pendingRevocations` state on calls to `addPendingRevocation()` - A revocation is removed from `pendingRevocations` state given a `transactionId` when the handler's `cleanup()` function is executed. ## References Forked from #6713 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds `pendingRevocations` to controller state with lifecycle management via `addPendingRevocation` and `submitRevocation`, updating metadata, tests, and changelog. > > - **GatorPermissionsController**: > - **State**: Add `pendingRevocations: { txId: string; permissionContext: Hex }[]` with default `[]` and UI exposure in metadata. > - **API**: > - New getter `pendingRevocations`. > - `addPendingRevocation` now appends to state immediately and cleans up on confirm/fail/drop/timeout by `txId`. > - `submitRevocation` submits to Snap and removes entry by `permissionContext` on success. > - **Internals**: Add private helpers to add/remove pending revocations; import `Hex` type. > - **Tests**: Update snapshots, add coverage for pending revocations lifecycle and getter. > - **Docs**: Update `CHANGELOG.md` to note exposed pending revocations and related actions. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 77e676e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 11b7561 commit 9e7baf9

File tree

3 files changed

+105
-9
lines changed

3 files changed

+105
-9
lines changed

packages/gator-permissions-controller/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [0.3.0]
11-
1210
### Added
1311

1412
- Add `submitRevocation` action to submit permission revocations through the gator permissions provider snap
1513
- Add `addPendingRevocation` action to queue revocations until transaction confirmation
14+
- Expose list of pending revocations in state
15+
16+
## [0.3.0]
1617

1718
### Changed
1819

packages/gator-permissions-controller/src/GatorPermissionsController.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ describe('GatorPermissionsController', () => {
115115
other: {},
116116
}),
117117
gatorPermissionsProviderSnapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
118+
pendingRevocations: [],
118119
};
119120

120121
const controller = new GatorPermissionsController({
@@ -454,6 +455,7 @@ describe('GatorPermissionsController', () => {
454455
"gatorPermissionsProviderSnapId": "npm:@metamask/gator-permissions-snap",
455456
"isFetchingGatorPermissions": false,
456457
"isGatorPermissionsEnabled": false,
458+
"pendingRevocations": Array [],
457459
}
458460
`);
459461
});
@@ -491,6 +493,7 @@ describe('GatorPermissionsController', () => {
491493
).toMatchInlineSnapshot(`
492494
Object {
493495
"gatorPermissionsMapSerialized": "{\\"native-token-stream\\":{},\\"native-token-periodic\\":{},\\"erc20-token-stream\\":{},\\"erc20-token-periodic\\":{},\\"other\\":{}}",
496+
"pendingRevocations": Array [],
494497
}
495498
`);
496499
});
@@ -735,6 +738,12 @@ describe('GatorPermissionsController', () => {
735738
isGatorPermissionsEnabled: true,
736739
gatorPermissionsProviderSnapId:
737740
MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
741+
pendingRevocations: [
742+
{
743+
txId: 'test-tx-id',
744+
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
745+
},
746+
],
738747
},
739748
});
740749

@@ -754,6 +763,7 @@ describe('GatorPermissionsController', () => {
754763
params: revocationParams,
755764
},
756765
});
766+
expect(controller.pendingRevocations).toStrictEqual([]);
757767
});
758768

759769
it('should throw GatorPermissionsNotEnabledError when gator permissions are disabled', async () => {
@@ -1038,6 +1048,33 @@ describe('GatorPermissionsController', () => {
10381048
).rejects.toThrow('Gator permissions are not enabled');
10391049
});
10401050
});
1051+
1052+
describe('get pendingRevocations', () => {
1053+
it('should return the pending revocations list', () => {
1054+
const messenger = getMessenger();
1055+
const controller = new GatorPermissionsController({
1056+
messenger,
1057+
state: {
1058+
isGatorPermissionsEnabled: true,
1059+
gatorPermissionsProviderSnapId:
1060+
MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
1061+
pendingRevocations: [
1062+
{
1063+
txId: 'test-tx-id',
1064+
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
1065+
},
1066+
],
1067+
},
1068+
});
1069+
1070+
expect(controller.pendingRevocations).toStrictEqual([
1071+
{
1072+
txId: 'test-tx-id',
1073+
permissionContext: '0x1234567890abcdef1234567890abcdef12345678',
1074+
},
1075+
]);
1076+
});
1077+
});
10411078
});
10421079

10431080
/**

packages/gator-permissions-controller/src/GatorPermissionsController.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
TransactionControllerTransactionDroppedEvent,
1616
TransactionControllerTransactionFailedEvent,
1717
} from '@metamask/transaction-controller';
18-
import type { Json } from '@metamask/utils';
18+
import type { Hex, Json } from '@metamask/utils';
1919

2020
import type { DecodedPermission } from './decodePermission';
2121
import {
@@ -104,6 +104,14 @@ export type GatorPermissionsControllerState = {
104104
* Default value is `@metamask/gator-permissions-snap`
105105
*/
106106
gatorPermissionsProviderSnapId: SnapId;
107+
108+
/**
109+
* List of gator permission pending a revocation transaction
110+
*/
111+
pendingRevocations: {
112+
txId: string;
113+
permissionContext: Hex;
114+
}[];
107115
};
108116

109117
const gatorPermissionsControllerMetadata: StateMetadata<GatorPermissionsControllerState> =
@@ -132,6 +140,12 @@ const gatorPermissionsControllerMetadata: StateMetadata<GatorPermissionsControll
132140
includeInDebugSnapshot: false,
133141
usedInUi: false,
134142
},
143+
pendingRevocations: {
144+
includeInStateLogs: true,
145+
persist: false,
146+
includeInDebugSnapshot: false,
147+
usedInUi: true,
148+
},
135149
} satisfies StateMetadata<GatorPermissionsControllerState>;
136150

137151
/**
@@ -150,6 +164,7 @@ export function getDefaultGatorPermissionsControllerState(): GatorPermissionsCon
150164
),
151165
isFetchingGatorPermissions: false,
152166
gatorPermissionsProviderSnapId: defaultGatorPermissionsProviderSnapId,
167+
pendingRevocations: [],
153168
};
154169
}
155170

@@ -313,6 +328,32 @@ export default class GatorPermissionsController extends BaseController<
313328
});
314329
}
315330

331+
#addPendingRevocationToState(txId: string, permissionContext: Hex) {
332+
this.update((state) => {
333+
state.pendingRevocations = [
334+
...state.pendingRevocations,
335+
{ txId, permissionContext },
336+
];
337+
});
338+
}
339+
340+
#removePendingRevocationFromStateByTxId(txId: string) {
341+
this.update((state) => {
342+
state.pendingRevocations = state.pendingRevocations.filter(
343+
(pendingRevocations) => pendingRevocations.txId !== txId,
344+
);
345+
});
346+
}
347+
348+
#removePendingRevocationFromStateByPermissionContext(permissionContext: Hex) {
349+
this.update((state) => {
350+
state.pendingRevocations = state.pendingRevocations.filter(
351+
(pendingRevocations) =>
352+
pendingRevocations.permissionContext !== permissionContext,
353+
);
354+
});
355+
}
356+
316357
#registerMessageHandlers(): void {
317358
this.messenger.registerActionHandler(
318359
`${controllerName}:fetchAndUpdateGatorPermissions`,
@@ -536,6 +577,15 @@ export default class GatorPermissionsController extends BaseController<
536577
});
537578
}
538579

580+
/**
581+
* Gets the pending revocations list.
582+
*
583+
* @returns The pending revocations list.
584+
*/
585+
get pendingRevocations(): { txId: string; permissionContext: Hex }[] {
586+
return this.state.pendingRevocations;
587+
}
588+
539589
/**
540590
* Fetches the gator permissions from profile sync and updates the state.
541591
*
@@ -689,6 +739,10 @@ export default class GatorPermissionsController extends BaseController<
689739
snapRequest,
690740
);
691741

742+
this.#removePendingRevocationFromStateByPermissionContext(
743+
revocationParams.permissionContext,
744+
);
745+
692746
controllerLog('Successfully submitted revocation', {
693747
permissionContext: revocationParams.permissionContext,
694748
result,
@@ -728,6 +782,7 @@ export default class GatorPermissionsController extends BaseController<
728782
});
729783

730784
this.#assertGatorPermissionsEnabled();
785+
this.#addPendingRevocationToState(txId, permissionContext);
731786

732787
type PendingRevocationHandlers = {
733788
confirmed?: (
@@ -751,7 +806,7 @@ export default class GatorPermissionsController extends BaseController<
751806
};
752807

753808
// Cleanup function to unsubscribe from all events and clear timeout
754-
const cleanup = () => {
809+
const cleanup = (txIdToRemove: string) => {
755810
if (handlers.confirmed) {
756811
this.messenger.unsubscribe(
757812
'TransactionController:transactionConfirmed',
@@ -773,6 +828,9 @@ export default class GatorPermissionsController extends BaseController<
773828
if (handlers.timeoutId !== undefined) {
774829
clearTimeout(handlers.timeoutId);
775830
}
831+
832+
// Remove the pending revocation from the state
833+
this.#removePendingRevocationFromStateByTxId(txIdToRemove);
776834
};
777835

778836
// Handle confirmed transaction - submit revocation
@@ -783,8 +841,6 @@ export default class GatorPermissionsController extends BaseController<
783841
permissionContext,
784842
});
785843

786-
cleanup();
787-
788844
this.submitRevocation({ permissionContext }).catch((error) => {
789845
controllerLog(
790846
'Failed to submit revocation after transaction confirmed',
@@ -795,6 +851,8 @@ export default class GatorPermissionsController extends BaseController<
795851
},
796852
);
797853
});
854+
855+
cleanup(transactionMeta.id);
798856
}
799857
};
800858

@@ -807,7 +865,7 @@ export default class GatorPermissionsController extends BaseController<
807865
error: payload.error,
808866
});
809867

810-
cleanup();
868+
cleanup(payload.transactionMeta.id);
811869
}
812870
};
813871

@@ -819,7 +877,7 @@ export default class GatorPermissionsController extends BaseController<
819877
permissionContext,
820878
});
821879

822-
cleanup();
880+
cleanup(payload.transactionMeta.id);
823881
}
824882
};
825883

@@ -843,7 +901,7 @@ export default class GatorPermissionsController extends BaseController<
843901
txId,
844902
permissionContext,
845903
});
846-
cleanup();
904+
cleanup(txId);
847905
}, PENDING_REVOCATION_TIMEOUT);
848906
}
849907
}

0 commit comments

Comments
 (0)