diff --git a/app/scripts/background.js b/app/scripts/background.js index d36376e20..956487f89 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -223,7 +223,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => { * @property {object} provider - The current selected network provider. * @property {string} provider.rpcUrl - The address for the RPC API, if using an RPC API. * @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks. - * @property {string} network - A stringified number of the current network ID. + * @property {string} networkId - The stringified number of the current network ID. + * @property {string} networkStatus - Either "unknown", "available", "unavailable", or "blocked", depending on the status of the currently selected network. * @property {object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values. * @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string. * @property {TransactionMeta[]} currentNetworkTxList - An array of transactions associated with the currently selected network. diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index cd8040a68..c90ebc590 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -17,8 +17,8 @@ import NetworkController, { NetworkControllerEventTypes } from './network'; import PreferencesController from './preferences'; describe('DetectTokensController', function () { - const sandbox = sinon.createSandbox(); - let assetsContractController, + let sandbox, + assetsContractController, keyringMemStore, network, preferences, @@ -32,87 +32,94 @@ describe('DetectTokensController', function () { getAccounts: noop, }; + const infuraProjectId = 'infura-project-id'; + beforeEach(async function () { - keyringMemStore = new ObservableStore({ isUnlocked: false }); - const networkControllerMessenger = new ControllerMessenger(); - network = new NetworkController({ - messenger: networkControllerMessenger, - infuraProjectId: 'foo', - }); - network.initializeProvider(networkControllerProviderConfig); - provider = network.getProviderAndBlockTracker().provider; - - const tokenListMessenger = new ControllerMessenger().getRestricted({ - name: 'TokenListController', - }); - tokenListController = new TokenListController({ - chainId: '1', - preventPollingOnNetworkRestart: false, - onNetworkStateChange: sinon.spy(), - onPreferencesStateChange: sinon.spy(), - messenger: tokenListMessenger, - }); - await tokenListController.start(); - - preferences = new PreferencesController({ - network, - provider, - tokenListController, - onInfuraIsBlocked: sinon.stub(), - onInfuraIsUnblocked: sinon.stub(), - }); - preferences.setAddresses([ - '0x7e57e2', - '0xbc86727e770de68b1060c91f6bb6945c73e10388', - ]); - preferences.setUseTokenDetection(true); - - tokensController = new TokensController({ - onPreferencesStateChange: preferences.store.subscribe.bind( - preferences.store, - ), - onNetworkStateChange: (cb) => - network.store.subscribe((networkState) => { - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.provider, + sandbox = sinon.createSandbox(); + // Disable all requests, even those to localhost + nock.disableNetConnect(); + nock('https://mainnet.infura.io') + .post(`/v3/${infuraProjectId}`) + .reply(200, (_uri, requestBody) => { + if (requestBody.method === 'eth_getBlockByNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: { + number: '0x42', }, }; - return cb(modifiedNetworkState); - }), - }); + } - assetsContractController = new AssetsContractController({ - onPreferencesStateChange: preferences.store.subscribe.bind( - preferences.store, - ), - onNetworkStateChange: (cb) => - networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, - () => { - const networkState = network.store.getState(); - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.provider, - chainId: convertHexToDecimal(networkState.provider.chainId), - }, - }; - return cb(modifiedNetworkState); - }, - ), - }); + if (requestBody.method === 'eth_blockNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '0x42', + }; + } - sandbox - .stub(network, '_getLatestBlock') - .callsFake(() => Promise.resolve({})); - sandbox - .stub(tokensController, '_instantiateNewEthersProvider') - .returns(null); - sandbox - .stub(tokensController, '_detectIsERC721') - .returns(Promise.resolve(false)); + throw new Error(`(Infura) Mock not defined for ${requestBody.method}`); + }) + .persist(); + nock('https://sepolia.infura.io') + .post(`/v3/${infuraProjectId}`) + .reply(200, (_uri, requestBody) => { + if (requestBody.method === 'eth_getBlockByNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: { + number: '0x42', + }, + }; + } + + if (requestBody.method === 'eth_blockNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '0x42', + }; + } + + throw new Error(`(Infura) Mock not defined for ${requestBody.method}`); + }) + .persist(); + nock('http://localhost:8545') + .post('/') + .reply(200, (_uri, requestBody) => { + if (requestBody.method === 'eth_getBlockByNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: { + number: '0x42', + }, + }; + } + + if (requestBody.method === 'eth_blockNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '0x42', + }; + } + + if (requestBody.method === 'net_version') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '1337', + }; + } + + throw new Error( + `(localhost) Mock not defined for ${requestBody.method}`, + ); + }) + .persist(); nock('https://token-api.metaswap.codefi.network') .get(`/tokens/1`) .reply(200, [ @@ -183,9 +190,82 @@ describe('DetectTokensController', function () { .get(`/tokens/3`) .reply(200, { error: 'ChainId 3 is not supported' }) .persist(); + + keyringMemStore = new ObservableStore({ isUnlocked: false }); + const networkControllerMessenger = new ControllerMessenger(); + network = new NetworkController({ + messenger: networkControllerMessenger, + infuraProjectId, + }); + await network.initializeProvider(networkControllerProviderConfig); + provider = network.getProviderAndBlockTracker().provider; + + const tokenListMessenger = new ControllerMessenger().getRestricted({ + name: 'TokenListController', + }); + tokenListController = new TokenListController({ + chainId: '1', + preventPollingOnNetworkRestart: false, + onNetworkStateChange: sinon.spy(), + onPreferencesStateChange: sinon.spy(), + messenger: tokenListMessenger, + }); + await tokenListController.start(); + + preferences = new PreferencesController({ + network, + provider, + tokenListController, + onInfuraIsBlocked: sinon.stub(), + onInfuraIsUnblocked: sinon.stub(), + }); + preferences.setAddresses([ + '0x7e57e2', + '0xbc86727e770de68b1060c91f6bb6945c73e10388', + ]); + preferences.setUseTokenDetection(true); + + tokensController = new TokensController({ + config: { provider }, + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: (cb) => + network.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + providerConfig: { + ...networkState.provider, + }, + }; + return cb(modifiedNetworkState); + }), + }); + + assetsContractController = new AssetsContractController({ + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: (cb) => + networkControllerMessenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + () => { + const networkState = network.store.getState(); + const modifiedNetworkState = { + ...networkState, + providerConfig: { + ...networkState.provider, + chainId: convertHexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }, + ), + }); }); - after(function () { + afterEach(function () { + nock.enableNetConnect('localhost'); sandbox.restore(); }); diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js index 0fa749d36..ca48d85c4 100644 --- a/app/scripts/controllers/network/network-controller.js +++ b/app/scripts/controllers/network/network-controller.js @@ -11,6 +11,8 @@ import EthQuery from 'eth-query'; // eslint-disable-next-line no-unused-vars import { ControllerMessenger } from '@metamask/base-controller'; import { v4 as random } from 'uuid'; +import { hasProperty, isPlainObject } from '@metamask/utils'; +import { errorCodes } from 'eth-rpc-errors'; import { INFURA_PROVIDER_TYPES, BUILT_IN_NETWORKS, @@ -18,8 +20,8 @@ import { TEST_NETWORK_TICKER_MAP, CHAIN_IDS, NETWORK_TYPES, + NetworkStatus, } from '../../../../shared/constants/network'; -import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -36,40 +38,57 @@ import { createNetworkClient } from './create-network-client'; * @property {string} [nickname] - Personalized network name. */ -const env = process.env.METAMASK_ENV; -const fetchWithTimeout = getFetchWithTimeout(); +function buildDefaultProviderConfigState() { + if (process.env.IN_TEST) { + return { + type: NETWORK_TYPES.RPC, + rpcUrl: 'http://localhost:8545', + chainId: '0x539', + nickname: 'Localhost 8545', + ticker: 'ETH', + }; + } else if ( + process.env.METAMASK_DEBUG || + process.env.METAMASK_ENV === 'test' + ) { + return { + type: NETWORK_TYPES.GOERLI, + chainId: CHAIN_IDS.GOERLI, + ticker: TEST_NETWORK_TICKER_MAP.GOERLI, + }; + } -const name = 'NetworkController'; - -let defaultProviderConfigOpts; -if (process.env.IN_TEST) { - defaultProviderConfigOpts = { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - nickname: 'Localhost 8545', - }; -} else if (process.env.METAMASK_DEBUG || env === 'test') { - defaultProviderConfigOpts = { - type: NETWORK_TYPES.GOERLI, - chainId: CHAIN_IDS.GOERLI, - ticker: TEST_NETWORK_TICKER_MAP.GOERLI, - }; -} else { - defaultProviderConfigOpts = { + return { type: NETWORK_TYPES.MAINNET, chainId: CHAIN_IDS.MAINNET, + ticker: 'ETH', }; } -const defaultProviderConfig = { - ticker: 'ETH', - ...defaultProviderConfigOpts, -}; +function buildDefaultNetworkIdState() { + return null; +} -const defaultNetworkDetailsState = { - EIPS: { 1559: undefined }, -}; +function buildDefaultNetworkStatusState() { + return NetworkStatus.Unknown; +} + +function buildDefaultNetworkDetailsState() { + return { + EIPS: { + 1559: undefined, + }, + }; +} + +function buildDefaultNetworkConfigurationsState() { + return {}; +} + +/** + * The name of the controller. + */ +const name = 'NetworkController'; /** * The set of event types that this controller can publish via its messenger. @@ -98,8 +117,6 @@ export const NetworkControllerEventTypes = { }; export default class NetworkController extends EventEmitter { - static defaultProviderConfig = defaultProviderConfig; - /** * Construct a NetworkController. * @@ -121,31 +138,33 @@ export default class NetworkController extends EventEmitter { // create stores this.providerStore = new ObservableStore( - state.provider || { ...defaultProviderConfig }, + state.provider || buildDefaultProviderConfigState(), ); this.previousProviderStore = new ObservableStore( this.providerStore.getState(), ); - this.networkStore = new ObservableStore('loading'); - // We need to keep track of a few details about the current network - // Ideally we'd merge this.networkStore with this new store, but doing so - // will require a decent sized refactor of how we're accessing network - // state. Currently this is only used for detecting EIP 1559 support but - // can be extended to track other network details. + this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState()); + this.networkStatusStore = new ObservableStore( + buildDefaultNetworkStatusState(), + ); + // We need to keep track of a few details about the current network. + // Ideally we'd merge this.networkStatusStore with this new store, but doing + // so will require a decent sized refactor of how we're accessing network + // state. Currently this is only used for detecting EIP-1559 support but can + // be extended to track other network details. this.networkDetails = new ObservableStore( - state.networkDetails || { - ...defaultNetworkDetailsState, - }, + state.networkDetails || buildDefaultNetworkDetailsState(), ); this.networkConfigurationsStore = new ObservableStore( - state.networkConfigurations || {}, + state.networkConfigurations || buildDefaultNetworkConfigurationsState(), ); this.store = new ComposedStore({ provider: this.providerStore, previousProviderStore: this.previousProviderStore, - network: this.networkStore, + networkId: this.networkIdStore, + networkStatus: this.networkStatusStore, networkDetails: this.networkDetails, networkConfigurations: this.networkConfigurationsStore, }); @@ -189,10 +208,12 @@ export default class NetworkController extends EventEmitter { } /** - * Method to check if the block header contains fields that indicate EIP 1559 - * support (baseFeePerGas). + * Determines whether the network supports EIP-1559 by checking whether the + * latest block has a `baseFeePerGas` property, then updates state + * appropriately. * - * @returns {Promise} true if current network supports EIP 1559 + * @returns {Promise} A promise that resolves to true if the network + * supports EIP-1559 and false otherwise. */ async getEIP1559Compatibility() { const { EIPS } = this.networkDetails.getState(); @@ -201,15 +222,28 @@ export default class NetworkController extends EventEmitter { if (EIPS[1559] !== undefined) { return EIPS[1559]; } - const latestBlock = await this._getLatestBlock(); - const supportsEIP1559 = - latestBlock && latestBlock.baseFeePerGas !== undefined; - this._setNetworkEIPSupport(1559, supportsEIP1559); + const supportsEIP1559 = await this._determineEIP1559Compatibility(); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); return supportsEIP1559; } + /** + * Captures information about the currently selected network — namely, + * the network ID and whether the network supports EIP-1559 — and then uses + * the results of these requests to determine the status of the network. + */ async lookupNetwork() { - // Prevent firing when provider is not defined. + const { chainId, type } = this.providerStore.getState(); + let networkChanged = false; + let networkId; + let supportsEIP1559; + let networkStatus; + if (!this._provider) { log.warn( 'NetworkController - lookupNetwork aborted due to missing provider', @@ -217,46 +251,102 @@ export default class NetworkController extends EventEmitter { return; } - const { chainId } = this.providerStore.getState(); if (!chainId) { log.warn( 'NetworkController - lookupNetwork aborted due to missing chainId', ); - this._setNetworkState('loading'); - this._clearNetworkDetails(); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); return; } - // Ping the RPC endpoint so we can confirm that it works - const initialNetwork = this.networkStore.getState(); - const { type } = this.providerStore.getState(); const isInfura = INFURA_PROVIDER_TYPES.includes(type); - if (isInfura) { - this._checkInfuraAvailability(type); - } else { - this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); + const listener = () => { + networkChanged = true; + this.messenger.unsubscribe( + NetworkControllerEventTypes.NetworkDidChange, + listener, + ); + }; + this.messenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + listener, + ); + + try { + const results = await Promise.all([ + this._getNetworkId(), + this._determineEIP1559Compatibility(), + ]); + networkId = results[0]; + supportsEIP1559 = results[1]; + networkStatus = NetworkStatus.Available; + } catch (error) { + if (hasProperty(error, 'code')) { + let responseBody; + try { + responseBody = JSON.parse(error.message); + } catch { + // error.message must not be JSON + } + + if ( + isPlainObject(responseBody) && + responseBody.error === INFURA_BLOCKED_KEY + ) { + networkStatus = NetworkStatus.Blocked; + } else if (error.code === errorCodes.rpc.internal) { + networkStatus = NetworkStatus.Unknown; + } else { + networkStatus = NetworkStatus.Unavailable; + } + } else { + log.warn( + 'NetworkController - could not determine network status', + error, + ); + networkStatus = NetworkStatus.Unknown; + } } - let networkVersion; - let networkVersionError; - try { - networkVersion = await this._getNetworkId(); - } catch (error) { - networkVersionError = error; - } - if (initialNetwork !== this.networkStore.getState()) { + if (networkChanged) { + // If the network has changed, then `lookupNetwork` either has been or is + // in the process of being called, so we don't need to go further. return; } + this.messenger.unsubscribe( + NetworkControllerEventTypes.NetworkDidChange, + listener, + ); - if (networkVersionError) { - this._setNetworkState('loading'); - // keep network details in sync with network state - this._clearNetworkDetails(); + this.networkStatusStore.putState(networkStatus); + + if (networkStatus === NetworkStatus.Available) { + this.networkIdStore.putState(networkId); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); } else { - this._setNetworkState(networkVersion); - // look up EIP-1559 support - await this.getEIP1559Compatibility(); + this._resetNetworkId(); + this._resetNetworkDetails(); + } + + if (isInfura) { + if (networkStatus === NetworkStatus.Available) { + this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); + } else if (networkStatus === NetworkStatus.Blocked) { + this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked); + } + } else { + // Always publish infuraIsUnblocked regardless of network status to + // prevent consumers from being stuck in a blocked state if they were + // previously connected to an Infura network that was blocked + this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); } } @@ -319,13 +409,38 @@ export default class NetworkController extends EventEmitter { // Private // + /** + * Method to return the latest block for the current network + * + * @returns {object} Block header + */ + _getLatestBlock() { + const { provider } = this.getProviderAndBlockTracker(); + const ethQuery = new EthQuery(provider); + + return new Promise((resolve, reject) => { + ethQuery.sendAsync( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }, + ); + }); + } + /** * Get the network ID for the current selected network * * @returns {string} The network ID for the current network. */ async _getNetworkId() { - const ethQuery = new EthQuery(this._provider); + const { provider } = this.getProviderAndBlockTracker(); + const ethQuery = new EthQuery(provider); + return await new Promise((resolve, reject) => { ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { if (error) { @@ -338,49 +453,24 @@ export default class NetworkController extends EventEmitter { } /** - * Method to return the latest block for the current network - * - * @returns {object} Block header + * Clears the stored network ID. */ - _getLatestBlock() { - return new Promise((resolve, reject) => { - const { provider } = this.getProviderAndBlockTracker(); - const ethQuery = new EthQuery(provider); - ethQuery.sendAsync( - { method: 'eth_getBlockByNumber', params: ['latest', false] }, - (err, block) => { - if (err) { - return reject(err); - } - return resolve(block); - }, - ); - }); - } - - _setNetworkState(network) { - this.networkStore.putState(network); + _resetNetworkId() { + this.networkIdStore.putState(buildDefaultNetworkIdState()); } /** - * Set EIP support indication in the networkDetails store - * - * @param {number} EIPNumber - The number of the EIP to mark support for - * @param {boolean} isSupported - True if the EIP is supported + * Resets network status to the default ("unknown"). */ - _setNetworkEIPSupport(EIPNumber, isSupported) { - this.networkDetails.putState({ - EIPS: { - [EIPNumber]: isSupported, - }, - }); + _resetNetworkStatus() { + this.networkStatusStore.putState(buildDefaultNetworkStatusState()); } /** - * Reset EIP support to default (no support) + * Clears details previously stored for the network. */ - _clearNetworkDetails() { - this.networkDetails.putState({ ...defaultNetworkDetailsState }); + _resetNetworkDetails() { + this.networkDetails.putState(buildDefaultNetworkDetailsState()); } /** @@ -394,67 +484,26 @@ export default class NetworkController extends EventEmitter { this._switchNetwork(config); } - async _checkInfuraAvailability(network) { - const rpcUrl = `https://${network}.infura.io/v3/${this._infuraProjectId}`; - - let networkChanged = false; - const listener = () => { - networkChanged = true; - this.messenger.unsubscribe( - NetworkControllerEventTypes.NetworkDidChange, - listener, - ); - }; - this.messenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, - listener, - ); - - try { - const response = await fetchWithTimeout(rpcUrl, { - method: 'POST', - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_blockNumber', - params: [], - id: 1, - }), - }); - - if (networkChanged) { - return; - } - - if (response.ok) { - this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); - } else { - const responseMessage = await response.json(); - if (networkChanged) { - return; - } - if (responseMessage.error === INFURA_BLOCKED_KEY) { - this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked); - } - } - } catch (err) { - log.warn(`MetaMask - Infura availability check failed`, err); - } + /** + * Retrieves the latest block from the currently selected network; if the + * block has a `baseFeePerGas` property, then we know that the network + * supports EIP-1559; otherwise it doesn't. + * + * @returns {Promise} A promise that resolves to true if the network + * supports EIP-1559 and false otherwise. + */ + async _determineEIP1559Compatibility() { + const latestBlock = await this._getLatestBlock(); + return latestBlock && latestBlock.baseFeePerGas !== undefined; } _switchNetwork(opts) { - // Indicate to subscribers that network is about to change this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange); - // Set loading state - this._setNetworkState('loading'); - // Reset network details - this._clearNetworkDetails(); - // Configure the provider appropriately + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); this._configureProvider(opts); - // Notify subscribers that network has changed - this.messenger.publish( - NetworkControllerEventTypes.NetworkDidChange, - opts.type, - ); + this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange); this.lookupNetwork(); } diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index ce160a65a..e8684179e 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -3,19 +3,11 @@ import { isMatch } from 'lodash'; import { v4 } from 'uuid'; import nock from 'nock'; import sinon from 'sinon'; -import * as ethJsonRpcProvider from '@metamask/eth-json-rpc-provider'; import { ControllerMessenger } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; import { EVENT } from '../../../../shared/constants/metametrics'; import NetworkController from './network-controller'; -jest.mock('@metamask/eth-json-rpc-provider', () => { - return { - __esModule: true, - ...jest.requireActual('@metamask/eth-json-rpc-provider'), - }; -}); - jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -59,35 +51,7 @@ const originalSetTimeout = global.setTimeout; * `baseFeePerGas` property). */ const PRE_1559_BLOCK = { - difficulty: '0x0', - extraData: '0x', - gasLimit: '0x1c9c380', - gasUsed: '0x598c9b', - hash: '0xfb2086eb924ffce4061f94c3b65f303e0351f8e7deff185fe1f5e9001ff96f63', - logsBloom: - '0x7034820113921800018e8070900006316040002225c04a0624110010841018a2109040401004112a4c120f00220a2119020000714b143a04004106120130a8450080433129401068ed22000a54a48221a1020202524204045421b883882530009a1800b08a1309408008828403010d530440001a40003c0006240291008c0404c211610c690b00f1985e000009c02503240040010989c01cf2806840043815498e90012103e06084051542c0094002494008044c24a0a13281e0009601481073010800130402464202212202a8088210442a8ec81b080430075629e60a00a082005a3988400940a4009012a204011a0018a00903222a60420428888144210802', - miner: '0xffee087852cb4898e6c3532e776e68bc68b1143b', - mixHash: '0xb17ba50cd7261e77a213fb75704dcfd8a28fbcd78d100691a112b7cc2893efa2', - nonce: '0x0000000000000000', - number: '0x2', // number set to "2" to simplify tests - parentHash: - '0x31406d1bf1a2ca12371ce5b3ecb20568d6a8b9bf05b49b71b93ba33f317d5a82', - receiptsRoot: - '0x5ba97ece1afbac2a8fe0344f9022fe808342179b26ea3ecc2e0b8c4b46b7f8cd', - sha3Uncles: - '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - size: '0x70f4', - stateRoot: - '0x36bfb7ca106d41c4458292669126e091011031c5af612dee1c2e6424ef92b080', - timestamp: '0x639b6d9b', - totalDifficulty: '0xc70d815d562d3cfa955', - transactions: [ - // reduced to a single transaction to make fixture less verbose - '0x2761e939dc822f64141bd00bc7ef8cee16201af10e862469212396664cee81ce', - ], - transactionsRoot: - '0x98bbdfbe1074bc3aa72a77a281f16d6ba7e723d68f15937d80954fb34d323369', - uncles: [], + number: '0x42', }; /** @@ -123,28 +87,62 @@ const JSONRPC_RESPONSE_BODY_PROPERTIES = ['id', 'jsonrpc', 'result', 'error']; */ const INFURA_NETWORKS = [ { - nickname: 'Mainnet', networkType: 'mainnet', chainId: '0x1', - networkVersion: '1', + networkId: '1', ticker: 'ETH', }, { - nickname: 'Goerli', networkType: 'goerli', chainId: '0x5', - networkVersion: '5', + networkId: '5', ticker: 'GoerliETH', }, { - nickname: 'Sepolia', networkType: 'sepolia', chainId: '0xaa36a7', - networkVersion: '11155111', + networkId: '11155111', ticker: 'SepoliaETH', }, ]; +/** + * A response object for a successful request to `eth_getBlockByNumber`. It is + * assumed that the block number here is insignificant to the test. + */ +const SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE = { + result: BLOCK, + error: null, +}; + +/** + * A response object for a request that has been geoblocked by Infura. + */ +const BLOCKED_INFURA_RESPONSE = { + result: null, + error: 'countryBlocked', + httpStatus: 500, +}; + +/** + * A response object for a successful request to `net_version`. It is assumed + * that the network ID here is insignificant to the test. + */ +const SUCCESSFUL_NET_VERSION_RESPONSE = { + result: '42', + error: null, +}; + +/** + * A response object for a unsuccessful request to any RPC method. It is assumed + * that the error here is insignificant to the test. + */ +const UNSUCCESSFUL_JSON_RPC_RESPONSE = { + result: null, + error: 'oops', + httpStatus: 500, +}; + /** * Handles mocking provider requests for a particular network. */ @@ -183,6 +181,9 @@ class NetworkCommunications { customRpcUrl, }; this.networkClientType = networkClientType; + if (networkClientType !== 'infura' && networkClientType !== 'custom') { + throw new Error("networkClientType must be 'infura' or 'custom'"); + } this.#networkClientOptions = networkClientOptions; this.infuraProjectId = infuraProjectId; const rpcUrl = @@ -239,23 +240,6 @@ class NetworkCommunications { } const defaultMocksByRpcMethod = { - eth_blockNumber: { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: latestBlockNumber, - }, - // When the provider is configured for an Infura network, - // NetworkController makes a sentinel request for `eth_blockNumber`, so - // we ensure that it is mocked by default. Conversely, when the provider - // is configured for a custom RPC endpoint, we don't mock - // `eth_blockNumber` at all unless specified. Admittedly, this is a bit - // magical, but it saves us from having to think about this in tests - // if we don't have to. - times: this.networkClientType === 'infura' ? 1 : 0, - }, eth_getBlockByNumber: { request: { method: 'eth_getBlockByNumber', @@ -274,11 +258,29 @@ class NetworkCommunications { result: '1', }, }, + // The request that the block tracker makes always occurs after any + // request that the network controller makes (because such a request goes + // through the block cache middleware and that is what spawns the block + // tracker). + eth_blockNumber: { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: latestBlockNumber, + }, + // If there is no latest block number then the request that spawned the + // block tracker won't be cached inside of the block tracker, so the + // block tracker makes another request when it is asked for the latest + // block. + times: latestBlock === null ? 2 : 1, + }, }; const providedMocksByRpcMethod = { - eth_blockNumber: ethBlockNumberMocks, eth_getBlockByNumber: ethGetBlockByNumberMocks, net_version: netVersionMocks, + eth_blockNumber: ethBlockNumberMocks, }; const allMocks = []; @@ -298,23 +300,6 @@ class NetworkCommunications { } }); - // The request that the block tracker makes always occurs after any request - // that the network controller makes (because such a request goes through - // the block cache middleware and that is what spawns the block tracker). We - // don't need to customize the block tracker request; we just need to ensure - // that the block number it returns matches the same block number that - // `eth_getBlockByNumber` uses. - allMocks.push({ - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: latestBlockNumber, - }, - times: latestBlock === null ? 2 : 1, - }); - allMocks.forEach((mock) => { this.mockRpcCall(mock); }); @@ -448,34 +433,33 @@ describe('NetworkController', () => { }); it('accepts initial state', async () => { - const exampleInitialState = { - provider: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', - }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, - }; - await withController( { - state: exampleInitialState, + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + chainId: '0x9999', + nickname: 'Test initial state', + }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + }, }, ({ controller }) => { expect(controller.store.getState()).toMatchInlineSnapshot(` { - "network": "loading", "networkConfigurations": {}, "networkDetails": { "EIPS": { "1559": false, }, }, + "networkId": null, + "networkStatus": "unknown", "previousProviderStore": { "chainId": "0x9999", "nickname": "Test initial state", @@ -498,13 +482,14 @@ describe('NetworkController', () => { await withController(({ controller }) => { expect(controller.store.getState()).toMatchInlineSnapshot(` { - "network": "loading", "networkConfigurations": {}, "networkDetails": { "EIPS": { "1559": undefined, }, }, + "networkId": null, + "networkStatus": "unknown", "previousProviderStore": { "chainId": "0x539", "nickname": "Localhost 8545", @@ -538,7 +523,7 @@ describe('NetworkController', () => { await withController(async ({ controller, network }) => { network.mockEssentialRpcCalls({ eth_blockNumber: { - times: 1, + times: 2, }, }); await controller.initializeProvider(); @@ -547,7 +532,7 @@ describe('NetworkController', () => { blockTracker.addListener('latest', () => { // do nothing }); - expect(blockTracker.isRunning()).toBe(true); + expect(blockTracker.isRunning()).toBeTruthy(); await controller.destroy(); @@ -575,14 +560,9 @@ describe('NetworkController', () => { ); }); - for (const { - nickname, - networkType, - chainId, - networkVersion, - } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { - it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController( { state: { @@ -611,7 +591,7 @@ describe('NetworkController', () => { ); }); - it('emits infuraIsUnblocked (assuming that the request to eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { const messenger = buildMessenger(); await withController( @@ -642,7 +622,7 @@ describe('NetworkController', () => { ); }); - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { + it('determines the status of the network, storing it in state', async () => { await withController( { state: { @@ -656,15 +636,18 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls(); + expect(controller.store.getState().networkStatus).toBe('unknown'); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); }, ); }); - it(`persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)`, async () => { + it('determines whether the network supports EIP-1559 and stores the result in state without overwriting other state in the networkDetails store', async () => { await withController( { state: { @@ -674,9 +657,10 @@ describe('NetworkController', () => { // of the network selected, it just needs to exist chainId: '0x9999999', }, - }, - networkDetails: { - EIPS: {}, + networkDetails: { + EIPS: {}, + other: 'details', + }, }, }, async ({ controller, network }) => { @@ -686,9 +670,12 @@ describe('NetworkController', () => { await controller.initializeProvider(); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + other: 'details', + }); }, ); }); @@ -790,7 +777,48 @@ describe('NetworkController', () => { ); }); - it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: '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, network }) => { + network.mockEssentialRpcCalls(); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('determines the status of the network, storing it in state', async () => { await withController( { state: { @@ -814,20 +842,19 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); + expect(controller.store.getState().networkStatus).toBe('unknown'); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('available'); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('determines whether the network supports EIP-1559, storing it in state', async () => { await withController( { state: { @@ -846,6 +873,10 @@ describe('NetworkController', () => { id: 'testNetworkConfigurationId', }, }, + networkDetails: { + EIPS: {}, + other: 'details', + }, }, }, async ({ controller, network }) => { @@ -855,9 +886,12 @@ describe('NetworkController', () => { await controller.initializeProvider(); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + other: 'details', + }); }, ); }); @@ -888,9 +922,9 @@ describe('NetworkController', () => { }); }); - for (const { nickname, networkType, chainId } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is changed to "${networkType}"`, () => { - it(`returns a provider object that was pointed to another network before the switch and is pointed to ${nickname} afterward`, async () => { + it(`returns a provider object that was pointed to another network before the switch and is pointed to "${networkType}" afterward`, async () => { await withController( { state: { @@ -898,25 +932,18 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', - }, - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, }, }, }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: networkType, + }, + }); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); @@ -953,15 +980,21 @@ describe('NetworkController', () => { networkConfigurations: { testNetworkConfigurationId: { rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', + chainId: '0x1337', id: 'testNetworkConfigurationId', }, }, }, }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); @@ -980,7 +1013,7 @@ describe('NetworkController', () => { const { result: newChainIdResult } = await promisifiedSendAsync2({ method: 'eth_chainId', }); - expect(newChainIdResult).toBe('0xtest'); + expect(newChainIdResult).toBe('0x1337'); }, ); }); @@ -989,7 +1022,7 @@ describe('NetworkController', () => { describe('getEIP1559Compatibility', () => { describe('when the latest block has a baseFeePerGas property', () => { - it('persists to state that the network supports EIP-1559', async () => { + it('stores the fact that the network supports EIP-1559', async () => { await withController( { state: { @@ -1022,13 +1055,13 @@ describe('NetworkController', () => { const supportsEIP1559 = await controller.getEIP1559Compatibility(); - expect(supportsEIP1559).toBe(true); + expect(supportsEIP1559).toBeTruthy(); }); }); }); describe('when the latest block does not have a baseFeePerGas property', () => { - it('persists to state that the network does not support EIP-1559', async () => { + it('stores the fact that the network does not support EIP-1559', async () => { await withController( { state: { @@ -1067,7 +1100,7 @@ describe('NetworkController', () => { }); describe('when the request for the latest block responds with null', () => { - it('persists null to state as whether the network supports EIP-1559', async () => { + it('stores null as whether the network supports EIP-1559', async () => { await withController( { state: { @@ -1107,8 +1140,11 @@ describe('NetworkController', () => { it('does not make multiple requests to eth_getBlockByNumber when called multiple times and the request to eth_getBlockByNumber succeeded the first time', async () => { await withController(async ({ controller, network }) => { - // This mocks eth_getBlockByNumber once by default - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + times: 1, + }, + }); await withoutCallingGetEIP1559Compatibility({ controller, operation: async () => { @@ -1119,7 +1155,7 @@ describe('NetworkController', () => { await controller.getEIP1559Compatibility(); await controller.getEIP1559Compatibility(); - expect(network.nockScope.isDone()).toBe(true); + expect(network.nockScope.isDone()).toBeTruthy(); }); }); }); @@ -1165,15 +1201,16 @@ describe('NetworkController', () => { await withController({ messenger }, async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const promiseForInfuraIsUnblocked = waitForPublishedEvents({ + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ messenger, eventType: 'NetworkController:infuraIsUnblocked', + count: 0, operation: async () => { await controller.lookupNetwork(); }, }); - await expect(promiseForInfuraIsUnblocked).toNeverResolve(); + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); }); }); @@ -1183,23 +1220,285 @@ describe('NetworkController', () => { await withController({ messenger }, async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const promiseForInfuraIsBlocked = waitForPublishedEvents({ + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ messenger, eventType: 'NetworkController:infuraIsBlocked', + count: 0, operation: async () => { await controller.lookupNetwork(); }, }); - await expect(promiseForInfuraIsBlocked).toNeverResolve(); + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); }); }); }); - for (const { nickname, networkType, networkVersion } of INFURA_NETWORKS) { + describe('if the provider has initialized, but the current network has no chainId', () => { + it('does not update state in any way', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); + const stateAfterInitialization = controller.store.getState(); + + await controller.lookupNetwork(); + + expect(controller.store.getState()).toStrictEqual( + stateAfterInitialization, + ); + }, + ); + }); + + it('does not emit infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); + + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); + }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + }); + + INFURA_NETWORKS.forEach(({ networkType, networkId }) => { describe(`when the type in the provider configuration is "${networkType}"`, () => { - describe('if the request for eth_blockNumber responds successfully', () => { - it('emits infuraIsUnblocked as long as the network has not changed by the time the request ends', async () => { + describe('if the request for eth_getBlockByNumber responds successfully', () => { + it('stores the fact that the network is available', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + }, + ); + }); + + it('stores the ID of the network', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkId).toBe(networkId); + }, + ); + }); + + it('stores the fact that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: {}, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBeTruthy(); + }, + ); + }); + + it('stores the fact that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: {}, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBe(false); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { const messenger = buildMessenger(); await withController( @@ -1216,10 +1515,10 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - result: '0x42', - }, + eth_getBlockByNumber: { + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, }, }); await withoutCallingLookupNetwork({ @@ -1241,8 +1540,894 @@ describe('NetworkController', () => { }, ); }); + }); - it('does not emit infuraIsUnblocked if the network has changed by the time the request ends', async () => { + describe('if the request for eth_blockNumber responds with a "countryBlocked" error', () => { + it('stores the fact that the network is blocked', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkStatus).toBe( + 'blocked', + ); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // Ensure that each call to eth_blockNumber returns a + // different block number, otherwise the first + // eth_getBlockByNumber response will get cached under the + // first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: BLOCKED_INFURA_RESPONSE, + }, + ], + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe(networkId); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect( + controller.store.getState().networkDetails, + ).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsBlocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsBlocked).toBeTruthy(); + }, + ); + }); + + it('does not emit infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for eth_getBlockByNumber responds with a generic error', () => { + it('stores the network status as unavailable if the error does not translate to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: { + error: 'some error', + httpStatus: 405, + }, + }, + ], + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unavailable', + ); + }, + ); + }); + + it('stores the network status as unknown if the error translates to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: { + error: 'some error', + httpStatus: 500, + }, + }, + ], + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + // Ensure that each call to eth_blockNumber returns a + // different block number, otherwise the first + // eth_getBlockByNumber response will get cached under the + // first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe(networkId); + + // Advance block tracker loop to force a fresh call to + // eth_getBlockByNumber + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + const intentionalErrorMessage = + 'intentional error from eth_getBlockByNumber'; + + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + try { + await controller.lookupNetwork(); + } catch (error) { + if (error !== intentionalErrorMessage) { + console.error(error); + } + } + }, + }); + expect( + controller.store.getState().networkDetails, + ).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); + }, + ); + }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsBlockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('does not emit infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); + }); + }); + + describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: { + result: { + ...BLOCK, + number: '0x1', + }, + }, + }, + { + request: { + params: ['0x2', false], + }, + response: { + result: { + ...BLOCK, + number: '0x2', + }, + }, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); + }, + }); + }, + }, + ], + }); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls({ + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); + }, + ); + }); + + it('stores the ID of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); + }, + }); + }, + net_version: { + response: { + result: '111', + }, + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls({ + net_version: { + response: { + result: '222', + }, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkId).toBe('222'); + }, + ); + }); + + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails, + ).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { const anotherNetwork = INFURA_NETWORKS.find( (network) => network.networkType !== networkType, ); @@ -1269,17 +2454,15 @@ describe('NetworkController', () => { }, async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - result: '0x42', - }, + eth_getBlockByNumber: { beforeCompleting: async () => { await waitForPublishedEvents({ messenger, eventType: 'NetworkController:networkDidChange', operation: async () => { - await withoutCallingLookupNetwork({ + await waitForStateChanges({ controller, + propertyPath: ['networkStatus'], operation: () => { controller.setProviderType( anotherNetwork.networkType, @@ -1292,55 +2475,14 @@ describe('NetworkController', () => { }, }); const network2 = network1.with({ + networkClientType: 'infura', networkClientOptions: { infuraNetwork: anotherNetwork.networkType, }, }); - network2.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const promiseForInfuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsUnblocked).toNeverResolve(); - }, - ); - }); - }); - - describe('if the request for eth_blockNumber responds with a "countryBlocked" error', () => { - it('emits infuraIsBlocked as long as the network has not changed by the time the request ends', async () => { - const messenger = buildMessenger(); - - await withController( - { - messenger, - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - httpStatus: 500, - error: 'countryBlocked', - }, + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, }, }); await withoutCallingLookupNetwork({ @@ -1349,650 +2491,89 @@ describe('NetworkController', () => { await controller.initializeProvider(); }, }); - - const infuraIsBlocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(infuraIsBlocked).toBeTruthy(); - }, - ); - }); - - it('does not emit infuraIsBlocked if the network has changed by the time the request ends', async () => { - const messenger = buildMessenger(); - - await withController( - { - messenger, - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - }, - }, - }, - }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - httpStatus: 500, - error: 'countryBlocked', - }, - beforeCompleting: async () => { - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }); - }, - }); - }, - }, - }); - const network2 = new NetworkCommunications({ - networkClientType: 'rpc', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, - }); - network2.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); const promiseForInfuraIsBlocked = waitForPublishedEvents({ messenger, eventType: 'NetworkController:infuraIsBlocked', - operation: async () => { - await controller.lookupNetwork(); - }, }); - await expect(promiseForInfuraIsBlocked).toNeverResolve(); - }, - ); - }); - }); - - describe('if the request for eth_blockNumber responds with a generic error', () => { - it('does not emit infuraIsUnblocked', async () => { - const messenger = buildMessenger(); - - await withController( - { - messenger, - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - httpStatus: 500, - error: 'oops', - }, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const promiseForInfuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsUnblocked).toNeverResolve(); - }, - ); - }); - }); - - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().network).toBe(networkVersion); - }, - ); - }); - - it(`does not update the network state if it is already set to "${networkVersion}"`, async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_blockNumber: { - times: 2, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - const promiseForStateChanges = waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForStateChanges).toNeverResolve(); - }, - ); - }); - - describe('if the request for eth_getBlockByNumber responds successfully', () => { - it('persists to state that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - }); - await controller.initializeProvider(); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(true); - }, - ); - }); - - it('persists to state that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: PRE_1559_BLOCK, - }); - await controller.initializeProvider(); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(false); - }, - ); - }); - }); - - describe('if the request for eth_getBlockByNumber responds with an error', () => { - it('does not update the network details in any way', async () => { - const intentionalErrorMessage = - 'intentional error from eth_getBlockByNumber'; - - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_getBlockByNumber: { - response: { - error: intentionalErrorMessage, - }, - }, - }); - await withoutCallingGetEIP1559Compatibility({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - count: 0, - operation: async () => { - try { - await controller.getEIP1559Compatibility(); - } catch (error) { - if (error !== intentionalErrorMessage) { - console.error(error); - } - } - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - }, - ); - }); - }); - - describe('if the network was switched after the net_version request started but before it completed', () => { - it(`persists to state the network version of the newly switched network, not the initial one for ${nickname}`, async () => { - const oldNetworkVersion = networkVersion; - const newChainName = 'goerli'; - const newNetworkVersion = '5'; - - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller }) => { - let netVersionCallCount = 0; - - const fakeProviders = [ - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - netVersionCallCount += 1; - if (netVersionCallCount === 1) { - waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: () => { - controller.setProviderType(newChainName); - }, - }) - .then(() => { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: oldNetworkVersion, - }); - }) - .catch((error) => { - throw error; - }); - return; - } - - throw new Error( - "net_version shouldn't be called more than once", - ); - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: newNetworkVersion, - }); - return; - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - ]; - jest - .spyOn(ethJsonRpcProvider, 'providerFromEngine') - .mockImplementationOnce(() => fakeProviders[0]) - .mockImplementationOnce(() => fakeProviders[1]); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().network).toBe( - newNetworkVersion, - ); - }, - ); - }); - - it(`persists to state the EIP-1559 support for the newly switched network, not the initial one for ${nickname}`, async () => { - const oldNetworkVersion = networkVersion; - const newChainName = 'goerli'; - const newNetworkVersion = '5'; - - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller }) => { - let netVersionCallCount = 0; - - const fakeProviders = [ - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - netVersionCallCount += 1; - if (netVersionCallCount === 1) { - waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: () => { - controller.setProviderType(newChainName); - }, - }) - .then(() => { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: oldNetworkVersion, - }); - }) - .catch((error) => { - throw error; - }); - return; - } - - throw new Error( - "net_version shouldn't be called more than once", - ); - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: POST_1559_BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: newNetworkVersion, - }); - return; - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: PRE_1559_BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - ]; - jest - .spyOn(ethJsonRpcProvider, 'providerFromEngine') - .mockImplementationOnce(() => fakeProviders[0]) - .mockImplementationOnce(() => fakeProviders[1]); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); - }, - ); - }); - }); - }); - } - - describe(`when the type in the provider configuration is "rpc"`, () => { - it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); - - await withController( - { - messenger, - state: { - provider: { - type: 'rpc', - rpcUrl: '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, network }) => { - network.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { await controller.lookupNetwork(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, - }); - - expect(infuraIsUnblocked).toBeTruthy(); - }, - ); + ); + }); + }); }); + }); - describe('if the request for net_version responds successfully', () => { - it('persists the network version to state', async () => { + describe('when the type in the provider configuration is "rpc"', () => { + describe('if both net_version and eth_getBlockByNumber respond successfully', () => { + it('stores the fact the network is available', async () => { await withController( { state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, + chainId: '0x1337', }, }, }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + eth_getBlockByNumber: { + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + }, + ); + }); + + it('stores the ID of the network', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, net_version: { response: { result: '42', @@ -2005,48 +2586,39 @@ describe('NetworkController', () => { await controller.initializeProvider(); }, }); + expect(controller.store.getState().networkId).toBe(null); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkId'], operation: async () => { await controller.lookupNetwork(); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkId).toBe('42'); }, ); }); - it('does not persist the result of net_version if it matches what is already in state', async () => { + it('stores the fact that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { await withController( { state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, + chainId: '0x1337', }, }, }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, net_version: { - times: 2, - }, - eth_blockNumber: { - times: 2, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); await withoutCallingLookupNetwork({ @@ -2056,295 +2628,6 @@ describe('NetworkController', () => { }, }); - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - const promiseForStateChanges = waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForStateChanges).toNeverResolve(); - }, - ); - }); - - describe('if the request for eth_getBlockByNumber responds successfully', () => { - it('persists to state that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await controller.lookupNetwork(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(true); - }, - ); - }); - - it('persists to state that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'TEST2', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'TEST2', - id: 'testNetworkConfigurationId', - }, - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: PRE_1559_BLOCK, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await controller.lookupNetwork(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(false); - }, - ); - }); - }); - - describe('if the request for eth_getBlockByNumber responds with an error', () => { - it('does not update the network details in any way', async () => { - const intentionalErrorMessage = - 'intentional error from eth_getBlockByNumber'; - - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_getBlockByNumber: { - response: { - error: intentionalErrorMessage, - }, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - count: 0, - operation: async () => { - try { - await controller.lookupNetwork(); - } catch (error) { - if ( - !('data' in error) || - error.data !== intentionalErrorMessage - ) { - console.error(error); - } - } - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - }, - ); - }); - }); - }); - - describe('if the request for net_version responds with an error', () => { - it('resets the network status to "loading"', async () => { - const intentionalErrorMessage = 'intentional error from net_version'; - - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: '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, network }) => { - network.mockEssentialRpcCalls({ - net_version: [ - { - response: { - result: '42', - }, - }, - { - response: { - error: intentionalErrorMessage, - }, - }, - ], - }); - await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('42'); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - try { - await controller.lookupNetwork(); - } catch (error) { - if (error !== intentionalErrorMessage) { - console.error(error); - } - } - }, - }); - - expect(controller.store.getState().network).toBe('loading'); - }, - ); - }); - - it('removes from state whether the network supports EIP-1559', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - net_version: [ - { - response: { - result: '42', - }, - }, - { - response: { - error: 'oops', - }, - }, - ], - }); - await controller.initializeProvider(); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - await waitForStateChanges({ controller, propertyPath: ['networkDetails'], @@ -2353,6 +2636,306 @@ describe('NetworkController', () => { }, }); + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBeTruthy(); + }, + ); + }); + + it('stores the fact that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: PRE_1559_BLOCK, + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBe(false); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: PRE_1559_BLOCK, + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for eth_getBlockByNumber responds successfully, but the request for net_version responds with a generic error', () => { + it('stores the network status as available if the error does not translate to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: [ + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + { + response: { + error: 'some error', + httpStatus: 405, + }, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unavailable', + ); + }, + ); + }); + + it('stores the network status as unknown if the error translates to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: [ + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: [ + { + response: { + result: '42', + }, + }, + { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe('42'); + + // Advance block tracker loop to force a fresh call to + // eth_getBlockByNumber + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); expect(controller.store.getState().networkDetails).toStrictEqual({ EIPS: { 1559: undefined, @@ -2361,10 +2944,467 @@ describe('NetworkController', () => { }, ); }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for net_version responds successfully, but the request for eth_getBlockByNumber responds with a generic error', () => { + it('stores the fact that the network is unavailable', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + latestBlock: BLOCK, + net_version: { + response: { + result: '42', + }, + }, + eth_getBlockByNumber: [ + { + response: { + result: BLOCK, + }, + }, + { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe('42'); + + // Advance block tracker loop to force a fresh call to + // eth_getBlockByNumber + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); + }, + ); + }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); }); describe('if the network was switched after the net_version request started but before it completed', () => { - it('persists to state the network version of the newly switched network, not the initial network', async () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: [ + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + ], + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: { + result: { + ...BLOCK, + number: '0x1', + }, + }, + }, + { + request: { + params: ['0x2', false], + }, + response: { + result: { + ...BLOCK, + number: '0x2', + }, + }, + }, + ], + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('stores the ID of the second network, not the first', async () => { await withController( { state: { @@ -2373,24 +3413,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, - networkDetails: { - EIPS: {}, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, - testNetworkConfigurationId2: { - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, }, }, }, @@ -2403,28 +3425,21 @@ describe('NetworkController', () => { beforeCompleting: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId1', - ); + controller.setProviderType('goerli'); }, }); }, }, }); - const network2 = network1.with({ + const network2 = new NetworkCommunications({ + networkClientType: 'infura', networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-1', - }, - }); - network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: '222', - }, + infuraNetwork: 'goerli', }, }); + network2.mockEssentialRpcCalls(); await withoutCallingLookupNetwork({ controller, @@ -2435,19 +3450,261 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkId'], operation: async () => { await controller.lookupNetwork(); }, }); - expect(controller.store.getState().network).toBe('222'); + expect(controller.store.getState().networkId).toBe('5'); }, ); }); - it('persists to state the EIP-1559 support for the newly switched network, not the initial one', async () => { - const nonEip1559RpcUrl = 'https://mock-rpc-url-1'; + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + networkDetails: { + EIPS: {}, + other: 'details', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + // setProviderType clears networkDetails first, and then updates + // it to what we expect it to be + count: 2, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); + + await controller.lookupNetwork(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x1337', + ticker: 'RPC', + }, + networkDetails: { + EIPS: {}, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: { + result: { + ...BLOCK, + number: '0x1', + }, + }, + }, + { + request: { + params: ['0x2', false], + }, + response: { + result: { + ...BLOCK, + number: '0x2', + }, + }, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + ], + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('stores the network ID of the second network, not the first', async () => { await withController( { state: { @@ -2456,78 +3713,184 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, - networkDetails: { - EIPS: {}, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: nonEip1559RpcUrl, - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, - testNetworkConfigurationId2: { - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, }, }, }, async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ - net_version: { - response: { - result: '111', - }, + eth_getBlockByNumber: { beforeCompleting: async () => { await waitForStateChanges({ controller, - propertyPath: ['networkDetails'], + propertyPath: ['networkStatus'], operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId1', - ); + controller.setProviderType('goerli'); }, }); }, }, - eth_getBlockByNumber: { - response: { - result: POST_1559_BLOCK, - }, - }, - }); - const network2 = network1.with({ - networkClientOptions: { - customRpcUrl: nonEip1559RpcUrl, - }, - }); - network2.mockEssentialRpcCalls({ net_version: { response: { - result: '222', - }, - }, - eth_getBlockByNumber: { - response: { - result: PRE_1559_BLOCK, + result: '111', }, }, }); - await waitForLookupNetworkToComplete({ + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls(); + + await withoutCallingLookupNetwork({ controller, operation: async () => { await controller.initializeProvider(); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkId).toBe('5'); + }, + ); + }); + + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + networkDetails: { + EIPS: {}, + other: 'details', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + // setProviderType clears networkDetails first, and then updates + // it to what we expect it to be + count: 2, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); + + await controller.lookupNetwork(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, ); }); @@ -2564,28 +3927,21 @@ describe('NetworkController', () => { ); }); - it('captures the current provider configuration before overwriting it', async () => { + it('stores the current provider configuration before overwriting it', async () => { await withController( { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x9999', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, testNetworkConfigurationId2: { rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x9999', - ticker: 'RPC', + chainId: '0x222', id: 'testNetworkConfigurationId2', }, }, @@ -2600,16 +3956,16 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - controller.setActiveNetwork('testNetworkConfigurationId1'); + controller.setActiveNetwork('testNetworkConfigurationId2'); expect( controller.store.getState().previousProviderStore, ).toStrictEqual({ type: 'rpc', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x9999', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', }); }, ); @@ -2666,7 +4022,7 @@ describe('NetworkController', () => { ); }); - it('emits networkWillChange before making any changes to the network store', async () => { + it('emits networkWillChange before making any changes to the network status', async () => { const messenger = buildMessenger(); await withController( @@ -2675,22 +4031,21 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0xtest2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', ticker: 'TEST2', - id: 'testNetworkConfigurationId2', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST1', id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0xtest2', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', ticker: 'TEST2', id: 'testNetworkConfigurationId2', }, @@ -2700,22 +4055,16 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); const network2 = network1.with({ networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + customRpcUrl: 'https://mock-rpc-url-2', }, }); network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: '99', - }, - }, + net_version: UNSUCCESSFUL_JSON_RPC_RESPONSE, }); await waitForLookupNetworkToComplete({ controller, @@ -2723,8 +4072,9 @@ describe('NetworkController', () => { await controller.initializeProvider(); }, }); - const initialNetwork = controller.store.getState().network; - expect(initialNetwork).toBe('42'); + const initialNetworkStatus = + controller.store.getState().networkStatus; + expect(initialNetworkStatus).toBe('available'); const networkWillChange = await waitForPublishedEvents({ messenger, @@ -2733,7 +4083,9 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId2'); }, beforeResolving: () => { - expect(controller.store.getState().network).toBe(initialNetwork); + expect(controller.store.getState().networkStatus).toBe( + initialNetworkStatus, + ); }, }); expect(networkWillChange).toBeTruthy(); @@ -2741,7 +4093,7 @@ describe('NetworkController', () => { ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -2750,7 +4102,7 @@ describe('NetworkController', () => { rpcUrl: 'http://mock-rpc-url-2', chainId: '0xtest2', ticker: 'TEST2', - id: 'testNetworkConfigurationId2', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -2771,18 +4123,27 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ net_version: { - response: { - result: '255', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + }); + const network2 = network1.with({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url-1', + }, + }); + network2.mockEssentialRpcCalls({ + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it happens // before networkDidChange count: 1, @@ -2790,32 +4151,32 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId1'); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { provider: { type: 'rpc', - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - ticker: 'TEST2', - id: 'testNetworkConfigurationId2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST1', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - ticker: 'TEST', + chainId: '0x1111', + ticker: 'TEST1', id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', ticker: 'TEST2', id: 'testNetworkConfigurationId2', }, @@ -2850,7 +4211,7 @@ describe('NetworkController', () => { // before networkDidChange count: 1, operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId1'); + controller.setActiveNetwork('testNetworkConfigurationId2'); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ @@ -2923,20 +4284,21 @@ describe('NetworkController', () => { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', + chainId: '0x1337', ticker: 'TEST', }, }, }, }, - async ({ controller }) => { - const network = new NetworkCommunications({ + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = network1.with({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network.mockEssentialRpcCalls(); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider: providerBefore } = @@ -3028,7 +4390,7 @@ describe('NetworkController', () => { ); }); - it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { + it('determines the status of the network, storing it in state', async () => { await withController( { state: { @@ -3051,31 +4413,29 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); - await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('available'); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('determines whether the network supports EIP-1559, storing it in state', async () => { await withController( { state: { networkDetails: { EIPS: {}, + other: 'details', }, networkConfigurations: { testNetworkConfigurationId: { @@ -3101,30 +4461,28 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, propertyPath: ['networkDetails'], + // setActiveNetwork clears networkDetails first, and then updates it + // to what we expect it to be count: 2, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(controller.store.getState().networkDetails.EIPS['1559']).toBe( - true, - ); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); }); describe('setProviderType', () => { - for (const { - nickname, - networkType, - chainId, - networkVersion, - ticker, - } of INFURA_NETWORKS) { + for (const { networkType, chainId, ticker } of INFURA_NETWORKS) { describe(`given a type of "${networkType}"`, () => { - it('captures the current provider configuration before overwriting it', async () => { + it('stores the current provider configuration before overwriting it', async () => { await withController( { state: { @@ -3277,7 +4635,7 @@ describe('NetworkController', () => { }); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -3301,9 +4659,7 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ net_version: { - response: { - result: '255', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); const network2 = network1.with({ @@ -3315,11 +4671,13 @@ describe('NetworkController', () => { network2.mockEssentialRpcCalls(); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -3327,12 +4685,12 @@ describe('NetworkController', () => { controller.setProviderType(networkType); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -3393,7 +4751,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController(async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', @@ -3417,14 +4775,15 @@ describe('NetworkController', () => { }); it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ + await withController(async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = network1.with({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: networkType, }, }); - network.mockEssentialRpcCalls(); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider: providerBefore } = @@ -3461,7 +4820,7 @@ describe('NetworkController', () => { }); }); - it('emits infuraIsUnblocked (assuming that the request for eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { const messenger = buildMessenger(); await withController({ messenger }, async ({ controller }) => { @@ -3471,21 +4830,29 @@ describe('NetworkController', () => { infuraNetwork: networkType, }, }); - network.mockEssentialRpcCalls(); - - const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - controller.setProviderType(networkType); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, }, }); + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); - expect(infuraIsUnblocked).toBeTruthy(); + controller.setProviderType(networkType); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }); }); - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { + it('determines the status of the network, storing it in state', async () => { await withController(async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', @@ -3493,26 +4860,31 @@ describe('NetworkController', () => { infuraNetwork: networkType, }, }); - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.setProviderType(networkType); }, }); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe('available'); }); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('determines whether the network supports EIP-1559, storing it in state', async () => { await withController( { state: { networkDetails: { EIPS: {}, + other: 'details', }, }, }, @@ -3530,15 +4902,19 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, propertyPath: ['networkDetails'], + // setProviderType clears networkDetails first, and then updates + // it to what we expect it to be count: 2, operation: () => { controller.setProviderType(networkType); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -3569,12 +4945,7 @@ describe('NetworkController', () => { }); describe('resetConnection', () => { - for (const { - nickname, - networkType, - chainId, - networkVersion, - } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { it('emits networkWillChange', async () => { const messenger = buildMessenger(); @@ -3607,7 +4978,7 @@ describe('NetworkController', () => { ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -3627,11 +4998,13 @@ describe('NetworkController', () => { }); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -3639,12 +5012,12 @@ describe('NetworkController', () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -3690,7 +5063,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a new provider object pointed to the current Infura network (name: ${nickname}, chain ID: ${chainId})`, async () => { + it(`initializes a new provider object pointed to the current Infura network (type: "${networkType}", chain ID: ${chainId})`, async () => { await withController( { state: { @@ -3732,7 +5105,11 @@ describe('NetworkController', () => { }, }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + eth_blockNumber: { + times: 2, + }, + }); await controller.initializeProvider(); const { provider: providerBefore } = @@ -3777,7 +5154,7 @@ describe('NetworkController', () => { ); }); - it('emits infuraIsUnblocked (assuming that the request for eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { const messenger = buildMessenger(); await withController( @@ -3793,22 +5170,31 @@ describe('NetworkController', () => { }, }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); - - const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: () => { - controller.resetConnection(); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, }, }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); - expect(infuraIsUnblocked).toBeTruthy(); + controller.resetConnection(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, ); }); - it(`ensures that the network version in state is set to "${networkVersion}"`, async () => { + it('checks the status of the network again, updating state appropriately', async () => { await withController( { state: { @@ -3825,18 +5211,20 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); }, ); }); - it('does not ensure that EIP-1559 support for the current network is up to date', async () => { + it('checks whether the network supports EIP-1559 again, updating state appropriately', async () => { await withController( { state: { @@ -3853,15 +5241,25 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); - controller.resetConnection(); + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.resetConnection(); + }, + }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -3909,7 +5307,7 @@ describe('NetworkController', () => { ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -3932,22 +5330,21 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ - eth_blockNumber: { + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, times: 2, }, - net_version: { - response: { - result: '255', - }, + eth_blockNumber: { + times: 2, }, }); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it happens // before networkDidChange count: 1, @@ -3955,12 +5352,12 @@ describe('NetworkController', () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -4075,7 +5472,11 @@ describe('NetworkController', () => { }, }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + eth_blockNumber: { + times: 2, + }, + }); await controller.initializeProvider(); const { provider: providerBefore } = @@ -4174,7 +5575,7 @@ describe('NetworkController', () => { ); }); - it('ensures that the network version in state is up to date', async () => { + it('checks the status of the network again, updating state appropriately', async () => { await withController( { state: { @@ -4198,26 +5599,25 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); + expect(controller.store.getState().networkStatus).toBe('unknown'); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('available'); }, ); }); - it('does not ensure that EIP-1559 support for the current network is up to date', async () => { + it('ensures that EIP-1559 support for the current network is up to date', async () => { await withController( { state: { @@ -4243,15 +5643,25 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); - controller.resetConnection(); + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.resetConnection(); + }, + }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -4259,12 +5669,7 @@ describe('NetworkController', () => { }); describe('rollbackToPreviousProvider', () => { - for (const { - nickname, - networkType, - chainId, - networkVersion, - } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`if the previous provider configuration had a type of "${networkType}"`, () => { it('overwrites the the current provider configuration with the previous provider configuration', async () => { await withController( @@ -4272,68 +5677,65 @@ describe('NetworkController', () => { state: { provider: { type: networkType, - rpcUrl: '', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - nickname: '', - ticker: BUILT_IN_NETWORKS[networkType].ticker, + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + nickname: 'network 1', + ticker: 'TEST1', rpcPrefs: { - blockExplorerUrl: - BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + blockExplorerUrl: 'https://test-block-explorer-1.com', }, + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - nickname: 'test-chain', - ticker: 'TEST', + chainId: '0x111', + nickname: 'network 1', + ticker: 'TEST1', rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', + blockExplorerUrl: 'https://test-block-explorer-1.com', }, id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', + nickname: 'network 2', ticker: 'TEST2', rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', + blockExplorerUrl: 'https://test-block-explorer-2.com', }, id: 'testNetworkConfigurationId2', }, }, - networkDetails: { - EIPS: {}, - }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + customRpcUrl: 'https://mock-rpc-url-2', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId1'); + controller.setActiveNetwork('testNetworkConfigurationId2'); }, }); expect(controller.store.getState().provider).toStrictEqual({ - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', - }, - id: 'testNetworkConfigurationId1', type: 'rpc', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', + nickname: 'network 2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', }); await waitForLookupNetworkToComplete({ @@ -4344,14 +5746,14 @@ describe('NetworkController', () => { }); expect(controller.store.getState().provider).toStrictEqual({ type: networkType, - rpcUrl: '', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - nickname: '', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + nickname: 'network 1', + ticker: 'TEST1', rpcPrefs: { - blockExplorerUrl: - BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + blockExplorerUrl: 'https://test-block-explorer-1.com', }, + id: 'testNetworkConfigurationId1', }); }, ); @@ -4385,15 +5787,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -4419,7 +5821,7 @@ describe('NetworkController', () => { ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -4444,21 +5846,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: '255', - }, - }, - }); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4466,14 +5862,16 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); await waitForLookupNetworkToComplete({ controller, operation: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -4481,14 +5879,16 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); }, }); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -4513,17 +5913,17 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4563,7 +5963,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController( { state: { @@ -4588,15 +5988,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -4648,15 +6048,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -4708,15 +6108,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4742,7 +6142,7 @@ describe('NetworkController', () => { ); }); - it('emits infuraIsUnblocked (assuming that the request for eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { const messenger = buildMessenger(); await withController( @@ -4755,11 +6155,6 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', @@ -4770,103 +6165,117 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); - + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); await waitForLookupNetworkToComplete({ controller, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); await waitForLookupNetworkToComplete({ controller, operation: async () => { - const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: () => { - controller.rollbackToPreviousProvider(); - }, - }); - - expect(infuraIsUnblocked).toBeTruthy(); + controller.rollbackToPreviousProvider(); }, }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, ); }); - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { + it('checks the status of the previous network again and updates state accordingly', async () => { + const previousProvider = { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }; + const currentNetworkConfiguration = { + id: 'currentNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'TEST', + }; await withController( { state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, + provider: previousProvider, networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - }, + currentNetworkConfiguration, }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ net_version: { response: { - result: '255', + error: 'some error', + httpStatus: 405, }, }, }); - - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); + previousNetwork.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, }, }); - expect(controller.store.getState().network).toBe('255'); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork('currentNetworkConfiguration'); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unavailable', + ); await waitForLookupNetworkToComplete({ controller, - numberOfNetworkDetailsChanges: 2, operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { await withController( { state: { @@ -4886,24 +6295,19 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + currentNetwork.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + previousNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); - const network2 = network1.with({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, - }); - network2.mockEssentialRpcCalls({ - latestBlock: PRE_1559_BLOCK, - net_version: { - response: { - result: '99999', - }, - }, - }); await waitForLookupNetworkToComplete({ controller, @@ -4911,9 +6315,11 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); await waitForLookupNetworkToComplete({ controller, @@ -4922,9 +6328,11 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -4976,15 +6384,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5054,15 +6462,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -5088,7 +6496,7 @@ describe('NetworkController', () => { ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network state to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -5109,15 +6517,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5125,14 +6533,14 @@ describe('NetworkController', () => { controller.setProviderType('goerli'); }, }); - expect(controller.store.getState().network).toBe('5'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForLookupNetworkToComplete({ controller, operation: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -5140,14 +6548,16 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); }, }); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -5168,17 +6578,17 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5239,15 +6649,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -5295,15 +6705,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -5351,15 +6761,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5409,15 +6819,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5444,7 +6854,7 @@ describe('NetworkController', () => { ); }); - it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { + it('checks the status of the previous network again and updates state accordingly', async () => { await withController( { state: { @@ -5465,21 +6875,23 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ - net_version: { - response: { - result: '42', - }, - }, - }); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); + previousNetwork.mockEssentialRpcCalls({ + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); await waitForLookupNetworkToComplete({ controller, @@ -5487,7 +6899,7 @@ describe('NetworkController', () => { controller.setProviderType('goerli'); }, }); - expect(controller.store.getState().network).toBe('5'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForLookupNetworkToComplete({ controller, @@ -5495,12 +6907,12 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { await withController( { state: { @@ -5521,24 +6933,19 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - net_version: { - response: { - result: '99999', - }, - }, - }); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, }); + previousNetwork.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + }); await waitForLookupNetworkToComplete({ controller, @@ -5546,9 +6953,11 @@ describe('NetworkController', () => { controller.setProviderType('goerli'); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); await waitForLookupNetworkToComplete({ controller, @@ -5556,14 +6965,17 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); }); }); + describe('upsertNetworkConfiguration', () => { it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { const invalidChainId = '1'; @@ -5996,6 +7408,13 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://test-rpc-url', + }, + }); + network.mockEssentialRpcCalls(); const rpcUrlNetwork = { chainId: '0x1', rpcUrl: 'https://test-rpc-url', @@ -6536,7 +7955,9 @@ async function waitForPublishedEvents({ resetTimer(); }); - await operation(); + if (operation) { + await operation(); + } return await promiseForEventPayloads; } diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index f9737ef3a..ab6105ecf 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -17,7 +17,7 @@ import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, } from '../../../shared/constants/swaps'; import { GasEstimateTypes } from '../../../shared/constants/gas'; -import { CHAIN_IDS } from '../../../shared/constants/network'; +import { CHAIN_IDS, NetworkStatus } from '../../../shared/constants/network'; import { FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, FALLBACK_SMART_TRANSACTIONS_DEADLINE, @@ -136,10 +136,14 @@ export default class SwapsController { this.indexOfNewestCallInFlight = 0; this.ethersProvider = new Web3Provider(provider); - this._currentNetwork = networkController.store.getState().network; - onNetworkDidChange((network) => { - if (network !== 'loading' && network !== this._currentNetwork) { - this._currentNetwork = network; + this._currentNetworkId = networkController.store.getState().networkId; + onNetworkDidChange(() => { + const { networkId, networkStatus } = networkController.store.getState(); + if ( + networkStatus === NetworkStatus.Available && + networkId !== this._currentNetworkId + ) { + this._currentNetworkId = networkId; this.ethersProvider = new Web3Provider(provider); } }); diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index cebf695ae..648f57ccc 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -4,7 +4,11 @@ import sinon from 'sinon'; import { BigNumber } from '@ethersproject/bignumber'; import { mapValues } from 'lodash'; import BigNumberjs from 'bignumber.js'; -import { CHAIN_IDS, NETWORK_IDS } from '../../../shared/constants/network'; +import { + CHAIN_IDS, + NETWORK_IDS, + NetworkStatus, +} from '../../../shared/constants/network'; import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; import { createTestProviderTools } from '../../../test/stub/provider'; import { SECOND } from '../../../shared/constants/time'; @@ -97,11 +101,10 @@ const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({ function getMockNetworkController() { return { store: { - getState: () => { - return { - network: NETWORK_IDS.GOERLI, - }; - }, + getState: sinon.stub().returns({ + networkId: NETWORK_IDS.GOERLI, + networkStatus: NetworkStatus.Available, + }), }, }; } @@ -219,6 +222,10 @@ describe('SwapsController', function () { const currentEthersInstance = swapsController.ethersProvider; const changeNetwork = onNetworkDidChange.getCall(0).args[0]; + networkController.store.getState.returns({ + networkId: NETWORK_IDS.MAINNET, + networkStatus: NetworkStatus.Available, + }); changeNetwork(NETWORK_IDS.MAINNET); const newEthersInstance = swapsController.ethersProvider; @@ -245,6 +252,10 @@ describe('SwapsController', function () { const currentEthersInstance = swapsController.ethersProvider; const changeNetwork = onNetworkDidChange.getCall(0).args[0]; + networkController.store.getState.returns({ + networkId: null, + networkStatus: NetworkStatus.Unknown, + }); changeNetwork('loading'); const newEthersInstance = swapsController.ethersProvider; @@ -271,6 +282,10 @@ describe('SwapsController', function () { const currentEthersInstance = swapsController.ethersProvider; const changeNetwork = onNetworkDidChange.getCall(0).args[0]; + networkController.store.getState.returns({ + networkId: NETWORK_IDS.GOERLI, + networkStatus: NetworkStatus.Available, + }); changeNetwork(NETWORK_IDS.GOERLI); const newEthersInstance = swapsController.ethersProvider; diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 112071ffd..a98ee441d 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -44,6 +44,7 @@ import { HARDFORKS, CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, NETWORK_TYPES, + NetworkStatus, } from '../../../../shared/constants/network'; import { determineTransactionAssetType, @@ -115,7 +116,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain'; * * @param {object} opts * @param {object} opts.initState - initial transaction list default is an empty array - * @param {Function} opts.getNetworkState - Get the current network state. + * @param {Function} opts.getNetworkId - Get the current network ID. + * @param {Function} opts.getNetworkStatus - Get the current network status. * @param {Function} opts.onNetworkStateChange - Subscribe to network state change events. * @param {object} opts.blockTracker - An instance of eth-blocktracker * @param {object} opts.provider - A network provider. @@ -129,7 +131,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain'; export default class TransactionController extends EventEmitter { constructor(opts) { super(); - this.getNetworkState = opts.getNetworkState; + this.getNetworkId = opts.getNetworkId; + this.getNetworkStatus = opts.getNetworkStatus; this._getCurrentChainId = opts.getCurrentChainId; this.getProviderConfig = opts.getProviderConfig; this._getCurrentNetworkEIP1559Compatibility = @@ -167,7 +170,8 @@ export default class TransactionController extends EventEmitter { this.txStateManager = new TransactionStateManager({ initState: opts.initState, txHistoryLimit: opts.txHistoryLimit, - getNetworkState: this.getNetworkState, + getNetworkId: this.getNetworkId, + getNetworkStatus: this.getNetworkStatus, getCurrentChainId: opts.getCurrentChainId, }); @@ -226,10 +230,13 @@ export default class TransactionController extends EventEmitter { * @returns {number} The numerical chainId. */ getChainId() { - const networkState = this.getNetworkState(); + const networkStatus = this.getNetworkStatus(); const chainId = this._getCurrentChainId(); const integerChainId = parseInt(chainId, 16); - if (networkState === 'loading' || Number.isNaN(integerChainId)) { + if ( + networkStatus !== NetworkStatus.Available || + Number.isNaN(integerChainId) + ) { return 0; } return integerChainId; @@ -272,12 +279,13 @@ export default class TransactionController extends EventEmitter { }); } - // For 'rpc' we need to use the same basic configuration as mainnet, - // since we only support EVM compatible chains, and then override the + // For 'rpc' we need to use the same basic configuration as mainnet, since + // we only support EVM compatible chains, and then override the // name, chainId and networkId properties. This is done using the // `forCustomChain` static method on the Common class. const chainId = parseInt(this._getCurrentChainId(), 16); - const networkId = this.getNetworkState(); + const networkStatus = this.getNetworkStatus(); + const networkId = this.getNetworkId(); const customChainParams = { name, @@ -291,7 +299,8 @@ export default class TransactionController extends EventEmitter { // on a custom network that requires valid network id. I have not ran // into this limitation on any network I have attempted, even when // hardcoding networkId to 'loading'. - networkId: networkId === 'loading' ? 0 : parseInt(networkId, 10), + networkId: + networkStatus === NetworkStatus.Available ? parseInt(networkId, 10) : 0, }; return Common.forCustomChain( diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 096783fae..99b2cd4b1 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -27,12 +27,14 @@ import { } from '../../../../shared/constants/gas'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { NetworkStatus } from '../../../../shared/constants/network'; import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils'; import TransactionController from '.'; const noop = () => true; const currentNetworkId = '5'; const currentChainId = '0x5'; +const currentNetworkStatus = NetworkStatus.Available; const providerConfig = { type: 'goerli', }; @@ -46,7 +48,8 @@ describe('Transaction Controller', function () { providerResultStub, fromAccount, fragmentExists, - networkStore; + networkStatusStore, + getCurrentChainId; beforeEach(function () { fragmentExists = false; @@ -59,22 +62,27 @@ describe('Transaction Controller', function () { provider = createTestProviderTools({ scaffold: providerResultStub, networkId: currentNetworkId, - chainId: currentNetworkId, + chainId: parseInt(currentChainId, 16), }).provider; - networkStore = new ObservableStore(currentNetworkId); + networkStatusStore = new ObservableStore(currentNetworkStatus); fromAccount = getTestAccounts()[0]; const blockTrackerStub = new EventEmitter(); blockTrackerStub.getCurrentBlock = noop; blockTrackerStub.getLatestBlock = noop; + + getCurrentChainId = sinon.stub().callsFake(() => currentChainId); + txController = new TransactionController({ provider, getGasPrice() { return '0xee6b2800'; }, - getNetworkState: () => networkStore.getState(), - onNetworkStateChange: (listener) => networkStore.subscribe(listener), + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => networkStatusStore.getState(), + onNetworkStateChange: (listener) => + networkStatusStore.subscribe(listener), getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(false), getCurrentAccountEIP1559Compatibility: () => false, txHistoryLimit: 10, @@ -85,7 +93,7 @@ describe('Transaction Controller', function () { }), getProviderConfig: () => providerConfig, getPermittedAccounts: () => undefined, - getCurrentChainId: () => currentChainId, + getCurrentChainId, getParticipateInMetrics: () => false, trackMetaMetricsEvent: () => undefined, createEventFragment: () => undefined, @@ -467,8 +475,8 @@ describe('Transaction Controller', function () { ); }); - it('should fail if netId is loading', async function () { - networkStore.putState('loading'); + it('should fail if the network status is not "available"', async function () { + networkStatusStore.putState(NetworkStatus.Unknown); await assert.rejects( () => txController.addUnapprovedTransaction(undefined, { @@ -1079,8 +1087,19 @@ describe('Transaction Controller', function () { }); describe('#getChainId', function () { - it('returns 0 when the chainId is NaN', function () { - networkStore.putState('loading'); + it('returns the chain ID of the network when it is available', function () { + networkStatusStore.putState(NetworkStatus.Available); + assert.equal(txController.getChainId(), 5); + }); + + it('returns 0 when the network is not available', function () { + networkStatusStore.putState('asdflsfadf'); + assert.equal(txController.getChainId(), 0); + }); + + it('returns 0 when the chain ID cannot be parsed as a hex string', function () { + networkStatusStore.putState(NetworkStatus.Available); + getCurrentChainId.returns('$fdsjfldf'); assert.equal(txController.getChainId(), 0); }); }); diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index bd8e38051..98d18bca1 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -7,6 +7,7 @@ import { TransactionStatus } from '../../../../shared/constants/transaction'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { NetworkStatus } from '../../../../shared/constants/network'; import { generateHistoryEntry, replayHistory, @@ -54,13 +55,15 @@ export const ERROR_SUBMITTING = * transactions list keyed by id * @param {number} [opts.txHistoryLimit] - limit for how many finished * transactions can hang around in state - * @param {Function} opts.getNetworkState - Get the current network state. + * @param {Function} opts.getNetworkId - Get the current network Id. + * @param {Function} opts.getNetworkStatus - Get the current network status. */ export default class TransactionStateManager extends EventEmitter { constructor({ initState, txHistoryLimit, - getNetworkState, + getNetworkId, + getNetworkStatus, getCurrentChainId, }) { super(); @@ -70,7 +73,8 @@ export default class TransactionStateManager extends EventEmitter { ...initState, }); this.txHistoryLimit = txHistoryLimit; - this.getNetworkState = getNetworkState; + this.getNetworkId = getNetworkId; + this.getNetworkStatus = getNetworkStatus; this.getCurrentChainId = getCurrentChainId; } @@ -86,9 +90,10 @@ export default class TransactionStateManager extends EventEmitter { * @returns {TransactionMeta} the default txMeta object */ generateTxMeta(opts = {}) { - const netId = this.getNetworkState(); + const networkId = this.getNetworkId(); + const networkStatus = this.getNetworkStatus(); const chainId = this.getCurrentChainId(); - if (netId === 'loading') { + if (networkStatus !== NetworkStatus.Available) { throw new Error('MetaMask is having trouble connecting to the network'); } @@ -128,7 +133,7 @@ export default class TransactionStateManager extends EventEmitter { id: createId(), time: new Date().getTime(), status: TransactionStatus.unapproved, - metamaskNetworkId: netId, + metamaskNetworkId: networkId, originalGasEstimate: opts.txParams?.gas, userEditedGasLimit: false, chainId, @@ -149,12 +154,12 @@ export default class TransactionStateManager extends EventEmitter { */ getUnapprovedTxList() { const chainId = this.getCurrentChainId(); - const network = this.getNetworkState(); + const networkId = this.getNetworkId(); return pickBy( this.store.getState().transactions, (transaction) => transaction.status === TransactionStatus.unapproved && - transactionMatchesNetwork(transaction, chainId, network), + transactionMatchesNetwork(transaction, chainId, networkId), ); } @@ -413,7 +418,7 @@ export default class TransactionStateManager extends EventEmitter { limit, } = {}) { const chainId = this.getCurrentChainId(); - const network = this.getNetworkState(); + const networkId = this.getNetworkId(); // searchCriteria is an object that might have values that aren't predicate // methods. When providing any other value type (string, number, etc), we // consider this shorthand for "check the value at key for strict equality @@ -442,7 +447,7 @@ export default class TransactionStateManager extends EventEmitter { // when filterToCurrentNetwork is true. if ( filterToCurrentNetwork && - transactionMatchesNetwork(transaction, chainId, network) === false + transactionMatchesNetwork(transaction, chainId, networkId) === false ) { return false; } @@ -596,8 +601,7 @@ export default class TransactionStateManager extends EventEmitter { } /** - * Removes all transactions for the given address on the current network, - * preferring chainId for comparison over networkId. + * Removes all transactions for the given address on the current network. * * @param {string} address - hex string of the from address on the txParams * to remove @@ -605,8 +609,8 @@ export default class TransactionStateManager extends EventEmitter { wipeTransactions(address) { // network only tx const { transactions } = this.store.getState(); - const network = this.getNetworkState(); const chainId = this.getCurrentChainId(); + const networkId = this.getNetworkId(); // Update state this.store.updateState({ @@ -614,7 +618,7 @@ export default class TransactionStateManager extends EventEmitter { transactions, (transaction) => transaction.txParams.from === address && - transactionMatchesNetwork(transaction, chainId, network), + transactionMatchesNetwork(transaction, chainId, networkId), ), }); } diff --git a/app/scripts/controllers/transactions/tx-state-manager.test.js b/app/scripts/controllers/transactions/tx-state-manager.test.js index 677f59334..a3b420e10 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.test.js +++ b/app/scripts/controllers/transactions/tx-state-manager.test.js @@ -4,7 +4,11 @@ import { TransactionStatus, TransactionType, } from '../../../../shared/constants/transaction'; -import { CHAIN_IDS, NETWORK_IDS } from '../../../../shared/constants/network'; +import { + CHAIN_IDS, + NETWORK_IDS, + NetworkStatus, +} from '../../../../shared/constants/network'; import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import TxStateManager, { ERROR_SUBMITTING } from './tx-state-manager'; @@ -45,6 +49,7 @@ function generateTransactions( describe('TransactionStateManager', function () { let txStateManager; const currentNetworkId = NETWORK_IDS.GOERLI; + const currentNetworkStatus = NetworkStatus.Available; const currentChainId = CHAIN_IDS.MAINNET; const otherNetworkId = '2'; @@ -54,7 +59,8 @@ describe('TransactionStateManager', function () { transactions: {}, }, txHistoryLimit: 10, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); }); @@ -181,7 +187,8 @@ describe('TransactionStateManager', function () { [confirmedTx.id]: confirmedTx, }, }, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); @@ -246,7 +253,8 @@ describe('TransactionStateManager', function () { [confirmedTx3.id]: confirmedTx3, }, }, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); @@ -355,7 +363,8 @@ describe('TransactionStateManager', function () { [failedTx3Dupe.id]: failedTx3Dupe, }, }, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 633ac770d..bc0150b84 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -52,7 +52,8 @@ export const SENTRY_STATE = { isUnlocked: true, metaMetricsId: true, nativeCurrency: true, - network: true, + networkId: true, + networkStatus: true, nextNonce: true, participateInMetaMetrics: true, preferences: true, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0f201d55e..d4fff36bb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -74,7 +74,11 @@ import { GAS_DEV_API_BASE_URL, SWAPS_CLIENT_ID, } from '../../shared/constants/swaps'; -import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; +import { + CHAIN_IDS, + NETWORK_TYPES, + NetworkStatus, +} from '../../shared/constants/network'; import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; import { KeyringType } from '../../shared/constants/keyring'; import { @@ -334,7 +338,7 @@ export default class MetamaskController extends EventEmitter { { onPreferencesStateChange: (listener) => this.preferencesController.store.subscribe(listener), - onNetworkStateChange: (cb) => + onNetworkStateChange: (cb) => { this.networkController.store.subscribe((networkState) => { const modifiedNetworkState = { ...networkState, @@ -344,7 +348,8 @@ export default class MetamaskController extends EventEmitter { }, }; return cb(modifiedNetworkState); - }), + }); + }, }, { provider: this.provider, @@ -478,6 +483,8 @@ export default class MetamaskController extends EventEmitter { clientId: SWAPS_CLIENT_ID, getProvider: () => this.networkController.getProviderAndBlockTracker().provider, + // NOTE: This option is inaccurately named; it should be called + // onNetworkDidChange onNetworkStateChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, NetworkControllerEventTypes.NetworkDidChange, @@ -944,9 +951,11 @@ export default class MetamaskController extends EventEmitter { ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(this), - getNetworkState: () => this.networkController.store.getState().network, + getNetworkId: () => this.networkController.store.getState().networkId, + getNetworkStatus: () => + this.networkController.store.getState().networkStatus, onNetworkStateChange: (listener) => - this.networkController.networkStore.subscribe(listener), + this.networkController.networkIdStore.subscribe(listener), getCurrentChainId: () => this.networkController.store.getState().provider.chainId, preferencesStore: this.preferencesController.store, @@ -1144,7 +1153,8 @@ export default class MetamaskController extends EventEmitter { return cb(modifiedNetworkState); }); }, - getNetwork: () => this.networkController.store.getState().network, + getNetwork: () => + this.networkController.store.getState().networkId ?? 'loading', getNonceLock: this.txController.nonceTracker.getNonceLock.bind( this.txController.nonceTracker, ), @@ -1666,16 +1676,16 @@ export default class MetamaskController extends EventEmitter { function updatePublicConfigStore(memState) { const { chainId } = networkController.store.getState().provider; - if (memState.network !== 'loading') { + if (memState.networkStatus === NetworkStatus.Available) { publicConfigStore.putState(selectPublicState(chainId, memState)); } } - function selectPublicState(chainId, { isUnlocked, network }) { + function selectPublicState(chainId, { isUnlocked, networkId }) { return { isUnlocked, chainId, - networkVersion: network, + networkVersion: networkId ?? 'loading', }; } @@ -1686,12 +1696,7 @@ export default class MetamaskController extends EventEmitter { * Gets relevant state for the provider of an external origin. * * @param {string} origin - The origin to get the provider state for. - * @returns {Promise<{ - * isUnlocked: boolean, - * networkVersion: string, - * chainId: string, - * accounts: string[], - * }>} An object with relevant state properties. + * @returns {Promise<{ isUnlocked: boolean, networkVersion: string, chainId: string, accounts: string[] }>} An object with relevant state properties. */ async getProviderState(origin) { return { @@ -1709,10 +1714,10 @@ export default class MetamaskController extends EventEmitter { * @returns {object} An object with relevant network state properties. */ getProviderNetworkState(memState) { - const { network } = memState || this.getState(); + const { networkId } = memState || this.getState(); return { chainId: this.networkController.store.getState().provider.chainId, - networkVersion: network, + networkVersion: networkId ?? 'loading', }; } diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 7506c17cb..826e0236c 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -779,13 +779,13 @@ describe('MetaMaskController', function () { metamaskController.preferencesController, 'getSelectedAddress', ); - const getNetworkstub = sinon.stub( + const getNetworkIdStub = sinon.stub( metamaskController.txController.txStateManager, - 'getNetworkState', + 'getNetworkId', ); selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'); - getNetworkstub.returns(42); + getNetworkIdStub.returns(42); metamaskController.txController.txStateManager._addTransactionsToState([ createTxMeta({ diff --git a/app/scripts/migrations/083.test.js b/app/scripts/migrations/083.test.js new file mode 100644 index 000000000..6e0f58998 --- /dev/null +++ b/app/scripts/migrations/083.test.js @@ -0,0 +1,103 @@ +import { migrate } from './083'; + +describe('migration #83', () => { + it('updates the version metadata', async () => { + const originalVersionedData = buildOriginalVersionedData({ + meta: { + version: 9999999, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.meta).toStrictEqual({ + version: 83, + }); + }); + + it('does not change the state if the network controller state does not exist', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + test: '123', + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); + }); + + const nonObjects = [undefined, null, 'test', 1, ['test']]; + for (const invalidState of nonObjects) { + it(`does not change the state if the network controller state is ${invalidState}`, async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: invalidState, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); + }); + } + + it('does not change the state if the network controller state does not include "network"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + test: '123', + }, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); + }); + + it('replaces "network" in the network controller state with "networkId": null, "networkStatus": "unknown" if it is "loading"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + network: 'loading', + }, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual({ + NetworkController: { + networkId: null, + networkStatus: 'unknown', + }, + }); + }); + + it('replaces "network" in the network controller state with "networkId": network, "networkStatus": "available" if it is not "loading"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + network: '12345', + }, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual({ + NetworkController: { + networkId: '12345', + networkStatus: 'available', + }, + }); + }); +}); + +function buildOriginalVersionedData({ meta = {}, data = {} } = {}) { + return { + meta: { version: 999999, ...meta }, + data: { ...data }, + }; +} diff --git a/app/scripts/migrations/083.ts b/app/scripts/migrations/083.ts new file mode 100644 index 000000000..e55c1960c --- /dev/null +++ b/app/scripts/migrations/083.ts @@ -0,0 +1,47 @@ +import { cloneDeep } from 'lodash'; +import { hasProperty, isObject } from '@metamask/utils'; + +export const version = 83; + +/** + * The `network` property in state was replaced with `networkId` and `networkStatus`. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if ( + !hasProperty(state, 'NetworkController') || + !isObject(state.NetworkController) || + !hasProperty(state.NetworkController, 'network') + ) { + return state; + } + + const NetworkController = { ...state.NetworkController }; + + if (NetworkController.network === 'loading') { + NetworkController.networkId = null; + NetworkController.networkStatus = 'unknown'; + } else { + NetworkController.networkId = NetworkController.network; + NetworkController.networkStatus = 'available'; + } + + delete NetworkController.network; + + return { ...state, NetworkController }; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index fdd29924c..c3f8e515f 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -86,6 +86,7 @@ import m079 from './079'; import m080 from './080'; import * as m081 from './081'; import * as m082 from './082'; +import * as m083 from './083'; const migrations = [ m002, @@ -169,6 +170,7 @@ const migrations = [ m080, m081, m082, + m083, ]; export default migrations; diff --git a/shared/constants/network.ts b/shared/constants/network.ts index e2d6cf40e..1f20538c3 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -689,3 +689,30 @@ export const FEATURED_RPCS: RPCDefinition[] = [ export const SHOULD_SHOW_LINEA_TESTNET_NETWORK = new Date().getTime() > Date.UTC(2023, 2, 28, 8); + +/** + * Represents the availability state of the currently selected network. + */ +export enum NetworkStatus { + /** + * The network may or may not be able to receive requests, but either no + * attempt has been made to determine this, or an attempt was made but was + * unsuccessful. + */ + Unknown = 'unknown', + /** + * The network is able to receive and respond to requests. + */ + Available = 'available', + /** + * The network is unable to receive and respond to requests for unknown + * reasons. + */ + Unavailable = 'unavailable', + /** + * The network is not only unavailable, but is also inaccessible for the user + * specifically based on their location. This state only applies to Infura + * networks. + */ + Blocked = 'blocked', +} diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 8f08aee96..93d4515f4 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -72,7 +72,8 @@ "featureFlags": { "showIncomingTransactions": true }, - "network": "5", + "networkId": "5", + "networkStatus": "available", "provider": { "type": "rpc", "chainId": "0x5", diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 6fffc84a9..81b4d6385 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -180,7 +180,8 @@ function defaultFixture() { traits: {}, }, NetworkController: { - network: '1337', + networkId: '1337', + networkStatus: 'available', provider: { chainId: CHAIN_IDS.LOCALHOST, nickname: 'Localhost 8545', @@ -310,7 +311,8 @@ function onboardingFixture() { }, }, NetworkController: { - network: '1337', + networkId: '1337', + networkStatus: 'available', provider: { ticker: 'ETH', type: 'rpc', @@ -487,15 +489,6 @@ class FixtureBuilder { return this; } - withNetworkControllerSupportEIP1559() { - merge(this.fixture.data.NetworkController, { - networkDetails: { - EIPS: { 1559: true }, - }, - }); - return this; - } - withNftController(data) { merge( this.fixture.data.NftController diff --git a/test/e2e/tests/ens.spec.js b/test/e2e/tests/ens.spec.js index f5e3536c0..3a0dffee1 100644 --- a/test/e2e/tests/ens.spec.js +++ b/test/e2e/tests/ens.spec.js @@ -25,6 +25,34 @@ describe('ENS', function () { }; }); + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBalance' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x1', + }, + }; + }); + + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: {}, + }, + }; + }); + await mockServer .forPost(infuraUrl) .withJsonBodyIncluding({ method: 'eth_call' }) diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index dbe952963..b5542c1cb 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -298,9 +298,11 @@ describe('Send ETH from dapp using advanced gas controls', function () { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() - .withNetworkControllerSupportEIP1559() .build(), - ganacheOptions, + ganacheOptions: { + ...ganacheOptions, + hardfork: 'london', + }, title: this.test.title, }, async ({ driver }) => { diff --git a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js index 776b2fe4a..e616f04a4 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js @@ -11,10 +11,8 @@ jest.mock('../../../../../app/scripts/lib/util', () => ({ describe('Confirm Detail Row Component', () => { const mockState = { - appState: { - isLoading: false, - }, metamask: { + networkStatus: 'available', provider: { type: 'rpc', chainId: '0x5', diff --git a/ui/components/app/dropdowns/network-dropdown.test.js b/ui/components/app/dropdowns/network-dropdown.test.js index 6e8ee65a6..a4a1f6398 100644 --- a/ui/components/app/dropdowns/network-dropdown.test.js +++ b/ui/components/app/dropdowns/network-dropdown.test.js @@ -21,7 +21,8 @@ describe('Network Dropdown', () => { describe('NetworkDropdown in appState in false', () => { const mockState = { metamask: { - network: '1', + networkId: '1', + networkStatus: 'available', provider: { type: 'test', }, @@ -55,7 +56,8 @@ describe('Network Dropdown', () => { describe('NetworkDropdown in appState is true and show test networks is true', () => { const mockState = { metamask: { - network: '1', + networkId: '1', + networkStatus: 'available', provider: { type: 'test', }, @@ -133,7 +135,8 @@ describe('Network Dropdown', () => { describe('NetworkDropdown in appState is true and show test networks is false', () => { const mockState = { metamask: { - network: '1', + networkId: '1', + networkStatus: 'available', provider: { type: 'test', }, diff --git a/ui/components/app/transaction-alerts/transaction-alerts.stories.js b/ui/components/app/transaction-alerts/transaction-alerts.stories.js index 1edddfd2d..7cb5886fe 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.stories.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.stories.js @@ -18,7 +18,7 @@ const customTransaction = ({ userFeeLevel: estimateUsed ? 'low' : 'medium', blockNumber: `${10902987 + i}`, id: 4678200543090545 + i, - metamaskNetworkId: testData?.metamask?.network, + metamaskNetworkId: testData?.metamask?.networkId, chainId: testData?.metamask?.provider?.chainId, status: 'confirmed', time: 1600654021000, diff --git a/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js b/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js index 4d31dca27..04286b4f8 100644 --- a/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js +++ b/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js @@ -343,7 +343,8 @@ describe('Confirm Transaction Duck', () => { metamask: { conversionRate: 468.58, currentCurrency: 'usd', - network: '5', + networkId: '5', + networkStatus: 'available', provider: { chainId: '0x5', }, @@ -368,7 +369,6 @@ describe('Confirm Transaction Duck', () => { }, confirmTransaction: {}, }; - const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); const store = mockStore(mockState); diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index 5f3a43f49..2ca94f71b 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -41,7 +41,8 @@ describe('MetaMask Reducers', () => { conversionRate: 1200.88200327, nativeCurrency: 'ETH', useCurrencyRateCheck: true, - network: '5', + networkId: '5', + networkStatus: 'available', provider: { type: 'testnet', chainId: '0x5', diff --git a/ui/helpers/utils/tx-helper.ts b/ui/helpers/utils/tx-helper.ts index 9c64a42c4..e29732d24 100644 --- a/ui/helpers/utils/tx-helper.ts +++ b/ui/helpers/utils/tx-helper.ts @@ -9,7 +9,7 @@ export default function txHelper( decryptMsgs: Record | null, encryptionPublicKeyMsgs: Record | null, typedMessages: Record | null, - network?: string, + networkId?: string | null, chainId?: string, ): Record { log.debug('tx-helper called with params:'); @@ -20,13 +20,13 @@ export default function txHelper( decryptMsgs, encryptionPublicKeyMsgs, typedMessages, - network, + networkId, chainId, }); - const txValues = network + const txValues = networkId ? valuesFor(unapprovedTxs).filter((txMeta) => - transactionMatchesNetwork(txMeta, chainId, network), + transactionMatchesNetwork(txMeta, chainId, networkId), ) : valuesFor(unapprovedTxs); log.debug(`tx helper found ${txValues.length} unapproved txs`); diff --git a/ui/index.js b/ui/index.js index 32addfc26..aa4520883 100644 --- a/ui/index.js +++ b/ui/index.js @@ -160,7 +160,7 @@ async function startApp(metamaskState, backgroundConnection, opts) { metamaskState.unapprovedDecryptMsgs, metamaskState.unapprovedEncryptionPublicKeyMsgs, metamaskState.unapprovedTypedMessages, - metamaskState.network, + metamaskState.networkId, metamaskState.provider.chainId, ); const numberOfUnapprovedTx = unapprovedTxsAll.length; diff --git a/ui/pages/confirm-signature-request/index.js b/ui/pages/confirm-signature-request/index.js index 1dd1cba87..51d977c00 100644 --- a/ui/pages/confirm-signature-request/index.js +++ b/ui/pages/confirm-signature-request/index.js @@ -63,7 +63,7 @@ const ConfirmTxScreen = ({ match }) => { unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, - network, + networkId, blockGasLimit, provider: { chainId }, } = useSelector((state) => state.metamask); @@ -76,7 +76,7 @@ const ConfirmTxScreen = ({ match }) => { {}, {}, {}, - network, + networkId, chainId, ); if (unconfTxList.length === 0 && !sendTo && unapprovedMessagesTotal === 0) { @@ -101,7 +101,7 @@ const ConfirmTxScreen = ({ match }) => { {}, {}, {}, - network, + networkId, chainId, ); const prevTxData = prevUnconfTxList[prevIndex] || {}; @@ -114,7 +114,7 @@ const ConfirmTxScreen = ({ match }) => { {}, {}, {}, - network, + networkId, chainId, ); @@ -137,7 +137,7 @@ const ConfirmTxScreen = ({ match }) => { chainId, currentNetworkTxList, match, - network, + networkId, sendTo, unapprovedMessagesTotal, unapprovedTxs, @@ -151,7 +151,7 @@ const ConfirmTxScreen = ({ match }) => { unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, - network, + networkId, chainId, ); diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 9abc1ee22..c3389f2b8 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -101,7 +101,7 @@ const mapStateToProps = (state, ownProps) => { conversionRate, identities, addressBook, - network, + networkId, unapprovedTxs, nextNonce, provider: { chainId }, @@ -160,7 +160,7 @@ const mapStateToProps = (state, ownProps) => { const currentNetworkUnapprovedTxs = Object.keys(unapprovedTxs) .filter((key) => - transactionMatchesNetwork(unapprovedTxs[key], chainId, network), + transactionMatchesNetwork(unapprovedTxs[key], chainId, networkId), ) .reduce((acc, key) => ({ ...acc, [key]: unapprovedTxs[key] }), {}); const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length; diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js index 326af8c73..fbe72d8b4 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js @@ -33,6 +33,7 @@ const baseStore = { unapprovedTxs: { 1: { id: 1, + metamaskNetworkId: '5', txParams: { from: '0x0', to: '0x85c1685cfceaa5c0bdb1609fc536e9a8387dd65e', @@ -58,6 +59,7 @@ const baseStore = { accounts: ['0x0'], }, ], + networkId: '5', networkDetails: { EIPS: {}, }, diff --git a/ui/selectors/confirm-transaction.test.js b/ui/selectors/confirm-transaction.test.js index b44197020..86a551aae 100644 --- a/ui/selectors/confirm-transaction.test.js +++ b/ui/selectors/confirm-transaction.test.js @@ -31,7 +31,7 @@ describe('Confirm Transaction Selector', () => { unapprovedMsgCount: 1, unapprovedPersonalMsgCount: 1, unapprovedTypedMessagesCount: 1, - network: '5', + networkId: '5', provider: { chainId: '0x5', }, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 21db5560f..be2ced685 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -24,6 +24,7 @@ import { CHAIN_ID_TO_RPC_URL_MAP, CHAIN_IDS, NETWORK_TYPES, + NetworkStatus, } from '../../shared/constants/network'; import { WebHIDConnectedStatuses, @@ -77,16 +78,13 @@ import { getPermissionSubjects } from './permissions'; ///: END:ONLY_INCLUDE_IN /** - * One of the only remaining valid uses of selecting the network subkey of the - * metamask state tree is to determine if the network is currently 'loading'. + * Returns true if the currently selected network is inaccessible or whether no + * provider has been set yet for the currently selected network. * - * This will be used for all cases where this state key is accessed only for that - * purpose. - * - * @param {object} state - redux state object + * @param {object} state - Redux state object. */ export function isNetworkLoading(state) { - return state.metamask.network === 'loading'; + return state.metamask.networkStatus !== NetworkStatus.Available; } export function getNetworkIdentifier(state) { @@ -248,7 +246,7 @@ export function getAccountType(state) { * @param {object} state - redux state object */ export function deprecatedGetCurrentNetworkId(state) { - return state.metamask.network; + return state.metamask.networkId ?? 'loading'; } export const getMetaMaskAccounts = createSelector( diff --git a/ui/selectors/transactions.js b/ui/selectors/transactions.js index 684541327..5e95e8d6f 100644 --- a/ui/selectors/transactions.js +++ b/ui/selectors/transactions.js @@ -29,14 +29,14 @@ export const incomingTxListSelector = (state) => { } const { - network, + networkId, provider: { chainId }, } = state.metamask; const selectedAddress = getSelectedAddress(state); return Object.values(state.metamask.incomingTransactions).filter( (tx) => tx.txParams.to === selectedAddress && - transactionMatchesNetwork(tx, chainId, network), + transactionMatchesNetwork(tx, chainId, networkId), ); }; export const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs; diff --git a/ui/store/store.ts b/ui/store/store.ts index 9d4a62a33..a4e9aa9d6 100644 --- a/ui/store/store.ts +++ b/ui/store/store.ts @@ -6,6 +6,7 @@ import { GasEstimateType, GasFeeEstimates } from '@metamask/gas-fee-controller'; import rootReducer from '../ducks'; import { LedgerTransportTypes } from '../../shared/constants/hardware-wallets'; import { TransactionMeta } from '../../shared/constants/transaction'; +import type { NetworkStatus } from '../../shared/constants/network'; /** * This interface is temporary and is copied from the message-manager.js file @@ -65,7 +66,8 @@ interface TemporaryBackgroundState { unapprovedMsgs: MessagesIndexedById; unapprovedPersonalMsgs: MessagesIndexedById; unapprovedTypedMessages: MessagesIndexedById; - network: string; + networkId: string | null; + networkStatus: NetworkStatus; pendingApprovals: ApprovalControllerState['pendingApprovals']; knownMethodData?: { [fourBytePrefix: string]: Record;