Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/snaps-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
113 changes: 113 additions & 0 deletions packages/snaps-controllers/src/snaps/registry/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const getRegistry = (args?: Partial<JsonSnapsRegistryArgs>) => {
registry: new JsonSnapsRegistry({
messenger,
publicKey: MOCK_PUBLIC_KEY,
clientConfig: {
type: 'extension',
version: '13.9.0' as SemVerVersion,
},
...args,
}),
messenger,
Expand Down Expand Up @@ -84,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();

Expand Down Expand Up @@ -240,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 });

Expand Down Expand Up @@ -372,6 +453,38 @@ describe('JsonSnapsRegistry', () => {
expect(result).toBe('1.0.0');
});

it('resolves to a compatible allowlisted version', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test to make sure we resolve to 1.1.0 if the client version allows for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchMock
.mockResponseOnce(JSON.stringify(MOCK_DATABASE_COMPATIBILITY))
.mockResponseOnce(JSON.stringify(MOCK_COMPATIBILITY_SIGNATURE_FILE));

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('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(
Expand Down
34 changes: 30 additions & 4 deletions packages/snaps-controllers/src/snaps/registry/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type JsonSnapsRegistryUrl = {
signature: string;
};

export type ClientConfig = {
type: 'extension' | 'mobile';
version: SemVerVersion;
};

export type JsonSnapsRegistryArgs = {
messenger: SnapsRegistryMessenger;
state?: SnapsRegistryState;
Expand All @@ -47,6 +52,7 @@ export type JsonSnapsRegistryArgs = {
recentFetchThreshold?: number;
refetchOnAllowlistMiss?: boolean;
publicKey?: Hex;
clientConfig: ClientConfig;
};

export type GetResult = {
Expand Down Expand Up @@ -117,6 +123,8 @@ export class JsonSnapsRegistry extends BaseController<

readonly #publicKey: Hex;

readonly #clientConfig: ClientConfig;

readonly #fetchFunction: typeof fetch;

readonly #recentFetchThreshold: number;
Expand All @@ -133,6 +141,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,
Expand Down Expand Up @@ -167,6 +176,7 @@ export class JsonSnapsRegistry extends BaseController<
});
this.#url = url;
this.#publicKey = publicKey;
this.#clientConfig = clientConfig;
this.#fetchFunction = fetchFunction;
this.#recentFetchThreshold = recentFetchThreshold;
this.#refetchOnAllowlistMiss = refetchOnAllowlistMiss;
Expand Down Expand Up @@ -284,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.
Expand Down Expand Up @@ -338,11 +352,23 @@ export class JsonSnapsRegistry extends BaseController<
return versionRange;
}

const targetVersion = getTargetVersion(
Object.keys(versions) as SemVerVersion[],
versionRange,
const compatibleVersions = Object.entries(versions).reduce<SemVerVersion[]>(
(accumulator, [version, metadata]) => {
const clientRange = metadata.clientVersions?.[this.#clientConfig.type];
if (
!clientRange ||
satisfiesVersionRange(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);
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:^"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down
Loading