diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index b416f3fab63..b4caba7456f 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -10,6 +10,7 @@ import { Caip25EndowmentPermissionName, Caip25CaveatMutators, createCaip25Caveat, + caip25CaveatBuilder, } from './caip25Permission'; import * as ScopeSupported from './scope/supported'; @@ -394,14 +395,7 @@ describe('caip25EndowmentBuilder', () => { }); describe('permission validator', () => { - const findNetworkClientIdByChainId = jest.fn(); - const listAccounts = jest.fn(); - const { validator } = caip25EndowmentBuilder.specificationBuilder({ - methodHooks: { - findNetworkClientIdByChainId, - listAccounts, - }, - }); + const { validator } = caip25EndowmentBuilder.specificationBuilder({}); it('throws an error if there is not exactly one caveat', () => { expect(() => { @@ -463,296 +457,233 @@ describe('caip25EndowmentBuilder', () => { ), ); }); + }); +}); - it('throws an error if the CAIP-25 caveat is malformed', () => { - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - missingRequiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); +describe('caip25CaveatBuilder', () => { + const findNetworkClientIdByChainId = jest.fn(); + const listAccounts = jest.fn(); + const { validator } = caip25CaveatBuilder({ + findNetworkClientIdByChainId, + listAccounts, + }); - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - missingOptionalScopes: {}, - isMultichainOrigin: true, - }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); + it('throws an error if the CAIP-25 caveat is malformed', () => { + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + missingRequiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: {}, + missingOptionalScopes: {}, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: 'NotABoolean', + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + }); - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: 'NotABoolean', - }, + it('asserts the internal required scopeStrings are supported', () => { + try { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); - }); - - it('asserts the internal required scopeStrings are supported', () => { - try { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - } catch (err) { - // noop - } - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'eip155:1', - expect.any(Function), - ); - - MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); - }); + }, + isMultichainOrigin: true, + }, + }); + } catch (err) { + // noop + } + expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( + 'eip155:1', + expect.any(Function), + ); + + MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + }); - it('asserts the internal optional scopeStrings are supported', () => { - try { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + it('asserts the internal optional scopeStrings are supported', () => { + try { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - } catch (err) { - // noop - } - - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'eip155:5', - expect.any(Function), - ); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + } catch (err) { + // noop + } - MockScopeSupported.isSupportedScopeString.mock.calls[1][1]('0x5'); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x5'); - }); + expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( + 'eip155:5', + expect.any(Function), + ); - it('does not throw if unable to find a network client for the chainId', () => { - findNetworkClientIdByChainId.mockImplementation(() => { - throw new Error('unable to find network client'); - }); - try { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - } catch (err) { - // noop - } + MockScopeSupported.isSupportedScopeString.mock.calls[1][1]('0x5'); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x5'); + }); - expect( - MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'), - ).toBe(false); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + it('does not throw if unable to find a network client for the chainId', () => { + findNetworkClientIdByChainId.mockImplementation(() => { + throw new Error('unable to find network client'); }); + try { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + } catch (err) { + // noop + } + + expect( + MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'), + ).toBe(false); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + }); - it('throws if not all scopeStrings are supported', () => { - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + it('throws if not all scopeStrings are supported', () => { + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received scopeString value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, - ), - ); - }); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received scopeString value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, + ), + ); + }); - it('throws if the eth accounts specified in the internal scopeObjects are not found in the wallet keyring', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - listAccounts.mockReturnValue([{ address: '0xdead' }]); // missing '0xbeef' + it('throws if the eth accounts specified in the internal scopeObjects are not found in the wallet keyring', () => { + MockScopeSupported.isSupportedScopeString.mockReturnValue(true); + listAccounts.mockReturnValue([{ address: '0xdead' }]); // missing '0xbeef' - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received eip155 account value(s) for caveat of type "${Caip25CaveatType}" that were not found in the wallet keyring.`, - ), - ); - }); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received eip155 account value(s) for caveat of type "${Caip25CaveatType}" that were not found in the wallet keyring.`, + ), + ); + }); - it('does not throw if the CAIP-25 caveat value is valid', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - listAccounts.mockReturnValue([ - { address: '0xdead' }, - { address: '0xbeef' }, - ]); + it('does not throw if the CAIP-25 caveat value is valid', () => { + MockScopeSupported.isSupportedScopeString.mockReturnValue(true); + listAccounts.mockReturnValue([ + { address: '0xdead' }, + { address: '0xbeef' }, + ]); - expect( - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + expect( + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }), - ).toBeUndefined(); - }); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }), + ).toBeUndefined(); }); }); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 6bc47b8dbc9..a865aefeb86 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -5,6 +5,7 @@ import type { ValidPermissionSpecification, PermissionValidatorConstraint, PermissionConstraint, + EndowmentCaveatSpecificationConstraint, } from '@metamask/permission-controller'; import { CaveatMutatorOperation, @@ -47,6 +48,11 @@ export type Caip25CaveatValue = { */ export const Caip25CaveatType = 'authorizedScopes'; +/** + * The target name of the CAIP-25 endowment permission. + */ +export const Caip25EndowmentPermissionName = 'endowment:caip25'; + /** * Creates a CAIP-25 permission caveat. * @param value - The CAIP-25 permission caveat value. @@ -59,75 +65,52 @@ export const createCaip25Caveat = (value: Caip25CaveatValue) => { }; }; -/** - * The target name of the CAIP-25 endowment permission. - */ -export const Caip25EndowmentPermissionName = 'endowment:caip25'; - -type Caip25EndowmentSpecification = ValidPermissionSpecification<{ - permissionType: PermissionType.Endowment; - targetName: typeof Caip25EndowmentPermissionName; - endowmentGetter: (_options?: EndowmentGetterParams) => null; - validator: PermissionValidatorConstraint; - allowedCaveats: Readonly> | null; -}>; - -type Caip25EndowmentSpecificationBuilderOptions = { - methodHooks: { - findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; - listAccounts: () => { address: Hex }[]; - }; +type Caip25EndowmentCaveatSpecificationBuilderOptions = { + findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; + listAccounts: () => { address: Hex }[]; }; /** - * Helper that returns a `endowment:caip25` specification that - * can be passed into the PermissionController constructor. + * Helper that returns a `authorizedScopes` CAIP-25 caveat specification + * that can be passed into the PermissionController constructor. * - * @param builderOptions - The specification builder options. - * @param builderOptions.methodHooks - The RPC method hooks needed by the method implementation. - * @returns The specification for the `caip25` endowment. + * @param options - The specification builder options. + * @param options.findNetworkClientIdByChainId - The hook for getting the networkClientId that serves a chainId. + * @param options.listAccounts - The hook for getting internalAccount objects for all evm accounts. + * @returns The specification for the `caip25` caveat. */ -const specificationBuilder: PermissionSpecificationBuilder< - PermissionType.Endowment, - Caip25EndowmentSpecificationBuilderOptions, - Caip25EndowmentSpecification -> = ({ methodHooks }: Caip25EndowmentSpecificationBuilderOptions) => { +export const caip25CaveatBuilder = ({ + findNetworkClientIdByChainId, + listAccounts, +}: Caip25EndowmentCaveatSpecificationBuilderOptions): EndowmentCaveatSpecificationConstraint & + Required> => { return { - permissionType: PermissionType.Endowment, - targetName: Caip25EndowmentPermissionName, - allowedCaveats: [Caip25CaveatType], - endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, - validator: (permission: PermissionConstraint) => { - const caip25Caveat = permission.caveats?.[0]; - if ( - permission.caveats?.length !== 1 || - caip25Caveat?.type !== Caip25CaveatType - ) { - throw new Error( - `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, - ); - } - + type: Caip25CaveatType, + validator: ( + caveat: { type: typeof Caip25CaveatType; value: unknown }, + _origin?: string, + _target?: string, + ) => { if ( - !caip25Caveat.value || - !hasProperty(caip25Caveat.value, 'requiredScopes') || - !hasProperty(caip25Caveat.value, 'optionalScopes') || - !hasProperty(caip25Caveat.value, 'isMultichainOrigin') || - typeof caip25Caveat.value.isMultichainOrigin !== 'boolean' + !caveat.value || + !hasProperty(caveat.value, 'requiredScopes') || + !hasProperty(caveat.value, 'optionalScopes') || + !hasProperty(caveat.value, 'isMultichainOrigin') || + typeof caveat.value.isMultichainOrigin !== 'boolean' ) { throw new Error( `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, ); } - const { requiredScopes, optionalScopes } = caip25Caveat.value; + const { requiredScopes, optionalScopes } = caveat.value; assertIsInternalScopesObject(requiredScopes); assertIsInternalScopesObject(optionalScopes); const isChainIdSupported = (chainId: Hex) => { try { - methodHooks.findNetworkClientIdByChainId(chainId); + findNetworkClientIdByChainId(chainId); return true; } catch (err) { return false; @@ -150,9 +133,9 @@ const specificationBuilder: PermissionSpecificationBuilder< // Fetch EVM accounts from native wallet keyring // These addresses are lowercased already - const existingEvmAddresses = methodHooks - .listAccounts() - .map((account) => account.address); + const existingEvmAddresses = listAccounts().map( + (account) => account.address, + ); const ethAccounts = getEthAccounts({ requiredScopes, optionalScopes, @@ -170,6 +153,43 @@ const specificationBuilder: PermissionSpecificationBuilder< }; }; +type Caip25EndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof Caip25EndowmentPermissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => null; + validator: PermissionValidatorConstraint; + allowedCaveats: Readonly> | null; +}>; + +/** + * Helper that returns a `endowment:caip25` specification that + * can be passed into the PermissionController constructor. + * + * @returns The specification for the `caip25` endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + Record, + Caip25EndowmentSpecification +> = () => { + return { + permissionType: PermissionType.Endowment, + targetName: Caip25EndowmentPermissionName, + allowedCaveats: [Caip25CaveatType], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, + validator: (permission: PermissionConstraint) => { + if ( + permission.caveats?.length !== 1 || + permission.caveats?.[0]?.type !== Caip25CaveatType + ) { + throw new Error( + `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, + ); + } + }, + }; +}; + /** * The `caip25` endowment specification builder. Passed to the * `PermissionController` for constructing and validating the diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 61f0fdcc429..38ab1bdde85 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -20,6 +20,7 @@ describe('@metamask/multichain', () => { "mergeScopeObject", "mergeScopes", "normalizeAndMergeScopes", + "caip25CaveatBuilder", "Caip25CaveatType", "createCaip25Caveat", "Caip25EndowmentPermissionName", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index d322c2b74de..5b56923ffa9 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -39,6 +39,7 @@ export { export type { Caip25CaveatValue } from './caip25Permission'; export { + caip25CaveatBuilder, Caip25CaveatType, createCaip25Caveat, Caip25EndowmentPermissionName,