Skip to content

Commit

Permalink
feat: upgrade assets controllers to 42 with multichain token rates (#…
Browse files Browse the repository at this point in the history
…12270)

## **Description**

This PR upgrades asset controllers to 42. This version contains
`resetState` functions in controllers, so it no longer needs to be
patched. It also allows us to poll erc20 token rates across chains.

## **Related issues**



## **Manual testing steps**

With PORTFOLIO_VIEW=false and PORTFOLIO_VIEW=true:

1. Import some erc20 tokens
2. Verify erc20 token prices are correct
3. Switch chains
4. Verify erc20 token prices are correct
5. In settings, change the fiat currency
6. Verify erc20 token prices are correct

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

No visual changes. erc20 prices should be correct when adding tokens,
switching networks, and switching currencies.


https://github.com/user-attachments/assets/29a733ae-630f-4d1f-952c-d5ac54bef14a

<!-- [screenshots/recordings] -->

## **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.
  • Loading branch information
bergeron authored Nov 20, 2024
1 parent a135266 commit 0ec4364
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 179 deletions.
4 changes: 2 additions & 2 deletions app/components/UI/Tokens/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jest.mock('../../../core/Engine', () => ({
updateExchangeRate: jest.fn(() => Promise.resolve()),
},
TokenRatesController: {
updateExchangeRates: jest.fn(() => Promise.resolve()),
updateExchangeRatesByChainId: jest.fn(() => Promise.resolve()),
},
NetworkController: {
getNetworkClientById: () => ({
Expand Down Expand Up @@ -359,7 +359,7 @@ describe('Tokens', () => {
Engine.context.CurrencyRateController.updateExchangeRate,
).toHaveBeenCalled();
expect(
Engine.context.TokenRatesController.updateExchangeRates,
Engine.context.TokenRatesController.updateExchangeRatesByChainId,
).toHaveBeenCalled();
});
});
Expand Down
18 changes: 13 additions & 5 deletions app/components/UI/Tokens/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
selectChainId,
selectNetworkConfigurations,
} from '../../../selectors/networkController';
import { getDecimalChainId } from '../../../util/networks';
import { getDecimalChainId, isPortfolioViewEnabled } from '../../../util/networks';
import { isZero } from '../../../util/lodash';
import createStyles from './styles';
import { TokenList } from './TokenList';
Expand Down Expand Up @@ -183,7 +183,17 @@ const Tokens: React.FC<TokensI> = ({ tokens }) => {
TokenDetectionController.detectTokens(),
AccountTrackerController.refresh(),
CurrencyRateController.updateExchangeRate(nativeCurrencies),
TokenRatesController.updateExchangeRates(),
...(isPortfolioViewEnabled
? Object.values(networkConfigurationsByChainId)
: [networkConfigurationsByChainId[chainId]]
).map((network) =>
TokenRatesController.updateExchangeRatesByChainId(
{
chainId: network.chainId,
nativeCurrency: network.nativeCurrency,
},
),
)
];
await Promise.all(actions).catch((error) => {
Logger.error(error, 'Error while refreshing tokens');
Expand Down Expand Up @@ -239,15 +249,13 @@ const Tokens: React.FC<TokensI> = ({ tokens }) => {
const onActionSheetPress = (index: number) =>
index === 0 ? removeToken() : null;

const isTokenFilterEnabled = process.env.PORTFOLIO_VIEW === 'true';

return (
<View
style={styles.wrapper}
testID={WalletViewSelectorsIDs.TOKENS_CONTAINER}
>
<View style={styles.actionBarWrapper}>
{isTokenFilterEnabled ? (
{isPortfolioViewEnabled ? (
<View style={styles.controlButtonOuterWrapper}>
<ButtonBase
label={
Expand Down
2 changes: 2 additions & 0 deletions app/components/hooks/AssetPolling/AssetPollingProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { ReactNode } from 'react';
import useCurrencyRatePolling from './useCurrencyRatePolling';
import useTokenRatesPolling from './useTokenRatesPolling';

// 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();
useTokenRatesPolling();

return <>{children}</>;
};
51 changes: 51 additions & 0 deletions app/components/hooks/AssetPolling/useTokenRatesPolling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import useTokenRatesPolling from './useTokenRatesPolling';
import { renderHookWithProvider } from '../../../util/test/renderWithProvider';
import Engine from '../../../core/Engine';

jest.mock('../../../core/Engine', () => ({
context: {
TokenRatesController: {
startPolling: jest.fn(),
stopPollingByPollingToken: jest.fn(),
},
},
}));

describe('useTokenRatesPolling', () => {

beforeEach(() => {
jest.resetAllMocks();
});

const state = {
engine: {
backgroundState: {
TokenRatesController: {
marketData: {},
},
NetworkController: {
networkConfigurationsByChainId: {
'0x1': {},
'0x89': {},
},
},
},
},
};

it('Should poll by provided chain ids, and stop polling on dismount', async () => {

const { unmount } = renderHookWithProvider(() => useTokenRatesPolling({chainIds: ['0x1']}), {state});

const mockedTokenRatesController = jest.mocked(Engine.context.TokenRatesController);

expect(mockedTokenRatesController.startPolling).toHaveBeenCalledTimes(1);
expect(
mockedTokenRatesController.startPolling
).toHaveBeenCalledWith({chainId: '0x1'});

expect(mockedTokenRatesController.stopPollingByPollingToken).toHaveBeenCalledTimes(0);
unmount();
expect(mockedTokenRatesController.stopPollingByPollingToken).toHaveBeenCalledTimes(1);
});
});
39 changes: 39 additions & 0 deletions app/components/hooks/AssetPolling/useTokenRatesPolling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useSelector } from 'react-redux';
import usePolling from '../usePolling';
import Engine from '../../../core/Engine';
import { selectChainId, selectNetworkConfigurations } from '../../../selectors/networkController';
import { Hex } from '@metamask/utils';
import { selectContractExchangeRates, selectTokenMarketData } from '../../../selectors/tokenRatesController';
import { isPortfolioViewEnabled } from '../../../util/networks';

const useTokenRatesPolling = ({ chainIds }: { chainIds?: Hex[] } = {}) => {

// Selectors to determine polling input
const networkConfigurations = useSelector(selectNetworkConfigurations);
const currentChainId = useSelector(selectChainId);

// Selectors returning state updated by the polling
const contractExchangeRates = useSelector(selectContractExchangeRates);
const tokenMarketData = useSelector(selectTokenMarketData);

const chainIdsToPoll = isPortfolioViewEnabled
? (chainIds ?? Object.keys(networkConfigurations))
: [currentChainId];

const { TokenRatesController } = Engine.context;

usePolling({
startPolling:
TokenRatesController.startPolling.bind(TokenRatesController),
stopPollingByPollingToken:
TokenRatesController.stopPollingByPollingToken.bind(TokenRatesController),
input: chainIdsToPoll.map((chainId) => ({chainId: chainId as Hex})),
});

return {
contractExchangeRates,
tokenMarketData
};
};

export default useTokenRatesPolling;
12 changes: 6 additions & 6 deletions app/core/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,8 @@ export class Engine {
assetsContractController.getBalancesInSingleCall.bind(
assetsContractController,
),
platform: 'mobile',
useAccountsAPI: true,
}),

new NftDetectionController({
Expand Down Expand Up @@ -1909,14 +1911,12 @@ export class Engine {
TokenDetectionController,
TokenListController,
TransactionController,
TokenRatesController,
} = this.context;

TokenListController.start();
TokenDetectionController.start();
// leaving the reference of TransactionController here, rather than importing it from utils to avoid circular dependency
TransactionController.startIncomingTransactionPolling();
TokenRatesController.start();
}

configureControllersOnNetworkChange() {
Expand Down Expand Up @@ -2159,11 +2159,11 @@ export class Engine {
// SelectedNetworkController.unsetAllDomains()

//Clear assets info
TokensController.reset();
NftController.reset();
TokensController.resetState();
NftController.resetState();

TokenBalancesController.reset();
TokenRatesController.reset();
TokenBalancesController.resetState();
TokenRatesController.resetState();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(TransactionController as any).update(() => ({
Expand Down
6 changes: 6 additions & 0 deletions app/selectors/tokenRatesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export const selectContractExchangeRates = createSelector(
(chainId: Hex, tokenRatesControllerState: TokenRatesControllerState) =>
tokenRatesControllerState.marketData[chainId],
);

export const selectTokenMarketData = createSelector(
selectTokenRatesControllerState,
(tokenRatesControllerState: TokenRatesControllerState) =>
tokenRatesControllerState.marketData,
);
3 changes: 3 additions & 0 deletions app/util/networks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -592,3 +592,6 @@ export const isChainPermissionsFeatureEnabled =

export const isPermissionsSettingsV1Enabled =
process.env.MM_PERMISSIONS_SETTINGS_V1_ENABLED === '1';

export const isPortfolioViewEnabled =
process.env.PORTFOLIO_VIEW === 'true';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"@metamask/accounts-controller": "^18.2.1",
"@metamask/address-book-controller": "^6.0.1",
"@metamask/approval-controller": "^7.1.0",
"@metamask/assets-controllers": "^41.0.0",
"@metamask/assets-controllers": "^42.0.0",
"@metamask/base-controller": "^7.0.1",
"@metamask/composable-controller": "^3.0.0",
"@metamask/controller-utils": "^11.3.0",
Expand Down
Loading

0 comments on commit 0ec4364

Please sign in to comment.