Skip to content

Commit a8e0d22

Browse files
feat!: Allow updating preinstalled Snaps via the registry (#3616)
Allow updating preinstalled Snaps by updating their entry in the registry. This is accomplished by searching the registry for updates whenever `updateRegistry` is called and triggering `#updateSnap`. As part of this PR the previously named `updateBlockedSnaps` has been renamed to `updateRegistry` and a flag to allow automatic updates has been added to `#updateSnap`. When an update is automatic, the permissions requested by the Snap are applied automatically similarly to how a preinstalled Snap is initialized. Preinstalled Snaps can still be installed and updated via the constructor. In case of any errors applying the update, the Snap is rolled back to its previous state. This behavior is put behind a feature flag that we can decide to enable at a later date. Closes #3593
1 parent 770eafa commit a8e0d22

File tree

2 files changed

+256
-56
lines changed

2 files changed

+256
-56
lines changed

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

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ import {
7070
import { hmac } from '@noble/hashes/hmac';
7171
import { sha512 } from '@noble/hashes/sha512';
7272
import { File } from 'buffer';
73+
import { createReadStream } from 'fs';
7374
import fetchMock from 'jest-fetch-mock';
75+
import path from 'path';
7476
import { pipeline } from 'readable-stream';
7577
import type { Duplex } from 'readable-stream';
7678
import { inc } from 'semver';
79+
import { Readable } from 'stream';
7780

7881
import {
7982
LEGACY_ENCRYPTION_KEY_DERIVATION_OPTIONS,
@@ -9953,7 +9956,7 @@ describe('SnapController', () => {
99539956
});
99549957
});
99559958

9956-
describe('updateBlockedSnaps', () => {
9959+
describe('updateRegistry', () => {
99579960
it('updates the registry database', async () => {
99589961
const registry = new MockSnapsRegistry();
99599962
const rootMessenger = getControllerMessenger(registry);
@@ -9967,7 +9970,7 @@ describe('SnapController', () => {
99679970
},
99689971
}),
99699972
);
9970-
await snapController.updateBlockedSnaps();
9973+
await snapController.updateRegistry();
99719974

99729975
expect(registry.update).toHaveBeenCalled();
99739976

@@ -10011,7 +10014,7 @@ describe('SnapController', () => {
1001110014
reason: { explanation, infoUrl },
1001210015
},
1001310016
});
10014-
await snapController.updateBlockedSnaps();
10017+
await snapController.updateRegistry();
1001510018

1001610019
// Ensure that CheckSnapBlockListArg is correct
1001710020
expect(registry.get).toHaveBeenCalledWith({
@@ -10070,7 +10073,7 @@ describe('SnapController', () => {
1007010073
registry.get.mockResolvedValueOnce({
1007110074
[mockSnap.id]: { status: SnapsRegistryStatus.Blocked },
1007210075
});
10073-
await snapController.updateBlockedSnaps();
10076+
await snapController.updateRegistry();
1007410077

1007510078
// The snap is blocked, disabled, and stopped
1007610079
expect(snapController.get(mockSnap.id)?.blocked).toBe(true);
@@ -10124,7 +10127,7 @@ describe('SnapController', () => {
1012410127
[mockSnapA.id]: { status: SnapsRegistryStatus.Unverified },
1012510128
[mockSnapB.id]: { status: SnapsRegistryStatus.Unverified },
1012610129
});
10127-
await snapController.updateBlockedSnaps();
10130+
await snapController.updateRegistry();
1012810131

1012910132
// A is unblocked, but still disabled
1013010133
expect(snapController.get(mockSnapA.id)?.blocked).toBe(false);
@@ -10168,7 +10171,7 @@ describe('SnapController', () => {
1016810171
new Promise<unknown>((resolve) => (resolveBlockListPromise = resolve)),
1016910172
);
1017010173

10171-
const updateBlockList = snapController.updateBlockedSnaps();
10174+
const updateBlockList = snapController.updateRegistry();
1017210175

1017310176
// Remove the snap while waiting for the blocklist
1017410177
await snapController.removeSnap(mockSnap.id);
@@ -10216,7 +10219,7 @@ describe('SnapController', () => {
1021610219
registry.get.mockResolvedValueOnce({
1021710220
[mockSnap.id]: { status: SnapsRegistryStatus.Blocked },
1021810221
});
10219-
await snapController.updateBlockedSnaps();
10222+
await snapController.updateRegistry();
1022010223

1022110224
// A is blocked and disabled
1022210225
expect(snapController.get(mockSnap.id)?.blocked).toBe(true);
@@ -10230,6 +10233,131 @@ describe('SnapController', () => {
1023010233

1023110234
snapController.destroy();
1023210235
});
10236+
10237+
it('updates preinstalled Snaps', async () => {
10238+
const registry = new MockSnapsRegistry();
10239+
const rootMessenger = getControllerMessenger(registry);
10240+
const messenger = getSnapControllerMessenger(rootMessenger);
10241+
10242+
// Simulate previous permissions, some of which will be removed
10243+
rootMessenger.registerActionHandler(
10244+
'PermissionController:getPermissions',
10245+
() => {
10246+
return {
10247+
[SnapEndowments.Rpc]: MOCK_RPC_ORIGINS_PERMISSION,
10248+
[SnapEndowments.LifecycleHooks]: MOCK_LIFECYCLE_HOOKS_PERMISSION,
10249+
};
10250+
},
10251+
);
10252+
10253+
const snapId = 'npm:@metamask/jsx-example-snap' as SnapId;
10254+
10255+
const mockSnap = getPersistedSnapObject({
10256+
id: snapId,
10257+
preinstalled: true,
10258+
});
10259+
10260+
const updateVersion = '1.2.1';
10261+
10262+
registry.resolveVersion.mockResolvedValue(updateVersion);
10263+
const fetchFunction = jest.fn().mockResolvedValueOnce({
10264+
// eslint-disable-next-line no-restricted-globals
10265+
headers: new Headers({ 'content-length': '5477' }),
10266+
ok: true,
10267+
body: Readable.toWeb(
10268+
createReadStream(
10269+
path.resolve(
10270+
__dirname,
10271+
`../../test/fixtures/metamask-jsx-example-snap-${updateVersion}.tgz`,
10272+
),
10273+
),
10274+
),
10275+
});
10276+
10277+
const snapController = getSnapController(
10278+
getSnapControllerOptions({
10279+
messenger,
10280+
state: {
10281+
snaps: getPersistedSnapsState(mockSnap),
10282+
},
10283+
fetchFunction,
10284+
featureFlags: {
10285+
autoUpdatePreinstalledSnaps: true,
10286+
},
10287+
}),
10288+
);
10289+
10290+
await snapController.updateRegistry();
10291+
10292+
const updatedSnap = snapController.get(snapId);
10293+
assert(updatedSnap);
10294+
10295+
expect(updatedSnap.version).toStrictEqual(updateVersion);
10296+
expect(updatedSnap.preinstalled).toBe(true);
10297+
10298+
expect(rootMessenger.call).toHaveBeenNthCalledWith(
10299+
7,
10300+
'PermissionController:revokePermissions',
10301+
{ [snapId]: [SnapEndowments.Rpc, SnapEndowments.LifecycleHooks] },
10302+
);
10303+
expect(rootMessenger.call).toHaveBeenNthCalledWith(
10304+
8,
10305+
'PermissionController:grantPermissions',
10306+
{
10307+
approvedPermissions: {
10308+
'endowment:rpc': {
10309+
caveats: [{ type: 'rpcOrigin', value: { dapps: true } }],
10310+
},
10311+
// eslint-disable-next-line @typescript-eslint/naming-convention
10312+
snap_dialog: {},
10313+
// eslint-disable-next-line @typescript-eslint/naming-convention
10314+
snap_manageState: {},
10315+
},
10316+
subject: { origin: snapId },
10317+
},
10318+
);
10319+
10320+
snapController.destroy();
10321+
});
10322+
10323+
it('does not update preinstalled Snaps when the feature flag is off', async () => {
10324+
const registry = new MockSnapsRegistry();
10325+
const rootMessenger = getControllerMessenger(registry);
10326+
const messenger = getSnapControllerMessenger(rootMessenger);
10327+
10328+
const snapId = 'npm:@metamask/jsx-example-snap' as SnapId;
10329+
10330+
const mockSnap = getPersistedSnapObject({
10331+
id: snapId,
10332+
preinstalled: true,
10333+
});
10334+
10335+
const updateVersion = '1.2.1';
10336+
10337+
registry.resolveVersion.mockResolvedValue(updateVersion);
10338+
10339+
const snapController = getSnapController(
10340+
getSnapControllerOptions({
10341+
messenger,
10342+
state: {
10343+
snaps: getPersistedSnapsState(mockSnap),
10344+
},
10345+
featureFlags: {
10346+
autoUpdatePreinstalledSnaps: false,
10347+
},
10348+
}),
10349+
);
10350+
10351+
await snapController.updateRegistry();
10352+
10353+
const snap = snapController.get(snapId);
10354+
assert(snap);
10355+
10356+
expect(snap.version).toStrictEqual(mockSnap.version);
10357+
expect(registry.resolveVersion).not.toHaveBeenCalled();
10358+
10359+
snapController.destroy();
10360+
});
1023310361
});
1023410362

1023510363
describe('clearState', () => {
@@ -11521,21 +11649,21 @@ describe('SnapController', () => {
1152111649
});
1152211650
});
1152311651

11524-
describe('SnapController:updateBlockedSnaps', () => {
11525-
it('calls SnapController.updateBlockedSnaps()', async () => {
11652+
describe('SnapController:updateRegistry', () => {
11653+
it('calls SnapController.updateRegistry()', async () => {
1152611654
const messenger = getSnapControllerMessenger();
1152711655
const snapController = getSnapController(
1152811656
getSnapControllerOptions({
1152911657
messenger,
1153011658
}),
1153111659
);
1153211660

11533-
const updateBlockedSnapsSpy = jest
11534-
.spyOn(snapController, 'updateBlockedSnaps')
11661+
const updateRegistrySpy = jest
11662+
.spyOn(snapController, 'updateRegistry')
1153511663
.mockImplementation();
1153611664

11537-
await messenger.call('SnapController:updateBlockedSnaps');
11538-
expect(updateBlockedSnapsSpy).toHaveBeenCalledTimes(1);
11665+
await messenger.call('SnapController:updateRegistry');
11666+
expect(updateRegistrySpy).toHaveBeenCalledTimes(1);
1153911667

1154011668
snapController.destroy();
1154111669
});

0 commit comments

Comments
 (0)