From cedabc62e45601c77871689425320c54d717275e Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Mon, 14 Oct 2024 18:29:15 +0100 Subject: [PATCH] feat: preferences controller to base controller v2 (#27398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In this PR, we want to bring PreferencesController up to date with our latest controller patterns by upgrading to BaseControllerV2. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27398?quickstart=1) ## **Related issues** Fixes: #25917 ## **Manual testing steps** Use case 1 1. Install the previous release 2. Complete user onboarding 3. Go to settings and change couple of user settings. For example language, currency and theme. 4. Close and disable MM in the extension 5. Checkout the version with these changes 6. Build and login 7. Make sure, the user preferences set earlier are still there Use case 2 1. Disable all the MM extensions 2. Install the version with these changes 3. When you click on MM, the default language should be English 4. Complete user onboarding 5. Go to settings and change couple of user settings. For example language, currency and theme. 6. Close and disable and enable the MM in extension. which forces user to login MM in the extension 7. Once you login again, make sure, the user preferences set earlier are still there ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Co-authored-by: MetaMask Bot Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> --- .eslintrc.js | 2 +- app/scripts/background.js | 5 +- .../account-tracker-controller.test.ts | 11 +- .../controllers/account-tracker-controller.ts | 18 +- app/scripts/controllers/app-state.js | 23 +- app/scripts/controllers/app-state.test.js | 7 +- app/scripts/controllers/metametrics.js | 17 +- app/scripts/controllers/metametrics.test.js | 49 +- .../controllers/mmi-controller.test.ts | 13 +- app/scripts/controllers/mmi-controller.ts | 2 +- .../preferences-controller.test.ts | 653 ++++++++++++------ .../controllers/preferences-controller.ts | 642 +++++++++++------ app/scripts/lib/backup.js | 6 +- app/scripts/lib/backup.test.js | 24 +- .../createRPCMethodTrackingMiddleware.test.js | 10 +- app/scripts/lib/ppom/ppom-middleware.test.ts | 14 +- app/scripts/lib/ppom/ppom-middleware.ts | 5 +- app/scripts/metamask-controller.js | 156 ++--- app/scripts/metamask-controller.test.js | 26 +- .../files-to-convert.json | 2 - lavamoat/browserify/beta/policy.json | 6 + lavamoat/browserify/flask/policy.json | 6 + lavamoat/browserify/main/policy.json | 6 + lavamoat/browserify/mmi/policy.json | 6 + package.json | 1 + shared/constants/mmi-controller.ts | 2 +- test/e2e/default-fixture.js | 26 + test/e2e/fixture-builder.js | 28 + ...rs-after-init-opt-in-background-state.json | 4 +- .../errors-after-init-opt-in-ui-state.json | 2 + ...s-before-init-opt-in-background-state.json | 4 +- .../errors-before-init-opt-in-ui-state.json | 4 +- yarn.lock | 13 + 33 files changed, 1147 insertions(+), 646 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 64c51bd1e503..a53619b179ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -316,7 +316,7 @@ module.exports = { 'app/scripts/controllers/swaps/**/*.test.ts', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', - 'app/scripts/controllers/preferences.test.js', + 'app/scripts/controllers/preferences-controller.test.ts', 'app/scripts/lib/**/*.test.js', 'app/scripts/metamask-controller.test.js', 'app/scripts/migrations/*.test.js', diff --git a/app/scripts/background.js b/app/scripts/background.js index 4fbbee449160..7d9d0f5684a6 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -235,7 +235,7 @@ function maybeDetectPhishing(theController) { return {}; } - const prefState = theController.preferencesController.store.getState(); + const prefState = theController.preferencesController.state; if (!prefState.usePhishDetect) { return {}; } @@ -758,8 +758,7 @@ export function setupController( controller.preferencesController, ), getUseAddressBarEnsResolution: () => - controller.preferencesController.store.getState() - .useAddressBarEnsResolution, + controller.preferencesController.state.useAddressBarEnsResolution, provider: controller.provider, }); diff --git a/app/scripts/controllers/account-tracker-controller.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts index dbabb927fa71..ad33541fb5b6 100644 --- a/app/scripts/controllers/account-tracker-controller.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -5,7 +5,6 @@ import { BlockTracker, Provider } from '@metamask/network-controller'; import { flushPromises } from '../../../test/lib/timer-helpers'; import { createTestProviderTools } from '../../../test/stub/provider'; -import PreferencesController from './preferences-controller'; import type { AccountTrackerControllerOptions, AllowedActions, @@ -166,13 +165,9 @@ function withController( provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), - preferencesController: { - store: { - getState: () => ({ - useMultiAccountBalanceChecker, - }), - }, - } as PreferencesController, + preferencesControllerState: { + useMultiAccountBalanceChecker, + }, messenger: controllerMessenger.getRestricted({ name: 'AccountTrackerController', allowedActions: [ diff --git a/app/scripts/controllers/account-tracker-controller.ts b/app/scripts/controllers/account-tracker-controller.ts index e2c78ea3f3f9..ec4789189a0c 100644 --- a/app/scripts/controllers/account-tracker-controller.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -45,7 +45,7 @@ import type { OnboardingControllerGetStateAction, OnboardingControllerStateChangeEvent, } from './onboarding'; -import PreferencesController from './preferences-controller'; +import { PreferencesControllerState } from './preferences-controller'; // Unique name for the controller const controllerName = 'AccountTrackerController'; @@ -170,7 +170,7 @@ export type AccountTrackerControllerOptions = { provider: Provider; blockTracker: BlockTracker; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; - preferencesController: PreferencesController; + preferencesControllerState: Partial; }; /** @@ -198,7 +198,7 @@ export default class AccountTrackerController extends BaseController< #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesController: AccountTrackerControllerOptions['preferencesController']; + #preferencesControllerState: AccountTrackerControllerOptions['preferencesControllerState']; #selectedAccount: InternalAccount; @@ -209,7 +209,7 @@ export default class AccountTrackerController extends BaseController< * @param options.provider - An EIP-1193 provider instance that uses the current global network * @param options.blockTracker - A block tracker, which emits events for each new block * @param options.getNetworkIdentifier - A function that returns the current network or passed network configuration - * @param options.preferencesController - The preferences controller + * @param options.preferencesControllerState - The state of preferences controller */ constructor(options: AccountTrackerControllerOptions) { super({ @@ -226,7 +226,7 @@ export default class AccountTrackerController extends BaseController< this.#blockTracker = options.blockTracker; this.#getNetworkIdentifier = options.getNetworkIdentifier; - this.#preferencesController = options.preferencesController; + this.#preferencesControllerState = options.preferencesControllerState; // subscribe to account removal this.messagingSystem.subscribe( @@ -257,7 +257,7 @@ export default class AccountTrackerController extends BaseController< 'AccountsController:selectedEvmAccountChange', (newAccount) => { const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + this.#preferencesControllerState; if ( this.#selectedAccount.id !== newAccount.id && @@ -672,8 +672,7 @@ export default class AccountTrackerController extends BaseController< const { chainId, provider, identifier } = this.#getCorrectNetworkClient(networkClientId); - const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; let addresses = []; if (useMultiAccountBalanceChecker) { @@ -724,8 +723,7 @@ export default class AccountTrackerController extends BaseController< provider: Provider, chainId: Hex, ): Promise { - const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; let balance = '0x0'; diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 3d8f9d176fb6..9dabf2313e57 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -29,7 +29,7 @@ export default class AppStateController extends EventEmitter { isUnlocked, initState, onInactiveTimeout, - preferencesStore, + preferencesController, messenger, extension, } = opts; @@ -86,12 +86,18 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock = []; addUnlockListener(this.handleUnlock.bind(this)); - preferencesStore.subscribe(({ preferences }) => { - const currentState = this.store.getState(); - if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { - this._setInactiveTimeout(preferences.autoLockTimeLimit); - } - }); + messenger.subscribe( + 'PreferencesController:stateChange', + ({ preferences }) => { + const currentState = this.store.getState(); + if ( + preferences && + currentState.timeoutMinutes !== preferences.autoLockTimeLimit + ) { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + }, + ); messenger.subscribe( 'KeyringController:qrKeyringStateChange', @@ -101,7 +107,8 @@ export default class AppStateController extends EventEmitter { }), ); - const { preferences } = preferencesStore.getState(); + const { preferences } = preferencesController.state; + this._setInactiveTimeout(preferences.autoLockTimeLimit); this.messagingSystem = messenger; diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js index c9ce8243b05c..46fe87d29add 100644 --- a/app/scripts/controllers/app-state.test.js +++ b/app/scripts/controllers/app-state.test.js @@ -13,13 +13,12 @@ describe('AppStateController', () => { initState, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + preferencesController: { + state: { preferences: { autoLockTimeLimit: 0, }, - })), + }, }, messenger: { call: jest.fn(() => ({ diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 15f4fa9b7788..aa5546ef7899 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -118,8 +118,9 @@ export default class MetaMetricsController { * @param {object} options * @param {object} options.segment - an instance of analytics for tracking * events that conform to the new MetaMetrics tracking plan. - * @param {object} options.preferencesStore - The preferences controller store, used - * to access and subscribe to preferences that will be attached to events + * @param {object} options.preferencesControllerState - The state of preferences controller + * @param {Function} options.onPreferencesStateChange - Used to attach a listener to the + * stateChange event emitted by the PreferencesController * @param {Function} options.onNetworkDidChange - Used to attach a listener to the * networkDidChange event emitted by the networkController * @param {Function} options.getCurrentChainId - Gets the current chain id from the @@ -132,7 +133,8 @@ export default class MetaMetricsController { */ constructor({ segment, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, onNetworkDidChange, getCurrentChainId, version, @@ -148,16 +150,15 @@ export default class MetaMetricsController { captureException(err); } }; - const prefState = preferencesStore.getState(); this.chainId = getCurrentChainId(); - this.locale = prefState.currentLocale.replace('_', '-'); + this.locale = preferencesControllerState.currentLocale.replace('_', '-'); this.version = environment === 'production' ? version : `${version}-${environment}`; this.extension = extension; this.environment = environment; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - this.selectedAddress = prefState.selectedAddress; + this.selectedAddress = preferencesControllerState.selectedAddress; ///: END:ONLY_INCLUDE_IF const abandonedFragments = omitBy(initState?.fragments, 'persist'); @@ -181,8 +182,8 @@ export default class MetaMetricsController { }, }); - preferencesStore.subscribe(({ currentLocale }) => { - this.locale = currentLocale.replace('_', '-'); + onPreferencesStateChange(({ currentLocale }) => { + this.locale = currentLocale?.replace('_', '-'); }); onNetworkDidChange(() => { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index a0505700ef01..ca5602de33c8 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -74,22 +74,6 @@ const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, }; -function getMockPreferencesStore({ currentLocale = LOCALE } = {}) { - let preferencesStore = { - currentLocale, - }; - const subscribe = jest.fn(); - const updateState = (newState) => { - preferencesStore = { ...preferencesStore, ...newState }; - subscribe.mock.calls[0][0](preferencesStore); - }; - return { - getState: jest.fn().mockReturnValue(preferencesStore), - updateState, - subscribe, - }; -} - const SAMPLE_PERSISTED_EVENT = { id: 'testid', persist: true, @@ -117,7 +101,10 @@ function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, marketingCampaignCookieId = null, - preferencesStore = getMockPreferencesStore(), + preferencesControllerState = { currentLocale: LOCALE }, + onPreferencesStateChange = () => { + // do nothing + }, getCurrentChainId = () => FAKE_CHAIN_ID, onNetworkDidChange = () => { // do nothing @@ -128,7 +115,8 @@ function getMetaMetricsController({ segment: segmentInstance || segment, getCurrentChainId, onNetworkDidChange, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, version: '0.0.1', environment: 'test', initState: { @@ -209,11 +197,16 @@ describe('MetaMetricsController', function () { }); it('should update when preferences changes', function () { - const preferencesStore = getMockPreferencesStore(); + let subscribeListener; + const onPreferencesStateChange = (listener) => { + subscribeListener = listener; + }; const metaMetricsController = getMetaMetricsController({ - preferencesStore, + preferencesControllerState: { currentLocale: LOCALE }, + onPreferencesStateChange, }); - preferencesStore.updateState({ currentLocale: 'en_UK' }); + + subscribeListener({ currentLocale: 'en_UK' }); expect(metaMetricsController.locale).toStrictEqual('en-UK'); }); }); @@ -732,9 +725,11 @@ describe('MetaMetricsController', function () { it('should track a page view if isOptInPath is true and user not yet opted in', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -746,6 +741,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith( { @@ -765,9 +761,11 @@ describe('MetaMetricsController', function () { it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -790,6 +788,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith( { diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 348ccd40916b..dbef190a5573 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -203,10 +203,10 @@ describe('MMIController', function () { }); metaMetricsController = new MetaMetricsController({ - preferencesStore: { - getState: jest.fn().mockReturnValue({ currentLocale: 'en' }), - subscribe: jest.fn(), + preferencesControllerState: { + currentLocale: 'en' }, + onPreferencesStateChange: jest.fn(), getCurrentChainId: jest.fn(), onNetworkDidChange: jest.fn(), }); @@ -245,13 +245,12 @@ describe('MMIController', function () { initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + preferencesController: { + state: { preferences: { autoLockTimeLimit: 0, }, - })), + }, }, messenger: mockMessenger, }), diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index d0e905d673d8..2373484d4a6e 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -44,8 +44,8 @@ import { import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; +import { PreferencesController } from './preferences-controller'; import AccountTrackerController from './account-tracker-controller'; -import PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; type UpdateCustodianTransactionsParameters = { diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index f825c1eb5aee..9c28ed7c43a0 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -3,13 +3,7 @@ */ import { ControllerMessenger } from '@metamask/base-controller'; import { AccountsController } from '@metamask/accounts-controller'; -import { - KeyringControllerGetAccountsAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetKeyringForAccountAction, - KeyringControllerStateChangeEvent, - KeyringControllerAccountRemovedEvent, -} from '@metamask/keyring-controller'; +import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { Hex } from '@metamask/utils'; import { CHAIN_IDS } from '../../../shared/constants/network'; @@ -18,10 +12,10 @@ import { ThemeType } from '../../../shared/constants/preferences'; import type { AllowedActions, AllowedEvents, - PreferencesControllerActions, - PreferencesControllerEvents, + PreferencesControllerMessenger, + PreferencesControllerState, } from './preferences-controller'; -import PreferencesController from './preferences-controller'; +import { PreferencesController } from './preferences-controller'; const NETWORK_CONFIGURATION_DATA = mockNetworkState( { @@ -40,102 +34,104 @@ const NETWORK_CONFIGURATION_DATA = mockNetworkState( }, ).networkConfigurationsByChainId; -describe('preferences controller', () => { - let controllerMessenger: ControllerMessenger< - | PreferencesControllerActions - | AllowedActions - | KeyringControllerGetAccountsAction - | KeyringControllerGetKeyringsByTypeAction - | KeyringControllerGetKeyringForAccountAction, - | PreferencesControllerEvents +const setupController = ({ + state, +}: { + state?: Partial; +}) => { + const controllerMessenger = new ControllerMessenger< + AllowedActions, + | AllowedEvents | KeyringControllerStateChangeEvent - | KeyringControllerAccountRemovedEvent | SnapControllerStateChangeEvent - | AllowedEvents - >; - let preferencesController: PreferencesController; - let accountsController: AccountsController; - - beforeEach(() => { - controllerMessenger = new ControllerMessenger(); - - const accountsControllerMessenger = controllerMessenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); - - const mockAccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }; - accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: mockAccountsControllerState, - }); - - const preferencesMessenger = controllerMessenger.getRestricted({ + >(); + const preferencesControllerMessenger: PreferencesControllerMessenger = + controllerMessenger.getRestricted({ name: 'PreferencesController', allowedActions: [ - `AccountsController:setSelectedAccount`, - `AccountsController:getAccountByAddress`, - `AccountsController:setAccountName`, + 'AccountsController:getAccountByAddress', + 'AccountsController:setAccountName', + 'AccountsController:getSelectedAccount', + 'AccountsController:setSelectedAccount', + 'NetworkController:getState', ], - allowedEvents: [`AccountsController:stateChange`], + allowedEvents: ['AccountsController:stateChange'], }); - preferencesController = new PreferencesController({ - initLangCode: 'en_US', + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue({ networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - messenger: preferencesMessenger, - }); + }), + ); + const controller = new PreferencesController({ + messenger: preferencesControllerMessenger, + state, + }); + + const accountsControllerMessenger = controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'KeyringController:stateChange', + 'SnapController:stateChange', + ], + allowedActions: [], }); + const mockAccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; + const accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: mockAccountsControllerState, + }); + + return { + controller, + messenger: controllerMessenger, + accountsController, + }; +}; +describe('preferences controller', () => { describe('useBlockie', () => { it('defaults useBlockie to false', () => { - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - false, - ); + const { controller } = setupController({}); + expect(controller.state.useBlockie).toStrictEqual(false); }); it('setUseBlockie to true', () => { - preferencesController.setUseBlockie(true); - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - true, - ); + const { controller } = setupController({}); + controller.setUseBlockie(true); + expect(controller.state.useBlockie).toStrictEqual(true); }); }); describe('setCurrentLocale', () => { it('checks the default currentLocale', () => { - const { currentLocale } = preferencesController.store.getState(); - expect(currentLocale).toStrictEqual('en_US'); + const { controller } = setupController({}); + const { currentLocale } = controller.state; + expect(currentLocale).toStrictEqual(''); }); it('sets current locale in preferences controller', () => { - preferencesController.setCurrentLocale('ja'); - const { currentLocale } = preferencesController.store.getState(); + const { controller } = setupController({}); + controller.setCurrentLocale('ja'); + const { currentLocale } = controller.state; expect(currentLocale).toStrictEqual('ja'); }); }); describe('setAccountLabel', () => { + const { controller, messenger, accountsController } = setupController({}); const mockName = 'mockName'; const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; it('updating name from preference controller will update the name in accounts controller and preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -150,21 +146,20 @@ describe('preferences controller', () => { ); let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; expect(firstAccount.metadata.name).toBe(firstPreferenceAccount.name); expect(secondAccount.metadata.name).toBe(secondPreferenceAccount.name); - preferencesController.setAccountLabel(firstAccount.address, mockName); + controller.setAccountLabel(firstAccount.address, mockName); // refresh state after state changed [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -181,7 +176,7 @@ describe('preferences controller', () => { }); it('updating name from accounts controller updates the name in preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -197,7 +192,7 @@ describe('preferences controller', () => { let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; @@ -210,8 +205,7 @@ describe('preferences controller', () => { [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -229,10 +223,11 @@ describe('preferences controller', () => { }); describe('setSelectedAddress', () => { + const { controller, messenger, accountsController } = setupController({}); it('updating selectedAddress from preferences controller updates the selectedAccount in accounts controller and preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -248,25 +243,26 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); - preferencesController.setSelectedAddress(secondAddress); + controller.setSelectedAddress(secondAddress); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); expect(updatedSelectedAddress).toBe(updatedSelectedAccount.address); + + expect(controller.getSelectedAddress()).toBe(secondAddress); }); it('updating selectedAccount from accounts controller updates the selectedAddress in preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -283,15 +279,14 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); const accounts = accountsController.listAccounts(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); accountsController.setSelectedAccount(accounts[1].id); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); @@ -300,173 +295,142 @@ describe('preferences controller', () => { }); describe('setPasswordForgotten', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(false); + expect(controller.state.forgottenPassword).toStrictEqual(false); }); it('should set the forgottenPassword property in state', () => { - preferencesController.setPasswordForgotten(true); - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(true); + controller.setPasswordForgotten(true); + expect(controller.state.forgottenPassword).toStrictEqual(true); }); }); describe('setUsePhishDetect', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); }); it('should set the usePhishDetect property in state', () => { - preferencesController.setUsePhishDetect(false); - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(false); + controller.setUsePhishDetect(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); }); }); describe('setUseMultiAccountBalanceChecker', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(true); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + true, + ); }); it('should set the setUseMultiAccountBalanceChecker property in state', () => { - preferencesController.setUseMultiAccountBalanceChecker(false); - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(false); + controller.setUseMultiAccountBalanceChecker(false); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + false, + ); }); }); describe('isRedesignedConfirmationsFeatureEnabled', () => { + const { controller } = setupController({}); it('isRedesignedConfirmationsFeatureEnabled should default to false', () => { expect( - preferencesController.store.getState().preferences - .isRedesignedConfirmationsDeveloperEnabled, + controller.state.preferences.isRedesignedConfirmationsDeveloperEnabled, ).toStrictEqual(false); }); }); describe('setUseSafeChainsListValidation', function () { + const { controller } = setupController({}); it('should default to true', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useSafeChainsListValidation).toStrictEqual(true); }); it('should set the `setUseSafeChainsListValidation` property in state', function () { - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(true); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(true); - preferencesController.setUseSafeChainsListValidation(false); + controller.setUseSafeChainsListValidation(false); - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(false); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(false); }); }); describe('setUseTokenDetection', function () { + const { controller } = setupController({}); it('should default to true for new users', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useTokenDetection).toStrictEqual(true); }); it('should set the useTokenDetection property in state', () => { - preferencesController.setUseTokenDetection(true); - expect( - preferencesController.store.getState().useTokenDetection, - ).toStrictEqual(true); + controller.setUseTokenDetection(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); }); it('should keep initial value of useTokenDetection for existing users', function () { - // TODO: Remove unregisterActionHandler and clearEventSubscriptions once the PreferencesController has been refactored to use the withController pattern. - controllerMessenger.unregisterActionHandler( - 'PreferencesController:getState', - ); - controllerMessenger.clearEventSubscriptions( - 'PreferencesController:stateChange', - ); - const preferencesControllerExistingUser = new PreferencesController({ - messenger: controllerMessenger.getRestricted({ - name: 'PreferencesController', - allowedActions: [], - allowedEvents: ['AccountsController:stateChange'], - }), - initLangCode: 'en_US', - initState: { - useTokenDetection: false, + const { controller: preferencesControllerExistingUser } = setupController( + { + state: { + useTokenDetection: false, + }, }, - networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - }); - const state = preferencesControllerExistingUser.store.getState(); + ); + const { state } = preferencesControllerExistingUser; expect(state.useTokenDetection).toStrictEqual(false); }); }); describe('setUseNftDetection', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); it('should set the useNftDetection property in state', () => { - preferencesController.setOpenSeaEnabled(true); - preferencesController.setUseNftDetection(true); - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + controller.setUseNftDetection(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); }); describe('setUse4ByteResolution', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(true); + expect(controller.state.use4ByteResolution).toStrictEqual(true); }); it('should set the use4ByteResolution property in state', () => { - preferencesController.setUse4ByteResolution(false); - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(false); + controller.setUse4ByteResolution(false); + expect(controller.state.use4ByteResolution).toStrictEqual(false); }); }); describe('setOpenSeaEnabled', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); it('should set the openSeaEnabled property in state', () => { - preferencesController.setOpenSeaEnabled(true); - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); }); describe('setAdvancedGasFee', () => { + const { controller } = setupController({}); it('should default to an empty object', () => { - expect( - preferencesController.store.getState().advancedGasFee, - ).toStrictEqual({}); + expect(controller.state.advancedGasFee).toStrictEqual({}); }); it('should set the setAdvancedGasFee property in state', () => { - preferencesController.setAdvancedGasFee({ + controller.setAdvancedGasFee({ chainId: CHAIN_IDS.GOERLI, gasFeePreferences: { maxBaseFee: '1.5', @@ -474,51 +438,44 @@ describe('preferences controller', () => { }, }); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .maxBaseFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].maxBaseFee, ).toStrictEqual('1.5'); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .priorityFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].priorityFee, ).toStrictEqual('2'); }); }); describe('setTheme', () => { + const { controller } = setupController({}); it('should default to value "OS"', () => { - expect(preferencesController.store.getState().theme).toStrictEqual('os'); + expect(controller.state.theme).toStrictEqual('os'); }); it('should set the setTheme property in state', () => { - preferencesController.setTheme(ThemeType.dark); - expect(preferencesController.store.getState().theme).toStrictEqual( - 'dark', - ); + controller.setTheme(ThemeType.dark); + expect(controller.state.theme).toStrictEqual('dark'); }); }); describe('setUseCurrencyRateCheck', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); }); it('should set the useCurrencyRateCheck property in state', () => { - preferencesController.setUseCurrencyRateCheck(false); - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(false); + controller.setUseCurrencyRateCheck(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); }); }); describe('setIncomingTransactionsPreferences', () => { + const { controller } = setupController({}); const addedNonTestNetworks = Object.keys(NETWORK_CONFIGURATION_DATA); it('should have default value combined', () => { - const state: { - incomingTransactionsPreferences: Record; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, @@ -533,13 +490,11 @@ describe('preferences controller', () => { }); it('should update incomingTransactionsPreferences with given value set', () => { - preferencesController.setIncomingTransactionsPreferences( + controller.setIncomingTransactionsPreferences( CHAIN_IDS.LINEA_MAINNET, false, ); - const state: { - incomingTransactionsPreferences: Record; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: false, @@ -555,10 +510,11 @@ describe('preferences controller', () => { }); describe('AccountsController:stateChange subscription', () => { + const { controller, messenger, accountsController } = setupController({}); it('sync the identities with the accounts in the accounts controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -574,7 +530,7 @@ describe('preferences controller', () => { const accounts = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; expect(accounts.map((account) => account.address)).toStrictEqual( Object.keys(identities), @@ -584,68 +540,313 @@ describe('preferences controller', () => { ///: BEGIN:ONLY_INCLUDE_IF(petnames) describe('setUseExternalNameSources', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the useExternalNameSources property in state', () => { - preferencesController.setUseExternalNameSources(false); - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(false); + controller.setUseExternalNameSources(false); + expect(controller.state.useExternalNameSources).toStrictEqual(false); }); }); ///: END:ONLY_INCLUDE_IF describe('setUseTransactionSimulations', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the setUseTransactionSimulations property in state', () => { - preferencesController.setUseTransactionSimulations(false); - expect( - preferencesController.store.getState().useTransactionSimulations, - ).toStrictEqual(false); + controller.setUseTransactionSimulations(false); + expect(controller.state.useTransactionSimulations).toStrictEqual(false); }); }); describe('setServiceWorkerKeepAlivePreference', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(true); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(true); }); it('should set the setServiceWorkerKeepAlivePreference property in state', () => { - preferencesController.setServiceWorkerKeepAlivePreference(false); - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(false); + controller.setServiceWorkerKeepAlivePreference(false); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(false); }); }); describe('setBitcoinSupportEnabled', () => { + const { controller } = setupController({}); it('has the default value as false', () => { - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); }); it('sets the bitcoinSupportEnabled property in state to true and then false', () => { - preferencesController.setBitcoinSupportEnabled(true); + controller.setBitcoinSupportEnabled(true); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(true); + + controller.setBitcoinSupportEnabled(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); + }); + }); + + describe('useNonceField', () => { + it('defaults useNonceField to false', () => { + const { controller } = setupController({}); + expect(controller.state.useNonceField).toStrictEqual(false); + }); + + it('setUseNonceField to true', () => { + const { controller } = setupController({}); + controller.setUseNonceField(true); + expect(controller.state.useNonceField).toStrictEqual(true); + }); + }); + + describe('globalThis.setPreference', () => { + it('setFeatureFlags to true', () => { + const { controller } = setupController({}); + globalThis.setPreference('showFiatInTestnets', true); + expect(controller.state.featureFlags.showFiatInTestnets).toStrictEqual( + true, + ); + }); + }); + + describe('useExternalServices', () => { + it('defaults useExternalServices to true', () => { + const { controller } = setupController({}); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); + }); + + it('useExternalServices to false', () => { + const { controller } = setupController({}); + controller.toggleExternalServices(false); + expect(controller.state.useExternalServices).toStrictEqual(false); + expect(controller.state.useTokenDetection).toStrictEqual(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + expect(controller.state.openSeaEnabled).toStrictEqual(false); + expect(controller.state.useNftDetection).toStrictEqual(false); + }); + }); + + describe('useRequestQueue', () => { + it('defaults useRequestQueue to true', () => { + const { controller } = setupController({}); + expect(controller.state.useRequestQueue).toStrictEqual(true); + }); + + it('setUseRequestQueue to false', () => { + const { controller } = setupController({}); + controller.setUseRequestQueue(false); + expect(controller.state.useRequestQueue).toStrictEqual(false); + }); + }); + + describe('addSnapAccountEnabled', () => { + it('defaults addSnapAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(false); + }); + + it('setAddSnapAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setAddSnapAccountEnabled(true); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(true); + }); + }); + + describe('watchEthereumAccountEnabled', () => { + it('defaults watchEthereumAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(false); + }); + + it('setWatchEthereumAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setWatchEthereumAccountEnabled(true); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(true); + }); + }); + + describe('bitcoinTestnetSupportEnabled', () => { + it('defaults bitcoinTestnetSupportEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual( + false, + ); + }); + + it('setBitcoinTestnetSupportEnabled to true', () => { + const { controller } = setupController({}); + controller.setBitcoinTestnetSupportEnabled(true); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual(true); + }); + }); + + describe('knownMethodData', () => { + it('defaults knownMethodData', () => { + const { controller } = setupController({}); + expect(controller.state.knownMethodData).toStrictEqual({}); + }); + + it('addKnownMethodData', () => { + const { controller } = setupController({}); + controller.addKnownMethodData('0x60806040', 'testMethodName'); + expect(controller.state.knownMethodData).toStrictEqual({ + '0x60806040': 'testMethodName', + }); + }); + }); + + describe('featureFlags', () => { + it('defaults featureFlags', () => { + const { controller } = setupController({}); + expect(controller.state.featureFlags).toStrictEqual({}); + }); + + it('setFeatureFlags', () => { + const { controller } = setupController({}); + controller.setFeatureFlag('showConfirmationAdvancedDetails', true); expect( - preferencesController.store.getState().bitcoinSupportEnabled, + controller.state.featureFlags.showConfirmationAdvancedDetails, ).toStrictEqual(true); + }); + }); - preferencesController.setBitcoinSupportEnabled(false); - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + describe('preferences', () => { + it('defaults preferences', () => { + const { controller } = setupController({}); + expect(controller.state.preferences).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + + it('setPreference', () => { + const { controller } = setupController({}); + controller.setPreference('showConfirmationAdvancedDetails', true); + expect(controller.getPreferences()).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: true, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + }); + + describe('ipfsGateway', () => { + it('defaults ipfsGate to dweb.link', () => { + const { controller } = setupController({}); + expect(controller.state.ipfsGateway).toStrictEqual('dweb.link'); + }); + + it('setIpfsGateway to test.link', () => { + const { controller } = setupController({}); + controller.setIpfsGateway('test.link'); + expect(controller.getIpfsGateway()).toStrictEqual('test.link'); + }); + }); + + describe('isIpfsGatewayEnabled', () => { + it('defaults isIpfsGatewayEnabled to true', () => { + const { controller } = setupController({}); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(true); + }); + + it('set isIpfsGatewayEnabled to false', () => { + const { controller } = setupController({}); + controller.setIsIpfsGatewayEnabled(false); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(false); + }); + }); + + describe('useAddressBarEnsResolution', () => { + it('defaults useAddressBarEnsResolution to true', () => { + const { controller } = setupController({}); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + }); + + it('set useAddressBarEnsResolution to false', () => { + const { controller } = setupController({}); + controller.setUseAddressBarEnsResolution(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + }); + }); + + describe('dismissSeedBackUpReminder', () => { + it('defaults dismissSeedBackUpReminder to false', () => { + const { controller } = setupController({}); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(false); + }); + + it('set dismissSeedBackUpReminder to true', () => { + const { controller } = setupController({}); + controller.setDismissSeedBackUpReminder(true); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(true); + }); + }); + + describe('snapsAddSnapAccountModalDismissed', () => { + it('defaults snapsAddSnapAccountModalDismissed to false', () => { + const { controller } = setupController({}); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + false, + ); + }); + + it('set snapsAddSnapAccountModalDismissed to true', () => { + const { controller } = setupController({}); + controller.setSnapsAddSnapAccountModalDismissed(true); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + true, + ); }); }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index eb126b176a41..a7ede69bb26c 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -1,4 +1,3 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerChangeEvent, AccountsControllerGetAccountByAddressAction, @@ -8,7 +7,18 @@ import { AccountsControllerState, } from '@metamask/accounts-controller'; import { Hex } from '@metamask/utils'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { Json } from 'json-rpc-engine'; +import { NetworkControllerGetStateAction } from '@metamask/network-controller'; +import { + ETHERSCAN_SUPPORTED_CHAIN_IDS, + type PreferencesState, +} from '@metamask/preferences-controller'; import { CHAIN_IDS, IPFS_DEFAULT_GATEWAY_URL, @@ -19,7 +29,7 @@ import { ThemeType } from '../../../shared/constants/preferences'; type AccountIdentityEntry = { address: string; name: string; - lastSelected: number | undefined; + lastSelected?: number; }; const mainNetworks = { @@ -38,10 +48,10 @@ const controllerName = 'PreferencesController'; /** * Returns the state of the {@link PreferencesController}. */ -export type PreferencesControllerGetStateAction = { - type: 'PreferencesController:getState'; - handler: () => PreferencesControllerState; -}; +export type PreferencesControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + PreferencesControllerState +>; /** * Actions exposed by the {@link PreferencesController}. @@ -51,10 +61,10 @@ export type PreferencesControllerActions = PreferencesControllerGetStateAction; /** * Event emitted when the state of the {@link PreferencesController} changes. */ -export type PreferencesControllerStateChangeEvent = { - type: 'PreferencesController:stateChange'; - payload: [PreferencesControllerState, []]; -}; +export type PreferencesControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + PreferencesControllerState +>; /** * Events emitted by {@link PreferencesController}. @@ -68,7 +78,8 @@ export type AllowedActions = | AccountsControllerGetAccountByAddressAction | AccountsControllerSetAccountNameAction | AccountsControllerGetSelectedAccountAction - | AccountsControllerSetSelectedAccountAction; + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction; /** * Events that this controller is allowed to subscribe. @@ -84,9 +95,7 @@ export type PreferencesControllerMessenger = RestrictedControllerMessenger< >; type PreferencesControllerOptions = { - networkConfigurationsByChainId?: Record; - initState?: Partial; - initLangCode?: string; + state?: Partial; messenger: PreferencesControllerMessenger; }; @@ -114,176 +123,356 @@ export type Preferences = { shouldShowAggregatedBalancePopover: boolean; }; -export type PreferencesControllerState = { - selectedAddress: string; +// Omitting showTestNetworks and smartTransactionsOptInStatus, as they already exists here in Preferences type +export type PreferencesControllerState = Omit< + PreferencesState, + 'showTestNetworks' | 'smartTransactionsOptInStatus' +> & { useBlockie: boolean; useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; - useTokenDetection: boolean; - useNftDetection: boolean; use4ByteResolution: boolean; useCurrencyRateCheck: boolean; useRequestQueue: boolean; - openSeaEnabled: boolean; - securityAlertsEnabled: boolean; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; - addSnapAccountEnabled: boolean; + addSnapAccountEnabled?: boolean; advancedGasFee: Record>; - featureFlags: Record; incomingTransactionsPreferences: Record; knownMethodData: Record; currentLocale: string; - identities: Record; - lostIdentities: Record; forgottenPassword: boolean; preferences: Preferences; - ipfsGateway: string; - isIpfsGatewayEnabled: boolean; useAddressBarEnsResolution: boolean; ledgerTransportType: LedgerTransportTypes; - snapRegistryList: Record; + // TODO: Replace `Json` with correct type + snapRegistryList: Record; theme: ThemeType; - snapsAddSnapAccountModalDismissed: boolean; + snapsAddSnapAccountModalDismissed?: boolean; useExternalNameSources: boolean; - useTransactionSimulations: boolean; enableMV3TimestampSave: boolean; useExternalServices: boolean; textDirection?: string; }; -export default class PreferencesController { - store: ObservableStore; +/** + * Function to get default state of the {@link PreferencesController}. + */ +export const getDefaultPreferencesControllerState = + (): PreferencesControllerState => ({ + selectedAddress: '', + useBlockie: false, + useNonceField: false, + usePhishDetect: true, + dismissSeedBackUpReminder: false, + useMultiAccountBalanceChecker: true, + useSafeChainsListValidation: true, + // set to true means the dynamic list from the API is being used + // set to false will be using the static list from contract-metadata + useTokenDetection: true, + useNftDetection: true, + use4ByteResolution: true, + useCurrencyRateCheck: true, + useRequestQueue: true, + openSeaEnabled: true, + securityAlertsEnabled: true, + watchEthereumAccountEnabled: false, + bitcoinSupportEnabled: false, + bitcoinTestnetSupportEnabled: false, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + addSnapAccountEnabled: false, + ///: END:ONLY_INCLUDE_IF + advancedGasFee: {}, + featureFlags: {}, + incomingTransactionsPreferences: { + ...mainNetworks, + ...testNetworks, + }, + knownMethodData: {}, + currentLocale: '', + identities: {}, + lostIdentities: {}, + forgottenPassword: false, + preferences: { + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + shouldShowAggregatedBalancePopover: true, // by default user should see popover; + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + // ENS decentralized website resolution + ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, + isIpfsGatewayEnabled: true, + useAddressBarEnsResolution: true, + // Ledger transport type is deprecated. We currently only support webhid + // on chrome, and u2f on firefox. + ledgerTransportType: window.navigator.hid + ? LedgerTransportTypes.webhid + : LedgerTransportTypes.u2f, + snapRegistryList: {}, + theme: ThemeType.os, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + snapsAddSnapAccountModalDismissed: false, + ///: END:ONLY_INCLUDE_IF + useExternalNameSources: true, + useTransactionSimulations: true, + enableMV3TimestampSave: true, + // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. + // Whenever useExternalServices is false, certain features will be disabled. + // The flag is true by Default, meaning the toggle is ON by default. + useExternalServices: true, + // from core PreferencesController + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + }); - private messagingSystem: PreferencesControllerMessenger; +/** + * {@link PreferencesController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + selectedAddress: { + persist: true, + anonymous: false, + }, + useBlockie: { + persist: true, + anonymous: true, + }, + useNonceField: { + persist: true, + anonymous: true, + }, + usePhishDetect: { + persist: true, + anonymous: true, + }, + dismissSeedBackUpReminder: { + persist: true, + anonymous: true, + }, + useMultiAccountBalanceChecker: { + persist: true, + anonymous: true, + }, + useSafeChainsListValidation: { + persist: true, + anonymous: false, + }, + useTokenDetection: { + persist: true, + anonymous: true, + }, + useNftDetection: { + persist: true, + anonymous: true, + }, + use4ByteResolution: { + persist: true, + anonymous: true, + }, + useCurrencyRateCheck: { + persist: true, + anonymous: true, + }, + useRequestQueue: { + persist: true, + anonymous: true, + }, + openSeaEnabled: { + persist: true, + anonymous: true, + }, + securityAlertsEnabled: { + persist: true, + anonymous: false, + }, + watchEthereumAccountEnabled: { + persist: true, + anonymous: false, + }, + bitcoinSupportEnabled: { + persist: true, + anonymous: false, + }, + bitcoinTestnetSupportEnabled: { + persist: true, + anonymous: false, + }, + addSnapAccountEnabled: { + persist: true, + anonymous: false, + }, + advancedGasFee: { + persist: true, + anonymous: true, + }, + featureFlags: { + persist: true, + anonymous: true, + }, + incomingTransactionsPreferences: { + persist: true, + anonymous: true, + }, + knownMethodData: { + persist: true, + anonymous: false, + }, + currentLocale: { + persist: true, + anonymous: true, + }, + identities: { + persist: true, + anonymous: false, + }, + lostIdentities: { + persist: true, + anonymous: false, + }, + forgottenPassword: { + persist: true, + anonymous: true, + }, + preferences: { + persist: true, + anonymous: true, + }, + ipfsGateway: { + persist: true, + anonymous: false, + }, + isIpfsGatewayEnabled: { + persist: true, + anonymous: false, + }, + useAddressBarEnsResolution: { + persist: true, + anonymous: true, + }, + ledgerTransportType: { + persist: true, + anonymous: true, + }, + snapRegistryList: { + persist: true, + anonymous: false, + }, + theme: { + persist: true, + anonymous: true, + }, + snapsAddSnapAccountModalDismissed: { + persist: true, + anonymous: false, + }, + useExternalNameSources: { + persist: true, + anonymous: false, + }, + useTransactionSimulations: { + persist: true, + anonymous: true, + }, + enableMV3TimestampSave: { + persist: true, + anonymous: true, + }, + useExternalServices: { + persist: true, + anonymous: false, + }, + textDirection: { + persist: true, + anonymous: false, + }, + isMultiAccountBalancesEnabled: { persist: true, anonymous: true }, + showIncomingTransactions: { persist: true, anonymous: true }, +}; +export class PreferencesController extends BaseController< + typeof controllerName, + PreferencesControllerState, + PreferencesControllerMessenger +> { /** + * Constructs a Preferences controller. * - * @param opts - Overrides the defaults for the initial state of this.store - * @property messenger - The controller messenger - * @property initState The stored object containing a users preferences, stored in local storage - * @property initState.useBlockie The users preference for blockie identicons within the UI - * @property initState.useNonceField The users preference for nonce field within the UI - * @property initState.featureFlags A key-boolean map, where keys refer to features and booleans to whether the - * user wishes to see that feature. - * - * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior. - * @property initState.knownMethodData Contains all data methods known by the user - * @property initState.currentLocale The preferred language locale key - * @property initState.selectedAddress A hex string that matches the currently selected address in the app + * @param options - the controller options + * @param options.messenger - The controller messenger + * @param options.state - The initial controller state */ - constructor(opts: PreferencesControllerOptions) { + constructor({ messenger, state }: PreferencesControllerOptions) { + const { networkConfigurationsByChainId } = messenger.call( + 'NetworkController:getState', + ); + const addedNonMainNetwork: Record = Object.values( - opts.networkConfigurationsByChainId ?? {}, + networkConfigurationsByChainId ?? {}, ).reduce((acc: Record, element) => { acc[element.chainId] = true; return acc; }, {}); - - const initState: PreferencesControllerState = { - selectedAddress: '', - useBlockie: false, - useNonceField: false, - usePhishDetect: true, - dismissSeedBackUpReminder: false, - useMultiAccountBalanceChecker: true, - useSafeChainsListValidation: true, - // set to true means the dynamic list from the API is being used - // set to false will be using the static list from contract-metadata - useTokenDetection: opts?.initState?.useTokenDetection ?? true, - useNftDetection: opts?.initState?.useTokenDetection ?? true, - use4ByteResolution: true, - useCurrencyRateCheck: true, - useRequestQueue: true, - openSeaEnabled: true, - securityAlertsEnabled: true, - watchEthereumAccountEnabled: false, - bitcoinSupportEnabled: false, - bitcoinTestnetSupportEnabled: false, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - addSnapAccountEnabled: false, - ///: END:ONLY_INCLUDE_IF - advancedGasFee: {}, - - // WARNING: Do not use feature flags for security-sensitive things. - // Feature flag toggling is available in the global namespace - // for convenient testing of pre-release features, and should never - // perform sensitive operations. - featureFlags: {}, - incomingTransactionsPreferences: { - ...mainNetworks, - ...addedNonMainNetwork, - ...testNetworks, - }, - knownMethodData: {}, - currentLocale: opts.initLangCode ?? '', - identities: {}, - lostIdentities: {}, - forgottenPassword: false, - preferences: { - autoLockTimeLimit: undefined, - showExtensionInFullSizeView: false, - showFiatInTestnets: false, - showTestNetworks: false, - smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible - showNativeTokenAsMainBalance: false, - useNativeCurrencyAsPrimaryCurrency: true, - hideZeroBalanceTokens: false, - petnamesEnabled: true, - redesignedConfirmationsEnabled: true, - redesignedTransactionsEnabled: true, - featureNotificationsEnabled: false, - showMultiRpcModal: false, - isRedesignedConfirmationsDeveloperEnabled: false, - showConfirmationAdvancedDetails: false, - tokenSortConfig: { - key: 'tokenFiatAmount', - order: 'dsc', - sortCallback: 'stringNumeric', + super({ + messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultPreferencesControllerState(), + incomingTransactionsPreferences: { + ...mainNetworks, + ...addedNonMainNetwork, + ...testNetworks, }, - shouldShowAggregatedBalancePopover: true, // by default user should see popover; + ...state, }, - // ENS decentralized website resolution - ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - isIpfsGatewayEnabled: true, - useAddressBarEnsResolution: true, - // Ledger transport type is deprecated. We currently only support webhid - // on chrome, and u2f on firefox. - ledgerTransportType: window.navigator.hid - ? LedgerTransportTypes.webhid - : LedgerTransportTypes.u2f, - snapRegistryList: {}, - theme: ThemeType.os, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - snapsAddSnapAccountModalDismissed: false, - ///: END:ONLY_INCLUDE_IF - useExternalNameSources: true, - useTransactionSimulations: true, - enableMV3TimestampSave: true, - // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. - // Whenever useExternalServices is false, certain features will be disabled. - // The flag is true by Default, meaning the toggle is ON by default. - useExternalServices: true, - ...opts.initState, - }; - - this.store = new ObservableStore(initState); - this.store.setMaxListeners(13); - - this.messagingSystem = opts.messenger; - this.messagingSystem.registerActionHandler( - `PreferencesController:getState`, - () => this.store.getState(), - ); - this.messagingSystem.registerInitialEventPayload({ - eventType: `PreferencesController:stateChange`, - getPayload: () => [this.store.getState(), []], }); this.messagingSystem.subscribe( @@ -302,7 +491,9 @@ export default class PreferencesController { * @param forgottenPassword - whether or not the user has forgotten their password */ setPasswordForgotten(forgottenPassword: boolean): void { - this.store.updateState({ forgottenPassword }); + this.update((state) => { + state.forgottenPassword = forgottenPassword; + }); } /** @@ -311,7 +502,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers blockie indicators */ setUseBlockie(val: boolean): void { - this.store.updateState({ useBlockie: val }); + this.update((state) => { + state.useBlockie = val; + }); } /** @@ -320,7 +513,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to set nonce */ setUseNonceField(val: boolean): void { - this.store.updateState({ useNonceField: val }); + this.update((state) => { + state.useNonceField = val; + }); } /** @@ -329,7 +524,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers phishing domain protection */ setUsePhishDetect(val: boolean): void { - this.store.updateState({ usePhishDetect: val }); + this.update((state) => { + state.usePhishDetect = val; + }); } /** @@ -338,7 +535,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on all security settings */ setUseMultiAccountBalanceChecker(val: boolean): void { - this.store.updateState({ useMultiAccountBalanceChecker: val }); + this.update((state) => { + state.useMultiAccountBalanceChecker = val; + }); } /** @@ -347,11 +546,15 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on validation for manually adding networks */ setUseSafeChainsListValidation(val: boolean): void { - this.store.updateState({ useSafeChainsListValidation: val }); + this.update((state) => { + state.useSafeChainsListValidation = val; + }); } toggleExternalServices(useExternalServices: boolean): void { - this.store.updateState({ useExternalServices }); + this.update((state) => { + state.useExternalServices = useExternalServices; + }); this.setUseTokenDetection(useExternalServices); this.setUseCurrencyRateCheck(useExternalServices); this.setUsePhishDetect(useExternalServices); @@ -366,7 +569,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use the static token list or dynamic token list from the API */ setUseTokenDetection(val: boolean): void { - this.store.updateState({ useTokenDetection: val }); + this.update((state) => { + state.useTokenDetection = val; + }); } /** @@ -375,7 +580,9 @@ export default class PreferencesController { * @param useNftDetection - Whether or not the user prefers to autodetect NFTs. */ setUseNftDetection(useNftDetection: boolean): void { - this.store.updateState({ useNftDetection }); + this.update((state) => { + state.useNftDetection = useNftDetection; + }); } /** @@ -384,7 +591,9 @@ export default class PreferencesController { * @param use4ByteResolution - (Privacy) Whether or not the user prefers to have smart contract name details resolved with 4byte.directory */ setUse4ByteResolution(use4ByteResolution: boolean): void { - this.store.updateState({ use4ByteResolution }); + this.update((state) => { + state.use4ByteResolution = use4ByteResolution; + }); } /** @@ -393,7 +602,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use currency rate check for ETH and tokens. */ setUseCurrencyRateCheck(val: boolean): void { - this.store.updateState({ useCurrencyRateCheck: val }); + this.update((state) => { + state.useCurrencyRateCheck = val; + }); } /** @@ -402,7 +613,9 @@ export default class PreferencesController { * @param val - Whether or not the user wants to have requests queued if network change is required. */ setUseRequestQueue(val: boolean): void { - this.store.updateState({ useRequestQueue: val }); + this.update((state) => { + state.useRequestQueue = val; + }); } /** @@ -411,8 +624,8 @@ export default class PreferencesController { * @param openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data. */ setOpenSeaEnabled(openSeaEnabled: boolean): void { - this.store.updateState({ - openSeaEnabled, + this.update((state) => { + state.openSeaEnabled = openSeaEnabled; }); } @@ -422,8 +635,8 @@ export default class PreferencesController { * @param securityAlertsEnabled - Whether or not the user prefers to use the security alerts. */ setSecurityAlertsEnabled(securityAlertsEnabled: boolean): void { - this.store.updateState({ - securityAlertsEnabled, + this.update((state) => { + state.securityAlertsEnabled = securityAlertsEnabled; }); } @@ -435,8 +648,8 @@ export default class PreferencesController { * enable the "Add Snap accounts" button. */ setAddSnapAccountEnabled(addSnapAccountEnabled: boolean): void { - this.store.updateState({ - addSnapAccountEnabled, + this.update((state) => { + state.addSnapAccountEnabled = addSnapAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -449,8 +662,8 @@ export default class PreferencesController { * enable the "Watch Ethereum account (Beta)" button. */ setWatchEthereumAccountEnabled(watchEthereumAccountEnabled: boolean): void { - this.store.updateState({ - watchEthereumAccountEnabled, + this.update((state) => { + state.watchEthereumAccountEnabled = watchEthereumAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -462,8 +675,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Beta)" button. */ setBitcoinSupportEnabled(bitcoinSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinSupportEnabled, + this.update((state) => { + state.bitcoinSupportEnabled = bitcoinSupportEnabled; }); } @@ -474,8 +687,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Testnet)" button. */ setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinTestnetSupportEnabled, + this.update((state) => { + state.bitcoinTestnetSupportEnabled = bitcoinTestnetSupportEnabled; }); } @@ -485,8 +698,8 @@ export default class PreferencesController { * @param useExternalNameSources - Whether or not to use external name providers in the name controller. */ setUseExternalNameSources(useExternalNameSources: boolean): void { - this.store.updateState({ - useExternalNameSources, + this.update((state) => { + state.useExternalNameSources = useExternalNameSources; }); } @@ -496,8 +709,8 @@ export default class PreferencesController { * @param useTransactionSimulations - Whether or not to use simulations in the transaction confirmations. */ setUseTransactionSimulations(useTransactionSimulations: boolean): void { - this.store.updateState({ - useTransactionSimulations, + this.update((state) => { + state.useTransactionSimulations = useTransactionSimulations; }); } @@ -515,12 +728,12 @@ export default class PreferencesController { chainId: string; gasFeePreferences: Record; }): void { - const { advancedGasFee } = this.store.getState(); - this.store.updateState({ - advancedGasFee: { + const { advancedGasFee } = this.state; + this.update((state) => { + state.advancedGasFee = { ...advancedGasFee, [chainId]: gasFeePreferences, - }, + }; }); } @@ -530,7 +743,9 @@ export default class PreferencesController { * @param val - 'default' or 'dark' value based on the mode selected by user. */ setTheme(val: ThemeType): void { - this.store.updateState({ theme: val }); + this.update((state) => { + state.theme = val; + }); } /** @@ -540,12 +755,14 @@ export default class PreferencesController { * @param methodData - Corresponding data method */ addKnownMethodData(fourBytePrefix: string, methodData: string): void { - const { knownMethodData } = this.store.getState(); + const { knownMethodData } = this.state; const updatedKnownMethodData = { ...knownMethodData }; updatedKnownMethodData[fourBytePrefix] = methodData; - this.store.updateState({ knownMethodData: updatedKnownMethodData }); + this.update((state) => { + state.knownMethodData = updatedKnownMethodData; + }); } /** @@ -557,9 +774,9 @@ export default class PreferencesController { const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key) ? 'rtl' : 'auto'; - this.store.updateState({ - currentLocale: key, - textDirection, + this.update((state) => { + state.currentLocale = key; + state.textDirection = textDirection; }); return textDirection; } @@ -605,7 +822,7 @@ export default class PreferencesController { * @returns whether this option is on or off. */ getUseRequestQueue(): boolean { - return this.store.getState().useRequestQueue; + return this.state.useRequestQueue; } /** @@ -648,14 +865,15 @@ export default class PreferencesController { * @returns the updated featureFlags object. */ setFeatureFlag(feature: string, activated: boolean): Record { - const currentFeatureFlags = this.store.getState().featureFlags; + const currentFeatureFlags = this.state.featureFlags; const updatedFeatureFlags = { ...currentFeatureFlags, [feature]: activated, }; - this.store.updateState({ featureFlags: updatedFeatureFlags }); - + this.update((state) => { + state.featureFlags = updatedFeatureFlags; + }); return updatedFeatureFlags; } @@ -677,7 +895,9 @@ export default class PreferencesController { [preference]: value, }; - this.store.updateState({ preferences: updatedPreferences }); + this.update((state) => { + state.preferences = updatedPreferences; + }); return updatedPreferences; } @@ -687,7 +907,7 @@ export default class PreferencesController { * @returns A map of user-selected preferences. */ getPreferences(): Preferences { - return this.store.getState().preferences; + return this.state.preferences; } /** @@ -696,7 +916,7 @@ export default class PreferencesController { * @returns The current IPFS gateway domain */ getIpfsGateway(): string { - return this.store.getState().ipfsGateway; + return this.state.ipfsGateway; } /** @@ -706,7 +926,9 @@ export default class PreferencesController { * @returns the update IPFS gateway domain */ setIpfsGateway(domain: string): string { - this.store.updateState({ ipfsGateway: domain }); + this.update((state) => { + state.ipfsGateway = domain; + }); return domain; } @@ -716,7 +938,9 @@ export default class PreferencesController { * @param enabled - Whether or not IPFS is enabled */ setIsIpfsGatewayEnabled(enabled: boolean): void { - this.store.updateState({ isIpfsGatewayEnabled: enabled }); + this.update((state) => { + state.isIpfsGatewayEnabled = enabled; + }); } /** @@ -725,7 +949,9 @@ export default class PreferencesController { * @param useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains */ setUseAddressBarEnsResolution(useAddressBarEnsResolution: boolean): void { - this.store.updateState({ useAddressBarEnsResolution }); + this.update((state) => { + state.useAddressBarEnsResolution = useAddressBarEnsResolution; + }); } /** @@ -739,7 +965,9 @@ export default class PreferencesController { setLedgerTransportPreference( ledgerTransportType: LedgerTransportTypes, ): string { - this.store.updateState({ ledgerTransportType }); + this.update((state) => { + state.ledgerTransportType = ledgerTransportType; + }); return ledgerTransportType; } @@ -749,8 +977,8 @@ export default class PreferencesController { * @param dismissSeedBackUpReminder - User preference for dismissing the back up reminder. */ setDismissSeedBackUpReminder(dismissSeedBackUpReminder: boolean): void { - this.store.updateState({ - dismissSeedBackUpReminder, + this.update((state) => { + state.dismissSeedBackUpReminder = dismissSeedBackUpReminder; }); } @@ -761,18 +989,24 @@ export default class PreferencesController { * @param value - preference of certain network, true to be enabled */ setIncomingTransactionsPreferences(chainId: Hex, value: boolean): void { - const previousValue = this.store.getState().incomingTransactionsPreferences; + const previousValue = this.state.incomingTransactionsPreferences; const updatedValue = { ...previousValue, [chainId]: value }; - this.store.updateState({ incomingTransactionsPreferences: updatedValue }); + this.update((state) => { + state.incomingTransactionsPreferences = updatedValue; + }); } setServiceWorkerKeepAlivePreference(value: boolean): void { - this.store.updateState({ enableMV3TimestampSave: value }); + this.update((state) => { + state.enableMV3TimestampSave = value; + }); } ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) setSnapsAddSnapAccountModalDismissed(value: boolean): void { - this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); + this.update((state) => { + state.snapsAddSnapAccountModalDismissed = value; + }); } ///: END:ONLY_INCLUDE_IF @@ -783,7 +1017,7 @@ export default class PreferencesController { newAccountsControllerState.internalAccounts; const selectedAccount = accounts[selectedAccountId]; - const { identities, lostIdentities } = this.store.getState(); + const { identities, lostIdentities } = this.state; const addresses = Object.values(accounts).map((account) => account.address.toLowerCase(), @@ -812,10 +1046,10 @@ export default class PreferencesController { {}, ); - this.store.updateState({ - identities: updatedIdentities, - lostIdentities: updatedLostIdentities, - selectedAddress: selectedAccount?.address || '', // it will be an empty string during onboarding + this.update((state) => { + state.identities = updatedIdentities; + state.lostIdentities = updatedLostIdentities; + state.selectedAddress = selectedAccount?.address || ''; // it will be an empty string during onboarding }); } } diff --git a/app/scripts/lib/backup.js b/app/scripts/lib/backup.js index 7c550c1581ab..c9da3628a99c 100644 --- a/app/scripts/lib/backup.js +++ b/app/scripts/lib/backup.js @@ -18,7 +18,7 @@ export default class Backup { } async restoreUserData(jsonString) { - const existingPreferences = this.preferencesController.store.getState(); + const existingPreferences = this.preferencesController.state; const { preferences, addressBook, network, internalAccounts } = JSON.parse(jsonString); if (preferences) { @@ -26,7 +26,7 @@ export default class Backup { preferences.lostIdentities = existingPreferences.lostIdentities; preferences.selectedAddress = existingPreferences.selectedAddress; - this.preferencesController.store.updateState(preferences); + this.preferencesController.update(preferences); } if (addressBook) { @@ -51,7 +51,7 @@ export default class Backup { async backupUserData() { const userData = { - preferences: { ...this.preferencesController.store.getState() }, + preferences: { ...this.preferencesController.state }, internalAccounts: { internalAccounts: this.accountsController.state.internalAccounts, }, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 0d9712ba5be5..7a322148c847 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -7,8 +7,7 @@ import { mockNetworkState } from '../../../test/stub/networks'; import Backup from './backup'; function getMockPreferencesController() { - const mcState = { - getSelectedAddress: jest.fn().mockReturnValue('0x01'), + const state = { selectedAddress: '0x01', identities: { '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': { @@ -24,15 +23,14 @@ function getMockPreferencesController() { name: 'Ledger 1', }, }, - update: (store) => (mcState.store = store), }; + const getSelectedAddress = jest.fn().mockReturnValue('0x01'); - mcState.store = { - getState: jest.fn().mockReturnValue(mcState), - updateState: (store) => (mcState.store = store), + return { + state, + getSelectedAddress, + update: jest.fn(), }; - - return mcState; } function getMockAddressBookController() { @@ -239,30 +237,30 @@ describe('Backup', function () { ).toStrictEqual('network-configuration-id-4'); // make sure identities are not lost after restore expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].lastSelected, ).toStrictEqual(1655380342907); expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].name, ).toStrictEqual('Account 3'); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].lastSelected, ).toStrictEqual(1655379648197); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].name, ).toStrictEqual('Ledger 1'); // make sure selected address is not lost after restore - expect(backup.preferencesController.store.selectedAddress).toStrictEqual( + expect(backup.preferencesController.state.selectedAddress).toStrictEqual( '0x01', ); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index f0b66430ee84..b96c708be2d3 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -58,13 +58,11 @@ const metaMetricsController = new MetaMetricsController({ segment: createSegmentMock(2, 10000), getCurrentChainId: () => '0x1338', onNetworkDidChange: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ - currentLocale: 'en_US', - preferences: {}, - })), + preferencesControllerState: { + currentLocale: 'en_US', + preferences: {}, }, + onPreferencesStateChange: jest.fn(), version: '0.0.1', environment: 'test', initState: { diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index d0adbefb264b..8977c00aa3d7 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -57,17 +57,17 @@ const createMiddleware = ( const ppomController = {}; const preferenceController = { - store: { - getState: () => ({ - securityAlertsEnabled: securityAlertsEnabled ?? true, - }), + state: { + securityAlertsEnabled: securityAlertsEnabled ?? true, }, }; if (error) { - preferenceController.store.getState = () => { - throw error; - }; + Object.defineProperty(preferenceController, 'state', { + get() { + throw error; + }, + }); } const networkController = { diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 1bad576e3881..3b393897b2e0 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -11,7 +11,7 @@ import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; -import PreferencesController from '../../controllers/preferences-controller'; +import { PreferencesController } from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; // eslint-disable-next-line import/no-restricted-paths @@ -76,8 +76,7 @@ export function createPPOMMiddleware< next: () => void, ) => { try { - const securityAlertsEnabled = - preferencesController.store.getState()?.securityAlertsEnabled; + const { securityAlertsEnabled } = preferencesController.state; const { chainId } = getProviderConfig({ diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 98af29fe38f1..b19c91a232ab 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -290,7 +290,7 @@ import { NetworkOrderController } from './controllers/network-order'; import { AccountOrderController } from './controllers/account-order'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { isStreamWritable, setupMultiplex } from './lib/stream-utils'; -import PreferencesController from './controllers/preferences-controller'; +import { PreferencesController } from './controllers/preferences-controller'; import AppStateController from './controllers/app-state'; import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; @@ -600,19 +600,20 @@ export default class MetamaskController extends EventEmitter { name: 'PreferencesController', allowedActions: [ 'AccountsController:setSelectedAccount', + 'AccountsController:getSelectedAccount', 'AccountsController:getAccountByAddress', 'AccountsController:setAccountName', + 'NetworkController:getState', ], allowedEvents: ['AccountsController:stateChange'], }); this.preferencesController = new PreferencesController({ - initState: initState.PreferencesController, - initLangCode: opts.initLangCode, + state: { + currentLocale: opts.initLangCode ?? '', + ...initState.PreferencesController, + }, messenger: preferencesMessenger, - provider: this.provider, - networkConfigurationsByChainId: - this.networkController.state.networkConfigurationsByChainId, }); const tokenListMessenger = this.controllerMessenger.getRestricted({ @@ -624,7 +625,7 @@ export default class MetamaskController extends EventEmitter { this.tokenListController = new TokenListController({ chainId: getCurrentChainId({ metamask: this.networkController.state }), preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( - this.preferencesController.store.getState(), + this.preferencesController.state, ), messenger: tokenListMessenger, state: initState.TokenListController, @@ -738,16 +739,19 @@ export default class MetamaskController extends EventEmitter { addNft: this.nftController.addNft.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] - disabled: - this.preferencesController.store.getState().useNftDetection === - undefined - ? false // the detection is enabled by default - : !this.preferencesController.store.getState().useNftDetection, + disabled: !this.preferencesController.state.useNftDetection, }); this.metaMetricsController = new MetaMetricsController({ segment, - preferencesStore: this.preferencesController.store, + onPreferencesStateChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', + ), + preferencesControllerState: { + currentLocale: this.preferencesController.state.currentLocale, + selectedAddress: this.preferencesController.state.selectedAddress, + }, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:networkDidChange', @@ -834,14 +838,17 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - preferencesStore: this.preferencesController.store, + preferencesController: this.preferencesController, messenger: this.controllerMessenger.getRestricted({ name: 'AppStateController', allowedActions: [ `${this.approvalController.name}:addRequest`, `${this.approvalController.name}:acceptRequest`, ], - allowedEvents: [`KeyringController:qrKeyringStateChange`], + allowedEvents: [ + `KeyringController:qrKeyringStateChange`, + 'PreferencesController:stateChange', + ], }), extension: this.extension, }); @@ -860,7 +867,7 @@ export default class MetamaskController extends EventEmitter { this.currencyRateController, ); this.currencyRateController.fetchExchangeRate = (...args) => { - if (this.preferencesController.store.getState().useCurrencyRateCheck) { + if (this.preferencesController.state.useCurrencyRateCheck) { return initialFetchExchangeRate(...args); } return { @@ -898,9 +905,10 @@ export default class MetamaskController extends EventEmitter { state: initState.PPOMController, chainId: getCurrentChainId({ metamask: this.networkController.state }), securityAlertsEnabled: - this.preferencesController.store.getState().securityAlertsEnabled, - onPreferencesChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, + this.preferencesController.state.securityAlertsEnabled, + onPreferencesChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', ), cdnBaseUrl: process.env.BLOCKAID_FILE_CDN, blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY, @@ -986,7 +994,8 @@ export default class MetamaskController extends EventEmitter { tokenPricesService: new CodefiTokenPricesServiceV2(), }); - this.preferencesController.store.subscribe( + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', previousValueComparator((prevState, currState) => { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; @@ -995,7 +1004,7 @@ export default class MetamaskController extends EventEmitter { } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { this.tokenRatesController.stop(); } - }, this.preferencesController.store.getState()), + }, this.preferencesController.state), ); this.ensController = new EnsController({ @@ -1259,9 +1268,13 @@ export default class MetamaskController extends EventEmitter { }), state: initState.SelectedNetworkController, useRequestQueuePreference: - this.preferencesController.store.getState().useRequestQueue, - onPreferencesStateChange: (listener) => - this.preferencesController.store.subscribe(listener), + this.preferencesController.state.useRequestQueue, + onPreferencesStateChange: (listener) => { + preferencesMessenger.subscribe( + 'PreferencesController:stateChange', + listener, + ); + }, domainProxyMap: new WeakRefObjectMap(), }); @@ -1366,8 +1379,7 @@ export default class MetamaskController extends EventEmitter { getFeatureFlags: () => { return { disableSnaps: - this.preferencesController.store.getState().useExternalServices === - false, + this.preferencesController.state.useExternalServices === false, }; }, }); @@ -1686,7 +1698,7 @@ export default class MetamaskController extends EventEmitter { }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, - preferencesController: this.preferencesController, + preferencesControllerState: this.preferencesController.state, }); // start and stop polling for balances based on activeControllerConnections @@ -1769,7 +1781,6 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ initState: initState.AlertController, - preferencesStore: this.preferencesController.store, controllerMessenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], @@ -1852,7 +1863,7 @@ export default class MetamaskController extends EventEmitter { getNetworkState: () => this.networkController.state, getPermittedAccounts: this.getPermittedAccounts.bind(this), getSavedGasFees: () => - this.preferencesController.store.getState().advancedGasFee[ + this.preferencesController.state.advancedGasFee[ getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { @@ -1863,8 +1874,7 @@ export default class MetamaskController extends EventEmitter { includeTokenTransfers: false, isEnabled: () => Boolean( - this.preferencesController.store.getState() - .incomingTransactionsPreferences?.[ + this.preferencesController.state.incomingTransactionsPreferences?.[ getCurrentChainId({ metamask: this.networkController.state }) ] && this.onboardingController.state.completedOnboarding, ), @@ -1873,7 +1883,7 @@ export default class MetamaskController extends EventEmitter { }, isMultichainEnabled: process.env.TRANSACTION_MULTICHAIN, isSimulationEnabled: () => - this.preferencesController.store.getState().useTransactionSimulations, + this.preferencesController.state.useTransactionSimulations, messenger: transactionControllerMessenger, onNetworkStateChange: (listener) => { networkControllerMessenger.subscribe( @@ -2138,7 +2148,7 @@ export default class MetamaskController extends EventEmitter { }); const isExternalNameSourcesEnabled = () => - this.preferencesController.store.getState().useExternalNameSources; + this.preferencesController.state.useExternalNameSources; this.nameController = new NameController({ messenger: this.controllerMessenger.getRestricted({ @@ -2353,7 +2363,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, TransactionController: this.txController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, @@ -2408,7 +2418,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, NetworkController: this.networkController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, @@ -2531,7 +2541,7 @@ export default class MetamaskController extends EventEmitter { } postOnboardingInitialization() { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; this.networkController.lookupNetwork(); @@ -2540,8 +2550,7 @@ export default class MetamaskController extends EventEmitter { } // post onboarding emit detectTokens event - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useTokenDetection, useNftDetection } = preferencesControllerState ?? {}; this.metaMetricsController.trackEvent({ @@ -2565,8 +2574,7 @@ export default class MetamaskController extends EventEmitter { this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2584,8 +2592,7 @@ export default class MetamaskController extends EventEmitter { this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2709,7 +2716,7 @@ export default class MetamaskController extends EventEmitter { * @returns The currently selected locale. */ getLocale() { - const { currentLocale } = this.preferencesController.store.getState(); + const { currentLocale } = this.preferencesController.state; return currentLocale; } @@ -2770,8 +2777,7 @@ export default class MetamaskController extends EventEmitter { 'SnapController:updateSnapState', ), maybeUpdatePhishingList: () => { - const { usePhishDetect } = - this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -2842,11 +2848,23 @@ export default class MetamaskController extends EventEmitter { */ setupControllerEventSubscriptions() { let lastSelectedAddress; + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', + previousValueComparator(async (prevState, currState) => { + const { currentLocale } = currState; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); - this.preferencesController.store.subscribe( - previousValueComparator((prevState, currState) => { - this.#onPreferencesControllerStateChange(currState, prevState); - }, this.preferencesController.store.getState()), + await updateCurrentLocale(currentLocale); + if (currState.incomingTransactionsPreferences?.[chainId]) { + this.txController.startIncomingTransactionPolling(); + } else { + this.txController.stopIncomingTransactionPolling(); + } + + this.#checkTokenListPolling(currState, prevState); + }, this.preferencesController.state), ); this.controllerMessenger.subscribe( @@ -4772,7 +4790,7 @@ export default class MetamaskController extends EventEmitter { const accounts = this.accountsController.listAccounts(); - const { identities } = this.preferencesController.store.getState(); + const { identities } = this.preferencesController.state; return { unlockedAccount, identities, accounts }; } @@ -4971,7 +4989,7 @@ export default class MetamaskController extends EventEmitter { chainId: getCurrentChainId({ metamask: this.networkController.state }), ppomController: this.ppomController, securityAlertsEnabled: - this.preferencesController.store.getState()?.securityAlertsEnabled, + this.preferencesController.state?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), ...otherParams, }; @@ -5143,7 +5161,7 @@ export default class MetamaskController extends EventEmitter { }) { if (sender.url) { if (this.onboardingController.state.completedOnboarding) { - if (this.preferencesController.store.getState().usePhishDetect) { + if (this.preferencesController.state.usePhishDetect) { const { hostname } = new URL(sender.url); this.phishingController.maybeUpdateState(); // Check if new connection is blocked if phishing detection is on @@ -5242,7 +5260,7 @@ export default class MetamaskController extends EventEmitter { * @param {ReadableStream} options.connectionStream - The Duplex stream to connect to. */ setupPhishingCommunication({ connectionStream }) { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -5636,7 +5654,7 @@ export default class MetamaskController extends EventEmitter { ); const isConfirmationRedesignEnabled = () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .redesignedConfirmationsEnabled; }; @@ -6450,7 +6468,7 @@ export default class MetamaskController extends EventEmitter { return null; } const { knownMethodData, use4ByteResolution } = - this.preferencesController.store.getState(); + this.preferencesController.state; const prefixedData = addHexPrefix(data); return getMethodDataName( knownMethodData, @@ -6463,11 +6481,11 @@ export default class MetamaskController extends EventEmitter { ); }, getIsRedesignedConfirmationsDeveloperEnabled: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .isRedesignedConfirmationsDeveloperEnabled; }, getIsConfirmationAdvancedDetailsOpen: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .showConfirmationAdvancedDetails; }, }; @@ -7063,30 +7081,6 @@ export default class MetamaskController extends EventEmitter { }; } - async #onPreferencesControllerStateChange(currentState, previousState) { - const { currentLocale } = currentState; - const chainId = getCurrentChainId({ - metamask: this.networkController.state, - }); - - await updateCurrentLocale(currentLocale); - - if (currentState.incomingTransactionsPreferences?.[chainId]) { - this.txController.startIncomingTransactionPolling(); - } else { - this.txController.stopIncomingTransactionPolling(); - } - - this.#checkTokenListPolling(currentState, previousState); - - // TODO: Remove once the preferences controller has been replaced with the core monorepo implementation - this.controllerMessenger.publish( - 'PreferencesController:stateChange', - currentState, - [], - ); - } - #checkTokenListPolling(currentState, previousState) { const previousEnabled = this.#isTokenListPollingRequired(previousState); const newEnabled = this.#isTokenListPollingRequired(currentState); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index bab66d9bc515..77b062bcfdc7 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -114,22 +114,6 @@ const rpcMethodMiddlewareMock = { }; jest.mock('./lib/rpc-method-middleware', () => rpcMethodMiddlewareMock); -jest.mock( - './controllers/preferences-controller', - () => - function (...args) { - const PreferencesController = jest.requireActual( - './controllers/preferences-controller', - ).default; - const controller = new PreferencesController(...args); - // jest.spyOn gets hoisted to the top of this function before controller is initialized. - // This forces us to replace the function directly with a jest stub instead. - // eslint-disable-next-line jest/prefer-spy-on - controller.store.subscribe = jest.fn(); - return controller; - }, -); - const KNOWN_PUBLIC_KEY = '02065bc80d3d12b3688e4ad5ab1e9eda6adf24aec2518bfc21b87c99d4c5077ab0'; @@ -357,10 +341,10 @@ describe('MetaMaskController', () => { let metamaskController; async function simulatePreferencesChange(preferences) { - metamaskController.preferencesController.store.subscribe.mock.lastCall[0]( + metamaskController.controllerMessenger.publish( + 'PreferencesController:stateChange', preferences, ); - await flushPromises(); } @@ -604,8 +588,7 @@ describe('MetaMaskController', () => { await localMetaMaskController.submitPassword(password); const identities = Object.keys( - localMetaMaskController.preferencesController.store.getState() - .identities, + localMetaMaskController.preferencesController.state.identities, ); const addresses = await localMetaMaskController.keyringController.getAccounts(); @@ -937,8 +920,7 @@ describe('MetaMaskController', () => { expect( Object.keys( - metamaskController.preferencesController.store.getState() - .identities, + metamaskController.preferencesController.state.identities, ), ).not.toContain(hardwareKeyringAccount); expect( diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 7ffbd68472d1..107c1bd7ad14 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -40,8 +40,6 @@ "app/scripts/controllers/permissions/selectors.test.js", "app/scripts/controllers/permissions/specifications.js", "app/scripts/controllers/permissions/specifications.test.js", - "app/scripts/controllers/preferences.js", - "app/scripts/controllers/preferences.test.js", "app/scripts/controllers/swaps.js", "app/scripts/controllers/swaps.test.js", "app/scripts/controllers/transactions/index.js", diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7eaa06a954b0..3df824f29c78 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2115,6 +2115,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/package.json b/package.json index 90da21554bc2..4973fa0da559 100644 --- a/package.json +++ b/package.json @@ -477,6 +477,7 @@ "@metamask/eslint-plugin-design-tokens": "^1.1.0", "@metamask/forwarder": "^1.1.0", "@metamask/phishing-warning": "^4.0.0", + "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", "@metamask/test-dapp": "^8.4.0", "@octokit/core": "^3.6.0", diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index e61d7ed807cd..a57a1eea2109 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -6,7 +6,7 @@ import { SignatureController } from '@metamask/signature-controller'; import { NetworkController } from '@metamask/network-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import PreferencesController from '../../app/scripts/controllers/preferences-controller'; +import { PreferencesController } from '../../app/scripts/controllers/preferences-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { AppStateController } from '../../app/scripts/controllers/app-state'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 2c0dfe9a23cb..5d3883a5e8f5 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -1,3 +1,6 @@ +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); const { FirstTimeFlowType } = require('../../shared/constants/onboarding'); @@ -232,6 +235,29 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, }, QueuedRequestController: { queuedRequestCount: 0, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index f1e9a7e5ae1d..4c802e13bfa0 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -4,6 +4,9 @@ const { } = require('@metamask/snaps-utils'); const { merge, mergeWith } = require('lodash'); const { toHex } = require('@metamask/controller-utils'); +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); @@ -94,6 +97,31 @@ function onboardingFixture() { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + showTestNetworks: false, + smartTransactionsOptInStatus: false, }, QueuedRequestController: { queuedRequestCount: 0, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 559e8a256d43..4658c175bfd5 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -228,7 +228,9 @@ "useExternalNameSources": "boolean", "useTransactionSimulations": true, "enableMV3TimestampSave": true, - "useExternalServices": "boolean" + "useExternalServices": "boolean", + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 2df9ee4e2f23..924769a3cb91 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -45,6 +45,7 @@ "completedOnboarding": true, "knownMethodData": "object", "use4ByteResolution": true, + "showIncomingTransactions": "object", "participateInMetaMetrics": true, "dataCollectionForMarketing": "boolean", "nextNonce": null, @@ -123,6 +124,7 @@ "forgottenPassword": false, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", + "isMultiAccountBalancesEnabled": "boolean", "useAddressBarEnsResolution": true, "ledgerTransportType": "webhid", "snapRegistryList": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index d22b69967027..e2cb7369d88a 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -129,7 +129,9 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 2dfd6ac6ef21..34cc62d3c560 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -129,7 +129,9 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/yarn.lock b/yarn.lock index bc7fc36aabbe..733c94112452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6057,6 +6057,18 @@ __metadata: languageName: node linkType: hard +"@metamask/preferences-controller@npm:^13.0.2": + version: 13.0.3 + resolution: "@metamask/preferences-controller@npm:13.0.3" + dependencies: + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + peerDependencies: + "@metamask/keyring-controller": ^17.0.0 + checksum: 10/d922c2e603c7a1ef0301dcfc7d5b6aa0bbdd9c318f0857fbbc9e95606609ae806e69c46231288953ce443322039781404565a46fe42bdfa731c4f0da20448d32 + languageName: node + linkType: hard + "@metamask/preinstalled-example-snap@npm:^0.1.0": version: 0.1.0 resolution: "@metamask/preinstalled-example-snap@npm:0.1.0" @@ -26165,6 +26177,7 @@ __metadata: "@metamask/phishing-warning": "npm:^4.0.0" "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" + "@metamask/preferences-controller": "npm:^13.0.2" "@metamask/preinstalled-example-snap": "npm:^0.1.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2"