From 7e41ef70d77eba058d8164fd4803b6872ee2bb89 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 12 Nov 2025 12:58:33 +0100 Subject: [PATCH 1/6] feat: Support specified clientVersions in the registry --- .../src/snaps/registry/json.test.ts | 4 +++ .../src/snaps/registry/json.ts | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/json.test.ts index 3dd5448eaa..f1e4e7b883 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.test.ts @@ -23,6 +23,10 @@ const getRegistry = (args?: Partial) => { registry: new JsonSnapsRegistry({ messenger, publicKey: MOCK_PUBLIC_KEY, + clientConfig: { + type: 'extension', + version: '13.9.0' as SemVerVersion, + }, ...args, }), messenger, diff --git a/packages/snaps-controllers/src/snaps/registry/json.ts b/packages/snaps-controllers/src/snaps/registry/json.ts index e9fea8fd74..484fcab4b1 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.ts @@ -12,6 +12,7 @@ import { assert, assertIsSemVerRange, Duration, + gtRange, inMilliseconds, satisfiesVersionRange, } from '@metamask/utils'; @@ -39,6 +40,11 @@ type JsonSnapsRegistryUrl = { signature: string; }; +export type ClientConfig = { + type: 'extension' | 'mobile'; + version: SemVerVersion; +}; + export type JsonSnapsRegistryArgs = { messenger: SnapsRegistryMessenger; state?: SnapsRegistryState; @@ -47,6 +53,7 @@ export type JsonSnapsRegistryArgs = { recentFetchThreshold?: number; refetchOnAllowlistMiss?: boolean; publicKey?: Hex; + clientConfig: ClientConfig; }; export type GetResult = { @@ -117,6 +124,8 @@ export class JsonSnapsRegistry extends BaseController< readonly #publicKey: Hex; + readonly #clientConfig: ClientConfig; + readonly #fetchFunction: typeof fetch; readonly #recentFetchThreshold: number; @@ -133,6 +142,7 @@ export class JsonSnapsRegistry extends BaseController< signature: SNAP_REGISTRY_SIGNATURE_URL, }, publicKey = DEFAULT_PUBLIC_KEY, + clientConfig, fetchFunction = globalThis.fetch.bind(undefined), recentFetchThreshold = inMilliseconds(5, Duration.Minute), refetchOnAllowlistMiss = true, @@ -167,6 +177,7 @@ export class JsonSnapsRegistry extends BaseController< }); this.#url = url; this.#publicKey = publicKey; + this.#clientConfig = clientConfig; this.#fetchFunction = fetchFunction; this.#recentFetchThreshold = recentFetchThreshold; this.#refetchOnAllowlistMiss = refetchOnAllowlistMiss; @@ -338,11 +349,20 @@ export class JsonSnapsRegistry extends BaseController< return versionRange; } - const targetVersion = getTargetVersion( - Object.keys(versions) as SemVerVersion[], - versionRange, + const compatibleVersions = Object.entries(versions).reduce( + (accumulator, [version, metadata]) => { + const clientRange = metadata.clientVersions?.[this.#clientConfig.type]; + if (!clientRange || gtRange(this.#clientConfig.version, clientRange)) { + accumulator.push(version as SemVerVersion); + } + + return accumulator; + }, + [], ); + const targetVersion = getTargetVersion(compatibleVersions, versionRange); + if (!targetVersion && this.#refetchOnAllowlistMiss && !refetch) { await this.#triggerUpdate(); return this.#resolveVersion(snapId, versionRange, true); From e0ea4d6ae101d4ee5dd32a3bcafcc90c2f9ddf6e Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 12 Nov 2025 13:08:56 +0100 Subject: [PATCH 2/6] Add test --- .../src/snaps/registry/json.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/json.test.ts index f1e4e7b883..18d82d1e5b 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.test.ts @@ -376,6 +376,49 @@ describe('JsonSnapsRegistry', () => { expect(result).toBe('1.0.0'); }); + it('resolves to a compatible allowlisted version', async () => { + fetchMock + .mockResponseOnce( + JSON.stringify({ + verifiedSnaps: { + [MOCK_SNAP_ID]: { + id: MOCK_SNAP_ID, + metadata: { + name: 'Mock Snap', + }, + versions: { + ['1.0.0' as SemVerVersion]: { + checksum: DEFAULT_SNAP_SHASUM, + }, + ['1.1.0' as SemVerVersion]: { + checksum: DEFAULT_SNAP_SHASUM, + clientVersions: { + extension: '>=15.0.0', + }, + }, + }, + }, + }, + }), + ) + .mockResponseOnce( + JSON.stringify({ + ...MOCK_SIGNATURE_FILE, + signature: + '0x3045022100e17cf0f34e4d521d984ed8f8492ce8a51b65178e7ba5508e01b179c5dd013c52022025e4a4008232a883048c8174e585cd89b9054eadc37330e00fd2f07a65c9bbb4', + }), + ); + + const { messenger } = getRegistry(); + const result = await messenger.call( + 'SnapsRegistry:resolveVersion', + MOCK_SNAP_ID, + '^1.0.0' as SemVerRange, + ); + + expect(result).toBe('1.0.0'); + }); + it('returns version range if snap is not on the allowlist', async () => { fetchMock .mockResponseOnce( From d61e1fc27782077ab5079595aed9260fc9faf02c Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 12 Nov 2025 14:09:44 +0100 Subject: [PATCH 3/6] Bump snaps-registry --- packages/snaps-controllers/package.json | 2 +- packages/snaps-utils/package.json | 2 +- yarn.lock | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index 7291d92efe..2170f0371c 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -90,7 +90,7 @@ "@metamask/phishing-controller": "^15.0.0", "@metamask/post-message-stream": "^10.0.0", "@metamask/rpc-errors": "^7.0.3", - "@metamask/snaps-registry": "^3.2.3", + "@metamask/snaps-registry": "^3.3.0", "@metamask/snaps-rpc-methods": "workspace:^", "@metamask/snaps-sdk": "workspace:^", "@metamask/snaps-utils": "workspace:^", diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index 6fcc6efc44..c9b33e1f21 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -85,7 +85,7 @@ "@metamask/permission-controller": "^12.1.0", "@metamask/rpc-errors": "^7.0.3", "@metamask/slip44": "^4.3.0", - "@metamask/snaps-registry": "^3.2.3", + "@metamask/snaps-registry": "^3.3.0", "@metamask/snaps-sdk": "workspace:^", "@metamask/superstruct": "^3.2.1", "@metamask/utils": "^11.8.1", diff --git a/yarn.lock b/yarn.lock index b970d42388..78eda15f36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4262,7 +4262,7 @@ __metadata: "@metamask/phishing-controller": "npm:^15.0.0" "@metamask/post-message-stream": "npm:^10.0.0" "@metamask/rpc-errors": "npm:^7.0.3" - "@metamask/snaps-registry": "npm:^3.2.3" + "@metamask/snaps-registry": "npm:^3.3.0" "@metamask/snaps-rpc-methods": "workspace:^" "@metamask/snaps-sdk": "workspace:^" "@metamask/snaps-utils": "workspace:^" @@ -4413,15 +4413,15 @@ __metadata: languageName: unknown linkType: soft -"@metamask/snaps-registry@npm:^3.2.3": - version: 3.2.3 - resolution: "@metamask/snaps-registry@npm:3.2.3" +"@metamask/snaps-registry@npm:^3.3.0": + version: 3.3.0 + resolution: "@metamask/snaps-registry@npm:3.3.0" dependencies: - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.4.0" "@noble/curves": "npm:^1.2.0" "@noble/hashes": "npm:^1.3.2" - checksum: 10/37760f29b7aaa337d815cf0c11fa34af5093d87fdc60a3750c494cf8bae6293cd52da03e7694b467b79733052d75ec6e3781ab3590d7259a050784e5be347d12 + checksum: 10/1a53ad150318cbaf703b639a3a831a6ac57f84b2266ac176e6b0d470df31ecf66f0f885256f17a7acae265ada085c904ba97f1e2cb5371e136bf90778ffaed0a languageName: node linkType: hard @@ -4599,7 +4599,7 @@ __metadata: "@metamask/post-message-stream": "npm:^10.0.0" "@metamask/rpc-errors": "npm:^7.0.3" "@metamask/slip44": "npm:^4.3.0" - "@metamask/snaps-registry": "npm:^3.2.3" + "@metamask/snaps-registry": "npm:^3.3.0" "@metamask/snaps-sdk": "workspace:^" "@metamask/superstruct": "npm:^3.2.1" "@metamask/utils": "npm:^11.8.1" From 8abe43e70ed545362365e5c0a6f903f03515f9f7 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 12 Nov 2025 14:24:56 +0100 Subject: [PATCH 4/6] Use satisfiesVersionRange --- packages/snaps-controllers/src/snaps/registry/json.test.ts | 5 ++++- packages/snaps-controllers/src/snaps/registry/json.ts | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/json.test.ts index 18d82d1e5b..5d4d57216d 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.test.ts @@ -389,6 +389,9 @@ describe('JsonSnapsRegistry', () => { versions: { ['1.0.0' as SemVerVersion]: { checksum: DEFAULT_SNAP_SHASUM, + clientVersions: { + extension: '>=13.9.0', + }, }, ['1.1.0' as SemVerVersion]: { checksum: DEFAULT_SNAP_SHASUM, @@ -405,7 +408,7 @@ describe('JsonSnapsRegistry', () => { JSON.stringify({ ...MOCK_SIGNATURE_FILE, signature: - '0x3045022100e17cf0f34e4d521d984ed8f8492ce8a51b65178e7ba5508e01b179c5dd013c52022025e4a4008232a883048c8174e585cd89b9054eadc37330e00fd2f07a65c9bbb4', + '0x30440220337683880fa580d8eed89c37f9b6d5639a015d416d58afc50ff0602820ab8af3022008b6a5b37a028d8d632fb311ddd0e92fc2d98cff013da4e47b99a754638a8876', }), ); diff --git a/packages/snaps-controllers/src/snaps/registry/json.ts b/packages/snaps-controllers/src/snaps/registry/json.ts index 484fcab4b1..5a8335df57 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.ts @@ -12,7 +12,6 @@ import { assert, assertIsSemVerRange, Duration, - gtRange, inMilliseconds, satisfiesVersionRange, } from '@metamask/utils'; @@ -352,7 +351,10 @@ export class JsonSnapsRegistry extends BaseController< const compatibleVersions = Object.entries(versions).reduce( (accumulator, [version, metadata]) => { const clientRange = metadata.clientVersions?.[this.#clientConfig.type]; - if (!clientRange || gtRange(this.#clientConfig.version, clientRange)) { + if ( + !clientRange || + satisfiesVersionRange(this.#clientConfig.version, clientRange) + ) { accumulator.push(version as SemVerVersion); } From b2c31c121ff77bbe20cdb7f6bd2e7c9fd4d8241a Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 12 Nov 2025 14:35:59 +0100 Subject: [PATCH 5/6] Check compatibility in :get --- .../src/snaps/registry/json.test.ts | 112 ++++++++++++------ .../src/snaps/registry/json.ts | 6 +- 2 files changed, 84 insertions(+), 34 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/json.test.ts index 5d4d57216d..d578bb71be 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.test.ts @@ -88,6 +88,43 @@ const MOCK_EMPTY_SIGNATURE_FILE = { format: 'DER', }; +const MOCK_DATABASE_COMPATIBILITY = { + verifiedSnaps: { + [MOCK_SNAP_ID]: { + id: MOCK_SNAP_ID, + metadata: { + name: 'Mock Snap', + }, + versions: { + ['1.0.0' as SemVerVersion]: { + checksum: DEFAULT_SNAP_SHASUM, + clientVersions: { + extension: '>=13.9.0', + }, + }, + ['1.1.0' as SemVerVersion]: { + checksum: DEFAULT_SNAP_SHASUM, + clientVersions: { + extension: '>=15.0.0', + }, + }, + }, + }, + }, + blockedSnaps: [], +}; + +/** + * To regenerate the signature, repeat the instructions above but with MOCK_DATABASE_COMPATIBILITY + */ +const MOCK_COMPATIBILITY_SIGNATURE = + '0x3045022100c0dd17483ac052b25a24c43a84de7b7b38194ac770cadb53a83ca950150631bd02204ed1f6b3359901199e2752d148079084cda13439150136055be5d4a3df205115'; +const MOCK_COMPATIBILITY_SIGNATURE_FILE = { + signature: MOCK_COMPATIBILITY_SIGNATURE, + curve: 'secp256k1', + format: 'DER', +}; + describe('JsonSnapsRegistry', () => { fetchMock.enableMocks(); @@ -244,6 +281,46 @@ describe('JsonSnapsRegistry', () => { }); }); + it('returns verified for compatible Snaps', async () => { + fetchMock + .mockResponseOnce(JSON.stringify(MOCK_DATABASE_COMPATIBILITY)) + .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); + + const { messenger } = getRegistry(); + const result = await messenger.call('SnapsRegistry:get', { + [MOCK_SNAP_ID]: { + version: '1.0.0' as SemVerVersion, + checksum: DEFAULT_SNAP_SHASUM, + }, + }); + + expect(result).toStrictEqual({ + [MOCK_SNAP_ID]: { + status: SnapsRegistryStatus.Verified, + }, + }); + }); + + it('returns unverified for non compatible Snaps', async () => { + fetchMock + .mockResponseOnce(JSON.stringify(MOCK_DATABASE_COMPATIBILITY)) + .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); + + const { messenger } = getRegistry(); + const result = await messenger.call('SnapsRegistry:get', { + [MOCK_SNAP_ID]: { + version: '1.1.0' as SemVerVersion, + checksum: DEFAULT_SNAP_SHASUM, + }, + }); + + expect(result).toStrictEqual({ + [MOCK_SNAP_ID]: { + status: SnapsRegistryStatus.Unverified, + }, + }); + }); + it('uses existing state if registry is unavailable', async () => { fetchMock.mockResponse('', { status: 404 }); @@ -378,39 +455,8 @@ describe('JsonSnapsRegistry', () => { it('resolves to a compatible allowlisted version', async () => { fetchMock - .mockResponseOnce( - JSON.stringify({ - verifiedSnaps: { - [MOCK_SNAP_ID]: { - id: MOCK_SNAP_ID, - metadata: { - name: 'Mock Snap', - }, - versions: { - ['1.0.0' as SemVerVersion]: { - checksum: DEFAULT_SNAP_SHASUM, - clientVersions: { - extension: '>=13.9.0', - }, - }, - ['1.1.0' as SemVerVersion]: { - checksum: DEFAULT_SNAP_SHASUM, - clientVersions: { - extension: '>=15.0.0', - }, - }, - }, - }, - }, - }), - ) - .mockResponseOnce( - JSON.stringify({ - ...MOCK_SIGNATURE_FILE, - signature: - '0x30440220337683880fa580d8eed89c37f9b6d5639a015d416d58afc50ff0602820ab8af3022008b6a5b37a028d8d632fb311ddd0e92fc2d98cff013da4e47b99a754638a8876', - }), - ); + .mockResponseOnce(JSON.stringify(MOCK_DATABASE_COMPATIBILITY)) + .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); const { messenger } = getRegistry(); const result = await messenger.call( diff --git a/packages/snaps-controllers/src/snaps/registry/json.ts b/packages/snaps-controllers/src/snaps/registry/json.ts index 5a8335df57..6421bcfdff 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.ts @@ -294,7 +294,11 @@ export class JsonSnapsRegistry extends BaseController< const verified = database?.verifiedSnaps[snapId]; const version = verified?.versions?.[snapInfo.version]; - if (version && version.checksum === snapInfo.checksum) { + const clientRange = version?.clientVersions?.[this.#clientConfig.type]; + const isCompatible = + !clientRange || + satisfiesVersionRange(this.#clientConfig.version, clientRange); + if (version && version.checksum === snapInfo.checksum && isCompatible) { return { status: SnapsRegistryStatus.Verified }; } // For now, if we have an allowlist miss, we can refetch once and try again. From 9176886da8fde61b91fe90eee39f52d77d58cef4 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 12 Nov 2025 14:41:10 +0100 Subject: [PATCH 6/6] Add another test --- .../src/snaps/registry/json.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/snaps-controllers/src/snaps/registry/json.test.ts b/packages/snaps-controllers/src/snaps/registry/json.test.ts index d578bb71be..dcf710327e 100644 --- a/packages/snaps-controllers/src/snaps/registry/json.test.ts +++ b/packages/snaps-controllers/src/snaps/registry/json.test.ts @@ -468,6 +468,23 @@ describe('JsonSnapsRegistry', () => { expect(result).toBe('1.0.0'); }); + it('resolves to the latest allowlisted version if compatible', async () => { + fetchMock + .mockResponseOnce(JSON.stringify(MOCK_DATABASE_COMPATIBILITY)) + .mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE)); + + const { messenger } = getRegistry({ + clientConfig: { type: 'extension', version: '15.0.0' as SemVerVersion }, + }); + const result = await messenger.call( + 'SnapsRegistry:resolveVersion', + MOCK_SNAP_ID, + '^1.0.0' as SemVerRange, + ); + + expect(result).toBe('1.1.0'); + }); + it('returns version range if snap is not on the allowlist', async () => { fetchMock .mockResponseOnce(