diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 9fafda276e0..f606a61b6f5 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1485,7 +1485,7 @@ "count": 2 }, "id-length": { - "count": 7 + "count": 4 } }, "packages/network-enablement-controller/src/selectors.ts": { diff --git a/packages/controller-utils/src/constants.ts b/packages/controller-utils/src/constants.ts index 73e61fc22cb..a55205f92ba 100644 --- a/packages/controller-utils/src/constants.ts +++ b/packages/controller-utils/src/constants.ts @@ -67,6 +67,19 @@ export const BUILT_IN_CUSTOM_NETWORKS_RPC = { 'megaeth-testnet': 'https://carrot.megaeth.com/rpc', 'megaeth-testnet-v2': 'https://carrot.megaeth.com/rpc', 'monad-testnet': 'https://testnet-rpc.monad.xyz', + // New additions for 20+ network performance testing + 'fantom-mainnet': 'https://rpc.ftm.tools', + 'gnosis-mainnet': 'https://rpc.gnosischain.com', + 'celo-mainnet': 'https://forno.celo.org', + 'cronos-mainnet': 'https://evm.cronos.org', + aurora: 'https://mainnet.aurora.dev', + 'moonbeam-mainnet': 'https://rpc.api.moonbeam.network', + 'moonriver-mainnet': 'https://rpc.api.moonriver.moonbeam.network', + 'klaytn-mainnet': 'https://public-en-cypress.klaytn.net', + 'avalanche-mainnet': 'https://api.avax.network/ext/bc/C/rpc', + 'zksync-era-mainnet': 'https://mainnet.era.zksync.io', + 'palm-mainnet': 'https://palm-mainnet.public.blastapi.io', + 'hypervm-mainnet': 'https://rpc.hyperliquid.xyz', }; /** @@ -178,6 +191,91 @@ export const BUILT_IN_NETWORKS = { blockExplorerUrl: BlockExplorerUrl['sei-mainnet'], }, }, + // New additions for 20+ network performance testing + [NetworkType['fantom-mainnet']]: { + chainId: ChainId['fantom-mainnet'], + ticker: NetworksTicker['fantom-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['fantom-mainnet'], + }, + }, + [NetworkType['gnosis-mainnet']]: { + chainId: ChainId['gnosis-mainnet'], + ticker: NetworksTicker['gnosis-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['gnosis-mainnet'], + }, + }, + [NetworkType['celo-mainnet']]: { + chainId: ChainId['celo-mainnet'], + ticker: NetworksTicker['celo-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['celo-mainnet'], + }, + }, + [NetworkType['cronos-mainnet']]: { + chainId: ChainId['cronos-mainnet'], + ticker: NetworksTicker['cronos-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['cronos-mainnet'], + }, + }, + [NetworkType.aurora]: { + chainId: ChainId.aurora, + ticker: NetworksTicker.aurora, + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl.aurora, + }, + }, + [NetworkType['moonbeam-mainnet']]: { + chainId: ChainId['moonbeam-mainnet'], + ticker: NetworksTicker['moonbeam-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['moonbeam-mainnet'], + }, + }, + [NetworkType['moonriver-mainnet']]: { + chainId: ChainId['moonriver-mainnet'], + ticker: NetworksTicker['moonriver-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['moonriver-mainnet'], + }, + }, + [NetworkType['klaytn-mainnet']]: { + chainId: ChainId['klaytn-mainnet'], + ticker: NetworksTicker['klaytn-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['klaytn-mainnet'], + }, + }, + [NetworkType['avalanche-mainnet']]: { + chainId: ChainId['avalanche-mainnet'], + ticker: NetworksTicker['avalanche-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['avalanche-mainnet'], + }, + }, + [NetworkType['zksync-era-mainnet']]: { + chainId: ChainId['zksync-era-mainnet'], + ticker: NetworksTicker['zksync-era-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['zksync-era-mainnet'], + }, + }, + [NetworkType['palm-mainnet']]: { + chainId: ChainId['palm-mainnet'], + ticker: NetworksTicker['palm-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['palm-mainnet'], + }, + }, + [NetworkType['hypervm-mainnet']]: { + chainId: ChainId['hypervm-mainnet'], + ticker: NetworksTicker['hypervm-mainnet'], + rpcPrefs: { + blockExplorerUrl: BlockExplorerUrl['hypervm-mainnet'], + }, + }, [NetworkType.rpc]: { chainId: undefined, blockExplorerUrl: undefined, diff --git a/packages/controller-utils/src/types.ts b/packages/controller-utils/src/types.ts index 6eb15887799..468d1c8c421 100644 --- a/packages/controller-utils/src/types.ts +++ b/packages/controller-utils/src/types.ts @@ -29,6 +29,19 @@ export const CustomNetworkType = { 'megaeth-testnet': 'megaeth-testnet', 'megaeth-testnet-v2': 'megaeth-testnet-v2', 'monad-testnet': 'monad-testnet', + // New additions for 20+ network performance testing + 'fantom-mainnet': 'fantom-mainnet', + 'gnosis-mainnet': 'gnosis-mainnet', + 'celo-mainnet': 'celo-mainnet', + 'cronos-mainnet': 'cronos-mainnet', + aurora: 'aurora', + 'moonbeam-mainnet': 'moonbeam-mainnet', + 'moonriver-mainnet': 'moonriver-mainnet', + 'klaytn-mainnet': 'klaytn-mainnet', + 'avalanche-mainnet': 'avalanche-mainnet', + 'zksync-era-mainnet': 'zksync-era-mainnet', + 'palm-mainnet': 'palm-mainnet', + 'hypervm-mainnet': 'hypervm-mainnet', } as const; export type CustomNetworkType = (typeof CustomNetworkType)[keyof typeof CustomNetworkType]; @@ -98,6 +111,18 @@ export enum BuiltInNetworkName { OptimismMainnet = 'optimism-mainnet', PolygonMainnet = 'polygon-mainnet', SeiMainnet = 'sei-mainnet', + // New additions for 20+ network performance testing + FantomMainnet = 'fantom-mainnet', + GnosisMainnet = 'gnosis-mainnet', + CeloMainnet = 'celo-mainnet', + CronosMainnet = 'cronos-mainnet', + MoonbeamMainnet = 'moonbeam-mainnet', + MoonriverMainnet = 'moonriver-mainnet', + KlaytnMainnet = 'klaytn-mainnet', + AvalancheMainnet = 'avalanche-mainnet', + ZkSyncEraMainnet = 'zksync-era-mainnet', + PalmMainnet = 'palm-mainnet', + HyperEvmMainnet = 'hypervm-mainnet', } /** @@ -125,6 +150,18 @@ export const ChainId = { [BuiltInNetworkName.OptimismMainnet]: '0xa', // toHex(10) [BuiltInNetworkName.PolygonMainnet]: '0x89', // toHex(137) [BuiltInNetworkName.SeiMainnet]: '0x531', // toHex(1329) + // New additions for 20+ network performance testing + [BuiltInNetworkName.FantomMainnet]: '0xfa', // toHex(250) + [BuiltInNetworkName.GnosisMainnet]: '0x64', // toHex(100) + [BuiltInNetworkName.CeloMainnet]: '0xa4ec', // toHex(42220) + [BuiltInNetworkName.CronosMainnet]: '0x19', // toHex(25) + [BuiltInNetworkName.MoonbeamMainnet]: '0x504', // toHex(1284) + [BuiltInNetworkName.MoonriverMainnet]: '0x505', // toHex(1285) + [BuiltInNetworkName.KlaytnMainnet]: '0x2019', // toHex(8217) + [BuiltInNetworkName.AvalancheMainnet]: '0xa86a', // toHex(43114) + [BuiltInNetworkName.ZkSyncEraMainnet]: '0x144', // toHex(324) + [BuiltInNetworkName.PalmMainnet]: '0x2a15c308d', // toHex(11297108109) + [BuiltInNetworkName.HyperEvmMainnet]: '0x3e7', // toHex(999) } as const; export type ChainId = (typeof ChainId)[keyof typeof ChainId]; @@ -154,6 +191,21 @@ export enum NetworksTicker { 'optimism-mainnet' = 'ETH', 'polygon-mainnet' = 'POL', 'sei-mainnet' = 'SEI', + // New additions for 20+ network performance testing + 'fantom-mainnet' = 'FTM', + 'gnosis-mainnet' = 'XDAI', + 'celo-mainnet' = 'CELO', + 'cronos-mainnet' = 'CRO', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + aurora = 'ETH', + 'moonbeam-mainnet' = 'GLMR', + 'moonriver-mainnet' = 'MOVR', + 'klaytn-mainnet' = 'KLAY', + 'avalanche-mainnet' = 'AVAX', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + 'zksync-era-mainnet' = 'ETH', + 'palm-mainnet' = 'PALM', + 'hypervm-mainnet' = 'HYPE', rpc = '', } /* eslint-enable @typescript-eslint/naming-convention */ @@ -178,6 +230,19 @@ export const BlockExplorerUrl = { [BuiltInNetworkName.OptimismMainnet]: 'https://optimistic.etherscan.io', [BuiltInNetworkName.PolygonMainnet]: 'https://polygonscan.com', [BuiltInNetworkName.SeiMainnet]: 'https://seitrace.com', + // New additions for 20+ network performance testing + [BuiltInNetworkName.FantomMainnet]: 'https://ftmscan.com', + [BuiltInNetworkName.GnosisMainnet]: 'https://gnosisscan.io', + [BuiltInNetworkName.CeloMainnet]: 'https://celoscan.io', + [BuiltInNetworkName.CronosMainnet]: 'https://cronoscan.com', + [BuiltInNetworkName.Aurora]: 'https://aurorascan.dev', + [BuiltInNetworkName.MoonbeamMainnet]: 'https://moonscan.io', + [BuiltInNetworkName.MoonriverMainnet]: 'https://moonriver.moonscan.io', + [BuiltInNetworkName.KlaytnMainnet]: 'https://scope.klaytn.com', + [BuiltInNetworkName.AvalancheMainnet]: 'https://snowtrace.io', + [BuiltInNetworkName.ZkSyncEraMainnet]: 'https://explorer.zksync.io', + [BuiltInNetworkName.PalmMainnet]: 'https://explorer.palm.io', + [BuiltInNetworkName.HyperEvmMainnet]: 'https://explorer.hyperliquid.xyz', } as const satisfies Record; export type BlockExplorerUrl = (typeof BlockExplorerUrl)[keyof typeof BlockExplorerUrl]; @@ -201,6 +266,19 @@ export const NetworkNickname = { [BuiltInNetworkName.OptimismMainnet]: 'Optimism Mainnet', [BuiltInNetworkName.PolygonMainnet]: 'Polygon Mainnet', [BuiltInNetworkName.SeiMainnet]: 'Sei Mainnet', + // New additions for 20+ network performance testing + [BuiltInNetworkName.FantomMainnet]: 'Fantom Opera', + [BuiltInNetworkName.GnosisMainnet]: 'Gnosis Chain', + [BuiltInNetworkName.CeloMainnet]: 'Celo', + [BuiltInNetworkName.CronosMainnet]: 'Cronos', + [BuiltInNetworkName.Aurora]: 'Aurora', + [BuiltInNetworkName.MoonbeamMainnet]: 'Moonbeam', + [BuiltInNetworkName.MoonriverMainnet]: 'Moonriver', + [BuiltInNetworkName.KlaytnMainnet]: 'Klaytn', + [BuiltInNetworkName.AvalancheMainnet]: 'Avalanche C-Chain', + [BuiltInNetworkName.ZkSyncEraMainnet]: 'zkSync Era', + [BuiltInNetworkName.PalmMainnet]: 'Palm', + [BuiltInNetworkName.HyperEvmMainnet]: 'HyperEVM', } as const satisfies Record; export type NetworkNickname = (typeof NetworkNickname)[keyof typeof NetworkNickname]; diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 1652fa011a1..718d56b1c74 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -15,6 +15,7 @@ import { NetworkNickname, BUILT_IN_CUSTOM_NETWORKS_RPC, BUILT_IN_NETWORKS, + BuiltInNetworkName, } from '@metamask/controller-utils'; import type { PollingBlockTrackerOptions } from '@metamask/eth-block-tracker'; import EthQuery from '@metamask/eth-query'; @@ -868,7 +869,7 @@ function getDefaultCustomNetworkConfigurationsByChainId(): Record< // Create the `networkConfigurationsByChainId` objects explicitly, // Because it is not always guaranteed that the custom networks are included in the // default networks. - return { + const configs = { [ChainId['megaeth-testnet']]: getCustomNetworkConfiguration( CustomNetworkType['megaeth-testnet'], ), @@ -878,7 +879,51 @@ function getDefaultCustomNetworkConfigurationsByChainId(): Record< [ChainId['monad-testnet']]: getCustomNetworkConfiguration( CustomNetworkType['monad-testnet'], ), + // New additions for 20+ network performance testing + [ChainId[BuiltInNetworkName.FantomMainnet]]: getCustomNetworkConfiguration( + CustomNetworkType['fantom-mainnet'], + ), + [ChainId[BuiltInNetworkName.GnosisMainnet]]: getCustomNetworkConfiguration( + CustomNetworkType['gnosis-mainnet'], + ), + [ChainId[BuiltInNetworkName.CeloMainnet]]: getCustomNetworkConfiguration( + CustomNetworkType['celo-mainnet'], + ), + [ChainId[BuiltInNetworkName.CronosMainnet]]: getCustomNetworkConfiguration( + CustomNetworkType['cronos-mainnet'], + ), + [ChainId[BuiltInNetworkName.Aurora]]: getCustomNetworkConfiguration( + CustomNetworkType.aurora, + ), + [ChainId[BuiltInNetworkName.MoonbeamMainnet]]: + getCustomNetworkConfiguration(CustomNetworkType['moonbeam-mainnet']), + [ChainId[BuiltInNetworkName.MoonriverMainnet]]: + getCustomNetworkConfiguration(CustomNetworkType['moonriver-mainnet']), + [ChainId[BuiltInNetworkName.KlaytnMainnet]]: getCustomNetworkConfiguration( + CustomNetworkType['klaytn-mainnet'], + ), + [ChainId[BuiltInNetworkName.AvalancheMainnet]]: + getCustomNetworkConfiguration(CustomNetworkType['avalanche-mainnet']), + [ChainId[BuiltInNetworkName.ZkSyncEraMainnet]]: + getCustomNetworkConfiguration(CustomNetworkType['zksync-era-mainnet']), + [ChainId[BuiltInNetworkName.PalmMainnet]]: getCustomNetworkConfiguration( + CustomNetworkType['palm-mainnet'], + ), + [ChainId[BuiltInNetworkName.HyperEvmMainnet]]: + getCustomNetworkConfiguration(CustomNetworkType['hypervm-mainnet']), }; + + console.log( + '[NetworkController] getDefaultCustomNetworkConfigurationsByChainId - returning', + Object.keys(configs).length, + 'custom network configurations', + ); + console.log( + '[NetworkController] Custom network chain IDs:', + Object.keys(configs), + ); + + return configs; } /** diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index e06d853b084..e390c4389dc 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -3,6 +3,9 @@ import type { InfuraNetworkType, } from '@metamask/controller-utils'; import { ChainId } from '@metamask/controller-utils'; + +import { createThrottledFetchForChainId } from './throttled-fetch'; + import type { PollingBlockTrackerOptions } from '@metamask/eth-block-tracker'; import { PollingBlockTracker } from '@metamask/eth-block-tracker'; import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; @@ -210,11 +213,23 @@ function createRpcServiceChain({ const availableEndpointUrls: [string, ...string[]] = isRpcFailoverEnabled ? [primaryEndpointUrl, ...(configuration.failoverRpcUrls ?? [])] : [primaryEndpointUrl]; - const rpcServiceConfigurations = availableEndpointUrls.map((endpointUrl) => ({ - ...getRpcServiceOptions(endpointUrl), - endpointUrl, - logger, - })); + const rpcServiceConfigurations = availableEndpointUrls.map((endpointUrl) => { + const options = getRpcServiceOptions(endpointUrl); + + // Apply network throttling based on chain ID if configured + const throttledFetch = createThrottledFetchForChainId( + endpointUrl, + configuration.chainId, + options.fetch, + ); + + return { + ...options, + fetch: throttledFetch, + endpointUrl, + logger, + }; + }); /** * Extracts the error from Cockatiel's `FailureReason` type received in diff --git a/packages/network-controller/src/index.ts b/packages/network-controller/src/index.ts index 98153162fe7..b265054523d 100644 --- a/packages/network-controller/src/index.ts +++ b/packages/network-controller/src/index.ts @@ -62,3 +62,9 @@ export type { NetworkClient } from './create-network-client'; export type { AbstractRpcService } from './rpc-service/abstract-rpc-service'; export type { RpcServiceRequestable } from './rpc-service/rpc-service-requestable'; export { isConnectionError } from './rpc-service/rpc-service'; +export { + NETWORK_THROTTLE_CONFIG, + createThrottledFetch, + createThrottledFetchForChainId, + createThrottledGetRpcServiceOptions, +} from './throttled-fetch'; diff --git a/packages/network-controller/src/throttled-fetch.test.ts b/packages/network-controller/src/throttled-fetch.test.ts new file mode 100644 index 00000000000..3fadada4e03 --- /dev/null +++ b/packages/network-controller/src/throttled-fetch.test.ts @@ -0,0 +1,94 @@ +import { createThrottledFetch, createThrottledFetchForChainId } from './throttled-fetch'; + +describe('throttled-fetch', () => { + describe('createThrottledFetch', () => { + it('should delay fetch calls by the specified amount', async () => { + const mockFetch = jest.fn().mockResolvedValue({ ok: true }); + const delayMs = 100; + const throttledFetch = createThrottledFetch(delayMs, mockFetch); + + const startTime = Date.now(); + await throttledFetch('https://example.com'); + const elapsedTime = Date.now() - startTime; + + expect(mockFetch).toHaveBeenCalledWith('https://example.com', undefined); + expect(elapsedTime).toBeGreaterThanOrEqual(delayMs - 10); // Allow small margin + }); + + it('should return the original fetch when delay is 0', () => { + const mockFetch = jest.fn(); + const throttledFetch = createThrottledFetch(0, mockFetch); + + expect(throttledFetch).toBe(mockFetch); + }); + + it('should pass through all fetch arguments', async () => { + const mockFetch = jest.fn().mockResolvedValue({ ok: true }); + const throttledFetch = createThrottledFetch(10, mockFetch); + + const url = 'https://example.com'; + const init = { method: 'POST', body: '{}' }; + await throttledFetch(url, init); + + expect(mockFetch).toHaveBeenCalledWith(url, init); + }); + }); + + describe('createThrottledFetchForChainId', () => { + it('should apply throttling for configured chain IDs', async () => { + const mockFetch = jest.fn().mockResolvedValue({ ok: true }); + const throttleConfig = { + '0x2019': 100, // Klaytn with 100ms delay + }; + + const throttledFetch = createThrottledFetchForChainId( + 'https://klaytn-rpc.example.com', + '0x2019', + mockFetch, + throttleConfig, + ); + + const startTime = Date.now(); + await throttledFetch('https://klaytn-rpc.example.com'); + const elapsedTime = Date.now() - startTime; + + expect(mockFetch).toHaveBeenCalled(); + expect(elapsedTime).toBeGreaterThanOrEqual(90); // Allow small margin + }); + + it('should not throttle unconfigured chain IDs', async () => { + const mockFetch = jest.fn().mockResolvedValue({ ok: true }); + const throttleConfig = { + '0x2019': 100, + }; + + const throttledFetch = createThrottledFetchForChainId( + 'https://mainnet.infura.io', + '0x1', // Ethereum Mainnet - not configured for throttling + mockFetch, + throttleConfig, + ); + + expect(throttledFetch).toBe(mockFetch); // Should return original fetch + }); + + it('should use default throttle config when not provided', async () => { + const mockFetch = jest.fn().mockResolvedValue({ ok: true }); + + // Using default config which includes Klaytn (0x2019) with 2000ms delay + const throttledFetch = createThrottledFetchForChainId( + 'https://klaytn-rpc.example.com', + '0x2019', + mockFetch, + ); + + const startTime = Date.now(); + await throttledFetch('https://klaytn-rpc.example.com'); + const elapsedTime = Date.now() - startTime; + + expect(mockFetch).toHaveBeenCalled(); + // Default config should have 2000ms for Klaytn + expect(elapsedTime).toBeGreaterThanOrEqual(1900); // Allow margin + }); + }); +}); diff --git a/packages/network-controller/src/throttled-fetch.ts b/packages/network-controller/src/throttled-fetch.ts new file mode 100644 index 00000000000..68769af9fc5 --- /dev/null +++ b/packages/network-controller/src/throttled-fetch.ts @@ -0,0 +1,117 @@ +/** + * Configuration for network throttling by chain ID. + * Maps chain IDs (in hex format) to delay in milliseconds. + */ +export const NETWORK_THROTTLE_CONFIG: Record = { + // '0x2019': 2000, // Klaytn - 2s delay + // '0x504': 1000, // Moonbeam - 1s delay + // '0x505': 1000, // Moonriver - 1s delay + // '0x4e454152': 1500, // Aurora - 1.5s delay + '0x1': 10000, // Ethereum Mainnet - 10s delay +}; + +/** + * Creates a throttled fetch function that adds artificial delays before making requests. + * Useful for testing slow network conditions. + * + * @param delayMs - The delay in milliseconds to add before each request + * @param originalFetch - The original fetch function to wrap + * @returns A throttled fetch function + */ +export function createThrottledFetch( + delayMs: number, + originalFetch: typeof fetch, +): typeof fetch { + if (delayMs <= 0) { + return originalFetch; + } + + return async ( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise => { + // Add artificial delay before making the request + await new Promise((resolve) => setTimeout(resolve, delayMs)); + return originalFetch(input, init); + }; +} + +/** + * Creates a fetch function that applies throttling based on the chain ID. + * Extracts chain ID from the RPC endpoint URL to determine the appropriate delay. + * + * @param rpcEndpointUrl - The RPC endpoint URL (may contain chain ID context) + * @param chainId - The chain ID in hex format (e.g., '0x2019') + * @param originalFetch - The original fetch function to wrap + * @param throttleConfig - Optional custom throttle configuration map + * @returns A throttled fetch function if configured for this chain, otherwise the original + */ +export function createThrottledFetchForChainId( + rpcEndpointUrl: string, + chainId: string, + originalFetch: typeof fetch, + throttleConfig: Record = NETWORK_THROTTLE_CONFIG, +): typeof fetch { + const delayMs = throttleConfig[chainId] || 0; + + if (delayMs === 0) { + return originalFetch; + } + + console.log( + `[NetworkThrottle] Applying ${delayMs}ms delay to chain ${chainId} (${rpcEndpointUrl})`, + ); + + return createThrottledFetch(delayMs, originalFetch); +} + +/** + * Creates a wrapped getRpcServiceOptions function that applies network throttling + * based on chain ID. This is meant to be used in NetworkController initialization. + * + * @param originalGetRpcServiceOptions - The original getRpcServiceOptions function + * @param getChainIdForUrl - Function to get the chain ID for a given RPC endpoint URL + * @param throttleConfig - Optional custom throttle configuration map + * @returns A wrapped getRpcServiceOptions function that applies throttling + * + * @example + * ```typescript + * const networkController = new NetworkController({ + * getRpcServiceOptions: createThrottledGetRpcServiceOptions( + * (rpcEndpointUrl) => ({ fetch, btoa }), + * (rpcEndpointUrl) => '0x1', // Get chainId from your config + * ), + * // ... other options + * }); + * ``` + */ +export function createThrottledGetRpcServiceOptions( + originalGetRpcServiceOptions: (rpcEndpointUrl: string) => { + fetch: typeof fetch; + btoa: typeof btoa; + [key: string]: unknown; + }, + getChainIdForUrl: (rpcEndpointUrl: string) => string | undefined, + throttleConfig: Record = NETWORK_THROTTLE_CONFIG, +): typeof originalGetRpcServiceOptions { + return (rpcEndpointUrl: string) => { + const options = originalGetRpcServiceOptions(rpcEndpointUrl); + const chainId = getChainIdForUrl(rpcEndpointUrl); + + if (!chainId) { + return options; + } + + const throttledFetch = createThrottledFetchForChainId( + rpcEndpointUrl, + chainId, + options.fetch, + throttleConfig, + ); + + return { + ...options, + fetch: throttledFetch, + }; + }; +} diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.ts b/packages/network-enablement-controller/src/NetworkEnablementController.ts index 7c85a955407..cbc24e0483a 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.ts @@ -106,35 +106,47 @@ export type NetworkEnablementControllerMessenger = Messenger< * @returns The default state with pre-enabled networks. */ const getDefaultNetworkEnablementControllerState = - (): NetworkEnablementControllerState => ({ - enabledNetworkMap: { - [KnownCaipNamespace.Eip155]: { - [ChainId[BuiltInNetworkName.Mainnet]]: true, - [ChainId[BuiltInNetworkName.LineaMainnet]]: true, - [ChainId[BuiltInNetworkName.BaseMainnet]]: true, - [ChainId[BuiltInNetworkName.ArbitrumOne]]: true, - [ChainId[BuiltInNetworkName.BscMainnet]]: true, - [ChainId[BuiltInNetworkName.OptimismMainnet]]: true, - [ChainId[BuiltInNetworkName.PolygonMainnet]]: true, - [ChainId[BuiltInNetworkName.SeiMainnet]]: true, - }, - [KnownCaipNamespace.Solana]: { - [SolScope.Mainnet]: true, - [SolScope.Testnet]: false, - [SolScope.Devnet]: false, - }, - [KnownCaipNamespace.Bip122]: { - [BtcScope.Mainnet]: true, - [BtcScope.Testnet]: false, - [BtcScope.Signet]: false, - }, - [KnownCaipNamespace.Tron]: { - [TrxScope.Mainnet]: true, - [TrxScope.Nile]: false, - [TrxScope.Shasta]: false, + (): NetworkEnablementControllerState => { + // Programmatically enable all popular networks for performance testing + const enabledEvmNetworks = POPULAR_NETWORKS.reduce>( + (acc, chainId) => ({ + ...acc, + [chainId]: true, + }), + {}, + ); + + console.log( + '[NetworkEnablementController] Generating default state with', + POPULAR_NETWORKS.length, + 'popular networks', + ); + console.log( + '[NetworkEnablementController] Enabled EVM networks:', + Object.keys(enabledEvmNetworks), + ); + + return { + enabledNetworkMap: { + [KnownCaipNamespace.Eip155]: enabledEvmNetworks, + [KnownCaipNamespace.Solana]: { + [SolScope.Mainnet]: true, + [SolScope.Testnet]: false, + [SolScope.Devnet]: false, + }, + [KnownCaipNamespace.Bip122]: { + [BtcScope.Mainnet]: true, + [BtcScope.Testnet]: false, + [BtcScope.Signet]: false, + }, + [KnownCaipNamespace.Tron]: { + [TrxScope.Mainnet]: true, + [TrxScope.Nile]: false, + [TrxScope.Shasta]: false, + }, }, - }, - }); + }; + }; // Metadata for the controller state const metadata = { @@ -174,14 +186,36 @@ export class NetworkEnablementController extends BaseController< messenger: NetworkEnablementControllerMessenger; state?: Partial; }) { + const defaultState = getDefaultNetworkEnablementControllerState(); + const finalState = { + ...defaultState, + ...state, + }; + + console.log( + '[NetworkEnablementController] Constructor - passed state:', + state ? 'YES (will override defaults)' : 'NO (using defaults)', + ); + console.log( + '[NetworkEnablementController] Constructor - default EVM networks count:', + Object.keys(defaultState.enabledNetworkMap[KnownCaipNamespace.Eip155]) + .length, + ); + console.log( + '[NetworkEnablementController] Constructor - final EVM networks count:', + Object.keys(finalState.enabledNetworkMap[KnownCaipNamespace.Eip155]) + .length, + ); + console.log( + '[NetworkEnablementController] Constructor - final enabled EVM networks:', + Object.keys(finalState.enabledNetworkMap[KnownCaipNamespace.Eip155]), + ); + super({ messenger, metadata, name: controllerName, - state: { - ...getDefaultNetworkEnablementControllerState(), - ...state, - }, + state: finalState, }); messenger.subscribe('NetworkController:networkAdded', ({ chainId }) => { @@ -212,22 +246,31 @@ export class NetworkEnablementController extends BaseController< enableNetwork(chainId: Hex | CaipChainId): void { const { namespace, storageKey } = deriveKeys(chainId); - this.update((s) => { + console.log( + '[NetworkEnablementController] enableNetwork called for:', + chainId, + 'namespace:', + namespace, + 'storageKey:', + storageKey, + ); + + this.update((state) => { // disable all networks in all namespaces first - Object.keys(s.enabledNetworkMap).forEach((ns) => { - Object.keys(s.enabledNetworkMap[ns]).forEach((key) => { - s.enabledNetworkMap[ns][key as CaipChainId | Hex] = false; + Object.keys(state.enabledNetworkMap).forEach((ns) => { + Object.keys(state.enabledNetworkMap[ns]).forEach((key) => { + state.enabledNetworkMap[ns][key as CaipChainId | Hex] = false; }); }); // if the namespace bucket does not exist, return // new nemespace are added only when a new network is added - if (!s.enabledNetworkMap[namespace]) { + if (!state.enabledNetworkMap[namespace]) { return; } // enable the network - s.enabledNetworkMap[namespace][storageKey] = true; + state.enabledNetworkMap[namespace][storageKey] = true; }); } @@ -260,19 +303,19 @@ export class NetworkEnablementController extends BaseController< ); } - this.update((s) => { + this.update((state) => { // Ensure the namespace bucket exists - this.#ensureNamespaceBucket(s, namespace); + this.#ensureNamespaceBucket(state, namespace); // Disable all networks in the specified namespace first - if (s.enabledNetworkMap[namespace]) { - Object.keys(s.enabledNetworkMap[namespace]).forEach((key) => { - s.enabledNetworkMap[namespace][key as CaipChainId | Hex] = false; + if (state.enabledNetworkMap[namespace]) { + Object.keys(state.enabledNetworkMap[namespace]).forEach((key) => { + state.enabledNetworkMap[namespace][key as CaipChainId | Hex] = false; }); } // Enable the target network in the specified namespace - s.enabledNetworkMap[namespace][storageKey] = true; + state.enabledNetworkMap[namespace][storageKey] = true; }); } @@ -287,11 +330,17 @@ export class NetworkEnablementController extends BaseController< * Popular networks that don't exist in NetworkController or MultichainNetworkController configurations will be skipped silently. */ enableAllPopularNetworks(): void { - this.update((s) => { + console.log( + '[NetworkEnablementController] enableAllPopularNetworks called - attempting to enable', + POPULAR_NETWORKS.length, + 'networks', + ); + + this.update((state) => { // First disable all networks across all namespaces - Object.keys(s.enabledNetworkMap).forEach((ns) => { - Object.keys(s.enabledNetworkMap[ns]).forEach((key) => { - s.enabledNetworkMap[ns][key as CaipChainId | Hex] = false; + Object.keys(state.enabledNetworkMap).forEach((ns) => { + Object.keys(state.enabledNetworkMap[ns]).forEach((key) => { + state.enabledNetworkMap[ns][key as CaipChainId | Hex] = false; }); }); @@ -303,6 +352,14 @@ export class NetworkEnablementController extends BaseController< 'MultichainNetworkController:getState', ); + console.log( + '[NetworkEnablementController] NetworkController has', + Object.keys(networkControllerState.networkConfigurationsByChainId) + .length, + 'networks configured', + ); + + let enabledCount = 0; // Enable all popular EVM networks that exist in NetworkController configurations POPULAR_NETWORKS.forEach((chainId) => { const { namespace, storageKey } = deriveKeys(chainId as Hex); @@ -312,12 +369,31 @@ export class NetworkEnablementController extends BaseController< networkControllerState.networkConfigurationsByChainId[chainId as Hex] ) { // Ensure namespace bucket exists - this.#ensureNamespaceBucket(s, namespace); + this.#ensureNamespaceBucket(state, namespace); // Enable the network - s.enabledNetworkMap[namespace][storageKey] = true; + state.enabledNetworkMap[namespace][storageKey] = true; + enabledCount += 1; + console.log( + '[NetworkEnablementController] Enabled network:', + chainId, + storageKey, + ); + } else { + console.log( + '[NetworkEnablementController] Network not found in NetworkController, skipping:', + chainId, + ); } }); + console.log( + '[NetworkEnablementController] Successfully enabled', + enabledCount, + 'of', + POPULAR_NETWORKS.length, + 'popular networks', + ); + // Enable Solana mainnet if it exists in MultichainNetworkController configurations const solanaKeys = deriveKeys(SolScope.Mainnet as CaipChainId); if ( @@ -326,9 +402,10 @@ export class NetworkEnablementController extends BaseController< ] ) { // Ensure namespace bucket exists - this.#ensureNamespaceBucket(s, solanaKeys.namespace); + this.#ensureNamespaceBucket(state, solanaKeys.namespace); // Enable Solana mainnet - s.enabledNetworkMap[solanaKeys.namespace][solanaKeys.storageKey] = true; + state.enabledNetworkMap[solanaKeys.namespace][solanaKeys.storageKey] = + true; } // Enable Bitcoin mainnet if it exists in MultichainNetworkController configurations @@ -339,9 +416,9 @@ export class NetworkEnablementController extends BaseController< ] ) { // Ensure namespace bucket exists - this.#ensureNamespaceBucket(s, bitcoinKeys.namespace); + this.#ensureNamespaceBucket(state, bitcoinKeys.namespace); // Enable Bitcoin mainnet - s.enabledNetworkMap[bitcoinKeys.namespace][bitcoinKeys.storageKey] = + state.enabledNetworkMap[bitcoinKeys.namespace][bitcoinKeys.storageKey] = true; } @@ -353,9 +430,9 @@ export class NetworkEnablementController extends BaseController< ] ) { // Ensure namespace bucket exists - this.#ensureNamespaceBucket(s, tronKeys.namespace); + this.#ensureNamespaceBucket(state, tronKeys.namespace); // Enable Tron mainnet - s.enabledNetworkMap[tronKeys.namespace][tronKeys.storageKey] = true; + state.enabledNetworkMap[tronKeys.namespace][tronKeys.storageKey] = true; } }); } @@ -372,7 +449,7 @@ export class NetworkEnablementController extends BaseController< * have been initialized and their configurations are available. */ init(): void { - this.update((s) => { + this.update((state) => { // Get network configurations from NetworkController (EVM networks) const networkControllerState = this.messenger.call( 'NetworkController:getState', @@ -388,12 +465,10 @@ export class NetworkEnablementController extends BaseController< networkControllerState.networkConfigurationsByChainId, ).forEach((chainId) => { const { namespace, storageKey } = deriveKeys(chainId as Hex); - this.#ensureNamespaceBucket(s, namespace); + this.#ensureNamespaceBucket(state, namespace); // Only add network if it doesn't already exist in state (preserves user settings) - if (s.enabledNetworkMap[namespace][storageKey] === undefined) { - s.enabledNetworkMap[namespace][storageKey] = false; - } + state.enabledNetworkMap[namespace][storageKey] ??= false; }); // Initialize namespace buckets for all networks from MultichainNetworkController @@ -401,12 +476,10 @@ export class NetworkEnablementController extends BaseController< multichainState.multichainNetworkConfigurationsByChainId, ).forEach((chainId) => { const { namespace, storageKey } = deriveKeys(chainId as CaipChainId); - this.#ensureNamespaceBucket(s, namespace); + this.#ensureNamespaceBucket(state, namespace); // Only add network if it doesn't already exist in state (preserves user settings) - if (s.enabledNetworkMap[namespace][storageKey] === undefined) { - s.enabledNetworkMap[namespace][storageKey] = false; - } + state.enabledNetworkMap[namespace][storageKey] ??= false; }); }); } @@ -429,8 +502,8 @@ export class NetworkEnablementController extends BaseController< const derivedKeys = deriveKeys(chainId); const { namespace, storageKey } = derivedKeys; - this.update((s) => { - s.enabledNetworkMap[namespace][storageKey] = false; + this.update((state) => { + state.enabledNetworkMap[namespace][storageKey] = false; }); } @@ -461,7 +534,7 @@ export class NetworkEnablementController extends BaseController< #ensureNamespaceBucket( state: NetworkEnablementControllerState, ns: CaipNamespace, - ) { + ): void { if (!state.enabledNetworkMap[ns]) { state.enabledNetworkMap[ns] = {}; } @@ -515,15 +588,16 @@ export class NetworkEnablementController extends BaseController< const derivedKeys = deriveKeys(chainId); const { namespace, storageKey } = derivedKeys; - this.update((s) => { + this.update((state) => { // fallback and enable ethereum mainnet if (isOnlyNetworkEnabledInNamespace(this.state, derivedKeys)) { - s.enabledNetworkMap[namespace][ChainId[BuiltInNetworkName.Mainnet]] = - true; + state.enabledNetworkMap[namespace][ + ChainId[BuiltInNetworkName.Mainnet] + ] = true; } - if (namespace in s.enabledNetworkMap) { - delete s.enabledNetworkMap[namespace][storageKey]; + if (namespace in state.enabledNetworkMap) { + delete state.enabledNetworkMap[namespace][storageKey]; } }); } @@ -542,9 +616,9 @@ export class NetworkEnablementController extends BaseController< #onAddNetwork(chainId: Hex | CaipChainId): void { const { namespace, storageKey, reference } = deriveKeys(chainId); - this.update((s) => { + this.update((state) => { // Ensure the namespace bucket exists - this.#ensureNamespaceBucket(s, namespace); + this.#ensureNamespaceBucket(state, namespace); // Check if popular networks mode is active (>2 popular networks enabled) const inPopularNetworksMode = this.#isInPopularNetworksMode(); @@ -558,16 +632,16 @@ export class NetworkEnablementController extends BaseController< if (shouldKeepCurrentSelection) { // Add the popular network but don't enable it (keep current selection) - s.enabledNetworkMap[namespace][storageKey] = true; + state.enabledNetworkMap[namespace][storageKey] = true; } else { // Switch to the newly added network (disable all others, enable this one) - Object.keys(s.enabledNetworkMap).forEach((ns) => { - Object.keys(s.enabledNetworkMap[ns]).forEach((key) => { - s.enabledNetworkMap[ns][key as CaipChainId | Hex] = false; + Object.keys(state.enabledNetworkMap).forEach((ns) => { + Object.keys(state.enabledNetworkMap[ns]).forEach((key) => { + state.enabledNetworkMap[ns][key as CaipChainId | Hex] = false; }); }); // Enable the newly added network - s.enabledNetworkMap[namespace][storageKey] = true; + state.enabledNetworkMap[namespace][storageKey] = true; } }); } diff --git a/packages/network-enablement-controller/src/constants.ts b/packages/network-enablement-controller/src/constants.ts index 6bb3e652c00..66dc58a955d 100644 --- a/packages/network-enablement-controller/src/constants.ts +++ b/packages/network-enablement-controller/src/constants.ts @@ -7,10 +7,19 @@ export const POPULAR_NETWORKS = [ '0x38', // BNB Smart Chain (56) '0xa', // Optimism (10) '0x89', // Polygon (137) - '0x531', // Sei (Assuming 1329 used in EVM context) + '0x531', // Sei (1329) '0x144', // zkSync Era (324) '0x2a15c308d', // Palm (11297108109) '0x3e7', // HyperEVM (999) - '0x8f', // Monad (143) - '0x10e6', // MegaETH (4326) + '0x279f', // Monad Testnet (10143) - FIXED: was 0x8f (143) + '0x18c7', // MegaETH Testnet V2 (6343) - FIXED: was 0x10e6 (4326) + // New additions for 20+ network performance testing + '0xfa', // Fantom Opera (250) + '0x64', // Gnosis Chain (100) + '0xa4ec', // Celo (42220) + '0x19', // Cronos (25) + '0x4e454152', // Aurora (1313161554) + '0x504', // Moonbeam (1284) + '0x505', // Moonriver (1285) + '0x2019', // Klaytn (8217) - intentionally slow RPC for testing ];