@metamask/network-controller
Version:
Provides an interface to the currently selected network via a MetaMask-compatible provider object
956 lines (955 loc) • 87.6 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _NetworkController_instances, _NetworkController_ethQuery, _NetworkController_infuraProjectId, _NetworkController_previouslySelectedNetworkClientId, _NetworkController_providerProxy, _NetworkController_blockTrackerProxy, _NetworkController_autoManagedNetworkClientRegistry, _NetworkController_autoManagedNetworkClient, _NetworkController_log, _NetworkController_getRpcServiceOptions, _NetworkController_getBlockTrackerOptions, _NetworkController_networkConfigurationsByNetworkClientId, _NetworkController_isRpcFailoverEnabled, _NetworkController_updateRpcFailoverEnabled, _NetworkController_refreshNetwork, _NetworkController_getLatestBlock, _NetworkController_determineEIP1559Compatibility, _NetworkController_validateNetworkFields, _NetworkController_determineNetworkConfigurationToPersist, _NetworkController_registerNetworkClientsAsNeeded, _NetworkController_unregisterNetworkClientsAsNeeded, _NetworkController_updateNetworkConfigurations, _NetworkController_ensureAutoManagedNetworkClientRegistryPopulated, _NetworkController_createAutoManagedNetworkClientRegistry, _NetworkController_applyNetworkSelection;
function $importDefault(module) {
if (module?.__esModule) {
return module.default;
}
return module;
}
import { BaseController } from "@metamask/base-controller";
import { InfuraNetworkType, CustomNetworkType, NetworkType, isSafeChainId, isInfuraNetworkType, ChainId, NetworksTicker, NetworkNickname, BUILT_IN_CUSTOM_NETWORKS_RPC, BUILT_IN_NETWORKS } from "@metamask/controller-utils";
import $EthQuery from "@metamask/eth-query";
const EthQuery = $importDefault($EthQuery);
import { errorCodes } from "@metamask/rpc-errors";
import { createEventEmitterProxy } from "@metamask/swappable-obj-proxy";
import { hasProperty, isPlainObject, isStrictHexString } from "@metamask/utils";
import $deepEqual from "fast-deep-equal";
const deepEqual = $importDefault($deepEqual);
import { produce } from "immer";
import $lodash from "lodash";
const { cloneDeep } = $lodash;
import { createSelector } from "reselect";
import * as URI from "uri-js";
import { v4 as uuidV4 } from "uuid";
import { DEPRECATED_NETWORKS, INFURA_BLOCKED_KEY, NetworkStatus } from "./constants.mjs";
import { createAutoManagedNetworkClient } from "./create-auto-managed-network-client.mjs";
import { projectLogger, createModuleLogger } from "./logger.mjs";
import { NetworkClientType } from "./types.mjs";
const debugLog = createModuleLogger(projectLogger, 'NetworkController');
const INFURA_URL_REGEX = /^https:\/\/(?<networkName>[^.]+)\.infura\.io\/v\d+\/(?<apiKey>.+)$/u;
/**
* The type of an RPC endpoint.
*
* @see {@link CustomRpcEndpoint}
* @see {@link InfuraRpcEndpoint}
*/
export var RpcEndpointType;
(function (RpcEndpointType) {
RpcEndpointType["Custom"] = "custom";
RpcEndpointType["Infura"] = "infura";
})(RpcEndpointType || (RpcEndpointType = {}));
/**
* `Object.keys()` is intentionally generic: it returns the keys of an object,
* but it cannot make guarantees about the contents of that object, so the type
* of the keys is merely `string[]`. While this is technically accurate, it is
* also unnecessary if we have an object that we own and whose contents are
* known exactly.
*
* TODO: Move to @metamask/utils.
*
* @param object - The object.
* @returns The keys of an object, typed according to the type of the object
* itself.
*/
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
export function knownKeysOf(
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object) {
return Object.keys(object);
}
/**
* Type guard for determining whether the given value is an error object with a
* `code` property, such as an instance of Error.
*
* TODO: Move this to @metamask/utils.
*
* @param error - The object to check.
* @returns True if `error` has a `code`, false otherwise.
*/
function isErrorWithCode(error) {
return typeof error === 'object' && error !== null && 'code' in error;
}
const controllerName = 'NetworkController';
/**
* Constructs a value for the state property `networkConfigurationsByChainId`
* which will be used if it has not been provided to the constructor.
*
* @param [additionalDefaultNetworks] - An array of Hex Chain IDs representing the additional networks to be included as default.
* @returns The default value for `networkConfigurationsByChainId`.
*/
function getDefaultNetworkConfigurationsByChainId(additionalDefaultNetworks = []) {
const infuraNetworks = getDefaultInfuraNetworkConfigurationsByChainId();
const customNetworks = getDefaultCustomNetworkConfigurationsByChainId();
return additionalDefaultNetworks.reduce((obj, chainId) => {
if (hasProperty(customNetworks, chainId)) {
obj[chainId] = customNetworks[chainId];
}
return obj;
},
// Always include the infura networks in the default networks
infuraNetworks);
}
/**
* Constructs a `networkConfigurationsByChainId` object for all default Infura networks.
*
* @returns The `networkConfigurationsByChainId` object of all Infura networks.
*/
function getDefaultInfuraNetworkConfigurationsByChainId() {
return Object.values(InfuraNetworkType).reduce((obj, infuraNetworkType) => {
const chainId = ChainId[infuraNetworkType];
// Skip deprecated network as default network.
if (DEPRECATED_NETWORKS.has(chainId)) {
return obj;
}
const rpcEndpointUrl =
// This ESLint rule mistakenly produces an error.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`https://${infuraNetworkType}.infura.io/v3/{infuraProjectId}`;
const networkConfiguration = {
blockExplorerUrls: [],
chainId,
defaultRpcEndpointIndex: 0,
name: NetworkNickname[infuraNetworkType],
nativeCurrency: NetworksTicker[infuraNetworkType],
rpcEndpoints: [
{
failoverUrls: [],
networkClientId: infuraNetworkType,
type: RpcEndpointType.Infura,
url: rpcEndpointUrl,
},
],
};
return { ...obj, [chainId]: networkConfiguration };
}, {});
}
/**
* Constructs a `networkConfigurationsByChainId` object for all default custom networks.
*
* @returns The `networkConfigurationsByChainId` object of all custom networks.
*/
function getDefaultCustomNetworkConfigurationsByChainId() {
// Create the `networkConfigurationsByChainId` objects explicitly,
// Because it is not always guaranteed that the custom networks are included in the
// default networks.
return {
[ChainId['megaeth-testnet']]: getCustomNetworkConfiguration(CustomNetworkType['megaeth-testnet']),
[ChainId['monad-testnet']]: getCustomNetworkConfiguration(CustomNetworkType['monad-testnet']),
};
}
/**
* Constructs a `NetworkConfiguration` object by `CustomNetworkType`.
*
* @param customNetworkType - The type of the custom network.
* @returns The `NetworkConfiguration` object.
*/
function getCustomNetworkConfiguration(customNetworkType) {
const { ticker, rpcPrefs } = BUILT_IN_NETWORKS[customNetworkType];
const rpcEndpointUrl = BUILT_IN_CUSTOM_NETWORKS_RPC[customNetworkType];
return {
blockExplorerUrls: [rpcPrefs.blockExplorerUrl],
chainId: ChainId[customNetworkType],
defaultRpcEndpointIndex: 0,
defaultBlockExplorerUrlIndex: 0,
name: NetworkNickname[customNetworkType],
nativeCurrency: ticker,
rpcEndpoints: [
{
failoverUrls: [],
networkClientId: customNetworkType,
type: RpcEndpointType.Custom,
url: rpcEndpointUrl,
},
],
};
}
/**
* Constructs properties for the NetworkController state whose values will be
* used if not provided to the constructor.
*
* @param [additionalDefaultNetworks] - An array of Hex Chain IDs representing the additional networks to be included as default.
* @returns The default NetworkController state.
*/
export function getDefaultNetworkControllerState(additionalDefaultNetworks) {
const networksMetadata = {};
const networkConfigurationsByChainId = getDefaultNetworkConfigurationsByChainId(additionalDefaultNetworks);
return {
selectedNetworkClientId: InfuraNetworkType.mainnet,
networksMetadata,
networkConfigurationsByChainId,
};
}
/**
* Redux selector for getting all network configurations from NetworkController
* state, keyed by chain ID.
*
* @param state - NetworkController state
* @returns All registered network configurations, keyed by chain ID.
*/
const selectNetworkConfigurationsByChainId = (state) => state.networkConfigurationsByChainId;
/**
* Get a list of all network configurations.
*
* @param state - NetworkController state
* @returns A list of all available network configurations
*/
export function getNetworkConfigurations(state) {
return Object.values(state.networkConfigurationsByChainId);
}
/**
* Redux selector for getting a list of all network configurations from
* NetworkController state.
*
* @param state - NetworkController state
* @returns A list of all available network configurations
*/
export const selectNetworkConfigurations = createSelector(selectNetworkConfigurationsByChainId, (networkConfigurationsByChainId) => Object.values(networkConfigurationsByChainId));
/**
* Get a list of all available network client IDs from a list of network
* configurations.
*
* @param networkConfigurations - The array of network configurations
* @returns A list of all available client IDs
*/
export function getAvailableNetworkClientIds(networkConfigurations) {
return networkConfigurations.flatMap((networkConfiguration) => networkConfiguration.rpcEndpoints.map((rpcEndpoint) => rpcEndpoint.networkClientId));
}
/**
* Redux selector for getting a list of all available network client IDs
* from NetworkController state.
*
* @param state - NetworkController state
* @returns A list of all available network client IDs.
*/
export const selectAvailableNetworkClientIds = createSelector(selectNetworkConfigurations, getAvailableNetworkClientIds);
/**
* Determines whether the given URL is valid by attempting to parse it.
*
* @param url - The URL to test.
* @returns True if the URL is valid, false otherwise.
*/
function isValidUrl(url) {
const uri = URI.parse(url);
return (uri.error === undefined && (uri.scheme === 'http' || uri.scheme === 'https'));
}
/**
* Given an Infura API URL, extracts the subdomain that identifies the Infura
* network.
*
* @param rpcEndpointUrl - The URL to operate on.
* @returns The Infura network name that the URL references.
* @throws if the URL is not an Infura API URL, or if an Infura network is not
* present in the URL.
*/
function deriveInfuraNetworkNameFromRpcEndpointUrl(rpcEndpointUrl) {
const match = INFURA_URL_REGEX.exec(rpcEndpointUrl);
if (match?.groups) {
if (isInfuraNetworkType(match.groups.networkName)) {
return match.groups.networkName;
}
throw new Error(`Unknown Infura network '${match.groups.networkName}'`);
}
throw new Error('Could not derive Infura network from RPC endpoint URL');
}
/**
* Performs a series of checks that the given NetworkController state is
* internally consistent — that all parts of state that are supposed to match in
* fact do — so that working with the state later on doesn't cause unexpected
* errors.
*
* In the case of NetworkController, there are several parts of state that need
* to match. For instance, `defaultRpcEndpointIndex` needs to match an entry
* within `rpcEndpoints`, and `selectedNetworkClientId` needs to point to an RPC
* endpoint within a network configuration.
*
* @param state - The NetworkController state to verify.
* @throws if the state is invalid in some way.
*/
function validateInitialState(state) {
const networkConfigurationEntries = Object.entries(state.networkConfigurationsByChainId);
const networkClientIds = getAvailableNetworkClientIds(getNetworkConfigurations(state));
if (networkConfigurationEntries.length === 0) {
throw new Error('NetworkController state is invalid: `networkConfigurationsByChainId` cannot be empty');
}
for (const [chainId, networkConfiguration] of networkConfigurationEntries) {
if (chainId !== networkConfiguration.chainId) {
throw new Error(`NetworkController state has invalid \`networkConfigurationsByChainId\`: Network configuration '${networkConfiguration.name}' is filed under '${chainId}' which does not match its \`chainId\` of '${networkConfiguration.chainId}'`);
}
const isInvalidDefaultBlockExplorerUrlIndex = networkConfiguration.blockExplorerUrls.length > 0
? networkConfiguration.defaultBlockExplorerUrlIndex === undefined ||
networkConfiguration.blockExplorerUrls[networkConfiguration.defaultBlockExplorerUrlIndex] === undefined
: networkConfiguration.defaultBlockExplorerUrlIndex !== undefined;
if (isInvalidDefaultBlockExplorerUrlIndex) {
throw new Error(`NetworkController state has invalid \`networkConfigurationsByChainId\`: Network configuration '${networkConfiguration.name}' has a \`defaultBlockExplorerUrlIndex\` that does not refer to an entry in \`blockExplorerUrls\``);
}
if (networkConfiguration.rpcEndpoints[networkConfiguration.defaultRpcEndpointIndex] === undefined) {
throw new Error(`NetworkController state has invalid \`networkConfigurationsByChainId\`: Network configuration '${networkConfiguration.name}' has a \`defaultRpcEndpointIndex\` that does not refer to an entry in \`rpcEndpoints\``);
}
}
if ([...new Set(networkClientIds)].length < networkClientIds.length) {
throw new Error('NetworkController state has invalid `networkConfigurationsByChainId`: Every RPC endpoint across all network configurations must have a unique `networkClientId`');
}
}
/**
* Checks that the given initial NetworkController state is internally
* consistent similar to `validateInitialState`, but if an anomaly is detected,
* it does its best to correct the state and logs an error to Sentry.
*
* @param state - The NetworkController state to verify.
* @param messenger - The NetworkController messenger.
* @returns The corrected state.
*/
function correctInitialState(state, messenger) {
const networkConfigurationsSortedByChainId = getNetworkConfigurations(state).sort((a, b) => a.chainId.localeCompare(b.chainId));
const networkClientIds = getAvailableNetworkClientIds(networkConfigurationsSortedByChainId);
return produce(state, (newState) => {
if (!networkClientIds.includes(state.selectedNetworkClientId)) {
const firstNetworkConfiguration = networkConfigurationsSortedByChainId[0];
const newSelectedNetworkClientId = firstNetworkConfiguration.rpcEndpoints[firstNetworkConfiguration.defaultRpcEndpointIndex].networkClientId;
messenger.call('ErrorReportingService:captureException', new Error(`\`selectedNetworkClientId\` '${state.selectedNetworkClientId}' does not refer to an RPC endpoint within a network configuration; correcting to '${newSelectedNetworkClientId}'`));
newState.selectedNetworkClientId = newSelectedNetworkClientId;
}
});
}
/**
* Transforms a map of chain ID to network configuration to a map of network
* client ID to network configuration.
*
* @param networkConfigurationsByChainId - The network configurations, keyed by
* chain ID.
* @returns The network configurations, keyed by network client ID.
*/
function buildNetworkConfigurationsByNetworkClientId(networkConfigurationsByChainId) {
return new Map(Object.values(networkConfigurationsByChainId).flatMap((networkConfiguration) => {
return networkConfiguration.rpcEndpoints.map((rpcEndpoint) => {
return [rpcEndpoint.networkClientId, networkConfiguration];
});
}));
}
/**
* Controller that creates and manages an Ethereum network provider.
*/
export class NetworkController extends BaseController {
/**
* Constructs a NetworkController.
*
* @param options - The options; see {@link NetworkControllerOptions}.
*/
constructor(options) {
const { messenger, state, infuraProjectId, log, getRpcServiceOptions, getBlockTrackerOptions, additionalDefaultNetworks, isRpcFailoverEnabled = false, } = options;
const initialState = {
...getDefaultNetworkControllerState(additionalDefaultNetworks),
...state,
};
validateInitialState(initialState);
const correctedInitialState = correctInitialState(initialState, messenger);
if (!infuraProjectId || typeof infuraProjectId !== 'string') {
throw new Error('Invalid Infura project ID');
}
super({
name: controllerName,
metadata: {
selectedNetworkClientId: {
persist: true,
anonymous: false,
},
networksMetadata: {
persist: true,
anonymous: false,
},
networkConfigurationsByChainId: {
persist: true,
anonymous: false,
},
},
messenger,
state: correctedInitialState,
});
_NetworkController_instances.add(this);
_NetworkController_ethQuery.set(this, void 0);
_NetworkController_infuraProjectId.set(this, void 0);
_NetworkController_previouslySelectedNetworkClientId.set(this, void 0);
_NetworkController_providerProxy.set(this, void 0);
_NetworkController_blockTrackerProxy.set(this, void 0);
_NetworkController_autoManagedNetworkClientRegistry.set(this, void 0);
_NetworkController_autoManagedNetworkClient.set(this, void 0);
_NetworkController_log.set(this, void 0);
_NetworkController_getRpcServiceOptions.set(this, void 0);
_NetworkController_getBlockTrackerOptions.set(this, void 0);
_NetworkController_networkConfigurationsByNetworkClientId.set(this, void 0);
_NetworkController_isRpcFailoverEnabled.set(this, void 0);
__classPrivateFieldSet(this, _NetworkController_infuraProjectId, infuraProjectId, "f");
__classPrivateFieldSet(this, _NetworkController_log, log, "f");
__classPrivateFieldSet(this, _NetworkController_getRpcServiceOptions, getRpcServiceOptions, "f");
__classPrivateFieldSet(this, _NetworkController_getBlockTrackerOptions, getBlockTrackerOptions, "f");
__classPrivateFieldSet(this, _NetworkController_isRpcFailoverEnabled, isRpcFailoverEnabled, "f");
__classPrivateFieldSet(this, _NetworkController_previouslySelectedNetworkClientId, this.state.selectedNetworkClientId, "f");
__classPrivateFieldSet(this, _NetworkController_networkConfigurationsByNetworkClientId, buildNetworkConfigurationsByNetworkClientId(this.state.networkConfigurationsByChainId), "f");
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:getEthQuery`, () => {
return __classPrivateFieldGet(this, _NetworkController_ethQuery, "f");
});
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:getNetworkClientById`, this.getNetworkClientById.bind(this));
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:getEIP1559Compatibility`, this.getEIP1559Compatibility.bind(this));
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:setActiveNetwork`, this.setActiveNetwork.bind(this));
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:setProviderType`, this.setProviderType.bind(this));
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:findNetworkClientIdByChainId`, this.findNetworkClientIdByChainId.bind(this));
this.messagingSystem.registerActionHandler(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:getNetworkConfigurationByChainId`, this.getNetworkConfigurationByChainId.bind(this));
this.messagingSystem.registerActionHandler(
// ESLint is mistaken here; `name` is a string.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:getNetworkConfigurationByNetworkClientId`, this.getNetworkConfigurationByNetworkClientId.bind(this));
this.messagingSystem.registerActionHandler(`${this.name}:getSelectedNetworkClient`, this.getSelectedNetworkClient.bind(this));
this.messagingSystem.registerActionHandler(`${this.name}:getSelectedChainId`, this.getSelectedChainId.bind(this));
this.messagingSystem.registerActionHandler(
// ESLint is mistaken here; `name` is a string.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:addNetwork`, this.addNetwork.bind(this));
this.messagingSystem.registerActionHandler(
// ESLint is mistaken here; `name` is a string.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:removeNetwork`, this.removeNetwork.bind(this));
this.messagingSystem.registerActionHandler(
// ESLint is mistaken here; `name` is a string.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.name}:updateNetwork`, this.updateNetwork.bind(this));
}
/**
* Enables the RPC failover functionality. That is, if any RPC endpoints are
* configured with failover URLs, then traffic will automatically be diverted
* to them if those RPC endpoints are unavailable.
*/
enableRpcFailover() {
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_updateRpcFailoverEnabled).call(this, true);
}
/**
* Disables the RPC failover functionality. That is, even if any RPC endpoints
* are configured with failover URLs, then traffic will not automatically be
* diverted to them if those RPC endpoints are unavailable.
*/
disableRpcFailover() {
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_updateRpcFailoverEnabled).call(this, false);
}
/**
* Accesses the provider and block tracker for the currently selected network.
* @returns The proxy and block tracker proxies.
* @deprecated This method has been replaced by `getSelectedNetworkClient` (which has a more easily used return type) and will be removed in a future release.
*/
getProviderAndBlockTracker() {
return {
provider: __classPrivateFieldGet(this, _NetworkController_providerProxy, "f"),
blockTracker: __classPrivateFieldGet(this, _NetworkController_blockTrackerProxy, "f"),
};
}
/**
* Accesses the provider and block tracker for the currently selected network.
*
* @returns an object with the provider and block tracker proxies for the currently selected network.
*/
getSelectedNetworkClient() {
if (__classPrivateFieldGet(this, _NetworkController_providerProxy, "f") && __classPrivateFieldGet(this, _NetworkController_blockTrackerProxy, "f")) {
return {
provider: __classPrivateFieldGet(this, _NetworkController_providerProxy, "f"),
blockTracker: __classPrivateFieldGet(this, _NetworkController_blockTrackerProxy, "f"),
};
}
return undefined;
}
/**
* Accesses the chain ID from the selected network client.
*
* @returns The chain ID of the selected network client in hex format or undefined if there is no network client.
*/
getSelectedChainId() {
const networkConfiguration = this.getNetworkConfigurationByNetworkClientId(this.state.selectedNetworkClientId);
return networkConfiguration?.chainId;
}
/**
* Internally, the Infura and custom network clients are categorized by type
* so that when accessing either kind of network client, TypeScript knows
* which type to assign to the network client. For some cases it's more useful
* to be able to access network clients by ID instead of by type and then ID,
* so this function makes that possible.
*
* @returns The network clients registered so far, keyed by ID.
*/
getNetworkClientRegistry() {
const autoManagedNetworkClientRegistry = __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_ensureAutoManagedNetworkClientRegistryPopulated).call(this);
return Object.assign({}, autoManagedNetworkClientRegistry[NetworkClientType.Infura], autoManagedNetworkClientRegistry[NetworkClientType.Custom]);
}
getNetworkClientById(networkClientId) {
if (!networkClientId) {
throw new Error('No network client ID was provided.');
}
const autoManagedNetworkClientRegistry = __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_ensureAutoManagedNetworkClientRegistryPopulated).call(this);
if (isInfuraNetworkType(networkClientId)) {
const infuraNetworkClient = autoManagedNetworkClientRegistry[NetworkClientType.Infura][networkClientId];
// This is impossible to reach
/* istanbul ignore if */
if (!infuraNetworkClient) {
throw new Error(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`No Infura network client was found with the ID "${networkClientId}".`);
}
return infuraNetworkClient;
}
const customNetworkClient = autoManagedNetworkClientRegistry[NetworkClientType.Custom][networkClientId];
if (!customNetworkClient) {
throw new Error(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`No custom network client was found with the ID "${networkClientId}".`);
}
return customNetworkClient;
}
/**
* Ensures that network clients for Infura and custom RPC endpoints have been
* created. Then, consulting state, initializes and establishes the currently
* selected network client.
*/
async initializeProvider() {
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_applyNetworkSelection).call(this, this.state.selectedNetworkClientId);
await this.lookupNetwork();
}
/**
* Refreshes the network meta with EIP-1559 support and the network status
* based on the given network client ID.
*
* @param networkClientId - The ID of the network client to update.
*/
async lookupNetworkByClientId(networkClientId) {
const isInfura = isInfuraNetworkType(networkClientId);
let updatedNetworkStatus;
let updatedIsEIP1559Compatible;
try {
updatedIsEIP1559Compatible =
await __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_determineEIP1559Compatibility).call(this, networkClientId);
updatedNetworkStatus = NetworkStatus.Available;
}
catch (error) {
debugLog('NetworkController: lookupNetworkByClientId: ', error);
// TODO: mock ethQuery.sendAsync to throw error without error code
/* istanbul ignore else */
if (isErrorWithCode(error)) {
let responseBody;
if (isInfura &&
hasProperty(error, 'message') &&
typeof error.message === 'string') {
try {
responseBody = JSON.parse(error.message);
}
catch {
// error.message must not be JSON
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetworkByClientId: json parse error: ', error);
}
}
if (isPlainObject(responseBody) &&
responseBody.error === INFURA_BLOCKED_KEY) {
updatedNetworkStatus = NetworkStatus.Blocked;
}
else if (error.code === errorCodes.rpc.internal) {
updatedNetworkStatus = NetworkStatus.Unknown;
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetworkByClientId: rpc internal error: ', error);
}
else {
updatedNetworkStatus = NetworkStatus.Unavailable;
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetworkByClientId: ', error);
}
}
else if (typeof Error !== 'undefined' &&
hasProperty(error, 'message') &&
typeof error.message === 'string' &&
error.message.includes('No custom network client was found with the ID')) {
throw error;
}
else {
debugLog('NetworkController - could not determine network status', error);
updatedNetworkStatus = NetworkStatus.Unknown;
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetworkByClientId: ', error);
}
}
this.update((state) => {
if (state.networksMetadata[networkClientId] === undefined) {
state.networksMetadata[networkClientId] = {
status: NetworkStatus.Unknown,
EIPS: {},
};
}
const meta = state.networksMetadata[networkClientId];
meta.status = updatedNetworkStatus;
if (updatedIsEIP1559Compatible === undefined) {
delete meta.EIPS[1559];
}
else {
meta.EIPS[1559] = updatedIsEIP1559Compatible;
}
});
}
/**
* Persists the following metadata about the given or selected network to
* state:
*
* - The status of the network, namely, whether it is available, geo-blocked
* (Infura only), or unavailable, or whether the status is unknown
* - Whether the network supports EIP-1559, or whether it is unknown
*
* Note that it is possible for the network to be switched while this data is
* being collected. If that is the case, no metadata for the (now previously)
* selected network will be updated.
*
* @param networkClientId - The ID of the network client to update.
* If no ID is provided, uses the currently selected network.
*/
async lookupNetwork(networkClientId) {
if (networkClientId) {
await this.lookupNetworkByClientId(networkClientId);
return;
}
if (!__classPrivateFieldGet(this, _NetworkController_ethQuery, "f")) {
return;
}
const isInfura = __classPrivateFieldGet(this, _NetworkController_autoManagedNetworkClient, "f")?.configuration.type ===
NetworkClientType.Infura;
let networkChanged = false;
const listener = () => {
networkChanged = true;
try {
this.messagingSystem.unsubscribe('NetworkController:networkDidChange', listener);
}
catch (error) {
// In theory, this `catch` should not be necessary given that this error
// would occur "inside" of the call to `#determineEIP1559Compatibility`
// below and so it should be caught by the `try`/`catch` below (it is
// impossible to reproduce in tests for that reason). However, somehow
// it occurs within Mobile and so we have to add our own `try`/`catch`
// here.
/* istanbul ignore next */
if (!(error instanceof Error) ||
error.message !==
'Subscription not found for event: NetworkController:networkDidChange') {
// Again, this error should not happen and is impossible to reproduce
// in tests.
/* istanbul ignore next */
throw error;
}
}
};
this.messagingSystem.subscribe('NetworkController:networkDidChange', listener);
let updatedNetworkStatus;
let updatedIsEIP1559Compatible;
try {
const isEIP1559Compatible = await __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_determineEIP1559Compatibility).call(this, this.state.selectedNetworkClientId);
updatedNetworkStatus = NetworkStatus.Available;
updatedIsEIP1559Compatible = isEIP1559Compatible;
}
catch (error) {
// TODO: mock ethQuery.sendAsync to throw error without error code
/* istanbul ignore else */
if (isErrorWithCode(error)) {
let responseBody;
if (isInfura &&
hasProperty(error, 'message') &&
typeof error.message === 'string') {
try {
responseBody = JSON.parse(error.message);
}
catch (parseError) {
// error.message must not be JSON
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetwork: json parse error', parseError);
}
}
if (isPlainObject(responseBody) &&
responseBody.error === INFURA_BLOCKED_KEY) {
updatedNetworkStatus = NetworkStatus.Blocked;
}
else if (error.code === errorCodes.rpc.internal) {
updatedNetworkStatus = NetworkStatus.Unknown;
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetwork: rpc internal error', error);
}
else {
updatedNetworkStatus = NetworkStatus.Unavailable;
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetwork: ', error);
}
}
else {
debugLog('NetworkController - could not determine network status', error);
updatedNetworkStatus = NetworkStatus.Unknown;
__classPrivateFieldGet(this, _NetworkController_log, "f")?.warn('NetworkController: lookupNetwork: ', error);
}
}
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;
}
try {
this.messagingSystem.unsubscribe('NetworkController:networkDidChange', listener);
}
catch (error) {
if (!(error instanceof Error) ||
error.message !==
'Subscription not found for event: NetworkController:networkDidChange') {
throw error;
}
}
this.update((state) => {
const meta = state.networksMetadata[state.selectedNetworkClientId];
meta.status = updatedNetworkStatus;
if (updatedIsEIP1559Compatible === undefined) {
delete meta.EIPS[1559];
}
else {
meta.EIPS[1559] = updatedIsEIP1559Compatible;
}
});
if (isInfura) {
if (updatedNetworkStatus === NetworkStatus.Available) {
this.messagingSystem.publish('NetworkController:infuraIsUnblocked');
}
else if (updatedNetworkStatus === NetworkStatus.Blocked) {
this.messagingSystem.publish('NetworkController: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.messagingSystem.publish('NetworkController:infuraIsUnblocked');
}
}
/**
* Convenience method to update provider network type settings.
*
* @param type - Human readable network name.
* @deprecated This has been replaced by `setActiveNetwork`, and will be
* removed in a future release
*/
async setProviderType(type) {
if (type === NetworkType.rpc) {
throw new Error(
// This ESLint rule mistakenly produces an error.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`NetworkController - cannot call "setProviderType" with type "${NetworkType.rpc}". Use "setActiveNetwork"`);
}
if (!isInfuraNetworkType(type)) {
throw new Error(`Unknown Infura provider type "${String(type)}".`);
}
await this.setActiveNetwork(type);
}
/**
* Changes the selected network.
*
* @param networkClientId - The ID of a network client that will be used to
* make requests.
* @param options - Options for this method.
* @param options.updateState - Allows for updating state.
* @throws if no network client is associated with the given
* network client ID.
*/
async setActiveNetwork(networkClientId, options = {}) {
__classPrivateFieldSet(this, _NetworkController_previouslySelectedNetworkClientId, this.state.selectedNetworkClientId, "f");
await __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_refreshNetwork).call(this, networkClientId, options);
}
/**
* Determines whether the network supports EIP-1559 by checking whether the
* latest block has a `baseFeePerGas` property, then updates state
* appropriately.
*
* @param networkClientId - The networkClientId to fetch the correct provider against which to check 1559 compatibility.
* @returns A promise that resolves to true if the network supports EIP-1559
* , false otherwise, or `undefined` if unable to determine the compatibility.
*/
async getEIP1559Compatibility(networkClientId) {
if (networkClientId) {
return this.get1559CompatibilityWithNetworkClientId(networkClientId);
}
if (!__classPrivateFieldGet(this, _NetworkController_ethQuery, "f")) {
return false;
}
const { EIPS } = this.state.networksMetadata[this.state.selectedNetworkClientId];
if (EIPS[1559] !== undefined) {
return EIPS[1559];
}
const isEIP1559Compatible = await __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_determineEIP1559Compatibility).call(this, this.state.selectedNetworkClientId);
this.update((state) => {
if (isEIP1559Compatible !== undefined) {
state.networksMetadata[state.selectedNetworkClientId].EIPS[1559] =
isEIP1559Compatible;
}
});
return isEIP1559Compatible;
}
async get1559CompatibilityWithNetworkClientId(networkClientId) {
let metadata = this.state.networksMetadata[networkClientId];
if (metadata === undefined) {
await this.lookupNetwork(networkClientId);
metadata = this.state.networksMetadata[networkClientId];
}
const { EIPS } = metadata;
// may want to include some 'freshness' value - something to make sure we refetch this from time to time
return EIPS[1559];
}
/**
* Ensures that the provider and block tracker proxies are pointed to the
* currently selected network and refreshes the metadata for the
*/
async resetConnection() {
await __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_refreshNetwork).call(this, this.state.selectedNetworkClientId);
}
/**
* Returns the network configuration that has been filed under the given chain
* ID.
*
* @param chainId - The chain ID to use as a key.
* @returns The network configuration if one exists, or undefined.
*/
getNetworkConfigurationByChainId(chainId) {
return this.state.networkConfigurationsByChainId[chainId];
}
/**
* Returns the network configuration that contains an RPC endpoint with the
* given network client ID.
*
* @param networkClientId - The network client ID to use as a key.
* @returns The network configuration if one exists, or undefined.
*/
getNetworkConfigurationByNetworkClientId(networkClientId) {
return __classPrivateFieldGet(this, _NetworkController_networkConfigurationsByNetworkClientId, "f").get(networkClientId);
}
/**
* Creates and registers network clients for the collection of Infura and
* custom RPC endpoints that can be used to make requests for a particular
* chain, storing the given configuration object in state for later reference.
*
* @param fields - The object that describes the new network/chain and lists
* the RPC endpoints which front that chain.
* @returns The newly added network configuration.
* @throws if any part of `fields` would produce invalid state.
* @see {@link NetworkConfiguration}
*/
addNetwork(fields) {
const { rpcEndpoints: setOfRpcEndpointFields } = fields;
const autoManagedNetworkClientRegistry = __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_ensureAutoManagedNetworkClientRegistryPopulated).call(this);
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_validateNetworkFields).call(this, {
mode: 'add',
networkFields: fields,
autoManagedNetworkClientRegistry,
});
const networkClientOperations = setOfRpcEndpointFields.map((defaultOrCustomRpcEndpointFields) => {
const rpcEndpoint = defaultOrCustomRpcEndpointFields.type === RpcEndpointType.Custom
? {
...defaultOrCustomRpcEndpointFields,
networkClientId: uuidV4(),
}
: defaultOrCustomRpcEndpointFields;
return {
type: 'add',
rpcEndpoint,
};
});
const newNetworkConfiguration = __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_determineNetworkConfigurationToPersist).call(this, {
networkFields: fields,
networkClientOperations,
});
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_registerNetworkClientsAsNeeded).call(this, {
networkFields: fields,
networkClientOperations,
autoManagedNetworkClientRegistry,
});
this.update((state) => {
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_updateNetworkConfigurations).call(this, {
state,
mode: 'add',
networkFields: fields,
networkConfigurationToPersist: newNetworkConfiguration,
});
});
this.messagingSystem.publish(`${controllerName}:networkAdded`, newNetworkConfiguration);
return newNetworkConfiguration;
}
/**
* Updates the configuration for a previously stored network filed under the
* given chain ID, creating + registering new network clients to represent RPC
* endpoints that have been added and destroying + unregistering existing
* network clients for RPC endpoints that have been removed.
*
* Note that if `chainId` is changed, then all network clients associated with
* that chain will be removed and re-added, even if none of the RPC endpoints
* have changed.
*
* @param chainId - The chain ID associated with an existing network.
* @param fields - The object that describes the updates to the network/chain,
* including the new set of RPC endpoints which should front that chain.
* @param options - Options to provide.
* @param options.replacementSelectedRpcEndpointIndex - Usually you cannot
* remove an RPC endpoint that is being represented by the currently selected
* network client. This option allows you to specify another RPC endpoint
* (either an existing one or a new one) that should be used to select a new
* network instead.
* @returns The updated network configuration.
* @throws if `chainId` does not refer to an existing network configuration,
* if any part of `fields` would produce invalid state, etc.
* @see {@link NetworkConfiguration}
*/
async updateNetwork(chainId, fields, { replacementSelectedRpcEndpointIndex, } = {}) {
const existingNetworkConfiguration = this.state.networkConfigurationsByChainId[chainId];
if (existingNetworkConfiguration === undefined) {
throw new Error(`Could not update network: Cannot find network configuration for chain '${chainId}'`);
}
const existingChainId = chainId;
const { chainId: newChainId, rpcEndpoints: setOfNewRpcEndpointFields } = fields;
const autoManagedNetworkClientRegistry = __classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_ensureAutoManagedNetworkClientRegistryPopulated).call(this);
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_validateNetworkFields).call(this, {
mode: 'update',
networkFields: fields,
existingNetworkConfiguration,
autoManagedNetworkClientRegistry,
});
const networkClientOperations = [];
for (const newRpcEndpointFields of setOfNewRpcEndpointFields) {
const existingRpcEndpointForNoop = existingNetworkConfiguration.rpcEndpoints.find((rpcEndpoint) => {
return (rpcEndpoint.type === newRpcEndpointFields.type &&
rpcEndpoint.url === newRpcEndpointFields.url &&
(rpcEndpoint.networkClientId ===
newRpcEndpointFields.networkClientId ||
newRpcEndpointFields.networkClientId === undefined));
});
const existingRpcEndpointForReplaceWhenChainChanged = existingNetworkConfiguration.rpcEndpoints.find((rpcEndpoint) => {
return ((rpcEndpoint.type === RpcEndpointType.Infura &&
newRpcEndpointFields.type === RpcEndpointType.Infura) ||
(rpcEndpoint.type === newRpcEndpointFields.type &&
rpcEndpoint.networkClientId ===
newRpcEndpointFields.networkClientId &&