Skip to content

Commit

Permalink
feat: Integrate new TokenListController polling pattern [Core] (#4878)
Browse files Browse the repository at this point in the history
## Explanation

_TokenListController is currently responsible for maintaining a list of
all tokens per chain. This dataset is accessible via `tokenList` state
variable in metamask state. After this task is complete, this token list
will migrate it's polling pattern to leverage the base class, to execute
a single poll per chain, rather than on an interval for all chains_
___

Move `TokenListController` away from being scoped to a single polling
loop, to executing individual polling loops per chain, allowing it to be
more UI based in its controls.

The controller will accept `PollingInputs` from the extension from
various UI elements, and will be responsible for starting, stopping, and
deduping polls as needed. Deduping is baked into the inherited base
class `StaticIntervalPollingController`

Here is the corresponding [PR in
extension](MetaMask/metamask-extension#28198)
that consumes and integrates with this controller. There will also be an
additional mobile PR at some point to implement something similar.

## References

MetaMask/MetaMask-planning#3429

## Changelog

<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

### `@metamask/package-a`

- **<CATEGORY>**: Your change here
- **<CATEGORY>**: Your change here

### `@metamask/package-b`

- **<CATEGORY>**: Your change here
- **<CATEGORY>**: Your change here

## Checklist

- [ ] I've updated the test suite for new or updated code as appropriate
- [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [ ] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
- [ ] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
  • Loading branch information
gambinish authored Nov 5, 2024
1 parent 5f6b020 commit d664234
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 151 deletions.
106 changes: 13 additions & 93 deletions packages/assets-controllers/src/TokenListController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,18 +854,18 @@ describe('TokenListController', () => {
preventPollingOnNetworkRestart: false,
messenger,
interval: 100,
state: existingState,
});
await controller.start();
expect(controller.state.tokenList).toStrictEqual({});
expect(controller.state.tokenList).toStrictEqual(existingState.tokenList);
const pollingToken = controller.startPolling({ chainId: ChainId.mainnet });
await new Promise<void>((resolve) => setTimeout(() => resolve(), 150));
expect(controller.state.tokenList).toStrictEqual(
sampleSingleChainState.tokenList,
);

expect(controller.state.tokensChainsCache[toHex(1)].data).toStrictEqual(
sampleSingleChainState.tokensChainsCache[toHex(1)].data,
);
controller.destroy();
controller.stopPollingByPollingToken(pollingToken);
});

it('should update token list from cache before reaching the threshold time', async () => {
Expand Down Expand Up @@ -1116,45 +1116,6 @@ describe('TokenListController', () => {
tokensChainsCache: {},
preventPollingOnNetworkRestart: false,
});

// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await new Promise((resolve: any) => {
messenger.subscribe('TokenListController:stateChange', (_, patch) => {
const tokenListChanged = patch.find(
(p) => Object.keys(p.value.tokenList).length !== 0,
);
if (!tokenListChanged) {
return;
}

expect(controller.state.tokenList).toStrictEqual(
sampleTwoChainState.tokenList,
);

expect(
controller.state.tokensChainsCache[toHex(56)].data,
).toStrictEqual(sampleTwoChainState.tokensChainsCache[toHex(56)].data);
messenger.clearEventSubscriptions('TokenListController:stateChange');
controller.destroy();
controllerMessenger.clearEventSubscriptions(
'NetworkController:stateChange',
);
resolve();
});

controllerMessenger.publish(
'NetworkController:stateChange',
{
selectedNetworkClientId: selectedCustomNetworkClientId,
networkConfigurationsByChainId: {},
networksMetadata: {},
// @ts-expect-error This property isn't used and will get removed later.
providerConfig: {},
},
[],
);
});
});

describe('startPolling', () => {
Expand Down Expand Up @@ -1200,57 +1161,14 @@ describe('TokenListController', () => {
expiredCacheExistingState.tokenList,
);

controller.startPolling({ networkClientId: 'sepolia' });
controller.startPolling({ chainId: ChainId.sepolia });
await advanceTime({ clock, duration: 0 });

expect(fetchTokenListByChainIdSpy.mock.calls[0]).toStrictEqual(
expect.arrayContaining([ChainId.sepolia]),
);
});

it('should start polling against the token list API at the interval passed to the constructor', async () => {
const fetchTokenListByChainIdSpy = jest.spyOn(
tokenService,
'fetchTokenListByChainId',
);

const controllerMessenger = getControllerMessenger();
controllerMessenger.registerActionHandler(
'NetworkController:getNetworkClientById',
jest.fn().mockReturnValue({
configuration: {
type: NetworkType.goerli,
chainId: ChainId.goerli,
},
}),
);
const messenger = getRestrictedMessenger(controllerMessenger);
const controller = new TokenListController({
chainId: ChainId.mainnet,
preventPollingOnNetworkRestart: false,
messenger,
state: expiredCacheExistingState,
interval: pollingIntervalTime,
});
expect(controller.state.tokenList).toStrictEqual(
expiredCacheExistingState.tokenList,
);

controller.startPolling({ networkClientId: 'goerli' });
await advanceTime({ clock, duration: 0 });

expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1);
await advanceTime({ clock, duration: pollingIntervalTime / 2 });

expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1);
await advanceTime({ clock, duration: pollingIntervalTime / 2 });

expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(2);
await advanceTime({ clock, duration: pollingIntervalTime });

expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(3);
});

it('should update tokenList state and tokensChainsCache', async () => {
const startingState: TokenListState = {
tokenList: {},
Expand All @@ -1270,6 +1188,7 @@ describe('TokenListController', () => {
throw new Error('Invalid chainId');
}
});

const controllerMessenger = getControllerMessenger();
controllerMessenger.registerActionHandler(
'NetworkController:getNetworkClientById',
Expand All @@ -1296,7 +1215,7 @@ describe('TokenListController', () => {
);
const messenger = getRestrictedMessenger(controllerMessenger);
const controller = new TokenListController({
chainId: ChainId.mainnet,
chainId: ChainId.sepolia,
preventPollingOnNetworkRestart: false,
messenger,
state: startingState,
Expand All @@ -1307,13 +1226,14 @@ describe('TokenListController', () => {

// start polling for sepolia
const pollingToken = controller.startPolling({
networkClientId: 'sepolia',
chainId: ChainId.sepolia,
});

// wait a polling interval
await advanceTime({ clock, duration: pollingIntervalTime });

expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1);
// expect the state to be updated with the sepolia token list

expect(controller.state.tokenList).toStrictEqual(
sampleSepoliaTokensChainCache,
);
Expand All @@ -1327,18 +1247,18 @@ describe('TokenListController', () => {

// start polling for binance
controller.startPolling({
networkClientId: 'binance-network-client-id',
chainId: '0x38',
});
await advanceTime({ clock, duration: pollingIntervalTime });

// expect fetchTokenListByChain to be called for binance, but not for sepolia
// because the cache for the recently fetched sepolia token list is still valid
expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(2);

// expect tokenList to be updated with the binance token list
// expect tokenList to be not be updated with the binance token list, because sepolia is still this.chainId
// and the cache to now contain both the binance token list and the sepolia token list
expect(controller.state.tokenList).toStrictEqual(
sampleBinanceTokensChainsCache,
sampleSepoliaTokensChainCache,
);
// once we adopt this polling pattern we should no longer access the root tokenList state
// but rather access from the cache with a chainId selector.
Expand Down
Loading

0 comments on commit d664234

Please sign in to comment.