diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index 6c8479dc2b..7e4a5cffe6 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -5,10 +5,10 @@ import type { Provider } from '@metamask/eth-query'; import type { NetworkClientId, NetworkController, - NetworkState, } from '@metamask/network-controller'; import { StaticIntervalPollingControllerV1 } from '@metamask/polling-controller'; import type { PreferencesState } from '@metamask/preferences-controller'; +import type { Hex } from '@metamask/utils'; import { assert } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { cloneDeep } from 'lodash'; @@ -120,7 +120,7 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< private readonly getMultiAccountBalancesEnabled: () => PreferencesState['isMultiAccountBalancesEnabled']; - private readonly getCurrentChainId: () => NetworkState['providerConfig']['chainId']; + private readonly getCurrentChainId: () => Hex; private readonly getNetworkClientById: NetworkController['getNetworkClientById']; @@ -152,7 +152,7 @@ export class AccountTrackerController extends StaticIntervalPollingControllerV1< getIdentities: () => PreferencesState['identities']; getSelectedAddress: () => PreferencesState['selectedAddress']; getMultiAccountBalancesEnabled: () => PreferencesState['isMultiAccountBalancesEnabled']; - getCurrentChainId: () => NetworkState['providerConfig']['chainId']; + getCurrentChainId: () => Hex; getNetworkClientById: NetworkController['getNetworkClientById']; }, config?: Partial, diff --git a/packages/assets-controllers/src/AssetsContractController.ts b/packages/assets-controllers/src/AssetsContractController.ts index 71cad85390..2447c1e278 100644 --- a/packages/assets-controllers/src/AssetsContractController.ts +++ b/packages/assets-controllers/src/AssetsContractController.ts @@ -154,10 +154,15 @@ export class AssetsContractController extends BaseControllerV1< this.configure({ ipfsGateway }); }); - onNetworkDidChange((networkState) => { - if (this.config.chainId !== networkState.providerConfig.chainId) { + onNetworkDidChange(({ selectedNetworkClientId }) => { + const selectedNetworkClient = getNetworkClientById( + selectedNetworkClientId, + ); + const { chainId } = selectedNetworkClient.configuration; + + if (this.config.chainId !== chainId) { this.configure({ - chainId: networkState.providerConfig.chainId, + chainId: selectedNetworkClient.configuration.chainId, }); } }); diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index 4342f80143..80d3296a1f 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -1,7 +1,7 @@ import type { Network } from '@ethersproject/providers'; import type { - AddApprovalRequest, ApprovalStateChange, + GetApprovalsState, } from '@metamask/approval-controller'; import { ApprovalController } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; @@ -16,10 +16,12 @@ import { ERC20, NetworksTicker, NFT_API_BASE_URL, + InfuraNetworkType, } from '@metamask/controller-utils'; import type { + NetworkClientConfiguration, + NetworkClientId, NetworkState, - ProviderConfig, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; import { @@ -31,9 +33,17 @@ import nock from 'nock'; import * as sinon from 'sinon'; import { v4 } from 'uuid'; +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../base-controller/tests/helpers'; +import { + buildCustomNetworkClientConfiguration, + buildMockGetNetworkClientById, +} from '../../network-controller/tests/helpers'; import { getFormattedIpfsUrl } from './assetsUtil'; import { Source } from './constants'; -import type { Nft } from './NftController'; +import type { Nft, NftControllerMessenger } from './NftController'; import { NftController } from './NftController'; const CRYPTOPUNK_ADDRESS = '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB'; @@ -70,9 +80,6 @@ const GOERLI = { ticker: NetworksTicker.goerli, }; -type ApprovalActions = AddApprovalRequest; -type ApprovalEvents = ApprovalStateChange; - const controllerName = 'NftController' as const; // Mock out detectNetwork function for cleaner tests, Ethers calls this a bunch of times because the Web3Provider is paranoid. @@ -104,23 +111,49 @@ jest.mock('uuid', () => { /** * Setup a test controller instance. * - * @param options - Controller options. + * @param args - Arguments to this function. + * @param args.options - Controller options. + * @param args.mockNetworkClientConfigurationsByNetworkClientId - Used to construct + * mock versions of network clients and ultimately mock the + * `NetworkController:getNetworkClientById` action. * @returns A collection of test controllers and mocks. */ -function setupController( - options: Partial[0]> = {}, -) { +function setupController({ + options = {}, + mockNetworkClientConfigurationsByNetworkClientId = {}, +}: { + options?: Partial[0]>; + mockNetworkClientConfigurationsByNetworkClientId?: Record< + NetworkClientId, + NetworkClientConfiguration + >; +} = {}) { const onNetworkDidChangeListeners: ((state: NetworkState) => void)[] = []; - const changeNetwork = (providerConfig: ProviderConfig) => { + const changeNetwork = ({ + selectedNetworkClientId, + }: { + selectedNetworkClientId: NetworkClientId; + }) => { onNetworkDidChangeListeners.forEach((listener) => { listener({ ...defaultNetworkState, - providerConfig, + selectedNetworkClientId, }); }); }; - const messenger = new ControllerMessenger(); + const messenger = new ControllerMessenger< + ExtractAvailableAction | GetApprovalsState, + ExtractAvailableEvent | ApprovalStateChange + >(); + + const getNetworkClientById = buildMockGetNetworkClientById( + mockNetworkClientConfigurationsByNetworkClientId, + ); + messenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const approvalControllerMessenger = messenger.getRestricted({ name: 'ApprovalController', @@ -133,39 +166,12 @@ function setupController( showApprovalRequest: jest.fn(), }); - const mockGetNetworkClientById = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return { - configuration: { - chainId: SEPOLIA.chainId, - }, - }; - case 'goerli': - return { - configuration: { - chainId: GOERLI.chainId, - }, - }; - case 'customNetworkClientId-1': - return { - configuration: { - chainId: '0xa', - }, - }; - default: - throw new Error('Invalid network client id'); - } - }); - - const nftControllerMessenger = messenger.getRestricted< - typeof controllerName, - ApprovalActions['type'] - >({ + const nftControllerMessenger = messenger.getRestricted({ name: controllerName, - allowedActions: ['ApprovalController:addRequest'], + allowedActions: [ + 'ApprovalController:addRequest', + 'NetworkController:getNetworkClientById', + ], allowedEvents: [], }); @@ -184,7 +190,7 @@ function setupController( getERC721OwnerOf: jest.fn(), getERC1155BalanceOf: jest.fn(), getERC1155TokenURI: jest.fn(), - getNetworkClientById: mockGetNetworkClientById, + getNetworkClientById, onNftAdded: jest.fn(), messenger: nftControllerMessenger, ...options, @@ -316,10 +322,12 @@ describe('NftController', () => { }), ); const { nftController } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -389,7 +397,9 @@ describe('NftController', () => { it('should error if the user does not own the suggested ERC721 NFT', async function () { const { nftController, messenger } = setupController({ - getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), + options: { + getERC721OwnerOf: jest.fn().mockImplementation(() => '0x12345abcefg'), + }, }); const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); @@ -417,7 +427,9 @@ describe('NftController', () => { it('should error if the user does not own the suggested ERC1155 NFT', async function () { const { nftController, messenger } = setupController({ - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), + options: { + getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(0)), + }, }); const callActionSpy = jest.spyOn(messenger, 'call').mockResolvedValue({}); @@ -441,10 +453,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -500,10 +514,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -559,10 +575,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'ipfs://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'ipfs://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -618,10 +636,12 @@ describe('NftController', () => { ); const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'ipfs://testtokenuri.com'), - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + options: { + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'ipfs://testtokenuri.com'), + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -678,13 +698,15 @@ describe('NftController', () => { const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), + options: { + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -744,13 +766,15 @@ describe('NftController', () => { const { nftController, messenger, triggerPreferencesStateChange } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), + options: { + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC1155BalanceOf: jest.fn().mockImplementation(() => new BN(1)), + }, }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), @@ -815,18 +839,20 @@ describe('NftController', () => { changeNetwork, triggerPreferencesStateChange, } = setupController({ - getERC721OwnerOf: jest - .fn() - .mockImplementation(() => SECOND_OWNER_ADDRESS), - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721AssetName: jest - .fn() - .mockImplementation(() => 'testERC721Name'), - getERC721AssetSymbol: jest - .fn() - .mockImplementation(() => 'testERC721Symbol'), + options: { + getERC721OwnerOf: jest + .fn() + .mockImplementation(() => SECOND_OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721AssetName: jest + .fn() + .mockImplementation(() => 'testERC721Name'), + getERC721AssetSymbol: jest + .fn() + .mockImplementation(() => 'testERC721Symbol'), + }, }); const requestId = 'approval-request-id-1'; @@ -858,7 +884,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); nftController.watchNft(ERC721_NFT, ERC721, 'https://etherscan.io', { userAddress: SECOND_OWNER_ADDRESS, @@ -911,16 +937,18 @@ describe('NftController', () => { triggerPreferencesStateChange, changeNetwork, } = setupController({ - getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), - getERC721TokenURI: jest - .fn() - .mockImplementation(() => 'https://testtokenuri.com'), - getERC721AssetName: jest - .fn() - .mockImplementation(() => 'testERC721Name'), - getERC721AssetSymbol: jest - .fn() - .mockImplementation(() => 'testERC721Symbol'), + options: { + getERC721OwnerOf: jest.fn().mockImplementation(() => OWNER_ADDRESS), + getERC721TokenURI: jest + .fn() + .mockImplementation(() => 'https://testtokenuri.com'), + getERC721AssetName: jest + .fn() + .mockImplementation(() => 'testERC721Name'), + getERC721AssetSymbol: jest + .fn() + .mockImplementation(() => 'testERC721Symbol'), + }, }); const requestId = 'approval-request-id-1'; @@ -965,7 +993,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: '0xDifferentAddress', }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); // now accept the request approvalController.accept(requestId); await acceptedRequest; @@ -1000,7 +1028,7 @@ describe('NftController', () => { // getERC721OwnerOf not mocked // getERC1155BalanceOf not mocked - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const requestId = 'approval-request-id-1'; (v4 as jest.Mock).mockImplementationOnce(() => requestId); @@ -1019,7 +1047,9 @@ describe('NftController', () => { describe('addNft', () => { it('should add NFT and NFT contract', async () => { const { nftController } = setupController({ - getERC721AssetName: jest.fn().mockResolvedValue('Name'), + options: { + getERC721AssetName: jest.fn().mockResolvedValue('Name'), + }, }); const { selectedAddress, chainId } = nftController.config; @@ -1068,7 +1098,9 @@ describe('NftController', () => { it('should call onNftAdded callback correctly when NFT is manually added', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); await nftController.addNft('0x01', '1', { @@ -1092,7 +1124,9 @@ describe('NftController', () => { it('should call onNftAdded callback correctly when NFT is added via detection', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); const detectedUserAddress = '0x123'; @@ -1243,12 +1277,14 @@ describe('NftController', () => { it('should add NFT and get information from NFT-API', async () => { const { nftController } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC721 contract')), - getERC1155TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC1155 contract')), + options: { + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC721 contract')), + getERC1155TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC1155 contract')), + }, }); const { selectedAddress, chainId } = nftController.config; @@ -1272,13 +1308,15 @@ describe('NftController', () => { it('should add NFT erc721 and aggregate NFT data from both contract and NFT-API', async () => { const { nftController } = setupController({ - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest - .fn() - .mockResolvedValue( - 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', - ), + options: { + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest + .fn() + .mockResolvedValue( + 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov', + ), + }, }); nock(NFT_API_BASE_URL) .get( @@ -1336,14 +1374,16 @@ describe('NftController', () => { it('should add NFT erc1155 and get NFT information from contract when NFT API call fail', async () => { const { nftController } = setupController({ - getERC721TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not a 721 contract')), - getERC1155TokenURI: jest - .fn() - .mockResolvedValue( - 'https://api.opensea.io/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/0x{id}', - ), + options: { + getERC721TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not a 721 contract')), + getERC1155TokenURI: jest + .fn() + .mockResolvedValue( + 'https://api.opensea.io/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/0x{id}', + ), + }, }); nock('https://api.opensea.io') .get( @@ -1377,16 +1417,18 @@ describe('NftController', () => { it('should add NFT erc721 and get NFT information only from contract', async () => { const { nftController } = setupController({ - getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), - getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case ERC721_KUDOSADDRESS: - return 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov'; - default: - throw new Error('Not an ERC721 token'); - } - }), + options: { + getERC721AssetName: jest.fn().mockResolvedValue('KudosToken'), + getERC721AssetSymbol: jest.fn().mockResolvedValue('KDO'), + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case ERC721_KUDOSADDRESS: + return 'https://ipfs.gitcoin.co:443/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov'; + default: + throw new Error('Not an ERC721 token'); + } + }), + }, }); nock('https://ipfs.gitcoin.co:443') .get('/api/v0/cat/QmPmt6EAaioN78ECnW5oCL8v2YvVSpoBjLCjrXhhsAvoov') @@ -1438,10 +1480,10 @@ describe('NftController', () => { .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNft('0x01', '1234'); - changeNetwork(GOERLI); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect( nftController.state.allNfts[selectedAddress]?.[ChainId[GOERLI.type]], @@ -1463,7 +1505,9 @@ describe('NftController', () => { it('should add an nft and nftContract to state when all contract information is falsy and the source is left empty (defaults to "custom")', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); const { selectedAddress, chainId } = nftController.config; // TODO: Replace `any` with type @@ -1527,9 +1571,11 @@ describe('NftController', () => { it('should add an nft and nftContract to state when all contract information is falsy and the source is "dapp"', async () => { const mockOnNftAdded = jest.fn(); const { nftController, changeNetwork } = setupController({ - onNftAdded: mockOnNftAdded, + options: { + onNftAdded: mockOnNftAdded, + }, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1595,13 +1641,15 @@ describe('NftController', () => { it('should add an nft and nftContract when there is valid contract information and source is "detected"', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), + options: { + onNftAdded: mockOnNftAdded, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + }, }); nock(NFT_API_BASE_URL) .get( @@ -1707,13 +1755,15 @@ describe('NftController', () => { it('should not add an nft and nftContract when there is not valid contract information (or an issue fetching it) and source is "detected"', async () => { const mockOnNftAdded = jest.fn(); const { nftController } = setupController({ - onNftAdded: mockOnNftAdded, - getERC721AssetName: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - getERC721AssetSymbol: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), + options: { + onNftAdded: mockOnNftAdded, + getERC721AssetName: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + getERC721AssetSymbol: jest + .fn() + .mockRejectedValue(new Error('Failed to fetch')), + }, }); nock(NFT_API_BASE_URL) .get( @@ -1797,21 +1847,23 @@ describe('NftController', () => { it('should add NFT with metadata hosted in IPFS', async () => { const { nftController } = setupController({ - getERC721AssetName: jest - .fn() - .mockResolvedValue("Maltjik.jpg's Depressionists"), - getERC721AssetSymbol: jest.fn().mockResolvedValue('DPNS'), - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case ERC721_DEPRESSIONIST_ADDRESS: - return `ipfs://${DEPRESSIONIST_CID_V1}`; - default: - throw new Error('Not an ERC721 token'); - } - }), - getERC1155TokenURI: jest - .fn() - .mockRejectedValue(new Error('Not an ERC1155 token')), + options: { + getERC721AssetName: jest + .fn() + .mockResolvedValue("Maltjik.jpg's Depressionists"), + getERC721AssetSymbol: jest.fn().mockResolvedValue('DPNS'), + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case ERC721_DEPRESSIONIST_ADDRESS: + return `ipfs://${DEPRESSIONIST_CID_V1}`; + default: + throw new Error('Not an ERC721 token'); + } + }), + getERC1155TokenURI: jest + .fn() + .mockRejectedValue(new Error('Not an ERC1155 token')), + }, }); nftController.configure({ ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, @@ -1909,24 +1961,31 @@ describe('NftController', () => { ); const { nftController } = setupController({ - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x01': - return 'https://testtokenuri-1.com'; - case '0x02': - return 'https://testtokenuri-2.com'; - default: - throw new Error('Not an ERC721 token'); - } - }), - getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x03': - return 'https://testtokenuri-3.com'; - default: - throw new Error('Not an ERC1155 token'); - } - }), + options: { + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x01': + return 'https://testtokenuri-1.com'; + case '0x02': + return 'https://testtokenuri-2.com'; + default: + throw new Error('Not an ERC721 token'); + } + }), + getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x03': + return 'https://testtokenuri-3.com'; + default: + throw new Error('Not an ERC1155 token'); + } + }), + }, + mockNetworkClientConfigurationsByNetworkClientId: { + 'customNetworkClientId-1': buildCustomNetworkClientConfiguration({ + chainId: '0xa', + }), + }, }); await nftController.addNft('0x01', '1234', { @@ -2022,36 +2081,38 @@ describe('NftController', () => { ); const { nftController, changeNetwork } = setupController({ - getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x01': - return 'https://testtokenuri-1.com'; - case '0x02': - return 'https://testtokenuri-2.com'; - default: - throw new Error('Not an ERC721 token'); - } - }), - getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { - switch (tokenAddress) { - case '0x03': - return 'https://testtokenuri-3.com'; - default: - throw new Error('Not an ERC1155 token'); - } - }), + options: { + getERC721TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x01': + return 'https://testtokenuri-1.com'; + case '0x02': + return 'https://testtokenuri-2.com'; + default: + throw new Error('Not an ERC721 token'); + } + }), + getERC1155TokenURI: jest.fn().mockImplementation((tokenAddress) => { + switch (tokenAddress) { + case '0x03': + return 'https://testtokenuri-3.com'; + default: + throw new Error('Not an ERC1155 token'); + } + }), + }, }); await nftController.addNft('0x01', '1234', { userAddress, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.addNft('0x02', '4321', { userAddress, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNft('0x03', '5678', { userAddress, @@ -2247,11 +2308,11 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNftVerifyOwnership('0x01', '1234', { userAddress: firstAddress, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.addNftVerifyOwnership('0x02', '4321', { userAddress: secondAddress, }); @@ -2389,16 +2450,16 @@ describe('NftController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .stub(nftController, 'getNftInformation' as any) .returns({ name: 'name', image: 'url', description: 'description' }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await nftController.addNft('0x02', '4321'); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.addNft('0x01', '1234'); nftController.removeNft('0x01', '1234'); expect( nftController.state.allNfts[selectedAddress][GOERLI.chainId], ).toHaveLength(0); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect( nftController.state.allNfts[selectedAddress][SEPOLIA.chainId][0], @@ -2420,7 +2481,7 @@ describe('NftController', () => { const userAddress1 = '0x123'; const userAddress2 = '0x321'; - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -2449,7 +2510,7 @@ describe('NftController', () => { isCurrentlyOwned: true, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -2508,8 +2569,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2527,8 +2590,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2545,8 +2610,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2563,8 +2630,10 @@ describe('NftController', () => { .mockRejectedValue(new Error('ERC721 error')); const mockGetERC1155BalanceOf = jest.fn().mockResolvedValue(new BN(1)); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2581,8 +2650,10 @@ describe('NftController', () => { .mockRejectedValue(new Error('ERC721 error')); const mockGetERC1155BalanceOf = jest.fn().mockResolvedValue(new BN(0)); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const isOwner = await nftController.isNftOwner( @@ -2602,8 +2673,10 @@ describe('NftController', () => { .fn() .mockRejectedValue(new Error('ERC1155 error')); const { nftController } = setupController({ - getERC721OwnerOf: mockGetERC721OwnerOf, - getERC1155BalanceOf: mockGetERC1155BalanceOf, + options: { + getERC721OwnerOf: mockGetERC721OwnerOf, + getERC1155BalanceOf: mockGetERC1155BalanceOf, + }, }); const error = "Unable to verify ownership. Possibly because the standard is not supported or the user's currently selected network does not match the chain of the asset in question."; @@ -2862,7 +2935,7 @@ describe('NftController', () => { const userAddress1 = '0x123'; const userAddress2 = '0x321'; - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -2885,7 +2958,7 @@ describe('NftController', () => { }), ); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), openSeaEnabled: true, @@ -3015,7 +3088,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const { selectedAddress, chainId } = nftController.config; await nftController.addNft('0x02', '1', { @@ -3042,7 +3115,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: SECOND_OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.checkAndUpdateAllNftsOwnershipStatus({ userAddress: OWNER_ADDRESS, @@ -3137,7 +3210,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const { selectedAddress, chainId } = nftController.config; const nft = { @@ -3168,7 +3241,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: SECOND_OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await nftController.checkAndUpdateSingleNftOwnershipStatus(nft, false, { userAddress: OWNER_ADDRESS, @@ -3190,7 +3263,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: OWNER_ADDRESS, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const { selectedAddress, chainId } = nftController.config; const nft = { @@ -3221,7 +3294,7 @@ describe('NftController', () => { openSeaEnabled: true, selectedAddress: SECOND_OWNER_ADDRESS, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); const updatedNft = await nftController.checkAndUpdateSingleNftOwnershipStatus( @@ -3662,7 +3735,7 @@ describe('NftController', () => { it('should trigger calling updateNftMetadata when preferences change - openseaEnabled', async () => { const { nftController, triggerPreferencesStateChange, changeNetwork } = setupController(); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const spy = jest.spyOn(nftController, 'updateNftMetadata'); const testNetworkClientId = 'sepolia'; @@ -3705,7 +3778,7 @@ describe('NftController', () => { it('should trigger calling updateNftMetadata when preferences change - ipfs enabled', async () => { const { nftController, triggerPreferencesStateChange, changeNetwork } = setupController(); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); const spy = jest.spyOn(nftController, 'updateNftMetadata'); const testNetworkClientId = 'sepolia'; diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 9061b8f180..16e8fb5de3 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -21,6 +21,7 @@ import { import type { NetworkClientId, NetworkController, + NetworkControllerGetNetworkClientByIdAction, NetworkState, } from '@metamask/network-controller'; import type { PreferencesState } from '@metamask/preferences-controller'; @@ -221,7 +222,9 @@ const controllerName = 'NftController'; /** * The external actions available to the {@link NftController}. */ -type AllowedActions = AddApprovalRequest; +type AllowedActions = + | AddApprovalRequest + | NetworkControllerGetNetworkClientByIdAction; /** * The messenger of the {@link NftController}. @@ -1075,8 +1078,12 @@ export class NftController extends BaseControllerV1 { }, ); - onNetworkStateChange(({ providerConfig }) => { - const { chainId } = providerConfig; + onNetworkStateChange(({ selectedNetworkClientId }) => { + const selectedNetworkClient = getNetworkClientById( + selectedNetworkClientId, + ); + const { chainId } = selectedNetworkClient.configuration; + this.configure({ chainId }); }); } diff --git a/packages/assets-controllers/src/NftDetectionController.ts b/packages/assets-controllers/src/NftDetectionController.ts index 488d468d86..e47251fbd2 100644 --- a/packages/assets-controllers/src/NftDetectionController.ts +++ b/packages/assets-controllers/src/NftDetectionController.ts @@ -494,10 +494,13 @@ export class NftDetectionController extends StaticIntervalPollingControllerV1< } }); - onNetworkStateChange(({ providerConfig }) => { - this.configure({ - chainId: providerConfig.chainId, - }); + onNetworkStateChange(({ selectedNetworkClientId }) => { + const selectedNetworkClient = getNetworkClientById( + selectedNetworkClientId, + ); + const { chainId } = selectedNetworkClient.configuration; + + this.configure({ chainId }); }); this.getOpenSeaApiKey = getOpenSeaApiKey; this.addNft = addNft; diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index a325c1421d..8fd210168e 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -2,28 +2,30 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { ChainId, NetworkType, - NetworksTicker, convertHexToDecimal, toHex, + InfuraNetworkType, } from '@metamask/controller-utils'; -import type { - NetworkControllerGetNetworkClientByIdAction, - NetworkControllerStateChangeEvent, - NetworkState, - ProviderConfig, -} from '@metamask/network-controller'; -import { NetworkStatus } from '@metamask/network-controller'; +import type { NetworkState } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import nock from 'nock'; import * as sinon from 'sinon'; import { advanceTime } from '../../../tests/helpers'; +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../base-controller/tests/helpers'; +import { + buildCustomNetworkClientConfiguration, + buildInfuraNetworkClientConfiguration, + buildMockGetNetworkClientById, +} from '../../network-controller/tests/helpers'; import * as tokenService from './token-service'; import type { - TokenListStateChange, - GetTokenListState, TokenListMap, TokenListState, + TokenListControllerMessenger, } from './TokenListController'; import { TokenListController } from './TokenListController'; @@ -493,8 +495,8 @@ const expiredCacheExistingState: TokenListState = { }; type MainControllerMessenger = ControllerMessenger< - GetTokenListState | NetworkControllerGetNetworkClientByIdAction, - TokenListStateChange | NetworkControllerStateChangeEvent + ExtractAvailableAction, + ExtractAvailableEvent >; const getControllerMessenger = (): MainControllerMessenger => { @@ -513,31 +515,6 @@ const getRestrictedMessenger = ( return messenger; }; -/** - * Builds an object that satisfies the NetworkState shape using the given - * provider config. This can be used to return a complete value for the - * `NetworkController:stateChange` event. - * - * @param providerConfig - The provider config to use. - * @returns A complete state object for NetworkController. - */ -function buildNetworkControllerStateWithProviderConfig( - providerConfig: ProviderConfig, -): NetworkState { - const selectedNetworkClientId = providerConfig.type || 'uuid-1'; - return { - selectedNetworkClientId, - providerConfig, - networksMetadata: { - [selectedNetworkClientId]: { - EIPS: {}, - status: NetworkStatus.Available, - }, - }, - networkConfigurations: {}, - }; -} - describe('TokenListController', () => { afterEach(() => { jest.restoreAllMocks(); @@ -653,8 +630,17 @@ describe('TokenListController', () => { .get(getTokensPath(ChainId.mainnet)) .reply(200, sampleMainnetTokenList) .persist(); - + const selectedNetworkClientId = 'selectedNetworkClientId'; const controllerMessenger = getControllerMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById({ + [selectedNetworkClientId]: buildCustomNetworkClientConfiguration({ + chainId: toHex(1337), + }), + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const messenger = getRestrictedMessenger(controllerMessenger); let onNetworkStateChangeCallback!: (state: NetworkState) => void; const controller = new TokenListController({ @@ -669,13 +655,11 @@ describe('TokenListController', () => { expect(controller.state.tokenList).toStrictEqual( sampleSingleChainState.tokenList, ); - onNetworkStateChangeCallback( - buildNetworkControllerStateWithProviderConfig({ - chainId: ChainId.goerli, - type: NetworkType.goerli, - ticker: NetworksTicker.goerli, - }), - ); + onNetworkStateChangeCallback({ + selectedNetworkClientId, + networkConfigurations: {}, + networksMetadata: {}, + }); await new Promise((resolve) => setTimeout(() => resolve(), 500)); expect(controller.state.tokenList).toStrictEqual({}); @@ -971,8 +955,20 @@ describe('TokenListController', () => { .get(getTokensPath(toHex(56))) .reply(200, sampleBinanceTokenList) .persist(); - + const selectedCustomNetworkClientId = 'selectedCustomNetworkClientId'; const controllerMessenger = getControllerMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById({ + [InfuraNetworkType.goerli]: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), + [selectedCustomNetworkClientId]: buildCustomNetworkClientConfiguration({ + chainId: toHex(56), + }), + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const messenger = getRestrictedMessenger(controllerMessenger); const controller = new TokenListController({ chainId: ChainId.mainnet, @@ -995,11 +991,11 @@ describe('TokenListController', () => { controllerMessenger.publish( 'NetworkController:stateChange', - buildNetworkControllerStateWithProviderConfig({ - type: NetworkType.goerli, - chainId: ChainId.goerli, - ticker: NetworksTicker.goerli, - }), + { + selectedNetworkClientId: InfuraNetworkType.goerli, + networkConfigurations: {}, + networksMetadata: {}, + }, [], ); @@ -1014,12 +1010,11 @@ describe('TokenListController', () => { controllerMessenger.publish( 'NetworkController:stateChange', - buildNetworkControllerStateWithProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(56), - rpcUrl: 'http://localhost:8545', - ticker: 'TEST', - }), + { + selectedNetworkClientId: selectedCustomNetworkClientId, + networkConfigurations: {}, + networksMetadata: {}, + }, [], ); @@ -1069,7 +1064,20 @@ describe('TokenListController', () => { .reply(200, sampleBinanceTokenList) .persist(); + const selectedCustomNetworkClientId = 'selectedCustomNetworkClientId'; const controllerMessenger = getControllerMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById({ + [InfuraNetworkType.mainnet]: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + [selectedCustomNetworkClientId]: buildCustomNetworkClientConfiguration({ + chainId: toHex(56), + }), + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const messenger = getRestrictedMessenger(controllerMessenger); const controller = new TokenListController({ chainId: ChainId.goerli, @@ -1080,11 +1088,11 @@ describe('TokenListController', () => { await controller.start(); controllerMessenger.publish( 'NetworkController:stateChange', - buildNetworkControllerStateWithProviderConfig({ - type: NetworkType.mainnet, - chainId: ChainId.mainnet, - ticker: NetworksTicker.mainnet, - }), + { + selectedNetworkClientId: InfuraNetworkType.mainnet, + networkConfigurations: {}, + networksMetadata: {}, + }, [], ); @@ -1128,12 +1136,11 @@ describe('TokenListController', () => { controllerMessenger.publish( 'NetworkController:stateChange', - buildNetworkControllerStateWithProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(56), - rpcUrl: 'http://localhost:8545', - ticker: 'TEST', - }), + { + selectedNetworkClientId: selectedCustomNetworkClientId, + networkConfigurations: {}, + networksMetadata: {}, + }, [], ); }); diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index c83c1753e2..ce88c4b543 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -175,10 +175,16 @@ export class TokenListController extends StaticIntervalPollingController< * @param networkControllerState - The updated network controller state. */ async #onNetworkControllerStateChange(networkControllerState: NetworkState) { - if (this.chainId !== networkControllerState.providerConfig.chainId) { + const selectedNetworkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkControllerState.selectedNetworkClientId, + ); + const { chainId } = selectedNetworkClient.configuration; + + if (this.chainId !== chainId) { this.abortController.abort(); this.abortController = new AbortController(); - this.chainId = networkControllerState.providerConfig.chainId; + this.chainId = chainId; if (this.state.preventPollingOnNetworkRestart) { this.clearingTokenListData(); } else { diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index a82a195ff7..d5d0ae8701 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,16 +1,28 @@ import { + ChainId, + InfuraNetworkType, NetworksTicker, toChecksumHexAddress, toHex, } from '@metamask/controller-utils'; -import type { NetworkState } from '@metamask/network-controller'; +import type { + NetworkClientConfiguration, + NetworkClientId, + NetworkState, +} from '@metamask/network-controller'; +import { defaultState as defaultNetworkState } from '@metamask/network-controller'; import type { PreferencesState } from '@metamask/preferences-controller'; import type { Hex } from '@metamask/utils'; import { add0x } from '@metamask/utils'; +import assert from 'assert'; import nock from 'nock'; import { useFakeTimers } from 'sinon'; import { advanceTime, flushPromises } from '../../../tests/helpers'; +import { + buildCustomNetworkClientConfiguration, + buildMockGetNetworkClientById, +} from '../../network-controller/tests/helpers'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { AbstractTokenPricesService, @@ -738,9 +750,13 @@ describe('TokenRatesController', () => { describe('when polling is active', () => { it('should update exchange rates when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1337), + ticker: 'NEW', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -748,7 +764,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -764,16 +780,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); }); it('should update exchange rates when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1338), + ticker: 'TEST', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -781,7 +802,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -797,16 +818,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); }); it('should clear contractExchangeRates state when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1337), + ticker: 'NEW', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -814,7 +840,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -828,16 +854,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(controller.state.contractExchangeRates).toStrictEqual({}); }); it('should clear contractExchangeRates state when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1338), + ticker: 'TEST', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -845,7 +876,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -859,16 +890,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(controller.state.contractExchangeRates).toStrictEqual({}); }); it('should not update exchange rates when network state changes without a ticker/chain id change', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1337), + ticker: 'TEST', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -876,7 +912,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -892,7 +928,8 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'TEST' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); @@ -901,9 +938,13 @@ describe('TokenRatesController', () => { describe('when polling is inactive', () => { it('should not update exchange rates when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1337), + ticker: 'NEW', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -911,7 +952,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -926,16 +967,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); }); it('should not update exchange rates when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1338), + ticker: 'TEST', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -943,7 +989,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -958,16 +1004,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); }); it('should clear contractExchangeRates state when ticker changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1337), + ticker: 'NEW', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -975,7 +1026,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -988,16 +1039,21 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1337), ticker: 'NEW' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(controller.state.contractExchangeRates).toStrictEqual({}); }); it('should clear contractExchangeRates state when chain ID changes', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let networkStateChangeListener: (state: any) => Promise; + const getNetworkClientById = buildMockGetNetworkClientById({ + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: toHex(1338), + ticker: 'TEST', + }), + }); + let networkStateChangeListener: (state: NetworkState) => Promise; const onNetworkStateChange = jest .fn() .mockImplementation((listener) => { @@ -1005,7 +1061,7 @@ describe('TokenRatesController', () => { }); const controller = new TokenRatesController({ interval: 100, - getNetworkClientById: jest.fn(), + getNetworkClientById, chainId: toHex(1337), ticker: 'TEST', selectedAddress: defaultSelectedAddress, @@ -1018,7 +1074,8 @@ describe('TokenRatesController', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await networkStateChangeListener!({ - providerConfig: { chainId: toHex(1338), ticker: 'TEST' }, + ...defaultNetworkState, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }); expect(controller.state.contractExchangeRates).toStrictEqual({}); @@ -1604,7 +1661,7 @@ describe('TokenRatesController', () => { await callUpdateExchangeRatesMethod({ allTokens: { - [toHex(1)]: { + [ChainId.mainnet]: { [controller.config.selectedAddress]: [ { address: tokenAddress, @@ -1615,11 +1672,12 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(1), + chainId: ChainId.mainnet, controller, controllerEvents, method, nativeCurrency: 'ETH', + selectedNetworkClientId: InfuraNetworkType.mainnet, }); expect(controller.state.contractExchangeRates).toStrictEqual({}); @@ -1638,7 +1696,7 @@ describe('TokenRatesController', () => { await callUpdateExchangeRatesMethod({ allTokens: { // These tokens are for the right chain but wrong account - [toHex(1)]: { + [ChainId.mainnet]: { [differentAccount]: [ { address: tokenAddress, @@ -1660,11 +1718,12 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(1), + chainId: ChainId.mainnet, controller, controllerEvents, method, nativeCurrency: 'ETH', + selectedNetworkClientId: InfuraNetworkType.mainnet, }); expect(controller.state.contractExchangeRates).toStrictEqual({}); @@ -1689,7 +1748,7 @@ describe('TokenRatesController', () => { async () => await callUpdateExchangeRatesMethod({ allTokens: { - [toHex(1)]: { + [ChainId.mainnet]: { [controller.config.selectedAddress]: [ { address: tokenAddress, @@ -1700,11 +1759,12 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(1), + chainId: ChainId.mainnet, controller, controllerEvents, method, nativeCurrency: 'ETH', + selectedNetworkClientId: InfuraNetworkType.mainnet, }), ).rejects.toThrow('Failed to fetch'); expect(controller.state.contractExchangeRates).toStrictEqual({}); @@ -1716,7 +1776,7 @@ describe('TokenRatesController', () => { }); it('fetches rates for all tokens in batches', async () => { - const chainId = toHex(1); + const chainId = ChainId.mainnet; const ticker = 'ETH'; const tokenAddresses = [...new Array(200).keys()] .map(buildAddress) @@ -1750,6 +1810,7 @@ describe('TokenRatesController', () => { controllerEvents, method, nativeCurrency: ticker, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); const numBatches = Math.ceil( @@ -1795,7 +1856,7 @@ describe('TokenRatesController', () => { async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { - [toHex(1)]: { + [ChainId.mainnet]: { [controller.config.selectedAddress]: [ { address: tokenAddresses[0], @@ -1812,11 +1873,12 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(1), + chainId: ChainId.mainnet, controller, controllerEvents, method, nativeCurrency: 'ETH', + selectedNetworkClientId: InfuraNetworkType.mainnet, }); expect(controller.state).toMatchInlineSnapshot(` @@ -1908,6 +1970,12 @@ describe('TokenRatesController', () => { } it('updates exchange rates when native currency is not supported by the Price API', async () => { + const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; + const selectedNetworkClientConfiguration = + buildCustomNetworkClientConfiguration({ + chainId: toHex(137), + ticker: 'UNSUPPORTED', + }); const tokenAddresses = [ '0x0000000000000000000000000000000000000001', '0x0000000000000000000000000000000000000002', @@ -1935,16 +2003,23 @@ describe('TokenRatesController', () => { .get('/data/price') .query({ fsym: 'ETH', - tsyms: 'UNSUPPORTED', + tsyms: selectedNetworkClientConfiguration.ticker, }) - .reply(200, { UNSUPPORTED: 0.5 }); // .5 eth to 1 matic + .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); // .5 eth to 1 matic await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + }, + mockNetworkClientConfigurationsByNetworkClientId: { + [selectedNetworkClientId]: selectedNetworkClientConfiguration, + }, + }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { - [toHex(137)]: { + [selectedNetworkClientConfiguration.chainId]: { [controller.config.selectedAddress]: [ { address: tokenAddresses[0], @@ -1961,11 +2036,12 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(137), + chainId: selectedNetworkClientConfiguration.chainId, controller, controllerEvents, method, - nativeCurrency: 'UNSUPPORTED', + nativeCurrency: selectedNetworkClientConfiguration.ticker, + selectedNetworkClientId, }); // token value in terms of matic should be (token value in eth) * (eth value in matic) @@ -1990,15 +2066,19 @@ describe('TokenRatesController', () => { }); it('fetches rates for all tokens in batches when native currency is not supported by the Price API', async () => { - const chainId = toHex(1); - const ticker = 'UNSUPPORTED'; + const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; + const selectedNetworkClientConfiguration = + buildCustomNetworkClientConfiguration({ + chainId: toHex(999), + ticker: 'UNSUPPORTED', + }); const tokenAddresses = [...new Array(200).keys()] .map(buildAddress) .sort(); const tokenPricesService = buildMockTokenPricesService({ fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, validateCurrencySupported: (currency: unknown): currency is string => { - return currency !== ticker; + return currency !== selectedNetworkClientConfiguration.ticker; }, }); const fetchTokenPricesSpy = jest.spyOn( @@ -2012,28 +2092,31 @@ describe('TokenRatesController', () => { .get('/data/price') .query({ fsym: 'ETH', - tsyms: ticker, + tsyms: selectedNetworkClientConfiguration.ticker, }) - .reply(200, { [ticker]: 0.5 }); + .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); await withController( { options: { - ticker, tokenPricesService, }, + mockNetworkClientConfigurationsByNetworkClientId: { + [selectedNetworkClientId]: selectedNetworkClientConfiguration, + }, }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { - [chainId]: { + [selectedNetworkClientConfiguration.chainId]: { [controller.config.selectedAddress]: tokens, }, }, - chainId, + chainId: selectedNetworkClientConfiguration.chainId, controller, controllerEvents, method, - nativeCurrency: ticker, + nativeCurrency: selectedNetworkClientConfiguration.ticker, + selectedNetworkClientId, }); const numBatches = Math.ceil( @@ -2043,7 +2126,7 @@ describe('TokenRatesController', () => { for (let i = 1; i <= numBatches; i++) { expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - chainId, + chainId: selectedNetworkClientConfiguration.chainId, tokenAddresses: tokenAddresses.slice( (i - 1) * TOKEN_PRICES_BATCH_SIZE, i * TOKEN_PRICES_BATCH_SIZE, @@ -2056,6 +2139,12 @@ describe('TokenRatesController', () => { }); it('sets rates to undefined when chain is not supported by the Price API', async () => { + const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; + const selectedNetworkClientConfiguration = + buildCustomNetworkClientConfiguration({ + chainId: toHex(999), + ticker: 'TST', + }); const tokenAddresses = [ '0x0000000000000000000000000000000000000001', '0x0000000000000000000000000000000000000002', @@ -2080,11 +2169,18 @@ describe('TokenRatesController', () => { ) as unknown as AbstractTokenPricesService['validateChainIdSupported'], }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + }, + mockNetworkClientConfigurationsByNetworkClientId: { + [selectedNetworkClientId]: selectedNetworkClientConfiguration, + }, + }, async ({ controller, controllerEvents }) => { await callUpdateExchangeRatesMethod({ allTokens: { - [toHex(999)]: { + [selectedNetworkClientConfiguration.chainId]: { [controller.config.selectedAddress]: [ { address: tokenAddresses[0], @@ -2101,11 +2197,12 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(999), + chainId: selectedNetworkClientConfiguration.chainId, controller, controllerEvents, method, - nativeCurrency: 'TST', + nativeCurrency: selectedNetworkClientConfiguration.ticker, + selectedNetworkClientId, }); expect(controller.state).toMatchInlineSnapshot(` @@ -2171,7 +2268,8 @@ describe('TokenRatesController', () => { ], }, }, - chainId: toHex(1), + chainId: ChainId.mainnet, + selectedNetworkClientId: InfuraNetworkType.mainnet, controller, controllerEvents, method, @@ -2232,6 +2330,10 @@ type PartialConstructorParameters = { options?: Partial[0]>; config?: Partial; state?: Partial; + mockNetworkClientConfigurationsByNetworkClientId?: Record< + NetworkClientId, + NetworkClientConfiguration + >; }; type WithControllerArgs = @@ -2250,20 +2352,29 @@ type WithControllerArgs = async function withController( ...args: WithControllerArgs ) { - const [{ options, config, state }, testFunction] = - args.length === 2 - ? args - : [{ options: undefined, config: undefined, state: undefined }, args[0]]; + const [ + { + options = {}, + config = {}, + state = {}, + mockNetworkClientConfigurationsByNetworkClientId = {}, + }, + testFunction, + ] = args.length === 2 ? args : [{}, args[0]]; // explit cast used here because we know the `on____` functions are always // set in the constructor. const controllerEvents = {} as ControllerEvents; + const getNetworkClientById = buildMockGetNetworkClientById( + mockNetworkClientConfigurationsByNetworkClientId, + ); + const controllerOptions: ConstructorParameters< typeof TokenRatesController >[0] = { chainId: toHex(1), - getNetworkClientById: jest.fn(), + getNetworkClientById, onNetworkStateChange: (listener) => { controllerEvents.networkStateChange = listener; }, @@ -2315,6 +2426,8 @@ async function withController( * network we're getting updated exchange rates for. * @param args.setChainAsCurrent - When calling `updateExchangeRatesByChainId`, * this determines whether to set the chain as the globally selected chain. + * @param args.selectedNetworkClientId - The network client ID to use if + * `setChainAsCurrent` is true. */ async function callUpdateExchangeRatesMethod({ allTokens, @@ -2323,14 +2436,16 @@ async function callUpdateExchangeRatesMethod({ controllerEvents, method, nativeCurrency, + selectedNetworkClientId, setChainAsCurrent = true, }: { allTokens: TokenRatesConfig['allTokens']; - chainId: TokenRatesConfig['chainId']; + chainId: Hex; controller: TokenRatesController; controllerEvents: ControllerEvents; method: 'updateExchangeRates' | 'updateExchangeRatesByChainId'; nativeCurrency: TokenRatesConfig['nativeCurrency']; + selectedNetworkClientId?: NetworkClientId; setChainAsCurrent?: boolean; }) { if (method === 'updateExchangeRates' && !setChainAsCurrent) { @@ -2346,17 +2461,22 @@ async function callUpdateExchangeRatesMethod({ controllerEvents.tokensStateChange({ allDetectedTokens: {}, allTokens }); if (setChainAsCurrent) { + assert( + selectedNetworkClientId, + 'The "selectedNetworkClientId" option must be given if the "setChainAsCurrent" flag is also given', + ); + // We're using controller events here instead of calling `configure` // because `configure` does not update internal controller state correctly. // As with many BaseControllerV1-based controllers, runtime config // modification is allowed by the API but not supported in practice. + // + // @ts-expect-error Note that the state given here is intentionally + // incomplete because the controller only uses this one property, and the + // tests are written to only consider it. We want this to break if we start + // relying on more properties, as we'd need to update the tests accordingly. controllerEvents.networkStateChange({ - // Note that the state given here is intentionally incomplete because the - // controller only uses these two properties, and the tests are written to - // only consider these two. We want this to break if we start relying on - // more, as we'd need to update the tests accordingly. - // @ts-expect-error Intentionally incomplete state - providerConfig: { chainId, ticker: nativeCurrency }, + selectedNetworkClientId, }); } diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 2863bb7b96..af8273d576 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -254,8 +254,12 @@ export class TokenRatesController extends StaticIntervalPollingControllerV1< } }); - onNetworkStateChange(async ({ providerConfig }) => { - const { chainId, ticker } = providerConfig; + onNetworkStateChange(async ({ selectedNetworkClientId }) => { + const selectedNetworkClient = getNetworkClientById( + selectedNetworkClientId, + ); + const { chainId, ticker } = selectedNetworkClient.configuration; + if ( this.config.chainId !== chainId || this.config.nativeCurrency !== ticker diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index 307c7d7823..b4c1b763d8 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -11,14 +11,11 @@ import { ChainId, ORIGIN_METAMASK, convertHexToDecimal, - NetworkType, - toHex, - NetworksTicker, + InfuraNetworkType, } from '@metamask/controller-utils'; import type { NetworkClientConfiguration, NetworkClientId, - ProviderConfig, } from '@metamask/network-controller'; import { defaultState as defaultNetworkState } from '@metamask/network-controller'; import type { PreferencesState } from '@metamask/preferences-controller'; @@ -58,17 +55,6 @@ const uuidV1Mock = jest.mocked(uuidV1); const ERC20StandardMock = jest.mocked(ERC20Standard); const ERC1155StandardMock = jest.mocked(ERC1155Standard); -const SEPOLIA = { - chainId: toHex(11155111), - type: NetworkType.sepolia, - ticker: NetworksTicker.sepolia, -}; -const GOERLI = { - chainId: toHex(5), - type: NetworkType.goerli, - ticker: NetworksTicker.goerli, -}; - describe('TokensController', () => { beforeEach(() => { uuidV1Mock.mockReturnValue('9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'); @@ -300,7 +286,6 @@ describe('TokensController', () => { selectedAddress: secondAddress, }); expect(controller.state.tokens).toHaveLength(0); - triggerPreferencesStateChange({ ...getDefaultPreferencesState(), selectedAddress: firstAddress, @@ -321,17 +306,17 @@ describe('TokensController', () => { it('should add token by network', async () => { await withController(async ({ controller, changeNetwork }) => { - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await controller.addToken({ address: '0x01', symbol: 'bar', decimals: 2, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); expect(controller.state.tokens).toHaveLength(0); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect(controller.state.tokens[0]).toStrictEqual({ address: '0x01', decimals: 2, @@ -475,13 +460,13 @@ describe('TokensController', () => { ContractMock.mockReturnValue( buildMockEthersERC721Contract({ supportsInterface: false }), ); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await controller.addToken({ address: '0x02', symbol: 'baz', decimals: 2, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await controller.addToken({ address: '0x01', symbol: 'bar', @@ -491,7 +476,7 @@ describe('TokensController', () => { controller.ignoreTokens(['0x01']); expect(controller.state.tokens).toHaveLength(0); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect(controller.state.tokens[0]).toStrictEqual({ address: '0x02', decimals: 2, @@ -547,7 +532,7 @@ describe('TokensController', () => { ...getDefaultPreferencesState(), selectedAddress, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await controller.addToken({ address: '0x01', symbol: 'bar', @@ -594,7 +579,7 @@ describe('TokensController', () => { ...getDefaultPreferencesState(), selectedAddress, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await controller.addToken({ address: '0x01', symbol: 'bar', @@ -632,7 +617,7 @@ describe('TokensController', () => { ...getDefaultPreferencesState(), selectedAddress: selectedAddress1, }); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await controller.addToken({ address: '0x01', symbol: 'bar', @@ -644,7 +629,7 @@ describe('TokensController', () => { expect(controller.state.tokens).toHaveLength(0); expect(controller.state.ignoredTokens).toStrictEqual(['0x01']); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); expect(controller.state.ignoredTokens).toHaveLength(0); await controller.addToken({ @@ -920,7 +905,7 @@ describe('TokensController', () => { symbol: 'LINK', decimals: 18, }); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await expect(addTokenPromise).rejects.toThrow( 'TokensController Error: Switched networks while adding token', @@ -1012,9 +997,12 @@ describe('TokensController', () => { ); // The currently configured chain + address - const CONFIGURED_CHAIN = SEPOLIA; + const CONFIGURED_CHAIN = ChainId.sepolia; + const CONFIGURED_NETWORK_CLIENT_ID = InfuraNetworkType.sepolia; const CONFIGURED_ADDRESS = '0xConfiguredAddress'; - changeNetwork(CONFIGURED_CHAIN); + changeNetwork({ + selectedNetworkClientId: CONFIGURED_NETWORK_CLIENT_ID, + }); triggerPreferencesStateChange({ ...getDefaultPreferencesState(), selectedAddress: CONFIGURED_ADDRESS, @@ -1066,12 +1054,12 @@ describe('TokensController', () => { // Expect tokens under the correct chain + account expect(controller.state.allTokens).toStrictEqual({ - [CONFIGURED_CHAIN.chainId]: { + [CONFIGURED_CHAIN]: { [CONFIGURED_ADDRESS]: [addedTokenConfiguredAccount], }, }); expect(controller.state.allDetectedTokens).toStrictEqual({ - [CONFIGURED_CHAIN.chainId]: { + [CONFIGURED_CHAIN]: { [CONFIGURED_ADDRESS]: [detectedTokenConfiguredAccount], }, [OTHER_CHAIN]: { @@ -2052,7 +2040,7 @@ describe('TokensController', () => { buildMockEthersERC721Contract({ supportsInterface: false }), ); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); await controller.addToken({ address: '0x01', symbol: 'A', @@ -2065,7 +2053,7 @@ describe('TokensController', () => { }); const initialTokensFirst = controller.state.tokens; - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); await controller.addToken({ address: '0x03', symbol: 'C', @@ -2124,10 +2112,10 @@ describe('TokensController', () => { }, ]); - changeNetwork(SEPOLIA); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.sepolia }); expect(initialTokensFirst).toStrictEqual(controller.state.tokens); - changeNetwork(GOERLI); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.goerli }); expect(initialTokensSecond).toStrictEqual(controller.state.tokens); }); }); @@ -2302,7 +2290,9 @@ type WithControllerCallback = ({ triggerPreferencesStateChange, }: { controller: TokensController; - changeNetwork: (providerConfig: ProviderConfig) => void; + changeNetwork: (options: { + selectedNetworkClientId: NetworkClientId; + }) => void; messenger: UnrestrictedMessenger; approvalController: ApprovalController; triggerPreferencesStateChange: (state: PreferencesState) => void; @@ -2384,10 +2374,14 @@ async function withController( messenger.publish('PreferencesController:stateChange', state, []); }; - const changeNetwork = (providerConfig: ProviderConfig) => { + const changeNetwork = ({ + selectedNetworkClientId, + }: { + selectedNetworkClientId: NetworkClientId; + }) => { messenger.publish('NetworkController:networkDidChange', { ...defaultNetworkState, - providerConfig, + selectedNetworkClientId, }); }; diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index a96de9cd6c..083fee1704 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -273,10 +273,14 @@ export class TokensController extends BaseControllerV1< this.messagingSystem.subscribe( 'NetworkController:networkDidChange', - ({ providerConfig }) => { + ({ selectedNetworkClientId }) => { + const selectedNetworkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + const { chainId } = selectedNetworkClient.configuration; const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state; const { selectedAddress } = this.config; - const { chainId } = providerConfig; this.abortController.abort(); this.abortController = new AbortController(); this.configure({ chainId }); diff --git a/packages/ens-controller/src/EnsController.test.ts b/packages/ens-controller/src/EnsController.test.ts index 9a49dfe2ca..7367a09740 100644 --- a/packages/ens-controller/src/EnsController.test.ts +++ b/packages/ens-controller/src/EnsController.test.ts @@ -1,14 +1,22 @@ import * as providersModule from '@ethersproject/providers'; import { ControllerMessenger } from '@metamask/base-controller'; import { - NetworkType, - NetworksTicker, toChecksumHexAddress, toHex, + InfuraNetworkType, } from '@metamask/controller-utils'; +import { defaultState as defaultNetworkState } from '@metamask/network-controller'; +import type { + ExtractAvailableAction, + ExtractAvailableEvent, +} from '../../base-controller/tests/helpers'; +import { buildMockGetNetworkClientById } from '../../network-controller/tests/helpers'; import { EnsController, DEFAULT_ENS_NETWORK_MAP } from './EnsController'; -import type { EnsControllerState } from './EnsController'; +import type { + EnsControllerState, + EnsControllerMessenger, +} from './EnsController'; const defaultState: EnsControllerState = { ensEntries: {}, @@ -36,6 +44,11 @@ jest.mock('@ethersproject/providers', () => { }; }); +type RootMessenger = ControllerMessenger< + ExtractAvailableAction, + ExtractAvailableEvent +>; + const ZERO_X_ERROR_ADDRESS = '0x'; const address1 = '0x32Be343B94f860124dC4fEe278FDCBD38C102D88'; @@ -51,14 +64,28 @@ const address3Checksum = toChecksumHexAddress(address3); const name = 'EnsController'; /** - * Constructs a restricted controller messenger. + * Constructs the root messenger. + * + * @returns A restricted controller messenger. + */ +function getRootMessenger(): RootMessenger { + return new ControllerMessenger(); +} + +/** + * Constructs the messenger restricted to EnsController actions and events. * + * @param rootMessenger - The root messenger to base the restricted messenger + * off of. * @returns A restricted controller messenger. */ -function getMessenger() { - return new ControllerMessenger().getRestricted({ +function getRestrictedMessenger(rootMessenger: RootMessenger) { + return rootMessenger.getRestricted< + 'EnsController', + 'NetworkController:getNetworkClientById' + >({ name, - allowedActions: [], + allowedActions: ['NetworkController:getNetworkClientById'], allowedEvents: [], }); } @@ -74,17 +101,19 @@ function getProvider() { describe('EnsController', () => { it('should set default state', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.state).toStrictEqual(defaultState); }); it('should return registry address for `.`', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.get('0x1', '.')).toStrictEqual({ ensName: '.', @@ -94,17 +123,19 @@ describe('EnsController', () => { }); it('should not return registry address for unrecognized chains', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.get('0x666', '.')).toBeNull(); }); it('should add a new ENS entry and return true', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.state.ensEntries['0x1'][name1]).toStrictEqual({ @@ -115,9 +146,10 @@ describe('EnsController', () => { }); it('should clear ensResolutionsByAddress state propery when resetState is called', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, state: { ensResolutionsByAddress: { [address1Checksum]: 'peaksignal.eth', @@ -135,9 +167,15 @@ describe('EnsController', () => { }); it('should clear ensResolutionsByAddress state propery on networkDidChange', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, state: { ensResolutionsByAddress: { [address1Checksum]: 'peaksignal.eth', @@ -146,11 +184,8 @@ describe('EnsController', () => { provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -159,9 +194,10 @@ describe('EnsController', () => { }); it('should add a new ENS entry with null address and return true', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, null)).toBe(true); expect(controller.state.ensEntries['0x1'][name1]).toStrictEqual({ @@ -172,9 +208,10 @@ describe('EnsController', () => { }); it('should update an ENS entry and return true', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.set('0x1', name1, address2)).toBe(true); @@ -186,9 +223,10 @@ describe('EnsController', () => { }); it('should update an ENS entry with null address and return true', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.set('0x1', name1, null)).toBe(true); @@ -200,9 +238,10 @@ describe('EnsController', () => { }); it('should not update an ENS entry if the address is the same (valid address) and return false', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.set('0x1', name1, address1)).toBe(false); @@ -214,9 +253,10 @@ describe('EnsController', () => { }); it('should not update an ENS entry if the address is the same (null) and return false', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, null)).toBe(true); expect(controller.set('0x1', name1, null)).toBe(false); @@ -228,9 +268,10 @@ describe('EnsController', () => { }); it('should add multiple ENS entries and update without side effects', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.set('0x1', name2, address2)).toBe(true); @@ -254,9 +295,10 @@ describe('EnsController', () => { }); it('should get ENS default registry by chainId when asking for `.`', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.get('0x1', name1)).toStrictEqual({ @@ -267,9 +309,10 @@ describe('EnsController', () => { }); it('should get ENS entry by chainId and ensName', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.get('0x1', name1)).toStrictEqual({ @@ -280,27 +323,30 @@ describe('EnsController', () => { }); it('should return null when getting nonexistent name', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.get('0x1', name2)).toBeNull(); }); it('should return null when getting nonexistent chainId', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.get(toHex(2), name1)).toBeNull(); }); it('should throw on attempt to set invalid ENS entry: chainId', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(() => { // @ts-expect-error Intentionally invalid chain ID @@ -312,9 +358,10 @@ describe('EnsController', () => { }); it('should throw on attempt to set invalid ENS entry: ENS name', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(() => { controller.set('0x1', 'foo.eth', address1); @@ -323,9 +370,10 @@ describe('EnsController', () => { }); it('should throw on attempt to set invalid ENS entry: address', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(() => { controller.set('0x1', name1, 'foo'); @@ -336,9 +384,10 @@ describe('EnsController', () => { }); it('should remove an ENS entry and return true', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.delete('0x1', name1)).toBe(true); @@ -346,9 +395,10 @@ describe('EnsController', () => { }); it('should remove chain entries completely when all entries are removed', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.delete('0x1', '.')).toBe(true); @@ -360,9 +410,10 @@ describe('EnsController', () => { }); it('should return false if an ENS entry was NOT deleted due to unsafe input', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); // @ts-expect-error Suppressing error to test runtime behavior expect(controller.delete('__proto__', 'bar')).toBe(false); @@ -370,9 +421,10 @@ describe('EnsController', () => { }); it('should return false if an ENS entry was NOT deleted', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); controller.set('0x1', name1, address1); expect(controller.delete('0x1', 'bar')).toBe(false); @@ -385,9 +437,10 @@ describe('EnsController', () => { }); it('should add multiple ENS entries and remove without side effects', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.set('0x1', name2, address2)).toBe(true); @@ -406,9 +459,10 @@ describe('EnsController', () => { }); it('should clear all ENS entries', () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const controller = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(controller.set('0x1', name1, address1)).toBe(true); expect(controller.set('0x1', name2, address2)).toBe(true); @@ -422,25 +476,29 @@ describe('EnsController', () => { describe('reverseResolveName', () => { it('should return undefined when eth provider is not defined', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, }); expect(await ens.reverseResolveAddress(address1)).toBeUndefined(); }); it('should return undefined when network is loading', async function () { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -448,17 +506,20 @@ describe('EnsController', () => { }); it('should return undefined when network is not ens supported', async function () { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: toHex(0), - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -466,7 +527,13 @@ describe('EnsController', () => { }); it('should only resolve an ENS name once', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest.spyOn(ethProvider, 'resolveName').mockResolvedValue(address1); jest @@ -475,15 +542,12 @@ describe('EnsController', () => { jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -493,20 +557,23 @@ describe('EnsController', () => { }); it('should fail if lookupAddress through an error', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest.spyOn(ethProvider, 'lookupAddress').mockRejectedValue('error'); jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -515,20 +582,23 @@ describe('EnsController', () => { }); it('should fail if lookupAddress returns a null value', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest.spyOn(ethProvider, 'lookupAddress').mockResolvedValue(null); jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -537,7 +607,13 @@ describe('EnsController', () => { }); it('should fail if resolveName through an error', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest .spyOn(ethProvider, 'lookupAddress') @@ -545,15 +621,12 @@ describe('EnsController', () => { jest.spyOn(ethProvider, 'resolveName').mockRejectedValue('error'); jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -562,7 +635,13 @@ describe('EnsController', () => { }); it('should fail if resolveName returns a null value', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest.spyOn(ethProvider, 'resolveName').mockResolvedValue(null); jest @@ -570,15 +649,12 @@ describe('EnsController', () => { .mockResolvedValue('peaksignal.eth'); jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -587,7 +663,13 @@ describe('EnsController', () => { }); it('should fail if registred address is zero x error address', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest .spyOn(ethProvider, 'resolveName') @@ -597,15 +679,12 @@ describe('EnsController', () => { .mockResolvedValue('peaksignal.eth'); jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); @@ -614,7 +693,13 @@ describe('EnsController', () => { }); it('should fail if the name is registered to a different address than the reverse resolved', async () => { - const messenger = getMessenger(); + const rootMessenger = getRootMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById(); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); + const ensControllerMessenger = getRestrictedMessenger(rootMessenger); const ethProvider = new providersModule.Web3Provider(getProvider()); jest.spyOn(ethProvider, 'resolveName').mockResolvedValue(address2); @@ -623,15 +708,12 @@ describe('EnsController', () => { .mockResolvedValue('peaksignal.eth'); jest.spyOn(providersModule, 'Web3Provider').mockReturnValue(ethProvider); const ens = new EnsController({ - messenger, + messenger: ensControllerMessenger, provider: getProvider(), onNetworkDidChange: (listener) => { listener({ - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - ticker: NetworksTicker.mainnet, - }, + ...defaultNetworkState, + selectedNetworkClientId: InfuraNetworkType.mainnet, }); }, }); diff --git a/packages/ens-controller/src/EnsController.ts b/packages/ens-controller/src/EnsController.ts index 898c1697c9..7398d81018 100644 --- a/packages/ens-controller/src/EnsController.ts +++ b/packages/ens-controller/src/EnsController.ts @@ -15,7 +15,10 @@ import { convertHexToDecimal, toHex, } from '@metamask/controller-utils'; -import type { NetworkState } from '@metamask/network-controller'; +import type { + NetworkControllerGetNetworkClientByIdAction, + NetworkState, +} from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { createProjectLogger } from '@metamask/utils'; import { toASCII } from 'punycode/'; @@ -69,11 +72,13 @@ export type EnsControllerState = { ensResolutionsByAddress: { [key: string]: string }; }; +type AllowedActions = NetworkControllerGetNetworkClientByIdAction; + export type EnsControllerMessenger = RestrictedControllerMessenger< typeof name, + AllowedActions, never, - never, - never, + AllowedActions['type'], never >; @@ -123,7 +128,7 @@ export class EnsController extends BaseController< state?: Partial; provider?: ExternalProvider | JsonRpcFetchFunc; onNetworkDidChange?: ( - listener: (networkState: Pick) => void, + listener: (networkState: NetworkState) => void, ) => void; }) { super({ @@ -149,9 +154,14 @@ export class EnsController extends BaseController< }); if (provider && onNetworkDidChange) { - onNetworkDidChange((networkState) => { + onNetworkDidChange(({ selectedNetworkClientId }) => { this.resetState(); - const currentChainId = networkState.providerConfig.chainId; + const selectedNetworkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + const currentChainId = selectedNetworkClient.configuration.chainId; + if (this.#getChainEnsSupport(currentChainId)) { this.#ethProvider = new Web3Provider(provider, { chainId: convertHexToDecimal(currentChainId), diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index e1c0fa4cca..4ea14cb03d 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -2,7 +2,6 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { ChainId, convertHexToDecimal, - NetworkType, toHex, } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; @@ -323,12 +322,15 @@ describe('GasFeeController', () => { .fn() .mockReturnValue(true), networkControllerState: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://some/url', - ticker: 'TEST', + networkConfigurations: { + 'AAAA-BBBB-CCCC-DDDD': { + id: 'AAAA-BBBB-CCCC-DDDD', + chainId: toHex(1337), + rpcUrl: 'http://some/url', + ticker: 'TEST', + }, }, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }, clientId: '99999', }); @@ -377,12 +379,15 @@ describe('GasFeeController', () => { .fn() .mockReturnValue(true), networkControllerState: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://some/url', - ticker: 'TEST', + networkConfigurations: { + 'AAAA-BBBB-CCCC-DDDD': { + id: 'AAAA-BBBB-CCCC-DDDD', + chainId: toHex(1337), + rpcUrl: 'http://some/url', + ticker: 'TEST', + }, }, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }, clientId: '99999', }); @@ -716,12 +721,15 @@ describe('GasFeeController', () => { await setupGasFeeController({ ...defaultConstructorOptions, networkControllerState: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://some/url', - ticker: 'TEST', + networkConfigurations: { + 'AAAA-BBBB-CCCC-DDDD': { + id: 'AAAA-BBBB-CCCC-DDDD', + chainId: toHex(1337), + rpcUrl: 'http://some/url', + ticker: 'TEST', + }, }, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }, clientId: '99999', }); @@ -860,12 +868,15 @@ describe('GasFeeController', () => { await setupGasFeeController({ ...defaultConstructorOptions, networkControllerState: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://some/url', - ticker: 'TEST', + networkConfigurations: { + 'AAAA-BBBB-CCCC-DDDD': { + id: 'AAAA-BBBB-CCCC-DDDD', + chainId: toHex(1337), + rpcUrl: 'http://some/url', + ticker: 'TEST', + }, }, + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', }, clientId: '99999', }); diff --git a/packages/gas-fee-controller/src/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts index b95c7da776..560673f3e7 100644 --- a/packages/gas-fee-controller/src/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -362,9 +362,13 @@ export class GasFeeController extends StaticIntervalPollingController< await this.#onNetworkControllerDidChange(networkControllerState); }); } else { - this.currentChainId = this.messagingSystem.call( + const { selectedNetworkClientId } = this.messagingSystem.call( 'NetworkController:getState', - ).providerConfig.chainId; + ); + this.currentChainId = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ).configuration.chainId; this.messagingSystem.subscribe( 'NetworkController:networkDidChange', async (networkControllerState) => { @@ -585,8 +589,13 @@ export class GasFeeController extends StaticIntervalPollingController< ); } - async #onNetworkControllerDidChange(networkControllerState: NetworkState) { - const newChainId = networkControllerState.providerConfig.chainId; + async #onNetworkControllerDidChange({ + selectedNetworkClientId, + }: NetworkState) { + const newChainId = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ).configuration.chainId; if (newChainId !== this.currentChainId) { this.ethQuery = new EthQuery(this.#getProvider()); diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index c98f3b5b5c..2c3c77b4dc 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -6,8 +6,6 @@ import type { import { BaseController } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS, - NetworksTicker, - ChainId, InfuraNetworkType, NetworkType, isSafeChainId, @@ -44,27 +42,6 @@ import type { const log = createModuleLogger(projectLogger, 'NetworkController'); -/** - * @type ProviderConfig - * - * Configuration passed to web3-provider-engine - * @property rpcUrl - RPC target URL. - * @property type - Human-readable network name. - * @property chainId - Network ID as per EIP-155. - * @property ticker - Currency ticker. - * @property nickname - Personalized network name. - * @property id - Network Configuration Id. - */ -export type ProviderConfig = { - rpcUrl?: string; - type: NetworkType; - chainId: Hex; - ticker: string; - nickname?: string; - rpcPrefs?: { blockExplorerUrl?: string }; - id?: NetworkConfigurationId; -}; - export type Block = { baseFeePerGas?: string; }; @@ -193,109 +170,6 @@ function isErrorWithCode(error: unknown): error is { code: string | number } { return typeof error === 'object' && error !== null && 'code' in error; } -/** - * Builds an identifier for an Infura network client for lookup purposes. - * - * @param infuraNetworkOrProviderConfig - The name of an Infura network or a - * provider config. - * @returns The built identifier. - */ -function buildInfuraNetworkClientId( - infuraNetworkOrProviderConfig: - | InfuraNetworkType - | (ProviderConfig & { type: InfuraNetworkType }), -): BuiltInNetworkClientId { - if (typeof infuraNetworkOrProviderConfig === 'string') { - return infuraNetworkOrProviderConfig; - } - return infuraNetworkOrProviderConfig.type; -} - -/** - * Builds an identifier for a custom network client for lookup purposes. - * - * @param args - This function can be called two ways: - * 1. The ID of a network configuration. - * 2. A provider config and a set of network configurations. - * @returns The built identifier. - */ -function buildCustomNetworkClientId( - ...args: - | [NetworkConfigurationId] - | [ - ProviderConfig & { type: typeof NetworkType.rpc; rpcUrl: string }, - NetworkConfigurations, - ] -): CustomNetworkClientId { - if (args.length === 1) { - return args[0]; - } - const [{ id, rpcUrl }, networkConfigurations] = args; - if (id === undefined) { - const matchingNetworkConfiguration = Object.values( - networkConfigurations, - ).find((networkConfiguration) => { - return networkConfiguration.rpcUrl === rpcUrl.toLowerCase(); - }); - if (matchingNetworkConfiguration) { - return matchingNetworkConfiguration.id; - } - return rpcUrl.toLowerCase(); - } - return id; -} - -/** - * Returns whether the given provider config refers to an Infura network. - * - * @param providerConfig - The provider config. - * @returns True if the provider config refers to an Infura network, false - * otherwise. - */ -function isInfuraProviderConfig( - providerConfig: ProviderConfig, -): providerConfig is ProviderConfig & { type: InfuraNetworkType } { - return isInfuraNetworkType(providerConfig.type); -} - -/** - * Returns whether the given provider config refers to an Infura network. - * - * @param providerConfig - The provider config. - * @returns True if the provider config refers to an Infura network, false - * otherwise. - */ -function isCustomProviderConfig( - providerConfig: ProviderConfig, -): providerConfig is ProviderConfig & { type: typeof NetworkType.rpc } { - return providerConfig.type === NetworkType.rpc; -} - -/** - * As a provider config represents the settings that are used to interface with - * an RPC endpoint, it must have both a chain ID and an RPC URL if it represents - * a custom network. These properties _should_ be set as they are validated in - * the UI when a user adds a custom network, but just to be safe we validate - * them here. - * - * In addition, historically the `rpcUrl` property on the ProviderConfig type - * has been optional, even though it should not be. Making this non-optional - * would be a breaking change, so this function types the provider config - * correctly so that we don't have to check `rpcUrl` in other places. - * - * @param providerConfig - A provider config. - * @throws if the provider config does not have a chain ID or an RPC URL. - */ -function validateCustomProviderConfig( - providerConfig: ProviderConfig & { type: typeof NetworkType.rpc }, -): asserts providerConfig is typeof providerConfig & { rpcUrl: string } { - if (providerConfig.chainId === undefined) { - throw new Error('chainId must be provided for custom RPC endpoints'); - } - if (providerConfig.rpcUrl === undefined) { - throw new Error('rpcUrl must be provided for custom RPC endpoints'); - } -} /** * The string that uniquely identifies an Infura network client. */ @@ -322,13 +196,11 @@ export type NetworksMetadata = { * @type NetworkState * * Network controller state - * @property providerConfig - RPC URL and network name provider settings of the currently connected network * @property properties - an additional set of network properties for the currently connected network * @property networkConfigurations - the full list of configured networks either preloaded or added by the user. */ export type NetworkState = { selectedNetworkClientId: NetworkClientId; - providerConfig: ProviderConfig; networkConfigurations: NetworkConfigurations; networksMetadata: NetworksMetadata; }; @@ -411,11 +283,6 @@ export type NetworkControllerGetStateAction = ControllerGetStateAction< NetworkState >; -export type NetworkControllerGetProviderConfigAction = { - type: `NetworkController:getProviderConfig`; - handler: () => ProviderConfig; -}; - export type NetworkControllerGetEthQueryAction = { type: `NetworkController:getEthQuery`; handler: () => EthQuery | undefined; @@ -464,7 +331,6 @@ export type NetworkControllerGetNetworkConfigurationByNetworkClientId = { export type NetworkControllerActions = | NetworkControllerGetStateAction - | NetworkControllerGetProviderConfigAction | NetworkControllerGetEthQueryAction | NetworkControllerGetNetworkClientByIdAction | NetworkControllerGetSelectedNetworkClientAction @@ -491,11 +357,6 @@ export type NetworkControllerOptions = { export const defaultState: NetworkState = { selectedNetworkClientId: NetworkType.mainnet, - providerConfig: { - type: NetworkType.mainnet, - chainId: ChainId.mainnet, - ticker: NetworksTicker.mainnet, - }, networksMetadata: {}, networkConfigurations: {}, }; @@ -554,7 +415,7 @@ export class NetworkController extends BaseController< #trackMetaMetricsEvent: (event: MetaMetricsEventPayload) => void; - #previousProviderConfig: ProviderConfig; + #previouslySelectedNetworkClientId: string; #providerProxy: ProviderProxy | undefined; @@ -562,6 +423,10 @@ export class NetworkController extends BaseController< #autoManagedNetworkClientRegistry?: AutoManagedNetworkClientRegistry; + #autoManagedNetworkClient?: + | AutoManagedNetworkClient + | AutoManagedNetworkClient; + constructor({ messenger, state, @@ -579,10 +444,6 @@ export class NetworkController extends BaseController< persist: true, anonymous: false, }, - providerConfig: { - persist: true, - anonymous: false, - }, networkConfigurations: { persist: true, anonymous: false, @@ -596,12 +457,6 @@ export class NetworkController extends BaseController< } this.#infuraProjectId = infuraProjectId; this.#trackMetaMetricsEvent = trackMetaMetricsEvent; - this.messagingSystem.registerActionHandler( - `${this.name}:getProviderConfig`, - () => { - return this.state.providerConfig; - }, - ); this.messagingSystem.registerActionHandler( `${this.name}:getEthQuery`, @@ -645,7 +500,8 @@ export class NetworkController extends BaseController< this.getSelectedNetworkClient.bind(this), ); - this.#previousProviderConfig = this.state.providerConfig; + this.#previouslySelectedNetworkClientId = + this.state.selectedNetworkClientId; } /** @@ -763,19 +619,27 @@ export class NetworkController extends BaseController< } /** - * Executes a series of steps to apply the changes to the provider config: + * Executes a series of steps to switch the network: * - * 1. Notifies subscribers that the network is about to change. - * 2. Looks up a known and preinitialized network client matching the provider - * config and re-points the provider and block tracker proxy to it. - * 3. Notifies subscribers that the network has changed. + * 1. Notifies subscribers via the messenger that the network is about to be + * switched (and, really, that the global provider and block tracker proxies + * will be re-pointed to a new network). + * 2. Looks up a known and preinitialized network client matching the given + * ID and uses it to re-point the aforementioned provider and block tracker + * proxies. + * 3. Notifies subscribers via the messenger that the network has switched. + * 4. Captures metadata for the newly switched network in state. + * + * @param networkClientId - The ID of a network client that requests will be + * routed through (either the name of an Infura network or the ID of a custom + * network configuration). */ - async #refreshNetwork() { + async #refreshNetwork(networkClientId: string) { this.messagingSystem.publish( 'NetworkController:networkWillChange', this.state, ); - this.#applyNetworkSelection(); + this.#applyNetworkSelection(networkClientId); this.messagingSystem.publish( 'NetworkController:networkDidChange', this.state, @@ -784,13 +648,11 @@ export class NetworkController extends BaseController< } /** - * Populates the network clients and establishes the initial network based on - * the provider configuration in state. + * Creates network clients for built-in and custom networks, then establishes + * the currently selected network client based on state. */ async initializeProvider() { - this.#ensureAutoManagedNetworkClientRegistryPopulated(); - - this.#applyNetworkSelection(); + this.#applyNetworkSelection(this.state.selectedNetworkClientId); await this.lookupNetwork(); } @@ -867,17 +729,19 @@ export class NetworkController extends BaseController< } /** - * Performs side effects after switching to a network. If the network is - * available, updates the network state with the network ID of the network and - * stores whether the network supports EIP-1559; otherwise clears said - * information about the network that may have been previously stored. + * Persists the following metadata about the given or selected network to + * state: * - * @param networkClientId - (Optional) The ID of the network client to update. + * - The status of the network, namely, whether it is available, geo-blocked + * (Infura only), or unavailable, or whether the status is unknown + * - Whether the network supports EIP-1559, or whether it is unknown + * + * Note that it is possible for the network to be switched while this data is + * being collected. If that is the case, no metadata for the (now previously) + * selected network will be updated. + * + * @param networkClientId - The ID of the network client to update. * If no ID is provided, uses the currently selected network. - * @fires infuraIsBlocked if the network is Infura-supported and is blocking - * requests. - * @fires infuraIsUnblocked if the network is Infura-supported and is not - * blocking requests, or if the network is not Infura-supported. */ async lookupNetwork(networkClientId?: NetworkClientId) { if (networkClientId) { @@ -889,7 +753,9 @@ export class NetworkController extends BaseController< return; } - const isInfura = isInfuraProviderConfig(this.state.providerConfig); + const isInfura = + this.#autoManagedNetworkClient?.configuration.type === + NetworkClientType.Infura; let networkChanged = false; const listener = () => { @@ -1000,50 +866,19 @@ export class NetworkController extends BaseController< } /** - * Convenience method to update provider RPC settings. + * Changes the selected network. * - * @param networkConfigurationIdOrType - The unique id for the network configuration to set as the active provider, - * or the type of a built-in network. + * @param networkClientId - The ID of a network client that requests will be + * routed through (either the name of an Infura network or the ID of a custom + * network configuration). + * @throws if no network client is associated with the given + * `networkClientId`. */ - async setActiveNetwork(networkConfigurationIdOrType: string) { - this.#previousProviderConfig = this.state.providerConfig; - - let targetNetwork: ProviderConfig; - if (isInfuraNetworkType(networkConfigurationIdOrType)) { - const ticker = NetworksTicker[networkConfigurationIdOrType]; - - targetNetwork = { - chainId: ChainId[networkConfigurationIdOrType], - id: undefined, - rpcPrefs: BUILT_IN_NETWORKS[networkConfigurationIdOrType].rpcPrefs, - rpcUrl: undefined, - nickname: undefined, - ticker, - type: networkConfigurationIdOrType, - }; - } else { - if ( - !Object.keys(this.state.networkConfigurations).includes( - networkConfigurationIdOrType, - ) - ) { - throw new Error( - `networkConfigurationId ${networkConfigurationIdOrType} does not match a configured networkConfiguration or built-in network type`, - ); - } - targetNetwork = { - ...this.state.networkConfigurations[networkConfigurationIdOrType], - type: NetworkType.rpc, - }; - } - - this.#ensureAutoManagedNetworkClientRegistryPopulated(); + async setActiveNetwork(networkClientId: string) { + this.#previouslySelectedNetworkClientId = + this.state.selectedNetworkClientId; - this.update((state) => { - state.providerConfig = targetNetwork; - }); - - await this.#refreshNetwork(); + await this.#refreshNetwork(networkClientId); } /** @@ -1148,11 +983,11 @@ export class NetworkController extends BaseController< } /** - * Re-initializes the provider and block tracker for the current network. + * Ensures that the provider and block tracker proxies are pointed to the + * currently selected network and refreshes the metadata for the */ async resetConnection() { - this.#ensureAutoManagedNetworkClientRegistryPopulated(); - await this.#refreshNetwork(); + await this.#refreshNetwork(this.state.selectedNetworkClientId); } /** @@ -1266,9 +1101,7 @@ export class NetworkController extends BaseController< const upsertedNetworkConfigurationId = existingNetworkConfiguration ? existingNetworkConfiguration.id : random(); - const networkClientId = buildCustomNetworkClientId( - upsertedNetworkConfigurationId, - ); + const networkClientId = upsertedNetworkConfigurationId; const customNetworkClientRegistry = autoManagedNetworkClientRegistry[NetworkClientType.Custom]; @@ -1341,7 +1174,7 @@ export class NetworkController extends BaseController< const autoManagedNetworkClientRegistry = this.#ensureAutoManagedNetworkClientRegistryPopulated(); - const networkClientId = buildCustomNetworkClientId(networkConfigurationId); + const networkClientId = networkConfigurationId; this.update((state) => { delete state.networkConfigurations[networkConfigurationId]; @@ -1356,18 +1189,14 @@ export class NetworkController extends BaseController< } /** - * Switches to the previously selected network, assuming that there is one - * (if not and `initializeProvider` has not been previously called, then this - * method is equivalent to calling `resetConnection`). + * Assuming that the network has been previously switched, switches to this + * new network. + * + * If the network has not been previously switched, this method is equivalent + * to {@link resetConnection}. */ async rollbackToPreviousProvider() { - this.#ensureAutoManagedNetworkClientRegistryPopulated(); - - this.update((state) => { - state.providerConfig = this.#previousProviderConfig; - }); - - await this.#refreshNetwork(); + await this.#refreshNetwork(this.#previouslySelectedNetworkClientId); } /** @@ -1441,7 +1270,6 @@ export class NetworkController extends BaseController< return [ ...this.#buildIdentifiedInfuraNetworkClientConfigurations(), ...this.#buildIdentifiedCustomNetworkClientConfigurations(), - ...this.#buildIdentifiedNetworkClientConfigurationsFromProviderConfig(), ].reduce( ( registry, @@ -1481,7 +1309,6 @@ export class NetworkController extends BaseController< InfuraNetworkClientConfiguration, ][] { return knownKeysOf(InfuraNetworkType).map((network) => { - const networkClientId = buildInfuraNetworkClientId(network); const networkClientConfiguration: InfuraNetworkClientConfiguration = { type: NetworkClientType.Infura, network, @@ -1489,11 +1316,7 @@ export class NetworkController extends BaseController< chainId: BUILT_IN_NETWORKS[network].chainId, ticker: BUILT_IN_NETWORKS[network].ticker, }; - return [ - NetworkClientType.Infura, - networkClientId, - networkClientConfiguration, - ]; + return [NetworkClientType.Infura, network, networkClientConfiguration]; }); } @@ -1516,9 +1339,7 @@ export class NetworkController extends BaseController< if (networkConfiguration.rpcUrl === undefined) { throw new Error('rpcUrl must be provided for custom RPC endpoints'); } - const networkClientId = buildCustomNetworkClientId( - networkConfigurationId, - ); + const networkClientId = networkConfigurationId; const networkClientConfiguration: CustomNetworkClientConfiguration = { type: NetworkClientType.Custom, chainId: networkConfiguration.chainId, @@ -1535,100 +1356,60 @@ export class NetworkController extends BaseController< } /** - * Converts the provider config object in state to a network client - * configuration object. + * Updates the global provider and block tracker proxies (accessible via + * {@link getSelectedNetworkClient}) to point to the same ones within the + * given network client, thereby magically switching any consumers using these + * proxies to use the new network. * - * @returns The network client config. - * @throws If the provider config is of type "rpc" and lacks either a - * `chainId` or an `rpcUrl`. - */ - #buildIdentifiedNetworkClientConfigurationsFromProviderConfig(): - | [ - [ - NetworkClientType.Custom, - CustomNetworkClientId, - CustomNetworkClientConfiguration, - ], - ] - | [] { - const { providerConfig } = this.state; - - if (isCustomProviderConfig(providerConfig)) { - validateCustomProviderConfig(providerConfig); - const networkClientId = buildCustomNetworkClientId( - providerConfig, - this.state.networkConfigurations, - ); - const networkClientConfiguration: CustomNetworkClientConfiguration = { - chainId: providerConfig.chainId, - rpcUrl: providerConfig.rpcUrl, - type: NetworkClientType.Custom, - ticker: providerConfig.ticker, - }; - return [ - [NetworkClientType.Custom, networkClientId, networkClientConfiguration], - ]; - } - - if (isInfuraProviderConfig(providerConfig)) { - return []; - } - - throw new Error(`Unrecognized network type: '${providerConfig.type}'`); - } - - /** - * Uses the information in the provider config object to look up a known and - * preinitialized network client. Once a network client is found, updates the - * provider and block tracker proxy to point to those from the network client, - * then finally creates an EthQuery that points to the provider proxy. + * Also refreshes the EthQuery instance accessible via the `getEthQuery` + * action to wrap the provider from the new network client. Note that this is + * not a proxy, so consumers will need to call `getEthQuery` again after the + * network switch. * - * @throws If no network client could be found matching the current provider - * config. + * @param networkClientId - The ID of a network client that requests will be + * routed through (either the name of an Infura network or the ID of a custom + * network configuration). + * @throws if no network client could be found matching the given ID. */ - #applyNetworkSelection() { - if (!this.#autoManagedNetworkClientRegistry) { - throw new Error( - 'initializeProvider must be called first in order to switch the network', - ); - } + #applyNetworkSelection(networkClientId: string) { + const autoManagedNetworkClientRegistry = + this.#ensureAutoManagedNetworkClientRegistryPopulated(); - const { providerConfig } = this.state; + let autoManagedNetworkClient: + | AutoManagedNetworkClient + | AutoManagedNetworkClient; - let autoManagedNetworkClient: AutoManagedNetworkClient; + if (isInfuraNetworkType(networkClientId)) { + const possibleAutoManagedNetworkClient = + autoManagedNetworkClientRegistry[NetworkClientType.Infura][ + networkClientId + ]; - let networkClientId: NetworkClientId; - if (isInfuraProviderConfig(providerConfig)) { - const networkClientType = NetworkClientType.Infura; - networkClientId = buildInfuraNetworkClientId(providerConfig); - const builtInNetworkClientRegistry = - this.#autoManagedNetworkClientRegistry[networkClientType]; - autoManagedNetworkClient = - builtInNetworkClientRegistry[networkClientId as BuiltInNetworkClientId]; - if (!autoManagedNetworkClient) { + if (!possibleAutoManagedNetworkClient) { + // This shouldn't happen, but is here just in case throw new Error( - `Could not find custom network matching ${networkClientId}`, + `Infura network client not found with ID '${networkClientId}'`, ); } - } else if (isCustomProviderConfig(providerConfig)) { - validateCustomProviderConfig(providerConfig); - const networkClientType = NetworkClientType.Custom; - networkClientId = buildCustomNetworkClientId( - providerConfig, - this.state.networkConfigurations, - ); - const customNetworkClientRegistry = - this.#autoManagedNetworkClientRegistry[networkClientType]; - autoManagedNetworkClient = customNetworkClientRegistry[networkClientId]; - if (!autoManagedNetworkClient) { + + autoManagedNetworkClient = possibleAutoManagedNetworkClient; + } else { + const possibleAutoManagedNetworkClient = + autoManagedNetworkClientRegistry[NetworkClientType.Custom][ + networkClientId + ]; + + if (!possibleAutoManagedNetworkClient) { throw new Error( - `Could not find built-in network matching ${networkClientId}`, + `Custom network client not found with ID '${networkClientId}'`, ); } - } else { - throw new Error('Could not determine type of provider config'); + + autoManagedNetworkClient = possibleAutoManagedNetworkClient; } + this.#autoManagedNetworkClient = autoManagedNetworkClient; + this.update((state) => { state.selectedNetworkClientId = networkClientId; if (state.networksMetadata[networkClientId] === undefined) { @@ -1639,20 +1420,23 @@ export class NetworkController extends BaseController< } }); - const { provider, blockTracker } = autoManagedNetworkClient; - if (this.#providerProxy) { - this.#providerProxy.setTarget(provider); + this.#providerProxy.setTarget(this.#autoManagedNetworkClient.provider); } else { - this.#providerProxy = createEventEmitterProxy(provider); + this.#providerProxy = createEventEmitterProxy( + this.#autoManagedNetworkClient.provider, + ); } if (this.#blockTrackerProxy) { - this.#blockTrackerProxy.setTarget(blockTracker); + this.#blockTrackerProxy.setTarget( + this.#autoManagedNetworkClient.blockTracker, + ); } else { - this.#blockTrackerProxy = createEventEmitterProxy(blockTracker, { - eventFilter: 'skipInternal', - }); + this.#blockTrackerProxy = createEventEmitterProxy( + this.#autoManagedNetworkClient.blockTracker, + { eventFilter: 'skipInternal' }, + ); } this.#ethQuery = new EthQuery(this.#providerProxy); diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 18e3476d7b..46e963e7af 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1,8 +1,8 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS, - ChainId, InfuraNetworkType, + isInfuraNetworkType, MAX_SAFE_CHAIN_ID, NetworkType, toHex, @@ -29,11 +29,14 @@ import type { NetworkControllerOptions, NetworkControllerStateChangeEvent, NetworkState, - ProviderConfig, } from '../src/NetworkController'; import { NetworkController } from '../src/NetworkController'; -import type { Provider } from '../src/types'; +import type { NetworkClientConfiguration, Provider } from '../src/types'; import { NetworkClientType } from '../src/types'; +import { + buildCustomNetworkClientConfiguration, + buildInfuraNetworkClientConfiguration, +} from './helpers'; jest.mock('../src/create-network-client'); @@ -183,11 +186,6 @@ describe('NetworkController', () => { Object { "networkConfigurations": Object {}, "networksMetadata": Object {}, - "providerConfig": Object { - "chainId": "0x1", - "ticker": "ETH", - "type": "mainnet", - }, "selectedNetworkClientId": "mainnet", } `); @@ -198,13 +196,6 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999' as const, - nickname: 'Test initial state', - ticker: 'TEST', - }, networksMetadata: { mainnet: { EIPS: { 1559: true }, @@ -225,13 +216,6 @@ describe('NetworkController', () => { "status": "unknown", }, }, - "providerConfig": Object { - "chainId": "0x9999", - "nickname": "Test initial state", - "rpcUrl": "http://example-custom-rpc.metamask.io", - "ticker": "TEST", - "type": "rpc", - }, "selectedNetworkClientId": "mainnet", } `); @@ -269,34 +253,13 @@ describe('NetworkController', () => { }); describe('initializeProvider', () => { - describe('when the type in the provider config is invalid', () => { - it('throws', async () => { - const invalidProviderConfig = {}; - await withController( - /* @ts-expect-error We're intentionally passing bad input. */ - { - state: { - providerConfig: invalidProviderConfig, - }, - }, - async ({ controller }) => { - await expect(async () => { - await controller.initializeProvider(); - }).rejects.toThrow("Unrecognized network type: 'undefined'"); - }, - ); - }); - }); - for (const { networkType } of INFURA_NETWORKS) { - describe(`when the type in the provider config is "${networkType}"`, () => { - it(`does not create another network client for the ${networkType} Infura network, since it is built in`, async () => { + describe(`when selectedNetworkClientId in state is the Infura network "${networkType}"`, () => { + it(`does not create another network client for the "${networkType}" network, since it is built in`, async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), + selectedNetworkClientId: networkType, }, infuraProjectId: 'some-infura-project-id', }, @@ -322,9 +285,7 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), + selectedNetworkClientId: networkType, }, infuraProjectId: 'some-infura-project-id', }, @@ -362,9 +323,10 @@ describe('NetworkController', () => { }); lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), + expectedNetworkClientConfiguration: + buildInfuraNetworkClientConfiguration(networkType), initialState: { - providerConfig: buildProviderConfig({ type: networkType }), + selectedNetworkClientId: networkType, }, operation: async (controller: NetworkController) => { await controller.initializeProvider(); @@ -373,1148 +335,469 @@ describe('NetworkController', () => { }); } - describe('when the type in the provider config is "rpc"', () => { - describe('if the provider config points to a network configuration', () => { - it('creates a network client for the custom RPC endpoint described by the network configuration, not the provider config', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://test.network.2', + describe('when selectedNetworkClientId in state is the ID of a network configuration', () => { + it('creates a network client using the network configuration', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://test.network.1', + chainId: toHex(1337), ticker: 'TEST', }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network.1', - chainId: toHex(1), - ticker: 'TEST', - }, - }, }, }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider([ + { + request: { + method: 'test_method', + params: [], }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + response: { + result: 'test response', + }, + }, + ]); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); + await controller.initializeProvider(); - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: toHex(1), - rpcUrl: 'https://test.network.1', - type: NetworkClientType.Custom, - ticker: 'TEST', - }); - expect(createNetworkClientMock).toHaveBeenCalledTimes(1); - }, - ); - }); + expect(createNetworkClientMock).toHaveBeenCalledWith({ + chainId: toHex(1337), + rpcUrl: 'https://test.network.1', + type: NetworkClientType.Custom, + ticker: 'TEST', + }); + expect(createNetworkClientMock).toHaveBeenCalledTimes(1); + }, + ); + }); - it('captures the resulting provider of the new network client', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://test.network.2', + it('captures the resulting provider of the new network client', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://test.network.1', + chainId: toHex(1337), ticker: 'TEST', }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network.1', - chainId: toHex(1), - ticker: 'TEST', - }, - }, }, }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await controller.initializeProvider(); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const { result } = await promisify(provider.sendAsync).call( - provider, - { - id: 1, - jsonrpc: '2.0', + }, + async ({ controller }) => { + const fakeProvider = buildFakeProvider([ + { + request: { method: 'test_method', params: [], }, - ); - expect(result).toBe('test response'); - }, - ); - }); - }); - - describe('if the provider config does not point to a network configuration, but matches one based on RPC URL (exactly)', () => { - it('creates a network client for the custom RPC endpoint described by the network configuration, not the provider config', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://test.network', - ticker: 'TEST', - }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network', - chainId: toHex(1), - ticker: 'TEST', - }, + response: { + result: 'test response', }, }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + ]); + const fakeNetworkClient = buildFakeClient(fakeProvider); + createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); + await controller.initializeProvider(); - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: toHex(1), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - ticker: 'TEST', - }); - expect(createNetworkClientMock).toHaveBeenCalledTimes(1); - }, - ); - }); + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is not set'); + const { result } = await promisify(provider.sendAsync).call( + provider, + { + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }, + ); + expect(result).toBe('test response'); + }, + ); + }); + }); + }); + + describe('getProviderAndBlockTracker', () => { + it('returns objects that proxy to the provider and block tracker as long as the provider has been initialized', async () => { + await withController(async ({ controller }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.initializeProvider(); + + const { provider, blockTracker } = + controller.getProviderAndBlockTracker(); + + expect(provider).toHaveProperty('sendAsync'); + expect(blockTracker).toHaveProperty('checkForLatestBlock'); + }); + }); + + it("returns undefined for both the provider and block tracker if the provider hasn't been initialized yet", async () => { + await withController(async ({ controller }) => { + const { provider, blockTracker } = + controller.getProviderAndBlockTracker(); + + expect(provider).toBeUndefined(); + expect(blockTracker).toBeUndefined(); + }); + }); - it('captures the resulting provider of the new network client', async () => { + for (const { networkType } of INFURA_NETWORKS) { + describe(`when the selectedNetworkClientId is changed to "${networkType}"`, () => { + it(`returns a provider object that was pointed to another network before the switch and is pointed to "${networkType}" afterward`, async () => { await withController( { state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://test.network', - ticker: 'TEST', - }, + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', networkConfigurations: { 'AAAA-AAAA-AAAA-AAAA': { id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network', - chainId: toHex(1), + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), ticker: 'TEST', }, }, }, + infuraProjectId: 'some-infura-project-id', }, async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'test', + }, + response: { + result: 'test response 1', + }, }, - response: { - result: 'test response', + ]), + buildFakeProvider([ + { + request: { + method: 'test', + }, + response: { + result: 'test response 2', + }, }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + chainId: toHex(1337), + rpcUrl: 'https://mock-rpc-url', + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + }) + .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const { result } = await promisify(provider.sendAsync).call( + assert(provider, 'Provider is somehow unset'); + + const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( provider, - { - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }, ); - expect(result).toBe('test response'); + const response1 = await promisifiedSendAsync1({ + id: '1', + jsonrpc: '2.0', + method: 'test', + }); + expect(response1.result).toBe('test response 1'); + + await controller.setProviderType(networkType); + const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( + provider, + ); + const response2 = await promisifiedSendAsync2({ + id: '2', + jsonrpc: '2.0', + method: 'test', + }); + expect(response2.result).toBe('test response 2'); }, ); }); }); + } - describe('if the provider config does not point to a network configuration, but matches one based on RPC URL (case-insensitively)', () => { - it('creates a network client for the custom RPC endpoint described by the network configuration, not the provider config', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'HTTPS://TEST.NETWORK', - ticker: 'TEST', - }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network', - chainId: toHex(1), - ticker: 'TEST', - }, + describe(`when the selectedNetworkClientId is changed to a network configuration ID`, () => { + it('returns a provider object that was pointed to another network before the switch and is pointed to the new network', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'goerli', + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'ABC', + id: 'testNetworkConfigurationId', }, }, }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider([ { request: { - method: 'test_method', - params: [], + method: 'test', }, response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await controller.initializeProvider(); - - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: toHex(1), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - ticker: 'TEST', - }); - expect(createNetworkClientMock).toHaveBeenCalledTimes(1); - }, - ); - }); - - it('captures the resulting provider of the new network client', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'HTTPS://TEST.NETWORK', - ticker: 'TEST', - }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network', - chainId: toHex(1), - ticker: 'TEST', + result: 'test response 1', }, }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ + ]), + buildFakeProvider([ { request: { - method: 'test_method', - params: [], + method: 'test', }, response: { - result: 'test response', + result: 'test response 2', }, }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: NetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + chainId: toHex(1337), + rpcUrl: 'https://mock-rpc-url', + type: NetworkClientType.Custom, + ticker: 'ABC', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.initializeProvider(); + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); - await controller.initializeProvider(); + const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( + provider, + ); + const response1 = await promisifiedSendAsync1({ + id: '1', + jsonrpc: '2.0', + method: 'test', + }); + expect(response1.result).toBe('test response 1'); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const { result } = await promisify(provider.sendAsync).call( - provider, - { - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }, - ); - expect(result).toBe('test response'); - }, - ); - }); + await controller.setActiveNetwork('testNetworkConfigurationId'); + const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( + provider, + ); + const response2 = await promisifiedSendAsync2({ + id: '2', + jsonrpc: '2.0', + method: 'test', + }); + expect(response2.result).toBe('test response 2'); + }, + ); }); + }); + }); - describe('if the provider config does not point to or match a network configuration', () => { - describe('if the provider config has a chain ID and RPC URL', () => { - it('creates a network client for a custom RPC endpoint using the provider config', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://example.com', - ticker: 'TEST', - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + describe('findNetworkConfigurationByChainId', () => { + it('returns the network configuration for the given chainId', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); + const networkClientId = + controller.findNetworkClientIdByChainId('0x1'); + expect(networkClientId).toBe('mainnet'); + }, + ); + }); - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: toHex(1337), - rpcUrl: 'http://example.com', - type: NetworkClientType.Custom, - ticker: 'TEST', - }); - expect(createNetworkClientMock).toHaveBeenCalledTimes(1); - }, - ); - }); + it('throws if the chainId doesnt exist in the configuration', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + expect(() => + controller.findNetworkClientIdByChainId('0xdeadbeef'), + ).toThrow("Couldn't find networkClientId for chainId"); + }, + ); + }); - it('captures the resulting provider of the new network client', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://example.com', - ticker: 'TEST', - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await controller.initializeProvider(); + it('is callable from the controller messenger', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ messenger }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const { result } = await promisify(provider.sendAsync).call( - provider, - { - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }, - ); - expect(result).toBe('test response'); - }, - ); - }); + const networkClientId = messenger.call( + 'NetworkController:findNetworkClientIdByChainId', + '0x1', + ); + expect(networkClientId).toBe('mainnet'); + }, + ); + }); + }); - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), - initialState: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), - }, - operation: async (controller: NetworkController) => { - await controller.initializeProvider(); - }, - }); - }); + describe('getNetworkClientById', () => { + describe('If passed an existing networkClientId', () => { + it('returns a valid built-in Infura NetworkClient', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - describe('if the chain ID is missing from the provider config', () => { - it('throws', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.initializeProvider(), - ).rejects.toThrow( - 'chainId must be provided for custom RPC endpoints', - ); - }, + const networkClientRegistry = controller.getNetworkClientRegistry(); + const networkClient = controller.getNetworkClientById( + NetworkType.mainnet, ); - }); - it('does not create a network client or capture a provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + expect(networkClient).toBe( + networkClientRegistry[NetworkType.mainnet], + ); + }, + ); + }); - try { - await controller.initializeProvider(); - } catch { - // ignore the error - } + it('returns a valid built-in Infura NetworkClient with a chainId in configuration', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); - }, + const networkClientRegistry = controller.getNetworkClientRegistry(); + const networkClient = controller.getNetworkClientById( + NetworkType.mainnet, ); - }); - }); - describe('if the RPC URL is missing from the provider config', () => { - it('throws', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.initializeProvider(), - ).rejects.toThrow( - 'rpcUrl must be provided for custom RPC endpoints', - ); - }, + expect(networkClient.configuration.chainId).toBe('0x1'); + expect(networkClientRegistry.mainnet.configuration.chainId).toBe( + '0x1', ); - }); + }, + ); + }); - it('does not create a network client or capture a provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: undefined, - }), + it('returns a valid custom NetworkClient', async () => { + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'ABC', + id: 'testNetworkConfigurationId', }, }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - try { - await controller.initializeProvider(); - } catch { - // ignore the error - } + const networkClientRegistry = controller.getNetworkClientRegistry(); + const networkClient = controller.getNetworkClientById( + 'testNetworkConfigurationId', + ); - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); - }, + expect(networkClient).toBe( + networkClientRegistry.testNetworkConfigurationId, ); - }); - }); + }, + ); }); }); - }); - - describe('getProviderAndBlockTracker', () => { - it('returns objects that proxy to the provider and block tracker as long as the provider has been initialized', async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); + describe('If passed a networkClientId that does not match a NetworkClient in the registry', () => { + it('throws an error', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - expect(provider).toHaveProperty('sendAsync'); - expect(blockTracker).toHaveProperty('checkForLatestBlock'); + expect(() => + controller.getNetworkClientById('non-existent-network-id'), + ).toThrow( + 'No custom network client was found with the ID "non-existent-network-id', + ); + }, + ); }); }); - it("returns undefined for both the provider and block tracker if the provider hasn't been initialized yet", async () => { - await withController(async ({ controller }) => { - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); + describe('If not passed a networkClientId', () => { + it('throws an error', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); + expect(() => + // @ts-expect-error Intentionally passing invalid type + controller.getNetworkClientById(), + ).toThrow('No network client ID was provided.'); + }, + ); }); }); + }); - for (const { networkType } of INFURA_NETWORKS) { - describe(`when the type in the provider configuration is changed to "${networkType}"`, () => { - it(`returns a provider object that was pointed to another network before the switch and is pointed to "${networkType}" afterward`, async () => { - await withController( - { - state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 1', - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - - const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( - provider, - ); - const response1 = await promisifiedSendAsync1({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response1.result).toBe('test response 1'); - - await controller.setProviderType(networkType); - const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( - provider, - ); - const response2 = await promisifiedSendAsync2({ - id: '2', - jsonrpc: '2.0', - method: 'test', - }); - expect(response2.result).toBe('test response 2'); - }, - ); - }); - }); - } - - describe('when the type in the provider configuration is changed to "rpc"', () => { - it('returns a provider object that was pointed to another network before the switch and is pointed to the new network', async () => { - await withController( - { - state: { - providerConfig: { - type: 'goerli', - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - ticker: 'TEST', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'ABC', - id: 'testNetworkConfigurationId', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 1', - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - ticker: 'ABC', - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - - const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( - provider, - ); - const response1 = await promisifiedSendAsync1({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response1.result).toBe('test response 1'); - - await controller.setActiveNetwork('testNetworkConfigurationId'); - const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( - provider, - ); - const response2 = await promisifiedSendAsync2({ - id: '2', - jsonrpc: '2.0', - method: 'test', - }); - expect(response2.result).toBe('test response 2'); - }, - ); - }); - }); - }); - - describe('findNetworkConfigurationByChainId', () => { - it('returns the network configuration for the given chainId', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClientId = - controller.findNetworkClientIdByChainId('0x1'); - expect(networkClientId).toBe('mainnet'); - }, - ); - }); - - it('throws if the chainId doesnt exist in the configuration', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - expect(() => - controller.findNetworkClientIdByChainId('0xdeadbeef'), - ).toThrow("Couldn't find networkClientId for chainId"); - }, - ); - }); - - it('is callable from the controller messenger', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ messenger }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClientId = messenger.call( - 'NetworkController:findNetworkClientIdByChainId', - '0x1', - ); - expect(networkClientId).toBe('mainnet'); - }, - ); - }); - }); - - describe('getNetworkClientById', () => { - describe('If passed an existing networkClientId', () => { - it('returns a valid built-in Infura NetworkClient', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClientRegistry = controller.getNetworkClientRegistry(); - const networkClient = controller.getNetworkClientById( - NetworkType.mainnet, - ); - - expect(networkClient).toBe( - networkClientRegistry[NetworkType.mainnet], - ); - }, - ); - }); - - it('returns a valid built-in Infura NetworkClient with a chainId in configuration', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClientRegistry = controller.getNetworkClientRegistry(); - const networkClient = controller.getNetworkClientById( - NetworkType.mainnet, - ); - - expect(networkClient.configuration.chainId).toBe('0x1'); - expect(networkClientRegistry.mainnet.configuration.chainId).toBe( - '0x1', - ); - }, - ); - }); - - it('returns a valid custom NetworkClient', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'ABC', - id: 'testNetworkConfigurationId', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClientRegistry = controller.getNetworkClientRegistry(); - const networkClient = controller.getNetworkClientById( - 'testNetworkConfigurationId', - ); - - expect(networkClient).toBe( - networkClientRegistry.testNetworkConfigurationId, - ); - }, - ); - }); - }); - - describe('If passed a networkClientId that does not match a NetworkClient in the registry', () => { - it('throws an error', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - expect(() => - controller.getNetworkClientById('non-existent-network-id'), - ).toThrow( - 'No custom network client was found with the ID "non-existent-network-id', - ); - }, - ); - }); - }); - - describe('If not passed a networkClientId', () => { - it('throws an error', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - expect(() => - // @ts-expect-error Intentionally passing invalid type - controller.getNetworkClientById(), - ).toThrow('No network client ID was provided.'); - }, - ); - }); - }); - }); - - describe('getNetworkClientRegistry', () => { - describe('if neither a provider config nor network configurations are present in state', () => { - it('returns the built-in Infura networks by default', async () => { - await withController( - { infuraProjectId: 'some-infura-project-id' }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClients = controller.getNetworkClientRegistry(); - const simplifiedNetworkClients = Object.entries(networkClients) - .map( - ([networkClientId, networkClient]) => - [networkClientId, networkClient.configuration] as const, - ) - .sort( - ( - [networkClientId1, _networkClient1], - [networkClientId2, _networkClient2], - ) => { - return networkClientId1.localeCompare(networkClientId2); - }, - ); - - expect(simplifiedNetworkClients).toStrictEqual([ - [ - 'goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, - network: InfuraNetworkType.goerli, - }, - ], - [ - 'linea-goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[NetworkType['linea-goerli']].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType['linea-goerli']].ticker, - network: InfuraNetworkType['linea-goerli'], - }, - ], - [ - 'linea-mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].chainId, - ticker: - BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].ticker, - network: InfuraNetworkType['linea-mainnet'], - }, - ], - [ - 'linea-sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].chainId, - ticker: - BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].ticker, - network: InfuraNetworkType['linea-sepolia'], - }, - ], - [ - 'mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, - network: InfuraNetworkType.mainnet, - }, - ], - [ - 'sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.sepolia].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType.sepolia].ticker, - network: InfuraNetworkType.sepolia, - }, - ], - ]); - }, - ); - }); - }); - - describe('if network configurations are present in state', () => { - it('incorporates them into the list of network clients, using the network configuration ID for identification', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network.1', - chainId: toHex(1), - ticker: 'TEST1', - }, - 'BBBB-BBBB-BBBB-BBBB': { - id: 'BBBB-BBBB-BBBB-BBBB', - rpcUrl: 'https://test.network.2', - chainId: toHex(2), - ticker: 'TEST2', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClients = controller.getNetworkClientRegistry(); - const simplifiedNetworkClients = Object.entries(networkClients) - .map( - ([networkClientId, networkClient]) => - [networkClientId, networkClient.configuration] as const, - ) - .sort( - ( - [networkClientId1, _networkClient1], - [networkClientId2, _networkClient2], - ) => { - return networkClientId1.localeCompare(networkClientId2); - }, - ); - - expect(simplifiedNetworkClients).toStrictEqual([ - [ - 'AAAA-AAAA-AAAA-AAAA', - { - type: NetworkClientType.Custom, - ticker: 'TEST1', - chainId: toHex(1), - rpcUrl: 'https://test.network.1', - }, - ], - [ - 'BBBB-BBBB-BBBB-BBBB', - { - type: NetworkClientType.Custom, - ticker: 'TEST2', - chainId: toHex(2), - rpcUrl: 'https://test.network.2', - }, - ], - [ - 'goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - network: InfuraNetworkType.goerli, - }, - ], - [ - 'linea-goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[NetworkType['linea-goerli']].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType['linea-goerli']].ticker, - network: InfuraNetworkType['linea-goerli'], - }, - ], - [ - 'linea-mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].chainId, - ticker: - BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].ticker, - network: InfuraNetworkType['linea-mainnet'], - }, - ], - [ - 'linea-sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].chainId, - ticker: - BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].ticker, - network: InfuraNetworkType['linea-sepolia'], - }, - ], - [ - 'mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, - network: InfuraNetworkType.mainnet, - }, - ], - [ - 'sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[NetworkType.sepolia].chainId, - ticker: BUILT_IN_NETWORKS[NetworkType.sepolia].ticker, - network: InfuraNetworkType.sepolia, - }, - ], - ]); - for (const networkClient of Object.values(networkClients)) { - expect(networkClient.provider).toHaveProperty('sendAsync'); - expect(networkClient.blockTracker).toHaveProperty( - 'checkForLatestBlock', - ); - } - }, - ); - }); - }); - - describe('if a provider config representing a built-in network is present in state', () => { - it('does not incorporate the network into the list of network clients since it is already present', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.mainnet, - chainId: ChainId.mainnet, - ticker: 'TEST', - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + describe('getNetworkClientRegistry', () => { + describe('if no network configurations are present in state', () => { + it('returns the built-in Infura networks by default', async () => { + await withController( + { infuraProjectId: 'some-infura-project-id' }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); const networkClients = controller.getNetworkClientRegistry(); const simplifiedNetworkClients = Object.entries(networkClients) @@ -1537,8 +820,8 @@ describe('NetworkController', () => { { type: NetworkClientType.Infura, infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, network: InfuraNetworkType.goerli, }, ], @@ -1548,10 +831,8 @@ describe('NetworkController', () => { type: NetworkClientType.Infura, infuraProjectId: 'some-infura-project-id', chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']].ticker, + BUILT_IN_NETWORKS[NetworkType['linea-goerli']].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType['linea-goerli']].ticker, network: InfuraNetworkType['linea-goerli'], }, ], @@ -1561,11 +842,9 @@ describe('NetworkController', () => { type: NetworkClientType.Infura, infuraProjectId: 'some-infura-project-id', chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .chainId, + BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].chainId, ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .ticker, + BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].ticker, network: InfuraNetworkType['linea-mainnet'], }, ], @@ -1575,11 +854,9 @@ describe('NetworkController', () => { type: NetworkClientType.Infura, infuraProjectId: 'some-infura-project-id', chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .chainId, + BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].chainId, ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .ticker, + BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].ticker, network: InfuraNetworkType['linea-sepolia'], }, ], @@ -1588,8 +865,8 @@ describe('NetworkController', () => { { type: NetworkClientType.Infura, infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].ticker, + chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, network: InfuraNetworkType.mainnet, }, ], @@ -1598,8 +875,8 @@ describe('NetworkController', () => { { type: NetworkClientType.Infura, infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].ticker, + chainId: BUILT_IN_NETWORKS[NetworkType.sepolia].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.sepolia].ticker, network: InfuraNetworkType.sepolia, }, ], @@ -1609,554 +886,140 @@ describe('NetworkController', () => { }); }); - describe('if a provider config representing a custom network is present in state', () => { - describe('if it does not point to a network configuration', () => { - describe("if it does not match an existing network configuration's RPC URL", () => { - it('incorporates the network into the list of network clients, using the chain ID and lowercased RPC URL for identification', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'HTTPS://TEST.NETWORK.2', - ticker: 'TEST', - }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network.1', - chainId: toHex(1), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClients = controller.getNetworkClientRegistry(); - const simplifiedNetworkClients = Object.entries(networkClients) - .map( - ([networkClientId, networkClient]) => - [networkClientId, networkClient.configuration] as const, - ) - .sort( - ( - [networkClientId1, _networkClient1], - [networkClientId2, _networkClient2], - ) => { - return networkClientId1.localeCompare(networkClientId2); - }, - ); - - expect(simplifiedNetworkClients).toStrictEqual([ - [ - 'AAAA-AAAA-AAAA-AAAA', - { - type: NetworkClientType.Custom, - ticker: 'TEST', - chainId: toHex(1), - rpcUrl: 'https://test.network.1', - }, - ], - [ - 'goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - network: InfuraNetworkType.goerli, - }, - ], - [ - 'https://test.network.2', - { - type: NetworkClientType.Custom, - ticker: 'TEST', - chainId: toHex(2), - rpcUrl: 'HTTPS://TEST.NETWORK.2', - }, - ], - [ - 'linea-goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .ticker, - network: InfuraNetworkType['linea-goerli'], - }, - ], - [ - 'linea-mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .ticker, - network: InfuraNetworkType['linea-mainnet'], - }, - ], - [ - 'linea-sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .ticker, - network: InfuraNetworkType['linea-sepolia'], - }, - ], - [ - 'mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].ticker, - network: InfuraNetworkType.mainnet, - }, - ], - [ - 'sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].ticker, - network: InfuraNetworkType.sepolia, - }, - ], - ]); - }, - ); - }); - }); - - describe("if it matches an existing network configuration's RPC URL exactly", () => { - it('does not incorporate the network into the list of network clients again, prioritizing the network configuration instead', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://test.network', - ticker: 'TEST', - }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network', - chainId: toHex(1), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClients = controller.getNetworkClientRegistry(); - const simplifiedNetworkClients = Object.entries(networkClients) - .map( - ([networkClientId, networkClient]) => - [networkClientId, networkClient.configuration] as const, - ) - .sort( - ( - [networkClientId1, _networkClient1], - [networkClientId2, _networkClient2], - ) => { - return networkClientId1.localeCompare(networkClientId2); - }, - ); - - expect(simplifiedNetworkClients).toStrictEqual([ - [ - 'AAAA-AAAA-AAAA-AAAA', - { - type: NetworkClientType.Custom, - ticker: 'TEST', - chainId: '0x1', - rpcUrl: 'https://test.network', - }, - ], - [ - 'goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - network: InfuraNetworkType.goerli, - }, - ], - [ - 'linea-goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .ticker, - network: InfuraNetworkType['linea-goerli'], - }, - ], - [ - 'linea-mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .ticker, - network: InfuraNetworkType['linea-mainnet'], - }, - ], - [ - 'linea-sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .ticker, - network: InfuraNetworkType['linea-sepolia'], - }, - ], - [ - 'mainnet', - { - type: NetworkClientType.Infura, - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].ticker, - infuraProjectId: 'some-infura-project-id', - network: InfuraNetworkType.mainnet, - }, - ], - [ - 'sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].ticker, - network: InfuraNetworkType.sepolia, - }, - ], - ]); - }, - ); - }); - }); - - describe("if it matches an existing network configuration's RPC URL case-insensitively", () => { - it('does not incorporate the network into the list of network clients again, prioritizing the network configuration instead', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://TEST.NETWORK', - ticker: 'TEST', - }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network', - chainId: toHex(1), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkClients = controller.getNetworkClientRegistry(); - const simplifiedNetworkClients = Object.entries(networkClients) - .map( - ([networkClientId, networkClient]) => - [networkClientId, networkClient.configuration] as const, - ) - .sort( - ( - [networkClientId1, _networkClient1], - [networkClientId2, _networkClient2], - ) => { - return networkClientId1.localeCompare(networkClientId2); - }, - ); - - expect(simplifiedNetworkClients).toStrictEqual([ - [ - 'AAAA-AAAA-AAAA-AAAA', - { - type: NetworkClientType.Custom, - ticker: 'TEST', - chainId: toHex(1), - rpcUrl: 'https://test.network', - }, - ], - [ - 'goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - network: InfuraNetworkType.goerli, - }, - ], - [ - 'linea-goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .ticker, - network: InfuraNetworkType['linea-goerli'], - }, - ], - [ - 'linea-mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .ticker, - network: InfuraNetworkType['linea-mainnet'], - }, - ], - [ - 'linea-sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .ticker, - network: InfuraNetworkType['linea-sepolia'], - }, - ], - [ - 'mainnet', - { - type: NetworkClientType.Infura, - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].ticker, - infuraProjectId: 'some-infura-project-id', - network: InfuraNetworkType.mainnet, - }, - ], - [ - 'sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].ticker, - network: InfuraNetworkType.sepolia, - }, - ], - ]); - }, - ); - }); - }); - }); - - describe('if it points to a network configuration', () => { - it('does not incorporate the network into the list of network clients again, prioritizing the network configuration', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(2), - rpcUrl: 'https://test.network.2', + describe('if network configurations are present in state', () => { + it('incorporates them into the list of network clients, using the network configuration ID for identification', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { id: 'AAAA-AAAA-AAAA-AAAA', - ticker: 'TEST', + rpcUrl: 'https://test.network.1', + chainId: toHex(1), + ticker: 'TEST1', }, - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - id: 'AAAA-AAAA-AAAA-AAAA', - rpcUrl: 'https://test.network.1', - chainId: toHex(1), - ticker: 'TEST', - }, + 'BBBB-BBBB-BBBB-BBBB': { + id: 'BBBB-BBBB-BBBB-BBBB', + rpcUrl: 'https://test.network.2', + chainId: toHex(2), + ticker: 'TEST2', }, }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller }) => { - const fakeNetworkClient = buildFakeClient(); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeNetworkClient = buildFakeClient(); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - const networkClients = controller.getNetworkClientRegistry(); - const simplifiedNetworkClients = Object.entries(networkClients) - .map( - ([networkClientId, networkClient]) => - [networkClientId, networkClient.configuration] as const, - ) - .sort( - ( - [networkClientId1, _networkClient1], - [networkClientId2, _networkClient2], - ) => { - return networkClientId1.localeCompare(networkClientId2); - }, - ); + const networkClients = controller.getNetworkClientRegistry(); + const simplifiedNetworkClients = Object.entries(networkClients) + .map( + ([networkClientId, networkClient]) => + [networkClientId, networkClient.configuration] as const, + ) + .sort( + ( + [networkClientId1, _networkClient1], + [networkClientId2, _networkClient2], + ) => { + return networkClientId1.localeCompare(networkClientId2); + }, + ); - expect(simplifiedNetworkClients).toStrictEqual([ - [ - 'AAAA-AAAA-AAAA-AAAA', - { - type: NetworkClientType.Custom, - ticker: 'TEST', - chainId: toHex(1), - rpcUrl: 'https://test.network.1', - }, - ], - [ - 'goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - network: InfuraNetworkType.goerli, - }, - ], - [ - 'linea-goerli', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-goerli']] - .ticker, - network: InfuraNetworkType['linea-goerli'], - }, - ], - [ - 'linea-mainnet', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-mainnet']] - .ticker, - network: InfuraNetworkType['linea-mainnet'], - }, - ], - [ - 'linea-sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .chainId, - ticker: - BUILT_IN_NETWORKS[InfuraNetworkType['linea-sepolia']] - .ticker, - network: InfuraNetworkType['linea-sepolia'], - }, - ], - [ - 'mainnet', - { - type: NetworkClientType.Infura, - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.mainnet].ticker, - infuraProjectId: 'some-infura-project-id', - network: InfuraNetworkType.mainnet, - }, - ], - [ - 'sepolia', - { - type: NetworkClientType.Infura, - infuraProjectId: 'some-infura-project-id', - chainId: - BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.sepolia].ticker, - network: InfuraNetworkType.sepolia, - }, - ], - ]); - }, - ); - }); + expect(simplifiedNetworkClients).toStrictEqual([ + [ + 'AAAA-AAAA-AAAA-AAAA', + { + type: NetworkClientType.Custom, + ticker: 'TEST1', + chainId: toHex(1), + rpcUrl: 'https://test.network.1', + }, + ], + [ + 'BBBB-BBBB-BBBB-BBBB', + { + type: NetworkClientType.Custom, + ticker: 'TEST2', + chainId: toHex(2), + rpcUrl: 'https://test.network.2', + }, + ], + [ + 'goerli', + { + type: NetworkClientType.Infura, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + network: InfuraNetworkType.goerli, + }, + ], + [ + 'linea-goerli', + { + type: NetworkClientType.Infura, + infuraProjectId: 'some-infura-project-id', + chainId: + BUILT_IN_NETWORKS[NetworkType['linea-goerli']].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType['linea-goerli']].ticker, + network: InfuraNetworkType['linea-goerli'], + }, + ], + [ + 'linea-mainnet', + { + type: NetworkClientType.Infura, + infuraProjectId: 'some-infura-project-id', + chainId: + BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].chainId, + ticker: + BUILT_IN_NETWORKS[NetworkType['linea-mainnet']].ticker, + network: InfuraNetworkType['linea-mainnet'], + }, + ], + [ + 'linea-sepolia', + { + type: NetworkClientType.Infura, + infuraProjectId: 'some-infura-project-id', + chainId: + BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].chainId, + ticker: + BUILT_IN_NETWORKS[NetworkType['linea-sepolia']].ticker, + network: InfuraNetworkType['linea-sepolia'], + }, + ], + [ + 'mainnet', + { + type: NetworkClientType.Infura, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, + network: InfuraNetworkType.mainnet, + }, + ], + [ + 'sepolia', + { + type: NetworkClientType.Infura, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[NetworkType.sepolia].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.sepolia].ticker, + network: InfuraNetworkType.sepolia, + }, + ], + ]); + for (const networkClient of Object.values(networkClients)) { + expect(networkClient.provider).toHaveProperty('sendAsync'); + expect(networkClient.blockTracker).toHaveProperty( + 'checkForLatestBlock', + ); + } + }, + ); }); }); }); @@ -2193,13 +1056,13 @@ describe('NetworkController', () => { [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( (networkType) => { - describe(`when the provider config in state contains a network type of "${networkType}"`, () => { + describe(`when selectedNetworkClientId in state is "${networkType}"`, () => { describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { it('stores the network status of the second network, not the first', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ type: networkType }), + selectedNetworkClientId: networkType, networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', @@ -2294,7 +1157,7 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: buildProviderConfig({ type: networkType }), + selectedNetworkClientId: networkType, networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', @@ -2394,7 +1257,7 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: buildProviderConfig({ type: networkType }), + selectedNetworkClientId: networkType, networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', @@ -2496,9 +1359,10 @@ describe('NetworkController', () => { }); lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), + expectedNetworkClientConfiguration: + buildInfuraNetworkClientConfiguration(networkType), initialState: { - providerConfig: buildProviderConfig({ type: networkType }), + selectedNetworkClientId: networkType, }, operation: async (controller) => { await controller.lookupNetwork(); @@ -2508,17 +1372,21 @@ describe('NetworkController', () => { }, ); - describe(`when the provider config in state contains a network type of "rpc"`, () => { + describe('when selectedNetworkClientId in state is the ID of a network configuration', () => { describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { it('stores the network status of the second network, not the first', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, infuraProjectId: 'some-infura-project-id', }, @@ -2575,8 +1443,7 @@ describe('NetworkController', () => { .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); expect( - controller.state.networksMetadata['https://mock-rpc-url'] - .status, + controller.state.networksMetadata['AAAA-AAAA-AAAA-AAAA'].status, ).toBe('available'); await waitForStateChanges({ @@ -2602,11 +1469,15 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, infuraProjectId: 'some-infura-project-id', }, @@ -2669,7 +1540,7 @@ describe('NetworkController', () => { .mockReturnValue(fakeNetworkClients[1]); await controller.initializeProvider(); expect( - controller.state.networksMetadata['https://mock-rpc-url'] + controller.state.networksMetadata['AAAA-AAAA-AAAA-AAAA'] .EIPS[1559], ).toBe(true); @@ -2686,7 +1557,7 @@ describe('NetworkController', () => { .EIPS[1559], ).toBe(false); expect( - controller.state.networksMetadata['https://mock-rpc-url'] + controller.state.networksMetadata['AAAA-AAAA-AAAA-AAAA'] .EIPS[1559], ).toBe(true); }, @@ -2697,11 +1568,15 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, infuraProjectId: 'some-infura-project-id', }, @@ -2788,9 +1663,18 @@ describe('NetworkController', () => { }); lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: NetworkType.rpc }), + expectedNetworkClientConfiguration: + buildCustomNetworkClientConfiguration(), initialState: { - providerConfig: buildProviderConfig({ type: NetworkType.rpc }), + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, operation: async (controller) => { await controller.lookupNetwork(); @@ -2800,35 +1684,28 @@ describe('NetworkController', () => { }); describe('setProviderType', () => { - for (const { - networkType, - chainId, - ticker, - blockExplorerUrl, - } of INFURA_NETWORKS) { - describe(`given a network type of "${networkType}"`, () => { + for (const { networkType } of INFURA_NETWORKS) { + describe(`given the Infura network "${networkType}"`, () => { refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: networkType, - }), + expectedNetworkClientConfiguration: + buildInfuraNetworkClientConfiguration(networkType), operation: async (controller) => { await controller.setProviderType(networkType); }, }); }); - it(`overwrites the provider configuration using a predetermined chainId, ticker, and blockExplorerUrl for "${networkType}", clearing id, rpcUrl, and nickname`, async () => { + it(`sets selectedNetworkClientId in state to the Infura network "${networkType}"`, async () => { await withController( { state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + id: 'AAAA-AAAA-AAAA-AAAA', }, }, }, @@ -2840,46 +1717,18 @@ describe('NetworkController', () => { await controller.setProviderType(networkType); - expect(controller.state.providerConfig).toStrictEqual({ - type: networkType, - rpcUrl: undefined, - chainId, - ticker, - nickname: undefined, - rpcPrefs: { blockExplorerUrl }, - id: undefined, - }); + expect(controller.state.selectedNetworkClientId).toBe(networkType); }, ); }); - - it(`updates state.selectedNetworkClientId, setting it to ${networkType}`, async () => { - await withController({}, async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await controller.setProviderType(networkType); - - expect(controller.state.selectedNetworkClientId).toStrictEqual( - networkType, - ); - }); - }); } - describe('given a network type of "rpc"', () => { + describe('given the ID of a network configuration', () => { it('throws because there is no way to switch to a custom RPC endpoint using this method', async () => { await withController( { state: { - providerConfig: { - type: NetworkType.rpc, - rpcUrl: 'http://somethingexisting.com', - chainId: toHex(99999), - ticker: 'something existing', - nickname: 'something existing', - }, + selectedNetworkClientId: 'mainnet', }, }, async ({ controller }) => { @@ -2981,15 +1830,13 @@ describe('NetworkController', () => { describe('setActiveNetwork', () => { refreshNetworkTests({ - expectedProviderConfig: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: undefined, - type: NetworkType.rpc, - }, + expectedNetworkClientConfiguration: buildCustomNetworkClientConfiguration( + { + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(111), + ticker: 'TEST', + }, + ), initialState: { networkConfigurations: { testNetworkConfigurationId: { @@ -2997,78 +1844,27 @@ describe('NetworkController', () => { chainId: toHex(111), ticker: 'TEST', nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: undefined, - }, - }, - }, - operation: async (controller) => { - await controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); - - describe('if the given ID does not match a network configuration in networkConfigurations or a built-in network type', () => { - it('throws', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('invalidNetworkConfigurationId'), - ).rejects.toThrow( - new Error( - 'networkConfigurationId invalidNetworkConfigurationId does not match a configured networkConfiguration or built-in network type', - ), - ); + id: 'testNetworkConfigurationId', + rpcPrefs: undefined, }, - ); - }); + }, + }, + operation: async (controller) => { + await controller.setActiveNetwork('testNetworkConfigurationId'); + }, }); - describe('if the network config does not contain an RPC URL', () => { + describe('if the given ID refers to no existing network clients (derived from known Infura networks and network configurations)', () => { it('throws', async () => { await withController( - // @ts-expect-error RPC URL intentionally omitted { state: { - providerConfig: { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - rpcPrefs: undefined, - }, networkConfigurations: { - testNetworkConfigurationId1: { + 'AAAA-AAAA-AAAA-AAAA': { rpcUrl: 'https://mock-rpc-url', chainId: toHex(111), ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId1', - rpcPrefs: undefined, - }, - testNetworkConfigurationId2: { - rpcUrl: undefined, - chainId: toHex(222), - ticker: 'something existing', - nickname: 'something existing', - id: 'testNetworkConfigurationId2', - rpcPrefs: undefined, + id: 'AAAA-AAAA-AAAA-AAAA', }, }, }, @@ -3076,54 +1872,32 @@ describe('NetworkController', () => { async ({ controller }) => { const fakeProvider = buildFakeProvider(); const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); await expect(() => - controller.setActiveNetwork('testNetworkConfigurationId2'), + controller.setActiveNetwork('invalidNetworkClientId'), ).rejects.toThrow( - 'rpcUrl must be provided for custom RPC endpoints', + new Error( + "Custom network client not found with ID 'invalidNetworkClientId'", + ), ); - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); }, ); }); }); - describe('if the network config does not contain a chain ID', () => { - it('throws', async () => { + describe('if the ID refers to a network client created for a network configuration', () => { + it('assigns selectedNetworkClientId in state to the ID', async () => { + const testNetworkClientId = 'AAAA-AAAA-AAAA-AAAA'; await withController( - // @ts-expect-error chain ID intentionally omitted { state: { - providerConfig: { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - rpcPrefs: undefined, - }, networkConfigurations: { - testNetworkConfigurationId1: { + [testNetworkClientId]: { rpcUrl: 'https://mock-rpc-url', chainId: toHex(111), ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId1', - rpcPrefs: undefined, - }, - testNetworkConfigurationId2: { - rpcUrl: 'http://somethingexisting.com', - chainId: undefined, - ticker: 'something existing', - nickname: 'something existing', - id: 'testNetworkConfigurationId2', - rpcPrefs: undefined, + id: testNetworkClientId, }, }, }, @@ -3131,175 +1905,47 @@ describe('NetworkController', () => { async ({ controller }) => { const fakeProvider = buildFakeProvider(); const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('testNetworkConfigurationId2'), - ).rejects.toThrow( - 'chainId must be provided for custom RPC endpoints', - ); - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeUndefined(); - expect(blockTracker).toBeUndefined(); - }, - ); - }); - }); - - it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { + mockCreateNetworkClient() + .calledWith({ rpcUrl: 'https://mock-rpc-url', chainId: toHex(111), + type: NetworkClientType.Custom, ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClient); - - await controller.setActiveNetwork('testNetworkConfigurationId'); + }) + .mockReturnValue(fakeNetworkClient); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }); - }, - ); - }); + await controller.setActiveNetwork(testNetworkClientId); - it('updates state.selectedNetworkClientId setting it to the networkConfiguration.id', async () => { - const testNetworkClientId = 'testNetworkConfigurationId'; - await withController( - { - state: { - networkConfigurations: { - [testNetworkClientId]: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: testNetworkClientId, - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }, - }, + expect(controller.state.selectedNetworkClientId).toStrictEqual( + testNetworkClientId, + ); }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClient); - - await controller.setActiveNetwork(testNetworkClientId); - - expect(controller.state.selectedNetworkClientId).toStrictEqual( - testNetworkClientId, - ); - }, - ); + ); + }); }); - for (const { - networkType, - chainId, - ticker, - blockExplorerUrl, - } of INFURA_NETWORKS) { - describe(`given a network type of "${networkType}"`, () => { + for (const { networkType } of INFURA_NETWORKS) { + describe(`if the ID refers to a network client created for the Infura network "${networkType}"`, () => { refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: networkType, - }), + expectedNetworkClientConfiguration: + buildInfuraNetworkClientConfiguration(networkType), operation: async (controller) => { await controller.setActiveNetwork(networkType); }, }); - }); - it(`overwrites the provider configuration using a predetermined chainId, ticker, and blockExplorerUrl for "${networkType}", clearing id, rpcUrl, and nickname`, async () => { - await withController( - { - state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - async ({ controller }) => { + it(`sets selectedNetworkClientId in state to "${networkType}"`, async () => { + await withController({}, async ({ controller }) => { const fakeProvider = buildFakeProvider(); const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); await controller.setActiveNetwork(networkType); - expect(controller.state.providerConfig).toStrictEqual({ - type: networkType, - rpcUrl: undefined, - chainId, - ticker, - nickname: undefined, - rpcPrefs: { blockExplorerUrl }, - id: undefined, - }); - }, - ); - }); - - it(`updates state.selectedNetworkClientId, setting it to ${networkType}`, async () => { - await withController({}, async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await controller.setActiveNetwork(networkType); - - expect(controller.state.selectedNetworkClientId).toStrictEqual( - networkType, - ); + expect(controller.state.selectedNetworkClientId).toStrictEqual( + networkType, + ); + }); }); }); } @@ -3704,11 +2350,12 @@ describe('NetworkController', () => { describe('resetConnection', () => { [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( (networkType) => { - describe(`when the type in the provider configuration is "${networkType}"`, () => { + describe(`when selectedNetworkClientId in state is the Infura network "${networkType}"`, () => { refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), + expectedNetworkClientConfiguration: + buildInfuraNetworkClientConfiguration(networkType), initialState: { - providerConfig: buildProviderConfig({ type: networkType }), + selectedNetworkClientId: networkType, }, operation: async (controller) => { await controller.resetConnection(); @@ -3718,11 +2365,24 @@ describe('NetworkController', () => { }, ); - describe(`when the type in the provider configuration is "rpc"`, () => { + describe('when selectedNetworkClientId in state is the ID of a network configuration', () => { refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: NetworkType.rpc }), + expectedNetworkClientConfiguration: + buildCustomNetworkClientConfiguration({ + rpcUrl: 'https://test.network.1', + chainId: toHex(1337), + ticker: 'TEST', + }), initialState: { - providerConfig: buildProviderConfig({ type: NetworkType.rpc }), + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://test.network.1', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, operation: async (controller) => { await controller.resetConnection(); @@ -3731,31 +2391,6 @@ describe('NetworkController', () => { }); }); - describe('NetworkController:getProviderConfig action', () => { - it('returns the provider config in state', async () => { - await withController( - { - state: { - providerConfig: { - type: NetworkType.mainnet, - ...BUILT_IN_NETWORKS.mainnet, - }, - }, - }, - async ({ messenger }) => { - const providerConfig = await messenger.call( - 'NetworkController:getProviderConfig', - ); - - expect(providerConfig).toStrictEqual({ - type: NetworkType.mainnet, - ...BUILT_IN_NETWORKS.mainnet, - }); - }, - ); - }); - }); - describe('NetworkController:getEthQuery action', () => { it('returns a EthQuery object that can be used to make requests to the currently selected network', async () => { await withController(async ({ controller, messenger }) => { @@ -4142,44 +2777,32 @@ describe('NetworkController', () => { }); describe('if the setActive option is not given', () => { - it('does not update the provider config to the new network configuration by default', async () => { - const originalProvider = { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TICKER', - id: 'testNetworkConfigurationId', - }; + it('does not update selectedNetworkClientId to refer to the new network configuration by default', async () => { + await withController(async ({ controller }) => { + const originalSelectedNetworkClientId = + controller.state.selectedNetworkClientId; - await withController( - { - state: { - providerConfig: originalProvider, - }, - }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); - expect(controller.state.providerConfig).toStrictEqual( - originalProvider, - ); - }, - ); + expect(controller.state.selectedNetworkClientId).toStrictEqual( + originalSelectedNetworkClientId, + ); + }); }); - it('does not set the new network to active by default', async () => { + it('does not re-point the provider and block tracker proxies to the new network by default', async () => { await withController( { infuraProjectId: 'some-infura-project-id' }, async ({ controller }) => { @@ -4256,45 +2879,33 @@ describe('NetworkController', () => { }); describe('if the setActive option is false', () => { - it('does not update the provider config to the new network configuration by default', async () => { - const originalProvider = { - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TICKER', - id: 'testNetworkConfigurationId', - }; + it('does not update selectedNetworkClientId to refer to the new network configuration', async () => { + await withController(async ({ controller }) => { + const originalSelectedNetworkClientId = + controller.state.selectedNetworkClientId; - await withController( - { - state: { - providerConfig: originalProvider, - }, - }, - async ({ controller }) => { - uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); + uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - }, - { - setActive: false, - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); + await controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test.network', + chainId: toHex(111), + ticker: 'TICKER', + }, + { + setActive: false, + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ); - expect(controller.state.providerConfig).toStrictEqual( - originalProvider, - ); - }, - ); + expect(controller.state.selectedNetworkClientId).toStrictEqual( + originalSelectedNetworkClientId, + ); + }); }); - it('does not set the new network to active by default', async () => { + it('does not re-point the provider and block tracker proxies to the new network', async () => { await withController( { infuraProjectId: 'some-infura-project-id' }, async ({ controller }) => { @@ -4372,7 +2983,7 @@ describe('NetworkController', () => { }); describe('if the setActive option is true', () => { - it('updates the provider config to the new network configuration', async () => { + it('updates selectedNetworkClientId to refer to the new network configuration', async () => { await withController(async ({ controller }) => { uuidV4Mock.mockReturnValue('AAAA-AAAA-AAAA-AAAA'); const newCustomNetworkClient = buildFakeClient(); @@ -4401,31 +3012,20 @@ describe('NetworkController', () => { source: 'dapp', }, ); - - expect(controller.state.providerConfig).toStrictEqual({ - type: NetworkType.rpc, - rpcUrl: 'https://test.network', - chainId: toHex(111), - ticker: 'TICKER', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://some.chainscan.io', - }, - id: 'AAAA-AAAA-AAAA-AAAA', - }); + + expect(controller.state.selectedNetworkClientId).toBe( + 'AAAA-AAAA-AAAA-AAAA', + ); }); }); refreshNetworkTests({ - expectedProviderConfig: { - type: NetworkType.rpc, - rpcUrl: 'https://some.other.network', - chainId: toHex(222), - ticker: 'TICKER2', - id: 'BBBB-BBBB-BBBB-BBBB', - nickname: undefined, - rpcPrefs: undefined, - }, + expectedNetworkClientConfiguration: + buildCustomNetworkClientConfiguration({ + rpcUrl: 'https://some.other.network', + chainId: toHex(222), + ticker: 'TICKER2', + }), initialState: { networkConfigurations: { 'AAAA-AAAA-AAAA-AAAA': { @@ -5079,755 +3679,170 @@ describe('NetworkController', () => { id: 'AAAA-AAAA-AAAA-AAAA', }, }, - }, - }, - async ({ controller }) => { - controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); - - expect(controller.state.networkConfigurations).toStrictEqual({}); - }, - ); - }); - - it('destroys and removes the network client in the network client registry that corresponds to the given ID', async () => { - await withController( - { - state: { - networkConfigurations: { - 'AAAA-AAAA-AAAA-AAAA': { - rpcUrl: 'https://test.network', - ticker: 'TICKER', - chainId: toHex(111), - id: 'AAAA-AAAA-AAAA-AAAA', - }, - }, - }, - }, - async ({ controller }) => { - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients() - .calledWith({ - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(buildFakeClient()); - const networkClientToDestroy = Object.values( - controller.getNetworkClientRegistry(), - ).find(({ configuration }) => { - return ( - configuration.type === NetworkClientType.Custom && - configuration.chainId === toHex(111) && - configuration.rpcUrl === 'https://test.network' - ); - }); - assert(networkClientToDestroy); - jest.spyOn(networkClientToDestroy, 'destroy'); - - controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); - - expect(networkClientToDestroy.destroy).toHaveBeenCalled(); - expect(controller.getNetworkClientRegistry()).not.toMatchObject({ - 'https://test.network': expect.objectContaining({ - configuration: { - chainId: toHex(111), - rpcUrl: 'https://test.network', - type: NetworkClientType.Custom, - ticker: 'TEST', - }, - }), - }); - }, - ); - }); - }); - - describe('given an ID that does not identify a network configuration in state', () => { - it('throws', async () => { - await withController(async ({ controller }) => { - expect(() => - controller.removeNetworkConfiguration('NONEXISTENT'), - ).toThrow( - `networkConfigurationId NONEXISTENT does not match a configured networkConfiguration`, - ); - }); - }); - - it('does not update the network client registry', async () => { - await withController(async ({ controller }) => { - mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients(); - const networkClients = controller.getNetworkClientRegistry(); - - try { - controller.removeNetworkConfiguration('NONEXISTENT'); - } catch { - // ignore error (it is tested elsewhere) - } - - expect(controller.getNetworkClientRegistry()).toStrictEqual( - networkClients, - ); - }); - }); - }); - }); - - describe('rollbackToPreviousProvider', () => { - describe('if a provider has not been set', () => { - [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( - (networkType) => { - describe(`when the type in the provider configuration is "${networkType}"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: networkType, - }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller) => { - await controller.rollbackToPreviousProvider(); - }, - }); - }); - }, - ); - - describe(`when the type in the provider configuration is "rpc"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), - initialState: { - providerConfig: buildProviderConfig({ type: NetworkType.rpc }), - }, - operation: async (controller) => { - await controller.rollbackToPreviousProvider(); - }, - }); - }); - }); - - describe('if a provider has been set', () => { - for (const { networkType } of INFURA_NETWORKS) { - describe(`if the previous provider configuration had a type of "${networkType}"`, () => { - it('emits networkWillChange with state payload', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setActiveNetwork('testNetworkConfiguration'); - - const networkWillChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - filter: ([networkState]) => networkState === controller.state, - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkWillChange).toBeFulfilled(); - }, - ); - }); - - it('emits networkDidChange with state payload', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setActiveNetwork('testNetworkConfiguration'); - - const networkDidChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - filter: ([networkState]) => networkState === controller.state, - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkDidChange).toBeFulfilled(); - }, - ); - }); - - it('overwrites the the current provider configuration with the previous provider configuration', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'rpc', - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }); - - await controller.rollbackToPreviousProvider(); - - expect(controller.state.providerConfig).toStrictEqual( - buildProviderConfig({ - type: networkType, - }), - ); - }, - ); - }); - - it('resets the network status to "unknown" before updating the provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); - - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', networkType, 'status'], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unknown'); - }, - }); - }, - ); - }); - - it(`initializes a provider pointed to the "${networkType}" Infura network`, async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - - await controller.rollbackToPreviousProvider(); + }, + }, + async ({ controller }) => { + controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const response = await promisifiedSendAsync({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response.result).toBe('test response'); - }, - ); - }); + expect(controller.state.networkConfigurations).toStrictEqual({}); + }, + ); + }); - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, + it('destroys and removes the network client in the network client registry that corresponds to the given ID', async () => { + await withController( + { + state: { + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + rpcUrl: 'https://test.network', + ticker: 'TICKER', + chainId: toHex(111), + id: 'AAAA-AAAA-AAAA-AAAA', }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); - - await controller.rollbackToPreviousProvider(); + }, + }, + async ({ controller }) => { + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients() + .calledWith({ + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(buildFakeClient()); + const networkClientToDestroy = Object.values( + controller.getNetworkClientRegistry(), + ).find(({ configuration }) => { + return ( + configuration.type === NetworkClientType.Custom && + configuration.chainId === toHex(111) && + configuration.rpcUrl === 'https://test.network' + ); + }); + assert(networkClientToDestroy); + jest.spyOn(networkClientToDestroy, 'destroy'); - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }, - ); - }); + controller.removeNetworkConfiguration('AAAA-AAAA-AAAA-AAAA'); - it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, + expect(networkClientToDestroy.destroy).toHaveBeenCalled(); + expect(controller.getNetworkClientRegistry()).not.toMatchObject({ + 'https://test.network': expect.objectContaining({ + configuration: { + chainId: toHex(111), + rpcUrl: 'https://test.network', + type: NetworkClientType.Custom, + ticker: 'TEST', }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); + }), + }); + }, + ); + }); + }); - await controller.rollbackToPreviousProvider(); + describe('given an ID that does not identify a network configuration in state', () => { + it('throws', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.removeNetworkConfiguration('NONEXISTENT'), + ).toThrow( + `networkConfigurationId NONEXISTENT does not match a configured networkConfiguration`, + ); + }); + }); - await expect( - promiseForNoInfuraIsUnblockedEvents, - ).toBeFulfilled(); - await expect(promiseForInfuraIsBlocked).toBeFulfilled(); - }, - ); - }); + it('does not update the network client registry', async () => { + await withController(async ({ controller }) => { + mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients(); + const networkClients = controller.getNetworkClientRegistry(); - it('checks the status of the previous network again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - error: rpcErrors.methodNotFound(), - }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unavailable'); + try { + controller.removeNetworkConfiguration('NONEXISTENT'); + } catch { + // ignore error (it is tested elsewhere) + } - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', networkType, 'status'], - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); - }, - ); - }); + expect(controller.getNetworkClientRegistry()).toStrictEqual( + networkClients, + ); + }); + }); + }); + }); - it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', + describe('rollbackToPreviousProvider', () => { + describe('when called not following any network switches', () => { + [NetworkType.mainnet, NetworkType.goerli, NetworkType.sepolia].forEach( + (networkType) => { + describe(`when selectedNetworkClientId in state is the Infura network "${networkType}"`, () => { + refreshNetworkTests({ + expectedNetworkClientConfiguration: + buildInfuraNetworkClientConfiguration(networkType), + initialState: { + selectedNetworkClientId: networkType, }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); - - await waitForStateChanges({ - messenger, - propertyPath: ['networksMetadata', networkType, 'EIPS'], - count: 2, - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); + operation: async (controller) => { + await controller.rollbackToPreviousProvider(); }, - ); + }); }); + }, + ); + + describe('when selectedNetworkClientId in state is the ID of a network configuration', () => { + refreshNetworkTests({ + expectedNetworkClientConfiguration: + buildCustomNetworkClientConfiguration({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }), + initialState: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurations: { + 'AAAA-AAAA-AAAA-AAAA': { + id: 'AAAA-AAAA-AAAA-AAAA', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + operation: async (controller) => { + await controller.rollbackToPreviousProvider(); + }, }); - } + }); + }); - describe(`if the previous provider configuration had a type of "rpc"`, () => { + for (const { networkType } of INFURA_NETWORKS) { + describe(`when called following a network switch away from the Infura network "${networkType}"`, () => { it('emits networkWillChange with state payload', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }, + }, }, }, async ({ controller, messenger }) => { const fakeProvider = buildFakeProvider(); const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setProviderType(InfuraNetworkType.goerli); + await controller.setActiveNetwork('testNetworkConfiguration'); const networkWillChange = waitForPublishedEvents({ messenger, @@ -5849,16 +3864,26 @@ describe('NetworkController', () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - }), + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }, + }, }, }, async ({ controller, messenger }) => { const fakeProvider = buildFakeProvider(); const fakeNetworkClient = buildFakeClient(fakeProvider); mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setProviderType(InfuraNetworkType.goerli); + await controller.setActiveNetwork('testNetworkConfiguration'); const networkDidChange = waitForPublishedEvents({ messenger, @@ -5876,20 +3901,222 @@ describe('NetworkController', () => { ); }); - it('overwrites the the current provider configuration with the previous provider configuration', async () => { + it('sets selectedNetworkClientId in state to the previous version', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + nickname: 'test network', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer.com', + }, + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - nickname: 'network', + type: NetworkClientType.Custom, ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect(controller.state.selectedNetworkClientId).toBe( + 'testNetworkConfiguration', + ); + + await controller.rollbackToPreviousProvider(); + + expect(controller.state.selectedNetworkClientId).toBe( + networkType, + ); + }, + ); + }); + + it('resets the network status to "unknown" before updating the provider', async () => { + await withController( + { + state: { + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + ]), + buildFakeProvider(), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); + + await waitForStateChanges({ + messenger, + propertyPath: ['networksMetadata', networkType, 'status'], + // We only care about the first state change, because it + // happens before networkDidChange + count: 1, + operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress + controller.rollbackToPreviousProvider(); + }, + beforeResolving: () => { + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unknown'); + }, + }); + }, + ); + }); + + it(`initializes a provider pointed to the "${networkType}" Infura network`, async () => { + await withController( + { + state: { + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider([ + { + request: { + method: 'test', + }, + response: { + result: 'test response', + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + + await controller.rollbackToPreviousProvider(); + + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); + const promisifiedSendAsync = promisify(provider.sendAsync).bind( + provider, + ); + const response = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', + method: 'test', + }); + expect(response.result).toBe('test response'); + }, + ); + }); + + it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { + await withController( + { + state: { + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', }, - }), + }, }, infuraProjectId: 'some-infura-project-id', }, @@ -5900,65 +4127,128 @@ describe('NetworkController', () => { buildFakeClient(fakeProviders[1]), ]; mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - }) - .mockReturnValue(fakeNetworkClients[0]) .calledWith({ rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), type: NetworkClientType.Custom, ticker: 'TEST', }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + const { provider: providerBefore } = + controller.getProviderAndBlockTracker(); + + await controller.rollbackToPreviousProvider(); + + const { provider: providerAfter } = + controller.getProviderAndBlockTracker(); + expect(providerBefore).toBe(providerAfter); + }, + ); + }); + + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { + await withController( + { + state: { + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + error: BLOCKED_INFURA_JSON_RPC_ERROR, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + type: NetworkClientType.Infura, + }) .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'goerli', - rpcUrl: undefined, - chainId: toHex(5), - ticker: 'GoerliETH', - nickname: undefined, - rpcPrefs: { - blockExplorerUrl: 'https://goerli.etherscan.io', - }, - id: undefined, + await controller.setActiveNetwork('testNetworkConfiguration'); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', }); await controller.rollbackToPreviousProvider(); - expect(controller.state.providerConfig).toStrictEqual( - buildProviderConfig({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - nickname: 'network', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }), - ); + + await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); + await expect(promiseForInfuraIsBlocked).toBeFulfilled(); }, ); }); - it('resets the network state to "unknown" before updating the provider', async () => { + it('checks the status of the previous network again and updates state accordingly', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, infuraProjectId: 'some-infura-project-id', }, async ({ controller, messenger }) => { const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + error: rpcErrors.methodNotFound(), + }, + ]), buildFakeProvider([ { request: { @@ -5967,84 +4257,85 @@ describe('NetworkController', () => { response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, }, ]), - buildFakeProvider(), ]; const fakeNetworkClients = [ buildFakeClient(fakeProviders[0]), buildFakeClient(fakeProviders[1]), ]; mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - }) - .mockReturnValue(fakeNetworkClients[0]) .calledWith({ rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), type: NetworkClientType.Custom, ticker: 'TEST', }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + type: NetworkClientType.Infura, + }) .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); + await controller.setActiveNetwork('testNetworkConfiguration'); expect( controller.state.networksMetadata[ controller.state.selectedNetworkClientId ].status, - ).toBe('available'); + ).toBe('unavailable'); await waitForStateChanges({ messenger, - propertyPath: [ - 'networksMetadata', - 'https://mock-rpc-url', - 'status', - ], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unknown'); + propertyPath: ['networksMetadata', networkType, 'status'], + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); }, ); }); - it('initializes a provider pointed to the given RPC URL', async () => { + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { await withController( { state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), + selectedNetworkClientId: networkType, + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, }, infuraProjectId: 'some-infura-project-id', }, - async ({ controller }) => { + async ({ controller, messenger }) => { const fakeProviders = [ - buildFakeProvider(), buildFakeProvider([ { request: { - method: 'test', + method: 'eth_getBlockByNumber', }, response: { - result: 'test response', + result: PRE_1559_BLOCK, + }, + }, + ]), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: POST_1559_BLOCK, }, }, ]), @@ -6055,274 +4346,571 @@ describe('NetworkController', () => { ]; mockCreateNetworkClient() .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', }) .mockReturnValue(fakeNetworkClients[0]) .calledWith({ + network: networkType, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setActiveNetwork('testNetworkConfiguration'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(false); + + await waitForStateChanges({ + messenger, + propertyPath: ['networksMetadata', networkType, 'EIPS'], + count: 2, + operation: async () => { + await controller.rollbackToPreviousProvider(); + }, + }); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); + }, + ); + }); + }); + } + + describe('when called following a network switch away from a network configuration', () => { + it('emits networkWillChange with state payload', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + }, + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.setProviderType(InfuraNetworkType.goerli); + + const networkWillChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', + filter: ([networkState]) => networkState === controller.state, + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + controller.rollbackToPreviousProvider(); + }, + }); + + await expect(networkWillChange).toBeFulfilled(); + }, + ); + }); + + it('emits networkDidChange with state payload', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + }, + async ({ controller, messenger }) => { + const fakeProvider = buildFakeProvider(); + const fakeNetworkClient = buildFakeClient(fakeProvider); + mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); + await controller.setProviderType(InfuraNetworkType.goerli); + + const networkDidChange = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', + filter: ([networkState]) => networkState === controller.state, + operation: () => { + // Intentionally not awaited because we're capturing an event + // emitted partway through the operation + controller.rollbackToPreviousProvider(); + }, + }); + + await expect(networkDidChange).toBeFulfilled(); + }, + ); + }); + + it('sets selectedNetworkClientId to the previous version', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + ticker: 'TEST', + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect(controller.state.selectedNetworkClientId).toBe('goerli'); + + await controller.rollbackToPreviousProvider(); + expect(controller.state.selectedNetworkClientId).toBe( + 'testNetworkConfiguration', + ); + }, + ); + }); + + it('resets the network state to "unknown" before updating the provider', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - type: NetworkClientType.Custom, ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - - await controller.rollbackToPreviousProvider(); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const response = await promisifiedSendAsync({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response.result).toBe('test response'); + }, + }, }, - ); - }); + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + ]), + buildFakeProvider(), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), + await waitForStateChanges({ + messenger, + propertyPath: [ + 'networksMetadata', + 'testNetworkConfiguration', + 'status', + ], + // We only care about the first state change, because it + // happens before networkDidChange + count: 1, + operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress + controller.rollbackToPreviousProvider(); }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ + beforeResolving: () => { + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unknown'); + }, + }); + }, + ); + }); + + it('initializes a provider pointed to the given RPC URL', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - type: NetworkClientType.Custom, ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); + }, + }, + }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider(), + buildFakeProvider([ + { + request: { + method: 'test', + }, + response: { + result: 'test response', + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); - await controller.rollbackToPreviousProvider(); + await controller.rollbackToPreviousProvider(); - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }, - ); - }); + const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); + const promisifiedSendAsync = promisify(provider.sendAsync).bind( + provider, + ); + const response = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', + method: 'test', + }); + expect(response.result).toBe('test response'); + }, + ); + }); - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, + it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - }), + ticker: 'TEST', + }, }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller, messenger }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + type: NetworkClientType.Infura, + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + const { provider: providerBefore } = + controller.getProviderAndBlockTracker(); + + await controller.rollbackToPreviousProvider(); + + const { provider: providerAfter } = + controller.getProviderAndBlockTracker(); + expect(providerBefore).toBe(providerAfter); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - type: NetworkClientType.Custom, ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - - const promiseForInfuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await controller.rollbackToPreviousProvider(); }, - }); - - await expect(promiseForInfuraIsUnblocked).toBeFulfilled(); + }, }, - ); - }); + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller, messenger }) => { + const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + + const promiseForInfuraIsUnblocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.rollbackToPreviousProvider(); + }, + }); - it('checks the status of the previous network again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, + await expect(promiseForInfuraIsUnblocked).toBeFulfilled(); + }, + ); + }); + + it('checks the status of the previous network again and updates state accordingly', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - }), + ticker: 'TEST', + }, }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - error: rpcErrors.methodNotFound(), + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + error: rpcErrors.methodNotFound(), + }, + ]), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('unavailable'); + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('unavailable'); - await controller.rollbackToPreviousProvider(); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].status, - ).toBe('available'); - }, - ); - }); + await controller.rollbackToPreviousProvider(); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].status, + ).toBe('available'); + }, + ); + }); - it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NetworkType.rpc, + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { + await withController( + { + state: { + selectedNetworkClientId: 'testNetworkConfiguration', + networkConfigurations: { + testNetworkConfiguration: { + id: 'testNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', chainId: toHex(1337), - }), + ticker: 'TEST', + }, }, - infuraProjectId: 'some-infura-project-id', }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, + infuraProjectId: 'some-infura-project-id', + }, + async ({ controller }) => { + const fakeProviders = [ + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, + response: { + result: PRE_1559_BLOCK, }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: InfuraNetworkType.goerli, - infuraProjectId: 'some-infura-project-id', - chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, - ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - ticker: 'TEST', - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(false); + }, + ]), + buildFakeProvider([ + { + request: { + method: 'eth_getBlockByNumber', + }, + response: { + result: POST_1559_BLOCK, + }, + }, + ]), + ]; + const fakeNetworkClients = [ + buildFakeClient(fakeProviders[0]), + buildFakeClient(fakeProviders[1]), + ]; + mockCreateNetworkClient() + .calledWith({ + network: InfuraNetworkType.goerli, + infuraProjectId: 'some-infura-project-id', + chainId: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[InfuraNetworkType.goerli].ticker, + type: NetworkClientType.Infura, + }) + .mockReturnValue(fakeNetworkClients[0]) + .calledWith({ + rpcUrl: 'https://mock-rpc-url', + chainId: toHex(1337), + type: NetworkClientType.Custom, + ticker: 'TEST', + }) + .mockReturnValue(fakeNetworkClients[1]); + await controller.setProviderType('goerli'); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(false); - await controller.rollbackToPreviousProvider(); - expect( - controller.state.networksMetadata[ - controller.state.selectedNetworkClientId - ].EIPS[1559], - ).toBe(true); - }, - ); - }); + await controller.rollbackToPreviousProvider(); + expect( + controller.state.networksMetadata[ + controller.state.selectedNetworkClientId + ].EIPS[1559], + ).toBe(true); + }, + ); }); }); }); @@ -6447,17 +5035,17 @@ function mockCreateNetworkClientWithDefaultsForBuiltInNetworkClients({ * covered by these tests. * * @param args - Arguments. - * @param args.expectedProviderConfig - The provider configuration that the - * operation is expected to set. + * @param args.expectedNetworkClientConfiguration - The network client + * configuration that the operation is expected to set. * @param args.initialState - The initial state of the network controller. * @param args.operation - The operation to test. */ function refreshNetworkTests({ - expectedProviderConfig, + expectedNetworkClientConfiguration, initialState, operation, }: { - expectedProviderConfig: ProviderConfig; + expectedNetworkClientConfiguration: NetworkClientConfiguration; initialState?: Partial; operation: (controller: NetworkController) => Promise; }) { @@ -6513,7 +5101,7 @@ function refreshNetworkTests({ ); }); - if (expectedProviderConfig.type === NetworkType.rpc) { + if (expectedNetworkClientConfiguration.type === NetworkClientType.Custom) { it('sets the provider to a custom RPC provider initialized with the RPC target and chain ID', async () => { await withController( { @@ -6537,10 +5125,10 @@ function refreshNetworkTests({ await operation(controller); expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: expectedProviderConfig.chainId, - rpcUrl: expectedProviderConfig.rpcUrl, + chainId: expectedNetworkClientConfiguration.chainId, + rpcUrl: expectedNetworkClientConfiguration.rpcUrl, type: NetworkClientType.Custom, - ticker: expectedProviderConfig.ticker, + ticker: expectedNetworkClientConfiguration.ticker, }); const { provider } = controller.getProviderAndBlockTracker(); assert(provider); @@ -6558,7 +5146,9 @@ function refreshNetworkTests({ ); }); } else { - it(`sets the provider to an Infura provider pointed to ${expectedProviderConfig.type}`, async () => { + it(`sets the provider to an Infura provider pointed to ${String( + expectedNetworkClientConfiguration.network, + )}`, async () => { await withController( { infuraProjectId: 'infura-project-id', @@ -6581,11 +5171,8 @@ function refreshNetworkTests({ await operation(controller); expect(createNetworkClientMock).toHaveBeenCalledWith({ - network: expectedProviderConfig.type, + ...expectedNetworkClientConfiguration, infuraProjectId: 'infura-project-id', - chainId: BUILT_IN_NETWORKS[expectedProviderConfig.type].chainId, - ticker: BUILT_IN_NETWORKS[expectedProviderConfig.type].ticker, - type: NetworkClientType.Infura, }); const { provider } = controller.getProviderAndBlockTracker(); assert(provider); @@ -6616,45 +5203,38 @@ function refreshNetworkTests({ buildFakeClient(fakeProviders[0]), buildFakeClient(fakeProviders[1]), ]; - const initializationNetworkClientOptions: Parameters< + const { selectedNetworkClientId } = controller.state; + let initializationNetworkClientOptions: Parameters< typeof createNetworkClient - >[0] = - controller.state.providerConfig.type === NetworkType.rpc - ? { - chainId: toHex(controller.state.providerConfig.chainId), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rpcUrl: controller.state.providerConfig.rpcUrl!, - type: NetworkClientType.Custom, - ticker: controller.state.providerConfig.ticker, - } - : { - network: controller.state.providerConfig.type, - infuraProjectId: 'infura-project-id', - chainId: - BUILT_IN_NETWORKS[controller.state.providerConfig.type] - .chainId, - ticker: - BUILT_IN_NETWORKS[controller.state.providerConfig.type] - .ticker, - type: NetworkClientType.Infura, - }; + >[0]; + + if (isInfuraNetworkType(selectedNetworkClientId)) { + initializationNetworkClientOptions = { + network: selectedNetworkClientId, + infuraProjectId: 'infura-project-id', + chainId: BUILT_IN_NETWORKS[selectedNetworkClientId].chainId, + ticker: BUILT_IN_NETWORKS[selectedNetworkClientId].ticker, + type: NetworkClientType.Infura, + }; + } else { + const networkConfiguration = + controller.state.networkConfigurations[selectedNetworkClientId]; + initializationNetworkClientOptions = { + chainId: networkConfiguration.chainId, + rpcUrl: networkConfiguration.rpcUrl, + type: NetworkClientType.Custom, + ticker: networkConfiguration.ticker, + }; + } + const operationNetworkClientOptions: Parameters< typeof createNetworkClient >[0] = - expectedProviderConfig.type === NetworkType.rpc - ? { - chainId: toHex(expectedProviderConfig.chainId), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rpcUrl: expectedProviderConfig.rpcUrl!, - type: NetworkClientType.Custom, - ticker: expectedProviderConfig.ticker, - } + expectedNetworkClientConfiguration.type === NetworkClientType.Custom + ? expectedNetworkClientConfiguration : { - network: expectedProviderConfig.type, + ...expectedNetworkClientConfiguration, infuraProjectId: 'infura-project-id', - chainId: BUILT_IN_NETWORKS[expectedProviderConfig.type].chainId, - ticker: BUILT_IN_NETWORKS[expectedProviderConfig.type].ticker, - type: NetworkClientType.Infura, }; mockCreateNetworkClient() .calledWith(initializationNetworkClientOptions) @@ -6674,7 +5254,11 @@ function refreshNetworkTests({ ); }); - lookupNetworkTests({ expectedProviderConfig, initialState, operation }); + lookupNetworkTests({ + expectedNetworkClientConfiguration, + initialState, + operation, + }); } /** @@ -6683,17 +5267,17 @@ function refreshNetworkTests({ * covered by these tests. * * @param args - Arguments. - * @param args.expectedProviderConfig - The provider configuration that the - * operation is expected to set. + * @param args.expectedNetworkClientConfiguration - The network client + * configuration that the operation is expected to set. * @param args.initialState - The initial state of the network controller. * @param args.operation - The operation to test. */ function lookupNetworkTests({ - expectedProviderConfig, + expectedNetworkClientConfiguration, initialState, operation, }: { - expectedProviderConfig: ProviderConfig; + expectedNetworkClientConfiguration: NetworkClientConfiguration; initialState?: Partial; operation: (controller: NetworkController) => Promise; }) { @@ -6910,7 +5494,7 @@ function lookupNetworkTests({ ); }); - if (expectedProviderConfig.type === NetworkType.rpc) { + if (expectedNetworkClientConfiguration.type === NetworkClientType.Custom) { it('emits infuraIsUnblocked', async () => { await withController( { @@ -7012,7 +5596,7 @@ function lookupNetworkTests({ }); describe('if a country blocked error is encountered while retrieving the network details of the current network', () => { - if (expectedProviderConfig.type === NetworkType.rpc) { + if (expectedNetworkClientConfiguration.type === NetworkClientType.Custom) { it('updates the network in state to "unknown"', async () => { await withController( { @@ -7326,7 +5910,7 @@ function lookupNetworkTests({ ); }); - if (expectedProviderConfig.type === NetworkType.rpc) { + if (expectedNetworkClientConfiguration.type === NetworkClientType.Custom) { it('emits infuraIsUnblocked', async () => { await withController( { @@ -7528,35 +6112,6 @@ async function withController( } } -/** - * Builds a complete ProviderConfig object, filling in values that are not - * provided with defaults. - * - * @param config - An incomplete ProviderConfig object. - * @returns The complete ProviderConfig object. - */ -function buildProviderConfig( - config: Partial = {}, -): ProviderConfig { - if (config.type && config.type !== NetworkType.rpc) { - return { - ...BUILT_IN_NETWORKS[config.type], - // This is redundant with the spread operation below, but this was - // required for TypeScript to understand that this property was set to an - // Infura type. - type: config.type, - ...config, - }; - } - return { - type: NetworkType.rpc, - chainId: toHex(1337), - rpcUrl: 'http://doesntmatter.com', - ticker: 'TEST', - ...config, - }; -} - /** * Builds an object that `createNetworkClient` returns. * diff --git a/packages/selected-network-controller/tests/SelectedNetworkController.test.ts b/packages/selected-network-controller/tests/SelectedNetworkController.test.ts index e740d15c98..9d6564ac8b 100644 --- a/packages/selected-network-controller/tests/SelectedNetworkController.test.ts +++ b/packages/selected-network-controller/tests/SelectedNetworkController.test.ts @@ -240,7 +240,6 @@ describe('SelectedNetworkController', () => { messenger.publish( 'NetworkController:stateChange', { - providerConfig: { chainId: '0x5', ticker: 'ETH', type: 'goerli' }, selectedNetworkClientId: 'goerli', networkConfigurations: {}, networksMetadata: {}, diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 231ef5336f..6758cc7997 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -13,25 +13,37 @@ import { toHex, BUILT_IN_NETWORKS, ORIGIN_METAMASK, + InfuraNetworkType, } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import HttpProvider from '@metamask/ethjs-provider-http'; import type { BlockTracker, - NetworkController, + NetworkClientConfiguration, + NetworkClientId, NetworkState, Provider, } from '@metamask/network-controller'; -import { NetworkClientType, NetworkStatus } from '@metamask/network-controller'; +import { + NetworkClientType, + NetworkStatus, + defaultState as defaultNetworkState, +} from '@metamask/network-controller'; import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import type { Hex } from '@metamask/utils'; import { createDeferredPromise } from '@metamask/utils'; import assert from 'assert'; import * as NonceTrackerPackage from 'nonce-tracker'; import * as uuidModule from 'uuid'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; +import { FakeProvider } from '../../../tests/fake-provider'; import { flushPromises } from '../../../tests/helpers'; import { mockNetwork } from '../../../tests/mock-network'; +import { + buildCustomNetworkClientConfiguration, + buildMockGetNetworkClientById, +} from '../../network-controller/tests/helpers'; import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './gas-flows/LineaGasFeeFlow'; import { TestGasFeeFlow } from './gas-flows/TestGasFeeFlow'; @@ -82,13 +94,6 @@ type UnrestrictedControllerMessenger = ControllerMessenger< TransactionControllerEvents | AllowedEvents >; -type NetworkClient = ReturnType; - -type NetworkClientConfiguration = Pick< - NetworkClient['configuration'], - 'chainId' ->; - const MOCK_V1_UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'; jest.mock('@metamask/eth-query'); @@ -294,9 +299,6 @@ function waitForTransactionFinished( const MOCK_PREFERENCES = { state: { selectedAddress: 'foo' } }; const INFURA_PROJECT_ID = '341eacb578dd44a1a049cbc5f6fd4035'; -const GOERLI_PROVIDER = new HttpProvider( - `https://goerli.infura.io/v3/${INFURA_PROJECT_ID}`, -); const MAINNET_PROVIDER = new HttpProvider( `https://mainnet.infura.io/v3/${INFURA_PROJECT_ID}`, ); @@ -305,6 +307,7 @@ const PALM_PROVIDER = new HttpProvider( ); type MockNetwork = { + chainId: Hex; provider: Provider; blockTracker: BlockTracker; state: NetworkState; @@ -312,6 +315,7 @@ type MockNetwork = { }; const MOCK_NETWORK: MockNetwork = { + chainId: ChainId.goerli, provider: MAINNET_PROVIDER, blockTracker: buildMockBlockTracker('0x102833C'), state: { @@ -322,34 +326,12 @@ const MOCK_NETWORK: MockNetwork = { status: NetworkStatus.Available, }, }, - providerConfig: { - type: NetworkType.goerli, - chainId: ChainId.goerli, - ticker: NetworksTicker.goerli, - }, - networkConfigurations: {}, - }, - subscribe: () => undefined, -}; -const MOCK_NETWORK_WITHOUT_CHAIN_ID: MockNetwork = { - provider: GOERLI_PROVIDER, - blockTracker: buildMockBlockTracker('0x102833C'), - state: { - selectedNetworkClientId: NetworkType.goerli, - networksMetadata: { - [NetworkType.goerli]: { - EIPS: { 1559: false }, - status: NetworkStatus.Available, - }, - }, - providerConfig: { - type: NetworkType.goerli, - } as NetworkState['providerConfig'], networkConfigurations: {}, }, subscribe: () => undefined, }; const MOCK_MAINNET_NETWORK: MockNetwork = { + chainId: ChainId.mainnet, provider: MAINNET_PROVIDER, blockTracker: buildMockBlockTracker('0x102833C'), state: { @@ -360,17 +342,13 @@ const MOCK_MAINNET_NETWORK: MockNetwork = { status: NetworkStatus.Available, }, }, - providerConfig: { - type: NetworkType.mainnet, - chainId: ChainId.mainnet, - ticker: NetworksTicker.mainnet, - }, networkConfigurations: {}, }, subscribe: () => undefined, }; const MOCK_LINEA_MAINNET_NETWORK: MockNetwork = { + chainId: ChainId['linea-mainnet'], provider: PALM_PROVIDER, blockTracker: buildMockBlockTracker('0xA6EDFC'), state: { @@ -381,17 +359,13 @@ const MOCK_LINEA_MAINNET_NETWORK: MockNetwork = { status: NetworkStatus.Available, }, }, - providerConfig: { - type: NetworkType['linea-mainnet'], - chainId: toHex(59144), - ticker: NetworksTicker['linea-mainnet'], - }, networkConfigurations: {}, }, subscribe: () => undefined, }; const MOCK_LINEA_GOERLI_NETWORK: MockNetwork = { + chainId: ChainId['linea-goerli'], provider: PALM_PROVIDER, blockTracker: buildMockBlockTracker('0xA6EDFC'), state: { @@ -402,32 +376,6 @@ const MOCK_LINEA_GOERLI_NETWORK: MockNetwork = { status: NetworkStatus.Available, }, }, - providerConfig: { - type: NetworkType['linea-goerli'], - chainId: toHex(59140), - ticker: NetworksTicker['linea-goerli'], - }, - networkConfigurations: {}, - }, - subscribe: () => undefined, -}; - -const MOCK_CUSTOM_NETWORK: MockNetwork = { - provider: PALM_PROVIDER, - blockTracker: buildMockBlockTracker('0xA6EDFC'), - state: { - selectedNetworkClientId: 'uuid-1', - networksMetadata: { - 'uuid-1': { - EIPS: { 1559: false }, - status: NetworkStatus.Available, - }, - }, - providerConfig: { - type: NetworkType.rpc, - chainId: toHex(11297108109), - ticker: 'TEST', - }, networkConfigurations: {}, }, subscribe: () => undefined, @@ -437,7 +385,7 @@ const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; const NONCE_MOCK = 12; const ACTION_ID_MOCK = '123456'; -const CHAIN_ID_MOCK = MOCK_NETWORK.state.providerConfig.chainId; +const CHAIN_ID_MOCK = MOCK_NETWORK.chainId; const NETWORK_CLIENT_ID_MOCK = 'networkClientIdMock'; const TRANSACTION_META_MOCK = { @@ -542,27 +490,80 @@ describe('TransactionController', () => { * @param args - The arguments to this function. * @param args.options - TransactionController options. * @param args.network - The mock network to use with the controller. + * @param args.network.blockTracker - The desired block tracker associated + * with the network. + * @param args.network.provider - The desired provider associated with the + * provider. + * @param args.network.state - The desired NetworkController state. + * @param args.network.onNetworkStateChange - The function to subscribe to + * changes in the NetworkController state. * @param args.messengerOptions - Options to build the mock unrestricted * messenger. - * @param args.messengerOptions.addTransactionApprovalRequest - Options to mock - * the `ApprovalController:addRequest` action call for transactions. + * @param args.messengerOptions.addTransactionApprovalRequest - Options to + * mock the `ApprovalController:addRequest` action call for transactions. + * @param args.mockNetworkClientConfigurationsByNetworkClientId - Network + * client configurations by network client ID. * @returns The new TransactionController instance. */ function setupController({ options: givenOptions = {}, - network = MOCK_NETWORK, + network = {}, messengerOptions = {}, + mockNetworkClientConfigurationsByNetworkClientId = {}, }: { options?: Partial[0]>; - network?: MockNetwork; + network?: { + blockTracker?: BlockTracker; + provider?: Provider; + state?: Partial; + onNetworkStateChange?: ( + listener: (networkState: NetworkState) => void, + ) => void; + }; messengerOptions?: { addTransactionApprovalRequest?: Parameters< typeof mockAddTransactionApprovalRequest >[1]; }; + mockNetworkClientConfigurationsByNetworkClientId?: Record< + NetworkClientId, + NetworkClientConfiguration + >; } = {}) { + let networkState = { + ...defaultNetworkState, + selectedNetworkClientId: MOCK_NETWORK.state.selectedNetworkClientId, + ...network.state, + }; + const blockTracker = network.blockTracker ?? new FakeBlockTracker(); + const provider = network.provider ?? new FakeProvider(); + const onNetworkDidChangeListeners: ((state: NetworkState) => void)[] = []; + const changeNetwork = ({ + selectedNetworkClientId, + }: { + selectedNetworkClientId: NetworkClientId; + }) => { + networkState = { + ...networkState, + selectedNetworkClientId, + }; + onNetworkDidChangeListeners.forEach((listener) => { + listener(networkState); + }); + }; + const onNetworkStateChange = ( + listener: (networkState: NetworkState) => void, + ) => onNetworkDidChangeListeners.push(listener); + const unrestrictedMessenger: UnrestrictedControllerMessenger = new ControllerMessenger(); + const getNetworkClientById = buildMockGetNetworkClientById( + mockNetworkClientConfigurationsByNetworkClientId, + ); + unrestrictedMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById, + ); const { addTransactionApprovalRequest = { state: 'pending' } } = messengerOptions; @@ -572,12 +573,12 @@ describe('TransactionController', () => { ); const { messenger: givenRestrictedMessenger, ...otherOptions } = { - blockTracker: network.blockTracker, + blockTracker, disableHistory: false, disableSendFlowHistory: false, disableSwaps: false, getCurrentNetworkEIP1559Compatibility: async () => false, - getNetworkState: () => network.state, + getNetworkState: () => networkState, // TODO: Replace with a real type // eslint-disable-next-line @typescript-eslint/no-explicit-any getNetworkClientRegistry: () => ({} as any), @@ -585,8 +586,8 @@ describe('TransactionController', () => { getSelectedAddress: () => ACCOUNT_MOCK, isMultichainEnabled: false, hooks: {}, - onNetworkStateChange: network.subscribe, - provider: network.provider, + onNetworkStateChange, + provider, sign: async (transaction: TypedTransaction) => transaction, transactionHistoryLimit: 40, ...givenOptions, @@ -613,6 +614,7 @@ describe('TransactionController', () => { controller, messenger: unrestrictedMessenger, mockTransactionApprovalRequest, + changeNetwork, }; } @@ -621,17 +623,18 @@ describe('TransactionController', () => { * TransactionController calls as it creates transactions. * * This helper allows the `addRequest` action to be in one of three states: - * approved, rejected, or pending. In the approved state, the promise which the - * action returns is resolved ahead of time, and in the rejected state, the - * promise is rejected ahead of time. Otherwise, in the pending state, the - * promise is unresolved and it is assumed that the test will resolve or reject - * the promise. + * approved, rejected, or pending. In the approved state, the promise which + * the action returns is resolved ahead of time, and in the rejected state, + * the promise is rejected ahead of time. Otherwise, in the pending state, the + * promise is unresolved and it is assumed that the test will resolve or + * reject the promise. * * @param messenger - The unrestricted messenger. - * @param options - Options for the mock. `state` controls the state of the - * promise as outlined above. Note, if the `state` is approved, then its - * `result` may be specified; if the `state` is rejected, then its `error` may - * be specified. + * @param options - An options bag which will be used to create an action + * handler that places the approval request in a certain state. The `state` + * option controls the state of the promise as outlined above: if the `state` + * is approved, then its `result` may be specified; if the `state` is + * rejected, then its `error` may be specified. * @returns An object which contains the aforementioned promise, functions to * manually approve or reject the approval (and therefore the promise), and * finally the mocked version of the action handler itself. @@ -687,7 +690,6 @@ describe('TransactionController', () => { ReturnType, Parameters > = jest.fn().mockReturnValue(promise); - if (options.state === 'approved') { approveTransaction(options.result); } else if (options.state === 'rejected') { @@ -707,22 +709,6 @@ describe('TransactionController', () => { }; } - /** - * Builds a network client that is only used in tests to get a chain ID. - * - * @param networkClient - The properties in the desired network client. - * Only needs to contain `configuration`. - * @param networkClient.configuration - The desired network client - * configuration. Only needs to contain `chainId`> - * @returns The network client. - */ - function buildMockNetworkClient(networkClient: { - configuration: NetworkClientConfiguration; - }): NetworkClient { - // Type assertion: We don't expect anything but the configuration to get used. - return networkClient as unknown as NetworkClient; - } - /** * Wait for a specified number of milliseconds. * @@ -765,18 +751,19 @@ describe('TransactionController', () => { return incomingTransactionHelperMock; }); + pendingTransactionTrackerMock = { + start: jest.fn(), + stop: jest.fn(), + startIfPendingTransactions: jest.fn(), + hub: { + on: jest.fn(), + removeAllListeners: jest.fn(), + }, + onStateChange: jest.fn(), + forceCheckTransaction: jest.fn(), + } as unknown as jest.Mocked; + pendingTransactionTrackerClassMock.mockImplementation(() => { - pendingTransactionTrackerMock = { - start: jest.fn(), - stop: jest.fn(), - startIfPendingTransactions: jest.fn(), - hub: { - on: jest.fn(), - removeAllListeners: jest.fn(), - }, - onStateChange: jest.fn(), - forceCheckTransaction: jest.fn(), - } as unknown as jest.Mocked; return pendingTransactionTrackerMock; }); @@ -907,7 +894,7 @@ describe('TransactionController', () => { transactions: [ { ...TRANSACTION_META_MOCK, - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, status: TransactionStatus.submitted, }, ], @@ -1315,9 +1302,7 @@ describe('TransactionController', () => { expect(updateSwapsTransactionMock).toHaveBeenCalledTimes(1); expect(transactionMeta.txParams.from).toBe(ACCOUNT_MOCK); - expect(transactionMeta.chainId).toBe( - MOCK_NETWORK.state.providerConfig.chainId, - ); + expect(transactionMeta.chainId).toBe(MOCK_NETWORK.chainId); expect(transactionMeta.deviceConfirmedOn).toBe(mockDeviceConfirmedOn); expect(transactionMeta.origin).toBe(mockOrigin); expect(transactionMeta.status).toBe(TransactionStatus.unapproved); @@ -1331,26 +1316,9 @@ describe('TransactionController', () => { describe('networkClientId exists in the MultichainTrackingHelper', () => { it('adds unapproved transaction to state when using networkClientId', async () => { - const { controller, messenger } = setupController({ + const { controller } = setupController({ options: { isMultichainEnabled: true }, }); - messenger.registerActionHandler( - 'NetworkController:getNetworkClientById', - (networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return buildMockNetworkClient({ - configuration: { - chainId: ChainId.sepolia, - }, - }); - default: - throw new Error( - `Unknown network client ID: ${networkClientId}`, - ); - } - }, - ); const sepoliaTxParams: TransactionParams = { chainId: ChainId.sepolia, from: ACCOUNT_MOCK, @@ -1362,7 +1330,7 @@ describe('TransactionController', () => { await controller.addTransaction(sepoliaTxParams, { origin: 'metamask', actionId: ACTION_ID_MOCK, - networkClientId: 'sepolia', + networkClientId: InfuraNetworkType.sepolia, }); const transactionMeta = controller.state.transactions[0]; @@ -1384,23 +1352,6 @@ describe('TransactionController', () => { }, }, }); - messenger.registerActionHandler( - 'NetworkController:getNetworkClientById', - (networkClientId) => { - switch (networkClientId) { - case 'sepolia': - return buildMockNetworkClient({ - configuration: { - chainId: ChainId.sepolia, - }, - }); - default: - throw new Error( - `Unknown network client ID: ${networkClientId}`, - ); - } - }, - ); multichainTrackingHelperMock.has.mockReturnValue(true); const submittedEventListener = jest.fn(); @@ -1418,7 +1369,7 @@ describe('TransactionController', () => { const { result } = await controller.addTransaction(sepoliaTxParams, { origin: 'metamask', actionId: ACTION_ID_MOCK, - networkClientId: 'sepolia', + networkClientId: InfuraNetworkType.sepolia, }); await result; @@ -1516,49 +1467,54 @@ describe('TransactionController', () => { ); }); - it.each([ - ['mainnet', MOCK_MAINNET_NETWORK], - ['custom network', MOCK_CUSTOM_NETWORK], - ])( - 'adds unapproved transaction to state after switching to %s', - async (_networkName, newNetwork) => { - const getNetworkState = jest.fn().mockReturnValue(MOCK_NETWORK.state); - - let networkStateChangeListener: ((state: NetworkState) => void) | null = - null; - - const onNetworkStateChange = ( - listener: (state: NetworkState) => void, - ) => { - networkStateChangeListener = listener; - }; + it('adds unapproved transaction to state after switching to Infura network', async () => { + const { controller, changeNetwork } = setupController({ + network: { + state: { + selectedNetworkClientId: InfuraNetworkType.goerli, + }, + }, + }); + changeNetwork({ selectedNetworkClientId: InfuraNetworkType.mainnet }); - const { controller } = setupController({ - options: { getNetworkState, onNetworkStateChange }, - }); + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); - // switch from Goerli to Mainnet - getNetworkState.mockReturnValue(newNetwork.state); + expect(controller.state.transactions[0].txParams.from).toBe(ACCOUNT_MOCK); + expect(controller.state.transactions[0].chainId).toBe(ChainId.mainnet); + expect(controller.state.transactions[0].status).toBe( + TransactionStatus.unapproved, + ); + }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - networkStateChangeListener!(newNetwork.state); + it('adds unapproved transaction to state after switching to custom network', async () => { + const { controller, changeNetwork } = setupController({ + network: { + state: { + selectedNetworkClientId: InfuraNetworkType.goerli, + }, + }, + mockNetworkClientConfigurationsByNetworkClientId: { + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + chainId: '0x1337', + }), + }, + }); + changeNetwork({ selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD' }); - await controller.addTransaction({ - from: ACCOUNT_MOCK, - to: ACCOUNT_MOCK, - }); + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); - expect(controller.state.transactions[0].txParams.from).toBe( - ACCOUNT_MOCK, - ); - expect(controller.state.transactions[0].chainId).toBe( - newNetwork.state.providerConfig.chainId, - ); - expect(controller.state.transactions[0].status).toBe( - TransactionStatus.unapproved, - ); - }, - ); + expect(controller.state.transactions[0].txParams.from).toBe(ACCOUNT_MOCK); + expect(controller.state.transactions[0].chainId).toBe('0x1337'); + expect(controller.state.transactions[0].status).toBe( + TransactionStatus.unapproved, + ); + }); it('throws if address invalid', async () => { const { controller } = setupController(); @@ -1622,15 +1578,19 @@ describe('TransactionController', () => { it('requests approval using the approval controller', async () => { const { controller, messenger } = setupController(); - jest.spyOn(messenger, 'call'); + const messengerCallSpy = jest.spyOn(messenger, 'call'); await controller.addTransaction({ from: ACCOUNT_MOCK, to: ACCOUNT_MOCK, }); - expect(messenger.call).toHaveBeenCalledTimes(1); - expect(messenger.call).toHaveBeenCalledWith( + expect( + messengerCallSpy.mock.calls.filter( + (args) => args[0] === 'ApprovalController:addRequest', + ), + ).toHaveLength(1); + expect(messengerCallSpy).toHaveBeenCalledWith( 'ApprovalController:addRequest', { id: expect.any(String), @@ -1657,7 +1617,9 @@ describe('TransactionController', () => { }, ); - expect(messenger.call).toHaveBeenCalledTimes(0); + expect(messenger.call).not.toHaveBeenCalledWith( + 'ApprovalController:addRequest', + ); }); it('calls security provider with transaction meta and sets response in to securityProviderResponse', async () => { @@ -1711,9 +1673,9 @@ describe('TransactionController', () => { expect(updateGasMock).toHaveBeenCalledTimes(1); expect(updateGasMock).toHaveBeenCalledWith({ ethQuery: expect.any(Object), - chainId: MOCK_NETWORK.state.providerConfig.chainId, - isCustomNetwork: - MOCK_NETWORK.state.providerConfig.type === NetworkType.rpc, + chainId: MOCK_NETWORK.chainId, + // TODO: Is this right? + isCustomNetwork: false, txMeta: expect.any(Object), }); }); @@ -1757,7 +1719,7 @@ describe('TransactionController', () => { expect(getSimulationDataMock).toHaveBeenCalledTimes(1); expect(getSimulationDataMock).toHaveBeenCalledWith({ - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, data: undefined, from: ACCOUNT_MOCK, to: ACCOUNT_MOCK, @@ -2003,7 +1965,18 @@ describe('TransactionController', () => { it('if no chainId defined', async () => { const { controller } = setupController({ - network: MOCK_NETWORK_WITHOUT_CHAIN_ID, + mockNetworkClientConfigurationsByNetworkClientId: { + 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + rpcUrl: 'https://test.network', + ticker: 'TEST', + chainId: undefined, + }), + }, + network: { + state: { + selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + }, + }, messengerOptions: { addTransactionApprovalRequest: { state: 'approved', @@ -2015,10 +1988,13 @@ describe('TransactionController', () => { }); it('if unexpected status', async () => { - const { controller, messenger } = setupController(); - - jest.spyOn(messenger, 'call').mockImplementationOnce(() => { - throw new Error('Unknown problem'); + const { controller } = setupController({ + messengerOptions: { + addTransactionApprovalRequest: { + state: 'rejected', + error: new Error('Unknown problem'), + }, + }, }); const { result } = await controller.addTransaction({ @@ -2033,10 +2009,13 @@ describe('TransactionController', () => { }); it('if unrecognised error', async () => { - const { controller, messenger } = setupController(); - - jest.spyOn(messenger, 'call').mockImplementationOnce(() => { - throw new Error('TestError'); + const { controller } = setupController({ + messengerOptions: { + addTransactionApprovalRequest: { + state: 'rejected', + error: new Error('TestError'), + }, + }, }); const { result } = await controller.addTransaction({ @@ -2051,12 +2030,8 @@ describe('TransactionController', () => { }); it('if transaction removed', async () => { - const { controller, messenger } = setupController(); - - jest.spyOn(messenger, 'call').mockImplementationOnce(() => { - controller.clearUnapprovedTransactions(); - throw new Error('Unknown problem'); - }); + const { controller, mockTransactionApprovalRequest } = + setupController(); const { result } = await controller.addTransaction({ from: ACCOUNT_MOCK, @@ -2066,6 +2041,9 @@ describe('TransactionController', () => { value: '0x0', }); + controller.clearUnapprovedTransactions(); + mockTransactionApprovalRequest.reject(new Error('Unknown problem')); + await expect(result).rejects.toThrow('Unknown problem'); }); }); @@ -4051,7 +4029,7 @@ describe('TransactionController', () => { const confirmed = { ...TRANSACTION_META_MOCK, id: 'testId1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, hash: '0x3', status: TransactionStatus.confirmed, txParams: { ...TRANSACTION_META_MOCK.txParams, nonce: '0x1' }, @@ -4263,7 +4241,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await expect( @@ -4293,7 +4271,7 @@ describe('TransactionController', () => { gas: '0x5208', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; // Send the transaction to put it in the process of being signed @@ -4324,7 +4302,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -4332,7 +4310,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const result = await controller.approveTransactionsWithSameNonce([ @@ -4363,7 +4341,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -4371,7 +4349,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await expect( @@ -4391,7 +4369,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { @@ -4400,7 +4378,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await controller.approveTransactionsWithSameNonce( @@ -4424,7 +4402,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; const mockTransactionParam2 = { @@ -4433,7 +4411,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, }; await controller.approveTransactionsWithSameNonce([ @@ -5011,13 +4989,17 @@ describe('TransactionController', () => { state: mockedControllerState as any, }, }); - jest.spyOn(messenger, 'call'); + const messengerCallSpy = jest.spyOn(messenger, 'call'); controller.initApprovals(); await flushPromises(); - expect(messenger.call).toHaveBeenCalledTimes(2); - expect(messenger.call).toHaveBeenCalledWith( + expect( + messengerCallSpy.mock.calls.filter( + (args) => args[0] === 'ApprovalController:addRequest', + ), + ).toHaveLength(2); + expect(messengerCallSpy).toHaveBeenCalledWith( 'ApprovalController:addRequest', { expectsResult: true, @@ -5028,7 +5010,7 @@ describe('TransactionController', () => { }, false, ); - expect(messenger.call).toHaveBeenCalledWith( + expect(messengerCallSpy).toHaveBeenCalledWith( 'ApprovalController:addRequest', { expectsResult: true, @@ -5073,16 +5055,17 @@ describe('TransactionController', () => { const mockedErrorMessage = 'mocked error'; - const { controller, messenger } = setupController({ - options: { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: mockedControllerState as any, - }, - }); + const { controller, messenger, mockTransactionApprovalRequest } = + setupController({ + options: { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: mockedControllerState as any, + }, + }); + const messengerCallSpy = jest.spyOn(messenger, 'call'); // Expect both calls to throw error, one with code property to check if it is handled - jest - .spyOn(messenger, 'call') + mockTransactionApprovalRequest.actionHandlerMock .mockImplementationOnce(() => { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw { message: mockedErrorMessage }; @@ -5105,7 +5088,11 @@ describe('TransactionController', () => { 'Error during persisted transaction approval', new Error(mockedErrorMessage), ); - expect(messenger.call).toHaveBeenCalledTimes(2); + expect( + messengerCallSpy.mock.calls.filter((args) => { + return args[0] === 'ApprovalController:addRequest'; + }), + ).toHaveLength(2); }); it('does not create any approval when there is no unapproved transaction', async () => { @@ -5113,7 +5100,9 @@ describe('TransactionController', () => { jest.spyOn(messenger, 'call'); controller.initApprovals(); await flushPromises(); - expect(messenger.call).not.toHaveBeenCalled(); + expect(messenger.call).not.toHaveBeenCalledWith( + 'ApprovalController:addRequest', + ); }); }); @@ -5279,7 +5268,7 @@ describe('TransactionController', () => { it('returns transactions matching current network', () => { const transactions: TransactionMeta[] = [ { - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, id: 'testId1', status: TransactionStatus.confirmed, time: 1, @@ -5293,7 +5282,7 @@ describe('TransactionController', () => { txParams: { from: '0x2' }, }, { - chainId: MOCK_NETWORK.state.providerConfig.chainId, + chainId: MOCK_NETWORK.chainId, id: 'testId3', status: TransactionStatus.submitted, time: 1, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 94dae32636..88a337cf1d 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -15,10 +15,10 @@ import type { import { BaseController } from '@metamask/base-controller'; import { query, - NetworkType, ApprovalType, ORIGIN_METAMASK, convertHexToDecimal, + isInfuraNetworkType, } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { @@ -35,7 +35,6 @@ import type { NetworkControllerFindNetworkClientIdByChainIdAction, NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; -import { NetworkClientType } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; import { add0x } from '@metamask/utils'; @@ -2946,14 +2945,9 @@ export class TransactionController extends BaseController< return { meta: transaction, isCompleted }; } - private getChainId(networkClientId?: NetworkClientId): Hex { - const globalChainId = this.#getGlobalChainId(); - const globalNetworkClientId = this.#getGlobalNetworkClientId(); - - if (!networkClientId || networkClientId === globalNetworkClientId) { - return globalChainId; - } - + private getChainId( + networkClientId: NetworkClientId = this.#getGlobalNetworkClientId(), + ): Hex { return this.messagingSystem.call( `NetworkController:getNetworkClientById`, networkClientId, @@ -3809,21 +3803,12 @@ export class TransactionController extends BaseController< } #getGlobalChainId() { - return this.getNetworkState().providerConfig.chainId; + return this.getChainId(); } - #isCustomNetwork(networkClientId?: NetworkClientId) { - const globalNetworkClientId = this.#getGlobalNetworkClientId(); - - if (!networkClientId || networkClientId === globalNetworkClientId) { - return this.getNetworkState().providerConfig.type === NetworkType.rpc; - } - - return ( - this.messagingSystem.call( - `NetworkController:getNetworkClientById`, - networkClientId, - ).configuration.type === NetworkClientType.Custom - ); + #isCustomNetwork( + networkClientId: NetworkClientId = this.#getGlobalNetworkClientId(), + ) { + return !isInfuraNetworkType(networkClientId); } }