From 526a627ffd6466931e0f0fcfb6b802bb2ea47271 Mon Sep 17 00:00:00 2001 From: Andraz <69682837+andyv09@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:33:56 +0100 Subject: [PATCH] feat: state migration (#585) Co-authored-by: martines3000 --- .changeset/gorgeous-cougars-marry.md | 6 ++ packages/snap/src/storage/Storage.service.ts | 16 +++- packages/snap/src/utils/config.ts | 2 +- packages/snap/src/utils/stateMigration.ts | 14 ++++ packages/snap/tests/data/defaultSnapState.ts | 6 +- .../snap/tests/unit/Storage.service.spec.ts | 65 +++++++++++++++- .../snap/tests/unit/requestParams.spec.ts | 14 ++-- packages/types/src/constants.ts | 2 +- packages/types/src/index.ts | 1 + packages/types/src/legacyState.ts | 77 +++++++++++++++++++ packages/types/src/state.ts | 2 +- 11 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 .changeset/gorgeous-cougars-marry.md create mode 100644 packages/snap/src/utils/stateMigration.ts create mode 100644 packages/types/src/legacyState.ts diff --git a/.changeset/gorgeous-cougars-marry.md b/.changeset/gorgeous-cougars-marry.md new file mode 100644 index 000000000..87ae95d5a --- /dev/null +++ b/.changeset/gorgeous-cougars-marry.md @@ -0,0 +1,6 @@ +--- +"@blockchain-lab-um/masca-types": patch +"@blockchain-lab-um/masca": patch +--- + +Add state migration & update tests diff --git a/packages/snap/src/storage/Storage.service.ts b/packages/snap/src/storage/Storage.service.ts index 7ad214431..0f431fa0d 100644 --- a/packages/snap/src/storage/Storage.service.ts +++ b/packages/snap/src/storage/Storage.service.ts @@ -6,18 +6,21 @@ import { import { getInitialSnapState } from '../utils/config'; import SnapStorage from './Snap.storage'; +import { migrateToV2 } from 'src/utils/stateMigration'; class StorageService { static instance: MascaState; static async init(): Promise { - const state = await SnapStorage.load(); + let state = await SnapStorage.load(); if (!state) { StorageService.instance = getInitialSnapState(); return; } + state = StorageService.migrateState(state); + StorageService.instance = state as MascaState; } @@ -38,6 +41,17 @@ class StorageService { StorageService.instance[CURRENT_STATE_VERSION].currentAccount ]; } + + static migrateState = (state: any): MascaState => { + if (state[CURRENT_STATE_VERSION]) return state; + + let newState = state; + if (state.v1) { + newState = migrateToV2(state); + } + + return newState; + }; } export default StorageService; diff --git a/packages/snap/src/utils/config.ts b/packages/snap/src/utils/config.ts index 0051822c4..4bb92af93 100644 --- a/packages/snap/src/utils/config.ts +++ b/packages/snap/src/utils/config.ts @@ -104,7 +104,7 @@ const initialPermissions: DappPermissions = { export const getInitialPermissions = () => cloneDeep(initialPermissions); const initialSnapState: MascaState = { - v1: { + v2: { accountState: {}, currentAccount: '', config: { diff --git a/packages/snap/src/utils/stateMigration.ts b/packages/snap/src/utils/stateMigration.ts new file mode 100644 index 000000000..e85889099 --- /dev/null +++ b/packages/snap/src/utils/stateMigration.ts @@ -0,0 +1,14 @@ +import { MascaLegacyStateV1, MascaState } from '@blockchain-lab-um/masca-types'; +import { getInitialPermissions } from './config'; + +export const migrateToV2 = (state: MascaLegacyStateV1): MascaState => { + const newState: any = { v2: state.v1 }; + + // Remove friendly dapps + newState.v2.config.dApp.friendlyDapps = undefined; + + // Initialize permissions + newState.v2.config.dApp.permissions = { 'masca.io': getInitialPermissions() }; + + return newState as MascaState; +}; diff --git a/packages/snap/tests/data/defaultSnapState.ts b/packages/snap/tests/data/defaultSnapState.ts index 369696d18..5750a46d2 100644 --- a/packages/snap/tests/data/defaultSnapState.ts +++ b/packages/snap/tests/data/defaultSnapState.ts @@ -9,9 +9,8 @@ import { getEmptyAccountState } from '../../src/utils/config'; const defaultSnapState = (address: string): MascaState => { const accountState: Record = {}; accountState[address] = getEmptyAccountState(); - - return { - v1: { + const state = { + [CURRENT_STATE_VERSION]: { accountState, currentAccount: address, config: { @@ -25,6 +24,7 @@ const defaultSnapState = (address: string): MascaState => { }, }, }; + return state as MascaState; }; export const getDefaultSnapState = (address: string): MascaState => { diff --git a/packages/snap/tests/unit/Storage.service.spec.ts b/packages/snap/tests/unit/Storage.service.spec.ts index 9d28b6390..b86b4e1f4 100644 --- a/packages/snap/tests/unit/Storage.service.spec.ts +++ b/packages/snap/tests/unit/Storage.service.spec.ts @@ -1,11 +1,15 @@ -import { CURRENT_STATE_VERSION } from '@blockchain-lab-um/masca-types'; +import { + CURRENT_STATE_VERSION, + MascaLegacyStateV1, +} from '@blockchain-lab-um/masca-types'; import { MetaMaskInpageProvider } from '@metamask/providers'; import { SnapsProvider } from '@metamask/snaps-sdk'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import StorageService from '../../src/storage/Storage.service'; import { getInitialSnapState } from '../../src/utils/config'; import { SnapMock, createMockSnap } from '../helpers/snapMock'; +import * as MigrateState from '../../src/utils/stateMigration'; describe('Storage Service', () => { let snapMock: SnapsProvider & SnapMock; @@ -59,3 +63,60 @@ describe('Storage Service', () => { expect.assertions(2); }); }); +describe('State Migration', () => { + let snapMock: SnapsProvider & SnapMock; + beforeEach(async () => { + snapMock = createMockSnap(); + snapMock.rpcMocks.snap_manageState({ + operation: 'clear', + }); + global.snap = snapMock; + global.ethereum = snapMock as unknown as MetaMaskInpageProvider; + }); + + it('should not migrate state from latest version', async () => { + const spy = vi.spyOn(MigrateState, 'migrateToV2'); + const state = getInitialSnapState(); + StorageService.set(state); + + await StorageService.save(); + + const newState = StorageService.get(); + + expect(newState).toHaveProperty('v2'); + + StorageService.init(); + await StorageService.save(); + + const newState2 = StorageService.get(); + expect(newState2).toHaveProperty('v2'); + expect(spy).toHaveBeenCalledTimes(0); + + expect.assertions(3); + }); + + it('should succeed migrating state from v1 to v2', async () => { + const spy = vi.spyOn(MigrateState, 'migrateToV2'); + let state: any = getInitialSnapState(); + state = { v1: state.v2 }; + state.v1.config.dApp.friendlyDapps = ['masca.io']; + state.v1.config.dApp.permissions = undefined; + const legacyStateV1: MascaLegacyStateV1 = state; + StorageService.set(legacyStateV1 as any); + + await StorageService.save(); + + const newState = StorageService.get(); + + expect(newState).toHaveProperty('v1'); + + StorageService.init(); + await StorageService.save(); + + const newState2 = StorageService.get(); + expect(newState2).toHaveProperty('v2'); + expect(spy).toHaveBeenCalled(); + + expect.assertions(3); + }); +}); diff --git a/packages/snap/tests/unit/requestParams.spec.ts b/packages/snap/tests/unit/requestParams.spec.ts index b327b9a41..106ebcf47 100644 --- a/packages/snap/tests/unit/requestParams.spec.ts +++ b/packages/snap/tests/unit/requestParams.spec.ts @@ -701,12 +701,13 @@ describe('Utils [requestParams]', () => { describe('failure', () => { it('empty object', () => { expect(() => isValidMascaState({})).toThrowError( - 'invalid_argument: $input.v1' + `invalid_argument: $input.${CURRENT_STATE_VERSION}` ); }); it('empty state with version', () => { - expect(() => isValidMascaState({ v1: {} })).toThrowError( - 'invalid_argument: $input.v1.accountState, $input.v1.currentAccount, $input.v1.config' + const state = { [CURRENT_STATE_VERSION]: {} }; + expect(() => isValidMascaState(state)).toThrowError( + `invalid_argument: $input.${CURRENT_STATE_VERSION}.accountState, $input.${CURRENT_STATE_VERSION}.currentAccount, $input.${CURRENT_STATE_VERSION}.config` ); }); it('null', () => { @@ -715,10 +716,9 @@ describe('Utils [requestParams]', () => { ); }); it('missing fields', () => { - expect(() => - isValidMascaState({ v1: { accountState: {} } }) - ).toThrowError( - 'invalid_argument: $input.v1.currentAccount, $input.v1.config' + const state = { [CURRENT_STATE_VERSION]: { accountState: {} } }; + expect(() => isValidMascaState(state)).toThrowError( + `invalid_argument: $input.${CURRENT_STATE_VERSION}.currentAccount, $input.${CURRENT_STATE_VERSION}.config` ); }); }); diff --git a/packages/types/src/constants.ts b/packages/types/src/constants.ts index f94e02a85..4e4720b9c 100644 --- a/packages/types/src/constants.ts +++ b/packages/types/src/constants.ts @@ -10,7 +10,7 @@ export type AvailableCredentialStores = export const isavailableCredentialStores = (x: string) => isIn(availableCredentialStores, x); -export const CURRENT_STATE_VERSION = 'v1'; +export const CURRENT_STATE_VERSION = 'v2'; /** * @description diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index db75cd004..78b13245d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -8,3 +8,4 @@ export * from './state.js'; export * from './networks.js'; export * from './typia-generated/index.js'; export * from './signData.js'; +export * from './legacyState.js'; diff --git a/packages/types/src/legacyState.ts b/packages/types/src/legacyState.ts new file mode 100644 index 000000000..13b91af61 --- /dev/null +++ b/packages/types/src/legacyState.ts @@ -0,0 +1,77 @@ +import { IdentityMerkleTreeMetaInformation } from '@0xpolygonid/js-sdk'; +import { Blockchain, DidMethod, NetworkId } from '@iden3/js-iden3-core'; +import type { W3CVerifiableCredential } from '@veramo/core'; + +import type { + AvailableCredentialStores, + AvailableMethods, +} from './constants.js'; + +interface MascaLegacyConfigV1 { + snap: { + acceptedTerms: boolean; + }; + dApp: { + disablePopups: boolean; + friendlyDapps: string[]; + }; +} + +interface MascaLegacyAccountConfigV1 { + ssi: { + selectedMethod: AvailableMethods; + storesEnabled: Record; + }; +} + +export interface MascaLegacyStateV1 { + /** + * Version 1 of Masca state + */ + v1: { + /** + * Account specific storage + */ + accountState: Record; + /** + * Current account + */ + currentAccount: string; + /** + * Configuration for Masca + */ + config: MascaLegacyConfigV1; + }; +} + +/** + * Masca State for a MetaMask address + */ +interface MascaLegacyAccountStateV1 { + polygon: { + state: PolygonLegacyStateV1; + }; + veramo: { + credentials: Record; + }; + general: { + account: MascaLegacyAccountConfigV1; + ceramicSession?: string; + }; +} + +interface PolygonLegacyBaseStateV1 { + credentials: Record; + identities: Record; + profiles: Record; + merkleTreeMeta: IdentityMerkleTreeMetaInformation[]; + merkleTree: Record; +} + +type PolygonLegacyStateV1 = Record< + DidMethod.Iden3 | DidMethod.PolygonId, + Record< + Blockchain.Ethereum | Blockchain.Polygon, + Record + > +>; diff --git a/packages/types/src/state.ts b/packages/types/src/state.ts index 9d6fbc2ac..94cd47387 100644 --- a/packages/types/src/state.ts +++ b/packages/types/src/state.ts @@ -38,7 +38,7 @@ export interface MascaState { /** * Version 1 of Masca state */ - v1: { + v2: { /** * Account specific storage */