diff --git a/src/BaseControllerV2.test.ts b/src/BaseControllerV2.test.ts index 697a5624a94..952cbfbcdfd 100644 --- a/src/BaseControllerV2.test.ts +++ b/src/BaseControllerV2.test.ts @@ -280,6 +280,47 @@ describe('getAnonymizedState', () => { expect(anonymizedState).toEqual({ txMeta: { history: [{ value: 9 }], value: 10 } }); }); + + it('should accept nested metadata', () => { + const anonymizeTransactionHistory = (history: { hash: string; value: number }[]) => { + return history.map((entry) => { + return { value: entry.value }; + }); + }; + + const anonymizedState = getAnonymizedState( + { + txMeta: { + hash: '0x123', + history: [ + { + hash: '0x123', + value: 9, + }, + ], + value: 10, + }, + }, + { + txMeta: { + hash: { + anonymous: false, + persist: false, + }, + history: { + anonymous: anonymizeTransactionHistory, + persist: false, + }, + value: { + anonymous: true, + persist: false, + }, + }, + }, + ); + + expect(anonymizedState).toEqual({ txMeta: { history: [{ value: 9 }], value: 10 } }); + }); }); describe('getPersistentState', () => { @@ -321,4 +362,39 @@ describe('getPersistentState', () => { ); expect(persistentState).toEqual({ password: 'secret password', privateKey: '123' }); }); + + it('should accept nested metadata', () => { + const anonymizedState = getPersistentState( + { + txMeta: { + hash: '0x123', + history: [ + { + hash: '0x123', + value: 9, + }, + ], + value: 10, + }, + }, + { + txMeta: { + hash: { + anonymous: false, + persist: false, + }, + history: { + anonymous: false, + persist: true, + }, + value: { + anonymous: false, + persist: true, + }, + }, + }, + ); + + expect(anonymizedState).toEqual({ txMeta: { history: [{ hash: '0x123', value: 9 }], value: 10 } }); + }); }); diff --git a/src/BaseControllerV2.ts b/src/BaseControllerV2.ts index 6cc5206bba3..26f8292116b 100644 --- a/src/BaseControllerV2.ts +++ b/src/BaseControllerV2.ts @@ -50,7 +50,9 @@ export type Anonymizer = (value: T) => T extends Primitive ? T : RecursivePar * get an anonymized representation of the state. */ export type StateMetadata = { - [P in keyof T]: StatePropertyMetadata; + [P in keyof T]: T[P] extends Primitive + ? StatePropertyMetadata + : StateMetadata | StatePropertyMetadata; }; /** @@ -153,10 +155,23 @@ export class BaseController> { } // This function acts as a type guard. Using a `typeof` conditional didn't seem to work. -function isAnonymizingFunction(x: boolean | Anonymizer): x is Anonymizer { +function isAnonymizingFunction(x: boolean | Anonymizer | StateMetadata): x is Anonymizer { return typeof x === 'function'; } +function isStatePropertyMetadata(x: StatePropertyMetadata | StateMetadata): x is StatePropertyMetadata { + const sortedKeys = Object.keys(x).sort(); + return sortedKeys.length === 2 && sortedKeys[0] === 'anonymous' && sortedKeys[1] === 'persist'; +} + +function isStateMetadata(x: StatePropertyMetadata | StateMetadata): x is StateMetadata { + return !isStatePropertyMetadata(x); +} + +function isPrimitive(x: Primitive | RecursivePartial): x is Primitive { + return ['boolean', 'string', 'number', 'null'].includes(typeof x); +} + /** * Returns an anonymized representation of the controller state. * @@ -174,11 +189,28 @@ export function getAnonymizedState>( ): RecursivePartial { return Object.keys(state).reduce((anonymizedState, _key) => { const key: keyof S = _key; // https://stackoverflow.com/questions/63893394/string-cannot-be-used-to-index-type-t - const metadataValue = metadata[key].anonymous; - if (isAnonymizingFunction(metadataValue)) { - anonymizedState[key] = metadataValue(state[key]); - } else if (metadataValue) { - anonymizedState[key] = state[key]; + const propertyMetadata = metadata[key]; + const propertyValue = state[key]; + // Ignore statement required because 'else' case is unreachable due to type + // The 'else if' condition is still required because it acts as a type guard + /* istanbul ignore else */ + if (isStateMetadata(propertyMetadata)) { + // Ignore statement required because this case is unreachable due to type + // This condition is still required because it acts as a type guard + /* istanbul ignore next */ + if (isPrimitive(propertyValue) || Array.isArray(propertyValue)) { + throw new Error(`Cannot assign metadata object to primitive type or array`); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + anonymizedState[key] = getAnonymizedState(propertyValue, propertyMetadata); + } else if (isStatePropertyMetadata(propertyMetadata)) { + const metadataValue = propertyMetadata.anonymous; + if (isAnonymizingFunction(metadataValue)) { + anonymizedState[key] = metadataValue(state[key]); + } else if (metadataValue) { + anonymizedState[key] = state[key]; + } } return anonymizedState; }, {} as RecursivePartial); @@ -191,12 +223,34 @@ export function getAnonymizedState>( * @param metadata - The controller state metadata, which describes which pieces of state should be persisted * @returns The subset of controller state that should be persisted */ -export function getPersistentState>(state: S, metadata: StateMetadata): Partial { +export function getPersistentState>( + state: S, + metadata: StateMetadata, +): RecursivePartial { return Object.keys(state).reduce((persistedState, _key) => { const key: keyof S = _key; // https://stackoverflow.com/questions/63893394/string-cannot-be-used-to-index-type-t - if (metadata[key].persist) { - persistedState[key] = state[key]; + const propertyMetadata = metadata[key]; + const propertyValue = state[key]; + + // Ignore statement required because 'else' case is unreachable due to type + // The 'else if' condition is still required because it acts as a type guard + /* istanbul ignore else */ + if (isStateMetadata(propertyMetadata)) { + // Ignore statement required because this case is unreachable due to type + // This condition is still required because it acts as a type guard + /* istanbul ignore next */ + if (isPrimitive(propertyValue) || Array.isArray(propertyValue)) { + throw new Error(`Cannot assign metadata object to primitive type or array`); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + persistedState[key] = getPersistentState(propertyValue, propertyMetadata); + } else if (isStatePropertyMetadata(propertyMetadata)) { + if (propertyMetadata.persist) { + persistedState[key] = state[key]; + } } + return persistedState; - }, {} as Partial); + }, {} as RecursivePartial); }