Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/assets-controllers/src/TokensController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
NetworkState,
} from '@metamask/network-controller';
import { getDefaultNetworkControllerState } from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';
import type { Patch } from 'immer';
import nock from 'nock';
import { v1 as uuidV1 } from 'uuid';
Expand Down Expand Up @@ -4324,6 +4325,73 @@ describe('TokensController', () => {
});
});

describe('when NetworkController has not configured every supported chain', () => {
it('only seeds mUSD on chains currently configured in NetworkController', async () => {
const account = createMockInternalAccount({
address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
});

await withController(
// Only mainnet is configured in NetworkController
{ listAccounts: [account], configuredChainIds: ['0x1'] },
({ controller }) => {
expect(
controller.state.allTokens['0x1'][account.address],
).toContainEqual(MUSD_TOKEN);
expect(controller.state.allTokens['0xe708']).toBeUndefined();
expect(controller.state.allTokens['0x8f']).toBeUndefined();
},
);
});

it('does not seed mUSD when none of the supported chains are configured', async () => {
const account = createMockInternalAccount({
address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
});

await withController(
{ listAccounts: [account], configuredChainIds: [] },
({ controller }) => {
expect(controller.state.allTokens).toStrictEqual({});
},
);
});

it('does not seed mUSD when NetworkController state lacks networkConfigurationsByChainId', async () => {
const account = createMockInternalAccount({
address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
});

await withController(
{ listAccounts: [account], configuredChainIds: null },
({ controller }) => {
expect(controller.state.allTokens).toStrictEqual({});
},
);
});

it('does not seed a supported chainId fired via networkAdded when NetworkController has not configured it yet', async () => {
const account = createMockInternalAccount({
address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
});

await withController(
// Only Linea is configured in NetworkController.
{ listAccounts: [account], configuredChainIds: ['0xe708'] },
({ controller, triggerNetworkAdded }) => {
expect(
controller.state.allTokens['0xe708'][account.address],
).toContainEqual(MUSD_TOKEN);

// Even though `0x1` is in MUSD_SUPPORTED_CHAIN_IDS, seeding
// respects the configured chain set reported by NetworkController.
triggerNetworkAdded('0x1');
expect(controller.state.allTokens['0x1']).toBeUndefined();
},
);
});
});

describe('when NetworkController:stateChange patch adds a supported chain', () => {
it('seeds mUSD for all accounts', async () => {
const account = createMockInternalAccount({
Expand Down Expand Up @@ -4422,6 +4490,16 @@ type WithControllerArgs<ReturnValue> =
>;
mocks?: WithControllerMockArgs;
listAccounts?: InternalAccount[];
/**
* The set of chain IDs to report as currently configured in
* `NetworkController` when the `NetworkController:getState` action is
* invoked. Defaults to all mUSD-supported chains so existing tests
* see seeding behave as if every chain is configured.
*
* Pass `null` to simulate `NetworkController:getState` returning a
* state object without a `networkConfigurationsByChainId` property.
*/
configuredChainIds?: Hex[] | null;
},
WithControllerCallback<ReturnValue>,
];
Expand All @@ -4448,6 +4526,7 @@ async function withController<ReturnValue>(
mockNetworkClientConfigurationsByNetworkClientId = {},
mocks = {} as WithControllerMockArgs,
listAccounts = [],
configuredChainIds = ['0x1', '0xe708', '0x8f'],
},
fn,
] = args.length === 2 ? args : [{}, args[0]];
Expand Down Expand Up @@ -4485,6 +4564,7 @@ async function withController<ReturnValue>(
actions: [
'ApprovalController:addRequest',
'NetworkController:getNetworkClientById',
'NetworkController:getState',
'AccountsController:getAccount',
'AccountsController:getSelectedAccount',
'AccountsController:listAccounts',
Expand Down Expand Up @@ -4523,6 +4603,37 @@ async function withController<ReturnValue>(
mockListAccounts,
);

const networkControllerStateMock: NetworkState =
configuredChainIds === null
? // Simulate a NetworkController state object that is missing the
// `networkConfigurationsByChainId` property altogether.
({
...getDefaultNetworkControllerState(),
networkConfigurationsByChainId:
undefined as unknown as NetworkState['networkConfigurationsByChainId'],
} as NetworkState)
: {
...getDefaultNetworkControllerState(),
networkConfigurationsByChainId:
configuredChainIds.reduce<
NetworkState['networkConfigurationsByChainId']
>((acc, chainId) => {
acc[chainId] = {
chainId,
blockExplorerUrls: [],
defaultRpcEndpointIndex: 0,
name: `Network ${chainId}`,
nativeCurrency: 'ETH',
rpcEndpoints: [],
} as never;
return acc;
}, {}),
};
messenger.registerActionHandler(
'NetworkController:getState',
() => networkControllerStateMock,
);

const controller = new TokensController({
chainId: ChainId.mainnet,
// The tests assume that this is set, but they shouldn't make that
Expand Down
44 changes: 43 additions & 1 deletion packages/assets-controllers/src/TokensController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { abiERC721 } from '@metamask/metamask-eth-abis';
import type {
NetworkClientId,
NetworkControllerGetNetworkClientByIdAction,
NetworkControllerGetStateAction,
NetworkControllerNetworkAddedEvent,
NetworkControllerNetworkDidChangeEvent,
NetworkControllerStateChangeEvent,
Expand Down Expand Up @@ -152,6 +153,7 @@ export type TokensControllerActions =
export type AllowedActions =
| ApprovalControllerAddRequestAction
| NetworkControllerGetNetworkClientByIdAction
| NetworkControllerGetStateAction
| AccountsControllerGetAccountAction
| AccountsControllerGetSelectedAccountAction
| AccountsControllerListAccountsAction;
Expand Down Expand Up @@ -1244,8 +1246,18 @@ export class TokensController extends BaseController<
return;
}

// Only seed for chains that are actually configured in NetworkController.
// The `NetworkController:networkAdded` subscriber re-runs seeding when a
// supported chain is added later, so this preserves correctness without
// emitting state changes for chainIds downstream subscribers (e.g.
// TokenRatesController) cannot resolve.
const configuredChainIds = this.#getConfiguredMusdChainIds();
if (configuredChainIds.length === 0) {
return;
}

this.update((state) => {
for (const chainId of MUSD_SUPPORTED_CHAIN_IDS) {
for (const chainId of configuredChainIds) {
state.allTokens[chainId] ??= {};
const accountTokens = state.allTokens[chainId][accountAddress] ?? [];
const alreadyPresent = accountTokens.some(
Expand All @@ -1261,6 +1273,36 @@ export class TokensController extends BaseController<
});
}

/**
* Returns the subset of `MUSD_SUPPORTED_CHAIN_IDS` that are currently
* configured in `NetworkController`. Returns an empty array if the
* NetworkController state is unavailable.
*
* @returns The subset of `MUSD_SUPPORTED_CHAIN_IDS` that are currently configured in `NetworkController`.
*/
#getConfiguredMusdChainIds(): Hex[] {
let networkConfigurationsByChainId:
| NetworkState['networkConfigurationsByChainId']
| undefined;
try {
({ networkConfigurationsByChainId } = this.messenger.call(
'NetworkController:getState',
));
} catch {
return [];
}
if (!networkConfigurationsByChainId) {
return [];
}
const configured: Hex[] = [];
for (const chainId of MUSD_SUPPORTED_CHAIN_IDS) {
if (networkConfigurationsByChainId[chainId]) {
configured.push(chainId);
}
}
return configured;
}

/**
* Seed mUSD for every existing EVM account via AccountsController.
* Called on KeyringController:unlock and on network/account events.
Expand Down
Loading