@@ -21,6 +21,7 @@ import {
2121 LimitExceededError ,
2222 ParseError ,
2323 UnsupportedMethodError ,
24+ type SnapsEthereumProvider ,
2425} from '@metamask/snaps-sdk' ;
2526import { z } from 'zod' ;
2627
@@ -31,6 +32,7 @@ const MAX_STORAGE_SIZE_BYTES = 400 * 1024; // 400kb limit as documented
3132const zStoredGrantedPermission = z . object ( {
3233 permissionResponse : zPermissionResponse ,
3334 siteOrigin : z . string ( ) . min ( 1 , 'Site origin cannot be empty' ) ,
35+ isRevoked : z . boolean ( ) . default ( false ) ,
3436} ) ;
3537
3638/**
@@ -100,17 +102,32 @@ export type ProfileSyncManager = {
100102 storeGrantedPermissionBatch : (
101103 storedGrantedPermission : StoredGrantedPermission [ ] ,
102104 ) => Promise < void > ;
105+ updatePermissionRevocationStatus : (
106+ permissionContext : Hex ,
107+ isRevoked : boolean ,
108+ ) => Promise < void > ;
109+ updatePermissionRevocationStatusWithPermission : (
110+ existingPermission : StoredGrantedPermission ,
111+ isRevoked : boolean ,
112+ ) => Promise < void > ;
113+ checkDelegationDisabledOnChain : (
114+ delegationHash : Hex ,
115+ chainId : Hex ,
116+ delegationManagerAddress : Hex ,
117+ ) => Promise < boolean > ;
103118} ;
104119
105120export type StoredGrantedPermission = {
106121 permissionResponse : PermissionResponse ;
107122 siteOrigin : string ;
123+ isRevoked : boolean ;
108124} ;
109125
110126export type ProfileSyncManagerConfig = {
111127 auth : JwtBearerAuth ;
112128 userStorage : UserStorage ;
113129 isFeatureEnabled : boolean ;
130+ ethereumProvider : SnapsEthereumProvider ;
114131} ;
115132
116133/**
@@ -122,7 +139,7 @@ export function createProfileSyncManager(
122139 config : ProfileSyncManagerConfig ,
123140) : ProfileSyncManager {
124141 const FEATURE = 'gator_7715_permissions' ;
125- const { auth, userStorage, isFeatureEnabled } = config ;
142+ const { auth, userStorage, isFeatureEnabled, ethereumProvider } = config ;
126143 const unConfiguredProfileSyncManager = {
127144 getAllGrantedPermissions : async ( ) => {
128145 logger . debug ( 'unConfiguredProfileSyncManager.getAllGrantedPermissions()' ) ;
@@ -143,6 +160,25 @@ export function createProfileSyncManager(
143160 'unConfiguredProfileSyncManager.storeGrantedPermissionBatch()' ,
144161 ) ;
145162 } ,
163+ updatePermissionRevocationStatus : async ( _ : Hex , __ : boolean ) => {
164+ logger . debug (
165+ 'unConfiguredProfileSyncManager.updatePermissionRevocationStatus()' ,
166+ ) ;
167+ } ,
168+ updatePermissionRevocationStatusWithPermission : async (
169+ _ : StoredGrantedPermission ,
170+ __ : boolean ,
171+ ) => {
172+ logger . debug (
173+ 'unConfiguredProfileSyncManager.updatePermissionRevocationStatusWithPermission()' ,
174+ ) ;
175+ } ,
176+ checkDelegationDisabledOnChain : async ( _ : Hex , __ : Hex , ___ : Hex ) => {
177+ logger . debug (
178+ 'unConfiguredProfileSyncManager.checkDelegationDisabledOnChain()' ,
179+ ) ;
180+ return false ; // Default to not disabled when feature is disabled
181+ } ,
146182 } ;
147183
148184 /**
@@ -306,6 +342,123 @@ export function createProfileSyncManager(
306342 }
307343 }
308344
345+ /**
346+ * Updates the revocation status of a granted permission in profile sync.
347+ *
348+ * @param permissionContext - The context of the granted permission to update.
349+ * @param isRevoked - The new revocation status.
350+ * @throws InvalidInputError if the permission is not found.
351+ */
352+ async function updatePermissionRevocationStatus (
353+ permissionContext : Hex ,
354+ isRevoked : boolean ,
355+ ) : Promise < void > {
356+ try {
357+ await authenticate ( ) ;
358+
359+ // First, get the existing permission
360+ const existingPermission = await getGrantedPermission ( permissionContext ) ;
361+ if ( ! existingPermission ) {
362+ throw new InvalidInputError (
363+ `Permission not found for delegation hash: ${ permissionContext } ` ,
364+ ) ;
365+ }
366+
367+ // Use the optimized version that accepts the existing permission
368+ await updatePermissionRevocationStatusWithPermission (
369+ existingPermission ,
370+ isRevoked ,
371+ ) ;
372+ } catch ( error ) {
373+ logger . error ( 'Error updating permission revocation status' ) ;
374+ throw error ;
375+ }
376+ }
377+
378+ /**
379+ * Updates the revocation status of a granted permission when you already have the permission object.
380+ * This is an optimized version that avoids re-fetching the permission.
381+ *
382+ * @param existingPermission - The existing permission object.
383+ * @param isRevoked - The new revocation status.
384+ */
385+ async function updatePermissionRevocationStatusWithPermission (
386+ existingPermission : StoredGrantedPermission ,
387+ isRevoked : boolean ,
388+ ) : Promise < void > {
389+ try {
390+ await authenticate ( ) ;
391+
392+ // Update the isRevoked flag
393+ const updatedPermission : StoredGrantedPermission = {
394+ ...existingPermission ,
395+ isRevoked,
396+ } ;
397+
398+ // Store the updated permission
399+ await storeGrantedPermission ( updatedPermission ) ;
400+ } catch ( error ) {
401+ logger . error (
402+ 'Error updating permission revocation status with existing permission' ,
403+ ) ;
404+ throw error ;
405+ }
406+ }
407+
408+ /**
409+ * Checks if a delegation is disabled on-chain by calling the DelegationManager contract.
410+ * @param delegationHash - The hash of the delegation to check.
411+ * @param chainId - The chain ID in hex format.
412+ * @param delegationManagerAddress - The address of the DelegationManager contract.
413+ * @returns True if the delegation is disabled, false otherwise.
414+ */
415+ async function checkDelegationDisabledOnChain (
416+ delegationHash : Hex ,
417+ chainId : Hex ,
418+ delegationManagerAddress : Hex ,
419+ ) : Promise < boolean > {
420+ try {
421+ logger . debug ( 'Checking delegation disabled status on-chain' , {
422+ delegationHash,
423+ chainId,
424+ delegationManagerAddress,
425+ } ) ;
426+
427+ // Encode the function call data for disabledDelegations(bytes32)
428+ const functionSelector = '0x2d40d052' ; // keccak256("disabledDelegations(bytes32)").slice(0, 10)
429+ const encodedParams = delegationHash . slice ( 2 ) . padStart ( 64 , '0' ) ; // Remove 0x and pad to 32 bytes
430+ const callData = `${ functionSelector } ${ encodedParams } ` ;
431+
432+ const result = await ethereumProvider . request < Hex > ( {
433+ method : 'eth_call' ,
434+ params : [
435+ {
436+ to : delegationManagerAddress ,
437+ data : callData ,
438+ } ,
439+ 'latest' ,
440+ ] ,
441+ } ) ;
442+
443+ if ( ! result ) {
444+ logger . warn ( 'No result from contract call' ) ;
445+ return false ;
446+ }
447+
448+ // Parse the boolean result (32 bytes, last byte is the boolean value)
449+ const isDisabled =
450+ result !==
451+ '0x0000000000000000000000000000000000000000000000000000000000000000' ;
452+
453+ logger . debug ( 'Delegation disabled status result' , { isDisabled } ) ;
454+ return isDisabled ;
455+ } catch ( error ) {
456+ logger . error ( 'Error checking delegation disabled status on-chain' , error ) ;
457+ // In case of error, assume not disabled to avoid blocking legitimate operations
458+ return false ;
459+ }
460+ }
461+
309462 /**
310463 * Feature flag to disable profile sync feature until message-signing-snap v1.1.2 released in MM 12.18: https://github.com/MetaMask/metamask-extension/pull/32521.
311464 */
@@ -315,6 +468,9 @@ export function createProfileSyncManager(
315468 getGrantedPermission,
316469 storeGrantedPermission,
317470 storeGrantedPermissionBatch,
471+ updatePermissionRevocationStatus,
472+ updatePermissionRevocationStatusWithPermission,
473+ checkDelegationDisabledOnChain,
318474 }
319475 : unConfiguredProfileSyncManager ;
320476}
0 commit comments