Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ linkStyle default opacity:0.5
gas_fee_controller --> polling_controller;
gator_permissions_controller --> base_controller;
gator_permissions_controller --> messenger;
gator_permissions_controller --> network_controller;
gator_permissions_controller --> transaction_controller;
geolocation_controller --> base_controller;
geolocation_controller --> controller_utils;
Expand Down
1 change: 1 addition & 0 deletions packages/gator-permissions-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Add `status` to `PermissionInfoWithMetadata` type, resolved from onchain data ([#8445](https://github.com/MetaMask/core/pull/8445))
- Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373))
- Bump `@metamask/transaction-controller` from `^64.0.0` to `^64.2.0` ([#8432](https://github.com/MetaMask/core/pull/8432), [#8447](https://github.com/MetaMask/core/pull/8447))
- Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457))
Expand Down
2 changes: 2 additions & 0 deletions packages/gator-permissions-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@
},
"dependencies": {
"@metamask/7715-permission-types": "^0.5.0",
"@metamask/abi-utils": "^2.0.3",
"@metamask/base-controller": "^9.1.0",
"@metamask/delegation-core": "^0.2.0",
"@metamask/delegation-deployments": "^0.12.0",
"@metamask/messenger": "^1.1.1",
"@metamask/network-controller": "^30.0.1",
"@metamask/snaps-controllers": "^19.0.0",
"@metamask/snaps-sdk": "^11.0.0",
"@metamask/snaps-utils": "^12.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { deriveStateFromMetadata } from '@metamask/base-controller';
import {
createTimestampTerms,
createNativeTokenStreamingTerms,
encodeDelegations,
ROOT_AUTHORITY,
} from '@metamask/delegation-core';
import {
Expand Down Expand Up @@ -35,11 +36,50 @@ import type { GatorPermissionsControllerMessenger } from './GatorPermissionsCont
import { GatorPermissionsController } from './GatorPermissionsController';
import type {
PermissionInfoWithMetadata,
GatorPermissionStatus,
StoredGatorPermission,
RevocationParams,
SupportedPermissionType,
} from './types';

const PERMISSION_STATUSES: GatorPermissionStatus[] = [
'Active',
'Revoked',
'Expired',
];

/**
* Default JSON-RPC behavior for permission status sync tests (disabled = false,
* latest block far in the future).
*
* @returns A Jest mock function suitable as a provider `request` implementation.
*/
function createDefaultPermissionStatusProviderRequest(): jest.MockedFunction<
(args: { method: string; params?: unknown[] }) => Promise<unknown>
> {
return jest.fn(async (req) => {
if (req.method === 'eth_call') {
return '0x0000000000000000000000000000000000000000000000000000000000000000';
}
if (req.method === 'eth_getBlockByNumber') {
return { timestamp: '0x77359400' };
}
throw new Error(`Unexpected RPC method in tests: ${req.method}`);
});
}

/**
* Handlers last wired by {@link getRootMessenger} for NetworkController actions
* (assertable when sync must resolve on-chain permission status).
*/
let lastNetworkControllerTestMocks: {
findNetworkClientIdByChainId: jest.Mock;
getNetworkClientById: jest.Mock;
permissionStatusProviderRequest: jest.MockedFunction<
(args: { method: string; params?: unknown[] }) => Promise<unknown>
>;
} | null = null;

const MOCK_CHAIN_ID_1: Hex = '0xaa36a7';
const MOCK_CHAIN_ID_2: Hex = '0x1';
const MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID =
Expand Down Expand Up @@ -217,6 +257,7 @@ describe('GatorPermissionsController', () => {
grantedPermissions.forEach((entry) => {
expect(entry.permissionResponse).toBeDefined();
expect(entry.siteOrigin).toBeDefined();
expect(PERMISSION_STATUSES).toContain(entry.status);
// Sanitized response omits internal fields (to, dependencies)
expect(
(entry.permissionResponse as Record<string, unknown>).to,
Expand Down Expand Up @@ -245,6 +286,142 @@ describe('GatorPermissionsController', () => {
]);
});

it('calls NetworkController to resolve on-chain status when snap returns a single-delegation context', async () => {
const frameworkContracts =
DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION][CHAIN_ID.sepolia];
const { NativeTokenStreamingEnforcer, DelegationManager } =
frameworkContracts;

const delegation = {
delegate: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63',
delegator: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778',
authority: ROOT_AUTHORITY,
caveats: [
{
enforcer: NativeTokenStreamingEnforcer,
terms: createNativeTokenStreamingTerms({
initialAmount: hexToBigInt('0x6f05b59d3b20000'),
maxAmount: hexToBigInt('0x22b1c8c1227a0000'),
amountPerSecond: hexToBigInt('0x6f05b59d3b20000'),
startTime: 1747699200,
}),
args: '0x',
},
],
salt: 0n,
signature: '0x' as Hex,
};
const encodedContext = encodeDelegations([delegation]);
const base = mockNativeTokenStreamStorageEntry(MOCK_CHAIN_ID_1);
const storedEntry = {
...base,
permissionResponse: {
...base.permissionResponse,
context: encodedContext,
delegationManager: DelegationManager,
},
};

const rootMessenger = getRootMessenger({
snapControllerHandleRequestActionHandler: jest
.fn()
.mockResolvedValue([storedEntry]),
});

const controller = new GatorPermissionsController({
messenger: getGatorPermissionsControllerMessenger(rootMessenger),
config: DEFAULT_TEST_CONFIG,
});

await rootMessenger.call(
'GatorPermissionsController:fetchAndUpdateGatorPermissions',
);

expect(lastNetworkControllerTestMocks).not.toBeNull();
expect(
lastNetworkControllerTestMocks?.findNetworkClientIdByChainId,
).toHaveBeenCalledWith(MOCK_CHAIN_ID_1);
expect(
lastNetworkControllerTestMocks?.getNetworkClientById,
).toHaveBeenCalledWith('test-network-client-id');
expect(
lastNetworkControllerTestMocks?.permissionStatusProviderRequest,
).toHaveBeenCalled();

expect(controller.state.grantedPermissions).toHaveLength(1);
expect(controller.state.grantedPermissions[0].status).toBe('Active');
});

it('defaults merged status to Active when persisted row omits status for the same context', async () => {
const frameworkContracts =
DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION][CHAIN_ID.sepolia];
const { NativeTokenStreamingEnforcer, DelegationManager } =
frameworkContracts;

const delegation = {
delegate: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63',
delegator: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778',
authority: ROOT_AUTHORITY,
caveats: [
{
enforcer: NativeTokenStreamingEnforcer,
terms: createNativeTokenStreamingTerms({
initialAmount: hexToBigInt('0x6f05b59d3b20000'),
maxAmount: hexToBigInt('0x22b1c8c1227a0000'),
amountPerSecond: hexToBigInt('0x6f05b59d3b20000'),
startTime: 1747699200,
}),
args: '0x',
},
],
salt: 0n,
signature: '0x' as Hex,
};
const encodedContext = encodeDelegations([delegation]);
const base = mockNativeTokenStreamStorageEntry(MOCK_CHAIN_ID_1);
const storedEntry = {
...base,
permissionResponse: {
...base.permissionResponse,
context: encodedContext,
delegationManager: DelegationManager,
},
};

const persistedRowMissingStatus = {
permissionResponse: {
chainId: storedEntry.permissionResponse.chainId,
from: storedEntry.permissionResponse.from,
permission: storedEntry.permissionResponse.permission,
context: encodedContext,
delegationManager: DelegationManager,
},
siteOrigin: storedEntry.siteOrigin,
} as PermissionInfoWithMetadata;

const rootMessenger = getRootMessenger({
snapControllerHandleRequestActionHandler: jest
.fn()
.mockResolvedValue([storedEntry]),
});

const controller = new GatorPermissionsController({
messenger: getGatorPermissionsControllerMessenger(rootMessenger),
config: DEFAULT_TEST_CONFIG,
state: {
grantedPermissions: [persistedRowMissingStatus],
lastSyncedTimestamp: 1,
},
});

await rootMessenger.call(
'GatorPermissionsController:fetchAndUpdateGatorPermissions',
);

expect(controller.state.grantedPermissions).toHaveLength(1);
expect(controller.state.grantedPermissions[0].status).toBe('Active');
});

it('categorizes erc20-token-revocation permissions into its own bucket', async () => {
const chainId = '0x1' as Hex;
// Create a minimal revocation permission entry and cast to satisfy types
Expand Down Expand Up @@ -285,6 +462,7 @@ describe('GatorPermissionsController', () => {
'erc20-token-revocation',
);
expect(grantedPermissions[0].permissionResponse.chainId).toBe(chainId);
expect(PERMISSION_STATUSES).toContain(grantedPermissions[0].status);
});

it('handles null permissions data', async () => {
Expand Down Expand Up @@ -1902,6 +2080,20 @@ function getRootMessenger({
namespace: MOCK_ANY_NAMESPACE,
});

const permissionStatusProviderRequest =
createDefaultPermissionStatusProviderRequest();
const findNetworkClientIdByChainId = jest
.fn()
.mockReturnValue('test-network-client-id');
const getNetworkClientById = jest.fn().mockReturnValue({
provider: { request: permissionStatusProviderRequest },
});
lastNetworkControllerTestMocks = {
findNetworkClientIdByChainId,
getNetworkClientById,
permissionStatusProviderRequest,
};

rootMessenger.registerActionHandler(
'SnapController:handleRequest',
snapControllerHandleRequestActionHandler,
Expand All @@ -1910,6 +2102,14 @@ function getRootMessenger({
'SnapController:hasSnap',
snapControllerHasActionHandler,
);
rootMessenger.registerActionHandler(
'NetworkController:findNetworkClientIdByChainId',
findNetworkClientIdByChainId,
);
rootMessenger.registerActionHandler(
'NetworkController:getNetworkClientById',
getNetworkClientById,
);
return rootMessenger;
}

Expand All @@ -1934,7 +2134,12 @@ function getGatorPermissionsControllerMessenger(
});
rootMessenger.delegate({
messenger: gatorPermissionsControllerMessenger,
actions: ['SnapController:handleRequest', 'SnapController:hasSnap'],
actions: [
'SnapController:handleRequest',
'SnapController:hasSnap',
'NetworkController:findNetworkClientIdByChainId',
'NetworkController:getNetworkClientById',
],
events: [
'TransactionController:transactionApproved',
'TransactionController:transactionRejected',
Expand Down
Loading
Loading