diff --git a/packages/assets-controllers/src/AssetsContractController.test.ts b/packages/assets-controllers/src/AssetsContractController.test.ts index 86e9c24d41..01f3e017d6 100644 --- a/packages/assets-controllers/src/AssetsContractController.test.ts +++ b/packages/assets-controllers/src/AssetsContractController.test.ts @@ -35,6 +35,7 @@ const setupControllers = () => { }); const network = new NetworkController({ messenger, + trackMetaMetricsEvent: jest.fn(), }); const preferences = new PreferencesController(); const assetsContract = new AssetsContractController({ diff --git a/packages/assets-controllers/src/TokenBalancesController.test.ts b/packages/assets-controllers/src/TokenBalancesController.test.ts index 054608c112..bc1e0dc2ad 100644 --- a/packages/assets-controllers/src/TokenBalancesController.test.ts +++ b/packages/assets-controllers/src/TokenBalancesController.test.ts @@ -130,6 +130,7 @@ describe('TokenBalancesController', () => { new NetworkController({ messenger, infuraProjectId: 'potato', + trackMetaMetricsEvent: jest.fn(), }); const preferences = new PreferencesController(); return { messenger, preferences }; diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 577efc9f5e..e24db98d9a 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -213,7 +213,7 @@ describe('TokenRatesController', () => { }); it('should update all rates', async () => { - new NetworkController({ messenger }); + new NetworkController({ messenger, trackMetaMetricsEvent: jest.fn() }); const preferences = new PreferencesController(); const tokensController = new TokensController({ onPreferencesStateChange: (listener) => preferences.subscribe(listener), diff --git a/packages/controller-utils/src/constants.ts b/packages/controller-utils/src/constants.ts index a104bf75b5..b09b8664e6 100644 --- a/packages/controller-utils/src/constants.ts +++ b/packages/controller-utils/src/constants.ts @@ -7,6 +7,11 @@ export const IPFS_DEFAULT_GATEWAY_URL = 'https://cloudflare-ipfs.com/ipfs/'; // NETWORKS ID export const GANACHE_CHAIN_ID = '1337'; +/** + * The largest possible chain ID we can handle. + * Explanation: https://gist.github.com/rekmarks/a47bd5f2525936c4b8eee31a16345553 + */ +export const MAX_SAFE_CHAIN_ID = 4503599627370476; // TOKEN STANDARDS export const ERC721 = 'ERC721'; diff --git a/packages/controller-utils/src/util.test.ts b/packages/controller-utils/src/util.test.ts index 540d3868e1..ec5b3ad935 100644 --- a/packages/controller-utils/src/util.test.ts +++ b/packages/controller-utils/src/util.test.ts @@ -1,6 +1,7 @@ import { BN } from 'ethereumjs-util'; import nock from 'nock'; import * as util from './util'; +import { MAX_SAFE_CHAIN_ID } from './constants'; const VALID = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0'; const SOME_API = 'https://someapi.com'; @@ -11,6 +12,14 @@ describe('util', () => { nock.cleanAll(); }); + it('isSafeChainId', () => { + expect(util.isSafeChainId(MAX_SAFE_CHAIN_ID + 1)).toBe(false); + expect(util.isSafeChainId(MAX_SAFE_CHAIN_ID)).toBe(true); + expect(util.isSafeChainId(0)).toBe(false); + // @ts-expect-error - ensure that string args return false. + expect(util.isSafeChainId('test')).toBe(false); + }); + it('bNToHex', () => { expect(util.BNToHex(new BN('1337'))).toBe('0x539'); }); diff --git a/packages/controller-utils/src/util.ts b/packages/controller-utils/src/util.ts index d16fbf8adb..19afd9be88 100644 --- a/packages/controller-utils/src/util.ts +++ b/packages/controller-utils/src/util.ts @@ -10,9 +10,23 @@ import { fromWei, toWei } from 'ethjs-unit'; import ensNamehash from 'eth-ens-namehash'; import deepEqual from 'fast-deep-equal'; import type { Json } from './types'; +import { MAX_SAFE_CHAIN_ID } from './constants'; const TIMEOUT_ERROR = new Error('timeout'); +/** + * Checks whether the given number primitive chain ID is safe. + * Because some cryptographic libraries we use expect the chain ID to be a + * number primitive, it must not exceed a certain size. + * + * @param chainId - The chain ID to check for safety. + * @returns Whether the given chain ID is safe. + */ +export function isSafeChainId(chainId: number): boolean { + return ( + Number.isSafeInteger(chainId) && chainId > 0 && chainId <= MAX_SAFE_CHAIN_ID + ); +} /** * Converts a BN object to a hex string with a '0x' prefix. * diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index 26bff8cd36..a569fadb5f 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -54,6 +54,7 @@ const setupNetworkController = ( const network = new NetworkController({ messenger: networkMessenger, infuraProjectId: '123', + trackMetaMetricsEvent: jest.fn(), }); return { network, networkMessenger }; diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index bf58c9ed74..84ea5e14c1 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -32,11 +32,13 @@ "@metamask/base-controller": "workspace:^", "@metamask/controller-utils": "workspace:^", "@metamask/swappable-obj-proxy": "^2.1.0", + "@metamask/utils": "^3.3.1", "async-mutex": "^0.2.6", "babel-runtime": "^6.26.0", "eth-json-rpc-infura": "^5.1.0", "eth-query": "^2.1.2", "immer": "^9.0.6", + "uuid": "^8.3.2", "web3-provider-engine": "^16.0.3" }, "devDependencies": { diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 7a24e649d7..710cd34a48 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -5,6 +5,7 @@ import createMetamaskProvider from 'web3-provider-engine/zero'; import { createEventEmitterProxy } from '@metamask/swappable-obj-proxy'; import type { SwappableProxy } from '@metamask/swappable-obj-proxy'; import { Mutex } from 'async-mutex'; +import { v4 as random } from 'uuid'; import type { Patch } from 'immer'; import { BaseControllerV2, @@ -16,8 +17,11 @@ import { TESTNET_NETWORK_TYPE_TO_TICKER_SYMBOL, NetworksChainId, NetworkType, + isSafeChainId, } from '@metamask/controller-utils'; +import { assertIsStrictHexString } from '@metamask/utils'; + /** * @type ProviderConfig * @@ -27,6 +31,7 @@ import { * @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 = { rpcTarget?: string; @@ -34,6 +39,7 @@ export type ProviderConfig = { chainId: string; ticker?: string; nickname?: string; + id?: string; }; export type Block = { @@ -44,19 +50,41 @@ export type NetworkDetails = { isEIP1559Compatible?: boolean; }; +/** + * Custom RPC network information + * + * @property rpcTarget - RPC target URL. + * @property chainId - Network ID as per EIP-155 + * @property nickname - Personalized network name. + * @property ticker - Currency ticker. + * @property rpcPrefs - Personalized preferences. + */ +export type NetworkConfiguration = { + rpcUrl: string; + chainId: string; + ticker: string; + nickname?: string; + rpcPrefs?: { + blockExplorerUrl: string; + }; +}; + /** * @type NetworkState * * Network controller state - * @property network - Network ID as per net_version - * @property isCustomNetwork - Identifies if the network is a custom network - * @property provider - RPC URL and network name provider settings + * @property network - Network ID as per net_version of the currently connected network + * @property isCustomNetwork - Identifies if the currently connected network is a custom network + * @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 = { network: string; isCustomNetwork: boolean; providerConfig: ProviderConfig; networkDetails: NetworkDetails; + networkConfigurations: Record; }; const LOCALHOST_RPC_URL = 'http://localhost:8545'; @@ -112,6 +140,7 @@ export type NetworkControllerMessenger = RestrictedControllerMessenger< export type NetworkControllerOptions = { messenger: NetworkControllerMessenger; + trackMetaMetricsEvent: () => void; infuraProjectId?: string; state?: Partial; }; @@ -121,6 +150,20 @@ export const defaultState: NetworkState = { isCustomNetwork: false, providerConfig: { type: MAINNET, chainId: NetworksChainId.mainnet }, networkDetails: { isEIP1559Compatible: false }, + networkConfigurations: {}, +}; + +type MetaMetricsEventPayload = { + event: string; + category: string; + referrer?: { url: string }; + actionId?: number; + environmentType?: string; + properties?: unknown; + sensitiveProperties?: unknown; + revenue?: number; + currency?: string; + value?: number; }; /** @@ -137,6 +180,8 @@ export class NetworkController extends BaseControllerV2< private infuraProjectId: string | undefined; + private trackMetaMetricsEvent: (event: MetaMetricsEventPayload) => void; + private mutex = new Mutex(); #provider: Provider | undefined; @@ -145,7 +190,12 @@ export class NetworkController extends BaseControllerV2< #blockTrackerProxy: BlockTrackerProxy | undefined; - constructor({ messenger, state, infuraProjectId }: NetworkControllerOptions) { + constructor({ + messenger, + state, + infuraProjectId, + trackMetaMetricsEvent, + }: NetworkControllerOptions) { super({ name, metadata: { @@ -165,11 +215,16 @@ export class NetworkController extends BaseControllerV2< persist: true, anonymous: false, }, + networkConfigurations: { + persist: true, + anonymous: false, + }, }, messenger, state: { ...defaultState, ...state }, }); this.infuraProjectId = infuraProjectId; + this.trackMetaMetricsEvent = trackMetaMetricsEvent; this.messagingSystem.registerActionHandler( `${this.name}:getProviderConfig`, () => { @@ -405,24 +460,27 @@ export class NetworkController extends BaseControllerV2< /** * Convenience method to update provider RPC settings. * - * @param rpcTarget - The RPC endpoint URL. - * @param chainId - The chain ID as per EIP-155. - * @param ticker - The currency ticker. - * @param nickname - Personalized network name. + * @param networkConfigurationId - The unique id for the network configuration to set as the active provider. */ - setRpcTarget( - rpcTarget: string, - chainId: string, - ticker?: string, - nickname?: string, - ) { + setActiveNetwork(networkConfigurationId: string) { + const targetNetwork = + this.state.networkConfigurations[networkConfigurationId]; + + if (!targetNetwork) { + throw new Error( + `networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`, + ); + } + this.update((state) => { state.providerConfig.type = RPC; - state.providerConfig.rpcTarget = rpcTarget; - state.providerConfig.chainId = chainId; - state.providerConfig.ticker = ticker; - state.providerConfig.nickname = nickname; + state.providerConfig.rpcTarget = targetNetwork.rpcUrl; + state.providerConfig.chainId = targetNetwork.chainId; + state.providerConfig.ticker = targetNetwork.ticker; + state.providerConfig.nickname = targetNetwork.nickname; + state.providerConfig.id = targetNetwork.id; }); + this.refreshNetwork(); } @@ -481,6 +539,131 @@ export class NetworkController extends BaseControllerV2< }); } } + + /** + * Adds a network configuration if the rpcUrl is not already present on an + * existing network configuration. Otherwise updates the entry with the matching rpcUrl. + * + * @param networkConfiguration - The network configuration to add or, if rpcUrl matches an existing entry, to modify. + * @param networkConfiguration.rpcUrl - RPC provider url. + * @param networkConfiguration.chainId - Network ID as per EIP-155. + * @param networkConfiguration.ticker - Currency ticker. + * @param networkConfiguration.nickname - Personalized network name. + * @param networkConfiguration.rpcPrefs - Personalized preferences (i.e. preferred blockExplorer) + * @param options - additional configuration options. + * @param options.setActive - An option to set the newly added networkConfiguration as the active provider. + * @param options.referrer - The site from which the call originated, or 'metamask' for internal calls - used for event metrics. + * @param options.source - Where the upsertNetwork event originated (i.e. from a dapp or from the network form) - used for event metrics. + * @returns id for the added or updated network configuration + */ + upsertNetworkConfiguration( + { rpcUrl, chainId, ticker, nickname, rpcPrefs }: NetworkConfiguration, + { + setActive = false, + referrer, + source, + }: { setActive?: boolean; referrer: string; source: string }, + ): string { + assertIsStrictHexString(chainId); + + if (!isSafeChainId(parseInt(chainId, 16))) { + throw new Error( + `Invalid chain ID "${chainId}": numerical value greater than max safe value.`, + ); + } + + if (!rpcUrl) { + throw new Error( + 'An rpcUrl is required to add or update network configuration', + ); + } + + if (!referrer || !source) { + throw new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ); + } + + try { + // eslint-disable-next-line no-new + new URL(rpcUrl); + } catch (e: any) { + if (e.message.includes('Invalid URL')) { + throw new Error('rpcUrl must be a valid URL'); + } + } + + if (!ticker) { + throw new Error( + 'A ticker is required to add or update networkConfiguration', + ); + } + + const newNetworkConfiguration = { + rpcUrl, + chainId, + ticker, + nickname, + rpcPrefs, + }; + + const oldNetworkConfigurations = this.state.networkConfigurations; + + const oldNetworkConfigurationId = Object.values( + oldNetworkConfigurations, + ).find( + (networkConfiguration) => + networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(), + )?.id; + + const newNetworkConfigurationId = oldNetworkConfigurationId || random(); + this.update((state) => { + state.networkConfigurations = { + ...oldNetworkConfigurations, + [newNetworkConfigurationId]: { + ...newNetworkConfiguration, + id: newNetworkConfigurationId, + }, + }; + }); + + if (!oldNetworkConfigurationId) { + this.trackMetaMetricsEvent({ + event: 'Custom Network Added', + category: 'Network', + referrer: { + url: referrer, + }, + properties: { + chain_id: chainId, + symbol: ticker, + source, + }, + }); + } + + if (setActive) { + this.setActiveNetwork(newNetworkConfigurationId); + } + + return newNetworkConfigurationId; + } + + /** + * Removes network configuration from state. + * + * @param networkConfigurationId - The networkConfigurationId of an existing network configuration + */ + removeNetworkConfiguration(networkConfigurationId: string) { + if (!this.state.networkConfigurations[networkConfigurationId]) { + throw new Error( + `networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`, + ); + } + this.update((state) => { + delete state.networkConfigurations[networkConfigurationId]; + }); + } } export default NetworkController; diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 6af6ed8b48..8f249040cc 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -9,6 +9,8 @@ import createInfuraProvider from 'eth-json-rpc-infura/src/createProvider'; import type { ProviderEngine } from 'web3-provider-engine'; import createMetamaskProvider from 'web3-provider-engine/zero'; import { Patch } from 'immer'; +import { v4 } from 'uuid'; +import { NetworkType } from '@metamask/controller-utils'; import { waitForResult } from '../../../tests/helpers'; import { NetworkController, @@ -32,6 +34,15 @@ jest.mock('web3-provider-engine/subproviders/provider'); jest.mock('eth-json-rpc-infura/src/createProvider'); jest.mock('web3-provider-engine/zero'); +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + // Store this up front so it doesn't get lost when it is stubbed const originalSetTimeout = global.setTimeout; @@ -39,7 +50,7 @@ const SubproviderMock = mocked(Subprovider); const createInfuraProviderMock = mocked(createInfuraProvider); const createMetamaskProviderMock = mocked(createMetamaskProvider); -// setProviderType setRpcTarget +// setProviderType setActiveNetwork // └───────────┬────────────┘ // set providerConfig refreshNetwork // │ │ └────────────────────────────────────────────┬──────────────────────────────────────────────┘ │ @@ -72,6 +83,7 @@ describe('NetworkController', () => { it('initializes the state with some defaults', async () => { await withController(({ controller }) => { expect(controller.state).toStrictEqual({ + networkConfigurations: {}, network: 'loading', isCustomNetwork: false, providerConfig: { type: 'mainnet' as const, chainId: '1' }, @@ -90,6 +102,7 @@ describe('NetworkController', () => { }, ({ controller }) => { expect(controller.state).toStrictEqual({ + networkConfigurations: {}, network: 'loading', isCustomNetwork: true, providerConfig: { type: 'mainnet', chainId: '1' }, @@ -2277,72 +2290,110 @@ describe('NetworkController', () => { }); }); - describe('setRpcTarget', () => { - describe('given only an RPC target and chain ID', () => { - it('updates the provider config in state with the RPC target and chain ID, clearing any existing ticker and nickname', async () => { - const messenger = buildMessenger(); - await withController( - { - messenger, - state: { - providerConfig: { - type: 'localhost', - rpcTarget: 'http://somethingexisting.com', + describe('setActiveNetwork', () => { + it('updates the provider config in state with the rpcTarget and chainId, clearing the previous provider details', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + providerConfig: { + type: 'localhost', + rpcTarget: 'http://somethingexisting.com', + chainId: '99999', + ticker: 'something existing', + nickname: 'something existing', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://somethingexisting.com', chainId: '99999', ticker: 'something existing', nickname: 'something existing', + id: 'testNetworkConfigurationId2', }, }, }, - async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider(); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); + }, + async ({ controller }) => { + const fakeMetamaskProvider = buildFakeMetamaskProvider(); + createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - await waitForStateChanges(messenger, { - propertyPath: ['network'], - produceStateChanges: () => { - controller.setRpcTarget('http://example.com', '123'); - }, - }); + await waitForStateChanges(messenger, { + propertyPath: ['network'], + produceStateChanges: () => { + controller.setActiveNetwork('testNetworkConfigurationId'); + }, + }); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'rpc', - rpcTarget: 'http://example.com', - chainId: '123', - ticker: undefined, - nickname: undefined, - }); - }, - ); - }); + expect(controller.state.providerConfig).toStrictEqual({ + type: 'rpc', + rpcTarget: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + nickname: undefined, + }); + }, + ); + }); - it('sets isCustomNetwork in state to true', async () => { - const messenger = buildMessenger(); - await withController( - { - messenger, - state: { - isCustomNetwork: false, + it('sets isCustomNetwork in state to true', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + isCustomNetwork: false, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, - async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider(); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); + }, + async ({ controller }) => { + const fakeMetamaskProvider = buildFakeMetamaskProvider(); + createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - await waitForStateChanges(messenger, { - propertyPath: ['isCustomNetwork'], - produceStateChanges: () => { - controller.setRpcTarget('http://example.com', '123'); - }, - }); + await waitForStateChanges(messenger, { + propertyPath: ['isCustomNetwork'], + produceStateChanges: () => { + controller.setActiveNetwork('testNetworkConfigurationId'); + }, + }); - expect(controller.state.isCustomNetwork).toBe(true); - }, - ); - }); + expect(controller.state.isCustomNetwork).toBe(true); + }, + ); + }); - it('sets the provider to a custom RPC provider initialized with the RPC target and chain ID, leaving nickname and ticker undefined', async () => { - await withController(async ({ controller }) => { + it('sets the provider to a custom RPC provider initialized with the RPC target and chain ID, leaving nickname and ticker undefined', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { const fakeMetamaskProvider = buildFakeMetamaskProvider([ { request: { @@ -2355,14 +2406,14 @@ describe('NetworkController', () => { ]); createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - controller.setRpcTarget('http://example.com', '123'); + controller.setActiveNetwork('testNetworkConfigurationId'); expect(createMetamaskProviderMock).toHaveBeenCalledWith({ - chainId: '123', - engineParams: { pollingInterval: 12000 }, + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', nickname: undefined, - rpcUrl: 'http://example.com', - ticker: undefined, + engineParams: { pollingInterval: 12000 }, }); const { provider } = controller.getProviderAndBlockTracker(); const promisifiedSendAsync = promisify(provider.sendAsync).bind( @@ -2372,12 +2423,28 @@ describe('NetworkController', () => { method: 'eth_chainId', }); expect(chainIdResult.result).toBe('0x1337'); - }); - }); + }, + ); + }); - it('updates networkDetails.isEIP1559Compatible in state based on the latest block (assuming that the request for eth_getBlockByNumber is made successfully)', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { + it('updates networkDetails.isEIP1559Compatible in state based on the latest block (assuming that the request for eth_getBlockByNumber is made successfully)', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + isCustomNetwork: false, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { const fakeMetamaskProvider = buildFakeMetamaskProvider([ { request: { @@ -2396,18 +2463,34 @@ describe('NetworkController', () => { await waitForStateChanges(messenger, { propertyPath: ['networkDetails', 'isEIP1559Compatible'], produceStateChanges: () => { - controller.setRpcTarget('http://example.com', '123'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); expect(controller.state.networkDetails.isEIP1559Compatible).toBe( true, ); - }); - }); + }, + ); + }); - it('ensures that the existing provider is stopped while replacing it', async () => { - await withController(({ controller }) => { + it('ensures that the existing provider is stopped while replacing it', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + ({ controller }) => { const fakeMetamaskProviders = [ buildFakeMetamaskProvider(), buildFakeMetamaskProvider(), @@ -2417,18 +2500,33 @@ describe('NetworkController', () => { .mockImplementationOnce(() => fakeMetamaskProviders[0]) .mockImplementationOnce(() => fakeMetamaskProviders[1]); - controller.setRpcTarget('http://example.com', '123'); - controller.setRpcTarget('http://example.com', '123'); + controller.setActiveNetwork('testNetworkConfigurationId'); + controller.setActiveNetwork('testNetworkConfigurationId'); assert(controller.getProviderAndBlockTracker().provider); jest.runAllTimers(); expect(fakeMetamaskProviders[0].stop).toHaveBeenCalled(); - }); - }); + }, + ); + }); - it('updates the version of the current network in state (assuming that the request for net_version is made successfully)', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { + it('updates the version of the current network in state (assuming that the request for net_version is made successfully)', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { const fakeMetamaskProvider = buildFakeMetamaskProvider([ { request: { @@ -2445,19 +2543,34 @@ describe('NetworkController', () => { await waitForStateChanges(messenger, { propertyPath: ['network'], produceStateChanges: () => { - controller.setRpcTarget('http://example.com', '123'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); expect(controller.state.network).toBe('42'); - }); - }); + }, + ); + }); - describe('when an "error" event occurs on the new provider', () => { - describe('if the network version could not be retrieved during the call to setRpcTarget', () => { - it('retrieves the network version again and, assuming success, persists it to state', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { + describe('when an "error" event occurs on the new provider', () => { + describe('if the network version could not be retrieved during the call to setActiveNetwork', () => { + it('retrieves the network version again and, assuming success, persists it to state', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { const fakeMetamaskProvider = buildFakeMetamaskProvider([ { request: { @@ -2483,7 +2596,7 @@ describe('NetworkController', () => { 'NetworkController:providerConfigChange', { produceEvents: () => { - controller.setRpcTarget('http://example.com', '123'); + controller.setActiveNetwork('testNetworkConfigurationId'); assert(controller.getProviderAndBlockTracker().provider); }, }, @@ -2498,14 +2611,29 @@ describe('NetworkController', () => { }, }); expect(controller.state.network).toBe('42'); - }); - }); + }, + ); }); + }); - describe('if the network version could be retrieved during the call to setRpcTarget', () => { - it('does not retrieve the network version again', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { + describe('if the network version could be retrieved during the call to setActiveNetwork', () => { + it('does not retrieve the network version again', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { const fakeMetamaskProvider = buildFakeMetamaskProvider([ { request: { @@ -2531,7 +2659,7 @@ describe('NetworkController', () => { 'NetworkController:providerConfigChange', { produceEvents: () => { - controller.setRpcTarget('http://example.com', '123'); + controller.setActiveNetwork('testNetworkConfigurationId'); assert(controller.getProviderAndBlockTracker().provider); }, }, @@ -2547,342 +2675,61 @@ describe('NetworkController', () => { }, }); expect(controller.state.network).toBe('1'); - }); - }); + }, + ); }); }); }); + }); - describe('given an RPC target, chain ID, ticker, and nickname', () => { - it('updates the provider config in state with the RPC target, chain ID, ticker, and nickname', async () => { - const messenger = buildMessenger(); - await withController( - { - messenger, - state: { - providerConfig: { - type: 'localhost', - rpcTarget: 'http://somethingexisting.com', - chainId: '99999', - ticker: 'something existing', - nickname: 'something existing', + describe('getEIP1559Compatibility', () => { + describe('if the state does not have a "networkDetails" property', () => { + describe('if no error is thrown while fetching the latest block', () => { + describe('if the block has a "baseFeePerGas" property', () => { + it('updates isEIP1559Compatible in state to true', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + // no "networkDetails" property + }, }, - }, - }, - async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider(); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); + async ({ controller }) => { + await setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + result: { + baseFeePerGas: '0x100', + }, + }, + }, + ], + stubGetEIP1559CompatibilityWhileSetting: true, + }); - await waitForStateChanges(messenger, { - propertyPath: ['network'], - produceStateChanges: () => { - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); + await waitForStateChanges(messenger, { + propertyPath: ['networkDetails', 'isEIP1559Compatible'], + produceStateChanges: async () => { + await controller.getEIP1559Compatibility(); + }, + }); + + expect( + controller.state.networkDetails.isEIP1559Compatible, + ).toBe(true); }, - }); + ); + }); - expect(controller.state.providerConfig).toStrictEqual({ - type: 'rpc', - rpcTarget: 'http://example.com', - chainId: '123', - ticker: 'ABC', - nickname: 'cool network', - }); - }, - ); - }); - - it('sets isCustomNetwork in state to true', async () => { - const messenger = buildMessenger(); - await withController( - { - messenger, - state: { - isCustomNetwork: false, - }, - }, - async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider(); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - - await waitForStateChanges(messenger, { - propertyPath: ['isCustomNetwork'], - produceStateChanges: () => { - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - }, - }); - - expect(controller.state.isCustomNetwork).toBe(true); - }, - ); - }); - - it('sets the provider to a custom RPC provider initialized with the RPC target, chain ID, and ticker, ignoring the nickname', async () => { - await withController(async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider([ - { - request: { - method: 'eth_chainId', - }, - response: { - result: '0x1337', - }, - }, - ]); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - - expect(createMetamaskProviderMock).toHaveBeenCalledWith({ - chainId: '123', - engineParams: { pollingInterval: 12000 }, - nickname: undefined, - rpcUrl: 'http://example.com', - ticker: 'ABC', - }); - const { provider } = controller.getProviderAndBlockTracker(); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const chainIdResult = await promisifiedSendAsync({ - method: 'eth_chainId', - }); - expect(chainIdResult.result).toBe('0x1337'); - }); - }); - - it('updates networkDetails.isEIP1559Compatible in state based on the latest block (assuming that the request for eth_getBlockByNumber is made successfully)', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider([ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', - }, - }, - }, - ]); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - - await waitForStateChanges(messenger, { - propertyPath: ['networkDetails', 'isEIP1559Compatible'], - produceStateChanges: () => { - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - }, - }); - - expect(controller.state.networkDetails.isEIP1559Compatible).toBe( - true, - ); - }); - }); - - it('ensures that the existing provider is stopped while replacing it', async () => { - await withController(({ controller }) => { - const fakeMetamaskProviders = [ - buildFakeMetamaskProvider(), - buildFakeMetamaskProvider(), - ]; - jest.spyOn(fakeMetamaskProviders[0], 'stop'); - createMetamaskProviderMock - .mockImplementationOnce(() => fakeMetamaskProviders[0]) - .mockImplementationOnce(() => fakeMetamaskProviders[1]); - - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - assert(controller.getProviderAndBlockTracker().provider); - jest.runAllTimers(); - - expect(fakeMetamaskProviders[0].stop).toHaveBeenCalled(); - }); - }); - - it('updates the version of the current network in state (assuming that the request for net_version is made successfully)', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider([ - { - request: { - method: 'net_version', - params: [], - }, - response: { - result: '42', - }, - }, - ]); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - - await waitForStateChanges(messenger, { - propertyPath: ['network'], - produceStateChanges: () => { - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - }, - }); - - expect(controller.state.network).toBe('42'); - }); - }); - - describe('when an "error" event occurs on the new provider', () => { - describe('if the network version could not be retrieved during the call to setRpcTarget', () => { - it('retrieves the network version again and, assuming success, persists it to state', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider([ - { - request: { - method: 'net_version', - }, - response: { - error: 'oops', - }, - }, - { - request: { - method: 'net_version', - }, - response: { - result: '42', - }, - }, - ]); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - - await waitForPublishedEvents( - messenger, - 'NetworkController:providerConfigChange', - { - produceEvents: () => { - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - assert(controller.getProviderAndBlockTracker().provider); - }, - }, - ); - - await waitForStateChanges(messenger, { - propertyPath: ['network'], - produceStateChanges: () => { - controller - .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); - }, - }); - expect(controller.state.network).toBe('42'); - }); - }); - }); - - describe('if the network version could be retrieved during the call to setRpcTarget', () => { - it('does not retrieve the network version again', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { - const fakeMetamaskProvider = buildFakeMetamaskProvider([ - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]); - createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); - - await waitForPublishedEvents( - messenger, - 'NetworkController:providerConfigChange', - { - produceEvents: () => { - controller.setRpcTarget( - 'http://example.com', - '123', - 'ABC', - 'cool network', - ); - assert(controller.getProviderAndBlockTracker().provider); - }, - }, - ); - - await waitForStateChanges(messenger, { - propertyPath: ['network'], - count: 0, - produceStateChanges: () => { - controller - .getProviderAndBlockTracker() - .provider.emit('error', { some: 'error' }); - }, - }); - expect(controller.state.network).toBe('1'); - }); - }); - }); - }); - }); - }); - - describe('getEIP1559Compatibility', () => { - describe('if the state does not have a "networkDetails" property', () => { - describe('if no error is thrown while fetching the latest block', () => { - describe('if the block has a "baseFeePerGas" property', () => { - it('updates isEIP1559Compatible in state to true', async () => { - const messenger = buildMessenger(); + it('returns a promise that resolves to true', async () => { await withController( { - messenger, state: { // no "networkDetails" property }, @@ -2905,53 +2752,14 @@ describe('NetworkController', () => { stubGetEIP1559CompatibilityWhileSetting: true, }); - await waitForStateChanges(messenger, { - propertyPath: ['networkDetails', 'isEIP1559Compatible'], - produceStateChanges: async () => { - await controller.getEIP1559Compatibility(); - }, - }); + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); - expect( - controller.state.networkDetails.isEIP1559Compatible, - ).toBe(true); + expect(isEIP1559Compatible).toBe(true); }, ); }); - - it('returns a promise that resolves to true', async () => { - await withController( - { - state: { - // no "networkDetails" property - }, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x100', - }, - }, - }, - ], - stubGetEIP1559CompatibilityWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(true); - }, - ); - }); - }); + }); describe('if the block does not have a "baseFeePerGas" property', () => { it('does not change networkDetails.isEIP1559Compatible in state', async () => { @@ -3508,192 +3316,872 @@ describe('NetworkController', () => { stubGetEIP1559CompatibilityWhileSetting: true, }); - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); + const isEIP1559Compatible = + await controller.getEIP1559Compatibility(); + + expect(isEIP1559Compatible).toBe(false); + }, + ); + }); + }); + }); + + describe('if an error is thrown while fetching the latest block', () => { + it('does not change networkDetails.isEIP1559Compatible in state', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkDetails: { + isEIP1559Compatible: false, + }, + }, + }, + async ({ controller }) => { + await setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + error: 'oops', + }, + }, + ], + stubGetEIP1559CompatibilityWhileSetting: true, + }); + const promiseForIsEIP1559CompatibleChanges = waitForStateChanges( + messenger, + { + propertyPath: ['networkDetails', 'isEIP1559Compatible'], + }, + ); + + try { + await controller.getEIP1559Compatibility(); + } catch (error) { + // catch the rejection (it is tested below) + } + + await expect( + promiseForIsEIP1559CompatibleChanges, + ).toNeverResolve(); + }, + ); + }); + + it('returns a promise that rejects with the error', async () => { + await withController( + { + state: { + networkDetails: { + isEIP1559Compatible: false, + }, + }, + }, + async ({ controller }) => { + await setFakeProvider(controller, { + stubs: [ + { + request: { + method: 'eth_getBlockByNumber', + params: ['latest', false], + }, + response: { + error: 'oops', + }, + }, + ], + stubGetEIP1559CompatibilityWhileSetting: true, + }); + + const promiseForIsEIP1559Compatible = + controller.getEIP1559Compatibility(); + + await expect(promiseForIsEIP1559Compatible).rejects.toThrow( + 'oops', + ); + }, + ); + }); + }); + }); + + describe('if isEIP1559Compatible in state is set to true', () => { + it('does not change networkDetails.isEIP1559Compatible in state', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + networkDetails: { + isEIP1559Compatible: true, + }, + }, + }, + async ({ controller }) => { + await setFakeProvider(controller, { + stubGetEIP1559CompatibilityWhileSetting: true, + }); + const promiseForIsEIP1559CompatibleChanges = waitForStateChanges( + messenger, + { propertyPath: ['networkDetails', 'isEIP1559Compatible'] }, + ); + + await controller.getEIP1559Compatibility(); + + await expect(promiseForIsEIP1559CompatibleChanges).toNeverResolve(); + }, + ); + }); + + it('returns a promise that resolves to true', async () => { + await withController( + { + state: { + networkDetails: { + isEIP1559Compatible: true, + }, + }, + }, + async ({ controller }) => { + await setFakeProvider(controller, { + stubGetEIP1559CompatibilityWhileSetting: true, + }); + + const result = await controller.getEIP1559Compatibility(); + + expect(result).toBe(true); + }, + ); + }); + }); + }); + + describe('NetworkController:getProviderConfig action', () => { + it('returns the provider config in state', async () => { + const messenger = buildMessenger(); + await withController( + { + messenger, + state: { + providerConfig: { + type: 'mainnet', + chainId: '1', + }, + }, + }, + async () => { + const providerConfig = await messenger.call( + 'NetworkController:getProviderConfig', + ); + + expect(providerConfig).toStrictEqual({ + type: 'mainnet', + chainId: '1', + }); + }, + ); + }); + }); + + describe('NetworkController:getEthQuery action', () => { + it('returns the EthQuery object set after the provider is set', async () => { + const messenger = buildMessenger(); + await withController({ messenger }, async ({ controller }) => { + const fakeEthQuery = { + sendAsync: jest.fn(), + }; + jest.spyOn(ethQueryModule, 'default').mockReturnValue(fakeEthQuery); + setFakeProvider(controller); + + const ethQuery = await messenger.call('NetworkController:getEthQuery'); + + expect(ethQuery).toBe(fakeEthQuery); + }); + }); + }); + + describe('upsertNetworkConfiguration', () => { + it('adds the given network configuration when its rpcURL does not match an existing configuration', async () => { + (v4 as jest.Mock).mockImplementationOnce( + () => 'network-configuration-id-1', + ); + const messenger = buildMessenger(); + await withController({ messenger }, async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x9999', + rpcUrl: 'https://test-rpc.com', + ticker: 'RPC', + }; + + expect(controller.state.networkConfigurations).toStrictEqual({}); + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: 'dapp', + }); + + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + ...rpcUrlNetwork, + nickname: undefined, + rpcPrefs: undefined, + id: 'network-configuration-id-1', + }, + ]), + ); + }); + }); + + it('update a network configuration when the configuration being added has an rpcURL that matches an existing configuration', async () => { + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://rpc-url.com', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://rpc-url.com', + ticker: 'new_rpc_ticker', + nickname: 'new_rpc_nickname', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + }, + { referrer: 'https://test-dapp.com', source: 'dapp' }, + ); + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + rpcUrl: 'https://rpc-url.com', + nickname: 'new_rpc_nickname', + ticker: 'new_rpc_ticker', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + id: 'testNetworkConfigurationId', + }, + ]), + ); + }, + ); + }); + + it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { + const invalidChainId = '1'; + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: invalidChainId, + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'rpc_url', + ticker: 'RPC', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).toThrow( + new Error('Value must be a hexadecimal string, starting with "0x".'), + ); + }); + }); + + it('throws if the given chain ID is greater than the maximum allowed ID', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: '0xFFFFFFFFFFFFFFFF', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'rpc_url', + ticker: 'RPC', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).toThrow( + new Error( + 'Invalid chain ID "0xFFFFFFFFFFFFFFFF": numerical value greater than max safe value.', + ), + ); + }); + }); + + it('throws if rpcUrl passed is not a valid Url', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: '0x9999', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + ticker: 'RPC', + rpcUrl: 'test', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).toThrow(new Error('rpcUrl must be a valid URL')); + }); + }); + + it('throws if the no (or a falsy) ticker is passed', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + // @ts-expect-error - we want to test the case where no ticker is present. + { + chainId: '0x5', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'https://mock-rpc-url', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', + }, + ), + ).toThrow( + new Error( + 'A ticker is required to add or update networkConfiguration', + ), + ); + }); + }); + + it('throws if an options object is not passed as a second argument', async () => { + await withController(async ({ controller }) => { + expect( + () => + // @ts-expect-error - we want to test the case where no second arg is passed. + controller.upsertNetworkConfiguration({ + chainId: '0x5', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'https://mock-rpc-url', + }), + // eslint-disable-next-line + ).toThrow(); + }); + }); + + it('throws if referrer and source arguments are not passed', async () => { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId'); + const trackEventSpy = jest.fn(); + await withController( + { + state: { + providerConfig: { + type: 'rpc', + rpcTarget: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + trackMetaMetricsEvent: trackEventSpy, + }, + async ({ controller }) => { + const newNetworkConfiguration = { + rpcUrl: 'https://new-chain-rpc-url', + chainId: '0x9999', + ticker: 'NEW', + nickname: 'new-chain', + rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, + }; + + expect(() => + // @ts-expect-error - we want to test the case where the options object is empty. + controller.upsertNetworkConfiguration(newNetworkConfiguration, {}), + ).toThrow( + 'referrer and source are required arguments for adding or updating a network configuration', + ); + }, + ); + }); + + it('should add the given network if all required properties are present but nither rpcPrefs nor nickname properties are passed', async () => { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId'); + await withController( + { + state: { + networkConfigurations: {}, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: 'dapp', + }); + + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + ...rpcUrlNetwork, + nickname: undefined, + rpcPrefs: undefined, + id: 'networkConfigurationId', + }, + ]), + ); + }, + ); + }); + + it('adds new networkConfiguration to networkController store, but only adds valid properties (rpcUrl, chainId, ticker, nickname, rpcPrefs) and fills any missing properties from this list as undefined', async function () { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId'); + await withController( + { + state: { + networkConfigurations: {}, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + invalidKey: 'new-chain', + invalidKey2: {}, + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: 'dapp', + }); + + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + nickname: undefined, + rpcPrefs: undefined, + id: 'networkConfigurationId', + }, + ]), + ); + }, + ); + }); - expect(isEIP1559Compatible).toBe(false); + it('should add the given network configuration if its rpcURL does not match an existing configuration without changing or overwriting other configurations', async () => { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId2'); + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId: { + rpcUrl: 'https://test-rpc-url', + ticker: 'ticker', + nickname: 'nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', }, - ); + }, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + nickname: 'RPC', + rpcPrefs: undefined, + rpcUrl: 'https://test-rpc-url-2', + ticker: 'RPC', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: 'dapp', }); - }); - }); - describe('if an error is thrown while fetching the latest block', () => { - it('does not change networkDetails.isEIP1559Compatible in state', async () => { - const messenger = buildMessenger(); - await withController( - { - messenger, - state: { - networkDetails: { - isEIP1559Compatible: false, - }, + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + rpcUrl: 'https://test-rpc-url', + ticker: 'ticker', + nickname: 'nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + { ...rpcUrlNetwork, id: 'networkConfigurationId2' }, + ]), + ); + }, + ); + }); + + it('should use the given configuration to update an existing network configuration that has a matching rpcUrl', async () => { + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId: { + rpcUrl: 'https://test-rpc-url', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_chainName', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', }, }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - error: 'oops', - }, - }, - ], - stubGetEIP1559CompatibilityWhileSetting: true, - }); - const promiseForIsEIP1559CompatibleChanges = waitForStateChanges( - messenger, - { - propertyPath: ['networkDetails', 'isEIP1559Compatible'], - }, - ); + }, + }, - try { - await controller.getEIP1559Compatibility(); - } catch (error) { - // catch the rejection (it is tested below) - } + async ({ controller }) => { + const updatedConfiguration = { + rpcUrl: 'https://test-rpc-url', + ticker: 'new_rpc_ticker', + nickname: 'new_rpc_chainName', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + }; + controller.upsertNetworkConfiguration(updatedConfiguration, { + referrer: 'https://test-dapp.com', + source: 'dapp', + }); + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual([ + { + rpcUrl: 'https://test-rpc-url', + nickname: 'new_rpc_chainName', + ticker: 'new_rpc_ticker', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + ]); + }, + ); + }); - await expect( - promiseForIsEIP1559CompatibleChanges, - ).toNeverResolve(); + it('should use the given configuration to update an existing network configuration that has a matching rpcUrl without changing or overwriting other networkConfigurations', async () => { + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId: { + rpcUrl: 'https://test-rpc-url', + ticker: 'ticker', + nickname: 'nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + networkConfigurationId2: { + rpcUrl: 'https://test-rpc-url-2', + ticker: 'ticker-2', + nickname: 'nickname-2', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x9999', + id: 'networkConfigurationId2', + }, + }, + }, + }, + async ({ controller }) => { + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test-rpc-url', + ticker: 'new-ticker', + nickname: 'new-nickname', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + }, + { + referrer: 'https://test-dapp.com', + source: 'dapp', }, ); - }); - it('returns a promise that rejects with the error', async () => { - await withController( + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual([ { - state: { - networkDetails: { - isEIP1559Compatible: false, - }, - }, + rpcUrl: 'https://test-rpc-url', + ticker: 'new-ticker', + nickname: 'new-nickname', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - error: 'oops', - }, - }, - ], - stubGetEIP1559CompatibilityWhileSetting: true, - }); - - const promiseForIsEIP1559Compatible = - controller.getEIP1559Compatibility(); - - await expect(promiseForIsEIP1559Compatible).rejects.toThrow( - 'oops', - ); + { + rpcUrl: 'https://test-rpc-url-2', + ticker: 'ticker-2', + nickname: 'nickname-2', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x9999', + id: 'networkConfigurationId2', }, - ); - }); - }); + ]); + }, + ); }); - describe('if isEIP1559Compatible in state is set to true', () => { - it('does not change networkDetails.isEIP1559Compatible in state', async () => { - const messenger = buildMessenger(); - await withController( - { - messenger, - state: { - networkDetails: { - isEIP1559Compatible: true, + it('should add the given network and not set it to active if the setActive option is not passed (or a falsy value is passed)', async () => { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId'); + const originalProvider = { + type: 'rpc' as NetworkType, + rpcTarget: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }; + await withController( + { + state: { + providerConfig: originalProvider, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', }, }, }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubGetEIP1559CompatibilityWhileSetting: true, - }); - const promiseForIsEIP1559CompatibleChanges = waitForStateChanges( - messenger, - { propertyPath: ['networkDetails', 'isEIP1559Compatible'] }, - ); - - await controller.getEIP1559Compatibility(); + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: 'dapp', + }); - await expect(promiseForIsEIP1559CompatibleChanges).toNeverResolve(); - }, - ); - }); + expect(controller.state.providerConfig).toStrictEqual( + originalProvider, + ); + }, + ); + }); - it('returns a promise that resolves to true', async () => { - await withController( - { - state: { - networkDetails: { - isEIP1559Compatible: true, + it('should add the given network and set it to active if the setActive option is passed as true', async () => { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId'); + await withController( + { + state: { + providerConfig: { + type: 'rpc', + rpcTarget: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', }, }, }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubGetEIP1559CompatibilityWhileSetting: true, - }); - - const result = await controller.getEIP1559Compatibility(); + }, + async ({ controller }) => { + const fakeMetamaskProvider = buildFakeMetamaskProvider(); + createMetamaskProviderMock.mockReturnValue(fakeMetamaskProvider); + const rpcUrlNetwork = { + rpcUrl: 'https://test-rpc-url', + chainId: '0x1', + ticker: 'test_ticker', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + setActive: true, + referrer: 'https://test-dapp.com', + source: 'dapp', + }); - expect(result).toBe(true); - }, - ); - }); + expect(controller.state.providerConfig).toStrictEqual({ + type: 'rpc', + rpcTarget: 'https://test-rpc-url', + chainId: '0x1', + ticker: 'test_ticker', + id: 'networkConfigurationId', + nickname: undefined, + }); + }, + ); }); - }); - describe('NetworkController:getProviderConfig action', () => { - it('returns the provider config in state', async () => { - const messenger = buildMessenger(); + it('adds new networkConfiguration to networkController store and calls to the metametrics event tracking with the correct values', async () => { + (v4 as jest.Mock).mockImplementationOnce(() => 'networkConfigurationId'); + const trackEventSpy = jest.fn(); await withController( { - messenger, state: { providerConfig: { - type: 'mainnet', - chainId: '1', + type: 'rpc', + rpcTarget: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, + trackMetaMetricsEvent: trackEventSpy, }, - async () => { - const providerConfig = await messenger.call( - 'NetworkController:getProviderConfig', - ); + async ({ controller }) => { + const newNetworkConfiguration = { + rpcUrl: 'https://new-chain-rpc-url', + chainId: '0x9999', + ticker: 'NEW', + nickname: 'new-chain', + rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, + }; + + controller.upsertNetworkConfiguration(newNetworkConfiguration, { + referrer: 'https://test-dapp.com', + source: 'dapp', + }); - expect(providerConfig).toStrictEqual({ - type: 'mainnet', - chainId: '1', + expect( + Object.values(controller.state.networkConfigurations), + ).toStrictEqual([ + { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + { + ...newNetworkConfiguration, + id: 'networkConfigurationId', + }, + ]); + expect(trackEventSpy).toHaveBeenCalledWith({ + event: 'Custom Network Added', + category: 'Network', + referrer: { + url: 'https://test-dapp.com', + }, + properties: { + chain_id: '0x9999', + symbol: 'NEW', + source: 'dapp', + }, }); }, ); }); }); - describe('NetworkController:getEthQuery action', () => { - it('returns the EthQuery object set after the provider is set', async () => { - const messenger = buildMessenger(); - await withController({ messenger }, async ({ controller }) => { - const fakeEthQuery = { - sendAsync: jest.fn(), - }; - jest.spyOn(ethQueryModule, 'default').mockReturnValue(fakeEthQuery); - setFakeProvider(controller); - - const ethQuery = await messenger.call('NetworkController:getEthQuery'); + describe('removeNetworkConfigurations', () => { + it('remove a network configuration', async () => { + const testNetworkConfigurationId = 'testNetworkConfigurationId'; + await withController( + { + state: { + networkConfigurations: { + [testNetworkConfigurationId]: { + rpcUrl: 'https://rpc-url.com', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '1', + id: testNetworkConfigurationId, + }, + }, + }, + }, + async ({ controller }) => { + controller.removeNetworkConfiguration(testNetworkConfigurationId); + expect(controller.state.networkConfigurations).toStrictEqual({}); + }, + ); + }); - expect(ethQuery).toBe(fakeEthQuery); - }); + it('throws if the networkConfigurationId it is passed does not correspond to a network configuration in state', async () => { + const testNetworkConfigurationId = 'testNetworkConfigurationId'; + const invalidNetworkConfigurationId = 'invalidNetworkConfigurationId'; + await withController( + { + state: { + networkConfigurations: { + [testNetworkConfigurationId]: { + rpcUrl: 'https://rpc-url.com', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '1', + id: testNetworkConfigurationId, + }, + }, + }, + }, + async ({ controller }) => { + expect(() => + controller.removeNetworkConfiguration( + invalidNetworkConfigurationId, + ), + ).toThrow( + `networkConfigurationId ${invalidNetworkConfigurationId} does not match a configured networkConfiguration`, + ); + }, + ); }); }); }); @@ -3750,6 +4238,7 @@ async function withController( args.length === 2 ? args : [{}, args[0]]; const controller = new NetworkController({ messenger, + trackMetaMetricsEvent: jest.fn(), ...rest, }); try { diff --git a/packages/network-controller/tests/provider-api-tests/helpers.ts b/packages/network-controller/tests/provider-api-tests/helpers.ts index acd8887f76..781241c2e1 100644 --- a/packages/network-controller/tests/provider-api-tests/helpers.ts +++ b/packages/network-controller/tests/provider-api-tests/helpers.ts @@ -268,10 +268,9 @@ const makeRpcCall = ( export type ProviderType = 'infura' | 'custom'; export type MockOptions = { - infuraNetwork?: NetworkType; providerType: ProviderType; + infuraNetwork?: NetworkType; customRpcUrl?: string; - customChainId?: string; }; export type MockCommunications = { @@ -370,7 +369,6 @@ export const withNetworkClient = async ( providerType, infuraNetwork = 'mainnet', customRpcUrl = MOCK_RPC_URL, - customChainId = '0x1', }: MockOptions, fn: (client: MockNetworkClient) => Promise, ): Promise => { @@ -386,6 +384,7 @@ export const withNetworkClient = async ( const controller = new NetworkController({ messenger, infuraProjectId: MOCK_INFURA_PROJECT_ID, + trackMetaMetricsEvent: jest.fn(), }); const getEIP1559CompatibilityMock = jest @@ -403,9 +402,15 @@ export const withNetworkClient = async ( if (providerType === 'infura') { controller.setProviderType(infuraNetwork); } else { - controller.setRpcTarget(customRpcUrl, customChainId); + controller.upsertNetworkConfiguration( + { + rpcUrl: customRpcUrl, + chainId: '0x9999', + ticker: 'TEST', + }, + { referrer: 'https://test-dapp.com', source: 'dapp', setActive: true }, + ); } - const ethQuery = messenger.call('NetworkController:getEthQuery'); const { provider, blockTracker } = controller.getProviderAndBlockTracker(); diff --git a/packages/network-controller/tests/provider-api-tests/shared-tests.ts b/packages/network-controller/tests/provider-api-tests/shared-tests.ts index bf8946d2e7..1ea74a8bb3 100644 --- a/packages/network-controller/tests/provider-api-tests/shared-tests.ts +++ b/packages/network-controller/tests/provider-api-tests/shared-tests.ts @@ -367,7 +367,7 @@ export const testsForProviderType = (providerType: ProviderType) => { describe('net_version', () => { it('does hit RPC endpoint to get net_version', async () => { await withMockedCommunications( - { providerType, infuraNetwork: 'goerli', customChainId: '5' }, + { providerType, infuraNetwork: 'goerli' }, async (comms) => { comms.mockRpcCall({ request: { method: 'net_version' }, diff --git a/packages/preferences-controller/jest.config.js b/packages/preferences-controller/jest.config.js index 5fd4fda362..6a84cceb23 100644 --- a/packages/preferences-controller/jest.config.js +++ b/packages/preferences-controller/jest.config.js @@ -17,9 +17,9 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 88.23, - functions: 95, - lines: 93.82, - statements: 93.82, + functions: 93.75, + lines: 92.54, + statements: 92.54, }, }, }); diff --git a/packages/preferences-controller/src/PreferencesController.test.ts b/packages/preferences-controller/src/PreferencesController.test.ts index 66c7c39d1f..f64ca48589 100644 --- a/packages/preferences-controller/src/PreferencesController.test.ts +++ b/packages/preferences-controller/src/PreferencesController.test.ts @@ -5,7 +5,6 @@ describe('PreferencesController', () => { const controller = new PreferencesController(); expect(controller.state).toStrictEqual({ featureFlags: {}, - frequentRpcList: [], identities: {}, ipfsGateway: 'https://ipfs.io/ipfs/', lostIdentities: {}, @@ -150,56 +149,6 @@ describe('PreferencesController', () => { expect(controller.state.selectedAddress).toStrictEqual('0x00'); }); - it('should add custom rpc url', () => { - const controller = new PreferencesController(); - const rpcUrlNetwork = { - chainId: undefined, - nickname: 'RPC', - rpcPrefs: undefined, - rpcUrl: 'rpc_url', - ticker: 'RPC', - }; - const localhostNetwork = { - chainId: undefined, - nickname: undefined, - rpcPrefs: undefined, - rpcUrl: 'http://localhost:8545', - ticker: 'LOCAL', - }; - controller.addToFrequentRpcList('rpc_url', undefined, 'RPC', 'RPC'); - controller.addToFrequentRpcList( - 'http://localhost:8545', - undefined, - 'LOCAL', - ); - - expect(controller.state.frequentRpcList).toStrictEqual([ - rpcUrlNetwork, - localhostNetwork, - ]); - controller.addToFrequentRpcList('rpc_url'); - expect(controller.state.frequentRpcList).toStrictEqual([ - localhostNetwork, - { ...rpcUrlNetwork, nickname: undefined, ticker: undefined }, - ]); - }); - - it('should remove custom rpc url', () => { - const controller = new PreferencesController(); - const rpcUrlNetwork = { - chainId: undefined, - nickname: undefined, - rpcPrefs: undefined, - rpcUrl: 'rpc_url', - ticker: undefined, - }; - controller.addToFrequentRpcList('rpc_url'); - expect(controller.state.frequentRpcList).toStrictEqual([rpcUrlNetwork]); - controller.removeFromFrequentRpcList('other_rpc_url'); - controller.removeFromFrequentRpcList('rpc_url'); - expect(controller.state.frequentRpcList).toStrictEqual([]); - }); - it('should set IPFS gateway', () => { const controller = new PreferencesController(); controller.setIpfsGateway('https://ipfs.infura.io/ipfs/'); diff --git a/packages/preferences-controller/src/PreferencesController.ts b/packages/preferences-controller/src/PreferencesController.ts index f89e24dffd..6fd100d9cc 100644 --- a/packages/preferences-controller/src/PreferencesController.ts +++ b/packages/preferences-controller/src/PreferencesController.ts @@ -18,45 +18,17 @@ export interface ContactEntry { importTime?: number; } -/** - * Custom RPC network information - * - * @property rpcUrl - RPC target URL. - * @property chainId - Network ID as per EIP-155 - * @property nickname - Personalized network name. - * @property ticker - Currency ticker. - * @property rpcPrefs - Personalized preferences. - */ -export interface FrequentRpc { - rpcUrl: string; - chainId?: number; - nickname?: string; - ticker?: string; - rpcPrefs?: RpcPreferences; -} - -/** - * Custom RPC network preferences - * - * @param blockExplorerUrl - Block explorer URL. - */ -export interface RpcPreferences { - blockExplorerUrl: string; -} - /** * @type PreferencesState * * Preferences controller state * @property featureFlags - Map of specific features to enable or disable - * @property frequentRpcList - A list of custom RPCs to provide the user * @property identities - Map of addresses to ContactEntry objects * @property lostIdentities - Map of lost addresses to ContactEntry objects * @property selectedAddress - Current coinbase account */ export interface PreferencesState extends BaseState { featureFlags: { [feature: string]: boolean }; - frequentRpcList: FrequentRpc[]; ipfsGateway: string; identities: { [address: string]: ContactEntry }; lostIdentities: { [address: string]: ContactEntry }; @@ -91,7 +63,6 @@ export class PreferencesController extends BaseController< super(config, state); this.defaultState = { featureFlags: {}, - frequentRpcList: [], identities: {}, ipfsGateway: 'https://ipfs.io/ipfs/', lostIdentities: {}, @@ -242,56 +213,6 @@ export class PreferencesController extends BaseController< this.update({ identities: { ...identities }, selectedAddress }); } - /** - * Adds custom RPC URL to state. - * - * @param url - The custom RPC URL. - * @param chainId - The chain ID of the network, as per EIP-155. - * @param ticker - Currency ticker. - * @param nickname - Personalized network name. - * @param rpcPrefs - Personalized preferences. - */ - addToFrequentRpcList( - url: string, - chainId?: number, - ticker?: string, - nickname?: string, - rpcPrefs?: RpcPreferences, - ) { - const { frequentRpcList } = this.state; - const index = frequentRpcList.findIndex(({ rpcUrl }) => { - return rpcUrl === url; - }); - if (index !== -1) { - frequentRpcList.splice(index, 1); - } - const newFrequestRpc: FrequentRpc = { - rpcUrl: url, - chainId, - ticker, - nickname, - rpcPrefs, - }; - frequentRpcList.push(newFrequestRpc); - this.update({ frequentRpcList: [...frequentRpcList] }); - } - - /** - * Removes custom RPC URL from state. - * - * @param url - Custom RPC URL. - */ - removeFromFrequentRpcList(url: string) { - const { frequentRpcList } = this.state; - const index = frequentRpcList.findIndex(({ rpcUrl }) => { - return rpcUrl === url; - }); - if (index !== -1) { - frequentRpcList.splice(index, 1); - } - this.update({ frequentRpcList: [...frequentRpcList] }); - } - /** * Sets selected address. * diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 85bb0df17e..86168e062e 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -138,6 +138,7 @@ const MOCK_NETWORK = { type: 'goerli' as NetworkType, chainId: NetworksChainId.goerli, }, + networkConfigurations: {}, }, subscribe: () => undefined, }; @@ -151,13 +152,18 @@ const MOCK_NETWORK_CUSTOM = { type: 'rpc' as NetworkType, chainId: '10', }, + networkConfigurations: {}, }, subscribe: () => undefined, }; const MOCK_NETWORK_WITHOUT_CHAIN_ID = { getProvider: () => PROVIDER, isCustomNetwork: false, - state: { network: '5', providerConfig: { type: 'goerli' as NetworkType } }, + state: { + network: '5', + providerConfig: { type: 'goerli' as NetworkType }, + networkConfigurations: {}, + }, subscribe: () => undefined, }; const MOCK_MAINNET_NETWORK = { @@ -170,6 +176,7 @@ const MOCK_MAINNET_NETWORK = { type: 'mainnet' as NetworkType, chainId: NetworksChainId.mainnet, }, + networkConfigurations: {}, }, subscribe: () => undefined, }; @@ -183,6 +190,7 @@ const MOCK_CUSTOM_NETWORK = { type: 'rpc' as NetworkType, chainId: '80001', }, + networkConfigurations: {}, }, subscribe: () => undefined, }; diff --git a/yarn.lock b/yarn.lock index 692f65e001..9f26067cf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1843,6 +1843,7 @@ __metadata: "@metamask/base-controller": "workspace:^" "@metamask/controller-utils": "workspace:^" "@metamask/swappable-obj-proxy": ^2.1.0 + "@metamask/utils": ^3.3.1 "@types/jest": ^26.0.22 "@types/lodash": ^4.14.191 async-mutex: ^0.2.6 @@ -1859,6 +1860,7 @@ __metadata: typedoc: ^0.22.15 typedoc-plugin-missing-exports: ^0.22.6 typescript: ~4.6.3 + uuid: ^8.3.2 web3-provider-engine: ^16.0.3 languageName: unknown linkType: soft