mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-12 12:47:14 +01:00
917290e764
* Revert "Moved subscribe and filter into network controller (#16693)"
This reverts commit 6f6984fa58
. That
commit was an RPC middleware refactor intended to move the subscribe
and filter middleware into the network controller, to simplify the
process of sharing this middleware between clients.
This refactor resulted in `eth_subscribe` notifications being sent on
the wrong connections, causing the UI to break in some cases (the UI
`provider` connection does not support notifications). This happened
because the `setupProviderEngine` function runs per-connection,
whereas the engine setup inside the network controller is global. The
global network client cannot support notifications because it has no
way to route them; they'll need to stay in the per-connection provider
engine.
Closes #17467
* Add e2e test
An e2e test has been added that confirms subscriptions are only
broadcast to the site that registered them. This test fails on
`develop`.
596 lines
18 KiB
JavaScript
596 lines
18 KiB
JavaScript
import { strict as assert } from 'assert';
|
|
import EventEmitter from 'events';
|
|
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
|
|
import { JsonRpcEngine } from 'json-rpc-engine';
|
|
import { providerFromEngine } from '@metamask/eth-json-rpc-middleware';
|
|
import log from 'loglevel';
|
|
import {
|
|
createSwappableProxy,
|
|
createEventEmitterProxy,
|
|
} from 'swappable-obj-proxy';
|
|
import EthQuery from 'eth-query';
|
|
import { v4 as random } from 'uuid';
|
|
import {
|
|
INFURA_PROVIDER_TYPES,
|
|
BUILT_IN_NETWORKS,
|
|
INFURA_BLOCKED_KEY,
|
|
TEST_NETWORK_TICKER_MAP,
|
|
CHAIN_IDS,
|
|
NETWORK_TYPES,
|
|
} from '../../../../shared/constants/network';
|
|
import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout';
|
|
import {
|
|
isPrefixedFormattedHexString,
|
|
isSafeChainId,
|
|
} from '../../../../shared/modules/network.utils';
|
|
import { EVENT } from '../../../../shared/constants/metametrics';
|
|
import createInfuraClient from './createInfuraClient';
|
|
import createJsonRpcClient from './createJsonRpcClient';
|
|
|
|
/**
|
|
* @typedef {object} NetworkConfiguration
|
|
* @property {string} rpcUrl - RPC target URL.
|
|
* @property {string} chainId - Network ID as per EIP-155
|
|
* @property {string} ticker - Currency ticker.
|
|
* @property {object} [rpcPrefs] - Personalized preferences.
|
|
* @property {string} [nickname] - Personalized network name.
|
|
*/
|
|
|
|
const env = process.env.METAMASK_ENV;
|
|
const fetchWithTimeout = getFetchWithTimeout();
|
|
|
|
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 = {
|
|
type: NETWORK_TYPES.MAINNET,
|
|
chainId: CHAIN_IDS.MAINNET,
|
|
};
|
|
}
|
|
|
|
const defaultProviderConfig = {
|
|
ticker: 'ETH',
|
|
...defaultProviderConfigOpts,
|
|
};
|
|
|
|
const defaultNetworkDetailsState = {
|
|
EIPS: { 1559: undefined },
|
|
};
|
|
|
|
export const NETWORK_EVENTS = {
|
|
// Fired after the actively selected network is changed
|
|
NETWORK_DID_CHANGE: 'networkDidChange',
|
|
// Fired when the actively selected network *will* change
|
|
NETWORK_WILL_CHANGE: 'networkWillChange',
|
|
// Fired when Infura returns an error indicating no support
|
|
INFURA_IS_BLOCKED: 'infuraIsBlocked',
|
|
// Fired when not using an Infura network or when Infura returns no error, indicating support
|
|
INFURA_IS_UNBLOCKED: 'infuraIsUnblocked',
|
|
};
|
|
|
|
export default class NetworkController extends EventEmitter {
|
|
static defaultProviderConfig = defaultProviderConfig;
|
|
|
|
/**
|
|
* Construct a NetworkController.
|
|
*
|
|
* @param {object} [options] - NetworkController options.
|
|
* @param {object} [options.state] - Initial controller state.
|
|
* @param {string} [options.infuraProjectId] - The Infura project ID.
|
|
* @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController
|
|
*/
|
|
constructor({ state = {}, infuraProjectId, trackMetaMetricsEvent } = {}) {
|
|
super();
|
|
|
|
// create stores
|
|
this.providerStore = new ObservableStore(
|
|
state.provider || { ...defaultProviderConfig },
|
|
);
|
|
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.networkDetails = new ObservableStore(
|
|
state.networkDetails || {
|
|
...defaultNetworkDetailsState,
|
|
},
|
|
);
|
|
|
|
this.networkConfigurationsStore = new ObservableStore(
|
|
state.networkConfigurations || {},
|
|
);
|
|
|
|
this.store = new ComposedStore({
|
|
provider: this.providerStore,
|
|
previousProviderStore: this.previousProviderStore,
|
|
network: this.networkStore,
|
|
networkDetails: this.networkDetails,
|
|
networkConfigurations: this.networkConfigurationsStore,
|
|
});
|
|
|
|
// provider and block tracker
|
|
this._provider = null;
|
|
this._blockTracker = null;
|
|
|
|
// provider and block tracker proxies - because the network changes
|
|
this._providerProxy = null;
|
|
this._blockTrackerProxy = null;
|
|
|
|
if (!infuraProjectId || typeof infuraProjectId !== 'string') {
|
|
throw new Error('Invalid Infura project ID');
|
|
}
|
|
this._infuraProjectId = infuraProjectId;
|
|
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
|
|
|
|
this.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
|
this.lookupNetwork();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Destroy the network controller, stopping any ongoing polling.
|
|
*
|
|
* In-progress requests will not be aborted.
|
|
*/
|
|
async destroy() {
|
|
await this._blockTracker?.destroy();
|
|
}
|
|
|
|
async initializeProvider() {
|
|
const { type, rpcUrl, chainId } = this.providerStore.getState();
|
|
this._configureProvider({ type, rpcUrl, chainId });
|
|
await this.lookupNetwork();
|
|
}
|
|
|
|
// return the proxies so the references will always be good
|
|
getProviderAndBlockTracker() {
|
|
const provider = this._providerProxy;
|
|
const blockTracker = this._blockTrackerProxy;
|
|
return { provider, blockTracker };
|
|
}
|
|
|
|
/**
|
|
* Method to check if the block header contains fields that indicate EIP 1559
|
|
* support (baseFeePerGas).
|
|
*
|
|
* @returns {Promise<boolean>} true if current network supports EIP 1559
|
|
*/
|
|
async getEIP1559Compatibility() {
|
|
const { EIPS } = this.networkDetails.getState();
|
|
// NOTE: This isn't necessary anymore because the block cache middleware
|
|
// already prevents duplicate requests from taking place
|
|
if (EIPS[1559] !== undefined) {
|
|
return EIPS[1559];
|
|
}
|
|
const latestBlock = await this._getLatestBlock();
|
|
const supportsEIP1559 =
|
|
latestBlock && latestBlock.baseFeePerGas !== undefined;
|
|
this._setNetworkEIPSupport(1559, supportsEIP1559);
|
|
return supportsEIP1559;
|
|
}
|
|
|
|
async lookupNetwork() {
|
|
// Prevent firing when provider is not defined.
|
|
if (!this._provider) {
|
|
log.warn(
|
|
'NetworkController - lookupNetwork aborted due to missing provider',
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { chainId } = this.providerStore.getState();
|
|
if (!chainId) {
|
|
log.warn(
|
|
'NetworkController - lookupNetwork aborted due to missing chainId',
|
|
);
|
|
this._setNetworkState('loading');
|
|
this._clearNetworkDetails();
|
|
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.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED);
|
|
}
|
|
|
|
let networkVersion;
|
|
let networkVersionError;
|
|
try {
|
|
networkVersion = await this._getNetworkId();
|
|
} catch (error) {
|
|
networkVersionError = error;
|
|
}
|
|
if (initialNetwork !== this.networkStore.getState()) {
|
|
return;
|
|
}
|
|
|
|
if (networkVersionError) {
|
|
this._setNetworkState('loading');
|
|
// keep network details in sync with network state
|
|
this._clearNetworkDetails();
|
|
} else {
|
|
this._setNetworkState(networkVersion);
|
|
// look up EIP-1559 support
|
|
await this.getEIP1559Compatibility();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A method for setting the currently selected network provider by networkConfigurationId.
|
|
*
|
|
* @param {string} networkConfigurationId - the universal unique identifier that corresponds to the network configuration to set as active.
|
|
* @returns {string} The rpcUrl of the network that was just set as active
|
|
*/
|
|
setActiveNetwork(networkConfigurationId) {
|
|
const targetNetwork =
|
|
this.networkConfigurationsStore.getState()[networkConfigurationId];
|
|
|
|
if (!targetNetwork) {
|
|
throw new Error(
|
|
`networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`,
|
|
);
|
|
}
|
|
|
|
this._setProviderConfig({
|
|
type: NETWORK_TYPES.RPC,
|
|
...targetNetwork,
|
|
});
|
|
|
|
return targetNetwork.rpcUrl;
|
|
}
|
|
|
|
setProviderType(type) {
|
|
assert.notStrictEqual(
|
|
type,
|
|
NETWORK_TYPES.RPC,
|
|
`NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`,
|
|
);
|
|
assert.ok(
|
|
INFURA_PROVIDER_TYPES.includes(type),
|
|
`Unknown Infura provider type "${type}".`,
|
|
);
|
|
const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[type];
|
|
this._setProviderConfig({
|
|
type,
|
|
rpcUrl: '',
|
|
chainId,
|
|
ticker: ticker ?? 'ETH',
|
|
nickname: '',
|
|
rpcPrefs: { blockExplorerUrl },
|
|
});
|
|
}
|
|
|
|
resetConnection() {
|
|
this._setProviderConfig(this.providerStore.getState());
|
|
}
|
|
|
|
rollbackToPreviousProvider() {
|
|
const config = this.previousProviderStore.getState();
|
|
this.providerStore.updateState(config);
|
|
this._switchNetwork(config);
|
|
}
|
|
|
|
//
|
|
// Private
|
|
//
|
|
|
|
/**
|
|
* 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);
|
|
return await new Promise((resolve, reject) => {
|
|
ethQuery.sendAsync({ method: 'net_version' }, (error, result) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Method to return the latest block for the current network
|
|
*
|
|
* @returns {object} Block header
|
|
*/
|
|
_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);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_setNetworkEIPSupport(EIPNumber, isSupported) {
|
|
this.networkDetails.updateState({
|
|
EIPS: {
|
|
[EIPNumber]: isSupported,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reset EIP support to default (no support)
|
|
*/
|
|
_clearNetworkDetails() {
|
|
this.networkDetails.putState({ ...defaultNetworkDetailsState });
|
|
}
|
|
|
|
/**
|
|
* Sets the provider config and switches the network.
|
|
*
|
|
* @param config
|
|
*/
|
|
_setProviderConfig(config) {
|
|
this.previousProviderStore.updateState(this.providerStore.getState());
|
|
this.providerStore.updateState(config);
|
|
this._switchNetwork(config);
|
|
}
|
|
|
|
async _checkInfuraAvailability(network) {
|
|
const rpcUrl = `https://${network}.infura.io/v3/${this._infuraProjectId}`;
|
|
|
|
let networkChanged = false;
|
|
this.once(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
|
|
networkChanged = true;
|
|
});
|
|
|
|
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.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED);
|
|
} else {
|
|
const responseMessage = await response.json();
|
|
if (networkChanged) {
|
|
return;
|
|
}
|
|
if (responseMessage.error === INFURA_BLOCKED_KEY) {
|
|
this.emit(NETWORK_EVENTS.INFURA_IS_BLOCKED);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
log.warn(`MetaMask - Infura availability check failed`, err);
|
|
}
|
|
}
|
|
|
|
_switchNetwork(opts) {
|
|
// Indicate to subscribers that network is about to change
|
|
this.emit(NETWORK_EVENTS.NETWORK_WILL_CHANGE);
|
|
// Set loading state
|
|
this._setNetworkState('loading');
|
|
// Reset network details
|
|
this._clearNetworkDetails();
|
|
// Configure the provider appropriately
|
|
this._configureProvider(opts);
|
|
// Notify subscribers that network has changed
|
|
this.emit(NETWORK_EVENTS.NETWORK_DID_CHANGE, opts.type);
|
|
}
|
|
|
|
_configureProvider({ type, rpcUrl, chainId }) {
|
|
// infura type-based endpoints
|
|
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
|
|
if (isInfura) {
|
|
this._configureInfuraProvider(type, this._infuraProjectId);
|
|
// url-based rpc endpoints
|
|
} else if (type === NETWORK_TYPES.RPC) {
|
|
this._configureStandardProvider(rpcUrl, chainId);
|
|
} else {
|
|
throw new Error(
|
|
`NetworkController - _configureProvider - unknown type "${type}"`,
|
|
);
|
|
}
|
|
}
|
|
|
|
_configureInfuraProvider(type, projectId) {
|
|
log.info('NetworkController - configureInfuraProvider', type);
|
|
const networkClient = createInfuraClient({
|
|
network: type,
|
|
projectId,
|
|
});
|
|
this._setNetworkClient(networkClient);
|
|
}
|
|
|
|
_configureStandardProvider(rpcUrl, chainId) {
|
|
log.info('NetworkController - configureStandardProvider', rpcUrl);
|
|
const networkClient = createJsonRpcClient({ rpcUrl, chainId });
|
|
this._setNetworkClient(networkClient);
|
|
}
|
|
|
|
_setNetworkClient({ networkMiddleware, blockTracker }) {
|
|
const engine = new JsonRpcEngine();
|
|
engine.push(networkMiddleware);
|
|
const provider = providerFromEngine(engine);
|
|
this._setProviderAndBlockTracker({ provider, blockTracker });
|
|
}
|
|
|
|
_setProviderAndBlockTracker({ provider, blockTracker }) {
|
|
// update or initialize proxies
|
|
if (this._providerProxy) {
|
|
this._providerProxy.setTarget(provider);
|
|
} else {
|
|
this._providerProxy = createSwappableProxy(provider);
|
|
}
|
|
if (this._blockTrackerProxy) {
|
|
this._blockTrackerProxy.setTarget(blockTracker);
|
|
} else {
|
|
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, {
|
|
eventFilter: 'skipInternal',
|
|
});
|
|
}
|
|
// set new provider and blockTracker
|
|
this._provider = provider;
|
|
this._blockTracker = blockTracker;
|
|
}
|
|
|
|
/**
|
|
* Network Configuration management functions
|
|
*/
|
|
|
|
/**
|
|
* Adds a network configuration if the rpcUrl is not already present on an
|
|
* existing network configuration. Otherwise updates the entry with the matching rpcUrl.
|
|
*
|
|
* @param {NetworkConfiguration} networkConfiguration - The network configuration to add or, if rpcUrl matches an existing entry, to modify.
|
|
* @param {object} options
|
|
* @param {boolean} options.setActive - An option to set the newly added networkConfiguration as the active provider.
|
|
* @param {string} options.referrer - The site from which the call originated, or 'metamask' for internal calls - used for event metrics.
|
|
* @param {string} options.source - Where the upsertNetwork event originated (i.e. from a dapp or from the network form)- used for event metrics.
|
|
* @returns {string} id for the added or updated network configuration
|
|
*/
|
|
upsertNetworkConfiguration(
|
|
{ rpcUrl, chainId, ticker, nickname, rpcPrefs },
|
|
{ setActive = false, referrer, source },
|
|
) {
|
|
assert.ok(
|
|
isPrefixedFormattedHexString(chainId),
|
|
`Invalid chain ID "${chainId}": invalid hex string.`,
|
|
);
|
|
assert.ok(
|
|
isSafeChainId(parseInt(chainId, 16)),
|
|
`Invalid chain ID "${chainId}": numerical value greater than max safe value.`,
|
|
);
|
|
|
|
if (!rpcUrl) {
|
|
throw new Error(
|
|
'An rpcUrl is required to add or update network configuration',
|
|
);
|
|
}
|
|
|
|
if (!referrer || !source) {
|
|
throw new Error(
|
|
'referrer and source are required arguments for adding or updating a network configuration',
|
|
);
|
|
}
|
|
|
|
try {
|
|
// eslint-disable-next-line no-new
|
|
new URL(rpcUrl);
|
|
} catch (e) {
|
|
if (e.message.includes('Invalid URL')) {
|
|
throw new Error('rpcUrl must be a valid URL');
|
|
}
|
|
}
|
|
|
|
if (!ticker) {
|
|
throw new Error(
|
|
'A ticker is required to add or update networkConfiguration',
|
|
);
|
|
}
|
|
|
|
const networkConfigurations = this.networkConfigurationsStore.getState();
|
|
const newNetworkConfiguration = {
|
|
rpcUrl,
|
|
chainId,
|
|
ticker,
|
|
nickname,
|
|
rpcPrefs,
|
|
};
|
|
|
|
const oldNetworkConfigurationId = Object.values(networkConfigurations).find(
|
|
(networkConfiguration) =>
|
|
networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(),
|
|
)?.id;
|
|
|
|
const newNetworkConfigurationId = oldNetworkConfigurationId || random();
|
|
this.networkConfigurationsStore.updateState({
|
|
...networkConfigurations,
|
|
[newNetworkConfigurationId]: {
|
|
...newNetworkConfiguration,
|
|
id: newNetworkConfigurationId,
|
|
},
|
|
});
|
|
|
|
if (!oldNetworkConfigurationId) {
|
|
this._trackMetaMetricsEvent({
|
|
event: 'Custom Network Added',
|
|
category: EVENT.CATEGORIES.NETWORK,
|
|
referrer: {
|
|
url: referrer,
|
|
},
|
|
properties: {
|
|
chain_id: chainId,
|
|
symbol: ticker,
|
|
source,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (setActive) {
|
|
this.setActiveNetwork(newNetworkConfigurationId);
|
|
}
|
|
|
|
return newNetworkConfigurationId;
|
|
}
|
|
|
|
/**
|
|
* Removes network configuration from state.
|
|
*
|
|
* @param {string} networkConfigurationId - the unique id for the network configuration to remove.
|
|
*/
|
|
removeNetworkConfiguration(networkConfigurationId) {
|
|
const networkConfigurations = {
|
|
...this.networkConfigurationsStore.getState(),
|
|
};
|
|
delete networkConfigurations[networkConfigurationId];
|
|
this.networkConfigurationsStore.putState(networkConfigurations);
|
|
}
|
|
}
|