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 aafde70f2072..3d3156595f6b 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -52,17 +52,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 5b9107337a05..4a6190fe67d6 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -14,7 +14,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'; import { trace, TraceContext, TraceName } from '../../../../shared/lib/trace'; @@ -78,8 +78,7 @@ export function createPPOMMiddleware< next: () => void, ) => { try { - const securityAlertsEnabled = - preferencesController.store.getState()?.securityAlertsEnabled; + const { securityAlertsEnabled } = preferencesController.state; // This will always exist as the SelectedNetworkMiddleware // adds networkClientId to the request before this middleware runs diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 83edd7ade418..21e1b1212d47 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; @@ -5637,7 +5655,7 @@ export default class MetamaskController extends EventEmitter { ); const isConfirmationRedesignEnabled = () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .redesignedConfirmationsEnabled; }; @@ -6451,7 +6469,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, @@ -6464,11 +6482,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; }, }; @@ -7064,30 +7082,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"