diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index a765a98b07..b91253e08e 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,6 +1,6 @@ { - "branches": 93.67, - "functions": 98.16, + "branches": 93.68, + "functions": 98.17, "lines": 98.51, - "statements": 98.34 + "statements": 98.35 } diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 89fc0d9331..20e5c3bd80 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -11581,4 +11581,113 @@ describe('SnapController', () => { snapController.destroy(); }); }); + + describe('SnapController:isMinimumPlatformVersion', () => { + it('returns true if the platform version is equal to the specified version', async () => { + const messenger = getSnapControllerMessenger(); + + const manifest = getSnapManifest({ + platformVersion: '6.0.0' as SemVerVersion, + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(getPersistedSnapObject({ manifest })), + }, + }), + ); + + expect( + messenger.call( + 'SnapController:isMinimumPlatformVersion', + MOCK_SNAP_ID, + manifest.platformVersion as SemVerVersion, + ), + ).toBe(true); + + snapController.destroy(); + }); + + it('returns true if the platform version is greater than the specified version', async () => { + const messenger = getSnapControllerMessenger(); + + const manifest = getSnapManifest({ + platformVersion: '6.0.0' as SemVerVersion, + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(getPersistedSnapObject({ manifest })), + }, + }), + ); + + expect( + messenger.call( + 'SnapController:isMinimumPlatformVersion', + MOCK_SNAP_ID, + '1.0.0' as SemVerVersion, + ), + ).toBe(true); + + snapController.destroy(); + }); + + it('returns false if the platform version is lesser than the specified version', async () => { + const messenger = getSnapControllerMessenger(); + + const manifest = getSnapManifest({ + platformVersion: '6.0.0' as SemVerVersion, + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(getPersistedSnapObject({ manifest })), + }, + }), + ); + + expect( + messenger.call( + 'SnapController:isMinimumPlatformVersion', + MOCK_SNAP_ID, + '7.0.0' as SemVerVersion, + ), + ).toBe(false); + + snapController.destroy(); + }); + + it('returns false if the platformVersion is undefined', async () => { + const messenger = getSnapControllerMessenger(); + + const manifest = getSnapManifest(); + delete manifest.platformVersion; + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(getPersistedSnapObject({ manifest })), + }, + }), + ); + + expect( + messenger.call( + 'SnapController:isMinimumPlatformVersion', + MOCK_SNAP_ID, + '999.0.0' as SemVerVersion, + ), + ).toBe(false); + + snapController.destroy(); + }); + }); }); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index df6d75ca31..49dc86d33e 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -113,6 +113,7 @@ import type { CaipAssetType, JsonRpcRequest, Hex, + SemVerVersion, } from '@metamask/utils'; import { hexToNumber, @@ -134,7 +135,7 @@ import { createMachine, interpret } from '@xstate/fsm'; import { Mutex } from 'async-mutex'; import type { Patch } from 'immer'; import { nanoid } from 'nanoid'; -import { gt } from 'semver'; +import { gt, gte } from 'semver'; import { ALLOWED_PERMISSIONS, @@ -462,6 +463,11 @@ export type GetSnapFile = { handler: SnapController['getSnapFile']; }; +export type IsMinimumPlatformVersion = { + type: `${typeof controllerName}:isMinimumPlatformVersion`; + handler: SnapController['isMinimumPlatformVersion']; +}; + export type SnapControllerGetStateAction = ControllerGetStateAction< typeof controllerName, SnapControllerState @@ -488,7 +494,8 @@ export type SnapControllerActions = | RevokeDynamicPermissions | GetSnapFile | SnapControllerGetStateAction - | StopAllSnaps; + | StopAllSnaps + | IsMinimumPlatformVersion; // Controller Messenger Events @@ -1256,6 +1263,11 @@ export class SnapController extends BaseController< `${controllerName}:stopAllSnaps`, async (...args) => this.stopAllSnaps(...args), ); + + this.messagingSystem.registerActionHandler( + `${controllerName}:isMinimumPlatformVersion`, + (...args) => this.isMinimumPlatformVersion(...args), + ); } #handlePreinstalledSnaps(preinstalledSnaps: PreinstalledSnap[]) { @@ -2178,6 +2190,26 @@ export class SnapController extends BaseController< return encoded; } + /** + * Determine if a given Snap ID supports a given minimum version of the Snaps platform + * by inspecting the platformVersion in the Snap manifest. + * + * @param snapId - The Snap ID. + * @param version - The version. + * @returns True if the platform version is equal or greater to the passed version, false otherwise. + */ + isMinimumPlatformVersion(snapId: SnapId, version: SemVerVersion): boolean { + const snap = this.getExpect(snapId); + + const { platformVersion } = snap.manifest; + + if (!platformVersion) { + return false; + } + + return gte(platformVersion, version); + } + /** * Completely clear the controller's state: delete all associated data, * handlers, event listeners, and permissions; tear down all snap providers.