From 60644deae9328aedde52e469d0aad089bc3e4959 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Wed, 15 Jul 2026 18:51:26 +0200 Subject: [PATCH 1/3] chore: support deprecating controller by controller --- .../account-tracker-controller-init.test.ts | 41 +++++++ .../account-tracker-controller-init.ts | 7 ++ .../assetsUnifyState/index.test.ts | 108 ++++++++++++++++++ .../assetsUnifyState/index.ts | 28 +++++ tests/feature-flags/feature-flag-registry.ts | 7 ++ 5 files changed, 191 insertions(+) diff --git a/app/core/Engine/controllers/account-tracker-controller-init.test.ts b/app/core/Engine/controllers/account-tracker-controller-init.test.ts index 5d29b7493da..b002b852aa1 100644 --- a/app/core/Engine/controllers/account-tracker-controller-init.test.ts +++ b/app/core/Engine/controllers/account-tracker-controller-init.test.ts @@ -11,6 +11,7 @@ import { import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger'; import { selectAssetsAccountApiBalancesEnabled } from '../../../selectors/featureFlagController/assetsAccountApiBalances'; import { selectBasicFunctionalityEnabled } from '../../../selectors/settings'; +import { selectIsControllerDeprecated } from '../../../selectors/featureFlagController/assetsUnifyState'; jest.mock('@metamask/assets-controllers'); @@ -25,6 +26,14 @@ jest.mock('../../../selectors/settings', () => ({ selectBasicFunctionalityEnabled: jest.fn().mockReturnValue(true), })); +jest.mock('../../../selectors/featureFlagController/assetsUnifyState', () => ({ + selectIsControllerDeprecated: jest.fn().mockReturnValue(() => false), +})); + +jest.mock('../../../store', () => ({ + store: { getState: jest.fn().mockReturnValue({}) }, +})); + function getInitRequestMock(): jest.Mocked< MessengerClientInitRequest > { @@ -68,6 +77,7 @@ describe('accountTrackerControllerInit', () => { accountsApiChainIds: expect.any(Function), allowExternalServices: expect.any(Function), isHomepageSectionsV1Enabled: expect.any(Function), + isDeprecated: expect.any(Function), }), ); }); @@ -113,4 +123,35 @@ describe('accountTrackerControllerInit', () => { expect(allowExternalServices()).toBe(false); expect(selectBasicFunctionalityEnabled).toHaveBeenCalled(); }); + + describe('isDeprecated', () => { + it('returns false when AccountTrackerController is not deprecated', () => { + jest.mocked(selectIsControllerDeprecated).mockReturnValue(() => false); + + accountTrackerControllerInit(getInitRequestMock()); + + const controllerMock = jest.mocked(AccountTrackerController); + const { isDeprecated } = controllerMock.mock.calls[0][0] as { + isDeprecated: () => boolean; + }; + + expect(isDeprecated()).toBe(false); + expect(selectIsControllerDeprecated).toHaveBeenCalledWith( + 'AccountTrackerController', + ); + }); + + it('returns true when AccountTrackerController is deprecated', () => { + jest.mocked(selectIsControllerDeprecated).mockReturnValue(() => true); + + accountTrackerControllerInit(getInitRequestMock()); + + const controllerMock = jest.mocked(AccountTrackerController); + const { isDeprecated } = controllerMock.mock.calls[0][0] as { + isDeprecated: () => boolean; + }; + + expect(isDeprecated()).toBe(true); + }); + }); }); diff --git a/app/core/Engine/controllers/account-tracker-controller-init.ts b/app/core/Engine/controllers/account-tracker-controller-init.ts index bccd0a39e32..b9f265bfe30 100644 --- a/app/core/Engine/controllers/account-tracker-controller-init.ts +++ b/app/core/Engine/controllers/account-tracker-controller-init.ts @@ -5,6 +5,9 @@ import { } from '@metamask/assets-controllers'; import { selectAssetsAccountApiBalancesEnabled } from '../../../selectors/featureFlagController/assetsAccountApiBalances'; import { selectBasicFunctionalityEnabled } from '../../../selectors/settings'; +import { selectIsControllerDeprecated } from '../../../selectors/featureFlagController/assetsUnifyState'; +import { store } from '../../../store'; + /** * Initialize the accountTracker controller. * @@ -34,6 +37,10 @@ export const accountTrackerControllerInit: MessengerClientInitFunction< selectAssetsAccountApiBalancesEnabled(getState()) as `0x${string}`[], allowExternalServices: () => selectBasicFunctionalityEnabled(getState()), isHomepageSectionsV1Enabled: () => true, + isDeprecated: () => + selectIsControllerDeprecated('AccountTrackerController')( + store.getState(), + ), }); return { diff --git a/app/selectors/featureFlagController/assetsUnifyState/index.test.ts b/app/selectors/featureFlagController/assetsUnifyState/index.test.ts index 0be0f30abed..f85209c1c69 100644 --- a/app/selectors/featureFlagController/assetsUnifyState/index.test.ts +++ b/app/selectors/featureFlagController/assetsUnifyState/index.test.ts @@ -1,6 +1,8 @@ import { selectIsAssetsUnifyStateEnabled, isAssetsUnifyStateFeatureEnabled, + getIsDeprecatedController, + selectIsControllerDeprecated, AssetsUnifyStateFeatureFlag, ASSETS_UNIFY_STATE_FLAG, ASSETS_UNIFY_STATE_FEATURE_VERSION_1, @@ -95,3 +97,109 @@ describe('selectIsAssetsUnifyStateEnabled', () => { expect(selectIsAssetsUnifyStateEnabled(mockedState)).toBe(false); }); }); + +describe('getIsDeprecatedController', () => { + it('returns true when the controller is listed in deprecatedControllers', () => { + const flag: AssetsUnifyStateFeatureFlag = { + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + deprecatedControllers: ['AccountTrackerController'], + }; + + expect(getIsDeprecatedController(flag, 'AccountTrackerController')).toBe( + true, + ); + }); + + it('returns false when the controller is not listed in deprecatedControllers', () => { + const flag: AssetsUnifyStateFeatureFlag = { + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + deprecatedControllers: ['SomeOtherController'], + }; + + expect(getIsDeprecatedController(flag, 'AccountTrackerController')).toBe( + false, + ); + }); + + it('returns false when deprecatedControllers is empty', () => { + const flag: AssetsUnifyStateFeatureFlag = { + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + deprecatedControllers: [], + }; + + expect(getIsDeprecatedController(flag, 'AccountTrackerController')).toBe( + false, + ); + }); + + it('returns false when deprecatedControllers is absent', () => { + const flag: AssetsUnifyStateFeatureFlag = { + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + }; + + expect(getIsDeprecatedController(flag, 'AccountTrackerController')).toBe( + false, + ); + }); + + it('returns false when flagValue is undefined', () => { + expect( + getIsDeprecatedController(undefined, 'AccountTrackerController'), + ).toBe(false); + }); + + it('returns false when flagValue is null', () => { + expect(getIsDeprecatedController(null, 'AccountTrackerController')).toBe( + false, + ); + }); +}); + +describe('selectIsControllerDeprecated', () => { + it('returns true when the controller is listed in deprecatedControllers', () => { + const mockedState = mockStateWith({ + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + deprecatedControllers: ['AccountTrackerController'], + }); + + expect( + selectIsControllerDeprecated('AccountTrackerController')(mockedState), + ).toBe(true); + }); + + it('returns false when deprecatedControllers is empty', () => { + const mockedState = mockStateWith({ + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + deprecatedControllers: [], + }); + + expect( + selectIsControllerDeprecated('AccountTrackerController')(mockedState), + ).toBe(false); + }); + + it('returns false when deprecatedControllers is absent', () => { + const mockedState = mockStateWith({ + enabled: true, + featureVersion: ASSETS_UNIFY_STATE_FEATURE_VERSION_1, + }); + + expect( + selectIsControllerDeprecated('AccountTrackerController')(mockedState), + ).toBe(false); + }); + + it('returns false when the flag is missing from state', () => { + expect( + selectIsControllerDeprecated('AccountTrackerController')( + mockedUndefinedFlagsState, + ), + ).toBe(false); + }); +}); diff --git a/app/selectors/featureFlagController/assetsUnifyState/index.ts b/app/selectors/featureFlagController/assetsUnifyState/index.ts index d84c6802d57..9be1715e89b 100644 --- a/app/selectors/featureFlagController/assetsUnifyState/index.ts +++ b/app/selectors/featureFlagController/assetsUnifyState/index.ts @@ -7,6 +7,7 @@ import { selectRemoteFeatureFlags } from '..'; export interface AssetsUnifyStateFeatureFlag { enabled: boolean; featureVersion: string | null; + deprecatedControllers?: string[]; } export const ASSETS_UNIFY_STATE_FLAG = 'assetsUnifyState'; @@ -35,6 +36,33 @@ export const isAssetsUnifyStateFeatureEnabled = ( ); }; +/** + * Checks if a controller is deprecated based on the assets unify state feature flag. + * + * @param flagValue - The raw feature flag value. + * @param controllerName - The name of the controller to check. + * @returns True if the controller is listed in deprecatedControllers, false otherwise. + */ +export const getIsDeprecatedController = ( + flagValue: unknown, + controllerName: string, +): boolean => { + if (!flagValue || typeof flagValue !== 'object') return false; + const parsed = flagValue as AssetsUnifyStateFeatureFlag; + return parsed.deprecatedControllers?.includes(controllerName) ?? false; +}; + +/** + * Selector factory to check if a specific controller is deprecated. + * + * @param controllerName - The name of the controller to check. + * @returns A selector that returns true if the controller is deprecated. + */ +export const selectIsControllerDeprecated = (controllerName: string) => + createSelector(selectRemoteFeatureFlags, (flags) => + getIsDeprecatedController(flags[ASSETS_UNIFY_STATE_FLAG], controllerName), + ); + /** * Selector to check if the assets unify state feature is enabled. * @returns Boolean indicating if the assets unify state feature is enabled. diff --git a/tests/feature-flags/feature-flag-registry.ts b/tests/feature-flags/feature-flag-registry.ts index 6641f83982b..ab78b53e638 100644 --- a/tests/feature-flags/feature-flag-registry.ts +++ b/tests/feature-flags/feature-flag-registry.ts @@ -250,6 +250,13 @@ export const FEATURE_FLAG_REGISTRY: Record = { enabled: false, featureVersion: null, minimumVersion: null, + deprecatedControllers: [], + }, + '8.3.0': { + enabled: true, + featureVersion: '1', + minimumVersion: '8.3.0', + deprecatedControllers: [], }, }, }, From ff7bb1d3251491f4bd1d30e4d9ad49b97742188c Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Wed, 15 Jul 2026 19:00:08 +0200 Subject: [PATCH 2/3] chore: revert changes to account tracker controller --- .../account-tracker-controller-init.test.ts | 41 ------------------- .../account-tracker-controller-init.ts | 6 --- 2 files changed, 47 deletions(-) diff --git a/app/core/Engine/controllers/account-tracker-controller-init.test.ts b/app/core/Engine/controllers/account-tracker-controller-init.test.ts index b002b852aa1..5d29b7493da 100644 --- a/app/core/Engine/controllers/account-tracker-controller-init.test.ts +++ b/app/core/Engine/controllers/account-tracker-controller-init.test.ts @@ -11,7 +11,6 @@ import { import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger'; import { selectAssetsAccountApiBalancesEnabled } from '../../../selectors/featureFlagController/assetsAccountApiBalances'; import { selectBasicFunctionalityEnabled } from '../../../selectors/settings'; -import { selectIsControllerDeprecated } from '../../../selectors/featureFlagController/assetsUnifyState'; jest.mock('@metamask/assets-controllers'); @@ -26,14 +25,6 @@ jest.mock('../../../selectors/settings', () => ({ selectBasicFunctionalityEnabled: jest.fn().mockReturnValue(true), })); -jest.mock('../../../selectors/featureFlagController/assetsUnifyState', () => ({ - selectIsControllerDeprecated: jest.fn().mockReturnValue(() => false), -})); - -jest.mock('../../../store', () => ({ - store: { getState: jest.fn().mockReturnValue({}) }, -})); - function getInitRequestMock(): jest.Mocked< MessengerClientInitRequest > { @@ -77,7 +68,6 @@ describe('accountTrackerControllerInit', () => { accountsApiChainIds: expect.any(Function), allowExternalServices: expect.any(Function), isHomepageSectionsV1Enabled: expect.any(Function), - isDeprecated: expect.any(Function), }), ); }); @@ -123,35 +113,4 @@ describe('accountTrackerControllerInit', () => { expect(allowExternalServices()).toBe(false); expect(selectBasicFunctionalityEnabled).toHaveBeenCalled(); }); - - describe('isDeprecated', () => { - it('returns false when AccountTrackerController is not deprecated', () => { - jest.mocked(selectIsControllerDeprecated).mockReturnValue(() => false); - - accountTrackerControllerInit(getInitRequestMock()); - - const controllerMock = jest.mocked(AccountTrackerController); - const { isDeprecated } = controllerMock.mock.calls[0][0] as { - isDeprecated: () => boolean; - }; - - expect(isDeprecated()).toBe(false); - expect(selectIsControllerDeprecated).toHaveBeenCalledWith( - 'AccountTrackerController', - ); - }); - - it('returns true when AccountTrackerController is deprecated', () => { - jest.mocked(selectIsControllerDeprecated).mockReturnValue(() => true); - - accountTrackerControllerInit(getInitRequestMock()); - - const controllerMock = jest.mocked(AccountTrackerController); - const { isDeprecated } = controllerMock.mock.calls[0][0] as { - isDeprecated: () => boolean; - }; - - expect(isDeprecated()).toBe(true); - }); - }); }); diff --git a/app/core/Engine/controllers/account-tracker-controller-init.ts b/app/core/Engine/controllers/account-tracker-controller-init.ts index b9f265bfe30..df25aa1ea7d 100644 --- a/app/core/Engine/controllers/account-tracker-controller-init.ts +++ b/app/core/Engine/controllers/account-tracker-controller-init.ts @@ -5,8 +5,6 @@ import { } from '@metamask/assets-controllers'; import { selectAssetsAccountApiBalancesEnabled } from '../../../selectors/featureFlagController/assetsAccountApiBalances'; import { selectBasicFunctionalityEnabled } from '../../../selectors/settings'; -import { selectIsControllerDeprecated } from '../../../selectors/featureFlagController/assetsUnifyState'; -import { store } from '../../../store'; /** * Initialize the accountTracker controller. @@ -37,10 +35,6 @@ export const accountTrackerControllerInit: MessengerClientInitFunction< selectAssetsAccountApiBalancesEnabled(getState()) as `0x${string}`[], allowExternalServices: () => selectBasicFunctionalityEnabled(getState()), isHomepageSectionsV1Enabled: () => true, - isDeprecated: () => - selectIsControllerDeprecated('AccountTrackerController')( - store.getState(), - ), }); return { From f364668df0b0b8a88ccf5359567648810570c80a Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Wed, 15 Jul 2026 19:00:54 +0200 Subject: [PATCH 3/3] chore: minor change --- app/core/Engine/controllers/account-tracker-controller-init.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/core/Engine/controllers/account-tracker-controller-init.ts b/app/core/Engine/controllers/account-tracker-controller-init.ts index df25aa1ea7d..bccd0a39e32 100644 --- a/app/core/Engine/controllers/account-tracker-controller-init.ts +++ b/app/core/Engine/controllers/account-tracker-controller-init.ts @@ -5,7 +5,6 @@ import { } from '@metamask/assets-controllers'; import { selectAssetsAccountApiBalancesEnabled } from '../../../selectors/featureFlagController/assetsAccountApiBalances'; import { selectBasicFunctionalityEnabled } from '../../../selectors/settings'; - /** * Initialize the accountTracker controller. *