diff --git a/packages/delegation-controller/CHANGELOG.md b/packages/delegation-controller/CHANGELOG.md index dcc70a44e49..8253fef4917 100644 --- a/packages/delegation-controller/CHANGELOG.md +++ b/packages/delegation-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export `DelegationControllerGetStateAction` type ([#8205](https://github.com/MetaMask/core/pull/8205)) + ## [2.0.2] ### Changed diff --git a/packages/delegation-controller/package.json b/packages/delegation-controller/package.json index 75dc9cb88b2..6d981f9047f 100644 --- a/packages/delegation-controller/package.json +++ b/packages/delegation-controller/package.json @@ -40,6 +40,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/delegation-controller", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/delegation-controller", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.ts", "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", @@ -60,6 +61,7 @@ "deepmerge": "^4.2.2", "jest": "^29.7.0", "ts-jest": "^29.2.5", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.3.3" diff --git a/packages/delegation-controller/src/DelegationController-method-action-types.ts b/packages/delegation-controller/src/DelegationController-method-action-types.ts new file mode 100644 index 00000000000..f773c95f731 --- /dev/null +++ b/packages/delegation-controller/src/DelegationController-method-action-types.ts @@ -0,0 +1,86 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { DelegationController } from './DelegationController'; + +/** + * Signs a delegation. + * + * @param params - The parameters for signing the delegation. + * @param params.delegation - The delegation to sign. + * @param params.chainId - The chainId of the chain to sign the delegation for. + * @returns The signature of the delegation. + */ +export type DelegationControllerSignDelegationAction = { + type: `DelegationController:signDelegation`; + handler: DelegationController['signDelegation']; +}; + +/** + * Stores a delegation in storage. + * + * @param params - The parameters for storing the delegation. + * @param params.entry - The delegation entry to store. + */ +export type DelegationControllerStoreAction = { + type: `DelegationController:store`; + handler: DelegationController['store']; +}; + +/** + * Lists delegation entries. + * + * @param filter - The filter to use to list the delegation entries. + * @returns A list of delegation entries that match the filter. + */ +export type DelegationControllerListAction = { + type: `DelegationController:list`; + handler: DelegationController['list']; +}; + +/** + * Retrieves the delegation entry for a given delegation hash. + * + * @param hash - The hash of the delegation to retrieve. + * @returns The delegation entry, or null if not found. + */ +export type DelegationControllerRetrieveAction = { + type: `DelegationController:retrieve`; + handler: DelegationController['retrieve']; +}; + +/** + * Retrieves a delegation chain from a delegation hash. + * + * @param hash - The hash of the delegation to retrieve. + * @returns The delegation chain, or null if not found. + */ +export type DelegationControllerChainAction = { + type: `DelegationController:chain`; + handler: DelegationController['chain']; +}; + +/** + * Deletes a delegation entrie from storage, along with any other entries + * that are redelegated from it. + * + * @param hash - The hash of the delegation to delete. + * @returns The number of entries deleted. + */ +export type DelegationControllerDeleteAction = { + type: `DelegationController:delete`; + handler: DelegationController['delete']; +}; + +/** + * Union of all DelegationController action types. + */ +export type DelegationControllerMethodActions = + | DelegationControllerSignDelegationAction + | DelegationControllerStoreAction + | DelegationControllerListAction + | DelegationControllerRetrieveAction + | DelegationControllerChainAction + | DelegationControllerDeleteAction; diff --git a/packages/delegation-controller/src/DelegationController.test.ts b/packages/delegation-controller/src/DelegationController.test.ts index 764a0140ef1..9ab505410c5 100644 --- a/packages/delegation-controller/src/DelegationController.test.ts +++ b/packages/delegation-controller/src/DelegationController.test.ts @@ -117,6 +117,7 @@ function createMessengerMock() { accountsControllerGetSelectedAccountMock, keyringControllerSignTypedMessageMock, messenger: delegationControllerMessenger, + rootMessenger: messenger, }; } @@ -154,7 +155,7 @@ function getDelegationEnvironmentMock(_chainId: Hex): DeleGatorEnvironment { * @returns The controller instance plus individual mock functions for each action. */ function createController(state?: DelegationControllerState) { - const { messenger, ...mocks } = createMessengerMock(); + const { messenger, rootMessenger, ...mocks } = createMessengerMock(); const controller = new TestDelegationController({ messenger, state, @@ -164,6 +165,7 @@ function createController(state?: DelegationControllerState) { return { controller, + rootMessenger, ...mocks, }; } @@ -184,13 +186,16 @@ describe(`${controllerName}`, () => { describe('sign', () => { it('signs a delegation message', async () => { - const { controller, keyringControllerSignTypedMessageMock } = + const { rootMessenger, keyringControllerSignTypedMessageMock } = createController(); - const signature = await controller.signDelegation({ - delegation: DELEGATION_MOCK, - chainId: CHAIN_ID_MOCK, - }); + const signature = await rootMessenger.call( + 'DelegationController:signDelegation', + { + delegation: DELEGATION_MOCK, + chainId: CHAIN_ID_MOCK, + }, + ); expect(signature).toBe(SIGNATURE_HASH_MOCK); expect(keyringControllerSignTypedMessageMock).toHaveBeenCalledWith( @@ -213,14 +218,14 @@ describe(`${controllerName}`, () => { }); it('throws if signature fails', async () => { - const { controller, keyringControllerSignTypedMessageMock } = + const { rootMessenger, keyringControllerSignTypedMessageMock } = createController(); keyringControllerSignTypedMessageMock.mockRejectedValue( new Error('Signature failed'), ); await expect( - controller.signDelegation({ + rootMessenger.call('DelegationController:signDelegation', { delegation: { ...DELEGATION_MOCK, salt: '0x1' as Hex, @@ -233,10 +238,12 @@ describe(`${controllerName}`, () => { describe('store', () => { it('stores a delegation entry in state', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); const hash = hashDelegationMock(DELEGATION_ENTRY_MOCK.delegation); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); expect(controller.state.delegations[hash]).toStrictEqual( DELEGATION_ENTRY_MOCK, @@ -244,15 +251,17 @@ describe(`${controllerName}`, () => { }); it('overwrites existing delegation with same hash', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); const hash = hashDelegationMock(DELEGATION_ENTRY_MOCK.delegation); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); const updatedEntry = { ...DELEGATION_ENTRY_MOCK, tags: ['test-tag'], }; - controller.store({ entry: updatedEntry }); + rootMessenger.call('DelegationController:store', { entry: updatedEntry }); expect(controller.state.delegations[hash]).toStrictEqual(updatedEntry); }); @@ -260,80 +269,98 @@ describe(`${controllerName}`, () => { describe('list', () => { it('lists all delegations for the requester as delegate', () => { - const { controller } = createController(); - controller.store({ + const { rootMessenger } = createController(); + rootMessenger.call('DelegationController:store', { entry: DELEGATION_ENTRY_MOCK, }); - const result = controller.list(); + const result = rootMessenger.call('DelegationController:list'); expect(result).toHaveLength(1); expect(result[0]).toStrictEqual(DELEGATION_ENTRY_MOCK); }); it('filters delegations by from address', () => { - const { controller } = createController(); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + const { rootMessenger } = createController(); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); - const result = controller.list({ from: DELEGATION_MOCK.delegator }); + const result = rootMessenger.call('DelegationController:list', { + from: DELEGATION_MOCK.delegator, + }); expect(result).toHaveLength(1); expect(result[0].delegation.delegator).toBe(DELEGATION_MOCK.delegator); }); it('filters delegations by chainId', () => { - const { controller } = createController(); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + const { rootMessenger } = createController(); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); - const result = controller.list({ chainId: CHAIN_ID_MOCK }); + const result = rootMessenger.call('DelegationController:list', { + chainId: CHAIN_ID_MOCK, + }); expect(result).toHaveLength(1); expect(result[0].chainId).toBe(CHAIN_ID_MOCK); }); it('filters delegations by tags', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const entryWithTags = { ...DELEGATION_ENTRY_MOCK, tags: ['test-tag'], }; - controller.store({ + rootMessenger.call('DelegationController:store', { entry: entryWithTags, }); - const result = controller.list({ tags: ['test-tag'] }); + const result = rootMessenger.call('DelegationController:list', { + tags: ['test-tag'], + }); expect(result).toHaveLength(1); expect(result[0].tags).toContain('test-tag'); }); it('only filters entries that contain all of the filter tags', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const entryWithTags = { ...DELEGATION_ENTRY_MOCK, tags: ['test-tag', 'test-tag-1'], }; - controller.store({ entry: entryWithTags }); + rootMessenger.call('DelegationController:store', { + entry: entryWithTags, + }); - const result = controller.list({ tags: ['test-tag', 'test-tag-2'] }); + const result = rootMessenger.call('DelegationController:list', { + tags: ['test-tag', 'test-tag-2'], + }); expect(result).toHaveLength(0); - const result2 = controller.list({ tags: ['test-tag', 'test-tag-1'] }); + const result2 = rootMessenger.call('DelegationController:list', { + tags: ['test-tag', 'test-tag-1'], + }); expect(result2).toHaveLength(1); expect(result2[0].tags).toContain('test-tag'); expect(result2[0].tags).toContain('test-tag-1'); }); it('combines multiple filters', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const entryWithTags = { ...DELEGATION_ENTRY_MOCK, tags: ['test-tag'], }; - controller.store({ entry: entryWithTags }); + rootMessenger.call('DelegationController:store', { + entry: entryWithTags, + }); - const result = controller.list({ + const result = rootMessenger.call('DelegationController:list', { from: DELEGATION_MOCK.delegator, chainId: CHAIN_ID_MOCK, tags: ['test-tag'], @@ -346,7 +373,7 @@ describe(`${controllerName}`, () => { }); it('filters delegations by from address when requester is not the delegator', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const otherDelegation = { ...DELEGATION_MOCK, delegator: '0x9234567890123456789012345678901234567890' as Address, @@ -355,30 +382,40 @@ describe(`${controllerName}`, () => { ...DELEGATION_ENTRY_MOCK, delegation: otherDelegation, }; - controller.store({ entry: DELEGATION_ENTRY_MOCK }); - controller.store({ entry: otherEntry }); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); + rootMessenger.call('DelegationController:store', { entry: otherEntry }); - const result = controller.list({ from: otherDelegation.delegator }); + const result = rootMessenger.call('DelegationController:list', { + from: otherDelegation.delegator, + }); expect(result).toHaveLength(1); expect(result[0].delegation.delegator).toBe(otherDelegation.delegator); }); it('filters delegations by from address when requester is the delegator', () => { - const { controller } = createController(); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + const { rootMessenger } = createController(); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); - const result = controller.list({ from: DELEGATION_MOCK.delegator }); + const result = rootMessenger.call('DelegationController:list', { + from: DELEGATION_MOCK.delegator, + }); expect(result).toHaveLength(1); expect(result[0].delegation.delegator).toBe(DELEGATION_MOCK.delegator); }); it('returns empty array when no delegations match filter', () => { - const { controller } = createController(); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + const { rootMessenger } = createController(); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); - const result = controller.list({ + const result = rootMessenger.call('DelegationController:list', { from: '0x9234567890123456789012345678901234567890' as Address, chainId: CHAIN_ID_MOCK, tags: ['non-existent-tag'], @@ -390,20 +427,25 @@ describe(`${controllerName}`, () => { describe('retrieve', () => { it('retrieves delegation by hash', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const hash = hashDelegationMock(DELEGATION_ENTRY_MOCK.delegation); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); - const result = controller.retrieve(hash); + const result = rootMessenger.call('DelegationController:retrieve', hash); expect(result).toStrictEqual(DELEGATION_ENTRY_MOCK); }); it('returns null if hash not found', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); - const result = controller.retrieve('0x123' as Hex); + const result = rootMessenger.call( + 'DelegationController:retrieve', + '0x123' as Hex, + ); expect(result).toBeNull(); }); @@ -411,7 +453,7 @@ describe(`${controllerName}`, () => { describe('chain', () => { it('retrieves delegation chain from hash', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const parentDelegation = { ...DELEGATION_MOCK, authority: ROOT_AUTHORITY, @@ -430,10 +472,13 @@ describe(`${controllerName}`, () => { ...DELEGATION_ENTRY_MOCK, delegation: childDelegation, }; - controller.store({ entry: parentEntry }); - controller.store({ entry: childEntry }); + rootMessenger.call('DelegationController:store', { entry: parentEntry }); + rootMessenger.call('DelegationController:store', { entry: childEntry }); - const result = controller.chain(childHash); + const result = rootMessenger.call( + 'DelegationController:chain', + childHash, + ); expect(result).toHaveLength(2); expect(result?.[0]).toStrictEqual(childEntry); @@ -441,9 +486,10 @@ describe(`${controllerName}`, () => { }); it('returns null if hash not found', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); - const result = controller.chain( + const result = rootMessenger.call( + 'DelegationController:chain', '0x1234567890123456789012345678901234567890123456789012345678901234' as Hex, ); @@ -465,13 +511,15 @@ describe(`${controllerName}`, () => { [hash]: invalidEntry, }, }; - const { controller } = createController(invalidState); + const { rootMessenger } = createController(invalidState); - expect(() => controller.chain(hash)).toThrow('Invalid delegation chain'); + expect(() => + rootMessenger.call('DelegationController:chain', hash), + ).toThrow('Invalid delegation chain'); }); it('returns null for root authority', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const rootDelegation = { ...DELEGATION_MOCK, authority: ROOT_AUTHORITY, @@ -480,9 +528,12 @@ describe(`${controllerName}`, () => { ...DELEGATION_ENTRY_MOCK, delegation: rootDelegation, }; - controller.store({ entry: rootEntry }); + rootMessenger.call('DelegationController:store', { entry: rootEntry }); - const result = controller.chain(ROOT_AUTHORITY); + const result = rootMessenger.call( + 'DelegationController:chain', + ROOT_AUTHORITY, + ); expect(result).toBeNull(); }); @@ -490,19 +541,21 @@ describe(`${controllerName}`, () => { describe('delete', () => { it('deletes delegation by hash', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); const hash = hashDelegationMock(DELEGATION_ENTRY_MOCK.delegation); - controller.store({ entry: DELEGATION_ENTRY_MOCK }); + rootMessenger.call('DelegationController:store', { + entry: DELEGATION_ENTRY_MOCK, + }); - const count = controller.delete(hash); + const count = rootMessenger.call('DelegationController:delete', hash); expect(count).toBe(1); expect(controller.state.delegations[hash]).toBeUndefined(); }); it('deletes delegation chain', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); const parentDelegation = { ...DELEGATION_MOCK, authority: ROOT_AUTHORITY, @@ -521,10 +574,13 @@ describe(`${controllerName}`, () => { ...DELEGATION_ENTRY_MOCK, delegation: childDelegation, }; - controller.store({ entry: parentEntry }); - controller.store({ entry: childEntry }); + rootMessenger.call('DelegationController:store', { entry: parentEntry }); + rootMessenger.call('DelegationController:store', { entry: childEntry }); - const count = controller.delete(parentHash); + const count = rootMessenger.call( + 'DelegationController:delete', + parentHash, + ); expect(count).toBe(2); expect(controller.state.delegations[childHash]).toBeUndefined(); @@ -532,7 +588,7 @@ describe(`${controllerName}`, () => { }); it('deletes delegation chain with multiple children', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); const parentDelegation = { ...DELEGATION_MOCK, authority: ROOT_AUTHORITY, @@ -562,11 +618,14 @@ describe(`${controllerName}`, () => { ...DELEGATION_ENTRY_MOCK, delegation: child2Delegation, }; - controller.store({ entry: parentEntry }); - controller.store({ entry: child1Entry }); - controller.store({ entry: child2Entry }); + rootMessenger.call('DelegationController:store', { entry: parentEntry }); + rootMessenger.call('DelegationController:store', { entry: child1Entry }); + rootMessenger.call('DelegationController:store', { entry: child2Entry }); - const count = controller.delete(parentHash); + const count = rootMessenger.call( + 'DelegationController:delete', + parentHash, + ); expect(count).toBe(3); expect(controller.state.delegations[parentHash]).toBeUndefined(); @@ -575,13 +634,16 @@ describe(`${controllerName}`, () => { }); it('returns 0 when trying to delete non-existent delegation', () => { - const { controller } = createController(); - const count = controller.delete('0x123' as Hex); + const { rootMessenger } = createController(); + const count = rootMessenger.call( + 'DelegationController:delete', + '0x123' as Hex, + ); expect(count).toBe(0); }); it('deletes delegation with complex chain structure', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); // Create a chain: root -> parent -> child1 -> grandchild1 // -> child2 -> grandchild2 const rootDelegation = { @@ -646,14 +708,21 @@ describe(`${controllerName}`, () => { delegation: grandchild2Delegation, }; - controller.store({ entry: rootEntry }); - controller.store({ entry: parentEntry }); - controller.store({ entry: child1Entry }); - controller.store({ entry: child2Entry }); - controller.store({ entry: grandchild1Entry }); - controller.store({ entry: grandchild2Entry }); + rootMessenger.call('DelegationController:store', { entry: rootEntry }); + rootMessenger.call('DelegationController:store', { entry: parentEntry }); + rootMessenger.call('DelegationController:store', { entry: child1Entry }); + rootMessenger.call('DelegationController:store', { entry: child2Entry }); + rootMessenger.call('DelegationController:store', { + entry: grandchild1Entry, + }); + rootMessenger.call('DelegationController:store', { + entry: grandchild2Entry, + }); - const count = controller.delete(parentHash); + const count = rootMessenger.call( + 'DelegationController:delete', + parentHash, + ); expect(count).toBe(5); // parent + 2 children + 2 grandchildren expect(controller.state.delegations[rootHash]).toBeDefined(); @@ -665,19 +734,22 @@ describe(`${controllerName}`, () => { }); it('handles empty nextHashes array gracefully', () => { - const { controller } = createController(); + const { controller, rootMessenger } = createController(); // Mock the state to have an empty delegations object controller.testUpdate((state) => { state.delegations = {}; }); // This should not throw and should return 0 - const count = controller.delete('0x123' as Hex); + const count = rootMessenger.call( + 'DelegationController:delete', + '0x123' as Hex, + ); expect(count).toBe(0); }); it('throws if the authority is invalid', () => { - const { controller } = createController(); + const { rootMessenger } = createController(); const invalidDelegation = { ...DELEGATION_MOCK, authority: '0x1234567890123456789012345678901234567890' as Hex, @@ -687,9 +759,11 @@ describe(`${controllerName}`, () => { delegation: invalidDelegation, }; - expect(() => controller.store({ entry: invalidEntry })).toThrow( - 'Invalid authority', - ); + expect(() => + rootMessenger.call('DelegationController:store', { + entry: invalidEntry, + }), + ).toThrow('Invalid authority'); }); }); diff --git a/packages/delegation-controller/src/DelegationController.ts b/packages/delegation-controller/src/DelegationController.ts index 856786adced..38952d2aa78 100644 --- a/packages/delegation-controller/src/DelegationController.ts +++ b/packages/delegation-controller/src/DelegationController.ts @@ -19,6 +19,15 @@ import { createTypedMessageParams, isHexEqual } from './utils'; export const controllerName = 'DelegationController'; +const MESSENGER_EXPOSED_METHODS = [ + 'signDelegation', + 'store', + 'list', + 'retrieve', + 'chain', + 'delete', +] as const; + const delegationControllerMetadata = { delegations: { includeInStateLogs: false, @@ -86,6 +95,11 @@ export class DelegationController extends BaseController< }); this.#hashDelegation = hashDelegation; this.#getDelegationEnvironment = getDelegationEnvironment; + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); } /** diff --git a/packages/delegation-controller/src/index.ts b/packages/delegation-controller/src/index.ts index 401a847c3bb..fd977f80220 100644 --- a/packages/delegation-controller/src/index.ts +++ b/packages/delegation-controller/src/index.ts @@ -5,6 +5,9 @@ export type { DelegationControllerRetrieveAction, DelegationControllerChainAction, DelegationControllerDeleteAction, +} from './DelegationController-method-action-types'; +export type { + DelegationControllerGetStateAction, DelegationControllerActions, DelegationControllerEvents, DelegationControllerMessenger, diff --git a/packages/delegation-controller/src/types.ts b/packages/delegation-controller/src/types.ts index 5d8a3f6bfbd..3d154bf45aa 100644 --- a/packages/delegation-controller/src/types.ts +++ b/packages/delegation-controller/src/types.ts @@ -6,10 +6,8 @@ import type { import type { KeyringControllerSignTypedMessageAction } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; -import type { - controllerName, - DelegationController, -} from './DelegationController'; +import type { controllerName } from './DelegationController'; +import type { DelegationControllerMethodActions } from './DelegationController-method-action-types'; type Hex = `0x${string}`; type Address = `0x${string}`; @@ -97,44 +95,9 @@ export type DelegationControllerGetStateAction = ControllerGetStateAction< DelegationControllerState >; -export type DelegationControllerSignDelegationAction = { - type: `${typeof controllerName}:signDelegation`; - handler: DelegationController['signDelegation']; -}; - -export type DelegationControllerStoreAction = { - type: `${typeof controllerName}:store`; - handler: DelegationController['store']; -}; - -export type DelegationControllerListAction = { - type: `${typeof controllerName}:list`; - handler: DelegationController['list']; -}; - -export type DelegationControllerRetrieveAction = { - type: `${typeof controllerName}:retrieve`; - handler: DelegationController['retrieve']; -}; - -export type DelegationControllerChainAction = { - type: `${typeof controllerName}:chain`; - handler: DelegationController['chain']; -}; - -export type DelegationControllerDeleteAction = { - type: `${typeof controllerName}:delete`; - handler: DelegationController['delete']; -}; - export type DelegationControllerActions = | DelegationControllerGetStateAction - | DelegationControllerSignDelegationAction - | DelegationControllerStoreAction - | DelegationControllerListAction - | DelegationControllerRetrieveAction - | DelegationControllerChainAction - | DelegationControllerDeleteAction; + | DelegationControllerMethodActions; export type DelegationControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 85931971b7b..9762db269c1 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Expose missing public `GatorPermissionsController` methods through its messenger ([#8205](https://github.com/MetaMask/core/pull/8205)) + - The following actions are now available: + - `GatorPermissionsController:initialize` + - Corresponding action types (e.g. `GatorPermissionsControllerInitializeAction`) are available as well. + ## [2.1.1] ### Changed diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index f1a84599919..972b325f67a 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -40,6 +40,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/gator-permissions-controller", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/gator-permissions-controller", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.ts", "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", @@ -67,6 +68,7 @@ "deepmerge": "^4.2.2", "jest": "^29.7.0", "ts-jest": "^29.2.5", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.3.3" diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController-method-action-types.ts b/packages/gator-permissions-controller/src/GatorPermissionsController-method-action-types.ts new file mode 100644 index 00000000000..ed9b6e4e561 --- /dev/null +++ b/packages/gator-permissions-controller/src/GatorPermissionsController-method-action-types.ts @@ -0,0 +1,132 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { GatorPermissionsController } from './GatorPermissionsController'; + +/** + * Fetches granted permissions from the gator permissions provider Snap and updates state. + * If a sync is already in progress, returns the same promise. After the sync completes, + * the next call will perform a new sync. + * + * @returns A promise that resolves when the sync completes. All data is available via the controller's state. + * @throws {GatorPermissionsFetchError} If the gator permissions fetch fails. + */ +export type GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction = { + type: `GatorPermissionsController:fetchAndUpdateGatorPermissions`; + handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; +}; + +/** + * Initializes the controller. Call once after construction to ensure the + * controller is ready for use. + * + * @returns A promise that resolves when initialization is complete. + */ +export type GatorPermissionsControllerInitializeAction = { + type: `GatorPermissionsController:initialize`; + handler: GatorPermissionsController['initialize']; +}; + +/** + * Decodes a permission context into a structured permission for a specific origin. + * + * This method validates the caller origin, decodes the provided `permissionContext` + * into delegations, identifies the permission type from the caveat enforcers, + * extracts the permission-specific data and expiry, and reconstructs a + * {@link DecodedPermission} containing chainId, account addresses, to, type and data. + * + * @param args - The arguments to this function. + * @param args.origin - The caller's origin; must match the configured permissions provider Snap id. + * @param args.chainId - Numeric EIP-155 chain id used for resolving enforcer contracts and encoding. + * @param args.delegation - delegation representing the permission. + * @param args.metadata - metadata included in the request. + * @param args.metadata.justification - the justification as specified in the request metadata. + * @param args.metadata.origin - the origin as specified in the request metadata. + * + * @returns A decoded permission object suitable for UI consumption and follow-up actions. + * @throws If the origin is not allowed, the context cannot be decoded into exactly one delegation, + * or the enforcers/terms do not match a supported permission type. + */ +export type GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction = + { + type: `GatorPermissionsController:decodePermissionFromPermissionContextForOrigin`; + handler: GatorPermissionsController['decodePermissionFromPermissionContextForOrigin']; + }; + +/** + * Submits a revocation to the gator permissions provider snap. + * + * @param revocationParams - The revocation parameters containing the permission context. + * @returns A promise that resolves when the revocation is submitted successfully. + * @throws {GatorPermissionsProviderError} If the snap request fails. + */ +export type GatorPermissionsControllerSubmitRevocationAction = { + type: `GatorPermissionsController:submitRevocation`; + handler: GatorPermissionsController['submitRevocation']; +}; + +/** + * Adds a pending revocation that will be submitted once the transaction is confirmed. + * + * This method sets up listeners for the user's approval/rejection decision and + * terminal transaction states (confirmed, failed, dropped). The flow is: + * 1. Wait for user to approve or reject the transaction + * 2. If approved, add to pending revocations state + * 3. If rejected, cleanup without adding to state + * 4. If confirmed, submit the revocation + * 5. If failed or dropped, cleanup + * + * Includes a timeout safety net to prevent memory leaks if the transaction never + * reaches a terminal state. + * + * @param params - The pending revocation parameters. + * @returns A promise that resolves when the listener is set up. + */ +export type GatorPermissionsControllerAddPendingRevocationAction = { + type: `GatorPermissionsController:addPendingRevocation`; + handler: GatorPermissionsController['addPendingRevocation']; +}; + +/** + * Submits a revocation directly without requiring an on-chain transaction. + * Used for already-disabled delegations that don't require an on-chain transaction. + * + * This method: + * 1. Adds the permission context to pending revocations state (disables UI button) + * 2. Immediately calls submitRevocation to remove from snap storage + * 3. On success, removes from pending revocations state (re-enables UI button) + * 4. On failure, keeps in pending revocations so UI can show error/retry state + * + * @param params - The revocation parameters containing the permission context. + * @returns A promise that resolves when the revocation is submitted successfully. + * @throws {GatorPermissionsProviderError} If the snap request fails. + */ +export type GatorPermissionsControllerSubmitDirectRevocationAction = { + type: `GatorPermissionsController:submitDirectRevocation`; + handler: GatorPermissionsController['submitDirectRevocation']; +}; + +/** + * Checks if a permission context is in the pending revocations list. + * + * @param permissionContext - The permission context to check. + * @returns `true` if the permission context is pending revocation, `false` otherwise. + */ +export type GatorPermissionsControllerIsPendingRevocationAction = { + type: `GatorPermissionsController:isPendingRevocation`; + handler: GatorPermissionsController['isPendingRevocation']; +}; + +/** + * Union of all GatorPermissionsController action types. + */ +export type GatorPermissionsControllerMethodActions = + | GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction + | GatorPermissionsControllerInitializeAction + | GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction + | GatorPermissionsControllerSubmitRevocationAction + | GatorPermissionsControllerAddPendingRevocationAction + | GatorPermissionsControllerSubmitDirectRevocationAction + | GatorPermissionsControllerIsPendingRevocationAction; diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts index 8d443e19a19..917b6e4441d 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.test.ts @@ -24,7 +24,7 @@ import type { Hex } from '@metamask/utils'; import { DELEGATION_FRAMEWORK_VERSION } from './constants'; import { GatorPermissionsFetchError } from './errors'; import type { GatorPermissionsControllerMessenger } from './GatorPermissionsController'; -import GatorPermissionsController from './GatorPermissionsController'; +import { GatorPermissionsController } from './GatorPermissionsController'; import type { PermissionInfoWithMetadata, StoredGatorPermission, @@ -199,7 +199,9 @@ describe('GatorPermissionsController', () => { config: DEFAULT_TEST_CONFIG, }); - await controller.fetchAndUpdateGatorPermissions(); + await rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); const { grantedPermissions } = controller.state; expect(Array.isArray(grantedPermissions)).toBe(true); @@ -270,7 +272,9 @@ describe('GatorPermissionsController', () => { config: DEFAULT_TEST_CONFIG, }); - await controller.fetchAndUpdateGatorPermissions(); + await rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); const { grantedPermissions } = controller.state; expect(grantedPermissions).toHaveLength(1); @@ -290,7 +294,9 @@ describe('GatorPermissionsController', () => { config: DEFAULT_TEST_CONFIG, }); - await controller.fetchAndUpdateGatorPermissions(); + await rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); expect(controller.state.grantedPermissions).toStrictEqual([]); }); @@ -305,7 +311,9 @@ describe('GatorPermissionsController', () => { config: DEFAULT_TEST_CONFIG, }); - await controller.fetchAndUpdateGatorPermissions(); + await rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); expect(controller.state.grantedPermissions).toStrictEqual([]); }); @@ -322,9 +330,11 @@ describe('GatorPermissionsController', () => { config: DEFAULT_TEST_CONFIG, }); - await expect(controller.fetchAndUpdateGatorPermissions()).rejects.toThrow( - 'Failed to fetch gator permissions', - ); + await expect( + rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ), + ).rejects.toThrow('Failed to fetch gator permissions'); expect(controller.state.isFetchingGatorPermissions).toBe(false); expect(controller.state.lastSyncedTimestamp).toBe(-1); @@ -345,13 +355,18 @@ describe('GatorPermissionsController', () => { snapControllerHandleRequestActionHandler: mockHandleRequestHandler, }); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger: getGatorPermissionsControllerMessenger(rootMessenger), config: DEFAULT_TEST_CONFIG, }); - const promise1 = controller.fetchAndUpdateGatorPermissions(); - const promise2 = controller.fetchAndUpdateGatorPermissions(); + const promise1 = rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); + const promise2 = rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); expect(promise1).toBe(promise2); @@ -367,15 +382,20 @@ describe('GatorPermissionsController', () => { snapControllerHandleRequestActionHandler: mockHandleRequestHandler, }); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger: getGatorPermissionsControllerMessenger(rootMessenger), config: DEFAULT_TEST_CONFIG, }); - await controller.fetchAndUpdateGatorPermissions(); + await rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); expect(mockHandleRequestHandler).toHaveBeenCalledTimes(1); - await controller.fetchAndUpdateGatorPermissions(); + await rootMessenger.call( + 'GatorPermissionsController:fetchAndUpdateGatorPermissions', + ); expect(mockHandleRequestHandler).toHaveBeenCalledTimes(2); }); }); @@ -396,7 +416,7 @@ describe('GatorPermissionsController', () => { expect(controller.state.lastSyncedTimestamp).toBe(-1); - await controller.initialize(); + await rootMessenger.call('GatorPermissionsController:initialize'); expect(mockHandleRequestHandler).toHaveBeenCalledTimes(1); expect(controller.state.lastSyncedTimestamp).not.toBe(-1); @@ -418,7 +438,7 @@ describe('GatorPermissionsController', () => { state: { lastSyncedTimestamp: recentTimestamp }, }); - await controller.initialize(); + await rootMessenger.call('GatorPermissionsController:initialize'); expect(mockHandleRequestHandler).not.toHaveBeenCalled(); expect(controller.state.lastSyncedTimestamp).toBe(recentTimestamp); @@ -440,7 +460,7 @@ describe('GatorPermissionsController', () => { state: { lastSyncedTimestamp: staleTimestamp }, }); - await controller.initialize(); + await rootMessenger.call('GatorPermissionsController:initialize'); expect(mockHandleRequestHandler).toHaveBeenCalledTimes(1); expect(controller.state.lastSyncedTimestamp).not.toBe(staleTimestamp); @@ -465,7 +485,7 @@ describe('GatorPermissionsController', () => { state: { lastSyncedTimestamp: lastSyncedTwoSecondsAgo }, }); - await controller.initialize(); + await rootMessenger.call('GatorPermissionsController:initialize'); expect(mockHandleRequestHandler).toHaveBeenCalledTimes(1); expect(controller.state.lastSyncedTimestamp).not.toBe( @@ -594,27 +614,32 @@ describe('GatorPermissionsController', () => { }); let controller: GatorPermissionsController; + let rootMessenger: RootMessenger; beforeEach(() => { + rootMessenger = getRootMessenger(); controller = new GatorPermissionsController({ - messenger: getGatorPermissionsControllerMessenger(), + messenger: getGatorPermissionsControllerMessenger(rootMessenger), config: DEFAULT_TEST_CONFIG, }); }); it('throws if contracts are not found', () => { expect(() => - controller.decodePermissionFromPermissionContextForOrigin({ - origin: controller.gatorPermissionsProviderSnapId, - chainId: 999999, - delegation: { - caveats: [], - delegator: '0x1111111111111111111111111111111111111111', - delegate: '0x2222222222222222222222222222222222222222', - authority: ROOT_AUTHORITY as Hex, + rootMessenger.call( + 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin', + { + origin: controller.gatorPermissionsProviderSnapId, + chainId: 999999, + delegation: { + caveats: [], + delegator: '0x1111111111111111111111111111111111111111', + delegate: '0x2222222222222222222222222222222222222222', + authority: ROOT_AUTHORITY as Hex, + }, + metadata: buildMetadata(''), }, - metadata: buildMetadata(''), - }), + ), ).toThrow('Contracts not found for chainId: 999999'); }); @@ -666,12 +691,15 @@ describe('GatorPermissionsController', () => { caveats, }; - const result = controller.decodePermissionFromPermissionContextForOrigin({ - origin: controller.gatorPermissionsProviderSnapId, - chainId, - delegation, - metadata: buildMetadata('Test justification'), - }); + const result = rootMessenger.call( + 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin', + { + origin: controller.gatorPermissionsProviderSnapId, + chainId, + delegation, + metadata: buildMetadata('Test justification'), + }, + ); expect(result.chainId).toBe(numberToHex(chainId)); expect(result.from).toBe(delegator); @@ -693,17 +721,20 @@ describe('GatorPermissionsController', () => { it('throws when origin does not match permissions provider', () => { expect(() => - controller.decodePermissionFromPermissionContextForOrigin({ - origin: 'not-the-provider', - chainId: 1, - delegation: { - delegate: '0x1', - delegator: '0x2', - authority: ROOT_AUTHORITY as Hex, - caveats: [], + rootMessenger.call( + 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin', + { + origin: 'not-the-provider', + chainId: 1, + delegation: { + delegate: '0x1', + delegator: '0x2', + authority: ROOT_AUTHORITY as Hex, + caveats: [], + }, + metadata: buildMetadata(''), }, - metadata: buildMetadata(''), - }), + ), ).toThrow('Origin not-the-provider not allowed'); }); @@ -726,17 +757,20 @@ describe('GatorPermissionsController', () => { ]; expect(() => - controller.decodePermissionFromPermissionContextForOrigin({ - origin: controller.gatorPermissionsProviderSnapId, - chainId, - delegation: { - delegate: delegatorAddressA, - delegator: delegateAddressB, - authority: ROOT_AUTHORITY as Hex, - caveats, + rootMessenger.call( + 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin', + { + origin: controller.gatorPermissionsProviderSnapId, + chainId, + delegation: { + delegate: delegatorAddressA, + delegator: delegateAddressB, + authority: ROOT_AUTHORITY as Hex, + caveats, + }, + metadata: buildMetadata(''), }, - metadata: buildMetadata(''), - }), + ), ).toThrow('Failed to decode permission'); }); @@ -771,17 +805,20 @@ describe('GatorPermissionsController', () => { ]; expect(() => - controller.decodePermissionFromPermissionContextForOrigin({ - origin: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, - chainId, - delegation: { - delegate: delegatorAddressA, - delegator: delegateAddressB, - authority: ROOT_AUTHORITY as Hex, - caveats, + rootMessenger.call( + 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin', + { + origin: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, + chainId, + delegation: { + delegate: delegatorAddressA, + delegator: delegateAddressB, + authority: ROOT_AUTHORITY as Hex, + caveats, + }, + metadata: buildMetadata(''), }, - metadata: buildMetadata(''), - }), + ), ).toThrow('Failed to decode permission'); }); @@ -830,17 +867,20 @@ describe('GatorPermissionsController', () => { '0x0000000000000000000000000000000000000000' as Hex; expect(() => - controller.decodePermissionFromPermissionContextForOrigin({ - origin: controller.gatorPermissionsProviderSnapId, - chainId, - delegation: { - delegate, - delegator, - authority: invalidAuthority, - caveats, + rootMessenger.call( + 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin', + { + origin: controller.gatorPermissionsProviderSnapId, + chainId, + delegation: { + delegate, + delegator, + authority: invalidAuthority, + caveats, + }, + metadata: buildMetadata(''), }, - metadata: buildMetadata(''), - }), + ), ).toThrow('Failed to decode permission'); }); }); @@ -848,11 +888,10 @@ describe('GatorPermissionsController', () => { describe('submitRevocation', () => { it('should successfully submit a revocation when gator permissions are enabled', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); const controller = new GatorPermissionsController({ messenger, @@ -876,7 +915,10 @@ describe('GatorPermissionsController', () => { txHash: undefined, }; - await controller.submitRevocation(revocationParams); + await rootMessenger.call( + 'GatorPermissionsController:submitRevocation', + revocationParams, + ); expect(mockHandleRequestHandler).toHaveBeenCalledWith({ snapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID, @@ -893,12 +935,12 @@ describe('GatorPermissionsController', () => { it('should submit revocation when controller is configured', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); - const controller = new GatorPermissionsController({ + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: DEFAULT_TEST_CONFIG, }); @@ -909,7 +951,10 @@ describe('GatorPermissionsController', () => { }; expect( - await controller.submitRevocation(revocationParams), + await rootMessenger.call( + 'GatorPermissionsController:submitRevocation', + revocationParams, + ), ).toBeUndefined(); }); @@ -917,13 +962,13 @@ describe('GatorPermissionsController', () => { const mockHandleRequestHandler = jest .fn() .mockRejectedValue(new Error('Snap request failed')); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -938,7 +983,10 @@ describe('GatorPermissionsController', () => { }; await expect( - controller.submitRevocation(revocationParams), + rootMessenger.call( + 'GatorPermissionsController:submitRevocation', + revocationParams, + ), ).rejects.toThrow( 'Failed to handle snap request to gator permissions provider for method permissionsProvider_submitRevocation', ); @@ -946,11 +994,10 @@ describe('GatorPermissionsController', () => { it('should clear pending revocation in finally block even if refresh fails', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); const controller = new GatorPermissionsController({ messenger, @@ -987,12 +1034,18 @@ describe('GatorPermissionsController', () => { // Should throw GatorPermissionsFetchError (not GatorPermissionsProviderError) // because revocation succeeded but refresh failed await expect( - controller.submitRevocation(revocationParams), + rootMessenger.call( + 'GatorPermissionsController:submitRevocation', + revocationParams, + ), ).rejects.toThrow(GatorPermissionsFetchError); // Verify the error message indicates refresh failure, not revocation failure await expect( - controller.submitRevocation(revocationParams), + rootMessenger.call( + 'GatorPermissionsController:submitRevocation', + revocationParams, + ), ).rejects.toThrow( 'Failed to refresh permissions list after successful revocation', ); @@ -1005,11 +1058,10 @@ describe('GatorPermissionsController', () => { describe('submitDirectRevocation', () => { it('should add to pending revocations and immediately submit revocation', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); const controller = new GatorPermissionsController({ messenger, @@ -1025,7 +1077,10 @@ describe('GatorPermissionsController', () => { txHash: undefined, }; - await controller.submitDirectRevocation(revocationParams); + await rootMessenger.call( + 'GatorPermissionsController:submitDirectRevocation', + revocationParams, + ); // Should have called submitRevocation expect(mockHandleRequestHandler).toHaveBeenCalledWith({ @@ -1045,11 +1100,10 @@ describe('GatorPermissionsController', () => { it('should add pending revocation with placeholder txId', async () => { const mockHandleRequestHandler = jest.fn().mockResolvedValue(undefined); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); const controller = new GatorPermissionsController({ messenger, @@ -1070,7 +1124,10 @@ describe('GatorPermissionsController', () => { // Spy on submitRevocation to check pending state before it's called const submitRevocationSpy = jest.spyOn(controller, 'submitRevocation'); - await controller.submitDirectRevocation(revocationParams); + await rootMessenger.call( + 'GatorPermissionsController:submitDirectRevocation', + revocationParams, + ); // Verify that pending revocation was added (before submitRevocation clears it) // We check by verifying submitRevocation was called, which clears pending @@ -1082,11 +1139,10 @@ describe('GatorPermissionsController', () => { const mockHandleRequestHandler = jest .fn() .mockRejectedValue(new Error('Snap request failed')); - const messenger = getMessenger( - getRootMessenger({ - snapControllerHandleRequestActionHandler: mockHandleRequestHandler, - }), - ); + const rootMessenger = getRootMessenger({ + snapControllerHandleRequestActionHandler: mockHandleRequestHandler, + }); + const messenger = getMessenger(rootMessenger); const controller = new GatorPermissionsController({ messenger, @@ -1105,7 +1161,10 @@ describe('GatorPermissionsController', () => { }; await expect( - controller.submitDirectRevocation(revocationParams), + rootMessenger.call( + 'GatorPermissionsController:submitDirectRevocation', + revocationParams, + ), ).rejects.toThrow( 'Failed to handle snap request to gator permissions provider for method permissionsProvider_submitRevocation', ); @@ -1118,10 +1177,12 @@ describe('GatorPermissionsController', () => { describe('isPendingRevocation', () => { it('should return true when permission context is in pending revocations', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const messenger = getMessenger(rootMessenger); const permissionContext = '0x1234567890abcdef1234567890abcdef12345678' as Hex; - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1138,12 +1199,19 @@ describe('GatorPermissionsController', () => { }, }); - expect(controller.isPendingRevocation(permissionContext)).toBe(true); + expect( + rootMessenger.call( + 'GatorPermissionsController:isPendingRevocation', + permissionContext, + ), + ).toBe(true); }); it('should return false when permission context is not in pending revocations', () => { - const messenger = getMessenger(); - const controller = new GatorPermissionsController({ + const rootMessenger = getRootMessenger(); + const messenger = getMessenger(rootMessenger); + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1161,17 +1229,20 @@ describe('GatorPermissionsController', () => { }); expect( - controller.isPendingRevocation( + rootMessenger.call( + 'GatorPermissionsController:isPendingRevocation', '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef' as Hex, ), ).toBe(false); }); it('should be case-insensitive when checking permission context', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const messenger = getMessenger(rootMessenger); const permissionContext = '0x1234567890abcdef1234567890abcdef12345678' as Hex; - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1189,7 +1260,10 @@ describe('GatorPermissionsController', () => { }); expect( - controller.isPendingRevocation(permissionContext.toUpperCase() as Hex), + rootMessenger.call( + 'GatorPermissionsController:isPendingRevocation', + permissionContext.toUpperCase() as Hex, + ), ).toBe(true); }); }); @@ -1210,7 +1284,8 @@ describe('GatorPermissionsController', () => { }); const messenger = getMessenger(rootMessenger); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1222,7 +1297,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction approved event (user confirms) rootMessenger.publish('TransactionController:transactionApproved', { @@ -1281,7 +1359,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction approved event (user confirms) rootMessenger.publish('TransactionController:transactionApproved', { @@ -1322,7 +1403,8 @@ describe('GatorPermissionsController', () => { }); const messenger = getMessenger(rootMessenger); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1335,7 +1417,10 @@ describe('GatorPermissionsController', () => { const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; const hash = '0x-mock-hash'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction approved event (user confirms) rootMessenger.publish('TransactionController:transactionApproved', { @@ -1398,7 +1483,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Verify pending revocation is not in state yet expect(controller.state.pendingRevocations).toStrictEqual([]); @@ -1436,7 +1524,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction failed event rootMessenger.publish('TransactionController:transactionFailed', { @@ -1482,7 +1573,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction dropped event rootMessenger.publish('TransactionController:transactionDropped', { @@ -1528,7 +1622,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction failed event rootMessenger.publish('TransactionController:transactionFailed', { @@ -1567,7 +1664,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction dropped event rootMessenger.publish('TransactionController:transactionDropped', { @@ -1592,7 +1692,8 @@ describe('GatorPermissionsController', () => { }); const messenger = getMessenger(rootMessenger); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1604,7 +1705,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Fast-forward time by 2 hours jest.advanceTimersByTime(2 * 60 * 60 * 1000); @@ -1635,7 +1739,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Before approval, pending revocation should not be in state expect(controller.state.pendingRevocations).toStrictEqual([]); @@ -1658,7 +1765,8 @@ describe('GatorPermissionsController', () => { }); const messenger = getMessenger(rootMessenger); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1670,7 +1778,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction approved event for our transaction rootMessenger.publish('TransactionController:transactionApproved', { @@ -1699,7 +1810,8 @@ describe('GatorPermissionsController', () => { }); const messenger = getMessenger(rootMessenger); - const controller = new GatorPermissionsController({ + // eslint-disable-next-line no-new + new GatorPermissionsController({ messenger, config: { ...DEFAULT_TEST_CONFIG, @@ -1711,7 +1823,10 @@ describe('GatorPermissionsController', () => { const txId = 'test-tx-id'; const permissionContext = '0x1234567890abcdef1234567890abcdef12345678'; - await controller.addPendingRevocation({ txId, permissionContext }); + await rootMessenger.call( + 'GatorPermissionsController:addPendingRevocation', + { txId, permissionContext }, + ); // Emit transaction approved event (user confirms) rootMessenger.publish('TransactionController:transactionApproved', { diff --git a/packages/gator-permissions-controller/src/GatorPermissionsController.ts b/packages/gator-permissions-controller/src/GatorPermissionsController.ts index 5a2489778fe..68434d30f8a 100644 --- a/packages/gator-permissions-controller/src/GatorPermissionsController.ts +++ b/packages/gator-permissions-controller/src/GatorPermissionsController.ts @@ -32,6 +32,7 @@ import { OriginNotAllowedError, PermissionDecodingError, } from './errors'; +import type { GatorPermissionsControllerMethodActions } from './GatorPermissionsController-method-action-types'; import { controllerLog } from './logger'; import { GatorPermissionsSnapRpcMethod } from './types'; import type { @@ -49,6 +50,16 @@ import { executeSnapRpc } from './utils'; // Unique name for the controller const controllerName = 'GatorPermissionsController'; +const MESSENGER_EXPOSED_METHODS = [ + 'fetchAndUpdateGatorPermissions', + 'initialize', + 'decodePermissionFromPermissionContextForOrigin', + 'submitRevocation', + 'addPendingRevocation', + 'submitDirectRevocation', + 'isPendingRevocation', +] as const; + // Default value for the gator permissions provider snap id const defaultGatorPermissionsProviderSnapId = 'npm:@metamask/gator-permissions-snap' as SnapId; @@ -175,65 +186,13 @@ export type GatorPermissionsControllerGetStateAction = ControllerGetStateAction< GatorPermissionsControllerState >; -/** - * The action which can be used to fetch and update gator permissions. - */ -export type GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction = { - type: `${typeof controllerName}:fetchAndUpdateGatorPermissions`; - handler: GatorPermissionsController['fetchAndUpdateGatorPermissions']; -}; - -export type GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction = - { - type: `${typeof controllerName}:decodePermissionFromPermissionContextForOrigin`; - handler: GatorPermissionsController['decodePermissionFromPermissionContextForOrigin']; - }; - -/** - * The action which can be used to submit a revocation. - */ -export type GatorPermissionsControllerSubmitRevocationAction = { - type: `${typeof controllerName}:submitRevocation`; - handler: GatorPermissionsController['submitRevocation']; -}; - -/** - * The action which can be used to add a pending revocation. - */ -export type GatorPermissionsControllerAddPendingRevocationAction = { - type: `${typeof controllerName}:addPendingRevocation`; - handler: GatorPermissionsController['addPendingRevocation']; -}; - -/** - * The action which can be used to submit a revocation directly without requiring - * an on-chain transaction (for already-disabled delegations). - */ -export type GatorPermissionsControllerSubmitDirectRevocationAction = { - type: `${typeof controllerName}:submitDirectRevocation`; - handler: GatorPermissionsController['submitDirectRevocation']; -}; - -/** - * The action which can be used to check if a permission context is pending revocation. - */ -export type GatorPermissionsControllerIsPendingRevocationAction = { - type: `${typeof controllerName}:isPendingRevocation`; - handler: GatorPermissionsController['isPendingRevocation']; -}; - /** * All actions that {@link GatorPermissionsController} registers, to be called * externally. */ export type GatorPermissionsControllerActions = | GatorPermissionsControllerGetStateAction - | GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction - | GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction - | GatorPermissionsControllerSubmitRevocationAction - | GatorPermissionsControllerAddPendingRevocationAction - | GatorPermissionsControllerSubmitDirectRevocationAction - | GatorPermissionsControllerIsPendingRevocationAction; + | GatorPermissionsControllerMethodActions; /** * All actions that {@link GatorPermissionsController} calls internally. @@ -282,7 +241,7 @@ export type GatorPermissionsControllerMessenger = Messenger< /** * Controller that manages gator permissions by reading from the gator permissions provider Snap. */ -export default class GatorPermissionsController extends BaseController< +export class GatorPermissionsController extends BaseController< typeof controllerName, GatorPermissionsControllerState, GatorPermissionsControllerMessenger @@ -340,7 +299,11 @@ export default class GatorPermissionsController extends BaseController< defaultGatorPermissionsProviderSnapId; this.#maxSyncIntervalMs = config.maxSyncIntervalMs ?? DEFAULT_MAX_SYNC_INTERVAL_MS; - this.#registerMessageHandlers(); + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); } /** @@ -387,40 +350,6 @@ export default class GatorPermissionsController extends BaseController< }); } - #registerMessageHandlers(): void { - this.messenger.registerActionHandler( - `${controllerName}:fetchAndUpdateGatorPermissions`, - this.fetchAndUpdateGatorPermissions.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:decodePermissionFromPermissionContextForOrigin`, - this.decodePermissionFromPermissionContextForOrigin.bind(this), - ); - - const submitRevocationAction = `${controllerName}:submitRevocation`; - - this.messenger.registerActionHandler( - submitRevocationAction, - this.submitRevocation.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:addPendingRevocation`, - this.addPendingRevocation.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:submitDirectRevocation`, - this.submitDirectRevocation.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:isPendingRevocation`, - this.isPendingRevocation.bind(this), - ); - } - /** * Converts a stored gator permission to permission info with metadata. * Strips internal fields (dependencies, to) from the permission response. @@ -1008,3 +937,5 @@ export default class GatorPermissionsController extends BaseController< ); } } + +export default GatorPermissionsController; diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 5c628f0ddf7..ee20c408423 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,15 +1,18 @@ export { default as GatorPermissionsController } from './GatorPermissionsController'; +export type { + GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction, + GatorPermissionsControllerAddPendingRevocationAction, + GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction, + GatorPermissionsControllerInitializeAction, + GatorPermissionsControllerIsPendingRevocationAction, + GatorPermissionsControllerSubmitDirectRevocationAction, + GatorPermissionsControllerSubmitRevocationAction, +} from './GatorPermissionsController-method-action-types'; export type { GatorPermissionsControllerState, GatorPermissionsControllerConfig, GatorPermissionsControllerMessenger, GatorPermissionsControllerGetStateAction, - GatorPermissionsControllerDecodePermissionFromPermissionContextForOriginAction, - GatorPermissionsControllerFetchAndUpdateGatorPermissionsAction, - GatorPermissionsControllerSubmitRevocationAction, - GatorPermissionsControllerAddPendingRevocationAction, - GatorPermissionsControllerSubmitDirectRevocationAction, - GatorPermissionsControllerIsPendingRevocationAction, GatorPermissionsControllerActions, GatorPermissionsControllerEvents, GatorPermissionsControllerStateChangeEvent, diff --git a/yarn.lock b/yarn.lock index d9a735e2cbc..9b11efb9956 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3422,6 +3422,7 @@ __metadata: deepmerge: "npm:^4.2.2" jest: "npm:^29.7.0" ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3" @@ -4067,6 +4068,7 @@ __metadata: deepmerge: "npm:^4.2.2" jest: "npm:^29.7.0" ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3"