Skip to content

Commit 9cf727b

Browse files
wip: OTA
1 parent bd2ad5d commit 9cf727b

File tree

2 files changed

+138
-41
lines changed

2 files changed

+138
-41
lines changed

packages/snaps-controllers/src/snaps/SnapController.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ import fetchMock from 'jest-fetch-mock';
7474
import { pipeline } from 'readable-stream';
7575
import type { Duplex } from 'readable-stream';
7676
import { inc } from 'semver';
77+
import path from 'path';
78+
import { Readable } from 'stream';
7779

7880
import {
7981
LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS,
@@ -121,6 +123,7 @@ import {
121123
waitForStateChange,
122124
} from '../test-utils';
123125
import { delay } from '../utils';
126+
import { createReadStream } from 'fs';
124127

125128
globalThis.crypto.getRandomValues = <Type extends ArrayBufferView | null>(
126129
array: Type,
@@ -10208,6 +10211,50 @@ describe('SnapController', () => {
1020810211

1020910212
snapController.destroy();
1021010213
});
10214+
10215+
it.only('updates preinstalled Snaps', async () => {
10216+
const registry = new MockSnapsRegistry();
10217+
const rootMessenger = getControllerMessenger(registry);
10218+
const messenger = getSnapControllerMessenger(rootMessenger);
10219+
10220+
const snapId = 'npm:@metamask/jsx-example-snap' as SnapId;
10221+
10222+
const mockSnap = getPersistedSnapObject({ id: snapId, preinstalled: true });
10223+
10224+
const updateVersion = '1.2.1';
10225+
10226+
registry.resolveVersion.mockResolvedValue(updateVersion);
10227+
const fetchFunction = jest.fn()
10228+
.mockResolvedValueOnce({
10229+
// eslint-disable-next-line no-restricted-globals
10230+
headers: new Headers({ 'content-length': '5477' }),
10231+
ok: true,
10232+
body: Readable.toWeb(
10233+
createReadStream(
10234+
path.resolve(
10235+
__dirname,
10236+
`../../test/fixtures/metamask-jsx-example-snap-${updateVersion}.tgz`,
10237+
),
10238+
),
10239+
),
10240+
} as any);
10241+
10242+
const snapController = getSnapController(
10243+
getSnapControllerOptions({
10244+
messenger,
10245+
state: {
10246+
snaps: getPersistedSnapsState(mockSnap),
10247+
},
10248+
fetchFunction,
10249+
}),
10250+
);
10251+
10252+
await snapController.updateBlockedSnaps();
10253+
10254+
expect(snapController.get(snapId)?.version).toStrictEqual(updateVersion);
10255+
10256+
snapController.destroy();
10257+
});
1021110258
});
1021210259

1021310260
describe('clearState', () => {

packages/snaps-controllers/src/snaps/SnapController.ts

Lines changed: 91 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type {
2-
AddApprovalRequest,
3-
UpdateRequestState,
1+
import {
2+
ORIGIN_METAMASK,
3+
type AddApprovalRequest,
4+
type UpdateRequestState,
45
} from '@metamask/approval-controller';
56
import type {
67
RestrictedMessenger,
@@ -1458,6 +1459,8 @@ export class SnapController extends BaseController<
14581459
* Checks all installed snaps against the block list and
14591460
* blocks/unblocks snaps as appropriate. See {@link SnapController.blockSnap}
14601461
* for more information.
1462+
*
1463+
* Also updates any preinstalled Snaps to the latest allowlisted version.
14611464
*/
14621465
async updateBlockedSnaps(): Promise<void> {
14631466
this.#assertCanUsePlatform();
@@ -1486,6 +1489,34 @@ export class SnapController extends BaseController<
14861489
return this.#unblockSnap(snapId as SnapId);
14871490
}),
14881491
);
1492+
1493+
await Promise.all(
1494+
Object.values(this.state.snaps)
1495+
.filter((snap) => snap.preinstalled)
1496+
.map(async (snap) => {
1497+
const resolvedVersion = (await this.#resolveAllowlistVersion(
1498+
snap.id,
1499+
'*' as SemVerRange,
1500+
)) as unknown as SemVerVersion;
1501+
1502+
if (gtVersion(resolvedVersion, snap.version)) {
1503+
const location = this.#detectSnapLocation(snap.id, {
1504+
versionRange: resolvedVersion as unknown as SemVerRange,
1505+
fetch: this.#fetchFunction,
1506+
allowLocal: false,
1507+
});
1508+
1509+
await this.updateSnap(
1510+
ORIGIN_METAMASK,
1511+
snap.id,
1512+
location,
1513+
resolvedVersion,
1514+
true,
1515+
true,
1516+
);
1517+
}
1518+
}),
1519+
);
14891520
}
14901521

14911522
/**
@@ -2650,7 +2681,7 @@ export class SnapController extends BaseController<
26502681
pendingInstalls.push(snapId);
26512682
}
26522683

2653-
result[snapId] = await this.processRequestedSnap(
2684+
result[snapId] = await this.#processRequestedSnap(
26542685
origin,
26552686
snapId,
26562687
location,
@@ -2704,10 +2735,7 @@ export class SnapController extends BaseController<
27042735
* @param versionRange - The semver range of the snap to install.
27052736
* @returns The resulting snap object, or an error if something went wrong.
27062737
*/
2707-
// TODO: Either fix this lint violation or explain why it's necessary to
2708-
// ignore.
2709-
// eslint-disable-next-line no-restricted-syntax
2710-
private async processRequestedSnap(
2738+
async #processRequestedSnap(
27112739
origin: string,
27122740
snapId: SnapId,
27132741
location: SnapLocation,
@@ -2872,21 +2900,24 @@ export class SnapController extends BaseController<
28722900
* @param location - The location implementation of the snap.
28732901
* @param newVersionRange - A semver version range in which the maximum version will be chosen.
28742902
* @param emitEvent - An optional boolean flag to indicate whether this update should emit an event.
2903+
* @param automaticUpdate
28752904
* @returns The snap metadata if updated, `null` otherwise.
28762905
*/
2906+
// TODO: Make hash private
28772907
async updateSnap(
28782908
origin: string,
28792909
snapId: SnapId,
28802910
location: SnapLocation,
28812911
newVersionRange: string = DEFAULT_REQUESTED_SNAP_VERSION,
28822912
emitEvent = true,
2913+
automaticUpdate = false,
28832914
): Promise<TruncatedSnap> {
28842915
this.#assertCanInstallSnaps();
28852916
this.#assertCanUsePlatform();
28862917

28872918
const snap = this.getExpect(snapId);
28882919

2889-
if (snap.preinstalled) {
2920+
if (snap.preinstalled && !automaticUpdate) {
28902921
throw new Error('Preinstalled Snaps cannot be manually updated.');
28912922
}
28922923

@@ -2896,11 +2927,15 @@ export class SnapController extends BaseController<
28962927
);
28972928
}
28982929

2899-
let pendingApproval = this.#createApproval({
2900-
origin,
2901-
snapId,
2902-
type: SNAP_APPROVAL_UPDATE,
2903-
});
2930+
let pendingApproval;
2931+
2932+
if (!automaticUpdate) {
2933+
pendingApproval = this.#createApproval({
2934+
origin,
2935+
snapId,
2936+
type: SNAP_APPROVAL_UPDATE,
2937+
});
2938+
}
29042939

29052940
try {
29062941
this.messagingSystem.publish(
@@ -2953,26 +2988,35 @@ export class SnapController extends BaseController<
29532988
manifest.initialConnections ?? {},
29542989
);
29552990

2956-
this.#updateApproval(pendingApproval.id, {
2957-
permissions: newPermissions,
2958-
newVersion: manifest.version,
2959-
newPermissions,
2960-
approvedPermissions,
2961-
unusedPermissions,
2962-
newConnections,
2963-
unusedConnections,
2964-
approvedConnections,
2965-
loading: false,
2966-
});
2991+
let approvedNewPermissions;
2992+
let requestData;
29672993

2968-
const { permissions: approvedNewPermissions, ...requestData } =
2969-
(await pendingApproval.promise) as PermissionsRequest;
2994+
if (automaticUpdate) {
2995+
// TODO: This probably doesn't work as it doesn't account for initialConnections.
2996+
approvedNewPermissions = newPermissions;
2997+
} else {
2998+
assert(pendingApproval);
2999+
this.#updateApproval(pendingApproval.id, {
3000+
permissions: newPermissions,
3001+
newVersion: manifest.version,
3002+
newPermissions,
3003+
approvedPermissions,
3004+
unusedPermissions,
3005+
newConnections,
3006+
unusedConnections,
3007+
approvedConnections,
3008+
loading: false,
3009+
});
29703010

2971-
pendingApproval = this.#createApproval({
2972-
origin,
2973-
snapId,
2974-
type: SNAP_APPROVAL_RESULT,
2975-
});
3011+
const { permissions: approvedNewPermissions, ...requestData } =
3012+
(await pendingApproval.promise) as PermissionsRequest;
3013+
3014+
pendingApproval = this.#createApproval({
3015+
origin,
3016+
snapId,
3017+
type: SNAP_APPROVAL_RESULT,
3018+
});
3019+
}
29763020

29773021
if (this.isRunning(snapId)) {
29783022
await this.stopSnap(snapId, SnapStatusEvents.Stop);
@@ -3034,10 +3078,13 @@ export class SnapController extends BaseController<
30343078
);
30353079
}
30363080

3037-
this.#updateApproval(pendingApproval.id, {
3038-
loading: false,
3039-
type: SNAP_APPROVAL_UPDATE,
3040-
});
3081+
if (!automaticUpdate) {
3082+
assert(pendingApproval);
3083+
this.#updateApproval(pendingApproval.id, {
3084+
loading: false,
3085+
type: SNAP_APPROVAL_UPDATE,
3086+
});
3087+
}
30413088

30423089
return truncatedSnap;
30433090
} catch (error) {
@@ -3046,11 +3093,14 @@ export class SnapController extends BaseController<
30463093
const errorString =
30473094
error instanceof Error ? error.message : error.toString();
30483095

3049-
this.#updateApproval(pendingApproval.id, {
3050-
loading: false,
3051-
error: errorString,
3052-
type: SNAP_APPROVAL_UPDATE,
3053-
});
3096+
if (!automaticUpdate) {
3097+
assert(pendingApproval);
3098+
this.#updateApproval(pendingApproval.id, {
3099+
loading: false,
3100+
error: errorString,
3101+
type: SNAP_APPROVAL_UPDATE,
3102+
});
3103+
}
30543104

30553105
this.messagingSystem.publish(
30563106
'SnapController:snapInstallFailed',

0 commit comments

Comments
 (0)