Skip to content

Commit

Permalink
feat: upgrade assets controllers to v44 (#12344)
Browse files Browse the repository at this point in the history
## **Description**

Upgrades the assets controllers to v44. In this version, the token
balances controller now stores erc20 balances across all chains and
accounts, instead of just the current chain and account like before.
This allows polling erc20 balances across chains.

## **Related issues**

Depends on: #12340

## **Manual testing steps**

With PORTFOLIO_VIEW=false and PORTFOLIO_VIEW=true:

1. Onboard
2. Verify erc20 tokens have correct balances
3. Switch chains
4. Verify the erc20 tokens on the new chain have correct balances
5. Send a TX that changes an erc20 balance
6. Verify it gets updated after tx completes

## **Screenshots/Recordings**

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

### **Before**

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

### **After**

<!-- [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.

---------

Co-authored-by: salimtb <[email protected]>
  • Loading branch information
bergeron and salimtb authored Nov 26, 2024
1 parent 5ed686b commit 16a6d6e
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 203 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ const mockInitialState: DeepPartial<RootState> = {
},
},
TokenBalancesController: {
contractBalances: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
},
tokenBalances: { },
},
AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
},
Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/AccountInfoCard/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const mockInitialState: DeepPartial<RootState> = {
}),
},
TokenBalancesController: {
contractBalances: {},
tokenBalances: {},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import { BN } from 'ethereumjs-util';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import AppConstants from '../../../../../../app/core/AppConstants';
Expand Down Expand Up @@ -76,11 +75,7 @@ const initialState = {
},
},
TokenBalancesController: {
contractBalances: {
'0x00': new BN(2),
'0x01': new BN(2),
'0x02': new BN(0),
},
tokenBalances: { },
},
},
},
Expand Down
44 changes: 37 additions & 7 deletions app/components/UI/Tokens/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,22 @@ jest.mock('../../../core/Engine', () => ({
},
}));

const selectedAddress = '0x123';

const initialState = {
engine: {
backgroundState: {
...backgroundState,
AccountsController: {
internalAccounts: {
selectedAccount: '1',
accounts: {
'1': {
address: selectedAddress,
},
},
},
},
TokensController: {
tokens: [
{
Expand Down Expand Up @@ -109,11 +121,15 @@ const initialState = {
},
},
TokenBalancesController: {
contractBalances: {
'0x00': new BN(2),
'0x01': new BN(2),
'0x02': new BN(0),
},
tokenBalances:{
[selectedAddress]: {
'0x1': {
'0x00': new BN(2),
'0x01': new BN(2),
'0x02': new BN(0),
}
}
}
},
},
},
Expand Down Expand Up @@ -263,9 +279,23 @@ describe('Tokens', () => {
},
},
},
AccountsController: {
internalAccounts: {
selectedAccount: '1',
accounts: {
'1': {
address: selectedAddress,
},
},
},
},
TokenBalancesController: {
contractBalances: {
'0x02': new BN(1),
tokenBalances: {
[selectedAddress]: {
'0x1': {
'0x02': new BN(1),
},
},
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion app/components/Views/DetectedTokens/components/Token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const Token = ({ token, selected, toggleSelected }: Props) => {
(tokenExchangeRates as Record<Hex, MarketDataDetails>)?.[address as Hex] ??
null;
const tokenBalance = renderFromTokenMinimalUnit(
tokenBalances[address],
tokenBalances[address as Hex],
decimals,
);
const tokenBalanceWithSymbol = `${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ const mockInitialState: DeepPartial<RootState> = {
},
},
TokenBalancesController: {
contractBalances: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
},
tokenBalances: { },
},
PreferencesController: {
selectedAddress: '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A',
Expand Down
2 changes: 1 addition & 1 deletion app/components/Views/confirmations/Send/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const initialState: DeepPartial<RootState> = {
addressBook: {},
},
TokenBalancesController: {
contractBalances: {},
tokenBalances: {},
},
TokenListController: {
tokenList: { '0x1': {} },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ const initialState: DeepPartial<RootState> = {
},
},
TokenBalancesController: {
contractBalances: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
},
tokenBalances: { },
},
CurrencyRateController: {
currentCurrency: 'usd',
Expand Down
27 changes: 27 additions & 0 deletions app/components/hooks/AssetPolling/AssetPollingProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render } from '@testing-library/react-native';

import { AssetPollingProvider } from './AssetPollingProvider';

jest.mock('./useCurrencyRatePolling', () => jest.fn());
jest.mock('./useTokenRatesPolling', () => jest.fn());
jest.mock('./useTokenDetectionPolling', () => jest.fn());
jest.mock('./useTokenListPolling', () => jest.fn());
jest.mock('./useTokenBalancesPolling', () => jest.fn());

describe('AssetPollingProvider', () => {
it('should call all polling hooks', () => {

Check warning on line 14 in app/components/hooks/AssetPolling/AssetPollingProvider.test.tsx

View workflow job for this annotation

GitHub Actions / scripts (lint)

Trailing spaces not allowed

Check warning on line 14 in app/components/hooks/AssetPolling/AssetPollingProvider.test.tsx

View workflow job for this annotation

GitHub Actions / scripts (lint)

Trailing spaces not allowed
render(
<AssetPollingProvider>
<div></div>
</AssetPollingProvider>
);

expect(jest.requireMock('./useCurrencyRatePolling')).toHaveBeenCalled();
expect(jest.requireMock('./useTokenRatesPolling')).toHaveBeenCalled();
expect(jest.requireMock('./useTokenDetectionPolling')).toHaveBeenCalled();
expect(jest.requireMock('./useTokenListPolling')).toHaveBeenCalled();
expect(jest.requireMock('./useTokenBalancesPolling')).toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions app/components/hooks/AssetPolling/AssetPollingProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import useCurrencyRatePolling from './useCurrencyRatePolling';
import useTokenRatesPolling from './useTokenRatesPolling';
import useTokenDetectionPolling from './useTokenDetectionPolling';
import useTokenListPolling from './useTokenListPolling';
import useTokenBalancesPolling from './useTokenBalancesPolling';

// This provider is a step towards making controller polling fully UI based.
// Eventually, individual UI components will call the use*Polling hooks to
Expand All @@ -12,6 +13,7 @@ export const AssetPollingProvider = ({ children }: { children: ReactNode }) => {
useTokenRatesPolling();
useTokenDetectionPolling();
useTokenListPolling();
useTokenBalancesPolling();

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

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

describe('useTokenBalancesPolling', () => {

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

const selectedChainId = '0x1' as const;
const state = {
engine: {
backgroundState: {
TokenBalancesController: {
tokenBalances: {},
},
NetworkController: {
selectedNetworkClientId: 'selectedNetworkClientId',
networkConfigurationsByChainId: {
[selectedChainId]: {
chainId: selectedChainId,
rpcEndpoints: [{
networkClientId: 'selectedNetworkClientId',
}]
},
'0x89': {},
},
},
},
},
};

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

const { unmount } = renderHookWithProvider(() => useTokenBalancesPolling(), {state});

const mockedTokenBalancesController = jest.mocked(Engine.context.TokenBalancesController);

expect(mockedTokenBalancesController.startPolling).toHaveBeenCalledTimes(1);
expect(
mockedTokenBalancesController.startPolling
).toHaveBeenCalledWith({chainId: selectedChainId});

expect(mockedTokenBalancesController.stopPollingByPollingToken).toHaveBeenCalledTimes(0);
unmount();
expect(mockedTokenBalancesController.stopPollingByPollingToken).toHaveBeenCalledTimes(1);
});
});
37 changes: 37 additions & 0 deletions app/components/hooks/AssetPolling/useTokenBalancesPolling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 { isPortfolioViewEnabled } from '../../../util/networks';
import { selectAllTokenBalances } from '../../../selectors/tokenBalancesController';

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

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

// Selectors returning state updated by the polling
const tokenBalances = useSelector(selectAllTokenBalances);

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

const { TokenBalancesController } = Engine.context;

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

return {
tokenBalances
};
};

export default useTokenBalancesPolling;
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ const mockInitialState = {
},
},
TokenBalancesController: {
contractBalances: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
tokenBalances: {
[MOCK_ADDRESS_1]: {
'0x1': {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
},
},
},
},
PreferencesController: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,31 @@ import { BN } from 'ethereumjs-util';
import { cloneDeep } from 'lodash';
import { backgroundState } from '../../../util/test/initial-root-state';

const accountAddress = '0x123';
const chainId = '0x1';

// initial state for the test store
const mockInitialState = {
engine: {
backgroundState: {
...backgroundState,
AccountsController: {
internalAccounts: {
selectedAccount: '1',
accounts: {
'1': {
address: accountAddress,
},
},
},
},
TokenBalancesController: {
contractBalances: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': new BN(0x2a),
tokenBalances: {
[accountAddress]: {
[chainId]: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': new BN(0x2a),
}
}
},
},
},
Expand All @@ -35,10 +52,20 @@ const testBalancesReducer = (state: any, action: any) => {
...state.engine.backgroundState,
TokenBalancesController: {
...state.engine.backgroundState.TokenBalancesController,
contractBalances: {
tokenBalances: {
...state.engine.backgroundState.TokenBalancesController
.contractBalances,
.tokenBalances,
...action.value,
[accountAddress]: {
...state.engine.backgroundState.TokenBalancesController
.tokenBalances[accountAddress],
...action.value[accountAddress],
[chainId]: {
...state.engine.backgroundState.TokenBalancesController
.tokenBalances[accountAddress][chainId],
...action.value[accountAddress][chainId],
}
},
},
},
},
Expand Down Expand Up @@ -104,7 +131,11 @@ describe('useTokenBalancesController()', () => {
testStore.dispatch({
type: 'add-balances',
value: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b96': new BN(0x539),
[accountAddress]: {
[chainId]: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b96': new BN(0x539),
}
}
},
});
});
Expand All @@ -122,7 +153,11 @@ describe('useTokenBalancesController()', () => {
testStore.dispatch({
type: 'add-balances',
value: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': new BN(0x2a),
[accountAddress]: {
[chainId]: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': new BN(0x2a),
}
}
},
});
});
Expand Down
Loading

0 comments on commit 16a6d6e

Please sign in to comment.