Skip to content

Commit e5e5d20

Browse files
committed
Add method decodePermissionFromPermissionContextForOrigin to GatorPermissionsController
- rejects any request from an origin other than the gator permission snap - attempts to identify the permission type, and decode the data - rejects any request where a unique permission is unable to be decoded Also renames GatorPermissionsController.test.ts
1 parent 5d74648 commit e5e5d20

File tree

8 files changed

+1551
-2
lines changed

8 files changed

+1551
-2
lines changed

packages/gator-permissions-controller/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
"dependencies": {
5050
"@metamask/7715-permission-types": "^0.3.0",
5151
"@metamask/base-controller": "^8.3.0",
52+
"@metamask/delegation-core": "^0.2.0-rc.1",
53+
"@metamask/delegation-deployments": "^0.12.0",
5254
"@metamask/snaps-sdk": "^9.0.0",
5355
"@metamask/snaps-utils": "^11.0.0",
5456
"@metamask/utils": "^11.4.2"

packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts renamed to packages/gator-permissions-controller/src/GatorPermissionsController.test.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ import { Messenger, deriveStateFromMetadata } from '@metamask/base-controller';
33
import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers';
44
import type { SnapId } from '@metamask/snaps-sdk';
55
import type { Hex } from '@metamask/utils';
6+
import {
7+
CHAIN_ID,
8+
DELEGATOR_CONTRACTS,
9+
} from '@metamask/delegation-deployments';
10+
import {
11+
createTimestampTerms,
12+
createNativeTokenStreamingTerms,
13+
encodeDelegations,
14+
} from '@metamask/delegation-core';
15+
import { toHex } from '@metamask/controller-utils';
616

717
import type { GatorPermissionsControllerMessenger } from './GatorPermissionsController';
818
import GatorPermissionsController from './GatorPermissionsController';
@@ -23,6 +33,7 @@ import type {
2333
ExtractAvailableAction,
2434
ExtractAvailableEvent,
2535
} from '../../base-controller/tests/helpers';
36+
import { DELEGATION_FRAMEWORK_VERSION } from './decodePermission';
2637

2738
const MOCK_CHAIN_ID_1: Hex = '0xaa36a7';
2839
const MOCK_CHAIN_ID_2: Hex = '0x1';
@@ -453,6 +464,179 @@ describe('GatorPermissionsController', () => {
453464
`);
454465
});
455466
});
467+
468+
describe('decodePermissionFromPermissionContextForOrigin', () => {
469+
const chainId = CHAIN_ID.sepolia;
470+
const contracts =
471+
DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION][chainId];
472+
473+
let controller: GatorPermissionsController;
474+
475+
beforeEach(() => {
476+
controller = new GatorPermissionsController({
477+
messenger: getMessenger(),
478+
});
479+
});
480+
481+
it('decodes a native-token-stream permission context successfully', async () => {
482+
const {
483+
TimestampEnforcer,
484+
NativeTokenStreamingEnforcer,
485+
ExactCalldataEnforcer,
486+
} = contracts;
487+
488+
const delegator = '0x1111111111111111111111111111111111111111';
489+
const delegate = '0x2222222222222222222222222222222222222222';
490+
491+
const timestampBeforeThreshold = 1720000;
492+
const expiryTerms = createTimestampTerms(
493+
{ timestampAfterThreshold: 0, timestampBeforeThreshold },
494+
{ out: 'hex' },
495+
);
496+
497+
const initialAmount = 123456n;
498+
const maxAmount = 999999n;
499+
const amountPerSecond = 1n;
500+
const startTime = 1715664;
501+
const streamTerms = createNativeTokenStreamingTerms(
502+
{ initialAmount, maxAmount, amountPerSecond, startTime },
503+
{ out: 'hex' },
504+
);
505+
506+
const caveats = [
507+
{
508+
enforcer: TimestampEnforcer,
509+
terms: expiryTerms,
510+
args: '0x',
511+
} as const,
512+
{
513+
enforcer: NativeTokenStreamingEnforcer,
514+
terms: streamTerms,
515+
args: '0x',
516+
} as const,
517+
{ enforcer: ExactCalldataEnforcer, terms: '0x', args: '0x' } as const,
518+
];
519+
520+
const permissionContext = encodeDelegations(
521+
[
522+
{
523+
delegate,
524+
delegator,
525+
authority: '0x',
526+
caveats,
527+
salt: 0n,
528+
signature: '0x',
529+
},
530+
],
531+
{ out: 'hex' },
532+
);
533+
534+
const result =
535+
await controller.decodePermissionFromPermissionContextForOrigin({
536+
origin: controller.permissionsProviderSnapId,
537+
chainId,
538+
permissionContext,
539+
});
540+
541+
expect(result.chainId).toBe(toHex(chainId));
542+
expect(result.address).toBe(delegator);
543+
expect(result.signer).toStrictEqual({
544+
type: 'account',
545+
data: { address: delegate },
546+
});
547+
expect(result.permission.type).toBe('native-token-stream');
548+
expect(result.expiry).toBe(timestampBeforeThreshold);
549+
expect(result.permission.data).toStrictEqual({
550+
initialAmount,
551+
maxAmount,
552+
amountPerSecond,
553+
startTime,
554+
});
555+
});
556+
557+
it('throws when origin does not match permissions provider', async () => {
558+
await expect(
559+
controller.decodePermissionFromPermissionContextForOrigin({
560+
origin: 'not-the-provider',
561+
chainId: 1,
562+
permissionContext: '0x',
563+
}),
564+
).rejects.toThrow('Origin not-the-provider not allowed');
565+
});
566+
567+
it('throws when the permission context contains multiple delegations', async () => {
568+
const permissionContext = encodeDelegations(
569+
[
570+
{
571+
delegate: '0x1',
572+
delegator: '0x2',
573+
authority: '0x',
574+
caveats: [],
575+
salt: 0n,
576+
signature: '0x',
577+
},
578+
{
579+
delegate: '0x3',
580+
delegator: '0x4',
581+
authority: '0x',
582+
caveats: [],
583+
salt: 0n,
584+
signature: '0x',
585+
},
586+
],
587+
{ out: 'hex' },
588+
);
589+
590+
await expect(
591+
controller.decodePermissionFromPermissionContextForOrigin({
592+
origin: controller.permissionsProviderSnapId,
593+
chainId: 1,
594+
permissionContext,
595+
}),
596+
).rejects.toThrow('Failed to decode permission');
597+
});
598+
599+
it('throws when enforcers do not identify a supported permission', async () => {
600+
const { TimestampEnforcer, ValueLteEnforcer } = contracts;
601+
602+
const expiryTerms = createTimestampTerms(
603+
{ timestampAfterThreshold: 0, timestampBeforeThreshold: 100 },
604+
{ out: 'hex' },
605+
);
606+
607+
const caveats = [
608+
{
609+
enforcer: TimestampEnforcer,
610+
terms: expiryTerms,
611+
args: '0x',
612+
} as const,
613+
// Include a forbidden/irrelevant enforcer without required counterparts
614+
{ enforcer: ValueLteEnforcer, terms: '0x', args: '0x' } as const,
615+
];
616+
617+
const permissionContext = encodeDelegations(
618+
[
619+
{
620+
delegate: '0x1111111111111111111111111111111111111111',
621+
delegator: '0x2222222222222222222222222222222222222222',
622+
authority: '0x',
623+
caveats,
624+
salt: 0n,
625+
signature: '0x',
626+
},
627+
],
628+
{ out: 'hex' },
629+
);
630+
631+
await expect(
632+
controller.decodePermissionFromPermissionContextForOrigin({
633+
origin: controller.permissionsProviderSnapId,
634+
chainId,
635+
permissionContext,
636+
}),
637+
).rejects.toThrow('Unable to identify permission type');
638+
});
639+
});
456640
});
457641

458642
/**

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

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Signer } from '@metamask/7715-permission-types';
1+
import type { Hex, Signer } from '@metamask/7715-permission-types';
22
import type {
33
RestrictedMessenger,
44
ControllerGetStateAction,
@@ -14,6 +14,8 @@ import {
1414
GatorPermissionsFetchError,
1515
GatorPermissionsNotEnabledError,
1616
GatorPermissionsProviderError,
17+
OriginNotAllowedError,
18+
PermissionDecodingError,
1719
} from './errors';
1820
import { controllerLog } from './logger';
1921
import type { StoredGatorPermissionSanitized } from './types';
@@ -27,6 +29,13 @@ import {
2729
deserializeGatorPermissionsMap,
2830
serializeGatorPermissionsMap,
2931
} from './utils';
32+
import { decodeDelegations } from '@metamask/delegation-core';
33+
import {
34+
DecodedPermission,
35+
getPermissionDataAndExpiry,
36+
identifyPermissionByEnforcers,
37+
reconstructDecodedPermission,
38+
} from './decodePermission';
3039

3140
// === GENERAL ===
3241

@@ -155,6 +164,12 @@ export type GatorPermissionsControllerDisableGatorPermissionsAction = {
155164
handler: GatorPermissionsController['disableGatorPermissions'];
156165
};
157166

167+
export type GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction =
168+
{
169+
type: `${typeof controllerName}:decodePermissionFromPermissionContextForOrigin`;
170+
handler: GatorPermissionsController['decodePermissionFromPermissionContextForOrigin'];
171+
};
172+
158173
/**
159174
* All actions that {@link GatorPermissionsController} registers, to be called
160175
* externally.
@@ -163,7 +178,8 @@ export type GatorPermissionsControllerActions =
163178
| GatorPermissionsControllerGetStateAction
164179
| GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction
165180
| GatorPermissionsControllerEnableGatorPermissionsAction
166-
| GatorPermissionsControllerDisableGatorPermissionsAction;
181+
| GatorPermissionsControllerDisableGatorPermissionsAction
182+
| GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction;
167183

168184
/**
169185
* All actions that {@link GatorPermissionsController} calls internally.
@@ -268,6 +284,11 @@ export default class GatorPermissionsController extends BaseController<
268284
`${controllerName}:disableGatorPermissions`,
269285
this.disableGatorPermissions.bind(this),
270286
);
287+
288+
this.messagingSystem.registerActionHandler(
289+
`${controllerName}:decodePermissionFromPermissionContextForOrigin`,
290+
this.decodePermissionFromPermissionContextForOrigin.bind(this),
291+
);
271292
}
272293

273294
/**
@@ -490,4 +511,73 @@ export default class GatorPermissionsController extends BaseController<
490511
this.#setIsFetchingGatorPermissions(false);
491512
}
492513
}
514+
515+
/**
516+
* Decodes a permission context into a structured permission for a specific origin.
517+
*
518+
* This method validates the caller origin, decodes the provided `permissionContext`
519+
* into delegations, identifies the permission type from the caveat enforcers,
520+
* extracts the permission-specific data and expiry, and reconstructs a
521+
* {@link DecodedPermission} containing chainId, account addresses, signer, type and data.
522+
*
523+
* @param origin - The caller's origin; must match the configured permissions provider Snap id.
524+
* @param chainId - Numeric EIP-155 chain id used for resolving enforcer contracts and encoding.
525+
* @param permissionContext - Encoded delegation payload (hex) containing the permission.
526+
* @returns A decoded permission object suitable for UI consumption and follow-up actions.
527+
* @throws If the origin is not allowed, the context cannot be decoded into exactly one delegation,
528+
* or the enforcers/terms do not match a supported permission type.
529+
*/
530+
public async decodePermissionFromPermissionContextForOrigin({
531+
origin,
532+
chainId,
533+
permissionContext,
534+
}: {
535+
origin: string;
536+
chainId: number;
537+
permissionContext: Hex;
538+
}): Promise<DecodedPermission> {
539+
if (origin !== this.permissionsProviderSnapId) {
540+
throw new OriginNotAllowedError({ origin });
541+
}
542+
543+
const delegations = decodeDelegations(permissionContext);
544+
545+
if (delegations.length !== 1) {
546+
throw new PermissionDecodingError({
547+
cause: 'Multiple delegations found',
548+
});
549+
}
550+
551+
const [{ caveats, delegator, delegate }] = delegations;
552+
553+
try {
554+
const enforcers = caveats.map((caveat) => caveat.enforcer);
555+
556+
const permissionType = identifyPermissionByEnforcers({
557+
enforcers,
558+
chainId,
559+
});
560+
561+
const { expiry, data } = getPermissionDataAndExpiry({
562+
chainId,
563+
caveats,
564+
permissionType,
565+
});
566+
567+
const permission = reconstructDecodedPermission({
568+
chainId,
569+
permissionType,
570+
delegator,
571+
delegate,
572+
expiry,
573+
data,
574+
});
575+
576+
return permission;
577+
} catch (error) {
578+
throw new PermissionDecodingError({
579+
cause: error as Error,
580+
});
581+
}
582+
}
493583
}

0 commit comments

Comments
 (0)