1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

NetworkController: Split network into networkId and networkStatus (#17556)

The `network` store of the network controller crams two types of data
into one place. It roughly tracks whether we have enough information to
make requests to the network and whether the network is capable of
receiving requests, but it also stores the ID of the network (as
obtained via `net_version`).

Generally we shouldn't be using the network ID for anything, as it has
been completely replaced by chain ID, which all custom RPC endpoints
have been required to support for over a year now. However, as the
network ID is used in various places within the extension codebase,
removing it entirely would be a non-trivial effort. So, minimally, this
commit splits `network` into two stores: `networkId` and
`networkStatus`. But it also expands the concept of network status.

Previously, the network was in one of two states: "loading" and
"not-loading". But now it can be in one of four states:

- `available`: The network is able to receive and respond to requests.
- `unavailable`: The network is not able to receive and respond to
  requests for unknown reasons.
- `blocked`: The network is actively blocking requests based on the
  user's geolocation. (This is specific to Infura.)
- `unknown`: We don't know whether the network can receive and respond
  to requests, either because we haven't checked or we tried to check
  and were unsuccessful.

This commit also changes how the network status is determined —
specifically, how many requests are used to determine that status, when
they occur, and whether they are awaited. Previously, the network
controller would make 2 to 3 requests during the course of running
`lookupNetwork`.

* First, if it was an Infura network, it would make a request for
  `eth_blockNumber` to determine whether Infura was blocking requests or
  not, then emit an appropriate event. This operation was not awaited.
* Then, regardless of the network, it would fetch the network ID via
  `net_version`. This operation was awaited.
* Finally, regardless of the network, it would fetch the latest block
  via `eth_getBlockByNumber`, then use the result to determine whether
  the network supported EIP-1559. This operation was awaited.

Now:

* One fewer request is made, specifically `eth_blockNumber`, as we don't
  need to make an extra request to determine whether Infura is blocking
  requests; we can reuse `eth_getBlockByNumber`;
* All requests are awaited, which makes `lookupNetwork` run fully
  in-band instead of partially out-of-band; and
* Both requests for `net_version` and `eth_getBlockByNumber` are
  performed in parallel to make `lookupNetwork` run slightly faster.
This commit is contained in:
Elliot Winkler 2023-03-30 16:49:12 -06:00 committed by GitHub
parent a53b9fb489
commit ed3cc404f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 3809 additions and 1985 deletions

View File

@ -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.

View File

@ -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();
});

View File

@ -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<boolean>} true if current network supports EIP 1559
* @returns {Promise<boolean>} 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<boolean>} 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();
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
});

View File

@ -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;

View File

@ -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(

View File

@ -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);
});
});

View File

@ -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),
),
});
}

View File

@ -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,
});

View File

@ -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,

View File

@ -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',
};
}

View File

@ -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({

View File

@ -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 },
};
}

View File

@ -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<string, unknown>;
}) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
versionedData.data = transformState(versionedData.data);
return versionedData;
}
function transformState(state: Record<string, unknown>) {
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 };
}

View File

@ -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;

View File

@ -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',
}

View File

@ -72,7 +72,8 @@
"featureFlags": {
"showIncomingTransactions": true
},
"network": "5",
"networkId": "5",
"networkStatus": "available",
"provider": {
"type": "rpc",
"chainId": "0x5",

View File

@ -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

View File

@ -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' })

View File

@ -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 }) => {

View File

@ -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',

View File

@ -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',
},

View File

@ -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,

View File

@ -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);

View File

@ -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',

View File

@ -9,7 +9,7 @@ export default function txHelper(
decryptMsgs: Record<string, any> | null,
encryptionPublicKeyMsgs: Record<string, any> | null,
typedMessages: Record<string, any> | null,
network?: string,
networkId?: string | null,
chainId?: string,
): Record<string, any> {
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`);

View File

@ -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;

View File

@ -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,
);

View File

@ -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;

View File

@ -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: {},
},

View File

@ -31,7 +31,7 @@ describe('Confirm Transaction Selector', () => {
unapprovedMsgCount: 1,
unapprovedPersonalMsgCount: 1,
unapprovedTypedMessagesCount: 1,
network: '5',
networkId: '5',
provider: {
chainId: '0x5',
},

View File

@ -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(

View File

@ -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;

View File

@ -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<string, unknown>;