From 2792684aadea4644bf759d8eade5532949225639 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Thu, 14 Nov 2024 09:46:03 -0800 Subject: [PATCH] feat: multichain currency rate polling (#12268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR uses the `CurrencyRateController` to poll native currency prices across chains. This will keep prices updated in state across all networks. Because we're polling across all networks, it's no longer necessary to trigger updates when switching chains, so these places have been removed. Once we start showing tokens across all networks, "switching chains" will become a less relevant action. ## **Related issues** ## **Manual testing steps** 1. Verify fiat prices are correct for the native currency on a network 2. Switch to a network with a different native currency, 3. Verify fiat prices are correct for that network ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/components/UI/NetworkModal/index.tsx | 4 +- .../NetworkSwitcher/NetworkSwitcher.test.tsx | 12 ------ .../Views/NetworkSwitcher/NetworkSwitcher.tsx | 4 +- .../Views/NetworkSelector/NetworkSelector.tsx | 16 ------- app/components/Views/Root/index.js | 9 ++-- .../NetworksSettings/NetworkSettings/index.js | 6 +-- .../Views/Settings/NetworksSettings/index.js | 3 +- .../AssetPolling/AssetPollingProvider.tsx | 11 +++++ .../useCurrencyRatePolling.test.ts | 42 +++++++++++++++++++ .../AssetPolling/useCurrencyRatePolling.ts | 39 +++++++++++++++++ app/core/Engine.ts | 10 +---- .../RPCMethods/lib/ethereum-chain-utils.js | 2 - .../wallet_addEthereumChain.test.js | 2 - app/selectors/currencyRateController.ts | 7 ++++ app/util/networks/handleNetworkSwitch.test.ts | 7 ---- app/util/networks/handleNetworkSwitch.ts | 5 --- 16 files changed, 111 insertions(+), 68 deletions(-) create mode 100644 app/components/hooks/AssetPolling/AssetPollingProvider.tsx create mode 100644 app/components/hooks/AssetPolling/useCurrencyRatePolling.test.ts create mode 100644 app/components/hooks/AssetPolling/useCurrencyRatePolling.ts diff --git a/app/components/UI/NetworkModal/index.tsx b/app/components/UI/NetworkModal/index.tsx index d62f026fc21..3c58495c9f5 100644 --- a/app/components/UI/NetworkModal/index.tsx +++ b/app/components/UI/NetworkModal/index.tsx @@ -317,12 +317,10 @@ const NetworkModals = (props: NetworkProps) => { }; const switchNetwork = async () => { - const { NetworkController, CurrencyRateController } = Engine.context; + const { NetworkController } = Engine.context; const url = new URLPARSE(rpcUrl); const existingNetwork = networkConfigurationByChainId[chainId]; - CurrencyRateController.updateExchangeRate([ticker]); - if (!isPrivateConnection(url.hostname)) { url.set('protocol', 'https:'); } diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx index 49eab63ff75..f9921440bd4 100644 --- a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx +++ b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.test.tsx @@ -303,18 +303,6 @@ describe('NetworkSwitcher View', () => { ], ] `); - expect( - (Engine.context.CurrencyRateController.updateExchangeRate as jest.Mock) - .mock.calls, - ).toMatchInlineSnapshot(` - [ - [ - [ - "POL", - ], - ], - ] - `); }); it('renders correctly with errors', async () => { diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx index 5178c067e10..9894e7e5d13 100644 --- a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx +++ b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx @@ -153,14 +153,13 @@ function NetworkSwitcher() { const switchNetwork = useCallback( (networkConfiguration) => { - const { CurrencyRateController, NetworkController } = Engine.context; + const { NetworkController } = Engine.context; const config = Object.values(networkConfigurations).find( ({ chainId }) => chainId === networkConfiguration.chainId, ); if (config) { const { - nativeCurrency: ticker, rpcEndpoints, defaultRpcEndpointIndex, } = config; @@ -168,7 +167,6 @@ function NetworkSwitcher() { const { networkClientId } = rpcEndpoints?.[defaultRpcEndpointIndex] ?? {}; - CurrencyRateController.updateExchangeRate([ticker]); NetworkController.setActiveNetwork(networkClientId); navigateToGetStarted(); } diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx index ea36deab166..21bf02d1b71 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx @@ -37,9 +37,7 @@ import Networks, { } from '../../../util/networks'; import { LINEA_MAINNET, - LINEA_SEPOLIA, MAINNET, - SEPOLIA, } from '../../../constants/network'; import Button from '../../../component-library/components/Buttons/Button/Button'; import { @@ -65,7 +63,6 @@ import createStyles from './NetworkSelector.styles'; import { BUILT_IN_NETWORKS, InfuraNetworkType, - TESTNET_TICKER_SYMBOLS, } from '@metamask/controller-utils'; import InfoModal from '../../../../app/components/UI/Swaps/components/InfoModal'; import hideKeyFromUrl from '../../../util/hideKeyFromUrl'; @@ -241,7 +238,6 @@ const NetworkSelector = () => { const onSetRpcTarget = async (networkConfiguration: NetworkConfiguration) => { const { - CurrencyRateController, NetworkController, SelectedNetworkController, } = Engine.context; @@ -254,7 +250,6 @@ const NetworkSelector = () => { const { name: nickname, chainId, - nativeCurrency: ticker, rpcEndpoints, defaultRpcEndpointIndex, } = networkConfiguration; @@ -268,8 +263,6 @@ const NetworkSelector = () => { networkConfigurationId, ); } else { - CurrencyRateController.updateExchangeRate([ticker]); - const { networkClientId } = rpcEndpoints[defaultRpcEndpointIndex]; await NetworkController.setActiveNetwork(networkClientId); @@ -370,7 +363,6 @@ const NetworkSelector = () => { }); const { NetworkController, - CurrencyRateController, AccountTrackerController, SelectedNetworkController, } = Engine.context; @@ -378,13 +370,6 @@ const NetworkSelector = () => { if (domainIsConnectedDapp && process.env.MULTICHAIN_V1) { SelectedNetworkController.setNetworkClientIdForDomain(origin, type); } else { - let ticker = type; - if (type === LINEA_SEPOLIA) { - ticker = TESTNET_TICKER_SYMBOLS.LINEA_SEPOLIA as InfuraNetworkType; - } - if (type === SEPOLIA) { - ticker = TESTNET_TICKER_SYMBOLS.SEPOLIA as InfuraNetworkType; - } const networkConfiguration = networkConfigurations[BUILT_IN_NETWORKS[type].chainId]; @@ -394,7 +379,6 @@ const NetworkSelector = () => { networkConfiguration.defaultRpcEndpointIndex ].networkClientId ?? type; - CurrencyRateController.updateExchangeRate([ticker]); NetworkController.setActiveNetwork(clientId); closeRpcModal(); AccountTrackerController.refresh(); diff --git a/app/components/Views/Root/index.js b/app/components/Views/Root/index.js index 7ffa81485d3..05a52cb033b 100644 --- a/app/components/Views/Root/index.js +++ b/app/components/Views/Root/index.js @@ -12,6 +12,7 @@ import { useAppTheme, ThemeContext } from '../../../util/theme'; import { ToastContextWrapper } from '../../../component-library/components/Toast'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { isTest } from '../../../util/test/utils'; +import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; /** * Top level of the component hierarchy @@ -85,9 +86,11 @@ const ConnectedRoot = () => { - - - + + + + + diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index 44873cd9344..2833c06f795 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -822,14 +822,13 @@ export class NetworkSettings extends PureComponent { shouldNetworkSwitchPopToWallet, navigation, }) => { - const { NetworkController, CurrencyRateController } = Engine.context; + const { NetworkController } = Engine.context; const url = new URL(rpcUrl); if (!isPrivateConnection(url.hostname)) { url.set('protocol', 'https:'); } - CurrencyRateController.updateExchangeRate([ticker]); const existingNetwork = this.props.networkConfigurations[chainId]; const indexRpc = rpcUrls.findIndex(({ url }) => url === rpcUrl); @@ -1530,7 +1529,7 @@ export class NetworkSettings extends PureComponent { }; switchToMainnet = () => { - const { NetworkController, CurrencyRateController } = Engine.context; + const { NetworkController } = Engine.context; const { networkConfigurations } = this.props; const { networkClientId } = @@ -1538,7 +1537,6 @@ export class NetworkSettings extends PureComponent { networkConfigurations.defaultRpcEndpointIndex ] ?? {}; - CurrencyRateController.updateExchangeRate(NetworksTicker.mainnet); NetworkController.setActiveNetwork(networkClientId); setTimeout(async () => { diff --git a/app/components/Views/Settings/NetworksSettings/index.js b/app/components/Views/Settings/NetworksSettings/index.js index b96e2bb7e4e..56a1b062165 100644 --- a/app/components/Views/Settings/NetworksSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/index.js @@ -186,9 +186,8 @@ class NetworksSettings extends PureComponent { }; switchToMainnet = () => { - const { NetworkController, CurrencyRateController } = Engine.context; + const { NetworkController } = Engine.context; - CurrencyRateController.updateExchangeRate(NetworksTicker.mainnet); NetworkController.setProviderType(MAINNET); setTimeout(async () => { diff --git a/app/components/hooks/AssetPolling/AssetPollingProvider.tsx b/app/components/hooks/AssetPolling/AssetPollingProvider.tsx new file mode 100644 index 00000000000..7c7c3eda5fa --- /dev/null +++ b/app/components/hooks/AssetPolling/AssetPollingProvider.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode } from 'react'; +import useCurrencyRatePolling from './useCurrencyRatePolling'; + +// This provider is a step towards making controller polling fully UI based. +// Eventually, individual UI components will call the use*Polling hooks to +// poll and return particular data. This polls globally in the meantime. +export const AssetPollingProvider = ({ children }: { children: ReactNode }) => { + useCurrencyRatePolling(); + + return <>{children}; +}; diff --git a/app/components/hooks/AssetPolling/useCurrencyRatePolling.test.ts b/app/components/hooks/AssetPolling/useCurrencyRatePolling.test.ts new file mode 100644 index 00000000000..fbf94e9d316 --- /dev/null +++ b/app/components/hooks/AssetPolling/useCurrencyRatePolling.test.ts @@ -0,0 +1,42 @@ +import useCurrencyRatePolling from './useCurrencyRatePolling'; +import { renderHookWithProvider } from '../../../util/test/renderWithProvider'; +import Engine from '../../../core/Engine'; + +jest.mock('../../../core/Engine', () => ({ + context: { + CurrencyRateController: { + startPolling: jest.fn(), + stopPollingByPollingToken: jest.fn(), + }, + }, +})); + +describe('useCurrencyRatePolling', () => { + + it('Should poll by the native currencies in network state', async () => { + + const state = { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + nativeCurrency: 'ETH', + }, + '0x89': { + nativeCurrency: 'POL', + }, + }, + }, + }, + }, + }; + + renderHookWithProvider(() => useCurrencyRatePolling(), {state}); + + expect( + jest.mocked(Engine.context.CurrencyRateController.startPolling) + ).toHaveBeenCalledWith({nativeCurrencies: ['ETH', 'POL']}); + + }); +}); diff --git a/app/components/hooks/AssetPolling/useCurrencyRatePolling.ts b/app/components/hooks/AssetPolling/useCurrencyRatePolling.ts new file mode 100644 index 00000000000..7663c1e5dcb --- /dev/null +++ b/app/components/hooks/AssetPolling/useCurrencyRatePolling.ts @@ -0,0 +1,39 @@ +import { useSelector } from 'react-redux'; +import usePolling from '../usePolling'; +import { selectNetworkConfigurations } from '../../../selectors/networkController'; +import Engine from '../../../core/Engine'; +import { selectConversionRate, selectCurrencyRates } from '../../../selectors/currencyRateController'; + +// Polls native currency prices across networks. +const useCurrencyRatePolling = () => { + + // Selectors to determine polling input + const networkConfigurations = useSelector(selectNetworkConfigurations); + + // Selectors returning state updated by the polling + const conversionRate = useSelector(selectConversionRate); + const currencyRates = useSelector(selectCurrencyRates); + + const nativeCurrencies = [ + ...new Set( + Object.values(networkConfigurations).map((n) => n.nativeCurrency), + ), + ]; + + const { CurrencyRateController } = Engine.context; + + usePolling({ + startPolling: + CurrencyRateController.startPolling.bind(CurrencyRateController), + stopPollingByPollingToken: + CurrencyRateController.stopPollingByPollingToken.bind(CurrencyRateController), + input: [{nativeCurrencies}], + }); + + return { + conversionRate, + currencyRates, + }; +}; + +export default useCurrencyRatePolling; diff --git a/app/core/Engine.ts b/app/core/Engine.ts index f166cc73782..55d6fdbf2e3 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -708,15 +708,7 @@ export class Engine { }), state: initialState.CurrencyRateController, }); - const currentNetworkConfig = - networkController.getNetworkConfigurationByNetworkClientId( - networkController?.state.selectedNetworkClientId, - ); - currencyRateController.startPolling({ - nativeCurrencies: currentNetworkConfig?.nativeCurrency - ? [currentNetworkConfig?.nativeCurrency] - : [], - }); + const gasFeeController = new GasFeeController({ // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: this.controllerMessenger.getRestricted({ diff --git a/app/core/RPCMethods/lib/ethereum-chain-utils.js b/app/core/RPCMethods/lib/ethereum-chain-utils.js index cb71659205f..dc3575ad718 100644 --- a/app/core/RPCMethods/lib/ethereum-chain-utils.js +++ b/app/core/RPCMethods/lib/ethereum-chain-utils.js @@ -207,7 +207,6 @@ export async function switchToNetwork({ isAddNetworkFlow = false, }) { const { - CurrencyRateController, NetworkController, PermissionController, SelectedNetworkController, @@ -300,7 +299,6 @@ export async function switchToNetwork({ networkConfigurationId || networkConfiguration.networkType, ); } else { - CurrencyRateController.updateExchangeRate(requestData.ticker); NetworkController.setActiveNetwork( networkConfigurationId || networkConfiguration.networkType, ); diff --git a/app/core/RPCMethods/wallet_addEthereumChain.test.js b/app/core/RPCMethods/wallet_addEthereumChain.test.js index ff2bbd5cd61..5ed8451424b 100644 --- a/app/core/RPCMethods/wallet_addEthereumChain.test.js +++ b/app/core/RPCMethods/wallet_addEthereumChain.test.js @@ -429,7 +429,6 @@ describe('RPC Method - wallet_addEthereumChain', () => { }), ); expect(spyOnSetActiveNetwork).toHaveBeenCalledTimes(1); - expect(spyOnUpdateExchangeRate).toHaveBeenCalledTimes(1); }); it('should not add a networkConfiguration that has a chainId that already exists in wallet state, and should switch to the existing network', async () => { @@ -468,7 +467,6 @@ describe('RPC Method - wallet_addEthereumChain', () => { expect(spyOnAddNetwork).not.toHaveBeenCalled(); expect(spyOnSetActiveNetwork).toHaveBeenCalledTimes(1); - expect(spyOnUpdateExchangeRate).toHaveBeenCalledTimes(1); }); describe('MM_CHAIN_PERMISSIONS is enabled', () => { diff --git a/app/selectors/currencyRateController.ts b/app/selectors/currencyRateController.ts index fa58efe5106..9617d02710d 100644 --- a/app/selectors/currencyRateController.ts +++ b/app/selectors/currencyRateController.ts @@ -34,3 +34,10 @@ export const selectCurrentCurrency = createSelector( (currencyRateControllerState: CurrencyRateState) => currencyRateControllerState?.currentCurrency, ); + +export const selectCurrencyRates = createSelector( + selectCurrencyRateControllerState, + ( + currencyRateControllerState: CurrencyRateState, + ) => currencyRateControllerState?.currencyRates, +); diff --git a/app/util/networks/handleNetworkSwitch.test.ts b/app/util/networks/handleNetworkSwitch.test.ts index 9084fb3c89e..776891fd60b 100644 --- a/app/util/networks/handleNetworkSwitch.test.ts +++ b/app/util/networks/handleNetworkSwitch.test.ts @@ -136,9 +136,6 @@ describe('useHandleNetworkSwitch', () => { const nickname = handleNetworkSwitch('1338'); - expect( - mockEngine.context.CurrencyRateController.updateExchangeRate, - ).toBeCalledWith(['TEST']); expect( mockEngine.context.NetworkController.setActiveNetwork, ).toBeCalledWith('networkId1'); @@ -153,10 +150,6 @@ describe('useHandleNetworkSwitch', () => { const networkType = handleNetworkSwitch('11155111'); - // TODO: This is a bug, it should be set to SepoliaETH - expect( - mockEngine.context.CurrencyRateController.updateExchangeRate, - ).toBeCalledWith(['ETH']); expect( mockEngine.context.NetworkController.setProviderType, ).not.toBeCalledWith(); diff --git a/app/util/networks/handleNetworkSwitch.ts b/app/util/networks/handleNetworkSwitch.ts index b05ad5df60c..60fddb9012f 100644 --- a/app/util/networks/handleNetworkSwitch.ts +++ b/app/util/networks/handleNetworkSwitch.ts @@ -1,4 +1,3 @@ -import { CurrencyRateController } from '@metamask/assets-controllers'; import { toHex } from '@metamask/controller-utils'; import { NetworkController } from '@metamask/network-controller'; import Engine from '../../core/Engine'; @@ -22,8 +21,6 @@ const handleNetworkSwitch = (switchToChainId: string): string | undefined => { return; } - const currencyRateController = Engine.context - .CurrencyRateController as CurrencyRateController; const networkController = Engine.context .NetworkController as NetworkController; const chainId = selectChainId(store.getState()); @@ -44,13 +41,11 @@ const handleNetworkSwitch = (switchToChainId: string): string | undefined => { , { name: nickname, - nativeCurrency: ticker, rpcEndpoints, defaultRpcEndpointIndex, }, ] = entry; - currencyRateController.updateExchangeRate([ticker]); const { networkClientId } = rpcEndpoints[defaultRpcEndpointIndex]; networkController.setActiveNetwork(networkClientId);