From 39ea68077e3c01d399899372e853f95c3f9c1af7 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 23 May 2024 21:16:56 +0100 Subject: [PATCH 01/14] upgrade TokenRatesController to BaseControllerV2 --- .../src/TokenRatesController.test.ts | 2043 +++++++++-------- .../src/TokenRatesController.ts | 356 +-- 2 files changed, 1258 insertions(+), 1141 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index a82a195ff7a..9137f89c1d5 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,33 +1,76 @@ +import type { AddApprovalRequest } from '@metamask/approval-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; import { NetworksTicker, toChecksumHexAddress, toHex, } from '@metamask/controller-utils'; -import type { NetworkState } from '@metamask/network-controller'; -import type { PreferencesState } from '@metamask/preferences-controller'; +import type { + NetworkClientId, + NetworkController, + NetworkState, +} from '@metamask/network-controller'; +import { defaultState as defaultNetworkState } from '@metamask/network-controller'; +import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; +import type { CustomNetworkClientConfiguration } from '@metamask/network-controller/src/types'; +import { + getDefaultPreferencesState, + type PreferencesState, +} from '@metamask/preferences-controller'; import type { Hex } from '@metamask/utils'; import { add0x } from '@metamask/utils'; import nock from 'nock'; import { useFakeTimers } from 'sinon'; -import { advanceTime, flushPromises } from '../../../tests/helpers'; +import { advanceTime } from '../../../tests/helpers'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { AbstractTokenPricesService, TokenPrice, TokenPricesByTokenAddress, } from './token-prices-service/abstract-token-prices-service'; -import { TokenRatesController } from './TokenRatesController'; +import { controllerName, TokenRatesController } from './TokenRatesController'; import type { - TokenRatesConfig, + AllowedActions, + AllowedEvents, Token, - TokenRatesState, + TokenRatesControllerMessenger, } from './TokenRatesController'; import type { TokensState } from './TokensController'; +import { getDefaultTokensState } from './TokensController'; const defaultSelectedAddress = '0x0000000000000000000000000000000000000001'; const mockTokenAddress = '0x0000000000000000000000000000000000000010'; +type MainControllerMessenger = ControllerMessenger< + AllowedActions | AddApprovalRequest, + AllowedEvents +>; + +/** + * Builds a messenger that `TokenRatesController` can use to communicate with other controllers. + * @param controllerMessenger - The main controller messenger. + * @returns The restricted messenger. + */ +function buildTokenRatesControllerMessenger( + controllerMessenger: MainControllerMessenger = new ControllerMessenger(), +): TokenRatesControllerMessenger { + return controllerMessenger.getRestricted({ + name: controllerName, + allowedActions: [ + 'TokensController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + 'PreferencesController:getState', + ], + allowedEvents: [ + 'PreferencesController:stateChange', + 'TokensController:stateChange', + 'NetworkController:stateChange', + ], + }); +} + describe('TokenRatesController', () => { afterEach(() => { jest.restoreAllMocks(); @@ -46,14 +89,11 @@ describe('TokenRatesController', () => { it('should set default state', () => { const controller = new TokenRatesController({ - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), + currentChainId: '0x1', + currentTicker: NetworksTicker.mainnet, + currentAddress: defaultSelectedAddress, tokenPricesService: buildMockTokenPricesService(), + messenger: buildTokenRatesControllerMessenger(), }); expect(controller.state).toStrictEqual({ contractExchangeRates: {}, @@ -61,41 +101,15 @@ describe('TokenRatesController', () => { }); }); - it('should initialize with the default config', () => { - const controller = new TokenRatesController({ - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - tokenPricesService: buildMockTokenPricesService(), - }); - expect(controller.config).toStrictEqual({ - interval: 180000, - threshold: 21600000, - allDetectedTokens: {}, - allTokens: {}, - disabled: false, - nativeCurrency: NetworksTicker.mainnet, - chainId: '0x1', - selectedAddress: defaultSelectedAddress, - }); - }); - it('should not poll by default', async () => { const fetchSpy = jest.spyOn(globalThis, 'fetch'); new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), + currentChainId: '0x1', + currentTicker: NetworksTicker.mainnet, + currentAddress: defaultSelectedAddress, tokenPricesService: buildMockTokenPricesService(), + messenger: buildTokenRatesControllerMessenger(), }); await advanceTime({ clock, duration: 500 }); @@ -123,10 +137,16 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -140,16 +160,10 @@ describe('TokenRatesController', () => { }, }, allDetectedTokens: {}, - }, - }, - async ({ controller, controllerEvents }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -178,10 +192,13 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -195,16 +212,13 @@ describe('TokenRatesController', () => { ], }, }, - }, - }, - async ({ controller, controllerEvents }) => { + }); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -219,7 +233,6 @@ describe('TokenRatesController', () => { }, }, }); - // Once when starting, and another when tokens state changes expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); }, @@ -247,19 +260,23 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: tokensState, }, - async ({ controller, controllerEvents }) => { + async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), + ...tokensState, + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange(tokensState); + triggerTokensStateChange({ + ...getDefaultTokensState(), + ...tokensState, + }); // Once when starting, and that's it expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); @@ -285,22 +302,22 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, - }, - config: { - allTokens: tokens, - allDetectedTokens: {}, + currentChainId: chainId, + currentAddress: selectedAddress, }, }, - async ({ controller, controllerEvents }) => { + async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: tokens, + allDetectedTokens: {}, + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: tokens, }); @@ -317,10 +334,16 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -334,16 +357,10 @@ describe('TokenRatesController', () => { }, }, allDetectedTokens: {}, - }, - }, - async ({ controller, controllerEvents }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -382,10 +399,16 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -399,16 +422,10 @@ describe('TokenRatesController', () => { ], }, }, - }, - }, - async ({ controller, controllerEvents }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -447,10 +464,16 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -464,16 +487,10 @@ describe('TokenRatesController', () => { ], }, }, - }, - }, - async ({ controller, controllerEvents }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -501,10 +518,16 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -518,16 +541,10 @@ describe('TokenRatesController', () => { ], }, }, - }, - }, - async ({ controller, controllerEvents }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -555,10 +572,16 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -578,16 +601,10 @@ describe('TokenRatesController', () => { ], }, }, - }, - }, - async ({ controller, controllerEvents }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); + }); await controller.start(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -624,10 +641,13 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -641,15 +661,12 @@ describe('TokenRatesController', () => { }, }, allDetectedTokens: {}, - }, - }, - async ({ controller, controllerEvents }) => { + }); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: { [chainId]: { [selectedAddress]: [ @@ -677,10 +694,13 @@ describe('TokenRatesController', () => { await withController( { options: { - chainId, - selectedAddress, + currentChainId: chainId, + currentAddress: selectedAddress, }, - config: { + }, + async ({ controller, triggerTokensStateChange }) => { + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -694,15 +714,12 @@ describe('TokenRatesController', () => { ], }, }, - }, - }, - async ({ controller, controllerEvents }) => { + }); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ + triggerTokensStateChange({ + ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { [chainId]: { @@ -738,290 +755,245 @@ describe('TokenRatesController', () => { describe('when polling is active', () => { it('should update exchange rates when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - await controller.start(); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + await controller.start(); + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1337), + ticker: 'NEW', + }, + }); - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + }, + ); }); it('should update exchange rates when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - await controller.start(); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + await controller.start(); + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1338), + ticker: 'Test', + }, + }); - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + }, + ); }); it('should clear contractExchangeRates state when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - await controller.start(); - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + await controller.start(); + jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1337), + ticker: 'NEW', + }, + }); - expect(controller.state.contractExchangeRates).toStrictEqual({}); + expect(controller.state.contractExchangeRates).toStrictEqual({}); + }, + ); }); it('should clear contractExchangeRates state when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - await controller.start(); - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + await controller.start(); + jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1338), + ticker: 'TEST', + }, + }); - expect(controller.state.contractExchangeRates).toStrictEqual({}); + expect(controller.state.contractExchangeRates).toStrictEqual({}); + }, + ); }); it('should not update exchange rates when network state changes without a ticker/chain id change', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - await controller.start(); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'TEST' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + await controller.start(); + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1337), + ticker: 'TEST', + }, + }); - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + }, + ); }); }); describe('when polling is inactive', () => { it('should not update exchange rates when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1337), + ticker: 'NEW', + }, + }); - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + }, + ); }); it('should not update exchange rates when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1338), + ticker: 'TEST', + }, + }); - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + }, + ); }); it('should clear contractExchangeRates state when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1337), + ticker: 'NEW', + }, + }); - expect(controller.state.contractExchangeRates).toStrictEqual({}); + expect(controller.state.contractExchangeRates).toStrictEqual({}); + }, + ); }); it('should clear contractExchangeRates state when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; - const onNetworkStateChange = jest - .fn() - .mockImplementation((listener) => { - networkStateChangeListener = listener; - }); - const controller = new TokenRatesController({ - interval: 100, - getNetworkClientById: jest.fn(), - chainId: toHex(1337), - ticker: 'TEST', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange, - tokenPricesService: buildMockTokenPricesService(), - }); - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, - }); + await withController( + { + options: { + interval: 100, + currentChainId: toHex(1337), + currentTicker: 'TEST', + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId: toHex(1338), + ticker: 'TEST', + }, + }); - expect(controller.state.contractExchangeRates).toStrictEqual({}); + expect(controller.state.contractExchangeRates).toStrictEqual({}); + }, + ); }); }); }); @@ -1039,144 +1011,75 @@ describe('TokenRatesController', () => { describe('when polling is active', () => { it('should update exchange rates when selected address changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let preferencesStateChangeListener: (state: any) => Promise; - const onPreferencesStateChange = jest - .fn() - .mockImplementation((listener) => { - preferencesStateChangeListener = listener; - }); const alternateSelectedAddress = '0x0000000000000000000000000000000000000002'; - const controller = new TokenRatesController( - { - interval: 100, - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange, - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - tokenPricesService: buildMockTokenPricesService(), - }, + await withController( { - allTokens: { - '0x1': { - [alternateSelectedAddress]: [ - { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, - { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, - ], - }, + options: { + interval: 100, }, }, - ); - await controller.start(); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: alternateSelectedAddress, - }); + async ({ controller, triggerPreferencesStateChange }) => { + await controller.start(); + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + selectedAddress: alternateSelectedAddress, + }); - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + }, + ); }); it('should not update exchange rates when preferences state changes without selected address changing', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let preferencesStateChangeListener: (state: any) => Promise; - const onPreferencesStateChange = jest - .fn() - .mockImplementation((listener) => { - preferencesStateChangeListener = listener; - }); - const controller = new TokenRatesController( - { - interval: 100, - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange, - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - tokenPricesService: buildMockTokenPricesService(), - }, + await withController( { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, - { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, - ], - }, + options: { + interval: 100, }, }, - ); - await controller.start(); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: defaultSelectedAddress, - exampleConfig: 'exampleValue', - }); + async ({ controller, triggerPreferencesStateChange }) => { + await controller.start(); + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + selectedAddress: defaultSelectedAddress, + openSeaEnabled: false, + }); - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + }, + ); }); }); describe('when polling is inactive', () => { it('should not update exchange rates when selected address changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let preferencesStateChangeListener: (state: any) => Promise; - const onPreferencesStateChange = jest - .fn() - .mockImplementation((listener) => { - preferencesStateChangeListener = listener; - }); const alternateSelectedAddress = '0x0000000000000000000000000000000000000002'; - const controller = new TokenRatesController( - { - interval: 100, - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange, - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - tokenPricesService: buildMockTokenPricesService(), - }, + await withController( { - allTokens: { - '0x1': { - [alternateSelectedAddress]: [ - { address: '0x02', decimals: 0, symbol: '', aggregators: [] }, - { address: '0x03', decimals: 0, symbol: '', aggregators: [] }, - ], - }, + options: { + interval: 100, }, }, - ); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await preferencesStateChangeListener!({ - selectedAddress: alternateSelectedAddress, - }); + async ({ controller, triggerPreferencesStateChange }) => { + const updateExchangeRatesSpy = jest + .spyOn(controller, 'updateExchangeRates') + .mockResolvedValue(); + triggerPreferencesStateChange({ + ...getDefaultPreferencesState(), + selectedAddress: alternateSelectedAddress, + }); - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + }, + ); }); }); }); @@ -1197,42 +1100,46 @@ describe('TokenRatesController', () => { const interval = 100; const tokenPricesService = buildMockTokenPricesService(); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - const controller = new TokenRatesController( - { - interval, - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - tokenPricesService, - }, + await withController( { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, + options: { + interval, + tokenPricesService, }, }, - ); + async ({ controller, triggerTokensStateChange }) => { + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: mockTokenAddress, + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, + }, + }); + await controller.start(); - await controller.start(); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + 1, + ); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(2); + await advanceTime({ clock, duration: interval }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + 2, + ); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(3); + await advanceTime({ clock, duration: interval }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + 3, + ); + }, + ); }); }); @@ -1241,41 +1148,43 @@ describe('TokenRatesController', () => { const interval = 100; const tokenPricesService = buildMockTokenPricesService(); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - const controller = new TokenRatesController( - { - interval, - getNetworkClientById: jest.fn(), - chainId: '0x1', - ticker: NetworksTicker.mainnet, - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - tokenPricesService, - }, + await withController( { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, + options: { + interval, + tokenPricesService, }, }, - ); + async ({ controller, triggerTokensStateChange }) => { + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: mockTokenAddress, + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, + }, + }); + await controller.start(); - await controller.start(); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + 1, + ); - controller.stop(); + controller.stop(); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: interval }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + 1, + ); + }, + ); }); }); }); @@ -1295,48 +1204,56 @@ describe('TokenRatesController', () => { const interval = 100; const tokenPricesService = buildMockTokenPricesService(); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - const controller = new TokenRatesController( + await withController( { - interval, - chainId: '0x2', - ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - getNetworkClientById: jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - ticker: NetworksTicker.mainnet, - }, - }), - tokenPricesService, + options: { + interval, + currentChainId: '0x2', + currentTicker: 'ticker', + tokenPricesService, + }, }, - { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], + async ({ + controller, + mockGetNetworkClientById, + triggerTokensStateChange, + }) => { + mockGetNetworkClientById( + () => + ({ + configuration: { + chainId: '0x1', + ticker: NetworksTicker.mainnet, }, - ], + } as unknown as AutoManagedNetworkClient), + ); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: mockTokenAddress, + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, }, - }, - }, - ); + }); + controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 0 }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: 0 }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(2); + await advanceTime({ clock, duration: interval }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(2); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(3); + await advanceTime({ clock, duration: interval }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(3); + }, + ); }); describe('updating state on poll', () => { @@ -1348,55 +1265,64 @@ describe('TokenRatesController', () => { return currency === 'ETH'; }, }); - const controller = new TokenRatesController( + const interval = 100; + await withController( { - chainId: '0x2', - ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - getNetworkClientById: jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - ticker: NetworksTicker.mainnet, - }, - }), - tokenPricesService, - }, - { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, + options: { + interval, + currentChainId: '0x2', + currentTicker: 'ticker', + tokenPricesService, }, }, - ); - - controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 0 }); - - expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( - { - '0x1': { - ETH: { - '0x02': 0.001, - '0x03': 0.002, + async ({ + controller, + triggerTokensStateChange, + mockGetNetworkClientById, + }) => { + mockGetNetworkClientById( + () => + ({ + configuration: { + chainId: '0x1', + ticker: NetworksTicker.mainnet, + }, + } as unknown as AutoManagedNetworkClient), + ); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: '0x02', + decimals: 0, + symbol: '', + aggregators: [], + }, + { + address: '0x03', + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, }, - }, + }); + controller.startPollingByNetworkClientId('mainnet'); + await advanceTime({ clock, duration: 0 }); + + expect( + controller.state.contractExchangeRatesByChainId, + ).toStrictEqual({ + '0x1': { + ETH: { + '0x02': 0.001, + '0x03': 0.002, + }, + }, + }); }, ); }); @@ -1413,62 +1339,70 @@ describe('TokenRatesController', () => { return currency !== 'LOL'; }, }); - const controller = new TokenRatesController( + + await withController( { - chainId: '0x2', - ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - getNetworkClientById: jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - ticker: 'LOL', - }, - }), - tokenPricesService, + options: { + currentChainId: '0x2', + currentTicker: 'ticker', + tokenPricesService, + }, }, - { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], + async ({ + controller, + mockGetNetworkClientById, + triggerTokensStateChange, + }) => { + mockGetNetworkClientById( + () => + ({ + configuration: { + chainId: '0x1', + ticker: 'LOL', }, - ], + } as unknown as AutoManagedNetworkClient), + ); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: '0x02', + decimals: 0, + symbol: '', + aggregators: [], + }, + { + address: '0x03', + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, }, - }, - }, - ); - - controller.startPollingByNetworkClientId('mainnet'); - // flush promises and advance setTimeouts they enqueue 3 times - // needed because fetch() doesn't resolve immediately, so any - // downstream promises aren't flushed until the next advanceTime loop - await advanceTime({ clock, duration: 1, stepSize: 1 / 3 }); - - expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( - { - '0x1': { - LOL: { - // token price in LOL = (token price in ETH) * (ETH value in LOL) - '0x02': 0.0005, - '0x03': 0.001, + }); + controller.startPollingByNetworkClientId('mainnet'); + // flush promises and advance setTimeouts they enqueue 3 times + // needed because fetch() doesn't resolve immediately, so any + // downstream promises aren't flushed until the next advanceTime loop + await advanceTime({ clock, duration: 1, stepSize: 1 / 3 }); + + expect( + controller.state.contractExchangeRatesByChainId, + ).toStrictEqual({ + '0x1': { + LOL: { + // token price in LOL = (token price in ETH) * (ETH value in LOL) + '0x02': 0.0005, + '0x03': 0.001, + }, }, - }, + }); + controller.stopAllPolling(); }, ); - controller.stopAllPolling(); }); it('returns the an empty object when market does not exist for pair', async () => { @@ -1479,58 +1413,65 @@ describe('TokenRatesController', () => { ); const tokenPricesService = buildMockTokenPricesService(); - const controller = new TokenRatesController( + await withController( { - chainId: '0x2', - ticker: 'ETH', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - getNetworkClientById: jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - ticker: 'LOL', - }, - }), - tokenPricesService, + options: { + currentChainId: '0x2', + currentTicker: 'ETH', + tokenPricesService, + }, }, - { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], + async ({ + controller, + mockGetNetworkClientById, + triggerTokensStateChange, + }) => { + mockGetNetworkClientById( + () => + ({ + configuration: { + chainId: '0x1', + ticker: 'LOL', }, - ], + } as unknown as AutoManagedNetworkClient), + ); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: '0x02', + decimals: 0, + symbol: '', + aggregators: [], + }, + { + address: '0x03', + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, }, - }, - }, - ); - - controller.startPollingByNetworkClientId('mainnet'); - // flush promises and advance setTimeouts they enqueue 3 times - // needed because fetch() doesn't resolve immediately, so any - // downstream promises aren't flushed until the next advanceTime loop - await advanceTime({ clock, duration: 1, stepSize: 1 / 3 }); - - expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( - { - '0x1': { - LOL: {}, - }, + }); + controller.startPollingByNetworkClientId('mainnet'); + // flush promises and advance setTimeouts they enqueue 3 times + // needed because fetch() doesn't resolve immediately, so any + // downstream promises aren't flushed until the next advanceTime loop + await advanceTime({ clock, duration: 1, stepSize: 1 / 3 }); + + expect( + controller.state.contractExchangeRatesByChainId, + ).toStrictEqual({ + '0x1': { + LOL: {}, + }, + }); + controller.stopAllPolling(); }, ); - controller.stopAllPolling(); }); }); }); @@ -1539,47 +1480,54 @@ describe('TokenRatesController', () => { const interval = 100; const tokenPricesService = buildMockTokenPricesService(); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - const controller = new TokenRatesController( + await withController( { - interval, - chainId: '0x2', - ticker: 'ticker', - selectedAddress: defaultSelectedAddress, - onPreferencesStateChange: jest.fn(), - onTokensStateChange: jest.fn(), - onNetworkStateChange: jest.fn(), - getNetworkClientById: jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - ticker: NetworksTicker.mainnet, - }, - }), - tokenPricesService, + options: { + currentChainId: '0x2', + currentTicker: 'ticker', + tokenPricesService, + }, }, - { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], + async ({ + controller, + triggerTokensStateChange, + mockGetNetworkClientById, + }) => { + mockGetNetworkClientById( + () => + ({ + configuration: { + chainId: '0x1', + ticker: NetworksTicker.mainnet, }, - ], + } as unknown as AutoManagedNetworkClient), + ); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: mockTokenAddress, + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, }, - }, - }, - ); - - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 0 }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + }); + const pollingToken = + controller.startPollingByNetworkClientId('mainnet'); + await advanceTime({ clock, duration: 0 }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); - controller.stopPollingByPollingToken(pollingToken); + controller.stopPollingByPollingToken(pollingToken); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: interval }); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + }, + ); }); }); @@ -1598,14 +1546,18 @@ describe('TokenRatesController', () => { ])('%s', (method) => { it('does not update state when disabled', async () => { await withController( - { config: { disabled: true } }, - async ({ controller, controllerEvents }) => { + {}, + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { const tokenAddress = '0x0000000000000000000000000000000000000001'; - + controller.disable(); await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddress, decimals: 18, @@ -1617,7 +1569,8 @@ describe('TokenRatesController', () => { }, chainId: toHex(1), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'ETH', }); @@ -1631,47 +1584,54 @@ describe('TokenRatesController', () => { }); it('does not update state if there are no tokens for the given chain and address', async () => { - await withController(async ({ controller, controllerEvents }) => { - const tokenAddress = '0x0000000000000000000000000000000000000001'; - const differentAccount = '0x1000000000000000000000000000000000000000'; - - await callUpdateExchangeRatesMethod({ - allTokens: { - // These tokens are for the right chain but wrong account - [toHex(1)]: { - [differentAccount]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], - }, - // These tokens are for the right account but wrong chain - [toHex(2)]: { - [controller.config.selectedAddress]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], - }, - }, - chainId: toHex(1), + await withController( + async ({ controller, - controllerEvents, - method, - nativeCurrency: 'ETH', - }); + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { + const tokenAddress = '0x0000000000000000000000000000000000000001'; + const differentAccount = '0x1000000000000000000000000000000000000000'; + controller.enable(); + await callUpdateExchangeRatesMethod({ + allTokens: { + // These tokens are for the right chain but wrong account + [toHex(1)]: { + [differentAccount]: [ + { + address: tokenAddress, + decimals: 18, + symbol: 'TST', + aggregators: [], + }, + ], + }, + // These tokens are for the right account but wrong chain + [toHex(2)]: { + [defaultSelectedAddress]: [ + { + address: tokenAddress, + decimals: 18, + symbol: 'TST', + aggregators: [], + }, + ], + }, + }, + chainId: toHex(1), + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + method, + nativeCurrency: 'ETH', + }); - expect(controller.state.contractExchangeRates).toStrictEqual({}); - expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( - {}, - ); - }); + expect(controller.state.contractExchangeRates).toStrictEqual({}); + expect(controller.state.contractExchangeRatesByChainId).toStrictEqual( + {}, + ); + }, + ); }); it('does not update state if the price update fails', async () => { @@ -1682,7 +1642,11 @@ describe('TokenRatesController', () => { }); await withController( { options: { tokenPricesService } }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { const tokenAddress = '0x0000000000000000000000000000000000000001'; await expect( @@ -1690,7 +1654,7 @@ describe('TokenRatesController', () => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddress, decimals: 18, @@ -1702,7 +1666,8 @@ describe('TokenRatesController', () => { }, chainId: toHex(1), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'ETH', }), @@ -1734,20 +1699,25 @@ describe('TokenRatesController', () => { await withController( { options: { - ticker, + currentTicker: ticker, tokenPricesService, }, }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { await callUpdateExchangeRatesMethod({ allTokens: { [chainId]: { - [controller.config.selectedAddress]: tokens, + [defaultSelectedAddress]: tokens, }, }, chainId, controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: ticker, }); @@ -1792,11 +1762,15 @@ describe('TokenRatesController', () => { }); await withController( { options: { tokenPricesService } }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 18, @@ -1814,27 +1788,28 @@ describe('TokenRatesController', () => { }, chainId: toHex(1), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'ETH', }); expect(controller.state).toMatchInlineSnapshot(` - Object { - "contractExchangeRates": Object { - "0x0000000000000000000000000000000000000001": 0.001, - "0x0000000000000000000000000000000000000002": 0.002, - }, - "contractExchangeRatesByChainId": Object { - "0x1": Object { - "ETH": Object { - "0x0000000000000000000000000000000000000001": 0.001, - "0x0000000000000000000000000000000000000002": 0.002, + Object { + "contractExchangeRates": Object { + "0x0000000000000000000000000000000000000001": 0.001, + "0x0000000000000000000000000000000000000002": 0.002, + }, + "contractExchangeRatesByChainId": Object { + "0x1": Object { + "ETH": Object { + "0x0000000000000000000000000000000000000001": 0.001, + "0x0000000000000000000000000000000000000002": 0.002, + }, }, }, - }, - } - `); + } + `); }, ); }); @@ -1861,11 +1836,15 @@ describe('TokenRatesController', () => { }); await withController( { options: { tokenPricesService } }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(2)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 18, @@ -1883,25 +1862,26 @@ describe('TokenRatesController', () => { }, chainId: toHex(2), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'ETH', setChainAsCurrent: false, }); expect(controller.state).toMatchInlineSnapshot(` - Object { - "contractExchangeRates": Object {}, - "contractExchangeRatesByChainId": Object { - "0x2": Object { - "ETH": Object { - "0x0000000000000000000000000000000000000001": 0.001, - "0x0000000000000000000000000000000000000002": 0.002, + Object { + "contractExchangeRates": Object {}, + "contractExchangeRatesByChainId": Object { + "0x2": Object { + "ETH": Object { + "0x0000000000000000000000000000000000000001": 0.001, + "0x0000000000000000000000000000000000000002": 0.002, + }, }, }, - }, - } - `); + } + `); }, ); }); @@ -1941,11 +1921,15 @@ describe('TokenRatesController', () => { await withController( { options: { tokenPricesService } }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(137)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 18, @@ -1963,28 +1947,29 @@ describe('TokenRatesController', () => { }, chainId: toHex(137), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'UNSUPPORTED', }); // token value in terms of matic should be (token value in eth) * (eth value in matic) expect(controller.state).toMatchInlineSnapshot(` - Object { - "contractExchangeRates": Object { - "0x0000000000000000000000000000000000000001": 0.0005, - "0x0000000000000000000000000000000000000002": 0.001, - }, - "contractExchangeRatesByChainId": Object { - "0x89": Object { - "UNSUPPORTED": Object { - "0x0000000000000000000000000000000000000001": 0.0005, - "0x0000000000000000000000000000000000000002": 0.001, + Object { + "contractExchangeRates": Object { + "0x0000000000000000000000000000000000000001": 0.0005, + "0x0000000000000000000000000000000000000002": 0.001, + }, + "contractExchangeRatesByChainId": Object { + "0x89": Object { + "UNSUPPORTED": Object { + "0x0000000000000000000000000000000000000001": 0.0005, + "0x0000000000000000000000000000000000000002": 0.001, + }, }, }, - }, - } - `); + } + `); }, ); }); @@ -2018,20 +2003,25 @@ describe('TokenRatesController', () => { await withController( { options: { - ticker, + currentTicker: ticker, tokenPricesService, }, }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { await callUpdateExchangeRatesMethod({ allTokens: { [chainId]: { - [controller.config.selectedAddress]: tokens, + [defaultSelectedAddress]: tokens, }, }, chainId, controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: ticker, }); @@ -2081,11 +2071,15 @@ describe('TokenRatesController', () => { }); await withController( { options: { tokenPricesService } }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { await callUpdateExchangeRatesMethod({ allTokens: { [toHex(999)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 18, @@ -2103,27 +2097,28 @@ describe('TokenRatesController', () => { }, chainId: toHex(999), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'TST', }); expect(controller.state).toMatchInlineSnapshot(` - Object { - "contractExchangeRates": Object { - "0x0000000000000000000000000000000000000001": undefined, - "0x0000000000000000000000000000000000000002": undefined, - }, - "contractExchangeRatesByChainId": Object { - "0x3e7": Object { - "TST": Object { - "0x0000000000000000000000000000000000000001": undefined, - "0x0000000000000000000000000000000000000002": undefined, + Object { + "contractExchangeRates": Object { + "0x0000000000000000000000000000000000000001": undefined, + "0x0000000000000000000000000000000000000002": undefined, + }, + "contractExchangeRatesByChainId": Object { + "0x3e7": Object { + "TST": Object { + "0x0000000000000000000000000000000000000001": undefined, + "0x0000000000000000000000000000000000000002": undefined, + }, }, }, - }, - } - `); + } + `); }, ); }); @@ -2150,12 +2145,16 @@ describe('TokenRatesController', () => { }); await withController( { options: { tokenPricesService } }, - async ({ controller, controllerEvents }) => { + async ({ + controller, + triggerTokensStateChange, + triggerNetworkStateChange, + }) => { const updateExchangeRates = async () => await callUpdateExchangeRatesMethod({ allTokens: { [toHex(1)]: { - [controller.config.selectedAddress]: [ + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 18, @@ -2173,7 +2172,8 @@ describe('TokenRatesController', () => { }, chainId: toHex(1), controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency: 'ETH', }); @@ -2182,36 +2182,27 @@ describe('TokenRatesController', () => { expect(fetchTokenPricesMock).toHaveBeenCalledTimes(1); expect(controller.state).toMatchInlineSnapshot(` - Object { - "contractExchangeRates": Object { - "0x0000000000000000000000000000000000000001": 0.001, - "0x0000000000000000000000000000000000000002": 0.002, - }, - "contractExchangeRatesByChainId": Object { - "0x1": Object { - "ETH": Object { - "0x0000000000000000000000000000000000000001": 0.001, - "0x0000000000000000000000000000000000000002": 0.002, + Object { + "contractExchangeRates": Object { + "0x0000000000000000000000000000000000000001": 0.001, + "0x0000000000000000000000000000000000000002": 0.002, + }, + "contractExchangeRatesByChainId": Object { + "0x1": Object { + "ETH": Object { + "0x0000000000000000000000000000000000000001": 0.001, + "0x0000000000000000000000000000000000000002": 0.002, + }, }, }, - }, - } - `); + } + `); }, ); }); }); }); -/** - * A collection of mock external controller events. - */ -type ControllerEvents = { - networkStateChange: (state: NetworkState) => void; - preferencesStateChange: (state: PreferencesState) => void; - tokensStateChange: (state: TokensState) => void; -}; - /** * A callback for the `withController` helper function. * @@ -2222,72 +2213,139 @@ type ControllerEvents = { */ type WithControllerCallback = ({ controller, - controllerEvents, + mockTokensGetState, + mockGetNetworkClientById, + mockNetworkState, + mockPreferencesGetState, + callActionSpy, + triggerPreferencesStateChange, + triggerTokensStateChange, + triggerNetworkStateChange, }: { controller: TokenRatesController; - controllerEvents: ControllerEvents; + mockTokensGetState: (state: TokensState) => void; + mockGetNetworkClientById: ( + handler: ( + networkClientId: NetworkClientId, + ) => AutoManagedNetworkClient, + ) => void; + mockNetworkState: (state: NetworkState) => void; + mockPreferencesGetState: (state: PreferencesState) => void; + callActionSpy: jest.SpyInstance; + triggerPreferencesStateChange: (state: PreferencesState) => void; + triggerTokensStateChange: (state: TokensState) => void; + triggerNetworkStateChange: (state: NetworkState) => void; }) => Promise | ReturnValue; -type PartialConstructorParameters = { +type WithControllerOptions = { options?: Partial[0]>; - config?: Partial; - state?: Partial; + messenger?: ControllerMessenger; }; type WithControllerArgs = | [WithControllerCallback] - | [PartialConstructorParameters, WithControllerCallback]; + | [WithControllerOptions, WithControllerCallback]; /** * Builds a controller based on the given options, and calls the given function * with that controller. * - * @param args - Either a function, or a set of partial constructor parameters - * plus a function. The function will be called with the built controller and a - * collection of controller event handlers. + * @param args - Either a function, or an options bag + a function. The options + * bag is equivalent to the controller options; the function will be called + * with the built controller. * @returns Whatever the callback returns. */ async function withController( ...args: WithControllerArgs -) { - const [{ options, config, state }, testFunction] = - args.length === 2 - ? args - : [{ options: undefined, config: undefined, state: undefined }, args[0]]; - - // explit cast used here because we know the `on____` functions are always - // set in the constructor. - const controllerEvents = {} as ControllerEvents; - - const controllerOptions: ConstructorParameters< - typeof TokenRatesController - >[0] = { - chainId: toHex(1), - getNetworkClientById: jest.fn(), - onNetworkStateChange: (listener) => { - controllerEvents.networkStateChange = listener; - }, - onPreferencesStateChange: (listener) => { - controllerEvents.preferencesStateChange = listener; - }, - onTokensStateChange: (listener) => { - controllerEvents.tokensStateChange = listener; - }, - selectedAddress: defaultSelectedAddress, - ticker: NetworksTicker.mainnet, +): Promise { + const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; + const { options, messenger } = rest; + const controllerMessenger = + messenger ?? new ControllerMessenger(); + + const mockTokensState = jest.fn(); + controllerMessenger.registerActionHandler( + 'TokensController:getState', + mockTokensState.mockReturnValue({ ...getDefaultTokensState() }), + ); + + const mockGetNetworkClientById = jest.fn< + ReturnType, + Parameters + >(); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + mockGetNetworkClientById.mockImplementation(() => { + return { + configuration: { chainId: '0x1' }, + provider: {}, + destroy: {}, + blockTracker: {}, + } as unknown as AutoManagedNetworkClient; + }), + ); + + const mockNetworkState = jest.fn(); + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + mockNetworkState.mockReturnValue({ ...defaultNetworkState }), + ); + + const mockPreferencesState = jest.fn(); + controllerMessenger.registerActionHandler( + 'PreferencesController:getState', + mockPreferencesState.mockReturnValue({ + ...getDefaultPreferencesState(), + }), + ); + + const callActionSpy = jest.spyOn(controllerMessenger, 'call'); + + const controller = new TokenRatesController({ + currentChainId: '0x1', + currentTicker: NetworksTicker.mainnet, + currentAddress: defaultSelectedAddress, tokenPricesService: buildMockTokenPricesService(), + messenger: buildTokenRatesControllerMessenger(controllerMessenger), ...options, - }; - - const controller = new TokenRatesController(controllerOptions, config, state); + }); try { - return await testFunction({ + return await fn({ controller, - controllerEvents, + mockTokensGetState: (state: TokensState) => { + mockTokensState.mockReturnValue(state); + }, + mockGetNetworkClientById: ( + handler: ( + networkClientId: NetworkClientId, + ) => AutoManagedNetworkClient, + ) => { + mockGetNetworkClientById.mockImplementation(handler); + }, + mockNetworkState: (state: NetworkState) => { + mockNetworkState.mockReturnValue(state); + }, + mockPreferencesGetState: (state: PreferencesState) => { + mockPreferencesState.mockReturnValue(state); + }, + callActionSpy, + triggerPreferencesStateChange: (state: PreferencesState) => { + controllerMessenger.publish( + 'PreferencesController:stateChange', + state, + [], + ); + }, + triggerTokensStateChange: (state: TokensState) => { + controllerMessenger.publish('TokensController:stateChange', state, []); + }, + triggerNetworkStateChange: (state: NetworkState) => { + controllerMessenger.publish('NetworkController:stateChange', state, []); + }, }); } finally { controller.stop(); - await flushPromises(); + controller.stopAllPolling(); } } @@ -2308,7 +2366,9 @@ async function withController( * @param args.chainId - The chain ID of the chain we want to update the * exchange rates for. * @param args.controller - The controller to call the method with. - * @param args.controllerEvents - Controller event handlers, used to + * @param args.triggerTokensStateChange - Controller event handlers, used to + * update controller configuration. + * @param args.triggerNetworkStateChange - Controller event handlers, used to * update controller configuration. * @param args.method - The "update exchange rates" method to call. * @param args.nativeCurrency - The symbol for the native currency of the @@ -2320,17 +2380,19 @@ async function callUpdateExchangeRatesMethod({ allTokens, chainId, controller, - controllerEvents, + triggerTokensStateChange, + triggerNetworkStateChange, method, nativeCurrency, setChainAsCurrent = true, }: { - allTokens: TokenRatesConfig['allTokens']; - chainId: TokenRatesConfig['chainId']; + allTokens: TokensState['allTokens']; + chainId: Hex; controller: TokenRatesController; - controllerEvents: ControllerEvents; + triggerTokensStateChange: (state: TokensState) => void; + triggerNetworkStateChange: (state: NetworkState) => void; method: 'updateExchangeRates' | 'updateExchangeRatesByChainId'; - nativeCurrency: TokenRatesConfig['nativeCurrency']; + nativeCurrency: string; setChainAsCurrent?: boolean; }) { if (method === 'updateExchangeRates' && !setChainAsCurrent) { @@ -2342,21 +2404,24 @@ async function callUpdateExchangeRatesMethod({ // controller only uses these two properties, and the tests are written to // only consider these two. We want this to break if we start relying on // more, as we'd need to update the tests accordingly. - // @ts-expect-error Intentionally incomplete state - controllerEvents.tokensStateChange({ allDetectedTokens: {}, allTokens }); + triggerTokensStateChange({ + ...getDefaultTokensState(), + allDetectedTokens: {}, + allTokens, + }); if (setChainAsCurrent) { // We're using controller events here instead of calling `configure` // because `configure` does not update internal controller state correctly. // As with many BaseControllerV1-based controllers, runtime config // modification is allowed by the API but not supported in practice. - controllerEvents.networkStateChange({ - // Note that the state given here is intentionally incomplete because the - // controller only uses these two properties, and the tests are written to - // only consider these two. We want this to break if we start relying on - // more, as we'd need to update the tests accordingly. - // @ts-expect-error Intentionally incomplete state - providerConfig: { chainId, ticker: nativeCurrency }, + triggerNetworkStateChange({ + ...defaultNetworkState, + providerConfig: { + ...defaultNetworkState.providerConfig, + chainId, + ticker: nativeCurrency, + }, }); } diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 099e500004e..95a8742b9ed 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -1,4 +1,8 @@ -import type { BaseConfig, BaseState } from '@metamask/base-controller'; +import type { + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { safelyExecute, toChecksumHexAddress, @@ -7,18 +11,26 @@ import { } from '@metamask/controller-utils'; import type { NetworkClientId, - NetworkController, - NetworkState, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, + NetworkControllerStateChangeEvent, } from '@metamask/network-controller'; -import { StaticIntervalPollingControllerV1 } from '@metamask/polling-controller'; -import type { PreferencesState } from '@metamask/preferences-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import type { + PreferencesControllerGetStateAction, + PreferencesControllerStateChangeEvent, +} from '@metamask/preferences-controller'; import { createDeferredPromise, type Hex } from '@metamask/utils'; import { isEqual } from 'lodash'; import { reduceInBatchesSerially, TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import { fetchExchangeRate as fetchNativeCurrencyExchangeRate } from './crypto-compare-service'; import type { AbstractTokenPricesService } from './token-prices-service/abstract-token-prices-service'; -import type { TokensState } from './TokensController'; +import type { + TokensControllerGetStateAction, + TokensControllerStateChangeEvent, + TokensState, +} from './TokensController'; /** * @type Token @@ -43,34 +55,13 @@ export interface Token { name?: string; } -/** - * @type TokenRatesConfig - * - * Token rates controller configuration - * @property interval - Polling interval used to fetch new token rates - * @property nativeCurrency - Current native currency selected to use base of rates - * @property chainId - Current network chainId - * @property tokens - List of tokens to track exchange rates for - * @property threshold - Threshold to invalidate the supportedChains - */ -// This interface was created before this ESLint rule was added. -// Convert to a `type` in a future major version. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export interface TokenRatesConfig extends BaseConfig { - interval: number; - nativeCurrency: string; - chainId: Hex; - selectedAddress: string; - allTokens: { [chainId: Hex]: { [key: string]: Token[] } }; - allDetectedTokens: { [chainId: Hex]: { [key: string]: Token[] } }; - threshold: number; -} +const DEFAULT_INTERVAL = 180000; // This interface was created before this ESLint rule was added. // Convert to a `type` in a future major version. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export interface ContractExchangeRates { - [address: string]: number | undefined; + [address: string]: number; } enum PollState { @@ -78,6 +69,24 @@ enum PollState { Inactive = 'Inactive', } +/** + * The external actions available to the {@link TokenRatesController}. + */ +export type AllowedActions = + | TokensControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetStateAction + | PreferencesControllerGetStateAction; +/** + * The external events available to the {@link TokenRatesController}. + */ +export type AllowedEvents = + | PreferencesControllerStateChangeEvent + | TokensControllerStateChangeEvent + | NetworkControllerStateChangeEvent; + +export const controllerName = 'TokenRatesController'; + /** * @type TokenRatesState * @@ -88,13 +97,35 @@ enum PollState { // This interface was created before this ESLint rule was added. // Convert to a `type` in a future major version. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export interface TokenRatesState extends BaseState { +export type TokenRatesState = { contractExchangeRates: ContractExchangeRates; contractExchangeRatesByChainId: Record< Hex, Record >; -} +}; + +export type TokenRatesControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + TokenRatesState +>; + +export type TokenRatesControllerActions = TokenRatesControllerGetStateAction; + +export type TokenRatesControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + TokenRatesControllerActions | AllowedActions, + TokenRatesControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +export type TokenRatesControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + TokenRatesState +>; + +export type TokenRatesControllerEvents = TokenRatesControllerStateChangeEvent; /** * Uses the CryptoCompare API to fetch the exchange rate between one currency @@ -133,13 +164,26 @@ async function getCurrencyConversionRate({ } } +const metadata = { + contractExchangeRates: { persist: true, anonymous: false }, + contractExchangeRatesByChainId: { persist: true, anonymous: false }, +}; + +export const getDefaultTokenRatesControllerState = (): TokenRatesState => { + return { + contractExchangeRates: {}, + contractExchangeRatesByChainId: {}, + }; +}; + /** * Controller that passively polls on a set interval for token-to-fiat exchange rates * for tokens stored in the TokensController */ -export class TokenRatesController extends StaticIntervalPollingControllerV1< - TokenRatesConfig, - TokenRatesState +export class TokenRatesController extends StaticIntervalPollingController< + typeof controllerName, + TokenRatesState, + TokenRatesControllerMessenger > { private handle?: ReturnType; @@ -149,124 +193,117 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< #inProcessExchangeRateUpdates: Record<`${Hex}:${string}`, Promise> = {}; - /** - * Name of this controller used during composition - */ - override name = 'TokenRatesController'; + #selectedAddress: string; + + #disabled: boolean; + + #chainId: Hex; + + #ticker: string; + + #interval: number; - private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + #allTokens: TokensState['allTokens']; + + #allDetectedTokens: TokensState['allDetectedTokens']; /** * Creates a TokenRatesController instance. * * @param options - The controller options. * @param options.interval - The polling interval in ms - * @param options.threshold - The duration in ms before metadata fetched from CoinGecko is considered stale - * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. - * @param options.chainId - The chain ID of the current network. - * @param options.ticker - The ticker for the current network. - * @param options.selectedAddress - The current selected address. - * @param options.onPreferencesStateChange - Allows subscribing to preference controller state changes. - * @param options.onTokensStateChange - Allows subscribing to token controller state changes. - * @param options.onNetworkStateChange - Allows subscribing to network state changes. - * @param options.tokenPricesService - An object in charge of retrieving token prices. - * @param config - Initial options used to configure this controller. - * @param state - Initial state to set on this controller. + * @param options.currentChainId - The chain ID of the current network. + * @param options.currentTicker - The ticker for the current network. + * @param options.currentAddress - The current selected address. + * @param options.disabled - Boolean to track if network requests are blocked + * @param options.tokenPricesService - An object in charge of retrieving token price + * @param options.messenger - The controller messaging system */ - constructor( - { - interval = 3 * 60 * 1000, - threshold = 6 * 60 * 60 * 1000, - getNetworkClientById, - chainId: initialChainId, - ticker: initialTicker, - selectedAddress: initialSelectedAddress, - onPreferencesStateChange, - onTokensStateChange, - onNetworkStateChange, - tokenPricesService, - }: { - interval?: number; - threshold?: number; - getNetworkClientById: NetworkController['getNetworkClientById']; - chainId: Hex; - ticker: string; - selectedAddress: string; - onPreferencesStateChange: ( - listener: (preferencesState: PreferencesState) => void, - ) => void; - onTokensStateChange: ( - listener: (tokensState: TokensState) => void, - ) => void; - onNetworkStateChange: ( - listener: (networkState: NetworkState) => void, - ) => void; - tokenPricesService: AbstractTokenPricesService; - }, - config?: Partial, - state?: Partial, - ) { - super(config, state); - this.defaultConfig = { - interval, - threshold, - disabled: false, - nativeCurrency: initialTicker, - chainId: initialChainId, - selectedAddress: initialSelectedAddress, - allTokens: {}, // TODO: initialize these correctly, maybe as part of BaseControllerV2 migration - allDetectedTokens: {}, - }; - - this.defaultState = { - contractExchangeRates: {}, - contractExchangeRatesByChainId: {}, - }; - this.initialize(); + constructor({ + interval = DEFAULT_INTERVAL, + currentChainId, + currentTicker, + currentAddress, + disabled = false, + tokenPricesService, + messenger, + }: { + interval?: number; + currentChainId: Hex; + currentTicker: string; + currentAddress: string; + disabled?: boolean; + tokenPricesService: AbstractTokenPricesService; + messenger: TokenRatesControllerMessenger; + }) { + super({ + name: controllerName, + messenger, + state: { ...getDefaultTokenRatesControllerState() }, + metadata, + }); + this.setIntervalLength(interval); - this.getNetworkClientById = getNetworkClientById; this.#tokenPricesService = tokenPricesService; + this.#disabled = disabled; + this.#interval = interval; + this.#chainId = currentChainId; + this.#ticker = currentTicker; + this.#selectedAddress = currentAddress; + this.#allTokens = {}; + this.#allDetectedTokens = {}; + + this.messagingSystem.subscribe( + 'PreferencesController:stateChange', + async (selectedAddress: string) => { + if (this.#selectedAddress !== selectedAddress) { + this.#selectedAddress = selectedAddress; + if (this.#pollState === PollState.Active) { + await this.updateExchangeRates(); + } + } + }, + ({ selectedAddress }) => { + return selectedAddress; + }, + ); - if (config?.disabled) { - this.configure({ disabled: true }, false, false); - } - - onPreferencesStateChange(async ({ selectedAddress }) => { - if (this.config.selectedAddress !== selectedAddress) { - this.configure({ selectedAddress }); - if (this.#pollState === PollState.Active) { + this.messagingSystem.subscribe( + 'TokensController:stateChange', + async ({ allTokens, allDetectedTokens }) => { + const previousTokenAddresses = this.#getTokenAddresses(this.#chainId); + this.#allTokens = allTokens; + this.#allDetectedTokens = allDetectedTokens; + + const newTokenAddresses = this.#getTokenAddresses(this.#chainId); + if ( + !isEqual(previousTokenAddresses, newTokenAddresses) && + this.#pollState === PollState.Active + ) { await this.updateExchangeRates(); } - } - }); - - onTokensStateChange(async ({ allTokens, allDetectedTokens }) => { - const previousTokenAddresses = this.#getTokenAddresses( - this.config.chainId, - ); - this.configure({ allTokens, allDetectedTokens }); - const newTokenAddresses = this.#getTokenAddresses(this.config.chainId); - if ( - !isEqual(previousTokenAddresses, newTokenAddresses) && - this.#pollState === PollState.Active - ) { - await this.updateExchangeRates(); - } - }); + }, + ({ allTokens, allDetectedTokens }) => { + return { allTokens, allDetectedTokens }; + }, + ); - onNetworkStateChange(async ({ providerConfig }) => { - const { chainId, ticker } = providerConfig; - if ( - this.config.chainId !== chainId || - this.config.nativeCurrency !== ticker - ) { - this.update({ contractExchangeRates: {} }); - this.configure({ chainId, nativeCurrency: ticker }); - if (this.#pollState === PollState.Active) { - await this.updateExchangeRates(); + this.messagingSystem.subscribe( + 'NetworkController:stateChange', + async ({ providerConfig }) => { + const { chainId, ticker } = providerConfig; + if (this.#chainId !== chainId || this.#ticker !== ticker) { + this.update((state) => { + state.contractExchangeRates = {}; + }); + this.#chainId = chainId; + this.#ticker = ticker; + if (this.#pollState === PollState.Active) { + await this.updateExchangeRates(); + } } - } - }); + }, + ); } /** @@ -276,10 +313,9 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< * @returns The list of tokens addresses for the current chain */ #getTokenAddresses(chainId: Hex): Hex[] { - const { allTokens, allDetectedTokens } = this.config; - const tokens = allTokens[chainId]?.[this.config.selectedAddress] || []; + const tokens = this.#allTokens[chainId]?.[this.#selectedAddress] || []; const detectedTokens = - allDetectedTokens[chainId]?.[this.config.selectedAddress] || []; + this.#allDetectedTokens[chainId]?.[this.#selectedAddress] || []; return [ ...new Set( @@ -290,6 +326,20 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< ].sort(); } + /** + * Allows controller to make active and passive polling requests + */ + enable(): void { + this.#disabled = false; + } + + /** + * Blocks controller from making network calls + */ + disable(): void { + this.#disabled = true; + } + /** * Start (or restart) polling. */ @@ -326,17 +376,16 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< // requests don't stack if they take longer than the polling interval this.handle = setTimeout(() => { this.#poll(); - }, this.config.interval); + }, this.#interval); } /** * Updates exchange rates for all tokens. */ async updateExchangeRates() { - const { chainId, nativeCurrency } = this.config; await this.updateExchangeRatesByChainId({ - chainId, - nativeCurrency, + chainId: this.#chainId, + nativeCurrency: this.#ticker, }); } @@ -354,7 +403,7 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< chainId: Hex; nativeCurrency: string; }) { - if (this.disabled) { + if (this.#disabled) { return; } @@ -388,8 +437,7 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< const existingContractExchangeRates = this.state.contractExchangeRates; const updatedContractExchangeRates = - chainId === this.config.chainId && - nativeCurrency === this.config.nativeCurrency + chainId === this.#chainId && nativeCurrency === this.#ticker ? newContractExchangeRates : existingContractExchangeRates; @@ -406,9 +454,10 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< }, }; - this.update({ - contractExchangeRates: updatedContractExchangeRates, - contractExchangeRatesByChainId: updatedContractExchangeRatesForChainId, + this.update((state) => { + state.contractExchangeRates = updatedContractExchangeRates; + state.contractExchangeRatesByChainId = + updatedContractExchangeRatesForChainId; }); updateSucceeded(); } catch (error: unknown) { @@ -478,7 +527,10 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< * @returns The controller state. */ async _executePoll(networkClientId: NetworkClientId): Promise { - const networkClient = this.getNetworkClientById(networkClientId); + const networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); await this.updateExchangeRatesByChainId({ chainId: networkClient.configuration.chainId, nativeCurrency: networkClient.configuration.ticker, @@ -564,7 +616,7 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< ] = await Promise.all([ this.#fetchAndMapExchangeRatesForSupportedNativeCurrency({ tokenAddresses, - chainId: this.config.chainId, + chainId: this.#chainId, nativeCurrency: FALL_BACK_VS_CURRENCY, }), getCurrencyConversionRate({ From 44d8c64ffa1a1a4a431f19ee81c8406db0c5c019 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 23 May 2024 21:39:12 +0100 Subject: [PATCH 02/14] Rename TokenRatesState to TokenRatesControllerState --- .../src/TokenRatesController.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 95a8742b9ed..2300a814ae4 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -97,7 +97,7 @@ export const controllerName = 'TokenRatesController'; // This interface was created before this ESLint rule was added. // Convert to a `type` in a future major version. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type TokenRatesState = { +export type TokenRatesControllerState = { contractExchangeRates: ContractExchangeRates; contractExchangeRatesByChainId: Record< Hex, @@ -107,7 +107,7 @@ export type TokenRatesState = { export type TokenRatesControllerGetStateAction = ControllerGetStateAction< typeof controllerName, - TokenRatesState + TokenRatesControllerState >; export type TokenRatesControllerActions = TokenRatesControllerGetStateAction; @@ -122,7 +122,7 @@ export type TokenRatesControllerMessenger = RestrictedControllerMessenger< export type TokenRatesControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, - TokenRatesState + TokenRatesControllerState >; export type TokenRatesControllerEvents = TokenRatesControllerStateChangeEvent; @@ -169,12 +169,13 @@ const metadata = { contractExchangeRatesByChainId: { persist: true, anonymous: false }, }; -export const getDefaultTokenRatesControllerState = (): TokenRatesState => { - return { - contractExchangeRates: {}, - contractExchangeRatesByChainId: {}, +export const getDefaultTokenRatesControllerState = + (): TokenRatesControllerState => { + return { + contractExchangeRates: {}, + contractExchangeRatesByChainId: {}, + }; }; -}; /** * Controller that passively polls on a set interval for token-to-fiat exchange rates @@ -182,7 +183,7 @@ export const getDefaultTokenRatesControllerState = (): TokenRatesState => { */ export class TokenRatesController extends StaticIntervalPollingController< typeof controllerName, - TokenRatesState, + TokenRatesControllerState, TokenRatesControllerMessenger > { private handle?: ReturnType; From 186833165d68743b3152ba25a14b58cd7a76f310 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 23 May 2024 21:54:31 +0100 Subject: [PATCH 03/14] TokenRatesConfig removed and TokenRatesState renamed in index.ts --- packages/assets-controllers/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index b276b2ccd30..e6d78ab5b98 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -32,9 +32,8 @@ export type { export { TokenListController } from './TokenListController'; export type { Token, - TokenRatesConfig, ContractExchangeRates, - TokenRatesState, + TokenRatesControllerState, } from './TokenRatesController'; export { TokenRatesController } from './TokenRatesController'; export type { From a701310d1b981f8eba4348cebd4257b531b72b9a Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Tue, 28 May 2024 10:14:59 +0100 Subject: [PATCH 04/14] controller state added back to constructor param --- .../src/TokenRatesController.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 2300a814ae4..f622c51f999 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -219,6 +219,7 @@ export class TokenRatesController extends StaticIntervalPollingController< * @param options.disabled - Boolean to track if network requests are blocked * @param options.tokenPricesService - An object in charge of retrieving token price * @param options.messenger - The controller messaging system + * @param options.state - Initial state to set on this controller */ constructor({ interval = DEFAULT_INTERVAL, @@ -228,6 +229,7 @@ export class TokenRatesController extends StaticIntervalPollingController< disabled = false, tokenPricesService, messenger, + state, }: { interval?: number; currentChainId: Hex; @@ -236,11 +238,12 @@ export class TokenRatesController extends StaticIntervalPollingController< disabled?: boolean; tokenPricesService: AbstractTokenPricesService; messenger: TokenRatesControllerMessenger; + state?: Partial; }) { super({ name: controllerName, messenger, - state: { ...getDefaultTokenRatesControllerState() }, + state: { ...getDefaultTokenRatesControllerState(), ...state }, metadata, }); @@ -254,6 +257,14 @@ export class TokenRatesController extends StaticIntervalPollingController< this.#allTokens = {}; this.#allDetectedTokens = {}; + this.#subscribeToPreferencesStateChange(); + + this.#subscribeToTokensStateChange(); + + this.#subscribeToNetworkStateChange(); + } + + #subscribeToPreferencesStateChange() { this.messagingSystem.subscribe( 'PreferencesController:stateChange', async (selectedAddress: string) => { @@ -268,7 +279,9 @@ export class TokenRatesController extends StaticIntervalPollingController< return selectedAddress; }, ); + } + #subscribeToTokensStateChange() { this.messagingSystem.subscribe( 'TokensController:stateChange', async ({ allTokens, allDetectedTokens }) => { @@ -288,7 +301,9 @@ export class TokenRatesController extends StaticIntervalPollingController< return { allTokens, allDetectedTokens }; }, ); + } + #subscribeToNetworkStateChange() { this.messagingSystem.subscribe( 'NetworkController:stateChange', async ({ providerConfig }) => { From d8e8c5e8093618a7bdf09f0928433b57d96fb8a6 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 30 May 2024 12:17:41 +0100 Subject: [PATCH 05/14] chain id and selected address are assigned from messenger system --- .../src/TokenRatesController.test.ts | 348 +++++++----------- .../src/TokenRatesController.ts | 34 +- 2 files changed, 151 insertions(+), 231 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 9137f89c1d5..81cd355670e 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,7 +1,9 @@ import type { AddApprovalRequest } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { + ChainId, NetworksTicker, + NetworkType, toChecksumHexAddress, toHex, } from '@metamask/controller-utils'; @@ -29,7 +31,11 @@ import type { TokenPrice, TokenPricesByTokenAddress, } from './token-prices-service/abstract-token-prices-service'; -import { controllerName, TokenRatesController } from './TokenRatesController'; +import { + controllerName, + getDefaultTokenRatesControllerState, + TokenRatesController, +} from './TokenRatesController'; import type { AllowedActions, AllowedEvents, @@ -87,31 +93,30 @@ describe('TokenRatesController', () => { clock.restore(); }); - it('should set default state', () => { - const controller = new TokenRatesController({ - currentChainId: '0x1', - currentTicker: NetworksTicker.mainnet, - currentAddress: defaultSelectedAddress, - tokenPricesService: buildMockTokenPricesService(), - messenger: buildTokenRatesControllerMessenger(), - }); - expect(controller.state).toStrictEqual({ - contractExchangeRates: {}, - contractExchangeRatesByChainId: {}, + it('should set default state', async () => { + await withController({}, async ({ controller }) => { + expect(controller.state).toStrictEqual({ + contractExchangeRates: {}, + contractExchangeRatesByChainId: {}, + }); }); }); it('should not poll by default', async () => { const fetchSpy = jest.spyOn(globalThis, 'fetch'); - new TokenRatesController({ - interval: 100, - currentChainId: '0x1', - currentTicker: NetworksTicker.mainnet, - currentAddress: defaultSelectedAddress, - tokenPricesService: buildMockTokenPricesService(), - messenger: buildTokenRatesControllerMessenger(), - }); - + await withController( + { + options: { + interval: 100, + }, + }, + async ({ controller }) => { + expect(controller.state).toStrictEqual({ + contractExchangeRates: {}, + contractExchangeRatesByChainId: {}, + }); + }, + ); await advanceTime({ clock, duration: 500 }); expect(fetchSpy).not.toHaveBeenCalled(); @@ -131,16 +136,9 @@ describe('TokenRatesController', () => { describe('when legacy polling is active', () => { it('should update exchange rates when any of the addresses in the "all tokens" collection change', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; const tokenAddresses = ['0xE1', '0xE2']; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -148,8 +146,8 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 0, @@ -165,8 +163,8 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[1], decimals: 0, @@ -186,23 +184,16 @@ describe('TokenRatesController', () => { }); it('should update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; const tokenAddresses = ['0xE1', '0xE2']; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 0, @@ -221,8 +212,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[1], decimals: 0, @@ -240,12 +231,10 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if both the "all tokens" or "all detected tokens" are exactly the same', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; const tokensState = { allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -258,12 +247,7 @@ describe('TokenRatesController', () => { allDetectedTokens: {}, }; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -285,11 +269,9 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if all of the tokens in "all tokens" just move to "all detected tokens"', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; const tokens = { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -300,12 +282,7 @@ describe('TokenRatesController', () => { }, }; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -329,15 +306,8 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if a new token is added to "all detected tokens" but is already present in "all tokens"', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -345,8 +315,8 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -362,8 +332,8 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -374,8 +344,8 @@ describe('TokenRatesController', () => { }, }, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -394,15 +364,8 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if a new token is added to "all tokens" but is already present in "all detected tokens"', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -411,8 +374,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -427,8 +390,8 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -439,8 +402,8 @@ describe('TokenRatesController', () => { }, }, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 0, @@ -459,15 +422,8 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, even if other parts of the token change', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -476,8 +432,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 3, @@ -493,8 +449,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: mockTokenAddress, decimals: 7, @@ -513,15 +469,8 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, when normalized to checksum addresses', async () => { - const chainId = '0xC'; - const selectedAddress = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -530,8 +479,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: '0x0EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE2', decimals: 3, @@ -547,8 +496,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: '0x0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2', decimals: 7, @@ -567,15 +516,8 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates if any of the addresses in "all tokens" or "all detected tokens" merely change order', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -584,8 +526,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: '0xE1', decimals: 0, @@ -607,8 +549,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: '0xE2', decimals: 0, @@ -635,22 +577,15 @@ describe('TokenRatesController', () => { describe('when legacy polling is inactive', () => { it('should not update exchange rates when any of the addresses in the "all tokens" collection change', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; const tokenAddresses = ['0xE1', '0xE2']; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 0, @@ -668,8 +603,8 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[1], decimals: 0, @@ -688,23 +623,16 @@ describe('TokenRatesController', () => { }); it('should not update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { - const chainId = '0xC'; - const selectedAddress = '0xA'; const tokenAddresses = ['0xE1', '0xE2']; await withController( - { - options: { - currentChainId: chainId, - currentAddress: selectedAddress, - }, - }, + {}, async ({ controller, triggerTokensStateChange }) => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[0], decimals: 0, @@ -722,8 +650,8 @@ describe('TokenRatesController', () => { ...getDefaultTokensState(), allTokens: {}, allDetectedTokens: { - [chainId]: { - [selectedAddress]: [ + [ChainId.mainnet]: { + [defaultSelectedAddress]: [ { address: tokenAddresses[1], decimals: 0, @@ -759,8 +687,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -772,7 +698,7 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1337), + chainId: ChainId.mainnet, ticker: 'NEW', }, }); @@ -787,8 +713,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -800,8 +724,8 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1338), - ticker: 'Test', + chainId: ChainId['linea-mainnet'], + ticker: NetworksTicker.mainnet, }, }); @@ -815,8 +739,13 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', + state: { + ...getDefaultTokenRatesControllerState(), + contractExchangeRates: { + '0x0000000000000000000000000000000000000001': 0.001, + '0x0000000000000000000000000000000000000002': 0.002, + }, + }, }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -826,7 +755,7 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1337), + chainId: ChainId.mainnet, ticker: 'NEW', }, }); @@ -841,8 +770,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -852,8 +779,8 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1338), - ticker: 'TEST', + chainId: ChainId['linea-mainnet'], + ticker: NetworksTicker.mainnet, }, }); @@ -867,8 +794,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -878,11 +803,7 @@ describe('TokenRatesController', () => { .mockResolvedValue(); triggerNetworkStateChange({ ...defaultNetworkState, - providerConfig: { - ...defaultNetworkState.providerConfig, - chainId: toHex(1337), - ticker: 'TEST', - }, + selectedNetworkClientId: NetworkType.mainnet, }); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); @@ -897,8 +818,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -909,7 +828,7 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1337), + chainId: ChainId.mainnet, ticker: 'NEW', }, }); @@ -924,8 +843,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -936,8 +853,8 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1338), - ticker: 'TEST', + chainId: ChainId['linea-mainnet'], + ticker: NetworksTicker.mainnet, }, }); @@ -951,8 +868,13 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', + state: { + ...getDefaultTokenRatesControllerState(), + contractExchangeRates: { + '0x0000000000000000000000000000000000000001': 0.001, + '0x0000000000000000000000000000000000000002': 0.002, + }, + }, }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -961,7 +883,7 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1337), + chainId: ChainId.mainnet, ticker: 'NEW', }, }); @@ -976,8 +898,6 @@ describe('TokenRatesController', () => { { options: { interval: 100, - currentChainId: toHex(1337), - currentTicker: 'TEST', }, }, async ({ controller, triggerNetworkStateChange }) => { @@ -986,8 +906,8 @@ describe('TokenRatesController', () => { ...defaultNetworkState, providerConfig: { ...defaultNetworkState.providerConfig, - chainId: toHex(1338), - ticker: 'TEST', + chainId: ChainId['linea-mainnet'], + ticker: NetworksTicker.mainnet, }, }); @@ -1108,10 +1028,11 @@ describe('TokenRatesController', () => { }, }, async ({ controller, triggerTokensStateChange }) => { + await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: mockTokenAddress, @@ -1123,7 +1044,6 @@ describe('TokenRatesController', () => { }, }, }); - await controller.start(); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( 1, @@ -1159,7 +1079,7 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: mockTokenAddress, @@ -1208,8 +1128,6 @@ describe('TokenRatesController', () => { { options: { interval, - currentChainId: '0x2', - currentTicker: 'ticker', tokenPricesService, }, }, @@ -1222,7 +1140,7 @@ describe('TokenRatesController', () => { () => ({ configuration: { - chainId: '0x1', + chainId: [ChainId.mainnet], ticker: NetworksTicker.mainnet, }, } as unknown as AutoManagedNetworkClient), @@ -1230,7 +1148,7 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: mockTokenAddress, @@ -1270,8 +1188,6 @@ describe('TokenRatesController', () => { { options: { interval, - currentChainId: '0x2', - currentTicker: 'ticker', tokenPricesService, }, }, @@ -1284,7 +1200,7 @@ describe('TokenRatesController', () => { () => ({ configuration: { - chainId: '0x1', + chainId: [ChainId.mainnet], ticker: NetworksTicker.mainnet, }, } as unknown as AutoManagedNetworkClient), @@ -1292,7 +1208,7 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: '0x02', @@ -1316,7 +1232,7 @@ describe('TokenRatesController', () => { expect( controller.state.contractExchangeRatesByChainId, ).toStrictEqual({ - '0x1': { + [ChainId.mainnet]: { ETH: { '0x02': 0.001, '0x03': 0.002, @@ -1343,8 +1259,6 @@ describe('TokenRatesController', () => { await withController( { options: { - currentChainId: '0x2', - currentTicker: 'ticker', tokenPricesService, }, }, @@ -1357,7 +1271,7 @@ describe('TokenRatesController', () => { () => ({ configuration: { - chainId: '0x1', + chainId: [ChainId.mainnet], ticker: 'LOL', }, } as unknown as AutoManagedNetworkClient), @@ -1365,7 +1279,7 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: '0x02', @@ -1392,7 +1306,7 @@ describe('TokenRatesController', () => { expect( controller.state.contractExchangeRatesByChainId, ).toStrictEqual({ - '0x1': { + [ChainId.mainnet]: { LOL: { // token price in LOL = (token price in ETH) * (ETH value in LOL) '0x02': 0.0005, @@ -1416,8 +1330,6 @@ describe('TokenRatesController', () => { await withController( { options: { - currentChainId: '0x2', - currentTicker: 'ETH', tokenPricesService, }, }, @@ -1430,7 +1342,7 @@ describe('TokenRatesController', () => { () => ({ configuration: { - chainId: '0x1', + chainId: [ChainId.mainnet], ticker: 'LOL', }, } as unknown as AutoManagedNetworkClient), @@ -1438,7 +1350,7 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: '0x02', @@ -1465,7 +1377,7 @@ describe('TokenRatesController', () => { expect( controller.state.contractExchangeRatesByChainId, ).toStrictEqual({ - '0x1': { + [ChainId.mainnet]: { LOL: {}, }, }); @@ -1483,8 +1395,6 @@ describe('TokenRatesController', () => { await withController( { options: { - currentChainId: '0x2', - currentTicker: 'ticker', tokenPricesService, }, }, @@ -1497,7 +1407,7 @@ describe('TokenRatesController', () => { () => ({ configuration: { - chainId: '0x1', + chainId: [ChainId.mainnet], ticker: NetworksTicker.mainnet, }, } as unknown as AutoManagedNetworkClient), @@ -1505,7 +1415,7 @@ describe('TokenRatesController', () => { triggerTokensStateChange({ ...getDefaultTokensState(), allTokens: { - '0x1': { + [ChainId.mainnet]: { [defaultSelectedAddress]: [ { address: mockTokenAddress, @@ -1682,7 +1592,7 @@ describe('TokenRatesController', () => { it('fetches rates for all tokens in batches', async () => { const chainId = toHex(1); - const ticker = 'ETH'; + const ticker = NetworksTicker.mainnet; const tokenAddresses = [...new Array(200).keys()] .map(buildAddress) .sort(); @@ -1699,7 +1609,6 @@ describe('TokenRatesController', () => { await withController( { options: { - currentTicker: ticker, tokenPricesService, }, }, @@ -2003,7 +1912,6 @@ describe('TokenRatesController', () => { await withController( { options: { - currentTicker: ticker, tokenPricesService, }, }, @@ -2277,7 +2185,7 @@ async function withController( 'NetworkController:getNetworkClientById', mockGetNetworkClientById.mockImplementation(() => { return { - configuration: { chainId: '0x1' }, + configuration: { chainId: [ChainId.mainnet] }, provider: {}, destroy: {}, blockTracker: {}, @@ -2296,15 +2204,13 @@ async function withController( 'PreferencesController:getState', mockPreferencesState.mockReturnValue({ ...getDefaultPreferencesState(), + selectedAddress: defaultSelectedAddress, }), ); const callActionSpy = jest.spyOn(controllerMessenger, 'call'); const controller = new TokenRatesController({ - currentChainId: '0x1', - currentTicker: NetworksTicker.mainnet, - currentAddress: defaultSelectedAddress, tokenPricesService: buildMockTokenPricesService(), messenger: buildTokenRatesControllerMessenger(controllerMessenger), ...options, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index f622c51f999..fd270793272 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -213,9 +213,6 @@ export class TokenRatesController extends StaticIntervalPollingController< * * @param options - The controller options. * @param options.interval - The polling interval in ms - * @param options.currentChainId - The chain ID of the current network. - * @param options.currentTicker - The ticker for the current network. - * @param options.currentAddress - The current selected address. * @param options.disabled - Boolean to track if network requests are blocked * @param options.tokenPricesService - An object in charge of retrieving token price * @param options.messenger - The controller messaging system @@ -223,18 +220,12 @@ export class TokenRatesController extends StaticIntervalPollingController< */ constructor({ interval = DEFAULT_INTERVAL, - currentChainId, - currentTicker, - currentAddress, disabled = false, tokenPricesService, messenger, state, }: { interval?: number; - currentChainId: Hex; - currentTicker: string; - currentAddress: string; disabled?: boolean; tokenPricesService: AbstractTokenPricesService; messenger: TokenRatesControllerMessenger; @@ -251,9 +242,11 @@ export class TokenRatesController extends StaticIntervalPollingController< this.#tokenPricesService = tokenPricesService; this.#disabled = disabled; this.#interval = interval; + const { chainId: currentChainId, ticker: currentTicker } = + this.#getChainIdAndTicker(); this.#chainId = currentChainId; this.#ticker = currentTicker; - this.#selectedAddress = currentAddress; + this.#selectedAddress = this.#getSelectedAddress(); this.#allTokens = {}; this.#allDetectedTokens = {}; @@ -373,6 +366,27 @@ export class TokenRatesController extends StaticIntervalPollingController< this.#pollState = PollState.Inactive; } + #getSelectedAddress(): string { + const { selectedAddress } = this.messagingSystem.call( + 'PreferencesController:getState', + ); + + return selectedAddress; + } + + #getChainIdAndTicker(): { + chainId: Hex; + ticker: string; + } { + const { providerConfig } = this.messagingSystem.call( + 'NetworkController:getState', + ); + return { + chainId: providerConfig.chainId, + ticker: providerConfig.ticker, + }; + } + /** * Clear the active polling timer, if present. */ From 0a550b6cf646f173e874aeff3a98204345847f81 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Tue, 4 Jun 2024 16:51:21 +0100 Subject: [PATCH 06/14] clean-up: removed unwanted commented code --- .../src/TokenRatesController.test.ts | 590 +----------------- 1 file changed, 9 insertions(+), 581 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 1ccead370ca..973bfb694d5 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -10,15 +10,12 @@ import { import type { NetworkClientId, NetworkController, - // NetworkController, NetworkState, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; -// import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; import type { CustomNetworkClientConfiguration, - // CustomNetworkClientConfiguration, NetworkClientConfiguration, } from '@metamask/network-controller/src/types'; import { @@ -170,22 +167,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // allDetectedTokens: {}, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -230,22 +211,6 @@ describe('TokenRatesController', () => { }, }, async ({ controller, triggerTokensStateChange }) => { - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: {}, - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); @@ -297,10 +262,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // ...tokensState, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -337,11 +298,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: tokens, - // allDetectedTokens: {}, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -380,22 +336,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // allDetectedTokens: {}, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -434,22 +374,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: {}, - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -486,22 +410,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: {}, - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 3, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -550,22 +458,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: {}, - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x0EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE2', - // decimals: 3, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -620,28 +512,6 @@ describe('TokenRatesController', () => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: {}, - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0xE1', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0xE2', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); await controller.start(); triggerTokensStateChange({ ...getDefaultTokensState(), @@ -695,22 +565,6 @@ describe('TokenRatesController', () => { }, }, async ({ controller, triggerTokensStateChange }) => { - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // allDetectedTokens: {}, - // }); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); @@ -758,22 +612,6 @@ describe('TokenRatesController', () => { }, }, async ({ controller, triggerTokensStateChange }) => { - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: {}, - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); @@ -1221,32 +1059,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - triggerPreferencesStateChange, - // mockTokensGetState, - }) => { - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [alternateSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller, triggerPreferencesStateChange }) => { await controller.start(); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -1289,32 +1102,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - triggerPreferencesStateChange, - // mockTokensGetState, - }) => { - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller, triggerPreferencesStateChange }) => { await controller.start(); const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') @@ -1362,32 +1150,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - triggerPreferencesStateChange, - // mockTokensGetState, - }) => { - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [alternateSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller, triggerPreferencesStateChange }) => { const updateExchangeRatesSpy = jest .spyOn(controller, 'updateExchangeRates') .mockResolvedValue(); @@ -1441,42 +1204,8 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - // triggerTokensStateChange, - // mockTokensGetState, - }) => { - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller }) => { await controller.start(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( 1, @@ -1524,36 +1253,6 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); await controller.start(); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( @@ -1609,51 +1308,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - // mockGetNetworkClientById, - // triggerTokensStateChange, - // mockTokensGetState, - }) => { - // mockGetNetworkClientById( - // () => - // ({ - // configuration: { - // chainId: ChainId.mainnet, - // ticker: NetworksTicker.mainnet, - // }, - // } as unknown as AutoManagedNetworkClient), - // ); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller }) => { controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -1706,63 +1361,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - // triggerTokensStateChange, - // mockGetNetworkClientById, - // mockTokensGetState, - }) => { - // mockGetNetworkClientById( - // () => - // ({ - // configuration: { - // chainId: ChainId.mainnet, - // ticker: NetworksTicker.mainnet, - // }, - // } as unknown as AutoManagedNetworkClient), - // ); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller }) => { controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -1867,41 +1466,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - // mockGetNetworkClientById, - // triggerTokensStateChange, - }) => { - // mockGetNetworkClientById( - // () => - // ({ - // configuration: { - // chainId: ChainId.mainnet, - // ticker: 'LOL', - // }, - // } as unknown as AutoManagedNetworkClient), - // ); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller }) => { controller.startPollingByNetworkClientId('mainnet'); // flush promises and advance setTimeouts they enqueue 3 times // needed because fetch() doesn't resolve immediately, so any @@ -2005,63 +1570,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - // mockGetNetworkClientById, - // mockTokensGetState, - // triggerTokensStateChange, - }) => { - // mockGetNetworkClientById( - // () => - // ({ - // configuration: { - // chainId: ChainId.mainnet, - // ticker: 'LOL', - // }, - // } as unknown as AutoManagedNetworkClient), - // ); - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller }) => { controller.startPollingByNetworkClientId('mainnet'); // flush promises and advance setTimeouts they enqueue 3 times // needed because fetch() doesn't resolve immediately, so any @@ -2103,51 +1612,7 @@ describe('TokenRatesController', () => { }, }, }, - async ({ - controller, - // triggerTokensStateChange, - // mockGetNetworkClientById, - // mockTokensGetState, - }) => { - // mockGetNetworkClientById( - // () => - // ({ - // configuration: { - // chainId: ChainId.mainnet, - // ticker: NetworksTicker.mainnet, - // }, - // } as unknown as AutoManagedNetworkClient), - // ); - // mockTokensGetState({ - // ...getDefaultTokensState(), - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); + async ({ controller }) => { const pollingToken = controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -2892,24 +2357,18 @@ describe('TokenRatesController', () => { */ type WithControllerCallback = ({ controller, - mockTokensGetState, mockGetNetworkClientById, - // mockNetworkState, - // mockPreferencesGetState, callActionSpy, triggerPreferencesStateChange, triggerTokensStateChange, triggerNetworkStateChange, }: { controller: TokenRatesController; - mockTokensGetState: (state: TokensControllerState) => void; mockGetNetworkClientById: ( handler: ( networkClientId: NetworkClientId, ) => AutoManagedNetworkClient, ) => void; - // mockNetworkState: (state: NetworkState) => void; - // mockPreferencesGetState: (state: PreferencesState) => void; callActionSpy: jest.SpyInstance; triggerPreferencesStateChange: (state: PreferencesState) => void; triggerTokensStateChange: (state: TokensControllerState) => void; @@ -2965,20 +2424,6 @@ async function withController( ReturnType, Parameters >(); - // controllerMessenger.registerActionHandler( - // 'NetworkController:getNetworkClientById', - // mockGetNetworkClientById.mockImplementation(() => { - // return { - // configuration: { - // chainId: ChainId.mainnet, - // ticker: NetworksTicker.mainnet, - // }, - // provider: {}, - // destroy: {}, - // blockTracker: {}, - // } as unknown as AutoManagedNetworkClient; - // }), - // ); const getNetworkClientById = buildMockGetNetworkClientById( mockNetworkClientConfigurationsByNetworkClientId, ); @@ -3012,9 +2457,6 @@ async function withController( try { return await fn({ controller, - mockTokensGetState: (state: TokensControllerState) => { - mockTokensState.mockReturnValue(state); - }, mockGetNetworkClientById: ( handler: ( networkClientId: NetworkClientId, @@ -3022,12 +2464,6 @@ async function withController( ) => { mockGetNetworkClientById.mockImplementation(handler); }, - // mockNetworkState: (state: NetworkState) => { - // mockNetworkState.mockReturnValue(state); - // }, - // mockPreferencesGetState: (state: PreferencesState) => { - // mockPreferencesState.mockReturnValue(state); - // }, callActionSpy, triggerPreferencesStateChange: (state: PreferencesState) => { controllerMessenger.publish( @@ -3127,14 +2563,6 @@ async function callUpdateExchangeRatesMethod({ ...defaultNetworkState, selectedNetworkClientId, }); - // // - // // @ts-expect-error Note that the state given here is intentionally - // // incomplete because the controller only uses this one property, and the - // // tests are written to only consider it. We want this to break if we start - // // relying on more properties, as we'd need to update the tests accordingly. - // controllerEvents.networkStateChange({ - // selectedNetworkClientId, - // }); } if (method === 'updateExchangeRates') { From a3fdb9ebce888e2266adabea993333872ae1843e Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 6 Jun 2024 12:27:34 +0100 Subject: [PATCH 07/14] added suggested changes --- .../src/TokenRatesController.test.ts | 38 ++----------------- .../src/TokenRatesController.ts | 26 +++++-------- packages/assets-controllers/src/index.ts | 9 ++++- 3 files changed, 22 insertions(+), 51 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index bd8e74acad7..c14228c8847 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -9,15 +9,10 @@ import { } from '@metamask/controller-utils'; import type { NetworkClientId, - NetworkController, NetworkState, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; -import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; -import type { - CustomNetworkClientConfiguration, - NetworkClientConfiguration, -} from '@metamask/network-controller/src/types'; +import type { NetworkClientConfiguration } from '@metamask/network-controller/src/types'; import { getDefaultPreferencesState, type PreferencesState, @@ -100,7 +95,7 @@ describe('TokenRatesController', () => { }); it('should set default state', async () => { - await withController({}, async ({ controller }) => { + await withController(async ({ controller }) => { expect(controller.state).toStrictEqual({ marketData: {}, }); @@ -741,7 +736,7 @@ describe('TokenRatesController', () => { ); }); - it('should clear contractExchangeRates state when chain ID changes', async () => { + it('should clear marketData state when chain ID changes', async () => { await withController( { options: { @@ -2320,19 +2315,11 @@ describe('TokenRatesController', () => { */ type WithControllerCallback = ({ controller, - mockGetNetworkClientById, - callActionSpy, triggerPreferencesStateChange, triggerTokensStateChange, triggerNetworkStateChange, }: { controller: TokenRatesController; - mockGetNetworkClientById: ( - handler: ( - networkClientId: NetworkClientId, - ) => AutoManagedNetworkClient, - ) => void; - callActionSpy: jest.SpyInstance; triggerPreferencesStateChange: (state: PreferencesState) => void; triggerTokensStateChange: (state: TokensControllerState) => void; triggerNetworkStateChange: (state: NetworkState) => void; @@ -2385,10 +2372,6 @@ async function withController( }), ); - const mockGetNetworkClientById = jest.fn< - ReturnType, - Parameters - >(); const getNetworkClientById = buildMockGetNetworkClientById( mockNetworkClientConfigurationsByNetworkClientId, ); @@ -2415,8 +2398,6 @@ async function withController( }), ); - const callActionSpy = jest.spyOn(controllerMessenger, 'call'); - const controller = new TokenRatesController({ tokenPricesService: buildMockTokenPricesService(), messenger: buildTokenRatesControllerMessenger(controllerMessenger), @@ -2425,14 +2406,6 @@ async function withController( try { return await fn({ controller, - mockGetNetworkClientById: ( - handler: ( - networkClientId: NetworkClientId, - ) => AutoManagedNetworkClient, - ) => { - mockGetNetworkClientById.mockImplementation(handler); - }, - callActionSpy, triggerPreferencesStateChange: (state: PreferencesState) => { controllerMessenger.publish( 'PreferencesController:stateChange', @@ -2508,10 +2481,7 @@ async function callUpdateExchangeRatesMethod({ 'The "setChainAsCurrent" flag cannot be enabled when calling the "updateExchangeRates" method', ); } - // Note that the state given here is intentionally incomplete because the - // controller only uses these two properties, and the tests are written to - // only consider these two. We want this to break if we start relying on - // more, as we'd need to update the tests accordingly. + triggerTokensStateChange({ ...getDefaultTokensState(), allDetectedTokens: {}, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 27a8bfbacac..19551d5e9b7 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -56,12 +56,9 @@ export type Token = { const DEFAULT_INTERVAL = 180000; -// This interface was created before this ESLint rule was added. -// Convert to a `type` in a future major version. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export interface ContractExchangeRates { - [address: string]: number; -} +export type ContractExchangeRates = { + [address: string]: number | undefined; +}; type MarketDataDetails = { tokenAddress: `0x${string}`; @@ -117,9 +114,6 @@ export const controllerName = 'TokenRatesController'; * Token rates controller state * @property marketData - Market data for tokens, keyed by chain ID and then token contract address. */ -// This interface was created before this ESLint rule was added. -// Convert to a `type` in a future major version. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type TokenRatesControllerState = { marketData: Record>; }; @@ -131,6 +125,11 @@ export type TokenRatesControllerGetStateAction = ControllerGetStateAction< export type TokenRatesControllerActions = TokenRatesControllerGetStateAction; +export type TokenRatesControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + TokenRatesControllerState +>; + export type TokenRatesControllerMessenger = RestrictedControllerMessenger< typeof controllerName, TokenRatesControllerActions | AllowedActions, @@ -139,11 +138,6 @@ export type TokenRatesControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; -export type TokenRatesControllerStateChangeEvent = ControllerStateChangeEvent< - typeof controllerName, - TokenRatesControllerState ->; - export type TokenRatesControllerEvents = TokenRatesControllerStateChangeEvent; /** @@ -183,7 +177,7 @@ async function getCurrencyConversionRate({ } } -const metadata = { +const tokenRatesControllerMetadata = { marketData: { persist: true, anonymous: false }, }; @@ -252,7 +246,7 @@ export class TokenRatesController extends StaticIntervalPollingController< name: controllerName, messenger, state: { ...getDefaultTokenRatesControllerState(), ...state }, - metadata, + metadata: tokenRatesControllerMetadata, }); this.setIntervalLength(interval); diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index 50bc4a27c64..520762c6dc3 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -68,8 +68,15 @@ export type { Token, ContractExchangeRates, TokenRatesControllerState, + TokenRatesController, + ContractMarketData, + TokenRatesControllerGetStateAction, + TokenRatesControllerActions, + TokenRatesControllerMessenger, + TokenRatesControllerStateChangeEvent, + TokenRatesControllerEvents, + getDefaultTokenRatesControllerState, } from './TokenRatesController'; -export { TokenRatesController } from './TokenRatesController'; export type { TokensControllerState, TokensControllerActions, From f52d94f0efd704c21777b3cab98a0060279b6bae Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 6 Jun 2024 16:58:05 +0100 Subject: [PATCH 08/14] Included PR suggestions --- .../assets-controllers/src/TokenRatesController.ts | 4 ++++ packages/assets-controllers/src/index.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 19551d5e9b7..3b5bfac54d9 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -40,7 +40,11 @@ import type { * @property address - Hex address of the token contract * @property decimals - Number of decimals the token uses * @property symbol - Symbol of the token + * @property aggregators - Token aggregators * @property image - Image of the token, url or bit32 image + * @property hasBalanceError - An error while updating the token balance + * @property isERC721 - Is this a ERC 721 token + * @property name - Name of the token */ export type Token = { diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index 520762c6dc3..88cf60275f2 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -65,17 +65,19 @@ export type { } from './TokenListController'; export { TokenListController } from './TokenListController'; export type { - Token, ContractExchangeRates, - TokenRatesControllerState, - TokenRatesController, ContractMarketData, - TokenRatesControllerGetStateAction, + Token, TokenRatesControllerActions, + TokenRatesControllerEvents, + TokenRatesControllerGetStateAction, TokenRatesControllerMessenger, + TokenRatesControllerState, TokenRatesControllerStateChangeEvent, - TokenRatesControllerEvents, +} from './TokenRatesController'; +export { getDefaultTokenRatesControllerState, + TokenRatesController, } from './TokenRatesController'; export type { TokensControllerState, From d2146d5ab9243118c2e89b6a793926c8ad205881 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 6 Jun 2024 17:05:11 +0100 Subject: [PATCH 09/14] Jsdoc comments updated --- packages/assets-controllers/src/TokenRatesController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 3b5bfac54d9..7e153b26f35 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -40,10 +40,10 @@ import type { * @property address - Hex address of the token contract * @property decimals - Number of decimals the token uses * @property symbol - Symbol of the token - * @property aggregators - Token aggregators + * @property aggregators - An array containing the token's aggregators * @property image - Image of the token, url or bit32 image - * @property hasBalanceError - An error while updating the token balance - * @property isERC721 - Is this a ERC 721 token + * @property hasBalanceError - 'true' if there is an error while updating the token balance + * @property isERC721 - 'true' if the token is a ERC721 token * @property name - Name of the token */ From dd09e2ec59b9fb54a692b2c77ff82872e1c0afd6 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Thu, 6 Jun 2024 17:45:45 +0100 Subject: [PATCH 10/14] private handle to #handle --- packages/assets-controllers/src/TokenRatesController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index b02b5e771a4..c0734c25c74 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -201,7 +201,7 @@ export class TokenRatesController extends StaticIntervalPollingController< TokenRatesControllerState, TokenRatesControllerMessenger > { - private handle?: ReturnType; + #handle?: ReturnType; #pollState = PollState.Inactive; @@ -430,8 +430,8 @@ export class TokenRatesController extends StaticIntervalPollingController< * Clear the active polling timer, if present. */ #stopPoll() { - if (this.handle) { - clearTimeout(this.handle); + if (this.#handle) { + clearTimeout(this.#handle); } } @@ -443,7 +443,7 @@ export class TokenRatesController extends StaticIntervalPollingController< // Poll using recursive `setTimeout` instead of `setInterval` so that // requests don't stack if they take longer than the polling interval - this.handle = setTimeout(() => { + this.#handle = setTimeout(() => { this.#poll(); }, this.#interval); } From 87f74c180860bdd20ebe4421fc4402a0f65f1e18 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Fri, 7 Jun 2024 11:06:33 +0100 Subject: [PATCH 11/14] remove unnecessary space before Token type --- packages/assets-controllers/src/TokenRatesController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index c0734c25c74..35e3198070e 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -46,7 +46,6 @@ import type { * @property isERC721 - 'true' if the token is a ERC721 token * @property name - Name of the token */ - export type Token = { address: string; decimals: number; From 71d960ee582739882c6ec6198f94d63dd02cc693 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Fri, 7 Jun 2024 12:12:17 +0100 Subject: [PATCH 12/14] The missing jsdoc comments added --- .../src/TokenRatesController.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 35e3198070e..afe3129b62d 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -86,6 +86,9 @@ type MarketDataDetails = { totalVolume: number; }; +/** + * Represents a mapping of token contract addresses to their market data. + */ export type ContractMarketData = Record; enum PollState { @@ -109,6 +112,9 @@ export type AllowedEvents = | TokensControllerStateChangeEvent | NetworkControllerStateChangeEvent; +/** + * The name of the {@link TokenRatesController}. + */ export const controllerName = 'TokenRatesController'; /** @@ -121,18 +127,35 @@ export type TokenRatesControllerState = { marketData: Record>; }; +/** + * The action that can be performed to get the state of the {@link TokenRatesController}. + */ export type TokenRatesControllerGetStateAction = ControllerGetStateAction< typeof controllerName, TokenRatesControllerState >; +/** + * The actions that can be performed using the {@link TokenRatesController}. + */ export type TokenRatesControllerActions = TokenRatesControllerGetStateAction; +/** + * The event that {@link TokenRatesController} can emit. + */ export type TokenRatesControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, TokenRatesControllerState >; +/** + * The events that {@link TokenRatesController} can emit. + */ +export type TokenRatesControllerEvents = TokenRatesControllerStateChangeEvent; + +/** + * The messenger of the {@link TokenRatesController} for communication. + */ export type TokenRatesControllerMessenger = RestrictedControllerMessenger< typeof controllerName, TokenRatesControllerActions | AllowedActions, @@ -141,8 +164,6 @@ export type TokenRatesControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; -export type TokenRatesControllerEvents = TokenRatesControllerStateChangeEvent; - /** * Uses the CryptoCompare API to fetch the exchange rate between one currency * and another, i.e., the multiplier to apply the amount of one currency in @@ -184,6 +205,11 @@ const tokenRatesControllerMetadata = { marketData: { persist: true, anonymous: false }, }; +/** + * Get the default {@link TokenRatesController} state. + * + * @returns The default {@link TokenRatesController} state. + */ export const getDefaultTokenRatesControllerState = (): TokenRatesControllerState => { return { @@ -229,7 +255,7 @@ export class TokenRatesController extends StaticIntervalPollingController< * @param options.interval - The polling interval in ms * @param options.disabled - Boolean to track if network requests are blocked * @param options.tokenPricesService - An object in charge of retrieving token price - * @param options.messenger - The controller messaging system + * @param options.messenger - The controller messenger instance for communication * @param options.state - Initial state to set on this controller */ constructor({ @@ -239,10 +265,25 @@ export class TokenRatesController extends StaticIntervalPollingController< messenger, state, }: { + /** + * The polling interval in milliseconds. + */ interval?: number; + /** + * To Allow/Block controller to make active and passive polling requests + */ disabled?: boolean; + /** + * The token price service instance. + */ tokenPricesService: AbstractTokenPricesService; + /** + * The messenger instance for communication. + */ messenger: TokenRatesControllerMessenger; + /** + * The initial state of the controller. + */ state?: Partial; }) { super({ From f96fcb76dbeaa8199df448524d070f6f5e5dcdf9 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Fri, 7 Jun 2024 12:29:05 +0100 Subject: [PATCH 13/14] clean up --- .../src/TokenRatesController.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index afe3129b62d..c3b4a5b94f2 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -265,25 +265,10 @@ export class TokenRatesController extends StaticIntervalPollingController< messenger, state, }: { - /** - * The polling interval in milliseconds. - */ interval?: number; - /** - * To Allow/Block controller to make active and passive polling requests - */ disabled?: boolean; - /** - * The token price service instance. - */ tokenPricesService: AbstractTokenPricesService; - /** - * The messenger instance for communication. - */ messenger: TokenRatesControllerMessenger; - /** - * The initial state of the controller. - */ state?: Partial; }) { super({ From db8654d02461e2bb50994612541c3c1b12f30e4f Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Fri, 7 Jun 2024 19:39:06 +0100 Subject: [PATCH 14/14] include suggestions --- packages/assets-controllers/src/TokenRatesController.test.ts | 4 ++-- packages/assets-controllers/src/TokenRatesController.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index c14228c8847..10d6122f6c7 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -874,7 +874,7 @@ describe('TokenRatesController', () => { ); }); - it('should clear contractExchangeRates state when ticker changes', async () => { + it('should clear marketData state when ticker changes', async () => { await withController( { options: { @@ -927,7 +927,7 @@ describe('TokenRatesController', () => { ); }); - it('should clear contractExchangeRates state when chain ID changes', async () => { + it('should clear marketData state when chain ID changes', async () => { await withController( { options: { diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index c3b4a5b94f2..40e87593872 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -104,6 +104,7 @@ export type AllowedActions = | NetworkControllerGetNetworkClientByIdAction | NetworkControllerGetStateAction | PreferencesControllerGetStateAction; + /** * The external events available to the {@link TokenRatesController}. */