diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 5dc85389f449..3ac66cb85d67 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -266,6 +266,11 @@ export const SENTRY_BACKGROUND_STATE = { PushPlatformNotificationsController: { fcmToken: false, }, + MultichainRatesController: { + fiatCurrency: true, + rates: true, + cryptocurrencies: true, + }, SelectedNetworkController: { domains: false }, SignatureController: { unapprovedMsgCount: true, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0b902939e962..a44a21cc98ea 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -10,6 +10,8 @@ import { TokenRatesController, TokensController, CodefiTokenPricesServiceV2, + RatesController, + fetchMultiExchangeRate, } from '@metamask/assets-controllers'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; @@ -149,6 +151,7 @@ import { } from '@metamask/snaps-utils'; ///: END:ONLY_INCLUDE_IF +import { isEvmAccountType } from '@metamask/keyring-api'; import { methodsRequiringNetworkSwitch, methodsWithConfirmation, @@ -922,6 +925,17 @@ export default class MetamaskController extends EventEmitter { state: initState.AccountOrderController, }); + const multichainRatesControllerMessenger = + this.controllerMessenger.getRestricted({ + name: 'RatesController', + }); + this.multichainRatesController = new RatesController({ + state: initState.MultichainRatesController, + messenger: multichainRatesControllerMessenger, + includeUsdRate: true, + fetchMultiExchangeRate, + }); + // token exchange rate tracker this.tokenRatesController = new TokenRatesController( { @@ -2195,6 +2209,7 @@ export default class MetamaskController extends EventEmitter { PhishingController: this.phishingController, SelectedNetworkController: this.selectedNetworkController, LoggingController: this.loggingController, + MultichainRatesController: this.multichainRatesController, ///: BEGIN:ONLY_INCLUDE_IF(snaps) SnapController: this.snapController, CronjobController: this.cronjobController, @@ -2250,6 +2265,7 @@ export default class MetamaskController extends EventEmitter { SelectedNetworkController: this.selectedNetworkController, LoggingController: this.loggingController, TxController: this.txController, + MultichainRatesController: this.multichainRatesController, ///: BEGIN:ONLY_INCLUDE_IF(snaps) SnapController: this.snapController, CronjobController: this.cronjobController, @@ -2329,6 +2345,7 @@ export default class MetamaskController extends EventEmitter { }); this.setupControllerEventSubscriptions(); + this.setupMultichainDataAndSubscriptions(); // For more information about these legacy streams, see here: // https://github.com/MetaMask/metamask-extension/issues/15491 @@ -2818,6 +2835,33 @@ export default class MetamaskController extends EventEmitter { ///: END:ONLY_INCLUDE_IF } + /** + * Sets up multichain data and subscriptions. + * This method is called during the MetaMaskController constructor. + * It starts the MultichainRatesController if selected account is non-EVM + * and subscribes to account changes. + */ + setupMultichainDataAndSubscriptions() { + if ( + !isEvmAccountType( + this.accountsController.getSelectedMultichainAccount().type, + ) + ) { + this.multichainRatesController.start(); + } + + this.controllerMessenger.subscribe( + 'AccountsController:selectedAccountChange', + (selectedAccount) => { + if (isEvmAccountType(selectedAccount.type)) { + this.multichainRatesController.stop(); + return; + } + this.multichainRatesController.start(); + }, + ); + } + /** * TODO:LegacyProvider: Delete * Constructor helper: initialize a public config store. diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 7bfe1be8bc4a..cf80709cfea1 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -14,12 +14,19 @@ import { METAMASK_STALELIST_FILE, METAMASK_HOTLIST_DIFF_FILE, } from '@metamask/phishing-controller'; -import { EthAccountType } from '@metamask/keyring-api'; +import { + BtcAccountType, + BtcMethod, + EthAccountType, +} from '@metamask/keyring-api'; import { NetworkType } from '@metamask/controller-utils'; import { ControllerMessenger } from '@metamask/base-controller'; import { LoggingController, LogType } from '@metamask/logging-controller'; import { TransactionController } from '@metamask/transaction-controller'; -import { TokenListController } from '@metamask/assets-controllers'; +import { + RatesController, + TokenListController, +} from '@metamask/assets-controllers'; import { NETWORK_TYPES } from '../../shared/constants/network'; import { createTestProviderTools } from '../../test/stub/provider'; import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; @@ -29,6 +36,7 @@ import mockEncryptor from '../../test/lib/mock-encryptor'; import * as tokenUtils from '../../shared/lib/token-util'; import { flushPromises } from '../../test/lib/timer-helpers'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; +import { createMockInternalAccount } from '../../test/jest/mocks'; import { deferredPromise } from './lib/util'; import MetaMaskController from './metamask-controller'; @@ -2025,6 +2033,114 @@ describe('MetaMaskController', () => { expect(TokenListController.prototype.start).toHaveBeenCalledTimes(1); }); }); + + describe('MultichainRatesController start/stop', () => { + const mockEvmAccount = createMockInternalAccount(); + const mockNonEvmAccount = { + ...mockEvmAccount, + id: '21690786-6abd-45d8-a9f0-9ff1d8ca76a1', + type: BtcAccountType.P2wpkh, + methods: [BtcMethod.SendMany], + address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + }; + + beforeEach(() => { + jest.spyOn(metamaskController.multichainRatesController, 'start'); + jest.spyOn(metamaskController.multichainRatesController, 'stop'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('starts MultichainRatesController if selected account is changed to non-EVM', async () => { + expect( + metamaskController.multichainRatesController.start, + ).not.toHaveBeenCalled(); + + metamaskController.controllerMessenger.publish( + 'AccountsController:selectedAccountChange', + mockNonEvmAccount, + ); + + expect( + metamaskController.multichainRatesController.start, + ).toHaveBeenCalledTimes(1); + }); + + it('stops MultichainRatesController if selected account is changed to EVM', async () => { + expect( + metamaskController.multichainRatesController.start, + ).not.toHaveBeenCalled(); + + metamaskController.controllerMessenger.publish( + 'AccountsController:selectedAccountChange', + mockNonEvmAccount, + ); + + expect( + metamaskController.multichainRatesController.start, + ).toHaveBeenCalledTimes(1); + + metamaskController.controllerMessenger.publish( + 'AccountsController:selectedAccountChange', + mockEvmAccount, + ); + expect( + metamaskController.multichainRatesController.start, + ).toHaveBeenCalledTimes(1); + expect( + metamaskController.multichainRatesController.stop, + ).toHaveBeenCalledTimes(1); + }); + + it('does not start MultichainRatesController if selected account is changed to EVM', async () => { + expect( + metamaskController.multichainRatesController.start, + ).not.toHaveBeenCalled(); + + metamaskController.controllerMessenger.publish( + 'AccountsController:selectedAccountChange', + mockEvmAccount, + ); + + expect( + metamaskController.multichainRatesController.start, + ).not.toHaveBeenCalled(); + }); + + it('starts MultichainRatesController if selected account is non-EVM account during initialization', async () => { + jest.spyOn(RatesController.prototype, 'start'); + const localMetamaskController = new MetaMaskController({ + showUserConfirmation: noop, + encryptor: mockEncryptor, + initState: { + ...cloneDeep(firstTimeState), + AccountsController: { + internalAccounts: { + accounts: { + [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockEvmAccount.id]: mockEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + }, + }, + initLangCode: 'en_US', + platform: { + showTransactionNotification: () => undefined, + getVersion: () => 'foo', + }, + browser: browserPolyfillMock, + infuraProjectId: 'foo', + isFirstMetaMaskControllerSetup: true, + }); + + expect( + localMetamaskController.multichainRatesController.start, + ).toHaveBeenCalled(); + }); + }); }); describe('MV3 Specific behaviour', () => { 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 dddbe2b554f4..266ac0dc29f4 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 @@ -100,11 +100,11 @@ "MetaMetricsController": { "participateInMetaMetrics": true, "metaMetricsId": "fake-metrics-id", + "dataCollectionForMarketing": "boolean", "eventsBeforeMetricsOptIn": "object", "traits": "object", "previousUserTraits": "object", "fragments": "object", - "dataCollectionForMarketing": "boolean", "segmentApiCalls": "object" }, "MetamaskNotificationsController": { @@ -119,6 +119,11 @@ "isUpdatingMetamaskNotificationsAccount": "object", "isCheckingAccountsPresence": "boolean" }, + "MultichainRatesController": { + "fiatCurrency": "usd", + "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, + "cryptocurrencies": ["btc"] + }, "NameController": { "names": "object", "nameSources": "object" }, "NetworkController": { "selectedNetworkClientId": "string", 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 96dc9ca07e48..8b5ead0902b3 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 @@ -178,6 +178,9 @@ "logs": "object", "methodData": "object", "lastFetchedBlockNumbers": "object", + "fiatCurrency": "usd", + "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, + "cryptocurrencies": ["btc"], "snaps": "object", "snapStates": "object", "unencryptedSnapStates": "object",