From 5eaf7d8666ed8270fe3c9e99818728d405ffd331 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:35:25 -0400 Subject: [PATCH 01/24] Add GatorPermissionsController package --- .../gator-permissions-controller/CHANGELOG.md | 13 + packages/gator-permissions-controller/LICENSE | 20 + .../gator-permissions-controller/README.md | 37 ++ .../jest.config.js | 26 ++ .../gator-permissions-controller/package.json | 75 ++++ .../GatorPermissionsController.ts | 348 ++++++++++++++++++ .../gator-permissions-controller/src/index.ts | 16 + .../tsconfig.build.json | 13 + .../tsconfig.json | 11 + .../gator-permissions-controller/typedoc.json | 7 + 10 files changed, 566 insertions(+) create mode 100644 packages/gator-permissions-controller/CHANGELOG.md create mode 100644 packages/gator-permissions-controller/LICENSE create mode 100644 packages/gator-permissions-controller/README.md create mode 100644 packages/gator-permissions-controller/jest.config.js create mode 100644 packages/gator-permissions-controller/package.json create mode 100644 packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts create mode 100644 packages/gator-permissions-controller/src/index.ts create mode 100644 packages/gator-permissions-controller/tsconfig.build.json create mode 100644 packages/gator-permissions-controller/tsconfig.json create mode 100644 packages/gator-permissions-controller/typedoc.json diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md new file mode 100644 index 0000000000..a22b0d308c --- /dev/null +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] + +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gator-permissions-controllerr@0.1.0...HEAD + diff --git a/packages/gator-permissions-controller/LICENSE b/packages/gator-permissions-controller/LICENSE new file mode 100644 index 0000000000..7d002dced3 --- /dev/null +++ b/packages/gator-permissions-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/gator-permissions-controller/README.md b/packages/gator-permissions-controller/README.md new file mode 100644 index 0000000000..042ed211b8 --- /dev/null +++ b/packages/gator-permissions-controller/README.md @@ -0,0 +1,37 @@ +# `@metamask/gator-permissions-controller` + +A dedicated controller for reading gator permissions from profile sync storage. This controller fetches data from the encrypted user storage database and caches it locally, providing fast access to permissions across devices while maintaining privacy through client-side encryption. + +## Installation + +`yarn add @metamask/gator-permissions-controller` + +or + +`npm install @metamask/gator-permissions-controller` + +## Usage + +### Basic Setup + +```typescript +import { GatorPermissionsController } from '@metamask/gator-permissions-controller'; + +// Create the controller +const gatorPermissionsController = new GatorPermissionsController({ + messenger: yourMessenger, +}); + +// Enable the feature (requires authentication) +gatorPermissionsController.enableGatorPermissions(); +``` + +### Fetch from Profile Sync + +```typescript +const permissions = await gatorPermissionsController.fetchAndUpdateGatorPermissions(); +``` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). \ No newline at end of file diff --git a/packages/gator-permissions-controller/jest.config.js b/packages/gator-permissions-controller/jest.config.js new file mode 100644 index 0000000000..ca08413339 --- /dev/null +++ b/packages/gator-permissions-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json new file mode 100644 index 0000000000..d3ef5864a9 --- /dev/null +++ b/packages/gator-permissions-controller/package.json @@ -0,0 +1,75 @@ +{ + "name": "@metamask/gator-permissions-controller", + "version": "0.0.0", + "description": "Controller for managing gator permissions with profile sync integration", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/gator-permissions-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/gator-permissions-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/gator-permissions-controller", + "publish:preview": "yarn npm publish --tag preview", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^8.0.1", + "loglevel": "^1.8.1" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/profile-sync-controller": "^19.0.0", + "@ts-bridge/cli": "^0.6.1", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/profile-sync-controller": "^19.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} \ No newline at end of file diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts new file mode 100644 index 0000000000..d80e6fdb57 --- /dev/null +++ b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts @@ -0,0 +1,348 @@ +import type { + RestrictedMessenger, + ControllerGetStateAction, + ControllerStateChangeEvent, + StateMetadata, +} from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller'; +import type { AuthenticationController, UserStorageController } from '@metamask/profile-sync-controller'; +import log from 'loglevel'; + +// Unique name for the controller +const controllerName = 'GatorPermissionsController'; + +/** + * State shape for GatorPermissionsController + */ +export type GatorPermissionsControllerState = { + /** + * Flag that indicates if the gator permissions feature is enabled + */ + isGatorPermissionsEnabled: boolean; + + /** + * List of gator permissions cached from profile sync + */ + gatorPermissionsList: GatorPermission[]; + + /** + * Flag that indicates that fetching permissions is in progress + * This is used to show a loading spinner in the UI + */ + isFetchingGatorPermissions: boolean; + + /** + * Flag that indicates that updating gator permissions is in progress + */ + isUpdatingGatorPermissions: boolean; +}; + +/** + * Represents a gator permission entry + */ +export type GatorPermission = any; + +const metadata: StateMetadata = { + isGatorPermissionsEnabled: { + persist: true, + anonymous: false, + }, + gatorPermissionsList: { + persist: true, + anonymous: true, + }, + isFetchingGatorPermissions: { + persist: false, + anonymous: false, + }, + isUpdatingGatorPermissions: { + persist: false, + anonymous: false, + }, +}; + +export const defaultState: GatorPermissionsControllerState = { + isGatorPermissionsEnabled: false, + gatorPermissionsList: [], + isUpdatingGatorPermissions: false, + isFetchingGatorPermissions: false, +}; + +export type GatorPermissionsControllerGetStateAction = + ControllerGetStateAction< + typeof controllerName, + GatorPermissionsControllerState + >; + +export type GatorPermissionsControllerFetchPermissions = { + type: `${typeof controllerName}:fetchPermissions`; + handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; +}; + +export type GatorPermissionsControllerEnablePermissions = { + type: `${typeof controllerName}:enablePermissions`; + handler: GatorPermissionsController['enableGatorPermissions']; +}; + +export type GatorPermissionsControllerDisablePermissions = { + type: `${typeof controllerName}:disablePermissions`; + handler: GatorPermissionsController['disableGatorPermissions']; +}; + +// Messenger Actions +export type Actions = + | GatorPermissionsControllerGetStateAction + | GatorPermissionsControllerFetchPermissions + | GatorPermissionsControllerEnablePermissions + | GatorPermissionsControllerDisablePermissions; + +// Allowed Actions +export type AllowedActions = + // Auth Controller Requests + | AuthenticationController.AuthenticationControllerGetBearerToken + | AuthenticationController.AuthenticationControllerIsSignedIn + | AuthenticationController.AuthenticationControllerPerformSignIn + // User Storage Controller Requests + | UserStorageController.UserStorageControllerPerformGetStorageAllFeatureEntries; + +// Events +export type GatorPermissionsControllerStateChangeEvent = + ControllerStateChangeEvent< + typeof controllerName, + GatorPermissionsControllerState + >; + +export type PermissionsListUpdatedEvent = { + type: `${typeof controllerName}:permissionsListUpdated`; + payload: [GatorPermission[]]; +}; + +export type Events = + | GatorPermissionsControllerStateChangeEvent + | PermissionsListUpdatedEvent; + +// Allowed Events +export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; + +// Type for the messenger of GatorPermissionsController +export type GatorPermissionsControllerMessenger = RestrictedMessenger< + typeof controllerName, + Actions | AllowedActions, + Events | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +/** + * Controller that manages gator permissions by reading from profile sync + */ +export default class GatorPermissionsController extends BaseController< + typeof controllerName, + GatorPermissionsControllerState, + GatorPermissionsControllerMessenger +> { + readonly #auth = { + getBearerToken: async () => { + return await this.messagingSystem.call( + 'AuthenticationController:getBearerToken', + ); + }, + isSignedIn: () => { + return this.messagingSystem.call('AuthenticationController:isSignedIn'); + }, + signIn: async () => { + return await this.messagingSystem.call( + 'AuthenticationController:performSignIn', + ); + }, + }; + + /** + * Creates a GatorPermissionsController instance. + * + * @param args - The arguments to this function. + * @param args.messenger - Messenger used to communicate with BaseV2 controller. + * @param args.state - Initial state to set on this controller. + */ + constructor({ + messenger, + state, + }: { + messenger: GatorPermissionsControllerMessenger; + state?: Partial; + }) { + super({ + messenger, + metadata, + name: controllerName, + state: { ...defaultState, ...state }, + }); + + this.#registerMessageHandlers(); + this.#clearLoadingStates(); + } + + #setIsFetchingGatorPermissions(isFetchingGatorPermissions: boolean) { + this.update((state) => { + state.isFetchingGatorPermissions = isFetchingGatorPermissions; + }); + } + + #setIsGatorPermissionsEnabled(isGatorPermissionsEnabled: boolean) { + this.update((state) => { + state.isGatorPermissionsEnabled = isGatorPermissionsEnabled; + }); + } + + #setIsUpdatingGatorPermissions(isUpdatingGatorPermissions: boolean) { + this.update((state) => { + state.isUpdatingGatorPermissions = isUpdatingGatorPermissions; + }); + } + + #registerMessageHandlers(): void { + this.messagingSystem.registerActionHandler( + `${controllerName}:fetchPermissions`, + this.fetchAndUpdateGatorPermissions.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `${controllerName}:enablePermissions`, + this.enableGatorPermissions.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `${controllerName}:disablePermissions`, + this.disableGatorPermissions.bind(this), + ); + } + + #clearLoadingStates(): void { + this.update((state) => { + state.isUpdatingGatorPermissions = false; + state.isFetchingGatorPermissions = false; + state.isUpdatingGatorPermissionsKey = []; + }); + } + + /** + * Asserts that the gator permissions are enabled. + * @throws {Error} If the gator permissions are not enabled. + */ + #assertGatorPermissionsEnabled() { + if (!this.state.isGatorPermissionsEnabled) { + throw new Error('Gator permissions are not enabled'); + } + } + + /** + * Enables authentication if not already enabled. + * @throws {Error} If there is an error during the process. + */ + async #enableAuth() { + const isSignedIn = this.#auth.isSignedIn(); + if (!isSignedIn) { + await this.#auth.signIn(); + } + } + + /** + * Enables gator permissions for the user. + * This method ensures that the user is authenticated and enables the feature. + * + * @throws {Error} If there is an error during the process of enabling permissions. + */ + public async enableGatorPermissions( + isFetchingPermissions: boolean = true, + ) { + try { + this.#setIsUpdatingGatorPermissions(true); + await this.#enableAuth(); + this.#setIsGatorPermissionsEnabled(true); + + // Fetch initial permissions after enabling + if (isFetchingPermissions) { + await this.fetchAndUpdateGatorPermissions(); + } + } catch (e) { + log.error('Unable to enable gator permissions', e); + throw new Error('Unable to enable gator permissions'); + } finally { + this.#setIsUpdatingGatorPermissions(false); + } + } + + /** + * Disables gator permissions for the user. + * This method clears the permissions list and disables the feature. + * + * @throws {Error} If there is an error during the process. + */ + public async disableGatorPermissions() { + // Clear permissions from state + this.update((state) => { + state.isGatorPermissionsEnabled = false; + state.gatorPermissionsList = []; + }); + + this.messagingSystem.publish( + `${controllerName}:permissionsListUpdated`, + [], + ); + } + + /** + * Fetches the list of gator permissions from profile sync and updates the state. + * This is the main method that reads data from profile sync and caches it. + * + * @returns A promise that resolves to the list of permissions. + * @throws {Error} Throws an error if unauthenticated or from other operations. + */ + public async fetchAndUpdateGatorPermissions(): Promise { + try { + this.#setIsFetchingGatorPermissions(true); + this.#assertGatorPermissionsEnabled(); + + // Read all permissions from profile sync + await this.#enableAuth(); + const permissionsData = await this.messagingSystem.call( + 'UserStorageController:performGetStorageAllFeatureEntries', + 'gator_7715_permissions', + ); + + if (!permissionsData) { + this.update((state) => { + state.gatorPermissionsList = []; + }); + return []; + } + + const permissions: GatorPermission[] = []; + for (const permissionString of permissionsData) { + try { + const permission = JSON.parse(permissionString) as GatorPermission; + permissions.push(permission); + } catch (e) { + log.error('Failed to parse permission data:', e); + } + } + + // Update state with fetched permissions + this.update((state) => { + state.gatorPermissionsList = permissions; + }); + + this.messagingSystem.publish( + `${controllerName}:permissionsListUpdated`, + this.state.gatorPermissionsList, + ); + + return permissions; + } catch (err) { + log.error('Failed to fetch gator permissions', err); + throw new Error('Failed to fetch gator permissions'); + } finally { + this.#setIsFetchingGatorPermissions(false); + } + } +} \ No newline at end of file diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts new file mode 100644 index 0000000000..9f022bc29c --- /dev/null +++ b/packages/gator-permissions-controller/src/index.ts @@ -0,0 +1,16 @@ +export { default as GatorPermissionsController } from './GatorPermissionsController/GatorPermissionsController'; +export type { + GatorPermissionsControllerState, + GatorPermission, + GatorPermissionsControllerMessenger, + GatorPermissionsControllerGetStateAction, + GatorPermissionsControllerFetchPermissions, + GatorPermissionsControllerEnablePermissions, + GatorPermissionsControllerDisablePermissions, + Actions, + AllowedActions, + Events, + AllowedEvents, + GatorPermissionsControllerStateChangeEvent, + PermissionsListUpdatedEvent, +} from './GatorPermissionsController/GatorPermissionsController'; \ No newline at end of file diff --git a/packages/gator-permissions-controller/tsconfig.build.json b/packages/gator-permissions-controller/tsconfig.build.json new file mode 100644 index 0000000000..ae8fdda2dd --- /dev/null +++ b/packages/gator-permissions-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../profile-sync-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/gator-permissions-controller/tsconfig.json b/packages/gator-permissions-controller/tsconfig.json new file mode 100644 index 0000000000..6efb36b7b6 --- /dev/null +++ b/packages/gator-permissions-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../profile-sync-controller" }, + ], + "include": ["../../types", "./src"] +} diff --git a/packages/gator-permissions-controller/typedoc.json b/packages/gator-permissions-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/gator-permissions-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} From b8008de7333eb9cfe89ba853a1790c935e3f6911 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:26:59 -0400 Subject: [PATCH 02/24] Add gator permissions controller to root ts build config and dependency graph readme --- .github/CODEOWNERS | 5 +++ README.md | 4 +++ .../gator-permissions-controller/CHANGELOG.md | 5 +-- .../gator-permissions-controller/package.json | 5 ++- .../GatorPermissionsController.ts | 32 ++++++++++--------- teams.json | 1 + tsconfig.build.json | 1 + tsconfig.json | 1 + yarn.lock | 20 ++++++++++++ 9 files changed, 52 insertions(+), 22 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 709ef62807..01a8f2755d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,6 +26,9 @@ /packages/transaction-controller @MetaMask/confirmations /packages/user-operation-controller @MetaMask/confirmations +## Delegation Team +/packages/gator-permissions-controller @MetaMask/delegation + ## Earn Team /packages/earn-controller @MetaMask/earn ## Notifications Team @@ -111,6 +114,8 @@ /packages/ens-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/gas-fee-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/gas-fee-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers +/packages/gator-permissions-controller/package.json @MetaMask/delegation @MetaMask/wallet-framework-engineers +/packages/gator-permissions-controller/CHANGELOG.md @MetaMask/delegation @MetaMask/wallet-framework-engineers /packages/keyring-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/keyring-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/logging-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers diff --git a/README.md b/README.md index 0ad9ee8c9c..53e899da74 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/eth-json-rpc-provider`](packages/eth-json-rpc-provider) - [`@metamask/foundryup`](packages/foundryup) - [`@metamask/gas-fee-controller`](packages/gas-fee-controller) +- [`@metamask/gator-permissions-controller`](packages/gator-permissions-controller) - [`@metamask/json-rpc-engine`](packages/json-rpc-engine) - [`@metamask/json-rpc-middleware-stream`](packages/json-rpc-middleware-stream) - [`@metamask/keyring-controller`](packages/keyring-controller) @@ -99,6 +100,7 @@ linkStyle default opacity:0.5 eth_json_rpc_provider(["@metamask/eth-json-rpc-provider"]); foundryup(["@metamask/foundryup"]); gas_fee_controller(["@metamask/gas-fee-controller"]); + gator-permissions-controller(["@metamask/gator-permissions-controller"]); json_rpc_engine(["@metamask/json-rpc-engine"]); json_rpc_middleware_stream(["@metamask/json-rpc-middleware-stream"]); keyring_controller(["@metamask/keyring-controller"]); @@ -194,6 +196,8 @@ linkStyle default opacity:0.5 gas_fee_controller --> controller_utils; gas_fee_controller --> polling_controller; gas_fee_controller --> network_controller; + gator-permissions-controller --> base_controller; + gator-permissions-controller --> profile_sync_controller; json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; logging_controller --> base_controller; diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index a22b0d308c..670f270b1c 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,7 +7,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.0] - -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gator-permissions-controllerr@0.1.0...HEAD - +[Unreleased]: https://github.com/MetaMask/core/ \ No newline at end of file diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index d3ef5864a9..b0f5de4c95 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -47,8 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.0.1", - "loglevel": "^1.8.1" + "@metamask/base-controller": "^8.0.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", @@ -72,4 +71,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} \ No newline at end of file +} diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts index d80e6fdb57..c5badb5dc6 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts @@ -5,8 +5,10 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import type { AuthenticationController, UserStorageController } from '@metamask/profile-sync-controller'; -import log from 'loglevel'; +import type { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; // Unique name for the controller const controllerName = 'GatorPermissionsController'; @@ -37,10 +39,11 @@ export type GatorPermissionsControllerState = { isUpdatingGatorPermissions: boolean; }; +// TODO: Add type for gator permission /** * Represents a gator permission entry */ -export type GatorPermission = any; +export type GatorPermission = unknown; const metadata: StateMetadata = { isGatorPermissionsEnabled: { @@ -68,11 +71,10 @@ export const defaultState: GatorPermissionsControllerState = { isFetchingGatorPermissions: false, }; -export type GatorPermissionsControllerGetStateAction = - ControllerGetStateAction< - typeof controllerName, - GatorPermissionsControllerState - >; +export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + GatorPermissionsControllerState +>; export type GatorPermissionsControllerFetchPermissions = { type: `${typeof controllerName}:fetchPermissions`; @@ -237,6 +239,7 @@ export default class GatorPermissionsController extends BaseController< /** * Enables authentication if not already enabled. + * * @throws {Error} If there is an error during the process. */ async #enableAuth() { @@ -250,11 +253,10 @@ export default class GatorPermissionsController extends BaseController< * Enables gator permissions for the user. * This method ensures that the user is authenticated and enables the feature. * + * @param isFetchingPermissions - Whether to fetch permissions after enabling. * @throws {Error} If there is an error during the process of enabling permissions. */ - public async enableGatorPermissions( - isFetchingPermissions: boolean = true, - ) { + public async enableGatorPermissions(isFetchingPermissions: boolean = true) { try { this.#setIsUpdatingGatorPermissions(true); await this.#enableAuth(); @@ -265,7 +267,7 @@ export default class GatorPermissionsController extends BaseController< await this.fetchAndUpdateGatorPermissions(); } } catch (e) { - log.error('Unable to enable gator permissions', e); + console.error('Unable to enable gator permissions', e); throw new Error('Unable to enable gator permissions'); } finally { this.#setIsUpdatingGatorPermissions(false); @@ -323,7 +325,7 @@ export default class GatorPermissionsController extends BaseController< const permission = JSON.parse(permissionString) as GatorPermission; permissions.push(permission); } catch (e) { - log.error('Failed to parse permission data:', e); + console.error('Failed to parse permission data:', e); } } @@ -339,10 +341,10 @@ export default class GatorPermissionsController extends BaseController< return permissions; } catch (err) { - log.error('Failed to fetch gator permissions', err); + console.error('Failed to fetch gator permissions', err); throw new Error('Failed to fetch gator permissions'); } finally { this.#setIsFetchingGatorPermissions(false); } } -} \ No newline at end of file +} diff --git a/teams.json b/teams.json index 10adc9abbd..d3c2253f6d 100644 --- a/teams.json +++ b/teams.json @@ -36,6 +36,7 @@ "metamask/preferences-controller": "team-wallet-framework", "metamask/profile-sync-controller": "team-notifications", "metamask/rate-limit-controller": "team-snaps-platform", + "metamask/gator-permissions-controller": "team-readable-permissions", "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", "metamask/sample-controllers": "team-wallet-framework", "metamask/selected-network-controller": "team-wallet-api-platform,team-wallet-framework,team-assets", diff --git a/tsconfig.build.json b/tsconfig.build.json index 18ee0a6622..eb92ef8e0b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -22,6 +22,7 @@ { "path": "./packages/eth-json-rpc-provider/tsconfig.build.json" }, { "path": "./packages/foundryup/tsconfig.build.json" }, { "path": "./packages/gas-fee-controller/tsconfig.build.json" }, + { "path": "./packages/gator-permissions-controller/tsconfig.build.json" }, { "path": "./packages/json-rpc-engine/tsconfig.build.json" }, { "path": "./packages/json-rpc-middleware-stream/tsconfig.build.json" }, { "path": "./packages/keyring-controller/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 1830b6b33c..7387e99713 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,7 @@ { "path": "./packages/eth-json-rpc-provider" }, { "path": "./packages/foundryup" }, { "path": "./packages/gas-fee-controller" }, + { "path": "./packages/gator-permissions-controller" }, { "path": "./packages/json-rpc-engine" }, { "path": "./packages/json-rpc-middleware-stream" }, { "path": "./packages/keyring-controller" }, diff --git a/yarn.lock b/yarn.lock index 1a6083c4f8..e3944947f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3564,6 +3564,26 @@ __metadata: languageName: unknown linkType: soft +"@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller": + version: 0.0.0-use.local + resolution: "@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.1" + "@metamask/profile-sync-controller": "npm:^19.0.0" + "@ts-bridge/cli": "npm:^0.6.1" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/profile-sync-controller": ^19.0.0 + languageName: unknown + linkType: soft + "@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" From c12992e74cdf7041b9179a13ebbf594568799a34 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:38:14 -0400 Subject: [PATCH 03/24] Add ERC-7715 type definitions and increase test coverage --- .../gator-permissions-controller/package.json | 5 +- .../src/GatorPermissionContoller.test.ts | 573 ++++++++++++++++++ .../GatorPermissionsController.ts | 221 ++++--- .../gator-permissions-controller/src/index.ts | 17 +- .../gator-permissions-controller/src/types.ts | 192 ++++++ .../src/utils.test.ts | 42 ++ .../gator-permissions-controller/src/utils.ts | 25 + .../src/shared/storage-schema.ts | 2 + teams.json | 2 +- yarn.lock | 3 + 10 files changed, 998 insertions(+), 84 deletions(-) create mode 100644 packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts rename packages/gator-permissions-controller/src/{GatorPermissionsController => }/GatorPermissionsController.ts (61%) create mode 100644 packages/gator-permissions-controller/src/types.ts create mode 100644 packages/gator-permissions-controller/src/utils.test.ts create mode 100644 packages/gator-permissions-controller/src/utils.ts diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index b0f5de4c95..f21985d252 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -47,9 +47,12 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.0.1" + "@metamask/base-controller": "^8.0.1", + "@metamask/utils": "^11.2.0" }, "devDependencies": { + "@lavamoat/allow-scripts": "^3.0.4", + "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/profile-sync-controller": "^19.0.0", "@ts-bridge/cli": "^0.6.1", diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts new file mode 100644 index 0000000000..9ebc4b132d --- /dev/null +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -0,0 +1,573 @@ +import { Messenger } from '@metamask/base-controller'; + +import type { + AllowedActions, + AllowedEvents, +} from './GatorPermissionsController'; +import GatorPermissionsController from './GatorPermissionsController'; +import type { + AccountSigner, + Erc20TokenStreamPermission, + NativeTokenPeriodicPermission, + NativeTokenStreamPermission, + PermissionTypes, + StoredGatorPermission, +} from './types'; + +type MockGatorPermissionsStorageEntriesConfig = { + nativeTokenStream: number; + nativeTokenPeriodic: number; + erc20TokenStream: number; +}; + +/** + * Creates a mock gator permissions storage entry + * + * @param amount - The amount of mock gator permissions storage entries to create. + * @param mockStorageEntry - The mock gator permissions storage entry to create. + * @returns Mock gator permissions storage entry + */ +function createMockGatorPermissionsStorageEntries( + amount: number, + mockStorageEntry: StoredGatorPermission, +): string[] { + return Array.from({ length: amount }, (index: number) => + JSON.stringify({ + ...mockStorageEntry, + expiry: mockStorageEntry.permissionResponse.expiry + index, + }), + ); +} + +/** + * Creates a mock gator permissions storage entry + * + * @param config - The config for the mock gator permissions storage entries. + * @returns Mock gator permissions storage entry + */ +function mockGatorPermissionsStorageEntriesFactory( + config: MockGatorPermissionsStorageEntriesConfig, +): string[] { + const mockNativeTokenStreamStorageEntry: StoredGatorPermission< + AccountSigner, + NativeTokenStreamPermission + > = { + permissionResponse: { + chainId: '0xaa36a7', + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'native-token-stream', + data: { + maxAmount: '0x22b1c8c1227a0000', + initialAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', + }; + + const mockNativeTokenPeriodicStorageEntry: StoredGatorPermission< + AccountSigner, + NativeTokenPeriodicPermission + > = { + permissionResponse: { + chainId: '0xaa36a7', + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'native-token-periodic', + data: { + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', + }; + + const mockErc20TokenStreamStorageEntry: StoredGatorPermission< + AccountSigner, + Erc20TokenStreamPermission + > = { + permissionResponse: { + chainId: '0xaa36a7', + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'erc20-token-stream', + data: { + initialAmount: '0x22b1c8c1227a0000', + maxAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', + }; + + return [ + ...createMockGatorPermissionsStorageEntries( + config.nativeTokenStream, + mockNativeTokenStreamStorageEntry, + ), + ...createMockGatorPermissionsStorageEntries( + config.nativeTokenPeriodic, + mockNativeTokenPeriodicStorageEntry, + ), + ...createMockGatorPermissionsStorageEntries( + config.erc20TokenStream, + mockErc20TokenStreamStorageEntry, + ), + ]; +} + +/** + * Jest Test Utility - create Gator Permissions Messenger + * + * @returns Gator Permissions Messenger + */ +function createGatorPermissionsMessenger() { + const baseMessenger = new Messenger(); + const messenger = baseMessenger.getRestricted({ + name: 'GatorPermissionsController', + allowedActions: [ + 'AuthenticationController:getBearerToken', + 'AuthenticationController:isSignedIn', + 'AuthenticationController:performSignIn', + 'UserStorageController:performGetStorageAllFeatureEntries', + ], + allowedEvents: [], + }); + + return { messenger, baseMessenger }; +} + +/** + * Jest Test Utility - create Mock Gator Permissions Messenger + * + * @returns Mock Gator Permissions Messenger + */ +function createMockGatorPermissionsMessenger() { + const { baseMessenger, messenger } = createGatorPermissionsMessenger(); + + const mockCall = jest.spyOn(messenger, 'call'); + const mockGetBearerToken = jest.fn().mockResolvedValue('MOCK_BEARER_TOKEN'); + const mockIsSignedIn = jest.fn(); + const mockPerformSignIn = jest.fn(); + const mockGetStorageAllFeatureEntries = jest.fn().mockResolvedValue( + mockGatorPermissionsStorageEntriesFactory({ + nativeTokenStream: 5, + nativeTokenPeriodic: 5, + erc20TokenStream: 5, + }), + ); + + mockCall.mockImplementation((...args) => { + const [actionType] = args; + if (actionType === 'AuthenticationController:getBearerToken') { + return mockGetBearerToken(); + } + + if (actionType === 'AuthenticationController:isSignedIn') { + return mockIsSignedIn(); + } + + if (actionType === 'AuthenticationController:performSignIn') { + return mockPerformSignIn(); + } + + if ( + actionType === 'UserStorageController:performGetStorageAllFeatureEntries' + ) { + return mockGetStorageAllFeatureEntries(); + } + + throw new Error( + `MOCK_FAIL - unsupported messenger call: ${actionType as string}`, + ); + }); + + return { + messenger, + baseMessenger, + mockGetBearerToken, + mockIsSignedIn, + mockPerformSignIn, + mockGetStorageAllFeatureEntries, + }; +} + +describe('gator-permissions-controller - constructor() tests', () => { + it('creates GatorPermissionsController with default state', () => { + const controller = new GatorPermissionsController({ + messenger: createMockGatorPermissionsMessenger().messenger, + }); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.gatorPermissionsListStringify).toStrictEqual( + JSON.stringify({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }), + ); + expect(controller.state.isFetchingGatorPermissions).toBe(false); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); + + it('creates GatorPermissionsController with custom state', () => { + const customState = { + isGatorPermissionsEnabled: true, + gatorPermissionsListStringify: JSON.stringify({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }), + }; + + const controller = new GatorPermissionsController({ + messenger: createMockGatorPermissionsMessenger().messenger, + state: customState, + }); + + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.gatorPermissionsListStringify).toBe( + customState.gatorPermissionsListStringify, + ); + }); +}); + +describe('gator-permissions-controller - disableGatorPermissions() tests', () => { + it('disables gator permissions successfully', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + + // Then disable + await controller.disableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.gatorPermissionsListStringify).toBe( + JSON.stringify({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }), + ); + }); +}); + +describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests', () => { + it('fetches and updates gator permissions successfully', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + const result = await controller.fetchAndUpdateGatorPermissions(); + + expect(result).toStrictEqual({ + 'native-token-stream': expect.any(Array), + 'native-token-periodic': expect.any(Array), + 'erc20-token-stream': expect.any(Array), + }); + + expect(result['native-token-stream']).toHaveLength(5); + expect(result['native-token-periodic']).toHaveLength(5); + expect(result['erc20-token-stream']).toHaveLength(5); + expect(controller.state.isFetchingGatorPermissions).toBe(false); + }); + + it('throws error when gator permissions are not enabled', async () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); + + it('handles null permissions data', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue(null); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + const result = await controller.fetchAndUpdateGatorPermissions(); + + expect(result).toStrictEqual({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }); + }); + + it('handles empty permissions data', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue([]); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + const result = await controller.fetchAndUpdateGatorPermissions(); + + expect(result).toStrictEqual({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }); + }); + + it('throws error for invalid permission type', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue([ + JSON.stringify({ + permissionResponse: { + signer: { type: 'account', data: { address: '0x123' } }, + permission: { type: 'invalid-type' }, + }, + siteOrigin: 'http://localhost:8000', + }), + ]); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); + + it('throws error for non-account signer type', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue([ + JSON.stringify({ + permissionResponse: { + signer: { type: 'wallet', data: {} }, + permission: { type: 'native-token-stream' }, + }, + siteOrigin: 'http://localhost:8000', + }), + ]); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); + + it('handles error during fetch and update', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockRejectedValue( + new Error('Storage error'), + ); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + + expect(controller.state.isFetchingGatorPermissions).toBe(false); + }); +}); + +describe('gator-permissions-controller - gatorPermissionsList getter tests', () => { + it('returns parsed gator permissions list', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + const permissionsList = controller.gatorPermissionsList; + + expect(permissionsList).toStrictEqual({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }); + }); + + it('returns parsed gator permissions list with data', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ + messenger, + state: { + gatorPermissionsListStringify: JSON.stringify({ + 'native-token-stream': [{ id: '1' }], + 'native-token-periodic': [{ id: '2' }], + 'erc20-token-stream': [{ id: '3' }], + }), + }, + }); + + const permissionsList = controller.gatorPermissionsList; + + expect(permissionsList).toStrictEqual({ + 'native-token-stream': [{ id: '1' }], + 'native-token-periodic': [{ id: '2' }], + 'erc20-token-stream': [{ id: '3' }], + }); + }); +}); + +describe('gator-permissions-controller - private methods tests', () => { + it('clears loading states on initialization', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + expect(controller.state.isFetchingGatorPermissions).toBe(false); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); + + it('asserts gator permissions are enabled', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Should not throw when enabled + await controller.enableGatorPermissions(false); + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + }); + + it('asserts gator permissions are not enabled', async () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); +}); + +describe('gator-permissions-controller - message handlers tests', () => { + it('registers all message handlers', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + const mockRegisterActionHandler = jest.spyOn( + messenger, + 'registerActionHandler', + ); + + new GatorPermissionsController({ messenger }); + + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + expect.any(Function), + ); + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:enableGatorPermissions', + expect.any(Function), + ); + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:disableGatorPermissions', + expect.any(Function), + ); + }); +}); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts similarity index 61% rename from packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts rename to packages/gator-permissions-controller/src/GatorPermissionsController.ts index c5badb5dc6..b8b27da22f 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -10,9 +10,26 @@ import type { UserStorageController, } from '@metamask/profile-sync-controller'; +import type { + GatorPermissionsList, + NativeTokenStreamPermission, + NativeTokenPeriodicPermission, + Erc20TokenStreamPermission, + PermissionTypes, + SignerParam, + StoredGatorPermission, +} from './types'; +import { + deserializeGatorPermissionsList, + serializeGatorPermissionsList, +} from './utils'; + // Unique name for the controller const controllerName = 'GatorPermissionsController'; +// Unique name for the feature in profile sync +const GATOR_PERMISSIONS_FEATURE_NAME = 'gator_7715_permissions'; + /** * State shape for GatorPermissionsController */ @@ -23,9 +40,9 @@ export type GatorPermissionsControllerState = { isGatorPermissionsEnabled: boolean; /** - * List of gator permissions cached from profile sync + * JSON serialized object containing gator permissions fetched from profile sync indexed by permission type */ - gatorPermissionsList: GatorPermission[]; + gatorPermissionsListStringify: string; /** * Flag that indicates that fetching permissions is in progress @@ -39,18 +56,12 @@ export type GatorPermissionsControllerState = { isUpdatingGatorPermissions: boolean; }; -// TODO: Add type for gator permission -/** - * Represents a gator permission entry - */ -export type GatorPermission = unknown; - const metadata: StateMetadata = { isGatorPermissionsEnabled: { persist: true, anonymous: false, }, - gatorPermissionsList: { + gatorPermissionsListStringify: { persist: true, anonymous: true, }, @@ -64,39 +75,52 @@ const metadata: StateMetadata = { }, }; +const defaultGatorPermissionsList: GatorPermissionsList = { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], +}; + export const defaultState: GatorPermissionsControllerState = { isGatorPermissionsEnabled: false, - gatorPermissionsList: [], + gatorPermissionsListStringify: serializeGatorPermissionsList( + defaultGatorPermissionsList, + ), isUpdatingGatorPermissions: false, isFetchingGatorPermissions: false, }; +// Messenger Actions +type CreateActionsObj = { + [K in Controller]: { + type: `${typeof controllerName}:${K}`; + handler: GatorPermissionsController[K]; + }; +}; +type ActionsObj = CreateActionsObj< + | 'fetchAndUpdateGatorPermissions' + | 'enableGatorPermissions' + | 'disableGatorPermissions' +>; + export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< typeof controllerName, GatorPermissionsControllerState >; -export type GatorPermissionsControllerFetchPermissions = { - type: `${typeof controllerName}:fetchPermissions`; - handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; -}; +// Messenger Actions +export type Actions = + | ActionsObj[keyof ActionsObj] + | GatorPermissionsControllerGetStateAction; -export type GatorPermissionsControllerEnablePermissions = { - type: `${typeof controllerName}:enablePermissions`; - handler: GatorPermissionsController['enableGatorPermissions']; -}; +export type GatorPermissionsControllerFetchAndUpdateGatorPermissions = + ActionsObj['fetchAndUpdateGatorPermissions']; -export type GatorPermissionsControllerDisablePermissions = { - type: `${typeof controllerName}:disablePermissions`; - handler: GatorPermissionsController['disableGatorPermissions']; -}; +export type GatorPermissionsControllerEnableGatorPermissions = + ActionsObj['enableGatorPermissions']; -// Messenger Actions -export type Actions = - | GatorPermissionsControllerGetStateAction - | GatorPermissionsControllerFetchPermissions - | GatorPermissionsControllerEnablePermissions - | GatorPermissionsControllerDisablePermissions; +export type GatorPermissionsControllerDisableGatorPermissions = + ActionsObj['disableGatorPermissions']; // Allowed Actions export type AllowedActions = @@ -107,21 +131,14 @@ export type AllowedActions = // User Storage Controller Requests | UserStorageController.UserStorageControllerPerformGetStorageAllFeatureEntries; -// Events +// Messenger Events export type GatorPermissionsControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, GatorPermissionsControllerState >; -export type PermissionsListUpdatedEvent = { - type: `${typeof controllerName}:permissionsListUpdated`; - payload: [GatorPermission[]]; -}; - -export type Events = - | GatorPermissionsControllerStateChangeEvent - | PermissionsListUpdatedEvent; +export type Events = GatorPermissionsControllerStateChangeEvent; // Allowed Events export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; @@ -204,17 +221,17 @@ export default class GatorPermissionsController extends BaseController< #registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( - `${controllerName}:fetchPermissions`, + `${controllerName}:fetchAndUpdateGatorPermissions`, this.fetchAndUpdateGatorPermissions.bind(this), ); this.messagingSystem.registerActionHandler( - `${controllerName}:enablePermissions`, + `${controllerName}:enableGatorPermissions`, this.enableGatorPermissions.bind(this), ); this.messagingSystem.registerActionHandler( - `${controllerName}:disablePermissions`, + `${controllerName}:disableGatorPermissions`, this.disableGatorPermissions.bind(this), ); } @@ -223,12 +240,12 @@ export default class GatorPermissionsController extends BaseController< this.update((state) => { state.isUpdatingGatorPermissions = false; state.isFetchingGatorPermissions = false; - state.isUpdatingGatorPermissionsKey = []; }); } /** * Asserts that the gator permissions are enabled. + * * @throws {Error} If the gator permissions are not enabled. */ #assertGatorPermissionsEnabled() { @@ -237,6 +254,81 @@ export default class GatorPermissionsController extends BaseController< } } + /** + * Gets the categorized gator permissions list from the state. + * + * @returns The categorized gator permissions list. + */ + get gatorPermissionsList(): GatorPermissionsList { + return deserializeGatorPermissionsList( + this.state.gatorPermissionsListStringify, + ); + } + + /** + * Parses permissions from profile sync data and categorizes them by type. + * + * @param permissionsData - An JSON stringified array of permission strings from profile sync. + * @returns Parsed and categorized permissions list. + * @throws {Error} If permission type is invalid. + */ + #categorizePermissionsDataByType( + permissionsData: string[] | null, + ): GatorPermissionsList { + if (!permissionsData) { + return defaultGatorPermissionsList; + } + + return permissionsData.reduce( + (gatorPermissionsList, permissionString) => { + const parsedPermission = JSON.parse( + permissionString, + ) as StoredGatorPermission; + + if (parsedPermission.permissionResponse.signer.type !== 'account') { + throw new Error( + 'Invalid permission signer type. Only account signer is supported', + ); + } + + const permissionType = + parsedPermission.permissionResponse.permission.type; + + if (permissionType === 'native-token-stream') { + gatorPermissionsList['native-token-stream'].push( + parsedPermission as StoredGatorPermission< + SignerParam, + NativeTokenStreamPermission + >, + ); + } else if (permissionType === 'native-token-periodic') { + gatorPermissionsList['native-token-periodic'].push( + parsedPermission as StoredGatorPermission< + SignerParam, + NativeTokenPeriodicPermission + >, + ); + } else if (permissionType === 'erc20-token-stream') { + gatorPermissionsList['erc20-token-stream'].push( + parsedPermission as StoredGatorPermission< + SignerParam, + Erc20TokenStreamPermission + >, + ); + } else { + throw new Error('Invalid permission type '); + } + + return gatorPermissionsList; + }, + { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + } as GatorPermissionsList, + ); + } + /** * Enables authentication if not already enabled. * @@ -284,13 +376,10 @@ export default class GatorPermissionsController extends BaseController< // Clear permissions from state this.update((state) => { state.isGatorPermissionsEnabled = false; - state.gatorPermissionsList = []; + state.gatorPermissionsListStringify = serializeGatorPermissionsList( + defaultGatorPermissionsList, + ); }); - - this.messagingSystem.publish( - `${controllerName}:permissionsListUpdated`, - [], - ); } /** @@ -300,46 +389,28 @@ export default class GatorPermissionsController extends BaseController< * @returns A promise that resolves to the list of permissions. * @throws {Error} Throws an error if unauthenticated or from other operations. */ - public async fetchAndUpdateGatorPermissions(): Promise { + public async fetchAndUpdateGatorPermissions(): Promise { try { this.#setIsFetchingGatorPermissions(true); this.#assertGatorPermissionsEnabled(); - - // Read all permissions from profile sync await this.#enableAuth(); + + // Fetch all permissions from profile sync const permissionsData = await this.messagingSystem.call( 'UserStorageController:performGetStorageAllFeatureEntries', - 'gator_7715_permissions', + GATOR_PERMISSIONS_FEATURE_NAME, ); - if (!permissionsData) { - this.update((state) => { - state.gatorPermissionsList = []; - }); - return []; - } - - const permissions: GatorPermission[] = []; - for (const permissionString of permissionsData) { - try { - const permission = JSON.parse(permissionString) as GatorPermission; - permissions.push(permission); - } catch (e) { - console.error('Failed to parse permission data:', e); - } - } + // Categorize permissions by type and update state + const gatorPermissionsList = + this.#categorizePermissionsDataByType(permissionsData); - // Update state with fetched permissions this.update((state) => { - state.gatorPermissionsList = permissions; + state.gatorPermissionsListStringify = + serializeGatorPermissionsList(gatorPermissionsList); }); - this.messagingSystem.publish( - `${controllerName}:permissionsListUpdated`, - this.state.gatorPermissionsList, - ); - - return permissions; + return gatorPermissionsList; } catch (err) { console.error('Failed to fetch gator permissions', err); throw new Error('Failed to fetch gator permissions'); diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 9f022bc29c..d1dae0cee6 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,16 +1,19 @@ -export { default as GatorPermissionsController } from './GatorPermissionsController/GatorPermissionsController'; +export { default as GatorPermissionsController } from './GatorPermissionsController'; +export { + serializeGatorPermissionsList, + deserializeGatorPermissionsList, +} from './utils'; export type { GatorPermissionsControllerState, - GatorPermission, GatorPermissionsControllerMessenger, GatorPermissionsControllerGetStateAction, - GatorPermissionsControllerFetchPermissions, - GatorPermissionsControllerEnablePermissions, - GatorPermissionsControllerDisablePermissions, + GatorPermissionsControllerFetchAndUpdateGatorPermissions, + GatorPermissionsControllerEnableGatorPermissions, + GatorPermissionsControllerDisableGatorPermissions, Actions, AllowedActions, Events, AllowedEvents, GatorPermissionsControllerStateChangeEvent, - PermissionsListUpdatedEvent, -} from './GatorPermissionsController/GatorPermissionsController'; \ No newline at end of file +} from './GatorPermissionsController'; +export type { StoredGatorPermission, PermissionResponse } from './types'; diff --git a/packages/gator-permissions-controller/src/types.ts b/packages/gator-permissions-controller/src/types.ts new file mode 100644 index 0000000000..7d85073296 --- /dev/null +++ b/packages/gator-permissions-controller/src/types.ts @@ -0,0 +1,192 @@ +import type { Hex } from '@metamask/utils'; + +type BasePermission = { + type: string; + + /** + * Data structure varies by permission type. + */ + data: Record; + + /** + * set of restrictions or conditions that a signer must abide by when redeeming a Permission. + */ + rules?: Record; +}; + +export type MetaMaskBasePermissionData = { + /** + * A human-readable explanation of why the permission is being requested. + */ + justification: string; +}; + +export type NativeTokenStreamPermission = BasePermission & { + type: 'native-token-stream'; + data: MetaMaskBasePermissionData & { + initialAmount?: Hex; + maxAmount?: Hex; + amountPerSecond: Hex; + startTime: number; + }; +}; + +export type NativeTokenPeriodicPermission = BasePermission & { + type: 'native-token-periodic'; + data: MetaMaskBasePermissionData & { + periodAmount: Hex; + periodDuration: number; + startTime: number; + }; +}; + +export type Erc20TokenStreamPermission = BasePermission & { + type: 'erc20-token-stream'; + data: MetaMaskBasePermissionData & { + initialAmount?: Hex; + maxAmount?: Hex; + amountPerSecond: Hex; + startTime: number; + tokenAddress: Hex; + }; +}; + +/** + * Represents the type of the ERC-7715 permissions that can be granted. + */ +export type PermissionTypes = + | NativeTokenStreamPermission + | NativeTokenPeriodicPermission + | Erc20TokenStreamPermission; + +/** + * Represents an ERC-7715 account signer type. + */ +export type AccountSigner = { + type: 'account'; + data: { + address: Hex; + }; +}; + +/** + * Represents an ERC-7715 wallet signer type. + * + */ +export type WalletSigner = { + type: 'wallet'; + data: Record; +}; + +export type SignerParam = AccountSigner | WalletSigner; + +/** + * Represents a ERC-7715 permission request. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided. + */ +export type PermissionRequest< + Signer extends SignerParam, + Permission extends PermissionTypes, +> = { + /** + * hex-encoding of uint256 defined the chain with EIP-155 + */ + chainId: Hex; + + /** + * + * The account being targeted for this permission request. + * It is optional to let the user choose which account to grant permission from. + */ + address?: Hex; + + /** + * unix timestamp in seconds + */ + expiry: number; + + /** + * Boolean value that allows DApp to define whether the permission can be attenuated–adjusted to meet the user’s terms. + */ + isAdjustmentAllowed: boolean; + + /** + * An account that is associated with the recipient of the granted 7715 permission or alternatively the wallet will manage the session. + */ + signer: Signer; + + /** + * Defines the allowed behavior the signer can do on behalf of the account. + */ + permission: Permission; +}; + +/** + * Represents a ERC-7715 permission response. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided. + */ +export type PermissionResponse< + Signer extends SignerParam, + Permission extends PermissionTypes, +> = PermissionRequest & { + /** + * Is a catch-all to identify a permission for revoking permissions or submitting + * Defined in ERC-7710. + */ + context: Hex; + + /** + * The accountMeta field is required and contains information needed to deploy accounts. + * Each entry specifies a factory contract and its associated deployment data. + * If no account deployment is needed when redeeming the permission, this array must be empty. + * When non-empty, DApps MUST deploy the accounts by calling the factory contract with factoryData as the calldata. + * Defined in ERC-4337. + */ + accountMeta: { + factory: Hex; + factoryData: Hex; + }[]; + + /** + * If the signer type is account then delegationManager is required as defined in ERC-7710. + */ + signerMeta: { + delegationManager: Hex; + }; +}; + +/** + * Represents a gator ERC-7715 granted(ie. signed by an user account) permission entry that is stored in profile sync. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided + */ +export type StoredGatorPermission< + Signer extends SignerParam, + Permission extends PermissionTypes, +> = { + permissionResponse: PermissionResponse; + siteOrigin: string; +}; + +/** + * Represents a list of gator permissions filtered by permission type. + */ +export type GatorPermissionsList = { + 'native-token-stream': StoredGatorPermission< + SignerParam, + NativeTokenStreamPermission + >[]; + 'native-token-periodic': StoredGatorPermission< + SignerParam, + NativeTokenPeriodicPermission + >[]; + 'erc20-token-stream': StoredGatorPermission< + SignerParam, + Erc20TokenStreamPermission + >[]; +}; diff --git a/packages/gator-permissions-controller/src/utils.test.ts b/packages/gator-permissions-controller/src/utils.test.ts new file mode 100644 index 0000000000..0ae421586e --- /dev/null +++ b/packages/gator-permissions-controller/src/utils.test.ts @@ -0,0 +1,42 @@ +import { + deserializeGatorPermissionsList, + serializeGatorPermissionsList, +} from './utils'; + +describe('utils - serializeGatorPermissionsList() tests', () => { + it('serializes a gator permissions list to a string', () => { + const gatorPermissionsList = { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }; + + const serializedGatorPermissionsList = + serializeGatorPermissionsList(gatorPermissionsList); + + expect(serializedGatorPermissionsList).toStrictEqual( + JSON.stringify(gatorPermissionsList), + ); + }); +}); + +describe('utils - deserializeGatorPermissionsList() tests', () => { + it('deserializes a gator permissions list from a string', () => { + const gatorPermissionsList = { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }; + + const serializedGatorPermissionsList = + serializeGatorPermissionsList(gatorPermissionsList); + + const deserializedGatorPermissionsList = deserializeGatorPermissionsList( + serializedGatorPermissionsList, + ); + + expect(deserializedGatorPermissionsList).toStrictEqual( + gatorPermissionsList, + ); + }); +}); diff --git a/packages/gator-permissions-controller/src/utils.ts b/packages/gator-permissions-controller/src/utils.ts new file mode 100644 index 0000000000..9ceb16c0c7 --- /dev/null +++ b/packages/gator-permissions-controller/src/utils.ts @@ -0,0 +1,25 @@ +import type { GatorPermissionsList } from './types'; + +/** + * Serializes a gator permissions list to a string. + * + * @param gatorPermissionsList - The gator permissions list to serialize. + * @returns The serialized gator permissions list. + */ +export function serializeGatorPermissionsList( + gatorPermissionsList: GatorPermissionsList, +): string { + return JSON.stringify(gatorPermissionsList); +} + +/** + * Deserializes a gator permissions list from a string. + * + * @param gatorPermissionsList - The gator permissions list to deserialize. + * @returns The deserialized gator permissions list. + */ +export function deserializeGatorPermissionsList( + gatorPermissionsList: string, +): GatorPermissionsList { + return JSON.parse(gatorPermissionsList); +} \ No newline at end of file diff --git a/packages/profile-sync-controller/src/shared/storage-schema.ts b/packages/profile-sync-controller/src/shared/storage-schema.ts index 64f18ee3e0..760814aa40 100644 --- a/packages/profile-sync-controller/src/shared/storage-schema.ts +++ b/packages/profile-sync-controller/src/shared/storage-schema.ts @@ -14,6 +14,7 @@ export const USER_STORAGE_FEATURE_NAMES = { accounts: 'accounts_v2', networks: 'networks', addressBook: 'addressBook', + gatorPermissions: 'gator_7715_permissions', } as const; export type UserStorageFeatureNames = @@ -24,6 +25,7 @@ export const USER_STORAGE_SCHEMA = { [USER_STORAGE_FEATURE_NAMES.accounts]: [ALLOW_ARBITRARY_KEYS], // keyed by account addresses [USER_STORAGE_FEATURE_NAMES.networks]: [ALLOW_ARBITRARY_KEYS], // keyed by chains/networks [USER_STORAGE_FEATURE_NAMES.addressBook]: [ALLOW_ARBITRARY_KEYS], // keyed by address_chainId + [USER_STORAGE_FEATURE_NAMES.gatorPermissions]: ['gator_7715_permissions'], } as const; type UserStorageSchema = typeof USER_STORAGE_SCHEMA; diff --git a/teams.json b/teams.json index d3c2253f6d..8ef5c70a65 100644 --- a/teams.json +++ b/teams.json @@ -18,6 +18,7 @@ "metamask/ens-controller": "team-confirmations", "metamask/eth-json-rpc-provider": "team-wallet-api-platform,team-wallet-framework", "metamask/gas-fee-controller": "team-confirmations", + "metamask/gator-permissions-controller": "team-readable-permissions", "metamask/json-rpc-engine": "team-wallet-api-platform,team-wallet-framework", "metamask/json-rpc-middleware-stream": "team-wallet-api-platform,team-wallet-framework", "metamask/keyring-controller": "team-accounts", @@ -36,7 +37,6 @@ "metamask/preferences-controller": "team-wallet-framework", "metamask/profile-sync-controller": "team-notifications", "metamask/rate-limit-controller": "team-snaps-platform", - "metamask/gator-permissions-controller": "team-readable-permissions", "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", "metamask/sample-controllers": "team-wallet-framework", "metamask/selected-network-controller": "team-wallet-api-platform,team-wallet-framework,team-assets", diff --git a/yarn.lock b/yarn.lock index e3944947f8..ba4f94cb64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3568,9 +3568,12 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller" dependencies: + "@lavamoat/allow-scripts": "npm:^3.0.4" + "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.1" "@metamask/profile-sync-controller": "npm:^19.0.0" + "@metamask/utils": "npm:^11.2.0" "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" From 924d80fb5f42f7bf4dce072f588b4a87183b1fd7 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:21:31 -0400 Subject: [PATCH 04/24] Increase test coverage --- .../gator-permissions-controller/CHANGELOG.md | 2 +- .../src/GatorPermissionContoller.test.ts | 106 ++++++++++++++---- .../src/GatorPermissionsController.ts | 14 +-- 3 files changed, 85 insertions(+), 37 deletions(-) diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 670f270b1c..b518709c7b 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,4 +7,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -[Unreleased]: https://github.com/MetaMask/core/ \ No newline at end of file +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index 9ebc4b132d..e71e00f6f4 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -191,7 +191,6 @@ function createGatorPermissionsMessenger() { const messenger = baseMessenger.getRestricted({ name: 'GatorPermissionsController', allowedActions: [ - 'AuthenticationController:getBearerToken', 'AuthenticationController:isSignedIn', 'AuthenticationController:performSignIn', 'UserStorageController:performGetStorageAllFeatureEntries', @@ -211,7 +210,6 @@ function createMockGatorPermissionsMessenger() { const { baseMessenger, messenger } = createGatorPermissionsMessenger(); const mockCall = jest.spyOn(messenger, 'call'); - const mockGetBearerToken = jest.fn().mockResolvedValue('MOCK_BEARER_TOKEN'); const mockIsSignedIn = jest.fn(); const mockPerformSignIn = jest.fn(); const mockGetStorageAllFeatureEntries = jest.fn().mockResolvedValue( @@ -224,10 +222,6 @@ function createMockGatorPermissionsMessenger() { mockCall.mockImplementation((...args) => { const [actionType] = args; - if (actionType === 'AuthenticationController:getBearerToken') { - return mockGetBearerToken(); - } - if (actionType === 'AuthenticationController:isSignedIn') { return mockIsSignedIn(); } @@ -250,7 +244,6 @@ function createMockGatorPermissionsMessenger() { return { messenger, baseMessenger, - mockGetBearerToken, mockIsSignedIn, mockPerformSignIn, mockGetStorageAllFeatureEntries, @@ -302,12 +295,12 @@ describe('gator-permissions-controller - disableGatorPermissions() tests', () => const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); // Mock user already signed in - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); // Then disable @@ -329,12 +322,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); // Mock user already signed in - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -365,12 +358,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' createMockGatorPermissionsMessenger(); mockGetStorageAllFeatureEntries.mockResolvedValue(null); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -386,12 +379,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' createMockGatorPermissionsMessenger(); mockGetStorageAllFeatureEntries.mockResolvedValue([]); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -415,12 +408,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' siteOrigin: 'http://localhost:8000', }), ]); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -440,12 +433,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' siteOrigin: 'http://localhost:8000', }), ]); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -459,12 +452,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' mockGetStorageAllFeatureEntries.mockRejectedValue( new Error('Storage error'), ); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -527,12 +520,12 @@ describe('gator-permissions-controller - private methods tests', () => { const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); // Mock user already signed in - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Should not throw when enabled - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); }); @@ -571,3 +564,70 @@ describe('gator-permissions-controller - message handlers tests', () => { ); }); }); + +describe('gator-permissions-controller - enableGatorPermissions() tests', () => { + it('enables gator permissions successfully when user is already signed in', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockReturnValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + await controller.enableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); + + it('enables gator permissions successfully when user needs to sign in', async () => { + const { messenger, mockIsSignedIn, mockPerformSignIn } = + createMockGatorPermissionsMessenger(); + + // Mock user not signed in initially, then signed in after sign in + mockIsSignedIn.mockReturnValueOnce(false).mockReturnValueOnce(true); + mockPerformSignIn.mockResolvedValue(undefined); + + const controller = new GatorPermissionsController({ messenger }); + + await controller.enableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + expect(mockPerformSignIn).toHaveBeenCalled(); + }); + + it('handles error during enableGatorPermissions when signIn fails', async () => { + const { messenger, mockIsSignedIn, mockPerformSignIn } = + createMockGatorPermissionsMessenger(); + + // Mock user not signed in and sign in fails + mockIsSignedIn.mockReturnValue(false); + mockPerformSignIn.mockRejectedValue(new Error('Sign in failed')); + + const controller = new GatorPermissionsController({ messenger }); + + await expect(controller.enableGatorPermissions()).rejects.toThrow( + 'Unable to enable gator permissions', + ); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); +}); + +describe('gator-permissions-controller - auth methods tests', () => { + it('calls signIn when user is not signed in', async () => { + const { messenger, mockIsSignedIn, mockPerformSignIn } = + createMockGatorPermissionsMessenger(); + + mockIsSignedIn.mockReturnValue(false); + mockPerformSignIn.mockResolvedValue(undefined); + + const controller = new GatorPermissionsController({ messenger }); + + await controller.enableGatorPermissions(); + + expect(mockPerformSignIn).toHaveBeenCalled(); + }); +}); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index b8b27da22f..2eeed89e33 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -125,7 +125,6 @@ export type GatorPermissionsControllerDisableGatorPermissions = // Allowed Actions export type AllowedActions = // Auth Controller Requests - | AuthenticationController.AuthenticationControllerGetBearerToken | AuthenticationController.AuthenticationControllerIsSignedIn | AuthenticationController.AuthenticationControllerPerformSignIn // User Storage Controller Requests @@ -161,11 +160,6 @@ export default class GatorPermissionsController extends BaseController< GatorPermissionsControllerMessenger > { readonly #auth = { - getBearerToken: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:getBearerToken', - ); - }, isSignedIn: () => { return this.messagingSystem.call('AuthenticationController:isSignedIn'); }, @@ -345,19 +339,13 @@ export default class GatorPermissionsController extends BaseController< * Enables gator permissions for the user. * This method ensures that the user is authenticated and enables the feature. * - * @param isFetchingPermissions - Whether to fetch permissions after enabling. * @throws {Error} If there is an error during the process of enabling permissions. */ - public async enableGatorPermissions(isFetchingPermissions: boolean = true) { + public async enableGatorPermissions() { try { this.#setIsUpdatingGatorPermissions(true); await this.#enableAuth(); this.#setIsGatorPermissionsEnabled(true); - - // Fetch initial permissions after enabling - if (isFetchingPermissions) { - await this.fetchAndUpdateGatorPermissions(); - } } catch (e) { console.error('Unable to enable gator permissions', e); throw new Error('Unable to enable gator permissions'); From cbf1238d33d572bf1757940cefb00abcea768a3f Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:30:11 -0400 Subject: [PATCH 05/24] Add changelog --- packages/gator-permissions-controller/CHANGELOG.md | 4 ++++ packages/profile-sync-controller/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index b518709c7b..eff38c93eb 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,4 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Initial release ([#6033](https://github.com/MetaMask/core/pull/6033)) + [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index a36c8a0953..9aae67a18b 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `gatorPermissions` feature to user storage schema for gator permissions management ([#6033](https://github.com/MetaMask/core/pull/6033)) + ## [19.0.0] ### Changed From e67c32e0b94c369b5f50bb7e27d85f3a0fd5b49b Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:35:25 -0400 Subject: [PATCH 06/24] Add GatorPermissionsController package --- .../gator-permissions-controller/CHANGELOG.md | 13 + packages/gator-permissions-controller/LICENSE | 20 + .../gator-permissions-controller/README.md | 37 ++ .../jest.config.js | 26 ++ .../gator-permissions-controller/package.json | 75 ++++ .../GatorPermissionsController.ts | 348 ++++++++++++++++++ .../gator-permissions-controller/src/index.ts | 16 + .../tsconfig.build.json | 13 + .../tsconfig.json | 11 + .../gator-permissions-controller/typedoc.json | 7 + 10 files changed, 566 insertions(+) create mode 100644 packages/gator-permissions-controller/CHANGELOG.md create mode 100644 packages/gator-permissions-controller/LICENSE create mode 100644 packages/gator-permissions-controller/README.md create mode 100644 packages/gator-permissions-controller/jest.config.js create mode 100644 packages/gator-permissions-controller/package.json create mode 100644 packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts create mode 100644 packages/gator-permissions-controller/src/index.ts create mode 100644 packages/gator-permissions-controller/tsconfig.build.json create mode 100644 packages/gator-permissions-controller/tsconfig.json create mode 100644 packages/gator-permissions-controller/typedoc.json diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md new file mode 100644 index 0000000000..a22b0d308c --- /dev/null +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] + +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gator-permissions-controllerr@0.1.0...HEAD + diff --git a/packages/gator-permissions-controller/LICENSE b/packages/gator-permissions-controller/LICENSE new file mode 100644 index 0000000000..7d002dced3 --- /dev/null +++ b/packages/gator-permissions-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/gator-permissions-controller/README.md b/packages/gator-permissions-controller/README.md new file mode 100644 index 0000000000..042ed211b8 --- /dev/null +++ b/packages/gator-permissions-controller/README.md @@ -0,0 +1,37 @@ +# `@metamask/gator-permissions-controller` + +A dedicated controller for reading gator permissions from profile sync storage. This controller fetches data from the encrypted user storage database and caches it locally, providing fast access to permissions across devices while maintaining privacy through client-side encryption. + +## Installation + +`yarn add @metamask/gator-permissions-controller` + +or + +`npm install @metamask/gator-permissions-controller` + +## Usage + +### Basic Setup + +```typescript +import { GatorPermissionsController } from '@metamask/gator-permissions-controller'; + +// Create the controller +const gatorPermissionsController = new GatorPermissionsController({ + messenger: yourMessenger, +}); + +// Enable the feature (requires authentication) +gatorPermissionsController.enableGatorPermissions(); +``` + +### Fetch from Profile Sync + +```typescript +const permissions = await gatorPermissionsController.fetchAndUpdateGatorPermissions(); +``` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). \ No newline at end of file diff --git a/packages/gator-permissions-controller/jest.config.js b/packages/gator-permissions-controller/jest.config.js new file mode 100644 index 0000000000..ca08413339 --- /dev/null +++ b/packages/gator-permissions-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json new file mode 100644 index 0000000000..d3ef5864a9 --- /dev/null +++ b/packages/gator-permissions-controller/package.json @@ -0,0 +1,75 @@ +{ + "name": "@metamask/gator-permissions-controller", + "version": "0.0.0", + "description": "Controller for managing gator permissions with profile sync integration", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/gator-permissions-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/gator-permissions-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/gator-permissions-controller", + "publish:preview": "yarn npm publish --tag preview", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^8.0.1", + "loglevel": "^1.8.1" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/profile-sync-controller": "^19.0.0", + "@ts-bridge/cli": "^0.6.1", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/profile-sync-controller": "^19.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} \ No newline at end of file diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts new file mode 100644 index 0000000000..d80e6fdb57 --- /dev/null +++ b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts @@ -0,0 +1,348 @@ +import type { + RestrictedMessenger, + ControllerGetStateAction, + ControllerStateChangeEvent, + StateMetadata, +} from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller'; +import type { AuthenticationController, UserStorageController } from '@metamask/profile-sync-controller'; +import log from 'loglevel'; + +// Unique name for the controller +const controllerName = 'GatorPermissionsController'; + +/** + * State shape for GatorPermissionsController + */ +export type GatorPermissionsControllerState = { + /** + * Flag that indicates if the gator permissions feature is enabled + */ + isGatorPermissionsEnabled: boolean; + + /** + * List of gator permissions cached from profile sync + */ + gatorPermissionsList: GatorPermission[]; + + /** + * Flag that indicates that fetching permissions is in progress + * This is used to show a loading spinner in the UI + */ + isFetchingGatorPermissions: boolean; + + /** + * Flag that indicates that updating gator permissions is in progress + */ + isUpdatingGatorPermissions: boolean; +}; + +/** + * Represents a gator permission entry + */ +export type GatorPermission = any; + +const metadata: StateMetadata = { + isGatorPermissionsEnabled: { + persist: true, + anonymous: false, + }, + gatorPermissionsList: { + persist: true, + anonymous: true, + }, + isFetchingGatorPermissions: { + persist: false, + anonymous: false, + }, + isUpdatingGatorPermissions: { + persist: false, + anonymous: false, + }, +}; + +export const defaultState: GatorPermissionsControllerState = { + isGatorPermissionsEnabled: false, + gatorPermissionsList: [], + isUpdatingGatorPermissions: false, + isFetchingGatorPermissions: false, +}; + +export type GatorPermissionsControllerGetStateAction = + ControllerGetStateAction< + typeof controllerName, + GatorPermissionsControllerState + >; + +export type GatorPermissionsControllerFetchPermissions = { + type: `${typeof controllerName}:fetchPermissions`; + handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; +}; + +export type GatorPermissionsControllerEnablePermissions = { + type: `${typeof controllerName}:enablePermissions`; + handler: GatorPermissionsController['enableGatorPermissions']; +}; + +export type GatorPermissionsControllerDisablePermissions = { + type: `${typeof controllerName}:disablePermissions`; + handler: GatorPermissionsController['disableGatorPermissions']; +}; + +// Messenger Actions +export type Actions = + | GatorPermissionsControllerGetStateAction + | GatorPermissionsControllerFetchPermissions + | GatorPermissionsControllerEnablePermissions + | GatorPermissionsControllerDisablePermissions; + +// Allowed Actions +export type AllowedActions = + // Auth Controller Requests + | AuthenticationController.AuthenticationControllerGetBearerToken + | AuthenticationController.AuthenticationControllerIsSignedIn + | AuthenticationController.AuthenticationControllerPerformSignIn + // User Storage Controller Requests + | UserStorageController.UserStorageControllerPerformGetStorageAllFeatureEntries; + +// Events +export type GatorPermissionsControllerStateChangeEvent = + ControllerStateChangeEvent< + typeof controllerName, + GatorPermissionsControllerState + >; + +export type PermissionsListUpdatedEvent = { + type: `${typeof controllerName}:permissionsListUpdated`; + payload: [GatorPermission[]]; +}; + +export type Events = + | GatorPermissionsControllerStateChangeEvent + | PermissionsListUpdatedEvent; + +// Allowed Events +export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; + +// Type for the messenger of GatorPermissionsController +export type GatorPermissionsControllerMessenger = RestrictedMessenger< + typeof controllerName, + Actions | AllowedActions, + Events | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +/** + * Controller that manages gator permissions by reading from profile sync + */ +export default class GatorPermissionsController extends BaseController< + typeof controllerName, + GatorPermissionsControllerState, + GatorPermissionsControllerMessenger +> { + readonly #auth = { + getBearerToken: async () => { + return await this.messagingSystem.call( + 'AuthenticationController:getBearerToken', + ); + }, + isSignedIn: () => { + return this.messagingSystem.call('AuthenticationController:isSignedIn'); + }, + signIn: async () => { + return await this.messagingSystem.call( + 'AuthenticationController:performSignIn', + ); + }, + }; + + /** + * Creates a GatorPermissionsController instance. + * + * @param args - The arguments to this function. + * @param args.messenger - Messenger used to communicate with BaseV2 controller. + * @param args.state - Initial state to set on this controller. + */ + constructor({ + messenger, + state, + }: { + messenger: GatorPermissionsControllerMessenger; + state?: Partial; + }) { + super({ + messenger, + metadata, + name: controllerName, + state: { ...defaultState, ...state }, + }); + + this.#registerMessageHandlers(); + this.#clearLoadingStates(); + } + + #setIsFetchingGatorPermissions(isFetchingGatorPermissions: boolean) { + this.update((state) => { + state.isFetchingGatorPermissions = isFetchingGatorPermissions; + }); + } + + #setIsGatorPermissionsEnabled(isGatorPermissionsEnabled: boolean) { + this.update((state) => { + state.isGatorPermissionsEnabled = isGatorPermissionsEnabled; + }); + } + + #setIsUpdatingGatorPermissions(isUpdatingGatorPermissions: boolean) { + this.update((state) => { + state.isUpdatingGatorPermissions = isUpdatingGatorPermissions; + }); + } + + #registerMessageHandlers(): void { + this.messagingSystem.registerActionHandler( + `${controllerName}:fetchPermissions`, + this.fetchAndUpdateGatorPermissions.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `${controllerName}:enablePermissions`, + this.enableGatorPermissions.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `${controllerName}:disablePermissions`, + this.disableGatorPermissions.bind(this), + ); + } + + #clearLoadingStates(): void { + this.update((state) => { + state.isUpdatingGatorPermissions = false; + state.isFetchingGatorPermissions = false; + state.isUpdatingGatorPermissionsKey = []; + }); + } + + /** + * Asserts that the gator permissions are enabled. + * @throws {Error} If the gator permissions are not enabled. + */ + #assertGatorPermissionsEnabled() { + if (!this.state.isGatorPermissionsEnabled) { + throw new Error('Gator permissions are not enabled'); + } + } + + /** + * Enables authentication if not already enabled. + * @throws {Error} If there is an error during the process. + */ + async #enableAuth() { + const isSignedIn = this.#auth.isSignedIn(); + if (!isSignedIn) { + await this.#auth.signIn(); + } + } + + /** + * Enables gator permissions for the user. + * This method ensures that the user is authenticated and enables the feature. + * + * @throws {Error} If there is an error during the process of enabling permissions. + */ + public async enableGatorPermissions( + isFetchingPermissions: boolean = true, + ) { + try { + this.#setIsUpdatingGatorPermissions(true); + await this.#enableAuth(); + this.#setIsGatorPermissionsEnabled(true); + + // Fetch initial permissions after enabling + if (isFetchingPermissions) { + await this.fetchAndUpdateGatorPermissions(); + } + } catch (e) { + log.error('Unable to enable gator permissions', e); + throw new Error('Unable to enable gator permissions'); + } finally { + this.#setIsUpdatingGatorPermissions(false); + } + } + + /** + * Disables gator permissions for the user. + * This method clears the permissions list and disables the feature. + * + * @throws {Error} If there is an error during the process. + */ + public async disableGatorPermissions() { + // Clear permissions from state + this.update((state) => { + state.isGatorPermissionsEnabled = false; + state.gatorPermissionsList = []; + }); + + this.messagingSystem.publish( + `${controllerName}:permissionsListUpdated`, + [], + ); + } + + /** + * Fetches the list of gator permissions from profile sync and updates the state. + * This is the main method that reads data from profile sync and caches it. + * + * @returns A promise that resolves to the list of permissions. + * @throws {Error} Throws an error if unauthenticated or from other operations. + */ + public async fetchAndUpdateGatorPermissions(): Promise { + try { + this.#setIsFetchingGatorPermissions(true); + this.#assertGatorPermissionsEnabled(); + + // Read all permissions from profile sync + await this.#enableAuth(); + const permissionsData = await this.messagingSystem.call( + 'UserStorageController:performGetStorageAllFeatureEntries', + 'gator_7715_permissions', + ); + + if (!permissionsData) { + this.update((state) => { + state.gatorPermissionsList = []; + }); + return []; + } + + const permissions: GatorPermission[] = []; + for (const permissionString of permissionsData) { + try { + const permission = JSON.parse(permissionString) as GatorPermission; + permissions.push(permission); + } catch (e) { + log.error('Failed to parse permission data:', e); + } + } + + // Update state with fetched permissions + this.update((state) => { + state.gatorPermissionsList = permissions; + }); + + this.messagingSystem.publish( + `${controllerName}:permissionsListUpdated`, + this.state.gatorPermissionsList, + ); + + return permissions; + } catch (err) { + log.error('Failed to fetch gator permissions', err); + throw new Error('Failed to fetch gator permissions'); + } finally { + this.#setIsFetchingGatorPermissions(false); + } + } +} \ No newline at end of file diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts new file mode 100644 index 0000000000..9f022bc29c --- /dev/null +++ b/packages/gator-permissions-controller/src/index.ts @@ -0,0 +1,16 @@ +export { default as GatorPermissionsController } from './GatorPermissionsController/GatorPermissionsController'; +export type { + GatorPermissionsControllerState, + GatorPermission, + GatorPermissionsControllerMessenger, + GatorPermissionsControllerGetStateAction, + GatorPermissionsControllerFetchPermissions, + GatorPermissionsControllerEnablePermissions, + GatorPermissionsControllerDisablePermissions, + Actions, + AllowedActions, + Events, + AllowedEvents, + GatorPermissionsControllerStateChangeEvent, + PermissionsListUpdatedEvent, +} from './GatorPermissionsController/GatorPermissionsController'; \ No newline at end of file diff --git a/packages/gator-permissions-controller/tsconfig.build.json b/packages/gator-permissions-controller/tsconfig.build.json new file mode 100644 index 0000000000..ae8fdda2dd --- /dev/null +++ b/packages/gator-permissions-controller/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../profile-sync-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/gator-permissions-controller/tsconfig.json b/packages/gator-permissions-controller/tsconfig.json new file mode 100644 index 0000000000..6efb36b7b6 --- /dev/null +++ b/packages/gator-permissions-controller/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../profile-sync-controller" }, + ], + "include": ["../../types", "./src"] +} diff --git a/packages/gator-permissions-controller/typedoc.json b/packages/gator-permissions-controller/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/gator-permissions-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} From fa9e582fed0ee31e2f3a85487f0348484a2fdf60 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:26:59 -0400 Subject: [PATCH 07/24] Add gator permissions controller to root ts build config and dependency graph readme --- .github/CODEOWNERS | 5 +++ README.md | 4 +++ .../gator-permissions-controller/CHANGELOG.md | 5 +-- .../gator-permissions-controller/package.json | 5 ++- .../GatorPermissionsController.ts | 32 ++++++++++--------- teams.json | 1 + tsconfig.build.json | 1 + tsconfig.json | 1 + yarn.lock | 20 ++++++++++++ 9 files changed, 52 insertions(+), 22 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 709ef62807..01a8f2755d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,6 +26,9 @@ /packages/transaction-controller @MetaMask/confirmations /packages/user-operation-controller @MetaMask/confirmations +## Delegation Team +/packages/gator-permissions-controller @MetaMask/delegation + ## Earn Team /packages/earn-controller @MetaMask/earn ## Notifications Team @@ -111,6 +114,8 @@ /packages/ens-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/gas-fee-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/gas-fee-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers +/packages/gator-permissions-controller/package.json @MetaMask/delegation @MetaMask/wallet-framework-engineers +/packages/gator-permissions-controller/CHANGELOG.md @MetaMask/delegation @MetaMask/wallet-framework-engineers /packages/keyring-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/keyring-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/logging-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers diff --git a/README.md b/README.md index 0ad9ee8c9c..53e899da74 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/eth-json-rpc-provider`](packages/eth-json-rpc-provider) - [`@metamask/foundryup`](packages/foundryup) - [`@metamask/gas-fee-controller`](packages/gas-fee-controller) +- [`@metamask/gator-permissions-controller`](packages/gator-permissions-controller) - [`@metamask/json-rpc-engine`](packages/json-rpc-engine) - [`@metamask/json-rpc-middleware-stream`](packages/json-rpc-middleware-stream) - [`@metamask/keyring-controller`](packages/keyring-controller) @@ -99,6 +100,7 @@ linkStyle default opacity:0.5 eth_json_rpc_provider(["@metamask/eth-json-rpc-provider"]); foundryup(["@metamask/foundryup"]); gas_fee_controller(["@metamask/gas-fee-controller"]); + gator-permissions-controller(["@metamask/gator-permissions-controller"]); json_rpc_engine(["@metamask/json-rpc-engine"]); json_rpc_middleware_stream(["@metamask/json-rpc-middleware-stream"]); keyring_controller(["@metamask/keyring-controller"]); @@ -194,6 +196,8 @@ linkStyle default opacity:0.5 gas_fee_controller --> controller_utils; gas_fee_controller --> polling_controller; gas_fee_controller --> network_controller; + gator-permissions-controller --> base_controller; + gator-permissions-controller --> profile_sync_controller; json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; logging_controller --> base_controller; diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index a22b0d308c..670f270b1c 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,7 +7,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.0] - -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gator-permissions-controllerr@0.1.0...HEAD - +[Unreleased]: https://github.com/MetaMask/core/ \ No newline at end of file diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index d3ef5864a9..b0f5de4c95 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -47,8 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.0.1", - "loglevel": "^1.8.1" + "@metamask/base-controller": "^8.0.1" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", @@ -72,4 +71,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} \ No newline at end of file +} diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts index d80e6fdb57..c5badb5dc6 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts @@ -5,8 +5,10 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import type { AuthenticationController, UserStorageController } from '@metamask/profile-sync-controller'; -import log from 'loglevel'; +import type { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; // Unique name for the controller const controllerName = 'GatorPermissionsController'; @@ -37,10 +39,11 @@ export type GatorPermissionsControllerState = { isUpdatingGatorPermissions: boolean; }; +// TODO: Add type for gator permission /** * Represents a gator permission entry */ -export type GatorPermission = any; +export type GatorPermission = unknown; const metadata: StateMetadata = { isGatorPermissionsEnabled: { @@ -68,11 +71,10 @@ export const defaultState: GatorPermissionsControllerState = { isFetchingGatorPermissions: false, }; -export type GatorPermissionsControllerGetStateAction = - ControllerGetStateAction< - typeof controllerName, - GatorPermissionsControllerState - >; +export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + GatorPermissionsControllerState +>; export type GatorPermissionsControllerFetchPermissions = { type: `${typeof controllerName}:fetchPermissions`; @@ -237,6 +239,7 @@ export default class GatorPermissionsController extends BaseController< /** * Enables authentication if not already enabled. + * * @throws {Error} If there is an error during the process. */ async #enableAuth() { @@ -250,11 +253,10 @@ export default class GatorPermissionsController extends BaseController< * Enables gator permissions for the user. * This method ensures that the user is authenticated and enables the feature. * + * @param isFetchingPermissions - Whether to fetch permissions after enabling. * @throws {Error} If there is an error during the process of enabling permissions. */ - public async enableGatorPermissions( - isFetchingPermissions: boolean = true, - ) { + public async enableGatorPermissions(isFetchingPermissions: boolean = true) { try { this.#setIsUpdatingGatorPermissions(true); await this.#enableAuth(); @@ -265,7 +267,7 @@ export default class GatorPermissionsController extends BaseController< await this.fetchAndUpdateGatorPermissions(); } } catch (e) { - log.error('Unable to enable gator permissions', e); + console.error('Unable to enable gator permissions', e); throw new Error('Unable to enable gator permissions'); } finally { this.#setIsUpdatingGatorPermissions(false); @@ -323,7 +325,7 @@ export default class GatorPermissionsController extends BaseController< const permission = JSON.parse(permissionString) as GatorPermission; permissions.push(permission); } catch (e) { - log.error('Failed to parse permission data:', e); + console.error('Failed to parse permission data:', e); } } @@ -339,10 +341,10 @@ export default class GatorPermissionsController extends BaseController< return permissions; } catch (err) { - log.error('Failed to fetch gator permissions', err); + console.error('Failed to fetch gator permissions', err); throw new Error('Failed to fetch gator permissions'); } finally { this.#setIsFetchingGatorPermissions(false); } } -} \ No newline at end of file +} diff --git a/teams.json b/teams.json index 23b6fc3355..75e1b964cd 100644 --- a/teams.json +++ b/teams.json @@ -36,6 +36,7 @@ "metamask/preferences-controller": "team-wallet-framework", "metamask/profile-sync-controller": "team-notifications", "metamask/rate-limit-controller": "team-snaps-platform", + "metamask/gator-permissions-controller": "team-readable-permissions", "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", "metamask/sample-controllers": "team-wallet-framework", "metamask/selected-network-controller": "team-wallet-api-platform,team-wallet-framework,team-assets", diff --git a/tsconfig.build.json b/tsconfig.build.json index 18ee0a6622..eb92ef8e0b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -22,6 +22,7 @@ { "path": "./packages/eth-json-rpc-provider/tsconfig.build.json" }, { "path": "./packages/foundryup/tsconfig.build.json" }, { "path": "./packages/gas-fee-controller/tsconfig.build.json" }, + { "path": "./packages/gator-permissions-controller/tsconfig.build.json" }, { "path": "./packages/json-rpc-engine/tsconfig.build.json" }, { "path": "./packages/json-rpc-middleware-stream/tsconfig.build.json" }, { "path": "./packages/keyring-controller/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 1830b6b33c..7387e99713 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,7 @@ { "path": "./packages/eth-json-rpc-provider" }, { "path": "./packages/foundryup" }, { "path": "./packages/gas-fee-controller" }, + { "path": "./packages/gator-permissions-controller" }, { "path": "./packages/json-rpc-engine" }, { "path": "./packages/json-rpc-middleware-stream" }, { "path": "./packages/keyring-controller" }, diff --git a/yarn.lock b/yarn.lock index ab1f3d8cc0..158e94ffb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3564,6 +3564,26 @@ __metadata: languageName: unknown linkType: soft +"@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller": + version: 0.0.0-use.local + resolution: "@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.1" + "@metamask/profile-sync-controller": "npm:^19.0.0" + "@ts-bridge/cli": "npm:^0.6.1" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/profile-sync-controller": ^19.0.0 + languageName: unknown + linkType: soft + "@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" From f00b3e59476d0668683ce39be8e0834582a09831 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:38:14 -0400 Subject: [PATCH 08/24] Add ERC-7715 type definitions and increase test coverage --- .../gator-permissions-controller/package.json | 5 +- .../src/GatorPermissionContoller.test.ts | 573 ++++++++++++++++++ .../GatorPermissionsController.ts | 221 ++++--- .../gator-permissions-controller/src/index.ts | 17 +- .../gator-permissions-controller/src/types.ts | 192 ++++++ .../src/utils.test.ts | 42 ++ .../gator-permissions-controller/src/utils.ts | 25 + .../src/shared/storage-schema.ts | 2 + teams.json | 2 +- yarn.lock | 3 + 10 files changed, 998 insertions(+), 84 deletions(-) create mode 100644 packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts rename packages/gator-permissions-controller/src/{GatorPermissionsController => }/GatorPermissionsController.ts (61%) create mode 100644 packages/gator-permissions-controller/src/types.ts create mode 100644 packages/gator-permissions-controller/src/utils.test.ts create mode 100644 packages/gator-permissions-controller/src/utils.ts diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index b0f5de4c95..f21985d252 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -47,9 +47,12 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.0.1" + "@metamask/base-controller": "^8.0.1", + "@metamask/utils": "^11.2.0" }, "devDependencies": { + "@lavamoat/allow-scripts": "^3.0.4", + "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/profile-sync-controller": "^19.0.0", "@ts-bridge/cli": "^0.6.1", diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts new file mode 100644 index 0000000000..9ebc4b132d --- /dev/null +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -0,0 +1,573 @@ +import { Messenger } from '@metamask/base-controller'; + +import type { + AllowedActions, + AllowedEvents, +} from './GatorPermissionsController'; +import GatorPermissionsController from './GatorPermissionsController'; +import type { + AccountSigner, + Erc20TokenStreamPermission, + NativeTokenPeriodicPermission, + NativeTokenStreamPermission, + PermissionTypes, + StoredGatorPermission, +} from './types'; + +type MockGatorPermissionsStorageEntriesConfig = { + nativeTokenStream: number; + nativeTokenPeriodic: number; + erc20TokenStream: number; +}; + +/** + * Creates a mock gator permissions storage entry + * + * @param amount - The amount of mock gator permissions storage entries to create. + * @param mockStorageEntry - The mock gator permissions storage entry to create. + * @returns Mock gator permissions storage entry + */ +function createMockGatorPermissionsStorageEntries( + amount: number, + mockStorageEntry: StoredGatorPermission, +): string[] { + return Array.from({ length: amount }, (index: number) => + JSON.stringify({ + ...mockStorageEntry, + expiry: mockStorageEntry.permissionResponse.expiry + index, + }), + ); +} + +/** + * Creates a mock gator permissions storage entry + * + * @param config - The config for the mock gator permissions storage entries. + * @returns Mock gator permissions storage entry + */ +function mockGatorPermissionsStorageEntriesFactory( + config: MockGatorPermissionsStorageEntriesConfig, +): string[] { + const mockNativeTokenStreamStorageEntry: StoredGatorPermission< + AccountSigner, + NativeTokenStreamPermission + > = { + permissionResponse: { + chainId: '0xaa36a7', + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'native-token-stream', + data: { + maxAmount: '0x22b1c8c1227a0000', + initialAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', + }; + + const mockNativeTokenPeriodicStorageEntry: StoredGatorPermission< + AccountSigner, + NativeTokenPeriodicPermission + > = { + permissionResponse: { + chainId: '0xaa36a7', + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'native-token-periodic', + data: { + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', + }; + + const mockErc20TokenStreamStorageEntry: StoredGatorPermission< + AccountSigner, + Erc20TokenStreamPermission + > = { + permissionResponse: { + chainId: '0xaa36a7', + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'erc20-token-stream', + data: { + initialAmount: '0x22b1c8c1227a0000', + maxAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', + }; + + return [ + ...createMockGatorPermissionsStorageEntries( + config.nativeTokenStream, + mockNativeTokenStreamStorageEntry, + ), + ...createMockGatorPermissionsStorageEntries( + config.nativeTokenPeriodic, + mockNativeTokenPeriodicStorageEntry, + ), + ...createMockGatorPermissionsStorageEntries( + config.erc20TokenStream, + mockErc20TokenStreamStorageEntry, + ), + ]; +} + +/** + * Jest Test Utility - create Gator Permissions Messenger + * + * @returns Gator Permissions Messenger + */ +function createGatorPermissionsMessenger() { + const baseMessenger = new Messenger(); + const messenger = baseMessenger.getRestricted({ + name: 'GatorPermissionsController', + allowedActions: [ + 'AuthenticationController:getBearerToken', + 'AuthenticationController:isSignedIn', + 'AuthenticationController:performSignIn', + 'UserStorageController:performGetStorageAllFeatureEntries', + ], + allowedEvents: [], + }); + + return { messenger, baseMessenger }; +} + +/** + * Jest Test Utility - create Mock Gator Permissions Messenger + * + * @returns Mock Gator Permissions Messenger + */ +function createMockGatorPermissionsMessenger() { + const { baseMessenger, messenger } = createGatorPermissionsMessenger(); + + const mockCall = jest.spyOn(messenger, 'call'); + const mockGetBearerToken = jest.fn().mockResolvedValue('MOCK_BEARER_TOKEN'); + const mockIsSignedIn = jest.fn(); + const mockPerformSignIn = jest.fn(); + const mockGetStorageAllFeatureEntries = jest.fn().mockResolvedValue( + mockGatorPermissionsStorageEntriesFactory({ + nativeTokenStream: 5, + nativeTokenPeriodic: 5, + erc20TokenStream: 5, + }), + ); + + mockCall.mockImplementation((...args) => { + const [actionType] = args; + if (actionType === 'AuthenticationController:getBearerToken') { + return mockGetBearerToken(); + } + + if (actionType === 'AuthenticationController:isSignedIn') { + return mockIsSignedIn(); + } + + if (actionType === 'AuthenticationController:performSignIn') { + return mockPerformSignIn(); + } + + if ( + actionType === 'UserStorageController:performGetStorageAllFeatureEntries' + ) { + return mockGetStorageAllFeatureEntries(); + } + + throw new Error( + `MOCK_FAIL - unsupported messenger call: ${actionType as string}`, + ); + }); + + return { + messenger, + baseMessenger, + mockGetBearerToken, + mockIsSignedIn, + mockPerformSignIn, + mockGetStorageAllFeatureEntries, + }; +} + +describe('gator-permissions-controller - constructor() tests', () => { + it('creates GatorPermissionsController with default state', () => { + const controller = new GatorPermissionsController({ + messenger: createMockGatorPermissionsMessenger().messenger, + }); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.gatorPermissionsListStringify).toStrictEqual( + JSON.stringify({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }), + ); + expect(controller.state.isFetchingGatorPermissions).toBe(false); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); + + it('creates GatorPermissionsController with custom state', () => { + const customState = { + isGatorPermissionsEnabled: true, + gatorPermissionsListStringify: JSON.stringify({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }), + }; + + const controller = new GatorPermissionsController({ + messenger: createMockGatorPermissionsMessenger().messenger, + state: customState, + }); + + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.gatorPermissionsListStringify).toBe( + customState.gatorPermissionsListStringify, + ); + }); +}); + +describe('gator-permissions-controller - disableGatorPermissions() tests', () => { + it('disables gator permissions successfully', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + + // Then disable + await controller.disableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.gatorPermissionsListStringify).toBe( + JSON.stringify({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }), + ); + }); +}); + +describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests', () => { + it('fetches and updates gator permissions successfully', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + const result = await controller.fetchAndUpdateGatorPermissions(); + + expect(result).toStrictEqual({ + 'native-token-stream': expect.any(Array), + 'native-token-periodic': expect.any(Array), + 'erc20-token-stream': expect.any(Array), + }); + + expect(result['native-token-stream']).toHaveLength(5); + expect(result['native-token-periodic']).toHaveLength(5); + expect(result['erc20-token-stream']).toHaveLength(5); + expect(controller.state.isFetchingGatorPermissions).toBe(false); + }); + + it('throws error when gator permissions are not enabled', async () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); + + it('handles null permissions data', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue(null); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + const result = await controller.fetchAndUpdateGatorPermissions(); + + expect(result).toStrictEqual({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }); + }); + + it('handles empty permissions data', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue([]); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + const result = await controller.fetchAndUpdateGatorPermissions(); + + expect(result).toStrictEqual({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }); + }); + + it('throws error for invalid permission type', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue([ + JSON.stringify({ + permissionResponse: { + signer: { type: 'account', data: { address: '0x123' } }, + permission: { type: 'invalid-type' }, + }, + siteOrigin: 'http://localhost:8000', + }), + ]); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); + + it('throws error for non-account signer type', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockResolvedValue([ + JSON.stringify({ + permissionResponse: { + signer: { type: 'wallet', data: {} }, + permission: { type: 'native-token-stream' }, + }, + siteOrigin: 'http://localhost:8000', + }), + ]); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); + + it('handles error during fetch and update', async () => { + const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + createMockGatorPermissionsMessenger(); + + mockGetStorageAllFeatureEntries.mockRejectedValue( + new Error('Storage error'), + ); + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Enable first + await controller.enableGatorPermissions(false); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + + expect(controller.state.isFetchingGatorPermissions).toBe(false); + }); +}); + +describe('gator-permissions-controller - gatorPermissionsList getter tests', () => { + it('returns parsed gator permissions list', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + const permissionsList = controller.gatorPermissionsList; + + expect(permissionsList).toStrictEqual({ + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }); + }); + + it('returns parsed gator permissions list with data', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ + messenger, + state: { + gatorPermissionsListStringify: JSON.stringify({ + 'native-token-stream': [{ id: '1' }], + 'native-token-periodic': [{ id: '2' }], + 'erc20-token-stream': [{ id: '3' }], + }), + }, + }); + + const permissionsList = controller.gatorPermissionsList; + + expect(permissionsList).toStrictEqual({ + 'native-token-stream': [{ id: '1' }], + 'native-token-periodic': [{ id: '2' }], + 'erc20-token-stream': [{ id: '3' }], + }); + }); +}); + +describe('gator-permissions-controller - private methods tests', () => { + it('clears loading states on initialization', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + expect(controller.state.isFetchingGatorPermissions).toBe(false); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); + + it('asserts gator permissions are enabled', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockResolvedValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + // Should not throw when enabled + await controller.enableGatorPermissions(false); + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + }); + + it('asserts gator permissions are not enabled', async () => { + const { messenger } = createMockGatorPermissionsMessenger(); + + const controller = new GatorPermissionsController({ messenger }); + + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); + }); +}); + +describe('gator-permissions-controller - message handlers tests', () => { + it('registers all message handlers', () => { + const { messenger } = createMockGatorPermissionsMessenger(); + const mockRegisterActionHandler = jest.spyOn( + messenger, + 'registerActionHandler', + ); + + new GatorPermissionsController({ messenger }); + + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + expect.any(Function), + ); + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:enableGatorPermissions', + expect.any(Function), + ); + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:disableGatorPermissions', + expect.any(Function), + ); + }); +}); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts similarity index 61% rename from packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts rename to packages/gator-permissions-controller/src/GatorPermissionsController.ts index c5badb5dc6..b8b27da22f 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -10,9 +10,26 @@ import type { UserStorageController, } from '@metamask/profile-sync-controller'; +import type { + GatorPermissionsList, + NativeTokenStreamPermission, + NativeTokenPeriodicPermission, + Erc20TokenStreamPermission, + PermissionTypes, + SignerParam, + StoredGatorPermission, +} from './types'; +import { + deserializeGatorPermissionsList, + serializeGatorPermissionsList, +} from './utils'; + // Unique name for the controller const controllerName = 'GatorPermissionsController'; +// Unique name for the feature in profile sync +const GATOR_PERMISSIONS_FEATURE_NAME = 'gator_7715_permissions'; + /** * State shape for GatorPermissionsController */ @@ -23,9 +40,9 @@ export type GatorPermissionsControllerState = { isGatorPermissionsEnabled: boolean; /** - * List of gator permissions cached from profile sync + * JSON serialized object containing gator permissions fetched from profile sync indexed by permission type */ - gatorPermissionsList: GatorPermission[]; + gatorPermissionsListStringify: string; /** * Flag that indicates that fetching permissions is in progress @@ -39,18 +56,12 @@ export type GatorPermissionsControllerState = { isUpdatingGatorPermissions: boolean; }; -// TODO: Add type for gator permission -/** - * Represents a gator permission entry - */ -export type GatorPermission = unknown; - const metadata: StateMetadata = { isGatorPermissionsEnabled: { persist: true, anonymous: false, }, - gatorPermissionsList: { + gatorPermissionsListStringify: { persist: true, anonymous: true, }, @@ -64,39 +75,52 @@ const metadata: StateMetadata = { }, }; +const defaultGatorPermissionsList: GatorPermissionsList = { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], +}; + export const defaultState: GatorPermissionsControllerState = { isGatorPermissionsEnabled: false, - gatorPermissionsList: [], + gatorPermissionsListStringify: serializeGatorPermissionsList( + defaultGatorPermissionsList, + ), isUpdatingGatorPermissions: false, isFetchingGatorPermissions: false, }; +// Messenger Actions +type CreateActionsObj = { + [K in Controller]: { + type: `${typeof controllerName}:${K}`; + handler: GatorPermissionsController[K]; + }; +}; +type ActionsObj = CreateActionsObj< + | 'fetchAndUpdateGatorPermissions' + | 'enableGatorPermissions' + | 'disableGatorPermissions' +>; + export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< typeof controllerName, GatorPermissionsControllerState >; -export type GatorPermissionsControllerFetchPermissions = { - type: `${typeof controllerName}:fetchPermissions`; - handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; -}; +// Messenger Actions +export type Actions = + | ActionsObj[keyof ActionsObj] + | GatorPermissionsControllerGetStateAction; -export type GatorPermissionsControllerEnablePermissions = { - type: `${typeof controllerName}:enablePermissions`; - handler: GatorPermissionsController['enableGatorPermissions']; -}; +export type GatorPermissionsControllerFetchAndUpdateGatorPermissions = + ActionsObj['fetchAndUpdateGatorPermissions']; -export type GatorPermissionsControllerDisablePermissions = { - type: `${typeof controllerName}:disablePermissions`; - handler: GatorPermissionsController['disableGatorPermissions']; -}; +export type GatorPermissionsControllerEnableGatorPermissions = + ActionsObj['enableGatorPermissions']; -// Messenger Actions -export type Actions = - | GatorPermissionsControllerGetStateAction - | GatorPermissionsControllerFetchPermissions - | GatorPermissionsControllerEnablePermissions - | GatorPermissionsControllerDisablePermissions; +export type GatorPermissionsControllerDisableGatorPermissions = + ActionsObj['disableGatorPermissions']; // Allowed Actions export type AllowedActions = @@ -107,21 +131,14 @@ export type AllowedActions = // User Storage Controller Requests | UserStorageController.UserStorageControllerPerformGetStorageAllFeatureEntries; -// Events +// Messenger Events export type GatorPermissionsControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, GatorPermissionsControllerState >; -export type PermissionsListUpdatedEvent = { - type: `${typeof controllerName}:permissionsListUpdated`; - payload: [GatorPermission[]]; -}; - -export type Events = - | GatorPermissionsControllerStateChangeEvent - | PermissionsListUpdatedEvent; +export type Events = GatorPermissionsControllerStateChangeEvent; // Allowed Events export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; @@ -204,17 +221,17 @@ export default class GatorPermissionsController extends BaseController< #registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( - `${controllerName}:fetchPermissions`, + `${controllerName}:fetchAndUpdateGatorPermissions`, this.fetchAndUpdateGatorPermissions.bind(this), ); this.messagingSystem.registerActionHandler( - `${controllerName}:enablePermissions`, + `${controllerName}:enableGatorPermissions`, this.enableGatorPermissions.bind(this), ); this.messagingSystem.registerActionHandler( - `${controllerName}:disablePermissions`, + `${controllerName}:disableGatorPermissions`, this.disableGatorPermissions.bind(this), ); } @@ -223,12 +240,12 @@ export default class GatorPermissionsController extends BaseController< this.update((state) => { state.isUpdatingGatorPermissions = false; state.isFetchingGatorPermissions = false; - state.isUpdatingGatorPermissionsKey = []; }); } /** * Asserts that the gator permissions are enabled. + * * @throws {Error} If the gator permissions are not enabled. */ #assertGatorPermissionsEnabled() { @@ -237,6 +254,81 @@ export default class GatorPermissionsController extends BaseController< } } + /** + * Gets the categorized gator permissions list from the state. + * + * @returns The categorized gator permissions list. + */ + get gatorPermissionsList(): GatorPermissionsList { + return deserializeGatorPermissionsList( + this.state.gatorPermissionsListStringify, + ); + } + + /** + * Parses permissions from profile sync data and categorizes them by type. + * + * @param permissionsData - An JSON stringified array of permission strings from profile sync. + * @returns Parsed and categorized permissions list. + * @throws {Error} If permission type is invalid. + */ + #categorizePermissionsDataByType( + permissionsData: string[] | null, + ): GatorPermissionsList { + if (!permissionsData) { + return defaultGatorPermissionsList; + } + + return permissionsData.reduce( + (gatorPermissionsList, permissionString) => { + const parsedPermission = JSON.parse( + permissionString, + ) as StoredGatorPermission; + + if (parsedPermission.permissionResponse.signer.type !== 'account') { + throw new Error( + 'Invalid permission signer type. Only account signer is supported', + ); + } + + const permissionType = + parsedPermission.permissionResponse.permission.type; + + if (permissionType === 'native-token-stream') { + gatorPermissionsList['native-token-stream'].push( + parsedPermission as StoredGatorPermission< + SignerParam, + NativeTokenStreamPermission + >, + ); + } else if (permissionType === 'native-token-periodic') { + gatorPermissionsList['native-token-periodic'].push( + parsedPermission as StoredGatorPermission< + SignerParam, + NativeTokenPeriodicPermission + >, + ); + } else if (permissionType === 'erc20-token-stream') { + gatorPermissionsList['erc20-token-stream'].push( + parsedPermission as StoredGatorPermission< + SignerParam, + Erc20TokenStreamPermission + >, + ); + } else { + throw new Error('Invalid permission type '); + } + + return gatorPermissionsList; + }, + { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + } as GatorPermissionsList, + ); + } + /** * Enables authentication if not already enabled. * @@ -284,13 +376,10 @@ export default class GatorPermissionsController extends BaseController< // Clear permissions from state this.update((state) => { state.isGatorPermissionsEnabled = false; - state.gatorPermissionsList = []; + state.gatorPermissionsListStringify = serializeGatorPermissionsList( + defaultGatorPermissionsList, + ); }); - - this.messagingSystem.publish( - `${controllerName}:permissionsListUpdated`, - [], - ); } /** @@ -300,46 +389,28 @@ export default class GatorPermissionsController extends BaseController< * @returns A promise that resolves to the list of permissions. * @throws {Error} Throws an error if unauthenticated or from other operations. */ - public async fetchAndUpdateGatorPermissions(): Promise { + public async fetchAndUpdateGatorPermissions(): Promise { try { this.#setIsFetchingGatorPermissions(true); this.#assertGatorPermissionsEnabled(); - - // Read all permissions from profile sync await this.#enableAuth(); + + // Fetch all permissions from profile sync const permissionsData = await this.messagingSystem.call( 'UserStorageController:performGetStorageAllFeatureEntries', - 'gator_7715_permissions', + GATOR_PERMISSIONS_FEATURE_NAME, ); - if (!permissionsData) { - this.update((state) => { - state.gatorPermissionsList = []; - }); - return []; - } - - const permissions: GatorPermission[] = []; - for (const permissionString of permissionsData) { - try { - const permission = JSON.parse(permissionString) as GatorPermission; - permissions.push(permission); - } catch (e) { - console.error('Failed to parse permission data:', e); - } - } + // Categorize permissions by type and update state + const gatorPermissionsList = + this.#categorizePermissionsDataByType(permissionsData); - // Update state with fetched permissions this.update((state) => { - state.gatorPermissionsList = permissions; + state.gatorPermissionsListStringify = + serializeGatorPermissionsList(gatorPermissionsList); }); - this.messagingSystem.publish( - `${controllerName}:permissionsListUpdated`, - this.state.gatorPermissionsList, - ); - - return permissions; + return gatorPermissionsList; } catch (err) { console.error('Failed to fetch gator permissions', err); throw new Error('Failed to fetch gator permissions'); diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 9f022bc29c..d1dae0cee6 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,16 +1,19 @@ -export { default as GatorPermissionsController } from './GatorPermissionsController/GatorPermissionsController'; +export { default as GatorPermissionsController } from './GatorPermissionsController'; +export { + serializeGatorPermissionsList, + deserializeGatorPermissionsList, +} from './utils'; export type { GatorPermissionsControllerState, - GatorPermission, GatorPermissionsControllerMessenger, GatorPermissionsControllerGetStateAction, - GatorPermissionsControllerFetchPermissions, - GatorPermissionsControllerEnablePermissions, - GatorPermissionsControllerDisablePermissions, + GatorPermissionsControllerFetchAndUpdateGatorPermissions, + GatorPermissionsControllerEnableGatorPermissions, + GatorPermissionsControllerDisableGatorPermissions, Actions, AllowedActions, Events, AllowedEvents, GatorPermissionsControllerStateChangeEvent, - PermissionsListUpdatedEvent, -} from './GatorPermissionsController/GatorPermissionsController'; \ No newline at end of file +} from './GatorPermissionsController'; +export type { StoredGatorPermission, PermissionResponse } from './types'; diff --git a/packages/gator-permissions-controller/src/types.ts b/packages/gator-permissions-controller/src/types.ts new file mode 100644 index 0000000000..7d85073296 --- /dev/null +++ b/packages/gator-permissions-controller/src/types.ts @@ -0,0 +1,192 @@ +import type { Hex } from '@metamask/utils'; + +type BasePermission = { + type: string; + + /** + * Data structure varies by permission type. + */ + data: Record; + + /** + * set of restrictions or conditions that a signer must abide by when redeeming a Permission. + */ + rules?: Record; +}; + +export type MetaMaskBasePermissionData = { + /** + * A human-readable explanation of why the permission is being requested. + */ + justification: string; +}; + +export type NativeTokenStreamPermission = BasePermission & { + type: 'native-token-stream'; + data: MetaMaskBasePermissionData & { + initialAmount?: Hex; + maxAmount?: Hex; + amountPerSecond: Hex; + startTime: number; + }; +}; + +export type NativeTokenPeriodicPermission = BasePermission & { + type: 'native-token-periodic'; + data: MetaMaskBasePermissionData & { + periodAmount: Hex; + periodDuration: number; + startTime: number; + }; +}; + +export type Erc20TokenStreamPermission = BasePermission & { + type: 'erc20-token-stream'; + data: MetaMaskBasePermissionData & { + initialAmount?: Hex; + maxAmount?: Hex; + amountPerSecond: Hex; + startTime: number; + tokenAddress: Hex; + }; +}; + +/** + * Represents the type of the ERC-7715 permissions that can be granted. + */ +export type PermissionTypes = + | NativeTokenStreamPermission + | NativeTokenPeriodicPermission + | Erc20TokenStreamPermission; + +/** + * Represents an ERC-7715 account signer type. + */ +export type AccountSigner = { + type: 'account'; + data: { + address: Hex; + }; +}; + +/** + * Represents an ERC-7715 wallet signer type. + * + */ +export type WalletSigner = { + type: 'wallet'; + data: Record; +}; + +export type SignerParam = AccountSigner | WalletSigner; + +/** + * Represents a ERC-7715 permission request. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided. + */ +export type PermissionRequest< + Signer extends SignerParam, + Permission extends PermissionTypes, +> = { + /** + * hex-encoding of uint256 defined the chain with EIP-155 + */ + chainId: Hex; + + /** + * + * The account being targeted for this permission request. + * It is optional to let the user choose which account to grant permission from. + */ + address?: Hex; + + /** + * unix timestamp in seconds + */ + expiry: number; + + /** + * Boolean value that allows DApp to define whether the permission can be attenuated–adjusted to meet the user’s terms. + */ + isAdjustmentAllowed: boolean; + + /** + * An account that is associated with the recipient of the granted 7715 permission or alternatively the wallet will manage the session. + */ + signer: Signer; + + /** + * Defines the allowed behavior the signer can do on behalf of the account. + */ + permission: Permission; +}; + +/** + * Represents a ERC-7715 permission response. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided. + */ +export type PermissionResponse< + Signer extends SignerParam, + Permission extends PermissionTypes, +> = PermissionRequest & { + /** + * Is a catch-all to identify a permission for revoking permissions or submitting + * Defined in ERC-7710. + */ + context: Hex; + + /** + * The accountMeta field is required and contains information needed to deploy accounts. + * Each entry specifies a factory contract and its associated deployment data. + * If no account deployment is needed when redeeming the permission, this array must be empty. + * When non-empty, DApps MUST deploy the accounts by calling the factory contract with factoryData as the calldata. + * Defined in ERC-4337. + */ + accountMeta: { + factory: Hex; + factoryData: Hex; + }[]; + + /** + * If the signer type is account then delegationManager is required as defined in ERC-7710. + */ + signerMeta: { + delegationManager: Hex; + }; +}; + +/** + * Represents a gator ERC-7715 granted(ie. signed by an user account) permission entry that is stored in profile sync. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided + */ +export type StoredGatorPermission< + Signer extends SignerParam, + Permission extends PermissionTypes, +> = { + permissionResponse: PermissionResponse; + siteOrigin: string; +}; + +/** + * Represents a list of gator permissions filtered by permission type. + */ +export type GatorPermissionsList = { + 'native-token-stream': StoredGatorPermission< + SignerParam, + NativeTokenStreamPermission + >[]; + 'native-token-periodic': StoredGatorPermission< + SignerParam, + NativeTokenPeriodicPermission + >[]; + 'erc20-token-stream': StoredGatorPermission< + SignerParam, + Erc20TokenStreamPermission + >[]; +}; diff --git a/packages/gator-permissions-controller/src/utils.test.ts b/packages/gator-permissions-controller/src/utils.test.ts new file mode 100644 index 0000000000..0ae421586e --- /dev/null +++ b/packages/gator-permissions-controller/src/utils.test.ts @@ -0,0 +1,42 @@ +import { + deserializeGatorPermissionsList, + serializeGatorPermissionsList, +} from './utils'; + +describe('utils - serializeGatorPermissionsList() tests', () => { + it('serializes a gator permissions list to a string', () => { + const gatorPermissionsList = { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }; + + const serializedGatorPermissionsList = + serializeGatorPermissionsList(gatorPermissionsList); + + expect(serializedGatorPermissionsList).toStrictEqual( + JSON.stringify(gatorPermissionsList), + ); + }); +}); + +describe('utils - deserializeGatorPermissionsList() tests', () => { + it('deserializes a gator permissions list from a string', () => { + const gatorPermissionsList = { + 'native-token-stream': [], + 'native-token-periodic': [], + 'erc20-token-stream': [], + }; + + const serializedGatorPermissionsList = + serializeGatorPermissionsList(gatorPermissionsList); + + const deserializedGatorPermissionsList = deserializeGatorPermissionsList( + serializedGatorPermissionsList, + ); + + expect(deserializedGatorPermissionsList).toStrictEqual( + gatorPermissionsList, + ); + }); +}); diff --git a/packages/gator-permissions-controller/src/utils.ts b/packages/gator-permissions-controller/src/utils.ts new file mode 100644 index 0000000000..9ceb16c0c7 --- /dev/null +++ b/packages/gator-permissions-controller/src/utils.ts @@ -0,0 +1,25 @@ +import type { GatorPermissionsList } from './types'; + +/** + * Serializes a gator permissions list to a string. + * + * @param gatorPermissionsList - The gator permissions list to serialize. + * @returns The serialized gator permissions list. + */ +export function serializeGatorPermissionsList( + gatorPermissionsList: GatorPermissionsList, +): string { + return JSON.stringify(gatorPermissionsList); +} + +/** + * Deserializes a gator permissions list from a string. + * + * @param gatorPermissionsList - The gator permissions list to deserialize. + * @returns The deserialized gator permissions list. + */ +export function deserializeGatorPermissionsList( + gatorPermissionsList: string, +): GatorPermissionsList { + return JSON.parse(gatorPermissionsList); +} \ No newline at end of file diff --git a/packages/profile-sync-controller/src/shared/storage-schema.ts b/packages/profile-sync-controller/src/shared/storage-schema.ts index 64f18ee3e0..760814aa40 100644 --- a/packages/profile-sync-controller/src/shared/storage-schema.ts +++ b/packages/profile-sync-controller/src/shared/storage-schema.ts @@ -14,6 +14,7 @@ export const USER_STORAGE_FEATURE_NAMES = { accounts: 'accounts_v2', networks: 'networks', addressBook: 'addressBook', + gatorPermissions: 'gator_7715_permissions', } as const; export type UserStorageFeatureNames = @@ -24,6 +25,7 @@ export const USER_STORAGE_SCHEMA = { [USER_STORAGE_FEATURE_NAMES.accounts]: [ALLOW_ARBITRARY_KEYS], // keyed by account addresses [USER_STORAGE_FEATURE_NAMES.networks]: [ALLOW_ARBITRARY_KEYS], // keyed by chains/networks [USER_STORAGE_FEATURE_NAMES.addressBook]: [ALLOW_ARBITRARY_KEYS], // keyed by address_chainId + [USER_STORAGE_FEATURE_NAMES.gatorPermissions]: ['gator_7715_permissions'], } as const; type UserStorageSchema = typeof USER_STORAGE_SCHEMA; diff --git a/teams.json b/teams.json index 75e1b964cd..52d755ec62 100644 --- a/teams.json +++ b/teams.json @@ -18,6 +18,7 @@ "metamask/ens-controller": "team-confirmations", "metamask/eth-json-rpc-provider": "team-wallet-api-platform,team-wallet-framework", "metamask/gas-fee-controller": "team-confirmations", + "metamask/gator-permissions-controller": "team-readable-permissions", "metamask/json-rpc-engine": "team-wallet-api-platform,team-wallet-framework", "metamask/json-rpc-middleware-stream": "team-wallet-api-platform,team-wallet-framework", "metamask/keyring-controller": "team-accounts", @@ -36,7 +37,6 @@ "metamask/preferences-controller": "team-wallet-framework", "metamask/profile-sync-controller": "team-notifications", "metamask/rate-limit-controller": "team-snaps-platform", - "metamask/gator-permissions-controller": "team-readable-permissions", "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", "metamask/sample-controllers": "team-wallet-framework", "metamask/selected-network-controller": "team-wallet-api-platform,team-wallet-framework,team-assets", diff --git a/yarn.lock b/yarn.lock index 158e94ffb9..016186de43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3568,9 +3568,12 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller" dependencies: + "@lavamoat/allow-scripts": "npm:^3.0.4" + "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.1" "@metamask/profile-sync-controller": "npm:^19.0.0" + "@metamask/utils": "npm:^11.2.0" "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" From ef745e758b561475536e71e3a4bc30f3701e542a Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:21:31 -0400 Subject: [PATCH 09/24] Increase test coverage --- .../gator-permissions-controller/CHANGELOG.md | 2 +- .../src/GatorPermissionContoller.test.ts | 106 ++++++++++++++---- .../src/GatorPermissionsController.ts | 14 +-- 3 files changed, 85 insertions(+), 37 deletions(-) diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 670f270b1c..b518709c7b 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,4 +7,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -[Unreleased]: https://github.com/MetaMask/core/ \ No newline at end of file +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index 9ebc4b132d..e71e00f6f4 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -191,7 +191,6 @@ function createGatorPermissionsMessenger() { const messenger = baseMessenger.getRestricted({ name: 'GatorPermissionsController', allowedActions: [ - 'AuthenticationController:getBearerToken', 'AuthenticationController:isSignedIn', 'AuthenticationController:performSignIn', 'UserStorageController:performGetStorageAllFeatureEntries', @@ -211,7 +210,6 @@ function createMockGatorPermissionsMessenger() { const { baseMessenger, messenger } = createGatorPermissionsMessenger(); const mockCall = jest.spyOn(messenger, 'call'); - const mockGetBearerToken = jest.fn().mockResolvedValue('MOCK_BEARER_TOKEN'); const mockIsSignedIn = jest.fn(); const mockPerformSignIn = jest.fn(); const mockGetStorageAllFeatureEntries = jest.fn().mockResolvedValue( @@ -224,10 +222,6 @@ function createMockGatorPermissionsMessenger() { mockCall.mockImplementation((...args) => { const [actionType] = args; - if (actionType === 'AuthenticationController:getBearerToken') { - return mockGetBearerToken(); - } - if (actionType === 'AuthenticationController:isSignedIn') { return mockIsSignedIn(); } @@ -250,7 +244,6 @@ function createMockGatorPermissionsMessenger() { return { messenger, baseMessenger, - mockGetBearerToken, mockIsSignedIn, mockPerformSignIn, mockGetStorageAllFeatureEntries, @@ -302,12 +295,12 @@ describe('gator-permissions-controller - disableGatorPermissions() tests', () => const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); // Mock user already signed in - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); // Then disable @@ -329,12 +322,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); // Mock user already signed in - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -365,12 +358,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' createMockGatorPermissionsMessenger(); mockGetStorageAllFeatureEntries.mockResolvedValue(null); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -386,12 +379,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' createMockGatorPermissionsMessenger(); mockGetStorageAllFeatureEntries.mockResolvedValue([]); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -415,12 +408,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' siteOrigin: 'http://localhost:8000', }), ]); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -440,12 +433,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' siteOrigin: 'http://localhost:8000', }), ]); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -459,12 +452,12 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' mockGetStorageAllFeatureEntries.mockRejectedValue( new Error('Storage error'), ); - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Enable first - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -527,12 +520,12 @@ describe('gator-permissions-controller - private methods tests', () => { const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); // Mock user already signed in - mockIsSignedIn.mockResolvedValue(true); + mockIsSignedIn.mockReturnValue(true); const controller = new GatorPermissionsController({ messenger }); // Should not throw when enabled - await controller.enableGatorPermissions(false); + await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); }); @@ -571,3 +564,70 @@ describe('gator-permissions-controller - message handlers tests', () => { ); }); }); + +describe('gator-permissions-controller - enableGatorPermissions() tests', () => { + it('enables gator permissions successfully when user is already signed in', async () => { + const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); + + // Mock user already signed in + mockIsSignedIn.mockReturnValue(true); + + const controller = new GatorPermissionsController({ messenger }); + + await controller.enableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); + + it('enables gator permissions successfully when user needs to sign in', async () => { + const { messenger, mockIsSignedIn, mockPerformSignIn } = + createMockGatorPermissionsMessenger(); + + // Mock user not signed in initially, then signed in after sign in + mockIsSignedIn.mockReturnValueOnce(false).mockReturnValueOnce(true); + mockPerformSignIn.mockResolvedValue(undefined); + + const controller = new GatorPermissionsController({ messenger }); + + await controller.enableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + expect(mockPerformSignIn).toHaveBeenCalled(); + }); + + it('handles error during enableGatorPermissions when signIn fails', async () => { + const { messenger, mockIsSignedIn, mockPerformSignIn } = + createMockGatorPermissionsMessenger(); + + // Mock user not signed in and sign in fails + mockIsSignedIn.mockReturnValue(false); + mockPerformSignIn.mockRejectedValue(new Error('Sign in failed')); + + const controller = new GatorPermissionsController({ messenger }); + + await expect(controller.enableGatorPermissions()).rejects.toThrow( + 'Unable to enable gator permissions', + ); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.isUpdatingGatorPermissions).toBe(false); + }); +}); + +describe('gator-permissions-controller - auth methods tests', () => { + it('calls signIn when user is not signed in', async () => { + const { messenger, mockIsSignedIn, mockPerformSignIn } = + createMockGatorPermissionsMessenger(); + + mockIsSignedIn.mockReturnValue(false); + mockPerformSignIn.mockResolvedValue(undefined); + + const controller = new GatorPermissionsController({ messenger }); + + await controller.enableGatorPermissions(); + + expect(mockPerformSignIn).toHaveBeenCalled(); + }); +}); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index b8b27da22f..2eeed89e33 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -125,7 +125,6 @@ export type GatorPermissionsControllerDisableGatorPermissions = // Allowed Actions export type AllowedActions = // Auth Controller Requests - | AuthenticationController.AuthenticationControllerGetBearerToken | AuthenticationController.AuthenticationControllerIsSignedIn | AuthenticationController.AuthenticationControllerPerformSignIn // User Storage Controller Requests @@ -161,11 +160,6 @@ export default class GatorPermissionsController extends BaseController< GatorPermissionsControllerMessenger > { readonly #auth = { - getBearerToken: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:getBearerToken', - ); - }, isSignedIn: () => { return this.messagingSystem.call('AuthenticationController:isSignedIn'); }, @@ -345,19 +339,13 @@ export default class GatorPermissionsController extends BaseController< * Enables gator permissions for the user. * This method ensures that the user is authenticated and enables the feature. * - * @param isFetchingPermissions - Whether to fetch permissions after enabling. * @throws {Error} If there is an error during the process of enabling permissions. */ - public async enableGatorPermissions(isFetchingPermissions: boolean = true) { + public async enableGatorPermissions() { try { this.#setIsUpdatingGatorPermissions(true); await this.#enableAuth(); this.#setIsGatorPermissionsEnabled(true); - - // Fetch initial permissions after enabling - if (isFetchingPermissions) { - await this.fetchAndUpdateGatorPermissions(); - } } catch (e) { console.error('Unable to enable gator permissions', e); throw new Error('Unable to enable gator permissions'); From 22ab42d625f5030a5434d59a913be97e0be8f2b1 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:30:11 -0400 Subject: [PATCH 10/24] Add changelog --- packages/gator-permissions-controller/CHANGELOG.md | 4 ++++ packages/profile-sync-controller/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index b518709c7b..eff38c93eb 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,4 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Initial release ([#6033](https://github.com/MetaMask/core/pull/6033)) + [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index a36c8a0953..9aae67a18b 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `gatorPermissions` feature to user storage schema for gator permissions management ([#6033](https://github.com/MetaMask/core/pull/6033)) + ## [19.0.0] ### Changed From 1affd3102fd2e2c867469d21b5e56af8acecc79b Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:01:22 -0400 Subject: [PATCH 11/24] Resolve lint error --- packages/gator-permissions-controller/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gator-permissions-controller/src/utils.ts b/packages/gator-permissions-controller/src/utils.ts index 9ceb16c0c7..01b86c46b0 100644 --- a/packages/gator-permissions-controller/src/utils.ts +++ b/packages/gator-permissions-controller/src/utils.ts @@ -22,4 +22,4 @@ export function deserializeGatorPermissionsList( gatorPermissionsList: string, ): GatorPermissionsList { return JSON.parse(gatorPermissionsList); -} \ No newline at end of file +} From 95f93e487d3f724713a268db2d3135d766d74ec6 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:17:00 -0400 Subject: [PATCH 12/24] Resolve lint error in readme --- packages/gator-permissions-controller/README.md | 5 +++-- packages/gator-permissions-controller/tsconfig.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/gator-permissions-controller/README.md b/packages/gator-permissions-controller/README.md index 042ed211b8..104a52043e 100644 --- a/packages/gator-permissions-controller/README.md +++ b/packages/gator-permissions-controller/README.md @@ -29,9 +29,10 @@ gatorPermissionsController.enableGatorPermissions(); ### Fetch from Profile Sync ```typescript -const permissions = await gatorPermissionsController.fetchAndUpdateGatorPermissions(); +const permissions = + await gatorPermissionsController.fetchAndUpdateGatorPermissions(); ``` ## Contributing -This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). \ No newline at end of file +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/gator-permissions-controller/tsconfig.json b/packages/gator-permissions-controller/tsconfig.json index 6efb36b7b6..9dbbdba829 100644 --- a/packages/gator-permissions-controller/tsconfig.json +++ b/packages/gator-permissions-controller/tsconfig.json @@ -5,7 +5,7 @@ }, "references": [ { "path": "../base-controller" }, - { "path": "../profile-sync-controller" }, + { "path": "../profile-sync-controller" } ], "include": ["../../types", "./src"] } From 6f8850e59694f317a7bf7e84e0283a49540a3d62 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:30:22 -0400 Subject: [PATCH 13/24] Export all types from GatorPermissionsController package --- packages/gator-permissions-controller/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index d1dae0cee6..eadfcb36a6 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -16,4 +16,4 @@ export type { AllowedEvents, GatorPermissionsControllerStateChangeEvent, } from './GatorPermissionsController'; -export type { StoredGatorPermission, PermissionResponse } from './types'; +export type * from './types'; From 64087217ffbafac87cf9745704947b5c88aefcf0 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:27:08 -0400 Subject: [PATCH 14/24] Update GatorPermissionsList to filter by by permission type and chainId --- .../src/GatorPermissionContoller.test.ts | 336 ++++++++++-------- .../src/GatorPermissionsController.ts | 34 +- .../gator-permissions-controller/src/types.ts | 51 ++- .../src/utils.test.ts | 12 +- 4 files changed, 246 insertions(+), 187 deletions(-) diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index e71e00f6f4..8468118732 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -1,4 +1,5 @@ import { Messenger } from '@metamask/base-controller'; +import type { Hex } from '@metamask/utils'; import type { AllowedActions, @@ -14,10 +15,15 @@ import type { StoredGatorPermission, } from './types'; +const MOCK_CHAIN_ID_1: Hex = '0xaa36a7'; +const MOCK_CHAIN_ID_2: Hex = '0x1'; + type MockGatorPermissionsStorageEntriesConfig = { - nativeTokenStream: number; - nativeTokenPeriodic: number; - erc20TokenStream: number; + [chainId: string]: { + nativeTokenStream: number; + nativeTokenPeriodic: number; + erc20TokenStream: number; + }; }; /** @@ -31,7 +37,7 @@ function createMockGatorPermissionsStorageEntries( amount: number, mockStorageEntry: StoredGatorPermission, ): string[] { - return Array.from({ length: amount }, (index: number) => + return Array.from({ length: amount }, (_, index: number) => JSON.stringify({ ...mockStorageEntry, expiry: mockStorageEntry.permissionResponse.expiry + index, @@ -48,137 +54,144 @@ function createMockGatorPermissionsStorageEntries( function mockGatorPermissionsStorageEntriesFactory( config: MockGatorPermissionsStorageEntriesConfig, ): string[] { - const mockNativeTokenStreamStorageEntry: StoredGatorPermission< - AccountSigner, - NativeTokenStreamPermission - > = { - permissionResponse: { - chainId: '0xaa36a7', - address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { - type: 'account', - data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, - }, - permission: { - type: 'native-token-stream', - data: { - maxAmount: '0x22b1c8c1227a0000', - initialAmount: '0x6f05b59d3b20000', - amountPerSecond: '0x6f05b59d3b20000', - startTime: 1747699200, - justification: - 'This is a very important request for streaming allowance for some very important thing', + const result: string[] = []; + + // Create entries for each chainId + Object.entries(config).forEach(([chainId, counts]) => { + const mockNativeTokenStreamStorageEntry: StoredGatorPermission< + AccountSigner, + NativeTokenStreamPermission + > = { + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, }, - rules: {}, - }, - context: '0x00000000', - accountMeta: [ - { - factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', - factoryData: '0x0000000', + permission: { + type: 'native-token-stream', + data: { + maxAmount: '0x22b1c8c1227a0000', + initialAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', }, - ], - signerMeta: { - delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', }, - }, - siteOrigin: 'http://localhost:8000', - }; + siteOrigin: 'http://localhost:8000', + }; - const mockNativeTokenPeriodicStorageEntry: StoredGatorPermission< - AccountSigner, - NativeTokenPeriodicPermission - > = { - permissionResponse: { - chainId: '0xaa36a7', - address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { - type: 'account', - data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, - }, - permission: { - type: 'native-token-periodic', - data: { - periodAmount: '0x22b1c8c1227a0000', - periodDuration: 1747699200, - startTime: 1747699200, - justification: - 'This is a very important request for streaming allowance for some very important thing', + const mockNativeTokenPeriodicStorageEntry: StoredGatorPermission< + AccountSigner, + NativeTokenPeriodicPermission + > = { + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, }, - rules: {}, - }, - context: '0x00000000', - accountMeta: [ - { - factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', - factoryData: '0x0000000', + permission: { + type: 'native-token-periodic', + data: { + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', }, - ], - signerMeta: { - delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', }, - }, - siteOrigin: 'http://localhost:8000', - }; + siteOrigin: 'http://localhost:8000', + }; - const mockErc20TokenStreamStorageEntry: StoredGatorPermission< - AccountSigner, - Erc20TokenStreamPermission - > = { - permissionResponse: { - chainId: '0xaa36a7', - address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { - type: 'account', - data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, - }, - permission: { - type: 'erc20-token-stream', - data: { - initialAmount: '0x22b1c8c1227a0000', - maxAmount: '0x6f05b59d3b20000', - amountPerSecond: '0x6f05b59d3b20000', - startTime: 1747699200, - tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - justification: - 'This is a very important request for streaming allowance for some very important thing', + const mockErc20TokenStreamStorageEntry: StoredGatorPermission< + AccountSigner, + Erc20TokenStreamPermission + > = { + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, }, - rules: {}, - }, - context: '0x00000000', - accountMeta: [ - { - factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', - factoryData: '0x0000000', + permission: { + type: 'erc20-token-stream', + data: { + initialAmount: '0x22b1c8c1227a0000', + maxAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', }, - ], - signerMeta: { - delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', }, - }, - siteOrigin: 'http://localhost:8000', - }; + siteOrigin: 'http://localhost:8000', + }; - return [ - ...createMockGatorPermissionsStorageEntries( - config.nativeTokenStream, - mockNativeTokenStreamStorageEntry, - ), - ...createMockGatorPermissionsStorageEntries( - config.nativeTokenPeriodic, - mockNativeTokenPeriodicStorageEntry, - ), - ...createMockGatorPermissionsStorageEntries( - config.erc20TokenStream, - mockErc20TokenStreamStorageEntry, - ), - ]; + result.push( + ...createMockGatorPermissionsStorageEntries( + counts.nativeTokenStream, + mockNativeTokenStreamStorageEntry, + ), + ...createMockGatorPermissionsStorageEntries( + counts.nativeTokenPeriodic, + mockNativeTokenPeriodicStorageEntry, + ), + ...createMockGatorPermissionsStorageEntries( + counts.erc20TokenStream, + mockErc20TokenStreamStorageEntry, + ), + ); + }); + + return result; } /** @@ -214,9 +227,16 @@ function createMockGatorPermissionsMessenger() { const mockPerformSignIn = jest.fn(); const mockGetStorageAllFeatureEntries = jest.fn().mockResolvedValue( mockGatorPermissionsStorageEntriesFactory({ - nativeTokenStream: 5, - nativeTokenPeriodic: 5, - erc20TokenStream: 5, + [MOCK_CHAIN_ID_1]: { + nativeTokenStream: 5, + nativeTokenPeriodic: 5, + erc20TokenStream: 5, + }, + [MOCK_CHAIN_ID_2]: { + nativeTokenStream: 5, + nativeTokenPeriodic: 5, + erc20TokenStream: 5, + }, }), ); @@ -259,9 +279,9 @@ describe('gator-permissions-controller - constructor() tests', () => { expect(controller.state.isGatorPermissionsEnabled).toBe(false); expect(controller.state.gatorPermissionsListStringify).toStrictEqual( JSON.stringify({ - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }), ); expect(controller.state.isFetchingGatorPermissions).toBe(false); @@ -272,9 +292,9 @@ describe('gator-permissions-controller - constructor() tests', () => { const customState = { isGatorPermissionsEnabled: true, gatorPermissionsListStringify: JSON.stringify({ - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }), }; @@ -309,9 +329,9 @@ describe('gator-permissions-controller - disableGatorPermissions() tests', () => expect(controller.state.isGatorPermissionsEnabled).toBe(false); expect(controller.state.gatorPermissionsListStringify).toBe( JSON.stringify({ - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }), ); }); @@ -332,14 +352,18 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' const result = await controller.fetchAndUpdateGatorPermissions(); expect(result).toStrictEqual({ - 'native-token-stream': expect.any(Array), - 'native-token-periodic': expect.any(Array), - 'erc20-token-stream': expect.any(Array), + 'native-token-stream': expect.any(Object), + 'native-token-periodic': expect.any(Object), + 'erc20-token-stream': expect.any(Object), }); - expect(result['native-token-stream']).toHaveLength(5); - expect(result['native-token-periodic']).toHaveLength(5); - expect(result['erc20-token-stream']).toHaveLength(5); + // Check that each permission type has the expected chainId + expect(result['native-token-stream'][MOCK_CHAIN_ID_1]).toHaveLength(5); + expect(result['native-token-periodic'][MOCK_CHAIN_ID_1]).toHaveLength(5); + expect(result['erc20-token-stream'][MOCK_CHAIN_ID_1]).toHaveLength(5); + expect(result['native-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); + expect(result['native-token-periodic'][MOCK_CHAIN_ID_2]).toHaveLength(5); + expect(result['erc20-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); expect(controller.state.isFetchingGatorPermissions).toBe(false); }); @@ -368,9 +392,9 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' const result = await controller.fetchAndUpdateGatorPermissions(); expect(result).toStrictEqual({ - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }); }); @@ -389,9 +413,9 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' const result = await controller.fetchAndUpdateGatorPermissions(); expect(result).toStrictEqual({ - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }); }); @@ -476,9 +500,9 @@ describe('gator-permissions-controller - gatorPermissionsList getter tests', () const permissionsList = controller.gatorPermissionsList; expect(permissionsList).toStrictEqual({ - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }); }); @@ -489,9 +513,9 @@ describe('gator-permissions-controller - gatorPermissionsList getter tests', () messenger, state: { gatorPermissionsListStringify: JSON.stringify({ - 'native-token-stream': [{ id: '1' }], - 'native-token-periodic': [{ id: '2' }], - 'erc20-token-stream': [{ id: '3' }], + 'native-token-stream': { '0x1': [{ id: '1' }] }, + 'native-token-periodic': { '0x2': [{ id: '2' }] }, + 'erc20-token-stream': { '0x3': [{ id: '3' }] }, }), }, }); @@ -499,9 +523,9 @@ describe('gator-permissions-controller - gatorPermissionsList getter tests', () const permissionsList = controller.gatorPermissionsList; expect(permissionsList).toStrictEqual({ - 'native-token-stream': [{ id: '1' }], - 'native-token-periodic': [{ id: '2' }], - 'erc20-token-stream': [{ id: '3' }], + 'native-token-stream': { '0x1': [{ id: '1' }] }, + 'native-token-periodic': { '0x2': [{ id: '2' }] }, + 'erc20-token-stream': { '0x3': [{ id: '3' }] }, }); }); }); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 2eeed89e33..5b147253df 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -76,9 +76,9 @@ const metadata: StateMetadata = { }; const defaultGatorPermissionsList: GatorPermissionsList = { - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }; export const defaultState: GatorPermissionsControllerState = { @@ -260,13 +260,13 @@ export default class GatorPermissionsController extends BaseController< } /** - * Parses permissions from profile sync data and categorizes them by type. + * Parses permissions from profile sync data and categorizes them by type and chainId. * * @param permissionsData - An JSON stringified array of permission strings from profile sync. * @returns Parsed and categorized permissions list. * @throws {Error} If permission type is invalid. */ - #categorizePermissionsDataByType( + #categorizePermissionsDataByTypeAndChainId( permissionsData: string[] | null, ): GatorPermissionsList { if (!permissionsData) { @@ -287,23 +287,33 @@ export default class GatorPermissionsController extends BaseController< const permissionType = parsedPermission.permissionResponse.permission.type; + const { chainId } = parsedPermission.permissionResponse; if (permissionType === 'native-token-stream') { - gatorPermissionsList['native-token-stream'].push( + if (!gatorPermissionsList['native-token-stream'][chainId]) { + gatorPermissionsList['native-token-stream'][chainId] = []; + } + gatorPermissionsList['native-token-stream'][chainId].push( parsedPermission as StoredGatorPermission< SignerParam, NativeTokenStreamPermission >, ); } else if (permissionType === 'native-token-periodic') { - gatorPermissionsList['native-token-periodic'].push( + if (!gatorPermissionsList['native-token-periodic'][chainId]) { + gatorPermissionsList['native-token-periodic'][chainId] = []; + } + gatorPermissionsList['native-token-periodic'][chainId].push( parsedPermission as StoredGatorPermission< SignerParam, NativeTokenPeriodicPermission >, ); } else if (permissionType === 'erc20-token-stream') { - gatorPermissionsList['erc20-token-stream'].push( + if (!gatorPermissionsList['erc20-token-stream'][chainId]) { + gatorPermissionsList['erc20-token-stream'][chainId] = []; + } + gatorPermissionsList['erc20-token-stream'][chainId].push( parsedPermission as StoredGatorPermission< SignerParam, Erc20TokenStreamPermission @@ -316,9 +326,9 @@ export default class GatorPermissionsController extends BaseController< return gatorPermissionsList; }, { - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, } as GatorPermissionsList, ); } @@ -391,7 +401,7 @@ export default class GatorPermissionsController extends BaseController< // Categorize permissions by type and update state const gatorPermissionsList = - this.#categorizePermissionsDataByType(permissionsData); + this.#categorizePermissionsDataByTypeAndChainId(permissionsData); this.update((state) => { state.gatorPermissionsListStringify = diff --git a/packages/gator-permissions-controller/src/types.ts b/packages/gator-permissions-controller/src/types.ts index 7d85073296..ac0f7f5092 100644 --- a/packages/gator-permissions-controller/src/types.ts +++ b/packages/gator-permissions-controller/src/types.ts @@ -174,19 +174,44 @@ export type StoredGatorPermission< }; /** - * Represents a list of gator permissions filtered by permission type. + * Represents a list of gator permissions filtered by permission type and chainId. */ export type GatorPermissionsList = { - 'native-token-stream': StoredGatorPermission< - SignerParam, - NativeTokenStreamPermission - >[]; - 'native-token-periodic': StoredGatorPermission< - SignerParam, - NativeTokenPeriodicPermission - >[]; - 'erc20-token-stream': StoredGatorPermission< - SignerParam, - Erc20TokenStreamPermission - >[]; + 'native-token-stream': { + [chainId: Hex]: StoredGatorPermission< + SignerParam, + NativeTokenStreamPermission + >[]; + }; + 'native-token-periodic': { + [chainId: Hex]: StoredGatorPermission< + SignerParam, + NativeTokenPeriodicPermission + >[]; + }; + 'erc20-token-stream': { + [chainId: Hex]: StoredGatorPermission< + SignerParam, + Erc20TokenStreamPermission + >[]; + }; }; + +/** + * Represents the supported permission type(e.g. 'native-token-stream', 'native-token-periodic', 'erc20-token-stream') of the gator permissions list. + */ +export type SupportedGatorPermissionType = keyof GatorPermissionsList; + +/** + * Represents a list of gator permissions filtered by permission type.(ie, a record of gator permissions by permission type with chainId as the key) + */ +export type GatorPermissionsListByPermissionType< + PermissionType extends SupportedGatorPermissionType, +> = GatorPermissionsList[PermissionType]; + +/** + * Represents a list of gator permissions filtered by permission type and chainId.(ie, a array of gator permissions for a given chainId and permission type) + */ +export type GatorPermissionsListItemsByPermissionTypeAndChainId< + PermissionType extends SupportedGatorPermissionType, +> = GatorPermissionsList[PermissionType][Hex]; diff --git a/packages/gator-permissions-controller/src/utils.test.ts b/packages/gator-permissions-controller/src/utils.test.ts index 0ae421586e..2e273e22ad 100644 --- a/packages/gator-permissions-controller/src/utils.test.ts +++ b/packages/gator-permissions-controller/src/utils.test.ts @@ -6,9 +6,9 @@ import { describe('utils - serializeGatorPermissionsList() tests', () => { it('serializes a gator permissions list to a string', () => { const gatorPermissionsList = { - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }; const serializedGatorPermissionsList = @@ -23,9 +23,9 @@ describe('utils - serializeGatorPermissionsList() tests', () => { describe('utils - deserializeGatorPermissionsList() tests', () => { it('deserializes a gator permissions list from a string', () => { const gatorPermissionsList = { - 'native-token-stream': [], - 'native-token-periodic': [], - 'erc20-token-stream': [], + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, }; const serializedGatorPermissionsList = From 685514a46573f8cef39cede880c59a64af3a2690 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:45:11 -0400 Subject: [PATCH 15/24] Update @metamask/profile-sync-controller deps to 20.0.0 --- .../gator-permissions-controller/package.json | 4 +- yarn.lock | 74 +------------------ 2 files changed, 5 insertions(+), 73 deletions(-) diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index f21985d252..6031679123 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -54,7 +54,7 @@ "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/profile-sync-controller": "^19.0.0", + "@metamask/profile-sync-controller": "^20.0.0", "@ts-bridge/cli": "^0.6.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", @@ -65,7 +65,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/profile-sync-controller": "^19.0.0" + "@metamask/profile-sync-controller": "^20.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index ca43dd59d7..0dc43d5c2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3579,7 +3579,7 @@ __metadata: "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.1" - "@metamask/profile-sync-controller": "npm:^19.0.0" + "@metamask/profile-sync-controller": "npm:^20.0.0" "@metamask/utils": "npm:^11.2.0" "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" @@ -3590,7 +3590,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/profile-sync-controller": ^19.0.0 + "@metamask/profile-sync-controller": ^20.0.0 languageName: unknown linkType: soft @@ -4164,29 +4164,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^19.0.0": - version: 19.0.0 - resolution: "@metamask/profile-sync-controller@npm:19.0.0" - dependencies: - "@metamask/base-controller": "npm:^8.0.1" - "@metamask/snaps-sdk": "npm:^7.1.0" - "@metamask/snaps-utils": "npm:^9.4.0" - "@noble/ciphers": "npm:^0.5.2" - "@noble/hashes": "npm:^1.4.0" - immer: "npm:^9.0.6" - loglevel: "npm:^1.8.1" - siwe: "npm:^2.3.2" - peerDependencies: - "@metamask/accounts-controller": ^31.0.0 - "@metamask/keyring-controller": ^22.0.0 - "@metamask/network-controller": ^24.0.0 - "@metamask/providers": ^22.0.0 - "@metamask/snaps-controllers": ^12.0.0 - webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/4029a43e829288bcfe3fce51b382ce8c4323729ae1e3483213b26440b52b1914a67a56184704d87f8ac6d8c3bbc25ca32206c57b4c27cc23be15629247d05464 - languageName: node - linkType: hard - "@metamask/profile-sync-controller@npm:^20.0.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": version: 0.0.0-use.local resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" @@ -4506,19 +4483,6 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^7.1.0": - version: 7.1.0 - resolution: "@metamask/snaps-sdk@npm:7.1.0" - dependencies: - "@metamask/key-tree": "npm:^10.1.1" - "@metamask/providers": "npm:^22.1.0" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/superstruct": "npm:^3.2.1" - "@metamask/utils": "npm:^11.4.0" - checksum: 10/917cacb0fe9ca568da0177303b3aa1bf759b7ad8b7682dde52e631a45038487ccabd25cdca7aff97230f08f211d71eb3767976d6dd51e986f9af812de31a4f4e - languageName: node - linkType: hard - "@metamask/snaps-sdk@npm:^9.0.0": version: 9.0.0 resolution: "@metamask/snaps-sdk@npm:9.0.0" @@ -4564,38 +4528,6 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^9.4.0": - version: 9.4.0 - resolution: "@metamask/snaps-utils@npm:9.4.0" - dependencies: - "@babel/core": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - "@metamask/base-controller": "npm:^8.0.1" - "@metamask/key-tree": "npm:^10.1.1" - "@metamask/permission-controller": "npm:^11.0.6" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/slip44": "npm:^4.2.0" - "@metamask/snaps-registry": "npm:^3.2.3" - "@metamask/snaps-sdk": "npm:^7.1.0" - "@metamask/superstruct": "npm:^3.2.1" - "@metamask/utils": "npm:^11.4.0" - "@noble/hashes": "npm:^1.7.1" - "@scure/base": "npm:^1.1.1" - chalk: "npm:^4.1.2" - cron-parser: "npm:^4.5.0" - fast-deep-equal: "npm:^3.1.3" - fast-json-stable-stringify: "npm:^2.1.0" - fast-xml-parser: "npm:^4.4.1" - luxon: "npm:^3.5.0" - marked: "npm:^12.0.1" - rfdc: "npm:^1.3.0" - semver: "npm:^7.5.4" - ses: "npm:^1.12.0" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10/cda4c8f631859e3c6c364851064f132ad02fea0074493383e0e0facfff7b814321c87b1186c30b399fd2f1dc79ec97ed388450c59c627fd6db73549f964e0618 - languageName: node - linkType: hard - "@metamask/stake-sdk@npm:^3.2.1": version: 3.2.1 resolution: "@metamask/stake-sdk@npm:3.2.1" @@ -13531,7 +13463,7 @@ __metadata: languageName: node linkType: hard -"ses@npm:^1.12.0, ses@npm:^1.13.1": +"ses@npm:^1.13.1": version: 1.13.1 resolution: "ses@npm:1.13.1" dependencies: From 66ee8c0a5e715e93a4fe6bd1093335948f4bb9a0 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:11:10 -0400 Subject: [PATCH 16/24] Upgrade @metamask/utils version --- packages/gator-permissions-controller/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index 6031679123..f49f76eb1c 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/base-controller": "^8.0.1", - "@metamask/utils": "^11.2.0" + "@metamask/utils": "^11.4.2" }, "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", diff --git a/yarn.lock b/yarn.lock index 0dc43d5c2c..72acb19267 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3580,7 +3580,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.1" "@metamask/profile-sync-controller": "npm:^20.0.0" - "@metamask/utils": "npm:^11.2.0" + "@metamask/utils": "npm:^11.4.2" "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -4681,7 +4681,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.2.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2": version: 11.4.2 resolution: "@metamask/utils@npm:11.4.2" dependencies: From 5ddd10955016720984cfe46938d5ee512d349c33 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:43:21 -0400 Subject: [PATCH 17/24] Use SnapController instead of UserStorageContoller to send rpc request directly to gator-snap --- README.md | 1 - .../gator-permissions-controller/package.json | 6 +- .../src/GatorPermissionContoller.test.ts | 272 ++++++++---------- .../src/GatorPermissionsController.ts | 263 +++++++++-------- .../gator-permissions-controller/src/index.ts | 4 +- .../tsconfig.build.json | 5 +- .../tsconfig.json | 5 +- yarn.lock | 6 +- 8 files changed, 280 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index 53e899da74..f414f3a1e8 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,6 @@ linkStyle default opacity:0.5 gas_fee_controller --> polling_controller; gas_fee_controller --> network_controller; gator-permissions-controller --> base_controller; - gator-permissions-controller --> profile_sync_controller; json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; logging_controller --> base_controller; diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index f49f76eb1c..9d648de48b 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -48,13 +48,15 @@ }, "dependencies": { "@metamask/base-controller": "^8.0.1", + "@metamask/snaps-sdk": "^9.0.0", + "@metamask/snaps-utils": "^11.0.0", "@metamask/utils": "^11.4.2" }, "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/profile-sync-controller": "^20.0.0", + "@metamask/snaps-controllers": "^14.0.1", "@ts-bridge/cli": "^0.6.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", @@ -65,7 +67,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/profile-sync-controller": "^20.0.0" + "@metamask/snaps-controllers": "^14.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index 8468118732..6bf6a5efc7 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -1,9 +1,11 @@ import { Messenger } from '@metamask/base-controller'; +import type { SnapId } from '@metamask/snaps-sdk'; import type { Hex } from '@metamask/utils'; import type { AllowedActions, AllowedEvents, + GatorPermissionsControllerConfig, } from './GatorPermissionsController'; import GatorPermissionsController from './GatorPermissionsController'; import type { @@ -36,13 +38,14 @@ type MockGatorPermissionsStorageEntriesConfig = { function createMockGatorPermissionsStorageEntries( amount: number, mockStorageEntry: StoredGatorPermission, -): string[] { - return Array.from({ length: amount }, (_, index: number) => - JSON.stringify({ - ...mockStorageEntry, +): StoredGatorPermission[] { + return Array.from({ length: amount }, (_, index: number) => ({ + ...mockStorageEntry, + permissionResponse: { + ...mockStorageEntry.permissionResponse, expiry: mockStorageEntry.permissionResponse.expiry + index, - }), - ); + }, + })); } /** @@ -53,8 +56,8 @@ function createMockGatorPermissionsStorageEntries( */ function mockGatorPermissionsStorageEntriesFactory( config: MockGatorPermissionsStorageEntriesConfig, -): string[] { - const result: string[] = []; +): StoredGatorPermission[] { + const result: StoredGatorPermission[] = []; // Create entries for each chainId Object.entries(config).forEach(([chainId, counts]) => { @@ -203,11 +206,7 @@ function createGatorPermissionsMessenger() { const baseMessenger = new Messenger(); const messenger = baseMessenger.getRestricted({ name: 'GatorPermissionsController', - allowedActions: [ - 'AuthenticationController:isSignedIn', - 'AuthenticationController:performSignIn', - 'UserStorageController:performGetStorageAllFeatureEntries', - ], + allowedActions: ['SnapController:handleRequest', 'SnapController:has'], allowedEvents: [], }); @@ -223,9 +222,7 @@ function createMockGatorPermissionsMessenger() { const { baseMessenger, messenger } = createGatorPermissionsMessenger(); const mockCall = jest.spyOn(messenger, 'call'); - const mockIsSignedIn = jest.fn(); - const mockPerformSignIn = jest.fn(); - const mockGetStorageAllFeatureEntries = jest.fn().mockResolvedValue( + const mockGetGrantedPermissions = jest.fn().mockResolvedValue( mockGatorPermissionsStorageEntriesFactory({ [MOCK_CHAIN_ID_1]: { nativeTokenStream: 5, @@ -239,21 +236,15 @@ function createMockGatorPermissionsMessenger() { }, }), ); + const mockHasSnap = jest.fn().mockResolvedValue(true); mockCall.mockImplementation((...args) => { const [actionType] = args; - if (actionType === 'AuthenticationController:isSignedIn') { - return mockIsSignedIn(); + if (actionType === 'SnapController:handleRequest') { + return mockGetGrantedPermissions(); } - - if (actionType === 'AuthenticationController:performSignIn') { - return mockPerformSignIn(); - } - - if ( - actionType === 'UserStorageController:performGetStorageAllFeatureEntries' - ) { - return mockGetStorageAllFeatureEntries(); + if (actionType === 'SnapController:has') { + return mockHasSnap(); } throw new Error( @@ -264,16 +255,20 @@ function createMockGatorPermissionsMessenger() { return { messenger, baseMessenger, - mockIsSignedIn, - mockPerformSignIn, - mockGetStorageAllFeatureEntries, + mockGetGrantedPermissions, + mockHasSnap, }; } +const mockGatorPermissionsControllerConfig: GatorPermissionsControllerConfig = { + gatorPermissionsProviderSnapId: 'local:http://localhost:8082' as SnapId, +}; + describe('gator-permissions-controller - constructor() tests', () => { it('creates GatorPermissionsController with default state', () => { const controller = new GatorPermissionsController({ messenger: createMockGatorPermissionsMessenger().messenger, + config: mockGatorPermissionsControllerConfig, }); expect(controller.state.isGatorPermissionsEnabled).toBe(false); @@ -285,7 +280,6 @@ describe('gator-permissions-controller - constructor() tests', () => { }), ); expect(controller.state.isFetchingGatorPermissions).toBe(false); - expect(controller.state.isUpdatingGatorPermissions).toBe(false); }); it('creates GatorPermissionsController with custom state', () => { @@ -300,24 +294,40 @@ describe('gator-permissions-controller - constructor() tests', () => { const controller = new GatorPermissionsController({ messenger: createMockGatorPermissionsMessenger().messenger, + config: mockGatorPermissionsControllerConfig, state: customState, }); + expect(controller.permissionsProviderSnapId).toBe( + mockGatorPermissionsControllerConfig.gatorPermissionsProviderSnapId, + ); expect(controller.state.isGatorPermissionsEnabled).toBe(true); expect(controller.state.gatorPermissionsListStringify).toBe( customState.gatorPermissionsListStringify, ); }); + + it('creates GatorPermissionsController with default config', () => { + const controller = new GatorPermissionsController({ + messenger: createMockGatorPermissionsMessenger().messenger, + }); + + expect(controller.permissionsProviderSnapId).toBe( + '@metamask/gator-permissions-snap' as SnapId, + ); + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.isFetchingGatorPermissions).toBe(false); + }); }); describe('gator-permissions-controller - disableGatorPermissions() tests', () => { it('disables gator permissions successfully', async () => { - const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); - - // Mock user already signed in - mockIsSignedIn.mockReturnValue(true); + const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -339,12 +349,12 @@ describe('gator-permissions-controller - disableGatorPermissions() tests', () => describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests', () => { it('fetches and updates gator permissions successfully', async () => { - const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); - - // Mock user already signed in - mockIsSignedIn.mockReturnValue(true); + const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -370,7 +380,10 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' it('throws error when gator permissions are not enabled', async () => { const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( 'Failed to fetch gator permissions', @@ -378,13 +391,15 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); it('handles null permissions data', async () => { - const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + const { messenger, mockGetGrantedPermissions } = createMockGatorPermissionsMessenger(); - mockGetStorageAllFeatureEntries.mockResolvedValue(null); - mockIsSignedIn.mockReturnValue(true); + mockGetGrantedPermissions.mockResolvedValue(null); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -399,13 +414,15 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); it('handles empty permissions data', async () => { - const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + const { messenger, mockGetGrantedPermissions } = createMockGatorPermissionsMessenger(); - mockGetStorageAllFeatureEntries.mockResolvedValue([]); - mockIsSignedIn.mockReturnValue(true); + mockGetGrantedPermissions.mockResolvedValue([]); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -420,21 +437,30 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); it('throws error for invalid permission type', async () => { - const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + const { messenger, mockGetGrantedPermissions } = createMockGatorPermissionsMessenger(); - mockGetStorageAllFeatureEntries.mockResolvedValue([ - JSON.stringify({ + mockGetGrantedPermissions.mockResolvedValue([ + { permissionResponse: { + chainId: '0x1' as Hex, + address: '0x123', + expiry: 1750291200, + isAdjustmentAllowed: true, signer: { type: 'account', data: { address: '0x123' } }, permission: { type: 'invalid-type' }, + context: '0x00000000', + accountMeta: [], + signerMeta: {}, }, siteOrigin: 'http://localhost:8000', - }), + }, ]); - mockIsSignedIn.mockReturnValue(true); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -445,21 +471,30 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); it('throws error for non-account signer type', async () => { - const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + const { messenger, mockGetGrantedPermissions } = createMockGatorPermissionsMessenger(); - mockGetStorageAllFeatureEntries.mockResolvedValue([ - JSON.stringify({ + mockGetGrantedPermissions.mockResolvedValue([ + { permissionResponse: { + chainId: '0x1' as Hex, + address: '0x123', + expiry: 1750291200, + isAdjustmentAllowed: true, signer: { type: 'wallet', data: {} }, permission: { type: 'native-token-stream' }, + context: '0x00000000', + accountMeta: [], + signerMeta: {}, }, siteOrigin: 'http://localhost:8000', - }), + }, ]); - mockIsSignedIn.mockReturnValue(true); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -470,15 +505,15 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); it('handles error during fetch and update', async () => { - const { messenger, mockGetStorageAllFeatureEntries, mockIsSignedIn } = + const { messenger, mockGetGrantedPermissions } = createMockGatorPermissionsMessenger(); - mockGetStorageAllFeatureEntries.mockRejectedValue( - new Error('Storage error'), - ); - mockIsSignedIn.mockReturnValue(true); + mockGetGrantedPermissions.mockRejectedValue(new Error('Storage error')); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Enable first await controller.enableGatorPermissions(); @@ -495,7 +530,10 @@ describe('gator-permissions-controller - gatorPermissionsList getter tests', () it('returns parsed gator permissions list', () => { const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); const permissionsList = controller.gatorPermissionsList; @@ -511,6 +549,7 @@ describe('gator-permissions-controller - gatorPermissionsList getter tests', () const controller = new GatorPermissionsController({ messenger, + config: mockGatorPermissionsControllerConfig, state: { gatorPermissionsListStringify: JSON.stringify({ 'native-token-stream': { '0x1': [{ id: '1' }] }, @@ -534,34 +573,26 @@ describe('gator-permissions-controller - private methods tests', () => { it('clears loading states on initialization', () => { const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); expect(controller.state.isFetchingGatorPermissions).toBe(false); - expect(controller.state.isUpdatingGatorPermissions).toBe(false); }); it('asserts gator permissions are enabled', async () => { - const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); - - // Mock user already signed in - mockIsSignedIn.mockReturnValue(true); + const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); // Should not throw when enabled await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); }); - - it('asserts gator permissions are not enabled', async () => { - const { messenger } = createMockGatorPermissionsMessenger(); - - const controller = new GatorPermissionsController({ messenger }); - - await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', - ); - }); }); describe('gator-permissions-controller - message handlers tests', () => { @@ -572,7 +603,10 @@ describe('gator-permissions-controller - message handlers tests', () => { 'registerActionHandler', ); - new GatorPermissionsController({ messenger }); + new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); expect(mockRegisterActionHandler).toHaveBeenCalledWith( 'GatorPermissionsController:fetchAndUpdateGatorPermissions', @@ -590,68 +624,16 @@ describe('gator-permissions-controller - message handlers tests', () => { }); describe('gator-permissions-controller - enableGatorPermissions() tests', () => { - it('enables gator permissions successfully when user is already signed in', async () => { - const { messenger, mockIsSignedIn } = createMockGatorPermissionsMessenger(); - - // Mock user already signed in - mockIsSignedIn.mockReturnValue(true); - - const controller = new GatorPermissionsController({ messenger }); - - await controller.enableGatorPermissions(); - - expect(controller.state.isGatorPermissionsEnabled).toBe(true); - expect(controller.state.isUpdatingGatorPermissions).toBe(false); - }); - - it('enables gator permissions successfully when user needs to sign in', async () => { - const { messenger, mockIsSignedIn, mockPerformSignIn } = - createMockGatorPermissionsMessenger(); - - // Mock user not signed in initially, then signed in after sign in - mockIsSignedIn.mockReturnValueOnce(false).mockReturnValueOnce(true); - mockPerformSignIn.mockResolvedValue(undefined); + it('enables gator permissions successfully', async () => { + const { messenger } = createMockGatorPermissionsMessenger(); - const controller = new GatorPermissionsController({ messenger }); + const controller = new GatorPermissionsController({ + messenger, + config: mockGatorPermissionsControllerConfig, + }); await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); - expect(controller.state.isUpdatingGatorPermissions).toBe(false); - expect(mockPerformSignIn).toHaveBeenCalled(); - }); - - it('handles error during enableGatorPermissions when signIn fails', async () => { - const { messenger, mockIsSignedIn, mockPerformSignIn } = - createMockGatorPermissionsMessenger(); - - // Mock user not signed in and sign in fails - mockIsSignedIn.mockReturnValue(false); - mockPerformSignIn.mockRejectedValue(new Error('Sign in failed')); - - const controller = new GatorPermissionsController({ messenger }); - - await expect(controller.enableGatorPermissions()).rejects.toThrow( - 'Unable to enable gator permissions', - ); - - expect(controller.state.isGatorPermissionsEnabled).toBe(false); - expect(controller.state.isUpdatingGatorPermissions).toBe(false); - }); -}); - -describe('gator-permissions-controller - auth methods tests', () => { - it('calls signIn when user is not signed in', async () => { - const { messenger, mockIsSignedIn, mockPerformSignIn } = - createMockGatorPermissionsMessenger(); - - mockIsSignedIn.mockReturnValue(false); - mockPerformSignIn.mockResolvedValue(undefined); - - const controller = new GatorPermissionsController({ messenger }); - - await controller.enableGatorPermissions(); - - expect(mockPerformSignIn).toHaveBeenCalled(); }); }); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 5b147253df..711806e386 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -5,10 +5,9 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import type { - AuthenticationController, - UserStorageController, -} from '@metamask/profile-sync-controller'; +import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers'; +import type { SnapId } from '@metamask/snaps-sdk'; +import { HandlerType } from '@metamask/snaps-utils'; import type { GatorPermissionsList, @@ -27,8 +26,17 @@ import { // Unique name for the controller const controllerName = 'GatorPermissionsController'; -// Unique name for the feature in profile sync -const GATOR_PERMISSIONS_FEATURE_NAME = 'gator_7715_permissions'; +// Default value for the gator permissions provider snap id +const defaultGatorPermissionsProviderSnapId = + '@metamask/gator-permissions-snap' as SnapId; + +// Enum for the RPC methods of the gator permissions provider snap +enum GatorPermissionsSnapRpcMethod { + /** + * This method is used by the metamask to request a permissions provider to get granted permissions for all sites. + */ + PermissionProviderGetGrantedPermissions = 'permissionsProvider_getGrantedPermissions', +} /** * State shape for GatorPermissionsController @@ -49,11 +57,6 @@ export type GatorPermissionsControllerState = { * This is used to show a loading spinner in the UI */ isFetchingGatorPermissions: boolean; - - /** - * Flag that indicates that updating gator permissions is in progress - */ - isUpdatingGatorPermissions: boolean; }; const metadata: StateMetadata = { @@ -69,10 +72,6 @@ const metadata: StateMetadata = { persist: false, anonymous: false, }, - isUpdatingGatorPermissions: { - persist: false, - anonymous: false, - }, }; const defaultGatorPermissionsList: GatorPermissionsList = { @@ -86,7 +85,6 @@ export const defaultState: GatorPermissionsControllerState = { gatorPermissionsListStringify: serializeGatorPermissionsList( defaultGatorPermissionsList, ), - isUpdatingGatorPermissions: false, isFetchingGatorPermissions: false, }; @@ -109,7 +107,7 @@ export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< >; // Messenger Actions -export type Actions = +export type GatorPermissionsControllerActions = | ActionsObj[keyof ActionsObj] | GatorPermissionsControllerGetStateAction; @@ -122,35 +120,53 @@ export type GatorPermissionsControllerEnableGatorPermissions = export type GatorPermissionsControllerDisableGatorPermissions = ActionsObj['disableGatorPermissions']; -// Allowed Actions +/** + * Actions that this controller is allowed to call. + */ export type AllowedActions = - // Auth Controller Requests - | AuthenticationController.AuthenticationControllerIsSignedIn - | AuthenticationController.AuthenticationControllerPerformSignIn - // User Storage Controller Requests - | UserStorageController.UserStorageControllerPerformGetStorageAllFeatureEntries; + // Snap Requests + HandleSnapRequest | HasSnap; -// Messenger Events export type GatorPermissionsControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, GatorPermissionsControllerState >; -export type Events = GatorPermissionsControllerStateChangeEvent; +/** + * Events emitted by GatorPermissionsController. + */ +export type GatorPermissionsControllerActionsEvents = + GatorPermissionsControllerStateChangeEvent; -// Allowed Events +/** + * Events that this controller is allowed to subscribe to. + */ export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; -// Type for the messenger of GatorPermissionsController +/** + * Messenger type for the GatorPermissionsController. + */ export type GatorPermissionsControllerMessenger = RestrictedMessenger< typeof controllerName, - Actions | AllowedActions, - Events | AllowedEvents, + GatorPermissionsControllerActions | AllowedActions, + GatorPermissionsControllerActionsEvents | AllowedEvents, AllowedActions['type'], AllowedEvents['type'] >; +/** + * Configuration for the GatorPermissionsController. + * Default value is `{ gatorPermissionsProviderSnapId: '@metamask/gator-permissions-snap' }` + * when no config is provided. + */ +export type GatorPermissionsControllerConfig = { + /** + * The ID of the Snap of the gator permissions provider snap + */ + gatorPermissionsProviderSnapId: SnapId; +}; + /** * Controller that manages gator permissions by reading from profile sync */ @@ -159,16 +175,7 @@ export default class GatorPermissionsController extends BaseController< GatorPermissionsControllerState, GatorPermissionsControllerMessenger > { - readonly #auth = { - isSignedIn: () => { - return this.messagingSystem.call('AuthenticationController:isSignedIn'); - }, - signIn: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:performSignIn', - ); - }, - }; + private readonly gatorPermissionsProviderSnapId: SnapId; /** * Creates a GatorPermissionsController instance. @@ -176,13 +183,16 @@ export default class GatorPermissionsController extends BaseController< * @param args - The arguments to this function. * @param args.messenger - Messenger used to communicate with BaseV2 controller. * @param args.state - Initial state to set on this controller. + * @param args.config - Configuration for the GatorPermissionsController. */ constructor({ messenger, state, + config, }: { messenger: GatorPermissionsControllerMessenger; state?: Partial; + config?: GatorPermissionsControllerConfig; }) { super({ messenger, @@ -191,6 +201,10 @@ export default class GatorPermissionsController extends BaseController< state: { ...defaultState, ...state }, }); + this.gatorPermissionsProviderSnapId = + config?.gatorPermissionsProviderSnapId ?? + defaultGatorPermissionsProviderSnapId; + this.#registerMessageHandlers(); this.#clearLoadingStates(); } @@ -207,12 +221,6 @@ export default class GatorPermissionsController extends BaseController< }); } - #setIsUpdatingGatorPermissions(isUpdatingGatorPermissions: boolean) { - this.update((state) => { - state.isUpdatingGatorPermissions = isUpdatingGatorPermissions; - }); - } - #registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( `${controllerName}:fetchAndUpdateGatorPermissions`, @@ -232,7 +240,6 @@ export default class GatorPermissionsController extends BaseController< #clearLoadingStates(): void { this.update((state) => { - state.isUpdatingGatorPermissions = false; state.isFetchingGatorPermissions = false; }); } @@ -260,67 +267,104 @@ export default class GatorPermissionsController extends BaseController< } /** - * Parses permissions from profile sync data and categorizes them by type and chainId. + * Gets the gator permissions provider snap id that is used to fetch gator permissions. + * + * @returns The gator permissions provider snap id. + */ + get permissionsProviderSnapId(): SnapId { + return this.gatorPermissionsProviderSnapId; + } + + /** + * Forwards a Snap request to the SnapController. + * + * @param args - The request parameters. + * @param args.snapId - The ID of the Snap of the gator permissions provider snap. + * @returns A promise that resolves with the gator permissions. + */ + async #handleSnapRequestToGatorPermissionsProvider({ + snapId, + }: { + snapId: SnapId; + }): Promise[] | null> { + return this.messagingSystem.call('SnapController:handleRequest', { + snapId, + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + jsonrpc: '2.0', + method: + GatorPermissionsSnapRpcMethod.PermissionProviderGetGrantedPermissions, + }, + }) as Promise[] | null>; + } + + /** + * Categorizes stored gator permissions by type and chainId. * - * @param permissionsData - An JSON stringified array of permission strings from profile sync. + * @param storedGatorPermissions - An array of stored gator permissions. * @returns Parsed and categorized permissions list. * @throws {Error} If permission type is invalid. */ #categorizePermissionsDataByTypeAndChainId( - permissionsData: string[] | null, + storedGatorPermissions: + | StoredGatorPermission[] + | null, ): GatorPermissionsList { - if (!permissionsData) { + if (!storedGatorPermissions) { return defaultGatorPermissionsList; } - return permissionsData.reduce( - (gatorPermissionsList, permissionString) => { - const parsedPermission = JSON.parse( - permissionString, - ) as StoredGatorPermission; + return storedGatorPermissions.reduce( + (gatorPermissionsList, storedGatorPermission) => { + const { permissionResponse } = storedGatorPermission; + const permissionType = permissionResponse.permission.type; + const { chainId } = permissionResponse; - if (parsedPermission.permissionResponse.signer.type !== 'account') { + if (permissionResponse.signer.type !== 'account') { throw new Error( 'Invalid permission signer type. Only account signer is supported', ); } - const permissionType = - parsedPermission.permissionResponse.permission.type; - const { chainId } = parsedPermission.permissionResponse; - - if (permissionType === 'native-token-stream') { - if (!gatorPermissionsList['native-token-stream'][chainId]) { - gatorPermissionsList['native-token-stream'][chainId] = []; - } - gatorPermissionsList['native-token-stream'][chainId].push( - parsedPermission as StoredGatorPermission< - SignerParam, - NativeTokenStreamPermission - >, - ); - } else if (permissionType === 'native-token-periodic') { - if (!gatorPermissionsList['native-token-periodic'][chainId]) { - gatorPermissionsList['native-token-periodic'][chainId] = []; - } - gatorPermissionsList['native-token-periodic'][chainId].push( - parsedPermission as StoredGatorPermission< - SignerParam, - NativeTokenPeriodicPermission - >, - ); - } else if (permissionType === 'erc20-token-stream') { - if (!gatorPermissionsList['erc20-token-stream'][chainId]) { - gatorPermissionsList['erc20-token-stream'][chainId] = []; - } - gatorPermissionsList['erc20-token-stream'][chainId].push( - parsedPermission as StoredGatorPermission< - SignerParam, - Erc20TokenStreamPermission - >, - ); - } else { - throw new Error('Invalid permission type '); + switch (permissionType) { + case 'native-token-stream': + if (!gatorPermissionsList['native-token-stream'][chainId]) { + gatorPermissionsList['native-token-stream'][chainId] = []; + } + gatorPermissionsList['native-token-stream'][chainId].push( + storedGatorPermission as StoredGatorPermission< + SignerParam, + NativeTokenStreamPermission + >, + ); + break; + case 'native-token-periodic': + if (!gatorPermissionsList['native-token-periodic'][chainId]) { + gatorPermissionsList['native-token-periodic'][chainId] = []; + } + gatorPermissionsList['native-token-periodic'][chainId].push( + storedGatorPermission as StoredGatorPermission< + SignerParam, + NativeTokenPeriodicPermission + >, + ); + break; + case 'erc20-token-stream': + if (!gatorPermissionsList['erc20-token-stream'][chainId]) { + gatorPermissionsList['erc20-token-stream'][chainId] = []; + } + gatorPermissionsList['erc20-token-stream'][chainId].push( + storedGatorPermission as StoredGatorPermission< + SignerParam, + Erc20TokenStreamPermission + >, + ); + break; + default: + throw new Error( + `Unsupported permission type: ${permissionType as string}`, + ); } return gatorPermissionsList; @@ -333,18 +377,6 @@ export default class GatorPermissionsController extends BaseController< ); } - /** - * Enables authentication if not already enabled. - * - * @throws {Error} If there is an error during the process. - */ - async #enableAuth() { - const isSignedIn = this.#auth.isSignedIn(); - if (!isSignedIn) { - await this.#auth.signIn(); - } - } - /** * Enables gator permissions for the user. * This method ensures that the user is authenticated and enables the feature. @@ -352,16 +384,7 @@ export default class GatorPermissionsController extends BaseController< * @throws {Error} If there is an error during the process of enabling permissions. */ public async enableGatorPermissions() { - try { - this.#setIsUpdatingGatorPermissions(true); - await this.#enableAuth(); - this.#setIsGatorPermissionsEnabled(true); - } catch (e) { - console.error('Unable to enable gator permissions', e); - throw new Error('Unable to enable gator permissions'); - } finally { - this.#setIsUpdatingGatorPermissions(false); - } + this.#setIsGatorPermissionsEnabled(true); } /** @@ -371,7 +394,6 @@ export default class GatorPermissionsController extends BaseController< * @throws {Error} If there is an error during the process. */ public async disableGatorPermissions() { - // Clear permissions from state this.update((state) => { state.isGatorPermissionsEnabled = false; state.gatorPermissionsListStringify = serializeGatorPermissionsList( @@ -391,15 +413,12 @@ export default class GatorPermissionsController extends BaseController< try { this.#setIsFetchingGatorPermissions(true); this.#assertGatorPermissionsEnabled(); - await this.#enableAuth(); - // Fetch all permissions from profile sync - const permissionsData = await this.messagingSystem.call( - 'UserStorageController:performGetStorageAllFeatureEntries', - GATOR_PERMISSIONS_FEATURE_NAME, - ); + const permissionsData = + await this.#handleSnapRequestToGatorPermissionsProvider({ + snapId: this.gatorPermissionsProviderSnapId, + }); - // Categorize permissions by type and update state const gatorPermissionsList = this.#categorizePermissionsDataByTypeAndChainId(permissionsData); diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index eadfcb36a6..9504f62da8 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -10,9 +10,9 @@ export type { GatorPermissionsControllerFetchAndUpdateGatorPermissions, GatorPermissionsControllerEnableGatorPermissions, GatorPermissionsControllerDisableGatorPermissions, - Actions, + GatorPermissionsControllerActions, AllowedActions, - Events, + GatorPermissionsControllerActionsEvents, AllowedEvents, GatorPermissionsControllerStateChangeEvent, } from './GatorPermissionsController'; diff --git a/packages/gator-permissions-controller/tsconfig.build.json b/packages/gator-permissions-controller/tsconfig.build.json index ae8fdda2dd..e5fd7422b9 100644 --- a/packages/gator-permissions-controller/tsconfig.build.json +++ b/packages/gator-permissions-controller/tsconfig.build.json @@ -5,9 +5,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "references": [ - { "path": "../base-controller/tsconfig.build.json" }, - { "path": "../profile-sync-controller/tsconfig.build.json" } - ], + "references": [{ "path": "../base-controller/tsconfig.build.json" }], "include": ["../../types", "./src"] } diff --git a/packages/gator-permissions-controller/tsconfig.json b/packages/gator-permissions-controller/tsconfig.json index 9dbbdba829..34354c4b09 100644 --- a/packages/gator-permissions-controller/tsconfig.json +++ b/packages/gator-permissions-controller/tsconfig.json @@ -3,9 +3,6 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [ - { "path": "../base-controller" }, - { "path": "../profile-sync-controller" } - ], + "references": [{ "path": "../base-controller" }], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index 72acb19267..9012b1b246 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3579,7 +3579,9 @@ __metadata: "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.1" - "@metamask/profile-sync-controller": "npm:^20.0.0" + "@metamask/snaps-controllers": "npm:^14.0.1" + "@metamask/snaps-sdk": "npm:^9.0.0" + "@metamask/snaps-utils": "npm:^11.0.0" "@metamask/utils": "npm:^11.4.2" "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" @@ -3590,7 +3592,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/profile-sync-controller": ^20.0.0 + "@metamask/snaps-controllers": ^14.0.0 languageName: unknown linkType: soft From c6949ac9a57cde59f69b67eb8b44c1df71afe3f9 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:02:41 -0400 Subject: [PATCH 18/24] Use proper MetaMask logging --- .../src/GatorPermissionContoller.test.ts | 8 +++--- .../src/GatorPermissionsController.ts | 11 +++++--- .../src/logger.ts | 9 +++++++ .../src/utils.test.ts | 25 +++++++++++++++++++ .../gator-permissions-controller/src/utils.ts | 19 ++++++++++++-- 5 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 packages/gator-permissions-controller/src/logger.ts diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index 6bf6a5efc7..ef3de94f01 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -386,7 +386,7 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', + 'Gator permissions are not enabled', ); }); @@ -466,7 +466,7 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', + 'Unsupported permission type: invalid-type', ); }); @@ -500,7 +500,7 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', + 'Invalid permission signer type. Only account signer is supported', ); }); @@ -519,7 +519,7 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', + 'Storage error', ); expect(controller.state.isFetchingGatorPermissions).toBe(false); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 711806e386..f854b640a6 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -8,7 +8,9 @@ import { BaseController } from '@metamask/base-controller'; import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; +import { createModuleLogger } from '@metamask/utils'; +import { projectLogger } from './logger'; import type { GatorPermissionsList, NativeTokenStreamPermission, @@ -30,6 +32,9 @@ const controllerName = 'GatorPermissionsController'; const defaultGatorPermissionsProviderSnapId = '@metamask/gator-permissions-snap' as SnapId; +// Logger for the controller +const log = createModuleLogger(projectLogger, 'GatorPermissionsController'); + // Enum for the RPC methods of the gator permissions provider snap enum GatorPermissionsSnapRpcMethod { /** @@ -428,9 +433,9 @@ export default class GatorPermissionsController extends BaseController< }); return gatorPermissionsList; - } catch (err) { - console.error('Failed to fetch gator permissions', err); - throw new Error('Failed to fetch gator permissions'); + } catch (error) { + log('Failed to fetch gator permissions', error); + throw error; } finally { this.#setIsFetchingGatorPermissions(false); } diff --git a/packages/gator-permissions-controller/src/logger.ts b/packages/gator-permissions-controller/src/logger.ts new file mode 100644 index 0000000000..541ed1100c --- /dev/null +++ b/packages/gator-permissions-controller/src/logger.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ + +import { createProjectLogger, createModuleLogger } from '@metamask/utils'; + +export const projectLogger = createProjectLogger( + 'gator-permissions-controller', +); + +export { createModuleLogger }; diff --git a/packages/gator-permissions-controller/src/utils.test.ts b/packages/gator-permissions-controller/src/utils.test.ts index 2e273e22ad..dce550ec09 100644 --- a/packages/gator-permissions-controller/src/utils.test.ts +++ b/packages/gator-permissions-controller/src/utils.test.ts @@ -18,6 +18,23 @@ describe('utils - serializeGatorPermissionsList() tests', () => { JSON.stringify(gatorPermissionsList), ); }); + + it('throws an error when serialization fails', () => { + // Create a valid GatorPermissionsList structure but with circular reference + const gatorPermissionsList = { + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + }; + + // Add circular reference to cause JSON.stringify to fail + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (gatorPermissionsList as any).circular = gatorPermissionsList; + + expect(() => { + serializeGatorPermissionsList(gatorPermissionsList); + }).toThrow('Converting circular structure to JSON'); + }); }); describe('utils - deserializeGatorPermissionsList() tests', () => { @@ -39,4 +56,12 @@ describe('utils - deserializeGatorPermissionsList() tests', () => { gatorPermissionsList, ); }); + + it('throws an error when deserialization fails', () => { + const invalidJson = '{"invalid": json}'; + + expect(() => { + deserializeGatorPermissionsList(invalidJson); + }).toThrow('Unexpected token'); + }); }); diff --git a/packages/gator-permissions-controller/src/utils.ts b/packages/gator-permissions-controller/src/utils.ts index 01b86c46b0..072c361324 100644 --- a/packages/gator-permissions-controller/src/utils.ts +++ b/packages/gator-permissions-controller/src/utils.ts @@ -1,5 +1,10 @@ +import { createModuleLogger } from '@metamask/utils'; + +import { projectLogger } from './logger'; import type { GatorPermissionsList } from './types'; +const log = createModuleLogger(projectLogger, 'utils'); + /** * Serializes a gator permissions list to a string. * @@ -9,7 +14,12 @@ import type { GatorPermissionsList } from './types'; export function serializeGatorPermissionsList( gatorPermissionsList: GatorPermissionsList, ): string { - return JSON.stringify(gatorPermissionsList); + try { + return JSON.stringify(gatorPermissionsList); + } catch (error) { + log('Failed to serialize gator permissions list', error); + throw error; + } } /** @@ -21,5 +31,10 @@ export function serializeGatorPermissionsList( export function deserializeGatorPermissionsList( gatorPermissionsList: string, ): GatorPermissionsList { - return JSON.parse(gatorPermissionsList); + try { + return JSON.parse(gatorPermissionsList); + } catch (error) { + log('Failed to deserialize gator permissions list', error); + throw error; + } } From 746a07916b649d1673098240b31a303d2a247a0d Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:58:23 -0400 Subject: [PATCH 19/24] Change team name to delegation --- README.md | 2 +- teams.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f414f3a1e8..46dd381a98 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ linkStyle default opacity:0.5 eth_json_rpc_provider(["@metamask/eth-json-rpc-provider"]); foundryup(["@metamask/foundryup"]); gas_fee_controller(["@metamask/gas-fee-controller"]); - gator-permissions-controller(["@metamask/gator-permissions-controller"]); + gator_permissions_controller(["@metamask/gator-permissions-controller"]); json_rpc_engine(["@metamask/json-rpc-engine"]); json_rpc_middleware_stream(["@metamask/json-rpc-middleware-stream"]); keyring_controller(["@metamask/keyring-controller"]); diff --git a/teams.json b/teams.json index b0ea271ba0..d6eb889012 100644 --- a/teams.json +++ b/teams.json @@ -18,7 +18,7 @@ "metamask/ens-controller": "team-confirmations", "metamask/eth-json-rpc-provider": "team-wallet-api-platform,team-wallet-framework", "metamask/gas-fee-controller": "team-confirmations", - "metamask/gator-permissions-controller": "team-readable-permissions", + "metamask/gator-permissions-controller": "team-delegation", "metamask/json-rpc-engine": "team-wallet-api-platform,team-wallet-framework", "metamask/json-rpc-middleware-stream": "team-wallet-api-platform,team-wallet-framework", "metamask/keyring-controller": "team-accounts", From e87f1d6682f0b2c53c3be3a9f280df82c21d0561 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:46:14 -0400 Subject: [PATCH 20/24] Add custom error codes, handle custom permission types and add erc20-token-periodic permission type --- .../src/GatorPermissionContoller.test.ts | 387 +++++------------- .../src/GatorPermissionsController.ts | 261 ++++++------ .../src/errors.ts | 82 ++++ .../gator-permissions-controller/src/index.ts | 4 +- .../src/logger.ts | 7 + .../src/test/mock.test.ts | 371 +++++++++++++++++ .../src/test/mocks.ts | 285 +++++++++++++ .../gator-permissions-controller/src/types.ts | 132 ++++-- .../src/utils.test.ts | 67 +-- .../gator-permissions-controller/src/utils.ts | 51 +-- 10 files changed, 1161 insertions(+), 486 deletions(-) create mode 100644 packages/gator-permissions-controller/src/errors.ts create mode 100644 packages/gator-permissions-controller/src/test/mock.test.ts create mode 100644 packages/gator-permissions-controller/src/test/mocks.ts diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index ef3de94f01..90aac3b57a 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -8,195 +8,19 @@ import type { GatorPermissionsControllerConfig, } from './GatorPermissionsController'; import GatorPermissionsController from './GatorPermissionsController'; -import type { - AccountSigner, - Erc20TokenStreamPermission, - NativeTokenPeriodicPermission, - NativeTokenStreamPermission, - PermissionTypes, - StoredGatorPermission, -} from './types'; +import { + mockCustomPermissionStorageEntry, + mockErc20TokenPeriodicStorageEntry, + mockErc20TokenStreamStorageEntry, + mockGatorPermissionsStorageEntriesFactory, + mockNativeTokenPeriodicStorageEntry, + mockNativeTokenStreamStorageEntry, +} from './test/mocks'; +import type { GatorPermissionsMap } from './types'; const MOCK_CHAIN_ID_1: Hex = '0xaa36a7'; const MOCK_CHAIN_ID_2: Hex = '0x1'; -type MockGatorPermissionsStorageEntriesConfig = { - [chainId: string]: { - nativeTokenStream: number; - nativeTokenPeriodic: number; - erc20TokenStream: number; - }; -}; - -/** - * Creates a mock gator permissions storage entry - * - * @param amount - The amount of mock gator permissions storage entries to create. - * @param mockStorageEntry - The mock gator permissions storage entry to create. - * @returns Mock gator permissions storage entry - */ -function createMockGatorPermissionsStorageEntries( - amount: number, - mockStorageEntry: StoredGatorPermission, -): StoredGatorPermission[] { - return Array.from({ length: amount }, (_, index: number) => ({ - ...mockStorageEntry, - permissionResponse: { - ...mockStorageEntry.permissionResponse, - expiry: mockStorageEntry.permissionResponse.expiry + index, - }, - })); -} - -/** - * Creates a mock gator permissions storage entry - * - * @param config - The config for the mock gator permissions storage entries. - * @returns Mock gator permissions storage entry - */ -function mockGatorPermissionsStorageEntriesFactory( - config: MockGatorPermissionsStorageEntriesConfig, -): StoredGatorPermission[] { - const result: StoredGatorPermission[] = []; - - // Create entries for each chainId - Object.entries(config).forEach(([chainId, counts]) => { - const mockNativeTokenStreamStorageEntry: StoredGatorPermission< - AccountSigner, - NativeTokenStreamPermission - > = { - permissionResponse: { - chainId: chainId as Hex, - address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { - type: 'account', - data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, - }, - permission: { - type: 'native-token-stream', - data: { - maxAmount: '0x22b1c8c1227a0000', - initialAmount: '0x6f05b59d3b20000', - amountPerSecond: '0x6f05b59d3b20000', - startTime: 1747699200, - justification: - 'This is a very important request for streaming allowance for some very important thing', - }, - rules: {}, - }, - context: '0x00000000', - accountMeta: [ - { - factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', - factoryData: '0x0000000', - }, - ], - signerMeta: { - delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', - }, - }, - siteOrigin: 'http://localhost:8000', - }; - - const mockNativeTokenPeriodicStorageEntry: StoredGatorPermission< - AccountSigner, - NativeTokenPeriodicPermission - > = { - permissionResponse: { - chainId: chainId as Hex, - address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { - type: 'account', - data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, - }, - permission: { - type: 'native-token-periodic', - data: { - periodAmount: '0x22b1c8c1227a0000', - periodDuration: 1747699200, - startTime: 1747699200, - justification: - 'This is a very important request for streaming allowance for some very important thing', - }, - rules: {}, - }, - context: '0x00000000', - accountMeta: [ - { - factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', - factoryData: '0x0000000', - }, - ], - signerMeta: { - delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', - }, - }, - siteOrigin: 'http://localhost:8000', - }; - - const mockErc20TokenStreamStorageEntry: StoredGatorPermission< - AccountSigner, - Erc20TokenStreamPermission - > = { - permissionResponse: { - chainId: chainId as Hex, - address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { - type: 'account', - data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, - }, - permission: { - type: 'erc20-token-stream', - data: { - initialAmount: '0x22b1c8c1227a0000', - maxAmount: '0x6f05b59d3b20000', - amountPerSecond: '0x6f05b59d3b20000', - startTime: 1747699200, - tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', - justification: - 'This is a very important request for streaming allowance for some very important thing', - }, - rules: {}, - }, - context: '0x00000000', - accountMeta: [ - { - factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', - factoryData: '0x0000000', - }, - ], - signerMeta: { - delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', - }, - }, - siteOrigin: 'http://localhost:8000', - }; - - result.push( - ...createMockGatorPermissionsStorageEntries( - counts.nativeTokenStream, - mockNativeTokenStreamStorageEntry, - ), - ...createMockGatorPermissionsStorageEntries( - counts.nativeTokenPeriodic, - mockNativeTokenPeriodicStorageEntry, - ), - ...createMockGatorPermissionsStorageEntries( - counts.erc20TokenStream, - mockErc20TokenStreamStorageEntry, - ), - ); - }); - - return result; -} - /** * Jest Test Utility - create Gator Permissions Messenger * @@ -228,11 +52,35 @@ function createMockGatorPermissionsMessenger() { nativeTokenStream: 5, nativeTokenPeriodic: 5, erc20TokenStream: 5, + erc20TokenPeriodic: 5, + custom: { + count: 2, + data: [ + { + customData: 'customData-0', + }, + { + customData: 'customData-1', + }, + ], + }, }, [MOCK_CHAIN_ID_2]: { nativeTokenStream: 5, nativeTokenPeriodic: 5, erc20TokenStream: 5, + erc20TokenPeriodic: 5, + custom: { + count: 2, + data: [ + { + customData: 'customData-0', + }, + { + customData: 'customData-1', + }, + ], + }, }, }), ); @@ -272,11 +120,13 @@ describe('gator-permissions-controller - constructor() tests', () => { }); expect(controller.state.isGatorPermissionsEnabled).toBe(false); - expect(controller.state.gatorPermissionsListStringify).toStrictEqual( + expect(controller.state.gatorPermissionsMapSerialized).toStrictEqual( JSON.stringify({ 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }), ); expect(controller.state.isFetchingGatorPermissions).toBe(false); @@ -285,10 +135,12 @@ describe('gator-permissions-controller - constructor() tests', () => { it('creates GatorPermissionsController with custom state', () => { const customState = { isGatorPermissionsEnabled: true, - gatorPermissionsListStringify: JSON.stringify({ + gatorPermissionsMap: JSON.stringify({ 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }), }; @@ -302,8 +154,8 @@ describe('gator-permissions-controller - constructor() tests', () => { mockGatorPermissionsControllerConfig.gatorPermissionsProviderSnapId, ); expect(controller.state.isGatorPermissionsEnabled).toBe(true); - expect(controller.state.gatorPermissionsListStringify).toBe( - customState.gatorPermissionsListStringify, + expect(controller.state.gatorPermissionsMapSerialized).toBe( + customState.gatorPermissionsMap, ); }); @@ -329,19 +181,19 @@ describe('gator-permissions-controller - disableGatorPermissions() tests', () => config: mockGatorPermissionsControllerConfig, }); - // Enable first await controller.enableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(true); - // Then disable await controller.disableGatorPermissions(); expect(controller.state.isGatorPermissionsEnabled).toBe(false); - expect(controller.state.gatorPermissionsListStringify).toBe( + expect(controller.state.gatorPermissionsMapSerialized).toBe( JSON.stringify({ 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }), ); }); @@ -356,7 +208,6 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' config: mockGatorPermissionsControllerConfig, }); - // Enable first await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -365,6 +216,8 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' 'native-token-stream': expect.any(Object), 'native-token-periodic': expect.any(Object), 'erc20-token-stream': expect.any(Object), + 'erc20-token-periodic': expect.any(Object), + other: expect.any(Object), }); // Check that each permission type has the expected chainId @@ -374,7 +227,29 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' expect(result['native-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); expect(result['native-token-periodic'][MOCK_CHAIN_ID_2]).toHaveLength(5); expect(result['erc20-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); + expect(result.other[MOCK_CHAIN_ID_1]).toHaveLength(2); + expect(result.other[MOCK_CHAIN_ID_2]).toHaveLength(2); expect(controller.state.isFetchingGatorPermissions).toBe(false); + + // check that the gator permissions map is sanitized + const sanitizedCheck = (permissionType: keyof GatorPermissionsMap) => { + const flattenedStoredGatorPermissions = Object.values( + result[permissionType], + ).flat(); + flattenedStoredGatorPermissions.forEach((permission) => { + expect( + permission.permissionResponse.isAdjustmentAllowed, + ).toBeUndefined(); + expect(permission.permissionResponse.accountMeta).toBeUndefined(); + expect(permission.permissionResponse.signer).toBeUndefined(); + }); + }; + + sanitizedCheck('native-token-stream'); + sanitizedCheck('native-token-periodic'); + sanitizedCheck('erc20-token-stream'); + sanitizedCheck('erc20-token-periodic'); + sanitizedCheck('other'); }); it('throws error when gator permissions are not enabled', async () => { @@ -386,7 +261,7 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' }); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Gator permissions are not enabled', + 'Failed to fetch gator permissions', ); }); @@ -401,7 +276,6 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' config: mockGatorPermissionsControllerConfig, }); - // Enable first await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -410,6 +284,8 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }); }); @@ -424,7 +300,6 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' config: mockGatorPermissionsControllerConfig, }); - // Enable first await controller.enableGatorPermissions(); const result = await controller.fetchAndUpdateGatorPermissions(); @@ -433,77 +308,11 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }); }); - it('throws error for invalid permission type', async () => { - const { messenger, mockGetGrantedPermissions } = - createMockGatorPermissionsMessenger(); - - mockGetGrantedPermissions.mockResolvedValue([ - { - permissionResponse: { - chainId: '0x1' as Hex, - address: '0x123', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { type: 'account', data: { address: '0x123' } }, - permission: { type: 'invalid-type' }, - context: '0x00000000', - accountMeta: [], - signerMeta: {}, - }, - siteOrigin: 'http://localhost:8000', - }, - ]); - - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); - - // Enable first - await controller.enableGatorPermissions(); - - await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Unsupported permission type: invalid-type', - ); - }); - - it('throws error for non-account signer type', async () => { - const { messenger, mockGetGrantedPermissions } = - createMockGatorPermissionsMessenger(); - - mockGetGrantedPermissions.mockResolvedValue([ - { - permissionResponse: { - chainId: '0x1' as Hex, - address: '0x123', - expiry: 1750291200, - isAdjustmentAllowed: true, - signer: { type: 'wallet', data: {} }, - permission: { type: 'native-token-stream' }, - context: '0x00000000', - accountMeta: [], - signerMeta: {}, - }, - siteOrigin: 'http://localhost:8000', - }, - ]); - - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); - - // Enable first - await controller.enableGatorPermissions(); - - await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Invalid permission signer type. Only account signer is supported', - ); - }); - it('handles error during fetch and update', async () => { const { messenger, mockGetGrantedPermissions } = createMockGatorPermissionsMessenger(); @@ -515,19 +324,18 @@ describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests' config: mockGatorPermissionsControllerConfig, }); - // Enable first await controller.enableGatorPermissions(); await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Storage error', + 'Failed to fetch gator permissions', ); expect(controller.state.isFetchingGatorPermissions).toBe(false); }); }); -describe('gator-permissions-controller - gatorPermissionsList getter tests', () => { - it('returns parsed gator permissions list', () => { +describe('gator-permissions-controller - gatorPermissionsMap getter tests', () => { + it('returns parsed gator permissions map', () => { const { messenger } = createMockGatorPermissionsMessenger(); const controller = new GatorPermissionsController({ @@ -535,37 +343,52 @@ describe('gator-permissions-controller - gatorPermissionsList getter tests', () config: mockGatorPermissionsControllerConfig, }); - const permissionsList = controller.gatorPermissionsList; + const { gatorPermissionsMap } = controller; - expect(permissionsList).toStrictEqual({ + expect(gatorPermissionsMap).toStrictEqual({ 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }); }); - it('returns parsed gator permissions list with data', () => { + it('returns parsed gator permissions map with data when state is provided', () => { const { messenger } = createMockGatorPermissionsMessenger(); + const mockState = { + 'native-token-stream': { + '0x1': [mockNativeTokenStreamStorageEntry('0x1')], + }, + 'native-token-periodic': { + '0x2': [mockNativeTokenPeriodicStorageEntry('0x2')], + }, + 'erc20-token-stream': { + '0x3': [mockErc20TokenStreamStorageEntry('0x3')], + }, + 'erc20-token-periodic': { + '0x4': [mockErc20TokenPeriodicStorageEntry('0x4')], + }, + other: { + '0x5': [ + mockCustomPermissionStorageEntry('0x5', { + customData: 'customData-0', + }), + ], + }, + }; const controller = new GatorPermissionsController({ messenger, config: mockGatorPermissionsControllerConfig, state: { - gatorPermissionsListStringify: JSON.stringify({ - 'native-token-stream': { '0x1': [{ id: '1' }] }, - 'native-token-periodic': { '0x2': [{ id: '2' }] }, - 'erc20-token-stream': { '0x3': [{ id: '3' }] }, - }), + gatorPermissionsMapSerialized: JSON.stringify(mockState), }, }); - const permissionsList = controller.gatorPermissionsList; + const { gatorPermissionsMap } = controller; - expect(permissionsList).toStrictEqual({ - 'native-token-stream': { '0x1': [{ id: '1' }] }, - 'native-token-periodic': { '0x2': [{ id: '2' }] }, - 'erc20-token-stream': { '0x3': [{ id: '3' }] }, - }); + expect(gatorPermissionsMap).toStrictEqual(mockState); }); }); diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index f854b640a6..8bc0259d0f 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -8,21 +8,24 @@ import { BaseController } from '@metamask/base-controller'; import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; -import { createModuleLogger } from '@metamask/utils'; -import { projectLogger } from './logger'; -import type { - GatorPermissionsList, - NativeTokenStreamPermission, - NativeTokenPeriodicPermission, - Erc20TokenStreamPermission, - PermissionTypes, - SignerParam, - StoredGatorPermission, +import { + GatorPermissionsFetchError, + GatorPermissionsNotEnabledError, + GatorPermissionsProviderError, +} from './errors'; +import { controllerLog } from './logger'; +import type { StoredGatorPermissionSanitized } from './types'; +import { + GatorPermissionsSnapRpcMethod, + type GatorPermissionsMap, + type PermissionTypes, + type SignerParam, + type StoredGatorPermission, } from './types'; import { - deserializeGatorPermissionsList, - serializeGatorPermissionsList, + deserializeGatorPermissionsMap, + serializeGatorPermissionsMap, } from './utils'; // Unique name for the controller @@ -32,17 +35,6 @@ const controllerName = 'GatorPermissionsController'; const defaultGatorPermissionsProviderSnapId = '@metamask/gator-permissions-snap' as SnapId; -// Logger for the controller -const log = createModuleLogger(projectLogger, 'GatorPermissionsController'); - -// Enum for the RPC methods of the gator permissions provider snap -enum GatorPermissionsSnapRpcMethod { - /** - * This method is used by the metamask to request a permissions provider to get granted permissions for all sites. - */ - PermissionProviderGetGrantedPermissions = 'permissionsProvider_getGrantedPermissions', -} - /** * State shape for GatorPermissionsController */ @@ -53,9 +45,9 @@ export type GatorPermissionsControllerState = { isGatorPermissionsEnabled: boolean; /** - * JSON serialized object containing gator permissions fetched from profile sync indexed by permission type + * JSON serialized object containing gator permissions fetched from profile sync */ - gatorPermissionsListStringify: string; + gatorPermissionsMapSerialized: string; /** * Flag that indicates that fetching permissions is in progress @@ -69,9 +61,9 @@ const metadata: StateMetadata = { persist: true, anonymous: false, }, - gatorPermissionsListStringify: { + gatorPermissionsMapSerialized: { persist: true, - anonymous: true, + anonymous: false, }, isFetchingGatorPermissions: { persist: false, @@ -79,16 +71,18 @@ const metadata: StateMetadata = { }, }; -const defaultGatorPermissionsList: GatorPermissionsList = { +const defaultGatorPermissionsMap: GatorPermissionsMap = { 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }; export const defaultState: GatorPermissionsControllerState = { isGatorPermissionsEnabled: false, - gatorPermissionsListStringify: serializeGatorPermissionsList( - defaultGatorPermissionsList, + gatorPermissionsMapSerialized: serializeGatorPermissionsMap( + defaultGatorPermissionsMap, ), isFetchingGatorPermissions: false, }; @@ -252,34 +246,14 @@ export default class GatorPermissionsController extends BaseController< /** * Asserts that the gator permissions are enabled. * - * @throws {Error} If the gator permissions are not enabled. + * @throws {GatorPermissionsNotEnabledError} If the gator permissions are not enabled. */ #assertGatorPermissionsEnabled() { if (!this.state.isGatorPermissionsEnabled) { - throw new Error('Gator permissions are not enabled'); + throw new GatorPermissionsNotEnabledError(); } } - /** - * Gets the categorized gator permissions list from the state. - * - * @returns The categorized gator permissions list. - */ - get gatorPermissionsList(): GatorPermissionsList { - return deserializeGatorPermissionsList( - this.state.gatorPermissionsListStringify, - ); - } - - /** - * Gets the gator permissions provider snap id that is used to fetch gator permissions. - * - * @returns The gator permissions provider snap id. - */ - get permissionsProviderSnapId(): SnapId { - return this.gatorPermissionsProviderSnapId; - } - /** * Forwards a Snap request to the SnapController. * @@ -292,129 +266,173 @@ export default class GatorPermissionsController extends BaseController< }: { snapId: SnapId; }): Promise[] | null> { - return this.messagingSystem.call('SnapController:handleRequest', { - snapId, - origin: 'metamask', - handler: HandlerType.OnRpcRequest, - request: { - jsonrpc: '2.0', + try { + const response = (await this.messagingSystem.call( + 'SnapController:handleRequest', + { + snapId, + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + jsonrpc: '2.0', + method: + GatorPermissionsSnapRpcMethod.PermissionProviderGetGrantedPermissions, + }, + }, + )) as Promise< + StoredGatorPermission[] | null + >; + + return response; + } catch (error) { + controllerLog( + 'Failed to handle snap request to gator permissions provider', + error, + ); + throw new GatorPermissionsProviderError({ method: GatorPermissionsSnapRpcMethod.PermissionProviderGetGrantedPermissions, - }, - }) as Promise[] | null>; + cause: error as Error, + }); + } + } + + /** + * Sanitizes a stored gator permission by removing the fields that are not expose to MetaMask client. + * + * @param storedGatorPermission - The stored gator permission to sanitize. + * @returns The sanitized stored gator permission. + */ + #sanitizeStoredGatorPermission( + storedGatorPermission: StoredGatorPermission, + ): StoredGatorPermissionSanitized { + const { permissionResponse } = storedGatorPermission; + const { isAdjustmentAllowed, accountMeta, signer, ...rest } = + permissionResponse; + return { + ...storedGatorPermission, + permissionResponse: { ...rest }, + }; } /** * Categorizes stored gator permissions by type and chainId. * * @param storedGatorPermissions - An array of stored gator permissions. - * @returns Parsed and categorized permissions list. + * @returns The gator permissions map. + * @throws {SignerTypeNotSupportedError} If signer type is not account. * @throws {Error} If permission type is invalid. */ #categorizePermissionsDataByTypeAndChainId( storedGatorPermissions: | StoredGatorPermission[] | null, - ): GatorPermissionsList { + ): GatorPermissionsMap { if (!storedGatorPermissions) { - return defaultGatorPermissionsList; + return defaultGatorPermissionsMap; } return storedGatorPermissions.reduce( - (gatorPermissionsList, storedGatorPermission) => { + (gatorPermissionsMap, storedGatorPermission) => { const { permissionResponse } = storedGatorPermission; const permissionType = permissionResponse.permission.type; const { chainId } = permissionResponse; - if (permissionResponse.signer.type !== 'account') { - throw new Error( - 'Invalid permission signer type. Only account signer is supported', - ); - } + const sanitizedStoredGatorPermission = + this.#sanitizeStoredGatorPermission(storedGatorPermission); switch (permissionType) { case 'native-token-stream': - if (!gatorPermissionsList['native-token-stream'][chainId]) { - gatorPermissionsList['native-token-stream'][chainId] = []; - } - gatorPermissionsList['native-token-stream'][chainId].push( - storedGatorPermission as StoredGatorPermission< - SignerParam, - NativeTokenStreamPermission - >, - ); - break; case 'native-token-periodic': - if (!gatorPermissionsList['native-token-periodic'][chainId]) { - gatorPermissionsList['native-token-periodic'][chainId] = []; + case 'erc20-token-stream': + case 'erc20-token-periodic': + if (!gatorPermissionsMap[permissionType][chainId]) { + gatorPermissionsMap[permissionType][chainId] = []; } - gatorPermissionsList['native-token-periodic'][chainId].push( - storedGatorPermission as StoredGatorPermission< + + ( + gatorPermissionsMap[permissionType][ + chainId + ] as StoredGatorPermissionSanitized< SignerParam, - NativeTokenPeriodicPermission - >, - ); + PermissionTypes + >[] + ).push(sanitizedStoredGatorPermission); break; - case 'erc20-token-stream': - if (!gatorPermissionsList['erc20-token-stream'][chainId]) { - gatorPermissionsList['erc20-token-stream'][chainId] = []; + default: + if (!gatorPermissionsMap.other[chainId]) { + gatorPermissionsMap.other[chainId] = []; } - gatorPermissionsList['erc20-token-stream'][chainId].push( - storedGatorPermission as StoredGatorPermission< + + ( + gatorPermissionsMap.other[ + chainId + ] as StoredGatorPermissionSanitized< SignerParam, - Erc20TokenStreamPermission - >, - ); + PermissionTypes + >[] + ).push(sanitizedStoredGatorPermission); break; - default: - throw new Error( - `Unsupported permission type: ${permissionType as string}`, - ); } - return gatorPermissionsList; + return gatorPermissionsMap; }, { 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, - } as GatorPermissionsList, + 'erc20-token-periodic': {}, + other: {}, + } as GatorPermissionsMap, ); } /** - * Enables gator permissions for the user. - * This method ensures that the user is authenticated and enables the feature. + * Gets the gator permissions map from the state. * - * @throws {Error} If there is an error during the process of enabling permissions. + * @returns The gator permissions map. + */ + get gatorPermissionsMap(): GatorPermissionsMap { + return deserializeGatorPermissionsMap( + this.state.gatorPermissionsMapSerialized, + ); + } + + /** + * Gets the gator permissions provider snap id that is used to fetch gator permissions. + * + * @returns The gator permissions provider snap id. + */ + get permissionsProviderSnapId(): SnapId { + return this.gatorPermissionsProviderSnapId; + } + + /** + * Enables gator permissions for the user. */ public async enableGatorPermissions() { this.#setIsGatorPermissionsEnabled(true); } /** - * Disables gator permissions for the user. - * This method clears the permissions list and disables the feature. - * - * @throws {Error} If there is an error during the process. + * Clears the gator permissions map and disables the feature. */ public async disableGatorPermissions() { this.update((state) => { state.isGatorPermissionsEnabled = false; - state.gatorPermissionsListStringify = serializeGatorPermissionsList( - defaultGatorPermissionsList, + state.gatorPermissionsMapSerialized = serializeGatorPermissionsMap( + defaultGatorPermissionsMap, ); }); } /** - * Fetches the list of gator permissions from profile sync and updates the state. - * This is the main method that reads data from profile sync and caches it. + * Fetches the gator permissions from profile sync and updates the state. * - * @returns A promise that resolves to the list of permissions. - * @throws {Error} Throws an error if unauthenticated or from other operations. + * @returns A promise that resolves to the gator permissions map. + * @throws {GatorPermissionsFetchError} If the gator permissions fetch fails. */ - public async fetchAndUpdateGatorPermissions(): Promise { + public async fetchAndUpdateGatorPermissions(): Promise { try { this.#setIsFetchingGatorPermissions(true); this.#assertGatorPermissionsEnabled(); @@ -424,18 +442,21 @@ export default class GatorPermissionsController extends BaseController< snapId: this.gatorPermissionsProviderSnapId, }); - const gatorPermissionsList = + const gatorPermissionsMap = this.#categorizePermissionsDataByTypeAndChainId(permissionsData); this.update((state) => { - state.gatorPermissionsListStringify = - serializeGatorPermissionsList(gatorPermissionsList); + state.gatorPermissionsMapSerialized = + serializeGatorPermissionsMap(gatorPermissionsMap); }); - return gatorPermissionsList; + return gatorPermissionsMap; } catch (error) { - log('Failed to fetch gator permissions', error); - throw error; + controllerLog('Failed to fetch gator permissions', error); + throw new GatorPermissionsFetchError({ + message: 'Failed to fetch gator permissions', + cause: error as Error, + }); } finally { this.#setIsFetchingGatorPermissions(false); } diff --git a/packages/gator-permissions-controller/src/errors.ts b/packages/gator-permissions-controller/src/errors.ts new file mode 100644 index 0000000000..2deff1c4db --- /dev/null +++ b/packages/gator-permissions-controller/src/errors.ts @@ -0,0 +1,82 @@ +import type { GatorPermissionsSnapRpcMethod } from './types'; +import { GatorPermissionsControllerErrorCode } from './types'; + +/** + * Represents a base gator permissions error. + */ +type GatorPermissionsErrorParams = { + code: GatorPermissionsControllerErrorCode; + cause: Error; + message: string; +}; + +export class GatorPermissionsControllerError extends Error { + code: GatorPermissionsControllerErrorCode; + + cause: Error; + + constructor({ cause, message, code }: GatorPermissionsErrorParams) { + super(message); + + this.cause = cause; + this.code = code; + } +} + +export class GatorPermissionsFetchError extends GatorPermissionsControllerError { + constructor({ cause, message }: { cause: Error; message: string }) { + super({ + cause, + message, + code: GatorPermissionsControllerErrorCode.GatorPermissionsFetchError, + }); + } +} + +export class GatorPermissionsMapSerializationError extends GatorPermissionsControllerError { + data: unknown; + + constructor({ + cause, + message, + data, + }: { + cause: Error; + message: string; + data?: unknown; + }) { + super({ + cause, + message, + code: GatorPermissionsControllerErrorCode.GatorPermissionsMapSerializationError, + }); + + this.data = data; + } +} + +export class GatorPermissionsNotEnabledError extends GatorPermissionsControllerError { + constructor() { + super({ + cause: new Error('Gator permissions are not enabled'), + message: 'Gator permissions are not enabled', + code: GatorPermissionsControllerErrorCode.GatorPermissionsNotEnabled, + }); + } +} + +export class GatorPermissionsProviderError extends GatorPermissionsControllerError { + constructor({ + cause, + method, + }: { + cause: Error; + method: GatorPermissionsSnapRpcMethod; + }) { + super({ + cause, + message: `Failed to handle snap request to gator permissions provider for method ${method}`, + code: GatorPermissionsControllerErrorCode.GatorPermissionsProviderError, + }); + } +} diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 9504f62da8..dc8c0db07a 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,7 +1,7 @@ export { default as GatorPermissionsController } from './GatorPermissionsController'; export { - serializeGatorPermissionsList, - deserializeGatorPermissionsList, + serializeGatorPermissionsMap, + deserializeGatorPermissionsMap, } from './utils'; export type { GatorPermissionsControllerState, diff --git a/packages/gator-permissions-controller/src/logger.ts b/packages/gator-permissions-controller/src/logger.ts index 541ed1100c..03445d678e 100644 --- a/packages/gator-permissions-controller/src/logger.ts +++ b/packages/gator-permissions-controller/src/logger.ts @@ -6,4 +6,11 @@ export const projectLogger = createProjectLogger( 'gator-permissions-controller', ); +export const controllerLog = createModuleLogger( + projectLogger, + 'GatorPermissionsController', +); + +export const utilsLog = createModuleLogger(projectLogger, 'utils'); + export { createModuleLogger }; diff --git a/packages/gator-permissions-controller/src/test/mock.test.ts b/packages/gator-permissions-controller/src/test/mock.test.ts new file mode 100644 index 0000000000..eb0ff3ca6b --- /dev/null +++ b/packages/gator-permissions-controller/src/test/mock.test.ts @@ -0,0 +1,371 @@ +import { + mockGatorPermissionsStorageEntriesFactory, + type MockGatorPermissionsStorageEntriesConfig, +} from './mocks'; + +describe('mockGatorPermissionsStorageEntriesFactory', () => { + it('should create mock storage entries for all permission types', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 2, + nativeTokenPeriodic: 1, + erc20TokenStream: 3, + erc20TokenPeriodic: 1, + custom: { + count: 2, + data: [ + { customField1: 'value1', customField2: 123 }, + { customField3: 'value3', customField4: true }, + ], + }, + }, + '0x5': { + nativeTokenStream: 1, + nativeTokenPeriodic: 2, + erc20TokenStream: 1, + erc20TokenPeriodic: 2, + custom: { + count: 1, + data: [{ customField5: 'value5' }], + }, + }, + }; + + const result = mockGatorPermissionsStorageEntriesFactory(config); + + expect(result).toHaveLength(16); + + // Check that entries have different expiry times + const expiryTimes = result.map((entry) => entry.permissionResponse.expiry); + const uniqueExpiryTimes = new Set(expiryTimes); + expect(uniqueExpiryTimes.size).toBe(16); + + // Check that all entries have the correct chainId + const chainIds = result.map((entry) => entry.permissionResponse.chainId); + expect(chainIds).toContain('0x1'); + expect(chainIds).toContain('0x5'); + }); + + it('should create entries with correct permission types', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 1, + nativeTokenPeriodic: 1, + erc20TokenStream: 1, + erc20TokenPeriodic: 1, + custom: { + count: 1, + data: [{ testField: 'testValue' }], + }, + }, + }; + + const result = mockGatorPermissionsStorageEntriesFactory(config); + + expect(result).toHaveLength(5); + + // Check native-token-stream permission + const nativeTokenStreamEntry = result.find( + (entry) => + entry.permissionResponse.permission.type === 'native-token-stream', + ); + expect(nativeTokenStreamEntry).toBeDefined(); + expect( + nativeTokenStreamEntry?.permissionResponse.permission.data, + ).toMatchObject({ + maxAmount: '0x22b1c8c1227a0000', + initialAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }); + + // Check native-token-periodic permission + const nativeTokenPeriodicEntry = result.find( + (entry) => + entry.permissionResponse.permission.type === 'native-token-periodic', + ); + expect(nativeTokenPeriodicEntry).toBeDefined(); + expect( + nativeTokenPeriodicEntry?.permissionResponse.permission.data, + ).toMatchObject({ + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }); + + // Check erc20-token-stream permission + const erc20TokenStreamEntry = result.find( + (entry) => + entry.permissionResponse.permission.type === 'erc20-token-stream', + ); + expect(erc20TokenStreamEntry).toBeDefined(); + expect( + erc20TokenStreamEntry?.permissionResponse.permission.data, + ).toMatchObject({ + initialAmount: '0x22b1c8c1227a0000', + maxAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }); + + // Check erc20-token-periodic permission + const erc20TokenPeriodicEntry = result.find( + (entry) => + entry.permissionResponse.permission.type === 'erc20-token-periodic', + ); + expect(erc20TokenPeriodicEntry).toBeDefined(); + expect( + erc20TokenPeriodicEntry?.permissionResponse.permission.data, + ).toMatchObject({ + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }); + + // Check custom permission + const customEntry = result.find( + (entry) => entry.permissionResponse.permission.type === 'custom', + ); + expect(customEntry).toBeDefined(); + expect(customEntry?.permissionResponse.permission.data).toMatchObject({ + justification: + 'This is a very important request for streaming allowance for some very important thing', + testField: 'testValue', + }); + }); + + it('should handle empty counts for all permission types', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 0, + nativeTokenPeriodic: 0, + erc20TokenStream: 0, + erc20TokenPeriodic: 0, + custom: { + count: 0, + data: [], + }, + }, + }; + + const result = mockGatorPermissionsStorageEntriesFactory(config); + + expect(result).toHaveLength(0); + }); + + it('should handle multiple chain IDs', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 1, + nativeTokenPeriodic: 0, + erc20TokenStream: 0, + erc20TokenPeriodic: 0, + custom: { + count: 0, + data: [], + }, + }, + '0x5': { + nativeTokenStream: 0, + nativeTokenPeriodic: 1, + erc20TokenStream: 0, + erc20TokenPeriodic: 0, + custom: { + count: 0, + data: [], + }, + }, + '0xa': { + nativeTokenStream: 0, + nativeTokenPeriodic: 0, + erc20TokenStream: 1, + erc20TokenPeriodic: 0, + custom: { + count: 0, + data: [], + }, + }, + }; + + const result = mockGatorPermissionsStorageEntriesFactory(config); + + expect(result).toHaveLength(3); + + // Check that each chain ID is represented + const chainIds = result.map((entry) => entry.permissionResponse.chainId); + expect(chainIds).toContain('0x1'); + expect(chainIds).toContain('0x5'); + expect(chainIds).toContain('0xa'); + + // Check that each entry has the correct permission type for its chain + const chain0x1Entry = result.find( + (entry) => entry.permissionResponse.chainId === '0x1', + ); + expect(chain0x1Entry?.permissionResponse.permission.type).toBe( + 'native-token-stream', + ); + + const chain0x5Entry = result.find( + (entry) => entry.permissionResponse.chainId === '0x5', + ); + expect(chain0x5Entry?.permissionResponse.permission.type).toBe( + 'native-token-periodic', + ); + + const chain0xaEntry = result.find( + (entry) => entry.permissionResponse.chainId === '0xa', + ); + expect(chain0xaEntry?.permissionResponse.permission.type).toBe( + 'erc20-token-stream', + ); + }); + + it('should handle custom permissions with different data', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 0, + nativeTokenPeriodic: 0, + erc20TokenStream: 0, + erc20TokenPeriodic: 0, + custom: { + count: 3, + data: [ + { field1: 'value1', number1: 123 }, + { field2: 'value2', boolean1: true }, + { field3: 'value3', object1: { nested: 'value' } }, + ], + }, + }, + }; + + const result = mockGatorPermissionsStorageEntriesFactory(config); + + expect(result).toHaveLength(3); + + // Check that all entries are custom permissions + const permissionTypes = result.map( + (entry) => entry.permissionResponse.permission.type, + ); + expect(permissionTypes.every((type) => type === 'custom')).toBe(true); + + // Check that each entry has the correct custom data + const customData = result.map( + (entry) => entry.permissionResponse.permission.data, + ); + expect(customData[0]).toMatchObject({ + justification: + 'This is a very important request for streaming allowance for some very important thing', + field1: 'value1', + number1: 123, + }); + expect(customData[1]).toMatchObject({ + justification: + 'This is a very important request for streaming allowance for some very important thing', + field2: 'value2', + boolean1: true, + }); + expect(customData[2]).toMatchObject({ + justification: + 'This is a very important request for streaming allowance for some very important thing', + field3: 'value3', + object1: { nested: 'value' }, + }); + }); + + it('should throw error when custom count and data length mismatch', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 0, + nativeTokenPeriodic: 0, + erc20TokenStream: 0, + erc20TokenPeriodic: 0, + custom: { + count: 2, + data: [{ field1: 'value1' }], + }, + }, + }; + + expect(() => mockGatorPermissionsStorageEntriesFactory(config)).toThrow( + 'Custom permission count and data length mismatch', + ); + }); + + it('should handle complex configuration with multiple chain IDs and permission types', () => { + const config: MockGatorPermissionsStorageEntriesConfig = { + '0x1': { + nativeTokenStream: 2, + nativeTokenPeriodic: 1, + erc20TokenStream: 1, + erc20TokenPeriodic: 2, + custom: { + count: 1, + data: [{ complexField: { nested: { deep: 'value' } } }], + }, + }, + '0x5': { + nativeTokenStream: 1, + nativeTokenPeriodic: 3, + erc20TokenStream: 2, + erc20TokenPeriodic: 1, + custom: { + count: 2, + data: [{ arrayField: [1, 2, 3] }, { nullField: null }], + }, + }, + }; + + const result = mockGatorPermissionsStorageEntriesFactory(config); + + // Total expected entries + expect(result).toHaveLength(16); + + // Verify all entries have unique expiry times + const expiryTimes = result.map((entry) => entry.permissionResponse.expiry); + const uniqueExpiryTimes = new Set(expiryTimes); + expect(uniqueExpiryTimes.size).toBe(16); + + // Verify chain IDs are correct + const chainIds = result.map((entry) => entry.permissionResponse.chainId); + const chain0x1Count = chainIds.filter((id) => id === '0x1').length; + const chain0x5Count = chainIds.filter((id) => id === '0x5').length; + expect(chain0x1Count).toBe(7); + expect(chain0x5Count).toBe(9); + + // Verify permission types are distributed correctly + const permissionTypes = result.map( + (entry) => entry.permissionResponse.permission.type, + ); + const nativeTokenStreamCount = permissionTypes.filter( + (type) => type === 'native-token-stream', + ).length; + const nativeTokenPeriodicCount = permissionTypes.filter( + (type) => type === 'native-token-periodic', + ).length; + const erc20TokenStreamCount = permissionTypes.filter( + (type) => type === 'erc20-token-stream', + ).length; + const erc20TokenPeriodicCount = permissionTypes.filter( + (type) => type === 'erc20-token-periodic', + ).length; + const customCount = permissionTypes.filter( + (type) => type === 'custom', + ).length; + + expect(nativeTokenStreamCount).toBe(3); + expect(nativeTokenPeriodicCount).toBe(4); + expect(erc20TokenStreamCount).toBe(3); + expect(erc20TokenPeriodicCount).toBe(3); + expect(customCount).toBe(3); + }); +}); diff --git a/packages/gator-permissions-controller/src/test/mocks.ts b/packages/gator-permissions-controller/src/test/mocks.ts new file mode 100644 index 0000000000..e64a7be832 --- /dev/null +++ b/packages/gator-permissions-controller/src/test/mocks.ts @@ -0,0 +1,285 @@ +import type { Hex } from '@metamask/utils'; + +import type { + AccountSigner, + CustomPermission, + Erc20TokenPeriodicPermission, + Erc20TokenStreamPermission, + NativeTokenPeriodicPermission, + NativeTokenStreamPermission, + PermissionTypes, + StoredGatorPermission, +} from '../types'; + +export const mockNativeTokenStreamStorageEntry = ( + chainId: Hex, +): StoredGatorPermission => ({ + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291201, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'native-token-stream', + data: { + maxAmount: '0x22b1c8c1227a0000', + initialAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', +}); + +export const mockNativeTokenPeriodicStorageEntry = ( + chainId: Hex, +): StoredGatorPermission => ({ + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1850291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'native-token-periodic', + data: { + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', +}); + +export const mockErc20TokenStreamStorageEntry = ( + chainId: Hex, +): StoredGatorPermission => ({ + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750298200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'erc20-token-stream', + data: { + initialAmount: '0x22b1c8c1227a0000', + maxAmount: '0x6f05b59d3b20000', + amountPerSecond: '0x6f05b59d3b20000', + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', +}); + +export const mockErc20TokenPeriodicStorageEntry = ( + chainId: Hex, +): StoredGatorPermission => ({ + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291600, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'erc20-token-periodic', + data: { + periodAmount: '0x22b1c8c1227a0000', + periodDuration: 1747699200, + startTime: 1747699200, + tokenAddress: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + justification: + 'This is a very important request for streaming allowance for some very important thing', + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', +}); + +export const mockCustomPermissionStorageEntry = ( + chainId: Hex, + data: Record, +): StoredGatorPermission => ({ + permissionResponse: { + chainId: chainId as Hex, + address: '0xB68c70159E9892DdF5659ec42ff9BD2bbC23e778', + expiry: 1750291200, + isAdjustmentAllowed: true, + signer: { + type: 'account', + data: { address: '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63' }, + }, + permission: { + type: 'custom', + data: { + justification: + 'This is a very important request for streaming allowance for some very important thing', + ...data, + }, + rules: {}, + }, + context: '0x00000000', + accountMeta: [ + { + factory: '0x69Aa2f9fe1572F1B640E1bbc512f5c3a734fc77c', + factoryData: '0x0000000', + }, + ], + signerMeta: { + delegationManager: '0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3', + }, + }, + siteOrigin: 'http://localhost:8000', +}); + +export type MockGatorPermissionsStorageEntriesConfig = { + [chainId: string]: { + nativeTokenStream: number; + nativeTokenPeriodic: number; + erc20TokenStream: number; + erc20TokenPeriodic: number; + custom: { + count: number; + data: Record[]; + }; + }; +}; + +/** + * Creates a mock gator permissions storage entry + * + * @param config - The config for the mock gator permissions storage entries. + * @returns Mock gator permissions storage entry + */ +/** + * Creates mock gator permissions storage entries with unique expiry times + * + * @param config - The config for the mock gator permissions storage entries. + * @returns Mock gator permissions storage entries + */ +export function mockGatorPermissionsStorageEntriesFactory( + config: MockGatorPermissionsStorageEntriesConfig, +): StoredGatorPermission[] { + const result: StoredGatorPermission[] = []; + let globalIndex = 0; + + Object.entries(config).forEach(([chainId, counts]) => { + if (counts.custom.count !== counts.custom.data.length) { + throw new Error('Custom permission count and data length mismatch'); + } + + /** + * Creates a number of entries with unique expiry times + * + * @param count - The number of entries to create. + * @param createEntry - The function to create an entry. + */ + const createEntries = ( + count: number, + createEntry: () => StoredGatorPermission, + ) => { + for (let i = 0; i < count; i++) { + const entry = createEntry(); + entry.permissionResponse.expiry += globalIndex; + result.push(entry); + globalIndex += 1; + } + }; + + createEntries(counts.nativeTokenStream, () => + mockNativeTokenStreamStorageEntry(chainId as Hex), + ); + + createEntries(counts.nativeTokenPeriodic, () => + mockNativeTokenPeriodicStorageEntry(chainId as Hex), + ); + + createEntries(counts.erc20TokenStream, () => + mockErc20TokenStreamStorageEntry(chainId as Hex), + ); + + createEntries(counts.erc20TokenPeriodic, () => + mockErc20TokenPeriodicStorageEntry(chainId as Hex), + ); + + // Create custom entries + for (let i = 0; i < counts.custom.count; i++) { + const entry = mockCustomPermissionStorageEntry( + chainId as Hex, + counts.custom.data[i], + ); + entry.permissionResponse.expiry += globalIndex; + result.push(entry); + globalIndex += 1; + } + }); + + return result; +} diff --git a/packages/gator-permissions-controller/src/types.ts b/packages/gator-permissions-controller/src/types.ts index ac0f7f5092..345972b94b 100644 --- a/packages/gator-permissions-controller/src/types.ts +++ b/packages/gator-permissions-controller/src/types.ts @@ -1,5 +1,25 @@ import type { Hex } from '@metamask/utils'; +/** + * Enum for the error codes of the gator permissions controller. + */ +export enum GatorPermissionsControllerErrorCode { + GatorPermissionsFetchError = 'gator-permissions-fetch-error', + GatorPermissionsNotEnabled = 'gator-permissions-not-enabled', + GatorPermissionsProviderError = 'gator-permissions-provider-error', + GatorPermissionsMapSerializationError = 'gator-permissions-map-serialization-error', +} + +/** + * Enum for the RPC methods of the gator permissions provider snap. + */ +export enum GatorPermissionsSnapRpcMethod { + /** + * This method is used by the metamask to request a permissions provider to get granted permissions for all sites. + */ + PermissionProviderGetGrantedPermissions = 'permissionsProvider_getGrantedPermissions', +} + type BasePermission = { type: string; @@ -51,13 +71,30 @@ export type Erc20TokenStreamPermission = BasePermission & { }; }; +export type Erc20TokenPeriodicPermission = BasePermission & { + type: 'erc20-token-periodic'; + data: MetaMaskBasePermissionData & { + periodAmount: Hex; + periodDuration: number; + startTime: number; + tokenAddress: Hex; + }; +}; + +export type CustomPermission = BasePermission & { + type: 'custom'; + data: MetaMaskBasePermissionData & Record; +}; + /** * Represents the type of the ERC-7715 permissions that can be granted. */ export type PermissionTypes = | NativeTokenStreamPermission | NativeTokenPeriodicPermission - | Erc20TokenStreamPermission; + | Erc20TokenStreamPermission + | Erc20TokenPeriodicPermission + | CustomPermission; /** * Represents an ERC-7715 account signer type. @@ -87,8 +124,8 @@ export type SignerParam = AccountSigner | WalletSigner; * @template Permission - The type of the permission provided. */ export type PermissionRequest< - Signer extends SignerParam, - Permission extends PermissionTypes, + TSigner extends SignerParam, + TPermission extends PermissionTypes, > = { /** * hex-encoding of uint256 defined the chain with EIP-155 @@ -115,12 +152,12 @@ export type PermissionRequest< /** * An account that is associated with the recipient of the granted 7715 permission or alternatively the wallet will manage the session. */ - signer: Signer; + signer: TSigner; /** * Defines the allowed behavior the signer can do on behalf of the account. */ - permission: Permission; + permission: TPermission; }; /** @@ -130,9 +167,9 @@ export type PermissionRequest< * @template Permission - The type of the permission provided. */ export type PermissionResponse< - Signer extends SignerParam, - Permission extends PermissionTypes, -> = PermissionRequest & { + TSigner extends SignerParam, + TPermission extends PermissionTypes, +> = PermissionRequest & { /** * Is a catch-all to identify a permission for revoking permissions or submitting * Defined in ERC-7710. @@ -159,6 +196,22 @@ export type PermissionResponse< }; }; +/** + * Represents a sanitized version of the PermissionResponse type that is exposed in the gator-permissions-controller package. + * The some fields have been removed from the types exposed in the gator-permissions-controller package, + * but the field is still present in profile sync. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided. + */ +export type PermissionResponseSanitized< + TSigner extends SignerParam, + TPermission extends PermissionTypes, +> = Omit< + PermissionResponse, + 'isAdjustmentAllowed' | 'accountMeta' | 'signer' +>; + /** * Represents a gator ERC-7715 granted(ie. signed by an user account) permission entry that is stored in profile sync. * @@ -166,52 +219,79 @@ export type PermissionResponse< * @template Permission - The type of the permission provided */ export type StoredGatorPermission< - Signer extends SignerParam, - Permission extends PermissionTypes, + TSigner extends SignerParam, + TPermission extends PermissionTypes, > = { - permissionResponse: PermissionResponse; + permissionResponse: PermissionResponse; siteOrigin: string; }; /** - * Represents a list of gator permissions filtered by permission type and chainId. + * Represents a sanitized version of the StoredGatorPermission type. Some fields have been removed from the types exposed in the gator-permissions-controller package, + * but the fields are still present in profile sync. + * + * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. + * @template Permission - The type of the permission provided. */ -export type GatorPermissionsList = { +export type StoredGatorPermissionSanitized< + TSigner extends SignerParam, + TPermission extends PermissionTypes, +> = { + permissionResponse: PermissionResponseSanitized; + siteOrigin: string; +}; + +/** + * Represents a map of gator permissions by chainId and permission type. + */ +export type GatorPermissionsMap = { 'native-token-stream': { - [chainId: Hex]: StoredGatorPermission< + [chainId: Hex]: StoredGatorPermissionSanitized< SignerParam, NativeTokenStreamPermission >[]; }; 'native-token-periodic': { - [chainId: Hex]: StoredGatorPermission< + [chainId: Hex]: StoredGatorPermissionSanitized< SignerParam, NativeTokenPeriodicPermission >[]; }; 'erc20-token-stream': { - [chainId: Hex]: StoredGatorPermission< + [chainId: Hex]: StoredGatorPermissionSanitized< SignerParam, Erc20TokenStreamPermission >[]; }; + 'erc20-token-periodic': { + [chainId: Hex]: StoredGatorPermissionSanitized< + SignerParam, + Erc20TokenPeriodicPermission + >[]; + }; + other: { + [chainId: Hex]: StoredGatorPermissionSanitized< + SignerParam, + CustomPermission + >[]; + }; }; /** - * Represents the supported permission type(e.g. 'native-token-stream', 'native-token-periodic', 'erc20-token-stream') of the gator permissions list. + * Represents the supported permission type(e.g. 'native-token-stream', 'native-token-periodic', 'erc20-token-stream', 'erc20-token-periodic') of the gator permissions map. */ -export type SupportedGatorPermissionType = keyof GatorPermissionsList; +export type SupportedGatorPermissionType = keyof GatorPermissionsMap; /** - * Represents a list of gator permissions filtered by permission type.(ie, a record of gator permissions by permission type with chainId as the key) + * Represents a map of gator permissions for a given permission type with key of chainId. The value being an array of gator permissions for that chainId. */ -export type GatorPermissionsListByPermissionType< - PermissionType extends SupportedGatorPermissionType, -> = GatorPermissionsList[PermissionType]; +export type GatorPermissionsMapByPermissionType< + TPermissionType extends SupportedGatorPermissionType, +> = GatorPermissionsMap[TPermissionType]; /** - * Represents a list of gator permissions filtered by permission type and chainId.(ie, a array of gator permissions for a given chainId and permission type) + * Represents an array of gator permissions for a given permission type and chainId. */ -export type GatorPermissionsListItemsByPermissionTypeAndChainId< - PermissionType extends SupportedGatorPermissionType, -> = GatorPermissionsList[PermissionType][Hex]; +export type GatorPermissionsListByPermissionTypeAndChainId< + TPermissionType extends SupportedGatorPermissionType, +> = GatorPermissionsMap[TPermissionType][Hex]; diff --git a/packages/gator-permissions-controller/src/utils.test.ts b/packages/gator-permissions-controller/src/utils.test.ts index dce550ec09..81a5f73259 100644 --- a/packages/gator-permissions-controller/src/utils.test.ts +++ b/packages/gator-permissions-controller/src/utils.test.ts @@ -1,59 +1,60 @@ +import type { GatorPermissionsMap } from './types'; import { - deserializeGatorPermissionsList, - serializeGatorPermissionsList, + deserializeGatorPermissionsMap, + serializeGatorPermissionsMap, } from './utils'; -describe('utils - serializeGatorPermissionsList() tests', () => { - it('serializes a gator permissions list to a string', () => { - const gatorPermissionsList = { - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - }; +const defaultGatorPermissionsMap: GatorPermissionsMap = { + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, +}; - const serializedGatorPermissionsList = - serializeGatorPermissionsList(gatorPermissionsList); +describe('utils - serializeGatorPermissionsMap() tests', () => { + it('serializes a gator permissions list to a string', () => { + const serializedGatorPermissionsMap = serializeGatorPermissionsMap( + defaultGatorPermissionsMap, + ); - expect(serializedGatorPermissionsList).toStrictEqual( - JSON.stringify(gatorPermissionsList), + expect(serializedGatorPermissionsMap).toStrictEqual( + JSON.stringify(defaultGatorPermissionsMap), ); }); it('throws an error when serialization fails', () => { - // Create a valid GatorPermissionsList structure but with circular reference - const gatorPermissionsList = { + // Create a valid GatorPermissionsMap structure but with circular reference + const gatorPermissionsMap = { 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, }; // Add circular reference to cause JSON.stringify to fail // eslint-disable-next-line @typescript-eslint/no-explicit-any - (gatorPermissionsList as any).circular = gatorPermissionsList; + (gatorPermissionsMap as any).circular = gatorPermissionsMap; expect(() => { - serializeGatorPermissionsList(gatorPermissionsList); - }).toThrow('Converting circular structure to JSON'); + serializeGatorPermissionsMap(gatorPermissionsMap); + }).toThrow('Failed to serialize gator permissions map'); }); }); -describe('utils - deserializeGatorPermissionsList() tests', () => { +describe('utils - deserializeGatorPermissionsMap() tests', () => { it('deserializes a gator permissions list from a string', () => { - const gatorPermissionsList = { - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - }; - - const serializedGatorPermissionsList = - serializeGatorPermissionsList(gatorPermissionsList); + const serializedGatorPermissionsMap = serializeGatorPermissionsMap( + defaultGatorPermissionsMap, + ); - const deserializedGatorPermissionsList = deserializeGatorPermissionsList( - serializedGatorPermissionsList, + const deserializedGatorPermissionsMap = deserializeGatorPermissionsMap( + serializedGatorPermissionsMap, ); - expect(deserializedGatorPermissionsList).toStrictEqual( - gatorPermissionsList, + expect(deserializedGatorPermissionsMap).toStrictEqual( + defaultGatorPermissionsMap, ); }); @@ -61,7 +62,7 @@ describe('utils - deserializeGatorPermissionsList() tests', () => { const invalidJson = '{"invalid": json}'; expect(() => { - deserializeGatorPermissionsList(invalidJson); - }).toThrow('Unexpected token'); + deserializeGatorPermissionsMap(invalidJson); + }).toThrow('Failed to deserialize gator permissions map'); }); }); diff --git a/packages/gator-permissions-controller/src/utils.ts b/packages/gator-permissions-controller/src/utils.ts index 072c361324..50ee6851f4 100644 --- a/packages/gator-permissions-controller/src/utils.ts +++ b/packages/gator-permissions-controller/src/utils.ts @@ -1,40 +1,45 @@ -import { createModuleLogger } from '@metamask/utils'; - -import { projectLogger } from './logger'; -import type { GatorPermissionsList } from './types'; - -const log = createModuleLogger(projectLogger, 'utils'); +import { GatorPermissionsMapSerializationError } from './errors'; +import { utilsLog } from './logger'; +import type { GatorPermissionsMap } from './types'; /** - * Serializes a gator permissions list to a string. + * Serializes a gator permissions map to a string. * - * @param gatorPermissionsList - The gator permissions list to serialize. - * @returns The serialized gator permissions list. + * @param gatorPermissionsMap - The gator permissions map to serialize. + * @returns The serialized gator permissions map. */ -export function serializeGatorPermissionsList( - gatorPermissionsList: GatorPermissionsList, +export function serializeGatorPermissionsMap( + gatorPermissionsMap: GatorPermissionsMap, ): string { try { - return JSON.stringify(gatorPermissionsList); + return JSON.stringify(gatorPermissionsMap); } catch (error) { - log('Failed to serialize gator permissions list', error); - throw error; + utilsLog('Failed to serialize gator permissions map', error); + throw new GatorPermissionsMapSerializationError({ + cause: error as Error, + message: 'Failed to serialize gator permissions map', + data: gatorPermissionsMap, + }); } } /** - * Deserializes a gator permissions list from a string. + * Deserializes a gator permissions map from a string. * - * @param gatorPermissionsList - The gator permissions list to deserialize. - * @returns The deserialized gator permissions list. + * @param gatorPermissionsMap - The gator permissions map to deserialize. + * @returns The deserialized gator permissions map. */ -export function deserializeGatorPermissionsList( - gatorPermissionsList: string, -): GatorPermissionsList { +export function deserializeGatorPermissionsMap( + gatorPermissionsMap: string, +): GatorPermissionsMap { try { - return JSON.parse(gatorPermissionsList); + return JSON.parse(gatorPermissionsMap); } catch (error) { - log('Failed to deserialize gator permissions list', error); - throw error; + utilsLog('Failed to deserialize gator permissions map', error); + throw new GatorPermissionsMapSerializationError({ + cause: error as Error, + message: 'Failed to deserialize gator permissions map', + data: gatorPermissionsMap, + }); } } From 5d28140bf49820b34d954bf832d50fc7373ec9fa Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:37:49 -0400 Subject: [PATCH 21/24] Fix Bug: Incorrect Type Assertion in Async Method --- .../src/GatorPermissionsController.ts | 6 +----- packages/gator-permissions-controller/src/types.ts | 8 +++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 8bc0259d0f..8330365937 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -279,9 +279,7 @@ export default class GatorPermissionsController extends BaseController< GatorPermissionsSnapRpcMethod.PermissionProviderGetGrantedPermissions, }, }, - )) as Promise< - StoredGatorPermission[] | null - >; + )) as StoredGatorPermission[] | null; return response; } catch (error) { @@ -320,8 +318,6 @@ export default class GatorPermissionsController extends BaseController< * * @param storedGatorPermissions - An array of stored gator permissions. * @returns The gator permissions map. - * @throws {SignerTypeNotSupportedError} If signer type is not account. - * @throws {Error} If permission type is invalid. */ #categorizePermissionsDataByTypeAndChainId( storedGatorPermissions: diff --git a/packages/gator-permissions-controller/src/types.ts b/packages/gator-permissions-controller/src/types.ts index 345972b94b..66d3052e42 100644 --- a/packages/gator-permissions-controller/src/types.ts +++ b/packages/gator-permissions-controller/src/types.ts @@ -197,9 +197,8 @@ export type PermissionResponse< }; /** - * Represents a sanitized version of the PermissionResponse type that is exposed in the gator-permissions-controller package. - * The some fields have been removed from the types exposed in the gator-permissions-controller package, - * but the field is still present in profile sync. + * Represents a sanitized version of the PermissionResponse type. + * Some fields have been removed but the fields are still present in profile sync. * * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. * @template Permission - The type of the permission provided. @@ -227,8 +226,7 @@ export type StoredGatorPermission< }; /** - * Represents a sanitized version of the StoredGatorPermission type. Some fields have been removed from the types exposed in the gator-permissions-controller package, - * but the fields are still present in profile sync. + * Represents a sanitized version of the StoredGatorPermission type. Some fields have been removed but the fields are still present in profile sync. * * @template Signer - The type of the signer provided, either an AccountSigner or WalletSigner. * @template Permission - The type of the permission provided. From db13f1391946133039085876f24792fc69d911b6 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:40:38 -0400 Subject: [PATCH 22/24] Export GatorPermissionsControllerConfig type --- packages/gator-permissions-controller/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index dc8c0db07a..8cfd3c3ffb 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -15,5 +15,6 @@ export type { GatorPermissionsControllerActionsEvents, AllowedEvents, GatorPermissionsControllerStateChangeEvent, + GatorPermissionsControllerConfig, } from './GatorPermissionsController'; export type * from './types'; From b84d366f905a6e6434666d1ab2617c16852d463a Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:56:08 -0400 Subject: [PATCH 23/24] Fix: Mermaid Graph Naming Inconsistency --- README.md | 2 +- packages/gator-permissions-controller/package.json | 2 +- .../src/GatorPermissionContoller.test.ts | 4 ++-- yarn.lock | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 36bddff9ab..d8a494547d 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ linkStyle default opacity:0.5 gas_fee_controller --> controller_utils; gas_fee_controller --> polling_controller; gas_fee_controller --> network_controller; - gator-permissions-controller --> base_controller; + gator_permissions_controller --> base_controller; json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; logging_controller --> base_controller; diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index 9d648de48b..f8e19924e2 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.0.1", + "@metamask/base-controller": "^8.1.0", "@metamask/snaps-sdk": "^9.0.0", "@metamask/snaps-utils": "^11.0.0", "@metamask/utils": "^11.4.2" diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index 90aac3b57a..1b4479ed77 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -135,7 +135,7 @@ describe('gator-permissions-controller - constructor() tests', () => { it('creates GatorPermissionsController with custom state', () => { const customState = { isGatorPermissionsEnabled: true, - gatorPermissionsMap: JSON.stringify({ + gatorPermissionsMapSerialized: JSON.stringify({ 'native-token-stream': {}, 'native-token-periodic': {}, 'erc20-token-stream': {}, @@ -155,7 +155,7 @@ describe('gator-permissions-controller - constructor() tests', () => { ); expect(controller.state.isGatorPermissionsEnabled).toBe(true); expect(controller.state.gatorPermissionsMapSerialized).toBe( - customState.gatorPermissionsMap, + customState.gatorPermissionsMapSerialized, ); }); diff --git a/yarn.lock b/yarn.lock index 1b54fa5fd7..2b7cde1611 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3600,7 +3600,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^8.0.1" + "@metamask/base-controller": "npm:^8.1.0" "@metamask/snaps-controllers": "npm:^14.0.1" "@metamask/snaps-sdk": "npm:^9.0.0" "@metamask/snaps-utils": "npm:^11.0.0" From 96499e425c003d7d00c0ab7aa7900500c8012428 Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:24:07 -0400 Subject: [PATCH 24/24] Ensure controller guidelines are being followed --- .../gator-permissions-controller/package.json | 2 +- .../src/GatorPermissionContoller.test.ts | 738 +++++++++--------- .../src/GatorPermissionsController.ts | 181 +++-- .../gator-permissions-controller/src/index.ts | 35 +- yarn.lock | 2 +- 5 files changed, 492 insertions(+), 466 deletions(-) diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index f8e19924e2..bf5d031be8 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -67,7 +67,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/snaps-controllers": "^14.0.0" + "@metamask/snaps-controllers": "^14.0.1" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts index 1b4479ed77..4730e5490f 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionContoller.test.ts @@ -1,12 +1,9 @@ import { Messenger } from '@metamask/base-controller'; +import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import type { Hex } from '@metamask/utils'; -import type { - AllowedActions, - AllowedEvents, - GatorPermissionsControllerConfig, -} from './GatorPermissionsController'; +import type { GatorPermissionsControllerMessenger } from './GatorPermissionsController'; import GatorPermissionsController from './GatorPermissionsController'; import { mockCustomPermissionStorageEntry, @@ -16,447 +13,438 @@ import { mockNativeTokenPeriodicStorageEntry, mockNativeTokenStreamStorageEntry, } from './test/mocks'; -import type { GatorPermissionsMap } from './types'; +import type { + AccountSigner, + GatorPermissionsMap, + StoredGatorPermission, + PermissionTypes, +} from './types'; +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../base-controller/tests/helpers'; const MOCK_CHAIN_ID_1: Hex = '0xaa36a7'; const MOCK_CHAIN_ID_2: Hex = '0x1'; - -/** - * Jest Test Utility - create Gator Permissions Messenger - * - * @returns Gator Permissions Messenger - */ -function createGatorPermissionsMessenger() { - const baseMessenger = new Messenger(); - const messenger = baseMessenger.getRestricted({ - name: 'GatorPermissionsController', - allowedActions: ['SnapController:handleRequest', 'SnapController:has'], - allowedEvents: [], - }); - - return { messenger, baseMessenger }; -} - -/** - * Jest Test Utility - create Mock Gator Permissions Messenger - * - * @returns Mock Gator Permissions Messenger - */ -function createMockGatorPermissionsMessenger() { - const { baseMessenger, messenger } = createGatorPermissionsMessenger(); - - const mockCall = jest.spyOn(messenger, 'call'); - const mockGetGrantedPermissions = jest.fn().mockResolvedValue( - mockGatorPermissionsStorageEntriesFactory({ - [MOCK_CHAIN_ID_1]: { - nativeTokenStream: 5, - nativeTokenPeriodic: 5, - erc20TokenStream: 5, - erc20TokenPeriodic: 5, - custom: { - count: 2, - data: [ - { - customData: 'customData-0', - }, - { - customData: 'customData-1', - }, - ], +const MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID = + 'local:http://localhost:8082' as SnapId; +const MOCK_GATOR_PERMISSIONS_STORAGE_ENTRIES: StoredGatorPermission< + AccountSigner, + PermissionTypes +>[] = mockGatorPermissionsStorageEntriesFactory({ + [MOCK_CHAIN_ID_1]: { + nativeTokenStream: 5, + nativeTokenPeriodic: 5, + erc20TokenStream: 5, + erc20TokenPeriodic: 5, + custom: { + count: 2, + data: [ + { + customData: 'customData-0', }, - }, - [MOCK_CHAIN_ID_2]: { - nativeTokenStream: 5, - nativeTokenPeriodic: 5, - erc20TokenStream: 5, - erc20TokenPeriodic: 5, - custom: { - count: 2, - data: [ - { - customData: 'customData-0', - }, - { - customData: 'customData-1', - }, - ], + { + customData: 'customData-1', }, - }, - }), - ); - const mockHasSnap = jest.fn().mockResolvedValue(true); - - mockCall.mockImplementation((...args) => { - const [actionType] = args; - if (actionType === 'SnapController:handleRequest') { - return mockGetGrantedPermissions(); - } - if (actionType === 'SnapController:has') { - return mockHasSnap(); - } - - throw new Error( - `MOCK_FAIL - unsupported messenger call: ${actionType as string}`, - ); - }); - - return { - messenger, - baseMessenger, - mockGetGrantedPermissions, - mockHasSnap, - }; -} + ], + }, + }, + [MOCK_CHAIN_ID_2]: { + nativeTokenStream: 5, + nativeTokenPeriodic: 5, + erc20TokenStream: 5, + erc20TokenPeriodic: 5, + custom: { + count: 2, + data: [ + { + customData: 'customData-0', + }, + { + customData: 'customData-1', + }, + ], + }, + }, +}); -const mockGatorPermissionsControllerConfig: GatorPermissionsControllerConfig = { - gatorPermissionsProviderSnapId: 'local:http://localhost:8082' as SnapId, -}; +describe('GatorPermissionsController', () => { + describe('constructor', () => { + it('creates GatorPermissionsController with default state', () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); -describe('gator-permissions-controller - constructor() tests', () => { - it('creates GatorPermissionsController with default state', () => { - const controller = new GatorPermissionsController({ - messenger: createMockGatorPermissionsMessenger().messenger, - config: mockGatorPermissionsControllerConfig, + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.gatorPermissionsMapSerialized).toStrictEqual( + JSON.stringify({ + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, + }), + ); + expect(controller.state.isFetchingGatorPermissions).toBe(false); }); - expect(controller.state.isGatorPermissionsEnabled).toBe(false); - expect(controller.state.gatorPermissionsMapSerialized).toStrictEqual( - JSON.stringify({ - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, - }), - ); - expect(controller.state.isFetchingGatorPermissions).toBe(false); - }); - - it('creates GatorPermissionsController with custom state', () => { - const customState = { - isGatorPermissionsEnabled: true, - gatorPermissionsMapSerialized: JSON.stringify({ - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, - }), - }; + it('creates GatorPermissionsController with custom state', () => { + const customState = { + isGatorPermissionsEnabled: true, + gatorPermissionsMapSerialized: JSON.stringify({ + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, + }), + gatorPermissionsProviderSnapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + }; + + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + state: customState, + }); - const controller = new GatorPermissionsController({ - messenger: createMockGatorPermissionsMessenger().messenger, - config: mockGatorPermissionsControllerConfig, - state: customState, + expect(controller.state.gatorPermissionsProviderSnapId).toBe( + MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + ); + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + expect(controller.state.gatorPermissionsMapSerialized).toBe( + customState.gatorPermissionsMapSerialized, + ); }); - expect(controller.permissionsProviderSnapId).toBe( - mockGatorPermissionsControllerConfig.gatorPermissionsProviderSnapId, - ); - expect(controller.state.isGatorPermissionsEnabled).toBe(true); - expect(controller.state.gatorPermissionsMapSerialized).toBe( - customState.gatorPermissionsMapSerialized, - ); - }); + it('creates GatorPermissionsController with default config', () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); - it('creates GatorPermissionsController with default config', () => { - const controller = new GatorPermissionsController({ - messenger: createMockGatorPermissionsMessenger().messenger, + expect(controller.permissionsProviderSnapId).toBe( + '@metamask/gator-permissions-snap' as SnapId, + ); + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.isFetchingGatorPermissions).toBe(false); }); - expect(controller.permissionsProviderSnapId).toBe( - '@metamask/gator-permissions-snap' as SnapId, - ); - expect(controller.state.isGatorPermissionsEnabled).toBe(false); - expect(controller.state.isFetchingGatorPermissions).toBe(false); + it('isFetchingGatorPermissions is false on initialization', () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + state: { + isFetchingGatorPermissions: true, + }, + }); + + expect(controller.state.isFetchingGatorPermissions).toBe(false); + }); }); -}); -describe('gator-permissions-controller - disableGatorPermissions() tests', () => { - it('disables gator permissions successfully', async () => { - const { messenger } = createMockGatorPermissionsMessenger(); + describe('disableGatorPermissions', () => { + it('disables gator permissions successfully', async () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, + await controller.enableGatorPermissions(); + expect(controller.state.isGatorPermissionsEnabled).toBe(true); + + await controller.disableGatorPermissions(); + + expect(controller.state.isGatorPermissionsEnabled).toBe(false); + expect(controller.state.gatorPermissionsMapSerialized).toBe( + JSON.stringify({ + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, + }), + ); }); + }); - await controller.enableGatorPermissions(); - expect(controller.state.isGatorPermissionsEnabled).toBe(true); + describe('fetchAndUpdateGatorPermissions', () => { + it('fetches and updates gator permissions successfully', async () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); - await controller.disableGatorPermissions(); + await controller.enableGatorPermissions(); - expect(controller.state.isGatorPermissionsEnabled).toBe(false); - expect(controller.state.gatorPermissionsMapSerialized).toBe( - JSON.stringify({ - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, - }), - ); - }); -}); + const result = await controller.fetchAndUpdateGatorPermissions(); -describe('gator-permissions-controller - fetchAndUpdateGatorPermissions() tests', () => { - it('fetches and updates gator permissions successfully', async () => { - const { messenger } = createMockGatorPermissionsMessenger(); + expect(result).toStrictEqual({ + 'native-token-stream': expect.any(Object), + 'native-token-periodic': expect.any(Object), + 'erc20-token-stream': expect.any(Object), + 'erc20-token-periodic': expect.any(Object), + other: expect.any(Object), + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, + // Check that each permission type has the expected chainId + expect(result['native-token-stream'][MOCK_CHAIN_ID_1]).toHaveLength(5); + expect(result['native-token-periodic'][MOCK_CHAIN_ID_1]).toHaveLength(5); + expect(result['erc20-token-stream'][MOCK_CHAIN_ID_1]).toHaveLength(5); + expect(result['native-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); + expect(result['native-token-periodic'][MOCK_CHAIN_ID_2]).toHaveLength(5); + expect(result['erc20-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); + expect(result.other[MOCK_CHAIN_ID_1]).toHaveLength(2); + expect(result.other[MOCK_CHAIN_ID_2]).toHaveLength(2); + expect(controller.state.isFetchingGatorPermissions).toBe(false); + + // check that the gator permissions map is sanitized + const sanitizedCheck = (permissionType: keyof GatorPermissionsMap) => { + const flattenedStoredGatorPermissions = Object.values( + result[permissionType], + ).flat(); + flattenedStoredGatorPermissions.forEach((permission) => { + expect( + permission.permissionResponse.isAdjustmentAllowed, + ).toBeUndefined(); + expect(permission.permissionResponse.accountMeta).toBeUndefined(); + expect(permission.permissionResponse.signer).toBeUndefined(); + }); + }; + + sanitizedCheck('native-token-stream'); + sanitizedCheck('native-token-periodic'); + sanitizedCheck('erc20-token-stream'); + sanitizedCheck('erc20-token-periodic'); + sanitizedCheck('other'); }); - await controller.enableGatorPermissions(); + it('throws error when gator permissions are not enabled', async () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); - const result = await controller.fetchAndUpdateGatorPermissions(); + await controller.disableGatorPermissions(); - expect(result).toStrictEqual({ - 'native-token-stream': expect.any(Object), - 'native-token-periodic': expect.any(Object), - 'erc20-token-stream': expect.any(Object), - 'erc20-token-periodic': expect.any(Object), - other: expect.any(Object), + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); }); - // Check that each permission type has the expected chainId - expect(result['native-token-stream'][MOCK_CHAIN_ID_1]).toHaveLength(5); - expect(result['native-token-periodic'][MOCK_CHAIN_ID_1]).toHaveLength(5); - expect(result['erc20-token-stream'][MOCK_CHAIN_ID_1]).toHaveLength(5); - expect(result['native-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); - expect(result['native-token-periodic'][MOCK_CHAIN_ID_2]).toHaveLength(5); - expect(result['erc20-token-stream'][MOCK_CHAIN_ID_2]).toHaveLength(5); - expect(result.other[MOCK_CHAIN_ID_1]).toHaveLength(2); - expect(result.other[MOCK_CHAIN_ID_2]).toHaveLength(2); - expect(controller.state.isFetchingGatorPermissions).toBe(false); - - // check that the gator permissions map is sanitized - const sanitizedCheck = (permissionType: keyof GatorPermissionsMap) => { - const flattenedStoredGatorPermissions = Object.values( - result[permissionType], - ).flat(); - flattenedStoredGatorPermissions.forEach((permission) => { - expect( - permission.permissionResponse.isAdjustmentAllowed, - ).toBeUndefined(); - expect(permission.permissionResponse.accountMeta).toBeUndefined(); - expect(permission.permissionResponse.signer).toBeUndefined(); + it('handles null permissions data', async () => { + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: async () => null, }); - }; - - sanitizedCheck('native-token-stream'); - sanitizedCheck('native-token-periodic'); - sanitizedCheck('erc20-token-stream'); - sanitizedCheck('erc20-token-periodic'); - sanitizedCheck('other'); - }); - it('throws error when gator permissions are not enabled', async () => { - const { messenger } = createMockGatorPermissionsMessenger(); + const controller = new GatorPermissionsController({ + messenger: getMessenger(rootMessenger), + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); + await controller.enableGatorPermissions(); - await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', - ); - }); + const result = await controller.fetchAndUpdateGatorPermissions(); - it('handles null permissions data', async () => { - const { messenger, mockGetGrantedPermissions } = - createMockGatorPermissionsMessenger(); + expect(result).toStrictEqual({ + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, + }); + }); - mockGetGrantedPermissions.mockResolvedValue(null); + it('handles empty permissions data', async () => { + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: async () => [], + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); + const controller = new GatorPermissionsController({ + messenger: getMessenger(rootMessenger), + }); - await controller.enableGatorPermissions(); + await controller.enableGatorPermissions(); - const result = await controller.fetchAndUpdateGatorPermissions(); + const result = await controller.fetchAndUpdateGatorPermissions(); - expect(result).toStrictEqual({ - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, + expect(result).toStrictEqual({ + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, + }); }); - }); - it('handles empty permissions data', async () => { - const { messenger, mockGetGrantedPermissions } = - createMockGatorPermissionsMessenger(); - - mockGetGrantedPermissions.mockResolvedValue([]); + it('handles error during fetch and update', async () => { + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: async () => { + throw new Error('Storage error'); + }, + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); + const controller = new GatorPermissionsController({ + messenger: getMessenger(rootMessenger), + }); - await controller.enableGatorPermissions(); + await controller.enableGatorPermissions(); - const result = await controller.fetchAndUpdateGatorPermissions(); + await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( + 'Failed to fetch gator permissions', + ); - expect(result).toStrictEqual({ - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, + expect(controller.state.isFetchingGatorPermissions).toBe(false); }); }); - it('handles error during fetch and update', async () => { - const { messenger, mockGetGrantedPermissions } = - createMockGatorPermissionsMessenger(); + describe('gatorPermissionsMap getter tests', () => { + it('returns parsed gator permissions map', () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); - mockGetGrantedPermissions.mockRejectedValue(new Error('Storage error')); + const { gatorPermissionsMap } = controller; - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, + expect(gatorPermissionsMap).toStrictEqual({ + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, + }); }); - await controller.enableGatorPermissions(); - - await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', - ); - - expect(controller.state.isFetchingGatorPermissions).toBe(false); - }); -}); - -describe('gator-permissions-controller - gatorPermissionsMap getter tests', () => { - it('returns parsed gator permissions map', () => { - const { messenger } = createMockGatorPermissionsMessenger(); + it('returns parsed gator permissions map with data when state is provided', () => { + const mockState = { + 'native-token-stream': { + '0x1': [mockNativeTokenStreamStorageEntry('0x1')], + }, + 'native-token-periodic': { + '0x2': [mockNativeTokenPeriodicStorageEntry('0x2')], + }, + 'erc20-token-stream': { + '0x3': [mockErc20TokenStreamStorageEntry('0x3')], + }, + 'erc20-token-periodic': { + '0x4': [mockErc20TokenPeriodicStorageEntry('0x4')], + }, + other: { + '0x5': [ + mockCustomPermissionStorageEntry('0x5', { + customData: 'customData-0', + }), + ], + }, + }; - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + state: { + gatorPermissionsMapSerialized: JSON.stringify(mockState), + }, + }); - const { gatorPermissionsMap } = controller; + const { gatorPermissionsMap } = controller; - expect(gatorPermissionsMap).toStrictEqual({ - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, + expect(gatorPermissionsMap).toStrictEqual(mockState); }); }); - it('returns parsed gator permissions map with data when state is provided', () => { - const { messenger } = createMockGatorPermissionsMessenger(); - const mockState = { - 'native-token-stream': { - '0x1': [mockNativeTokenStreamStorageEntry('0x1')], - }, - 'native-token-periodic': { - '0x2': [mockNativeTokenPeriodicStorageEntry('0x2')], - }, - 'erc20-token-stream': { - '0x3': [mockErc20TokenStreamStorageEntry('0x3')], - }, - 'erc20-token-periodic': { - '0x4': [mockErc20TokenPeriodicStorageEntry('0x4')], - }, - other: { - '0x5': [ - mockCustomPermissionStorageEntry('0x5', { - customData: 'customData-0', - }), - ], - }, - }; - - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - state: { - gatorPermissionsMapSerialized: JSON.stringify(mockState), - }, - }); - - const { gatorPermissionsMap } = controller; + describe('message handlers tests', () => { + it('registers all message handlers', () => { + const messenger = getMessenger(); + const mockRegisterActionHandler = jest.spyOn( + messenger, + 'registerActionHandler', + ); - expect(gatorPermissionsMap).toStrictEqual(mockState); - }); -}); - -describe('gator-permissions-controller - private methods tests', () => { - it('clears loading states on initialization', () => { - const { messenger } = createMockGatorPermissionsMessenger(); + new GatorPermissionsController({ + messenger, + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + expect.any(Function), + ); + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:enableGatorPermissions', + expect.any(Function), + ); + expect(mockRegisterActionHandler).toHaveBeenCalledWith( + 'GatorPermissionsController:disableGatorPermissions', + expect.any(Function), + ); }); - - expect(controller.state.isFetchingGatorPermissions).toBe(false); }); - it('asserts gator permissions are enabled', async () => { - const { messenger } = createMockGatorPermissionsMessenger(); + describe('enableGatorPermissions', () => { + it('enables gator permissions successfully', async () => { + const controller = new GatorPermissionsController({ + messenger: getMessenger(), + }); - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); + await controller.enableGatorPermissions(); - // Should not throw when enabled - await controller.enableGatorPermissions(); - expect(controller.state.isGatorPermissionsEnabled).toBe(true); - }); -}); - -describe('gator-permissions-controller - message handlers tests', () => { - it('registers all message handlers', () => { - const { messenger } = createMockGatorPermissionsMessenger(); - const mockRegisterActionHandler = jest.spyOn( - messenger, - 'registerActionHandler', - ); - - new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, + expect(controller.state.isGatorPermissionsEnabled).toBe(true); }); - - expect(mockRegisterActionHandler).toHaveBeenCalledWith( - 'GatorPermissionsController:fetchAndUpdateGatorPermissions', - expect.any(Function), - ); - expect(mockRegisterActionHandler).toHaveBeenCalledWith( - 'GatorPermissionsController:enableGatorPermissions', - expect.any(Function), - ); - expect(mockRegisterActionHandler).toHaveBeenCalledWith( - 'GatorPermissionsController:disableGatorPermissions', - expect.any(Function), - ); }); }); -describe('gator-permissions-controller - enableGatorPermissions() tests', () => { - it('enables gator permissions successfully', async () => { - const { messenger } = createMockGatorPermissionsMessenger(); +/** + * The union of actions that the root messenger allows. + */ +type RootAction = ExtractAvailableAction; - const controller = new GatorPermissionsController({ - messenger, - config: mockGatorPermissionsControllerConfig, - }); +/** + * The union of events that the root messenger allows. + */ +type RootEvent = ExtractAvailableEvent; - await controller.enableGatorPermissions(); +/** + * Constructs the unrestricted messenger. This can be used to call actions and + * publish events within the tests for this controller. + * + * @param args - The arguments to this function. + * `GatorPermissionsController:getState` action on the messenger. + * @param args.snapControllerHandleRequestActionHandler - Used to mock the + * `SnapController:handleRequest` action on the messenger. + * @param args.snapControllerHasActionHandler - Used to mock the + * `SnapController:has` action on the messenger. + * @returns The unrestricted messenger suited for GatorPermissionsController. + */ +function getRootMessenger({ + snapControllerHandleRequestActionHandler = jest + .fn< + ReturnType, + Parameters + >() + .mockResolvedValue(MOCK_GATOR_PERMISSIONS_STORAGE_ENTRIES), + snapControllerHasActionHandler = jest + .fn, Parameters>() + .mockResolvedValue(true as never), +}: { + snapControllerHandleRequestActionHandler?: HandleSnapRequest['handler']; + snapControllerHasActionHandler?: HasSnap['handler']; +} = {}): Messenger { + const rootMessenger = new Messenger(); + + rootMessenger.registerActionHandler( + 'SnapController:handleRequest', + snapControllerHandleRequestActionHandler, + ); + rootMessenger.registerActionHandler( + 'SnapController:has', + snapControllerHasActionHandler, + ); + return rootMessenger; +} - expect(controller.state.isGatorPermissionsEnabled).toBe(true); +/** + * Constructs the messenger which is restricted to relevant SampleGasPricesController + * actions and events. + * + * @param rootMessenger - The root messenger to restrict. + * @returns The restricted messenger. + */ +function getMessenger( + rootMessenger = getRootMessenger(), +): GatorPermissionsControllerMessenger { + return rootMessenger.getRestricted({ + name: 'GatorPermissionsController', + allowedActions: ['SnapController:handleRequest', 'SnapController:has'], + allowedEvents: [], }); -}); +} diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 8330365937..9b5b02864d 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -28,6 +28,8 @@ import { serializeGatorPermissionsMap, } from './utils'; +// === GENERAL === + // Unique name for the controller const controllerName = 'GatorPermissionsController'; @@ -35,6 +37,16 @@ const controllerName = 'GatorPermissionsController'; const defaultGatorPermissionsProviderSnapId = '@metamask/gator-permissions-snap' as SnapId; +const defaultGatorPermissionsMap: GatorPermissionsMap = { + 'native-token-stream': {}, + 'native-token-periodic': {}, + 'erc20-token-stream': {}, + 'erc20-token-periodic': {}, + other: {}, +}; + +// === STATE === + /** * State shape for GatorPermissionsController */ @@ -54,9 +66,15 @@ export type GatorPermissionsControllerState = { * This is used to show a loading spinner in the UI */ isFetchingGatorPermissions: boolean; + + /** + * The ID of the Snap of the gator permissions provider snap + * Default value is `@metamask/gator-permissions-snap` + */ + gatorPermissionsProviderSnapId: SnapId; }; -const metadata: StateMetadata = { +const gatorPermissionsControllerMetadata = { isGatorPermissionsEnabled: { persist: true, anonymous: false, @@ -69,63 +87,87 @@ const metadata: StateMetadata = { persist: false, anonymous: false, }, -}; - -const defaultGatorPermissionsMap: GatorPermissionsMap = { - 'native-token-stream': {}, - 'native-token-periodic': {}, - 'erc20-token-stream': {}, - 'erc20-token-periodic': {}, - other: {}, -}; - -export const defaultState: GatorPermissionsControllerState = { - isGatorPermissionsEnabled: false, - gatorPermissionsMapSerialized: serializeGatorPermissionsMap( - defaultGatorPermissionsMap, - ), - isFetchingGatorPermissions: false, -}; + gatorPermissionsProviderSnapId: { + persist: false, + anonymous: false, + }, +} satisfies StateMetadata; -// Messenger Actions -type CreateActionsObj = { - [K in Controller]: { - type: `${typeof controllerName}:${K}`; - handler: GatorPermissionsController[K]; +/** + * Constructs the default {@link GatorPermissionsController} state. This allows + * consumers to provide a partial state object when initializing the controller + * and also helps in constructing complete state objects for this controller in + * tests. + * + * @returns The default {@link GatorPermissionsController} state. + */ +export function getDefaultGatorPermissionsControllerState(): GatorPermissionsControllerState { + return { + isGatorPermissionsEnabled: false, + gatorPermissionsMapSerialized: serializeGatorPermissionsMap( + defaultGatorPermissionsMap, + ), + isFetchingGatorPermissions: false, + gatorPermissionsProviderSnapId: defaultGatorPermissionsProviderSnapId, }; -}; -type ActionsObj = CreateActionsObj< - | 'fetchAndUpdateGatorPermissions' - | 'enableGatorPermissions' - | 'disableGatorPermissions' ->; +} + +// === MESSENGER === +/** + * The action which can be used to retrieve the state of the + * {@link GatorPermissionsController}. + */ export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< typeof controllerName, GatorPermissionsControllerState >; -// Messenger Actions -export type GatorPermissionsControllerActions = - | ActionsObj[keyof ActionsObj] - | GatorPermissionsControllerGetStateAction; +/** + * The action which can be used to fetch and update gator permissions. + */ +export type GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction = { + type: `${typeof controllerName}:fetchAndUpdateGatorPermissions`; + handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; +}; -export type GatorPermissionsControllerFetchAndUpdateGatorPermissions = - ActionsObj['fetchAndUpdateGatorPermissions']; +/** + * The action which can be used to enable gator permissions. + */ +export type GatorPermissionsControllerEnableGatorPermissionsAction = { + type: `${typeof controllerName}:enableGatorPermissions`; + handler: GatorPermissionsController['enableGatorPermissions']; +}; -export type GatorPermissionsControllerEnableGatorPermissions = - ActionsObj['enableGatorPermissions']; +/** + * The action which can be used to disable gator permissions. + */ +export type GatorPermissionsControllerDisableGatorPermissionsAction = { + type: `${typeof controllerName}:disableGatorPermissions`; + handler: GatorPermissionsController['disableGatorPermissions']; +}; -export type GatorPermissionsControllerDisableGatorPermissions = - ActionsObj['disableGatorPermissions']; +/** + * All actions that {@link GatorPermissionsController} registers, to be called + * externally. + */ +export type GatorPermissionsControllerActions = + | GatorPermissionsControllerGetStateAction + | GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction + | GatorPermissionsControllerEnableGatorPermissionsAction + | GatorPermissionsControllerDisableGatorPermissionsAction; /** - * Actions that this controller is allowed to call. + * All actions that {@link GatorPermissionsController} calls internally. + * + * SnapsController:handleRequest and SnapsController:has are allowed to be called + * internally because they are used to fetch gator permissions from the Snap. */ -export type AllowedActions = - // Snap Requests - HandleSnapRequest | HasSnap; +type AllowedActions = HandleSnapRequest | HasSnap; +/** + * The event that {@link GatorPermissionsController} publishes when updating state. + */ export type GatorPermissionsControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, @@ -133,15 +175,16 @@ export type GatorPermissionsControllerStateChangeEvent = >; /** - * Events emitted by GatorPermissionsController. + * All events that {@link GatorPermissionsController} publishes, to be subscribed to + * externally. */ -export type GatorPermissionsControllerActionsEvents = +export type GatorPermissionsControllerEvents = GatorPermissionsControllerStateChangeEvent; /** - * Events that this controller is allowed to subscribe to. + * Events that {@link GatorPermissionsController} is allowed to subscribe to internally. */ -export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; +type AllowedEvents = GatorPermissionsControllerStateChangeEvent; /** * Messenger type for the GatorPermissionsController. @@ -149,23 +192,11 @@ export type AllowedEvents = GatorPermissionsControllerStateChangeEvent; export type GatorPermissionsControllerMessenger = RestrictedMessenger< typeof controllerName, GatorPermissionsControllerActions | AllowedActions, - GatorPermissionsControllerActionsEvents | AllowedEvents, + GatorPermissionsControllerEvents | AllowedEvents, AllowedActions['type'], AllowedEvents['type'] >; -/** - * Configuration for the GatorPermissionsController. - * Default value is `{ gatorPermissionsProviderSnapId: '@metamask/gator-permissions-snap' }` - * when no config is provided. - */ -export type GatorPermissionsControllerConfig = { - /** - * The ID of the Snap of the gator permissions provider snap - */ - gatorPermissionsProviderSnapId: SnapId; -}; - /** * Controller that manages gator permissions by reading from profile sync */ @@ -174,38 +205,32 @@ export default class GatorPermissionsController extends BaseController< GatorPermissionsControllerState, GatorPermissionsControllerMessenger > { - private readonly gatorPermissionsProviderSnapId: SnapId; - /** * Creates a GatorPermissionsController instance. * * @param args - The arguments to this function. * @param args.messenger - Messenger used to communicate with BaseV2 controller. * @param args.state - Initial state to set on this controller. - * @param args.config - Configuration for the GatorPermissionsController. */ constructor({ messenger, state, - config, }: { messenger: GatorPermissionsControllerMessenger; state?: Partial; - config?: GatorPermissionsControllerConfig; }) { super({ - messenger, - metadata, name: controllerName, - state: { ...defaultState, ...state }, + metadata: gatorPermissionsControllerMetadata, + messenger, + state: { + ...getDefaultGatorPermissionsControllerState(), + ...state, + isFetchingGatorPermissions: false, + }, }); - this.gatorPermissionsProviderSnapId = - config?.gatorPermissionsProviderSnapId ?? - defaultGatorPermissionsProviderSnapId; - this.#registerMessageHandlers(); - this.#clearLoadingStates(); } #setIsFetchingGatorPermissions(isFetchingGatorPermissions: boolean) { @@ -237,12 +262,6 @@ export default class GatorPermissionsController extends BaseController< ); } - #clearLoadingStates(): void { - this.update((state) => { - state.isFetchingGatorPermissions = false; - }); - } - /** * Asserts that the gator permissions are enabled. * @@ -400,7 +419,7 @@ export default class GatorPermissionsController extends BaseController< * @returns The gator permissions provider snap id. */ get permissionsProviderSnapId(): SnapId { - return this.gatorPermissionsProviderSnapId; + return this.state.gatorPermissionsProviderSnapId; } /** @@ -435,7 +454,7 @@ export default class GatorPermissionsController extends BaseController< const permissionsData = await this.#handleSnapRequestToGatorPermissionsProvider({ - snapId: this.gatorPermissionsProviderSnapId, + snapId: this.state.gatorPermissionsProviderSnapId, }); const gatorPermissionsMap = diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 8cfd3c3ffb..8be1c11217 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -7,14 +7,33 @@ export type { GatorPermissionsControllerState, GatorPermissionsControllerMessenger, GatorPermissionsControllerGetStateAction, - GatorPermissionsControllerFetchAndUpdateGatorPermissions, - GatorPermissionsControllerEnableGatorPermissions, - GatorPermissionsControllerDisableGatorPermissions, + GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction, + GatorPermissionsControllerEnableGatorPermissionsAction, + GatorPermissionsControllerDisableGatorPermissionsAction, GatorPermissionsControllerActions, - AllowedActions, - GatorPermissionsControllerActionsEvents, - AllowedEvents, + GatorPermissionsControllerEvents, GatorPermissionsControllerStateChangeEvent, - GatorPermissionsControllerConfig, } from './GatorPermissionsController'; -export type * from './types'; +export type { + GatorPermissionsControllerErrorCode, + GatorPermissionsSnapRpcMethod, + MetaMaskBasePermissionData, + NativeTokenStreamPermission, + NativeTokenPeriodicPermission, + Erc20TokenStreamPermission, + Erc20TokenPeriodicPermission, + CustomPermission, + PermissionTypes, + AccountSigner, + WalletSigner, + SignerParam, + PermissionRequest, + PermissionResponse, + PermissionResponseSanitized, + StoredGatorPermission, + StoredGatorPermissionSanitized, + GatorPermissionsMap, + SupportedGatorPermissionType, + GatorPermissionsMapByPermissionType, + GatorPermissionsListByPermissionTypeAndChainId, +} from './types'; diff --git a/yarn.lock b/yarn.lock index 94fe8050b9..27dd6382fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3614,7 +3614,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/snaps-controllers": ^14.0.0 + "@metamask/snaps-controllers": ^14.0.1 languageName: unknown linkType: soft