Skip to content

Commit ebb10f9

Browse files
committed
chore: added new rpc for marking delegation as revoked
1 parent ab90ef7 commit ebb10f9

File tree

8 files changed

+701
-13
lines changed

8 files changed

+701
-13
lines changed

packages/gator-permissions-snap/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const profileSyncManager = createProfileSyncManager({
139139
storage: profileSyncOptions.keyStorageOptions,
140140
},
141141
),
142+
ethereumProvider: ethereum,
142143
});
143144

144145
const userEventDispatcher = new UserEventDispatcher();
@@ -187,6 +188,9 @@ const boundRpcHandlers: {
187188
rpcHandler.getPermissionOffers.bind(rpcHandler),
188189
[RpcMethod.PermissionsProviderGetGrantedPermissions]:
189190
rpcHandler.getGrantedPermissions.bind(rpcHandler),
191+
[RpcMethod.PermissionProviderSubmitRevocation]: async (
192+
params?: JsonRpcParams,
193+
) => rpcHandler.submitRevocation(params as Json),
190194
};
191195

192196
/**

packages/gator-permissions-snap/src/profileSync/profileSync.ts

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
LimitExceededError,
2222
ParseError,
2323
UnsupportedMethodError,
24+
type SnapsEthereumProvider,
2425
} from '@metamask/snaps-sdk';
2526
import { z } from 'zod';
2627

@@ -31,6 +32,7 @@ const MAX_STORAGE_SIZE_BYTES = 400 * 1024; // 400kb limit as documented
3132
const 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

105120
export type StoredGrantedPermission = {
106121
permissionResponse: PermissionResponse;
107122
siteOrigin: string;
123+
isRevoked: boolean;
108124
};
109125

110126
export 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
}

packages/gator-permissions-snap/src/rpc/permissions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const allowedPermissionsByOrigin: { [origin: string]: string[] } = {
99
RpcMethod.PermissionsProviderGetPermissionOffers,
1010
],
1111
}),
12-
metamask: [RpcMethod.PermissionsProviderGetGrantedPermissions],
12+
metamask: [
13+
RpcMethod.PermissionsProviderGetGrantedPermissions,
14+
RpcMethod.PermissionsProviderSubmitRevocation,
15+
],
1316
};
1417

1518
/**

packages/gator-permissions-snap/src/rpc/rpcHandler.ts

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import { InvalidInputError, type Json } from '@metamask/snaps-sdk';
44

55
import type { PermissionHandlerFactory } from '../core/permissionHandlerFactory';
66
import { DEFAULT_GATOR_PERMISSION_TO_OFFER } from '../permissions/permissionOffers';
7-
import type { ProfileSyncManager } from '../profileSync';
8-
import { validatePermissionRequestParam } from '../utils/validate';
7+
import type {
8+
ProfileSyncManager,
9+
StoredGrantedPermission,
10+
} from '../profileSync/profileSync';
11+
import {
12+
validatePermissionRequestParam,
13+
validateRevocationParams,
14+
} from '../utils/validate';
915

1016
/**
1117
* Type for the RPC handler methods.
@@ -32,6 +38,14 @@ export type RpcHandler = {
3238
* @returns The granted permissions.
3339
*/
3440
getGrantedPermissions(): Promise<Json>;
41+
42+
/**
43+
* Handles submit revocation requests.
44+
*
45+
* @param params - The parameters for the revocation.
46+
* @returns Success confirmation.
47+
*/
48+
submitRevocation(params: Json): Promise<Json>;
3549
};
3650

3751
/**
@@ -75,15 +89,19 @@ export function createRpcHandler(config: {
7589
throw new InvalidInputError(permissionResponse.reason);
7690
}
7791

78-
permissionsToStore.push({
92+
const storedPermission: StoredGrantedPermission = {
7993
permissionResponse: permissionResponse.response,
8094
siteOrigin,
81-
});
95+
isRevoked: false,
96+
};
97+
permissionsToStore.push(storedPermission);
8298
}
8399

84100
// Only after all permissions have been successfully processed, store them all in batch
85101
if (permissionsToStore.length > 0) {
86-
await profileSyncManager.storeGrantedPermissionBatch(permissionsToStore);
102+
await profileSyncManager.storeGrantedPermissionBatch(
103+
permissionsToStore as StoredGrantedPermission[],
104+
);
87105
}
88106

89107
// Return the permission responses
@@ -114,9 +132,74 @@ export function createRpcHandler(config: {
114132
return grantedPermission as Json[];
115133
};
116134

135+
/**
136+
* Handles submit revocation requests.
137+
*
138+
* @param params - The parameters for the revocation.
139+
* @returns Success confirmation.
140+
*/
141+
const submitRevocation = async (params: Json): Promise<Json> => {
142+
logger.debug('submitRevocation()', params);
143+
144+
const { delegationHash } = validateRevocationParams(params);
145+
146+
// First, get the existing permission to validate it exists
147+
const existingPermission =
148+
await profileSyncManager.getGrantedPermission(delegationHash);
149+
150+
if (!existingPermission) {
151+
throw new InvalidInputError(
152+
`Permission not found for delegation hash: ${delegationHash}`,
153+
);
154+
}
155+
156+
// Extract delegationManager and chainId from the permission response for logging
157+
const { chainId: permissionChainId, signerMeta } =
158+
existingPermission.permissionResponse;
159+
const { delegationManager } = signerMeta;
160+
161+
logger.debug(
162+
`Found permission - chainId: ${permissionChainId}, delegationManager: ${delegationManager ?? 'undefined'}`,
163+
);
164+
165+
// Check if the delegation is actually disabled on-chain
166+
if (!delegationManager) {
167+
throw new InvalidInputError(
168+
`No delegation manager found for delegation hash: ${delegationHash}`,
169+
);
170+
}
171+
172+
const isDelegationDisabled =
173+
await profileSyncManager.checkDelegationDisabledOnChain(
174+
delegationHash,
175+
permissionChainId,
176+
delegationManager,
177+
);
178+
179+
if (!isDelegationDisabled) {
180+
throw new InvalidInputError(
181+
`Delegation ${delegationHash} is not disabled on-chain. Cannot process revocation.`,
182+
);
183+
}
184+
185+
logger.debug(
186+
`Processing revocation for delegation ${delegationHash} - validated on-chain`,
187+
);
188+
189+
// Update the permission's revocation status using the optimized method
190+
// This avoids re-fetching the permission we already have
191+
await profileSyncManager.updatePermissionRevocationStatusWithPermission(
192+
existingPermission,
193+
true,
194+
);
195+
196+
return { success: true };
197+
};
198+
117199
return {
118200
grantPermission,
119201
getPermissionOffers,
120202
getGrantedPermissions,
203+
submitRevocation,
121204
};
122205
}

packages/gator-permissions-snap/src/rpc/rpcMethod.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ export enum RpcMethod {
1616
* This method is used by Metamask clients to retrieve granted permissions for all sites.
1717
*/
1818
PermissionsProviderGetGrantedPermissions = 'permissionsProvider_getGrantedPermissions',
19+
20+
/**
21+
* This method is used by MetaMask origin to submit a revocation and update the isRevoked flag.
22+
*/
23+
PermissionsProviderSubmitRevocation = 'permissionsProvider_submitRevocation',
1924
}

0 commit comments

Comments
 (0)