diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 4766b0221e47..382dd314ab87 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -815,7 +815,7 @@ "message": "Contract deployment" }, "contractDescription": { - "message": "To protect yourself against scammers, take a moment to verify contract details." + "message": "To protect yourself against scammers, take a moment to verify third-party details." }, "contractInteraction": { "message": "Contract interaction" @@ -830,10 +830,10 @@ "message": "Contract requesting signature" }, "contractRequestingSpendingCap": { - "message": "Contract requesting spending cap" + "message": "Third party requesting spending cap" }, "contractTitle": { - "message": "Contract details" + "message": "Third-party details" }, "contractToken": { "message": "Token contract" @@ -1808,14 +1808,14 @@ "message": "Your initial transaction was confirmed by the network. Click OK to go back." }, "inputLogicEmptyState": { - "message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later." + "message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later." }, "inputLogicEqualOrSmallerNumber": { - "message": "This allows the contract to spend $1 from your current balance.", + "message": "This allows the third party to spend $1 from your current balance.", "description": "$1 is the current token balance in the account and the name of the current token" }, "inputLogicHigherNumber": { - "message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap." + "message": "This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap." }, "insightsFromSnap": { "message": "Insights from $1", @@ -2486,6 +2486,9 @@ "message": "OpenSea is the first provider for this feature. More providers coming soon!", "description": "Description of a notification in the 'See What's New' popup. Describes Opensea Security Provider feature." }, + "notifications18Title": { + "message": "Stay safe with security alerts" + }, "notifications19ActionText": { "message": "Enable NFT autodetection" }, @@ -3328,7 +3331,7 @@ "description": "$1 is a token symbol" }, "revokeSpendingCapTooltipText": { - "message": "This contract will be unable to spend any more of your current or future tokens." + "message": "This third party will be unable to spend any more of your current or future tokens." }, "rpcUrl": { "message": "New RPC URL" @@ -4660,7 +4663,7 @@ "message": "Username" }, "verifyContractDetails": { - "message": "Verify contract details" + "message": "Verify third-party details" }, "verifyThisTokenDecimalOn": { "message": "Token decimal can be found on $1", @@ -4746,7 +4749,7 @@ "message": "Warning" }, "warningTooltipText": { - "message": "$1 The contract could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", + "message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, "weak": { diff --git a/app/scripts/background.js b/app/scripts/background.js index d92fddb816a9..3aff0170bf2f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -80,7 +80,6 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info'); const platform = new ExtensionPlatform(); const notificationManager = new NotificationManager(); -global.METAMASK_NOTIFIER = notificationManager; let popupIsOpen = false; let notificationIsOpen = false; @@ -727,7 +726,6 @@ export function setupController( } function getUnapprovedTransactionCount() { - const unapprovedTxCount = controller.txController.getUnapprovedTxCount(); const { unapprovedDecryptMsgCount } = controller.decryptMessageManager; const { unapprovedEncryptionPublicKeyMsgCount } = controller.encryptionPublicKeyManager; @@ -736,7 +734,6 @@ export function setupController( const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length; return ( - unapprovedTxCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + pendingApprovalCount + diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index c90ebc5903b7..10db93359403 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -13,7 +13,7 @@ import { convertHexToDecimal } from '@metamask/controller-utils'; import { NETWORK_TYPES } from '../../../shared/constants/network'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; -import NetworkController, { NetworkControllerEventTypes } from './network'; +import { NetworkController, NetworkControllerEventType } from './network'; import PreferencesController from './preferences'; describe('DetectTokensController', function () { @@ -248,7 +248,7 @@ describe('DetectTokensController', function () { ), onNetworkStateChange: (cb) => networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, () => { const networkState = network.store.getState(); const modifiedNetworkState = { diff --git a/app/scripts/controllers/network/index.js b/app/scripts/controllers/network/index.js deleted file mode 100644 index b91e1669884f..000000000000 --- a/app/scripts/controllers/network/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default, NetworkControllerEventTypes } from './network-controller'; diff --git a/app/scripts/controllers/network/index.ts b/app/scripts/controllers/network/index.ts new file mode 100644 index 000000000000..de3e59ea1a28 --- /dev/null +++ b/app/scripts/controllers/network/index.ts @@ -0,0 +1 @@ +export * from './network-controller'; diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js deleted file mode 100644 index 4449bc68342f..000000000000 --- a/app/scripts/controllers/network/network-controller.js +++ /dev/null @@ -1,679 +0,0 @@ -import { strict as assert } from 'assert'; -import EventEmitter from 'events'; -import { ComposedStore, ObservableStore } from '@metamask/obs-store'; -import log from 'loglevel'; -import { - createSwappableProxy, - createEventEmitterProxy, -} from '@metamask/swappable-obj-proxy'; -import EthQuery from 'eth-query'; -// ControllerMessenger is referred to in the JSDocs -// 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, - INFURA_BLOCKED_KEY, - TEST_NETWORK_TICKER_MAP, - CHAIN_IDS, - NETWORK_TYPES, - NetworkStatus, -} from '../../../../shared/constants/network'; -import { - isPrefixedFormattedHexString, - isSafeChainId, -} from '../../../../shared/modules/network.utils'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; -import { createNetworkClient } from './create-network-client'; - -/** - * @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. - */ - -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, - }; - } - - return { - type: NETWORK_TYPES.MAINNET, - chainId: CHAIN_IDS.MAINNET, - ticker: 'ETH', - }; -} - -function buildDefaultNetworkIdState() { - return null; -} - -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. - */ -export const NetworkControllerEventTypes = { - /** - * Fired after the current network is changed. - */ - NetworkDidChange: `${name}:networkDidChange`, - /** - * Fired when there is a request to change the current network, but no state - * changes have occurred yet. - */ - NetworkWillChange: `${name}:networkWillChange`, - /** - * Fired after the network is changed to an Infura network, but when Infura - * returns an error denying support for the user's location. - */ - InfuraIsBlocked: `${name}:infuraIsBlocked`, - /** - * Fired after the network is changed to an Infura network and Infura does not - * return an error denying support for the user's location, or after the - * network is changed to a custom network. - */ - InfuraIsUnblocked: `${name}:infuraIsUnblocked`, -}; - -export default class NetworkController extends EventEmitter { - /** - * Construct a NetworkController. - * - * @param {object} options - Options for this controller. - * @param {ControllerMessenger} options.messenger - The controller messenger. - * @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({ - messenger, - state = {}, - infuraProjectId, - trackMetaMetricsEvent, - } = {}) { - super(); - - this.messenger = messenger; - - // create stores - this.providerStore = new ObservableStore( - state.provider || buildDefaultProviderConfigState(), - ); - this.previousProviderStore = new ObservableStore( - this.providerStore.getState(), - ); - 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 || buildDefaultNetworkDetailsState(), - ); - - this.networkConfigurationsStore = new ObservableStore( - state.networkConfigurations || buildDefaultNetworkConfigurationsState(), - ); - - this.store = new ComposedStore({ - provider: this.providerStore, - previousProviderStore: this.previousProviderStore, - networkId: this.networkIdStore, - networkStatus: this.networkStatusStore, - 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; - } - - /** - * 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 }; - } - - /** - * Determines whether the network supports EIP-1559 by checking whether the - * latest block has a `baseFeePerGas` property, then updates state - * appropriately. - * - * @returns {Promise} A promise that resolves to true if the network - * supports EIP-1559 and false otherwise. - */ - 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 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() { - 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', - ); - return; - } - - if (!chainId) { - log.warn( - 'NetworkController - lookupNetwork aborted due to missing chainId', - ); - this._resetNetworkId(); - this._resetNetworkStatus(); - this._resetNetworkDetails(); - return; - } - - const isInfura = INFURA_PROVIDER_TYPES.includes(type); - - 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; - } - } - - 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, - ); - - this.networkStatusStore.putState(networkStatus); - - if (networkStatus === NetworkStatus.Available) { - this.networkIdStore.putState(networkId); - this.networkDetails.updateState({ - EIPS: { - ...this.networkDetails.getState().EIPS, - 1559: supportsEIP1559, - }, - }); - } else { - 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); - } - } - - /** - * 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.putState(config); - this._switchNetwork(config); - } - - // - // 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 { provider } = this.getProviderAndBlockTracker(); - const ethQuery = new EthQuery(provider); - - return await new Promise((resolve, reject) => { - ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - } - - /** - * Clears the stored network ID. - */ - _resetNetworkId() { - this.networkIdStore.putState(buildDefaultNetworkIdState()); - } - - /** - * Resets network status to the default ("unknown"). - */ - _resetNetworkStatus() { - this.networkStatusStore.putState(buildDefaultNetworkStatusState()); - } - - /** - * Clears details previously stored for the network. - */ - _resetNetworkDetails() { - this.networkDetails.putState(buildDefaultNetworkDetailsState()); - } - - /** - * Sets the provider config and switches the network. - * - * @param config - */ - _setProviderConfig(config) { - this.previousProviderStore.putState(this.providerStore.getState()); - this.providerStore.putState(config); - this._switchNetwork(config); - } - - /** - * Retrieves the latest block from the currently selected network; if the - * block has a `baseFeePerGas` property, then we know that the network - * supports EIP-1559; otherwise it doesn't. - * - * @returns {Promise} A promise that resolves to true if the network - * supports EIP-1559 and false otherwise. - */ - async _determineEIP1559Compatibility() { - const latestBlock = await this._getLatestBlock(); - return latestBlock && latestBlock.baseFeePerGas !== undefined; - } - - _switchNetwork(opts) { - this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange); - this._resetNetworkId(); - this._resetNetworkStatus(); - this._resetNetworkDetails(); - this._configureProvider(opts); - this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange); - this.lookupNetwork(); - } - - _configureProvider({ type, rpcUrl, chainId }) { - // infura type-based endpoints - const isInfura = INFURA_PROVIDER_TYPES.includes(type); - if (isInfura) { - this._configureInfuraProvider({ - type, - infuraProjectId: 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, infuraProjectId }) { - log.info('NetworkController - configureInfuraProvider', type); - const { provider, blockTracker } = createNetworkClient({ - network: type, - infuraProjectId, - type: 'infura', - }); - this._setProviderAndBlockTracker({ provider, blockTracker }); - } - - _configureStandardProvider(rpcUrl, chainId) { - log.info('NetworkController - configureStandardProvider', rpcUrl); - const { provider, blockTracker } = createNetworkClient({ - chainId, - rpcUrl, - type: 'custom', - }); - 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.putState({ - ...networkConfigurations, - [newNetworkConfigurationId]: { - ...newNetworkConfiguration, - id: newNetworkConfigurationId, - }, - }); - - if (!oldNetworkConfigurationId) { - this._trackMetaMetricsEvent({ - event: 'Custom Network Added', - category: MetaMetricsEventCategory.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); - } -} diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index a58c563a87f2..fb86440c47b9 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -6,7 +6,7 @@ import sinon from 'sinon'; import { ControllerMessenger } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; import { MetaMetricsNetworkEventSource } from '../../../../shared/constants/metametrics'; -import NetworkController from './network-controller'; +import { NetworkController } from './network-controller'; jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -1100,7 +1100,7 @@ describe('NetworkController', () => { }); describe('when the request for the latest block responds with null', () => { - it('stores null as whether the network supports EIP-1559', async () => { + it('persists false to state as whether the network supports EIP-1559', async () => { await withController( { state: { @@ -1118,13 +1118,13 @@ describe('NetworkController', () => { await controller.getEIP1559Compatibility(); expect(controller.store.getState().networkDetails.EIPS[1559]).toBe( - null, + false, ); }, ); }); - it('returns null', async () => { + it('returns false', async () => { await withController(async ({ controller, network }) => { network.mockEssentialRpcCalls({ latestBlock: null, @@ -1133,7 +1133,7 @@ describe('NetworkController', () => { const supportsEIP1559 = await controller.getEIP1559Compatibility(); - expect(supportsEIP1559).toBe(null); + expect(supportsEIP1559).toBe(false); }); }); }); diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts new file mode 100644 index 000000000000..9249e0fa2b22 --- /dev/null +++ b/app/scripts/controllers/network/network-controller.ts @@ -0,0 +1,1171 @@ +import { strict as assert } from 'assert'; +import EventEmitter from 'events'; +import { ComposedStore, ObservableStore } from '@metamask/obs-store'; +import log from 'loglevel'; +import { + createSwappableProxy, + createEventEmitterProxy, + SwappableProxy, +} from '@metamask/swappable-obj-proxy'; +import EthQuery from 'eth-query'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { v4 as uuid } from 'uuid'; +import { Hex, isPlainObject } from '@metamask/utils'; +import { errorCodes } from 'eth-rpc-errors'; +import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { PollingBlockTracker } from 'eth-block-tracker'; +import { + INFURA_PROVIDER_TYPES, + INFURA_BLOCKED_KEY, + TEST_NETWORK_TICKER_MAP, + CHAIN_IDS, + NETWORK_TYPES, + BUILT_IN_INFURA_NETWORKS, + BuiltInInfuraNetwork, + NetworkStatus, +} from '../../../../shared/constants/network'; +import { + isPrefixedFormattedHexString, + isSafeChainId, +} from '../../../../shared/modules/network.utils'; +import { + MetaMetricsEventCategory, + MetaMetricsEventPayload, +} from '../../../../shared/constants/metametrics'; +import { isErrorWithMessage } from '../../../../shared/modules/error'; +import { + createNetworkClient, + NetworkClientType, +} from './create-network-client'; + +/** + * The name of NetworkController. + */ +const name = 'NetworkController'; + +/** + * A block header object that `eth_getBlockByNumber` returns. Note that this + * type does not specify all of the properties present within the block header; + * within NetworkController, we are only interested in `baseFeePerGas`. + */ +type Block = { + baseFeePerGas?: unknown; +}; + +/** + * Encodes a few pieces of information: + * + * - Whether or not a provider is configured for an Infura network or a + * non-Infura network. + * - If an Infura network, then which network. + * - If a non-Infura network, then whether the network exists locally or + * remotely. + * + * Primarily used to build the network client and check the availability of a + * network. + */ +type ProviderType = BuiltInInfuraNetwork | typeof NETWORK_TYPES.RPC; + +/** + * The network ID of a network. + */ +type NetworkId = `${number}`; + +/** + * The ID of a network configuration. + */ +type NetworkConfigurationId = string; + +/** + * The chain ID of a network. + */ +type ChainId = Hex; + +/** + * The set of event types that NetworkController can publish via its messenger. + */ +export enum NetworkControllerEventType { + /** + * @see {@link NetworkControllerNetworkWillChangeEvent} + */ + NetworkWillChange = 'NetworkController:networkWillChange', + /** + * @see {@link NetworkControllerNetworkDidChangeEvent} + */ + NetworkDidChange = 'NetworkController:networkDidChange', + /** + * @see {@link NetworkControllerInfuraIsBlockedEvent} + */ + InfuraIsBlocked = 'NetworkController:infuraIsBlocked', + /** + * @see {@link NetworkControllerInfuraIsUnblockedEvent} + */ + InfuraIsUnblocked = 'NetworkController:infuraIsUnblocked', +} + +/** + * `networkWillChange` is published when the current network is about to be + * switched, but the new provider has not been created and no state changes have + * occurred yet. + */ +type NetworkControllerNetworkWillChangeEvent = { + type: NetworkControllerEventType.NetworkWillChange; + payload: []; +}; + +/** + * `networkDidChange` is published after a provider has been created for a newly + * switched network (but before the network has been confirmed to be available). + */ +type NetworkControllerNetworkDidChangeEvent = { + type: NetworkControllerEventType.NetworkDidChange; + payload: []; +}; + +/** + * `infuraIsBlocked` is published after the network is switched to an Infura + * network, but when Infura returns an error blocking the user based on their + * location. + */ +type NetworkControllerInfuraIsBlockedEvent = { + type: NetworkControllerEventType.InfuraIsBlocked; + payload: []; +}; + +/** + * `infuraIsBlocked` is published either after the network is switched to an + * Infura network and Infura does not return an error blocking the user based on + * their location, or the network is switched to a non-Infura network. + */ +type NetworkControllerInfuraIsUnblockedEvent = { + type: NetworkControllerEventType.InfuraIsUnblocked; + payload: []; +}; + +/** + * The set of events that the NetworkController messenger can publish. + */ +type NetworkControllerEvent = + | NetworkControllerNetworkDidChangeEvent + | NetworkControllerNetworkWillChangeEvent + | NetworkControllerInfuraIsBlockedEvent + | NetworkControllerInfuraIsUnblockedEvent; + +/** + * The messenger that the NetworkController uses to publish events. + */ +type NetworkControllerMessenger = RestrictedControllerMessenger< + typeof name, + never, + NetworkControllerEvent, + never, + NetworkControllerEventType +>; + +/** + * Information used to set up the middleware stack for a particular kind of + * network. Currently has overlap with `NetworkConfiguration`, although the + * two will be merged down the road. + */ +type ProviderConfiguration = { + /** + * Either a type of Infura network, "localhost" for a locally operated + * network, or "rpc" for everything else. + */ + type: ProviderType; + /** + * The chain ID as per EIP-155. + */ + chainId: ChainId; + /** + * The URL of the RPC endpoint. Only used when `type` is "localhost" or "rpc". + */ + rpcUrl?: string; + /** + * The shortname of the currency used by the network. + */ + ticker?: string; + /** + * The user-customizable name of the network. + */ + nickname?: string; + /** + * User-customizable details for the network. + */ + rpcPrefs?: { + blockExplorerUrl?: string; + }; +}; + +/** + * The contents of the `networkId` store. + */ +type NetworkIdState = NetworkId | null; + +/** + * Information about the network not held by any other part of state. Currently + * only used to capture whether a network supports EIP-1559. + */ +type NetworkDetails = { + /** + * EIPs supported by the network. + */ + EIPS: { + [eipNumber: number]: boolean | undefined; + }; +}; + +/** + * A "network configuration" represents connection data directly provided by + * users via the wallet UI for a custom network (we already have this + * information for networks that come pre-shipped with the wallet). Ultimately + * used to set up the middleware stack so that the wallet can make requests to + * the network. Currently has overlap with `ProviderConfiguration`, although the + * two will be merged down the road. + */ +type NetworkConfiguration = { + /** + * The unique ID of the network configuration. Useful for switching to and + * removing specific networks. + */ + id: NetworkConfigurationId; + /** + * The URL of the RPC endpoint. Only used when `type` is "localhost" or "rpc". + */ + rpcUrl: string; + /** + * The chain ID as per EIP-155. + */ + chainId: ChainId; + /** + * The shortname of the currency used for this network. + */ + ticker: string; + /** + * The user-customizable name of the network. + */ + nickname?: string; + /** + * User-customizable details for the network. + */ + rpcPrefs?: { + blockExplorerUrl: string; + }; +}; + +/** + * A set of network configurations, keyed by ID. + */ +type NetworkConfigurations = Record< + NetworkConfigurationId, + NetworkConfiguration +>; + +/** + * The state that NetworkController holds after combining its individual stores. + */ +type CompositeState = { + provider: ProviderConfiguration; + previousProviderStore: ProviderConfiguration; + networkId: NetworkIdState; + networkStatus: NetworkStatus; + networkDetails: NetworkDetails; + networkConfigurations: NetworkConfigurations; +}; + +/** + * The options that NetworkController takes. + */ +type NetworkControllerOptions = { + messenger: NetworkControllerMessenger; + state?: { + provider?: ProviderConfiguration; + networkDetails?: NetworkDetails; + networkConfigurations?: NetworkConfigurations; + }; + infuraProjectId: string; + trackMetaMetricsEvent: (payload: MetaMetricsEventPayload) => void; +}; + +/** + * 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: unknown): error is { code: string | number } { + return typeof error === 'object' && error !== null && 'code' in error; +} + +/** + * Asserts that the given value is a network ID, i.e., that it is a decimal + * number represented as a string. + * + * @param value - The value to check. + */ +function assertNetworkId(value: any): asserts value is NetworkId { + assert( + /^\d+$/u.test(value) && !Number.isNaN(Number(value)), + 'value is not a number', + ); +} + +/** + * Builds the default provider config used to initialize the network controller. + */ +function buildDefaultProviderConfigState(): ProviderConfiguration { + 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[NETWORK_TYPES.GOERLI], + }; + } + + return { + type: NETWORK_TYPES.MAINNET, + chainId: CHAIN_IDS.MAINNET, + ticker: 'ETH', + }; +} + +/** + * Builds the default network ID state used to initialize the network + * controller. + */ +function buildDefaultNetworkIdState(): NetworkIdState { + return null; +} + +/** + * Builds the default network status state used to initialize the network + * controller. + */ +function buildDefaultNetworkStatusState(): NetworkStatus { + return NetworkStatus.Unknown; +} + +/** + * Builds the default network details state used to initialize the + * network controller. + */ +function buildDefaultNetworkDetailsState(): NetworkDetails { + return { + EIPS: { + 1559: undefined, + }, + }; +} + +/** + * Builds the default network configurations state used to initialize the + * network controller. + */ +function buildDefaultNetworkConfigurationsState(): NetworkConfigurations { + return {}; +} + +/** + * Returns whether the given argument is a type that our Infura middleware + * recognizes. We can't calculate this inline because the usual type of `type`, + * which we get from the provider config, is not a subset of the type of + * `INFURA_PROVIDER_TYPES`, but rather a superset, and therefore we cannot make + * a proper comparison without TypeScript complaining. However, if we downcast + * both variables, then we are able to achieve this. As a bonus, this function + * also types the given argument as a `BuiltInInfuraNetwork` assuming that the + * check succeeds. + * + * @param type - A type to compare. + * @returns True or false, depending on whether the given type is one that our + * Infura middleware recognizes. + */ +function isInfuraProviderType(type: string): type is BuiltInInfuraNetwork { + const infuraProviderTypes: readonly string[] = INFURA_PROVIDER_TYPES; + return infuraProviderTypes.includes(type); +} + +/** + * The network controller creates and manages the "provider" object which allows + * our code and external dapps to make requests to a network. The requests are + * filtered through a set of middleware (provided by + * [`eth-json-rpc-middleware`][1]) which not only performs the HTTP request to + * the appropriate RPC endpoint but also uses caching to limit duplicate + * requests to Infura and smoothens interactions with the blockchain in general. + * + * [1]: https://github.com/MetaMask/eth-json-rpc-middleware + */ +export class NetworkController extends EventEmitter { + /** + * The messenger that NetworkController uses to publish events. + */ + messenger: NetworkControllerMessenger; + + /** + * Observable store containing the provider configuration. + */ + providerStore: ObservableStore; + + /** + * Observable store containing the provider configuration for the previously + * configured network. + */ + previousProviderStore: ObservableStore; + + /** + * Observable store containing the network ID for the current network or null + * if there is no current network. + */ + networkIdStore: ObservableStore; + + /** + * Observable store for the network status. + */ + networkStatusStore: ObservableStore; + + /** + * Observable store for details about the network. + */ + networkDetails: ObservableStore; + + /** + * Observable store for network configurations. + */ + networkConfigurationsStore: ObservableStore; + + /** + * Observable store containing a combination of data from all of the + * individual stores. + */ + store: ComposedStore; + + _provider: SafeEventEmitterProvider | null; + + _blockTracker: PollingBlockTracker | null; + + _providerProxy: SwappableProxy | null; + + _blockTrackerProxy: SwappableProxy | null; + + _infuraProjectId: NetworkControllerOptions['infuraProjectId']; + + _trackMetaMetricsEvent: NetworkControllerOptions['trackMetaMetricsEvent']; + + /** + * Constructs a network controller. + * + * @param options - Options for this constructor. + * @param options.messenger - The NetworkController messenger. + * @param options.state - Initial controller state. + * @param options.infuraProjectId - The Infura project ID. + * @param options.trackMetaMetricsEvent - A method to forward events to the + * {@link MetaMetricsController}. + */ + constructor({ + messenger, + state = {}, + infuraProjectId, + trackMetaMetricsEvent, + }: NetworkControllerOptions) { + super(); + + this.messenger = messenger; + + // create stores + this.providerStore = new ObservableStore( + state.provider || buildDefaultProviderConfigState(), + ); + this.previousProviderStore = new ObservableStore( + this.providerStore.getState(), + ); + 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 || buildDefaultNetworkDetailsState(), + ); + + this.networkConfigurationsStore = new ObservableStore( + state.networkConfigurations || buildDefaultNetworkConfigurationsState(), + ); + + this.store = new ComposedStore({ + provider: this.providerStore, + previousProviderStore: this.previousProviderStore, + networkId: this.networkIdStore, + networkStatus: this.networkStatusStore, + 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; + } + + /** + * Deactivates the controller, stopping any ongoing polling. + * + * In-progress requests will not be aborted. + */ + async destroy(): Promise { + await this._blockTracker?.destroy(); + } + + /** + * Creates the provider and block tracker for the configured network, + * using the provider to gather details about the network. + */ + async initializeProvider(): Promise { + const { type, rpcUrl, chainId } = this.providerStore.getState(); + this._configureProvider({ type, rpcUrl, chainId }); + await this.lookupNetwork(); + } + + /** + * Returns the proxies wrapping the currently set provider and block tracker. + */ + getProviderAndBlockTracker(): { + provider: SwappableProxy | null; + blockTracker: SwappableProxy | null; + } { + const provider = this._providerProxy; + const blockTracker = this._blockTrackerProxy; + return { provider, blockTracker }; + } + + /** + * Determines whether the network supports EIP-1559 by checking whether the + * latest block has a `baseFeePerGas` property, then updates state + * appropriately. + * + * @returns A promise that resolves to true if the network supports EIP-1559 + * and false otherwise. + */ + async getEIP1559Compatibility(): Promise { + 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 { provider } = this.getProviderAndBlockTracker(); + if (!provider) { + // Really we should throw an error if a provider hasn't been initialized + // yet, but that might have undesirable repercussions, so return false for + // now + return false; + } + + const supportsEIP1559 = await this._determineEIP1559Compatibility(provider); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); + return supportsEIP1559; + } + + /** + * Performs side effects after switching to a network. If the network is + * available, updates the network state with the network ID of the network and + * stores whether the network supports EIP-1559; otherwise clears said + * information about the network that may have been previously stored. + * + * @fires infuraIsBlocked if the network is Infura-supported and is blocking + * requests. + * @fires infuraIsUnblocked if the network is Infura-supported and is not + * blocking requests, or if the network is not Infura-supported. + */ + async lookupNetwork(): Promise { + const { chainId, type } = this.providerStore.getState(); + const { provider } = this.getProviderAndBlockTracker(); + let networkChanged = false; + let networkId: NetworkIdState = null; + let supportsEIP1559 = false; + let networkStatus: NetworkStatus; + + if (provider === null) { + log.warn( + 'NetworkController - lookupNetwork aborted due to missing provider', + ); + return; + } + + if (!chainId) { + log.warn( + 'NetworkController - lookupNetwork aborted due to missing chainId', + ); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); + return; + } + + const isInfura = isInfuraProviderType(type); + + const listener = () => { + networkChanged = true; + this.messenger.unsubscribe( + NetworkControllerEventType.NetworkDidChange, + listener, + ); + }; + this.messenger.subscribe( + NetworkControllerEventType.NetworkDidChange, + listener, + ); + + try { + const results = await Promise.all([ + this._getNetworkId(provider), + this._determineEIP1559Compatibility(provider), + ]); + const possibleNetworkId = results[0]; + assertNetworkId(possibleNetworkId); + networkId = possibleNetworkId; + supportsEIP1559 = results[1]; + networkStatus = NetworkStatus.Available; + } catch (error) { + if (isErrorWithCode(error) && isErrorWithMessage(error)) { + 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; + } + } + + 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( + NetworkControllerEventType.NetworkDidChange, + listener, + ); + + this.networkStatusStore.putState(networkStatus); + + if (networkStatus === NetworkStatus.Available) { + this.networkIdStore.putState(networkId); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); + } else { + this._resetNetworkId(); + this._resetNetworkDetails(); + } + + if (isInfura) { + if (networkStatus === NetworkStatus.Available) { + this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked); + } else if (networkStatus === NetworkStatus.Blocked) { + this.messenger.publish(NetworkControllerEventType.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(NetworkControllerEventType.InfuraIsUnblocked); + } + } + + /** + * Switches to the network specified by a network configuration. + * + * @param networkConfigurationId - The unique identifier that refers to a + * previously added network configuration. + * @returns The URL of the RPC endpoint representing the newly switched + * network. + */ + setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string { + 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; + } + + /** + * Switches to an Infura-supported network. + * + * @param type - The shortname of the network. + * @throws if the `type` is "rpc" or if it is not a known Infura-supported + * network. + */ + setProviderType(type: string): void { + assert.notStrictEqual( + type, + NETWORK_TYPES.RPC, + `NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`, + ); + assert.ok( + isInfuraProviderType(type), + `Unknown Infura provider type "${type}".`, + ); + const network = BUILT_IN_INFURA_NETWORKS[type]; + this._setProviderConfig({ + type, + rpcUrl: '', + chainId: network.chainId, + ticker: 'ticker' in network ? network.ticker : 'ETH', + nickname: '', + rpcPrefs: { blockExplorerUrl: network.blockExplorerUrl }, + }); + } + + /** + * Re-initializes the provider and block tracker for the current network. + */ + resetConnection(): void { + this._setProviderConfig(this.providerStore.getState()); + } + + /** + * Switches to the previous network, assuming that the current network is + * different than the initial network (if it is, then this is equivalent to + * calling `resetConnection`). + */ + rollbackToPreviousProvider(): void { + const config = this.previousProviderStore.getState(); + this.providerStore.putState(config); + this._switchNetwork(config); + } + + /** + * Fetches the latest block for the network. + * + * @param provider - A provider, which is guaranteed to be available. + * @returns A promise that either resolves to the block header or null if + * there is no latest block, or rejects with an error. + */ + _getLatestBlock(provider: SafeEventEmitterProvider): Promise { + return new Promise((resolve, reject) => { + const ethQuery = new EthQuery(provider); + ethQuery.sendAsync<['latest', false], Block | null>( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (...args) => { + if (args[0] === null) { + resolve(args[1]); + } else { + reject(args[0]); + } + }, + ); + }); + } + + /** + * Fetches the network ID for the network. + * + * @param provider - A provider, which is guaranteed to be available. + * @returns A promise that either resolves to the network ID, or rejects with + * an error. + */ + async _getNetworkId(provider: SafeEventEmitterProvider): Promise { + const ethQuery = new EthQuery(provider); + return await new Promise((resolve, reject) => { + ethQuery.sendAsync( + { method: 'net_version' }, + (...args) => { + if (args[0] === null) { + resolve(args[1]); + } else { + reject(args[0]); + } + }, + ); + }); + } + + /** + * Clears the stored network ID. + */ + _resetNetworkId(): void { + this.networkIdStore.putState(buildDefaultNetworkIdState()); + } + + /** + * Resets network status to the default ("unknown"). + */ + _resetNetworkStatus(): void { + this.networkStatusStore.putState(buildDefaultNetworkStatusState()); + } + + /** + * Clears details previously stored for the network. + */ + _resetNetworkDetails(): void { + this.networkDetails.putState(buildDefaultNetworkDetailsState()); + } + + /** + * Stores the given provider configuration representing a network in state, + * then uses it to create a new provider for that network. + * + * @param providerConfig - The provider configuration. + */ + _setProviderConfig(providerConfig: ProviderConfiguration): void { + this.previousProviderStore.putState(this.providerStore.getState()); + this.providerStore.putState(providerConfig); + this._switchNetwork(providerConfig); + } + + /** + * 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. + * + * @param provider - A provider, which is guaranteed to be available. + * @returns A promise that resolves to true if the network supports EIP-1559 + * and false otherwise. + */ + async _determineEIP1559Compatibility( + provider: SafeEventEmitterProvider, + ): Promise { + const latestBlock = await this._getLatestBlock(provider); + return latestBlock?.baseFeePerGas !== undefined; + } + + /** + * Executes a series of steps to change the current network: + * + * 1. Notifies subscribers that the network is about to change. + * 2. Clears state associated with the current network. + * 3. Creates a new network client along with a provider for the desired + * network. + * 4. Notifies subscribes that the network has changed. + * + * @param providerConfig - The provider configuration object that specifies + * the new network. + */ + _switchNetwork(providerConfig: ProviderConfiguration): void { + this.messenger.publish(NetworkControllerEventType.NetworkWillChange); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); + this._configureProvider(providerConfig); + this.messenger.publish(NetworkControllerEventType.NetworkDidChange); + this.lookupNetwork(); + } + + /** + * Creates a network client (a stack of middleware along with a provider and + * block tracker) to talk to a network. + * + * @param args - The arguments. + * @param args.type - The shortname of an Infura-supported network (see + * {@link NETWORK_TYPES}). + * @param args.rpcUrl - The URL of the RPC endpoint that represents the + * network. Only used for non-Infura networks. + * @param args.chainId - The chain ID of the network (as per EIP-155). Only + * used for non-Infura-supported networks (as we already know the chain ID of + * any Infura-supported network). + * @throws if the `type` if not a known Infura-supported network. + */ + _configureProvider({ type, rpcUrl, chainId }: ProviderConfiguration): void { + const isInfura = isInfuraProviderType(type); + if (isInfura) { + // infura type-based endpoints + this._configureInfuraProvider({ + type, + infuraProjectId: this._infuraProjectId, + }); + } else if (type === NETWORK_TYPES.RPC && rpcUrl) { + // url-based rpc endpoints + this._configureStandardProvider(rpcUrl, chainId); + } else { + throw new Error( + `NetworkController - _configureProvider - unknown type "${type}"`, + ); + } + } + + /** + * Creates a network client (a stack of middleware along with a provider and + * block tracker) to talk to an Infura-supported network. + * + * @param args - The arguments. + * @param args.type - The shortname of the Infura network (see + * {@link NETWORK_TYPES}). + * @param args.infuraProjectId - An Infura API key. ("Project ID" is a + * now-obsolete term we've retained for backward compatibility.) + */ + _configureInfuraProvider({ + type, + infuraProjectId, + }: { + type: BuiltInInfuraNetwork; + infuraProjectId: NetworkControllerOptions['infuraProjectId']; + }): void { + log.info('NetworkController - configureInfuraProvider', type); + const { provider, blockTracker } = createNetworkClient({ + network: type, + infuraProjectId, + type: NetworkClientType.Infura, + }); + this._setProviderAndBlockTracker({ provider, blockTracker }); + } + + /** + * Creates a network client (a stack of middleware along with a provider and + * block tracker) to talk to a non-Infura-supported network. + * + * @param rpcUrl - The URL of the RPC endpoint that represents the network. + * @param chainId - The chain ID of the network (as per EIP-155). + */ + _configureStandardProvider(rpcUrl: string, chainId: ChainId): void { + log.info('NetworkController - configureStandardProvider', rpcUrl); + const { provider, blockTracker } = createNetworkClient({ + chainId, + rpcUrl, + type: NetworkClientType.Custom, + }); + this._setProviderAndBlockTracker({ provider, blockTracker }); + } + + /** + * Given a provider and a block tracker, updates any proxies pointing to + * these objects that have been previously set, or initializes any proxies + * that have not been previously set. + * + * @param args - The arguments. + * @param args.provider - The provider. + * @param args.blockTracker - The block tracker. + */ + _setProviderAndBlockTracker({ + provider, + blockTracker, + }: { + provider: SafeEventEmitterProvider; + blockTracker: PollingBlockTracker; + }): void { + // 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 + */ + + /** + * Updates an existing network configuration matching the same RPC URL as the + * given network configuration; otherwise adds the network configuration. + * Following the upsert, the `trackMetaMetricsEvent` callback specified + * via the NetworkController constructor will be called to (presumably) create + * a MetaMetrics event. + * + * @param networkConfiguration - The network configuration to upsert. + * @param networkConfiguration.chainId - The chain ID of the network as per + * EIP-155. + * @param networkConfiguration.ticker - The shortname of the currency used by + * the network. + * @param networkConfiguration.nickname - The user-customizable name of the + * network. + * @param networkConfiguration.rpcPrefs - User-customizable details for the + * network. + * @param networkConfiguration.rpcUrl - The URL of the RPC endpoint. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.setActive - Switches to the network specified by + * the given network configuration following the upsert. + * @param additionalArgs.referrer - The site from which the call originated, + * or 'metamask' for internal calls; used for event metrics. + * @param additionalArgs.source - Where the metric event originated (i.e. from + * a dapp or from the network form); used for event metrics. + * @throws if the `chainID` does not match EIP-155 or is too large. + * @throws if `rpcUrl` is not a valid URL. + * @returns The ID for the added or updated network configuration. + */ + upsertNetworkConfiguration( + { + rpcUrl, + chainId, + ticker, + nickname, + rpcPrefs, + }: Omit, + { + setActive = false, + referrer, + source, + }: { + setActive?: boolean; + referrer: string; + source: string; + }, + ): NetworkConfigurationId { + 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 (isErrorWithMessage(e) && 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 || uuid(); + this.networkConfigurationsStore.putState({ + ...networkConfigurations, + [newNetworkConfigurationId]: { + ...newNetworkConfiguration, + id: newNetworkConfigurationId, + }, + }); + + if (!oldNetworkConfigurationId) { + this._trackMetaMetricsEvent({ + event: 'Custom Network Added', + category: MetaMetricsEventCategory.Network, + referrer: { + url: referrer, + }, + properties: { + chain_id: chainId, + symbol: ticker, + source, + }, + }); + } + + if (setActive) { + this.setActiveNetwork(newNetworkConfigurationId); + } + + return newNetworkConfigurationId; + } + + /** + * Removes a network configuration from state. + * + * @param networkConfigurationId - The unique id for the network configuration + * to remove. + */ + removeNetworkConfiguration( + networkConfigurationId: NetworkConfigurationId, + ): void { + const networkConfigurations = { + ...this.networkConfigurationsStore.getState(), + }; + delete networkConfigurations[networkConfigurationId]; + this.networkConfigurationsStore.putState(networkConfigurations); + } +} diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 51f2ba5a0f13..6fcb1bac5f8c 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -4,7 +4,7 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { TokenListController } from '@metamask/assets-controllers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import PreferencesController from './preferences'; -import NetworkController from './network'; +import { NetworkController } from './network'; describe('preferences controller', function () { let preferencesController; diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index c3026e0434f3..ce5e85d0ddb1 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -52,7 +52,10 @@ import { determineTransactionType, isEIP1559Transaction, } from '../../../../shared/modules/transaction.utils'; -import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { + ORIGIN_METAMASK, + MESSAGE_TYPE, +} from '../../../../shared/constants/app'; import { calcGasTotal, getSwapsTokensReceivedFromTxMeta, @@ -156,6 +159,7 @@ export default class TransactionController extends EventEmitter { this.getAccountType = opts.getAccountType; this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails; this.securityProviderRequest = opts.securityProviderRequest; + this.messagingSystem = opts.messenger; this.memStore = new ObservableStore({}); @@ -798,6 +802,7 @@ export default class TransactionController extends EventEmitter { this.txStateManager.getTransactionWithActionId(actionId); if (existingTxMeta) { this.emit('newUnapprovedTx', existingTxMeta); + this._requestApproval(existingTxMeta); existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta); return existingTxMeta; } @@ -870,6 +875,7 @@ export default class TransactionController extends EventEmitter { this.addTransaction(txMeta); this.emit('newUnapprovedTx', txMeta); + this._requestApproval(txMeta); txMeta = await this.addTransactionGasDefaults(txMeta); @@ -1355,6 +1361,7 @@ export default class TransactionController extends EventEmitter { try { // approve this.txStateManager.setTxStatusApproved(txId); + this._acceptApproval(txMeta); // get next nonce const fromAddress = txMeta.txParams.from; // wait for a nonce @@ -1734,6 +1741,7 @@ export default class TransactionController extends EventEmitter { async cancelTransaction(txId, actionId) { const txMeta = this.txStateManager.getTransaction(txId); this.txStateManager.setTxStatusRejected(txId); + this._rejectApproval(txMeta); this._trackTransactionMetricsEvent( txMeta, TransactionMetaMetricsEvent.rejected, @@ -2596,4 +2604,54 @@ export default class TransactionController extends EventEmitter { }, ); } + + _requestApproval(txMeta) { + const id = this._getApprovalId(txMeta); + const { origin } = txMeta; + const type = MESSAGE_TYPE.TRANSACTION; + const requestData = { txId: txMeta.id }; + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + requestData, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + _acceptApproval(txMeta) { + const id = this._getApprovalId(txMeta); + + try { + this.messagingSystem.call('ApprovalController:acceptRequest', id); + } catch (error) { + log.error('Failed to accept transaction approval request', error); + } + } + + _rejectApproval(txMeta) { + const id = this._getApprovalId(txMeta); + + try { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + id, + new Error('Rejected'), + ); + } catch (error) { + log.error('Failed to reject transaction approval request', error); + } + } + + _getApprovalId(txMeta) { + return String(txMeta.id); + } } diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index c16f1fa2c7eb..601b1fb886d6 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -29,7 +29,10 @@ import { GasRecommendations, } from '../../../../shared/constants/gas'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; -import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { + MESSAGE_TYPE, + 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 '.'; @@ -52,7 +55,8 @@ describe('Transaction Controller', function () { fromAccount, fragmentExists, networkStatusStore, - getCurrentChainId; + getCurrentChainId, + messengerMock; beforeEach(function () { fragmentExists = false; @@ -76,6 +80,7 @@ describe('Transaction Controller', function () { blockTrackerStub.getLatestBlock = noop; getCurrentChainId = sinon.stub().callsFake(() => currentChainId); + messengerMock = { call: sinon.stub().returns(Promise.resolve()) }; txController = new TransactionController({ provider, @@ -108,6 +113,7 @@ describe('Transaction Controller', function () { getAccountType: () => 'MetaMask', getDeviceModel: () => 'N/A', securityProviderRequest: () => undefined, + messenger: messengerMock, }); txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }); @@ -489,6 +495,67 @@ describe('Transaction Controller', function () { { message: 'MetaMask is having trouble connecting to the network' }, ); }); + + it('should create an approval request', async function () { + const txMeta = await txController.addUnapprovedTransaction( + undefined, + { + from: selectedAddress, + to: recipientAddress, + }, + ORIGIN_METAMASK, + ); + + assert.equal(messengerMock.call.callCount, 1); + assert.deepEqual(messengerMock.call.getCall(0).args, [ + 'ApprovalController:addRequest', + { + id: String(txMeta.id), + origin: ORIGIN_METAMASK, + requestData: { txId: txMeta.id }, + type: MESSAGE_TYPE.TRANSACTION, + }, + true, // Show popup + ]); + }); + + it('should still create an approval request when called twice with same actionId', async function () { + await txController.addUnapprovedTransaction( + undefined, + { + from: selectedAddress, + to: recipientAddress, + }, + ORIGIN_METAMASK, + undefined, + undefined, + '12345', + ); + + const secondTxMeta = await txController.addUnapprovedTransaction( + undefined, + { + from: selectedAddress, + to: recipientAddress, + }, + undefined, + undefined, + undefined, + '12345', + ); + + assert.equal(messengerMock.call.callCount, 2); + assert.deepEqual(messengerMock.call.getCall(1).args, [ + 'ApprovalController:addRequest', + { + id: String(secondTxMeta.id), + origin: ORIGIN_METAMASK, + requestData: { txId: secondTxMeta.id }, + type: MESSAGE_TYPE.TRANSACTION, + }, + true, // Show popup + ]); + }); }); describe('#createCancelTransaction', function () { @@ -997,9 +1064,11 @@ describe('Transaction Controller', function () { }); describe('#approveTransaction', function () { - it('does not overwrite set values', async function () { - const originalValue = '0x01'; - const txMeta = { + let originalValue, txMeta, signStub, pubStub; + + beforeEach(function () { + originalValue = '0x01'; + txMeta = { id: '1', status: TransactionStatus.unapproved, metamaskNetworkId: currentNetworkId, @@ -1019,17 +1088,22 @@ describe('Transaction Controller', function () { providerResultStub.eth_gasPrice = wrongValue; providerResultStub.eth_estimateGas = '0x5209'; - const signStub = sinon + signStub = sinon .stub(txController, 'signTransaction') .callsFake(() => Promise.resolve()); - const pubStub = sinon - .stub(txController, 'publishTransaction') - .callsFake(() => { - txController.setTxHash('1', originalValue); - txController.txStateManager.setTxStatusSubmitted('1'); - }); + pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { + txController.setTxHash('1', originalValue); + txController.txStateManager.setTxStatusSubmitted('1'); + }); + }); + + afterEach(function () { + signStub.restore(); + pubStub.restore(); + }); + it('does not overwrite set values', async function () { await txController.approveTransaction(txMeta.id); const result = txController.txStateManager.getTransaction(txMeta.id); const params = result.txParams; @@ -1042,8 +1116,21 @@ describe('Transaction Controller', function () { TransactionStatus.submitted, 'should have reached the submitted status.', ); - signStub.restore(); - pubStub.restore(); + }); + + it('should accept the approval request', async function () { + await txController.approveTransaction(txMeta.id); + + assert.equal(messengerMock.call.callCount, 1); + assert.deepEqual(messengerMock.call.getCall(0).args, [ + 'ApprovalController:acceptRequest', + txMeta.id, + ]); + }); + + it('should not throw if accepting approval request throws', async function () { + messengerMock.call.throws(); + await txController.approveTransaction(txMeta.id); }); }); @@ -1108,7 +1195,7 @@ describe('Transaction Controller', function () { }); describe('#cancelTransaction', function () { - it('should emit a status change to rejected', function (done) { + beforeEach(function () { txController.txStateManager._addTransactionsToState([ { id: 0, @@ -1181,7 +1268,9 @@ describe('Transaction Controller', function () { history: [{}], }, ]); + }); + it('should emit a status change to rejected', function (done) { txController.once('tx:status-update', (txId, status) => { try { assert.equal( @@ -1198,6 +1287,22 @@ describe('Transaction Controller', function () { txController.cancelTransaction(0); }); + + it('should reject the approval request', function () { + txController.cancelTransaction(0); + + assert.equal(messengerMock.call.callCount, 1); + assert.deepEqual(messengerMock.call.getCall(0).args, [ + 'ApprovalController:rejectRequest', + '0', + new Error('Rejected'), + ]); + }); + + it('should not throw if rejecting approval request throws', async function () { + messengerMock.call.throws(); + txController.cancelTransaction(0); + }); }); describe('#createSpeedUpTransaction', function () { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 28a7311c0e16..7315f301f574 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -143,8 +143,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { setupMultiplex } from './lib/stream-utils'; import EnsController from './controllers/ens'; -import NetworkController, { - NetworkControllerEventTypes, +import { + NetworkController, + NetworkControllerEventType, } from './controllers/network'; import PreferencesController from './controllers/preferences'; import AppStateController from './controllers/app-state'; @@ -263,7 +264,7 @@ export default class MetamaskController extends EventEmitter { const networkControllerMessenger = this.controllerMessenger.getRestricted({ name: 'NetworkController', - allowedEvents: Object.values(NetworkControllerEventTypes), + allowedEvents: Object.values(NetworkControllerEventType), }); this.networkController = new NetworkController({ messenger: networkControllerMessenger, @@ -310,11 +311,11 @@ export default class MetamaskController extends EventEmitter { initLangCode: opts.initLangCode, onInfuraIsBlocked: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.InfuraIsBlocked, + NetworkControllerEventType.InfuraIsBlocked, ), onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.InfuraIsUnblocked, + NetworkControllerEventType.InfuraIsUnblocked, ), tokenListController: this.tokenListController, provider: this.provider, @@ -452,7 +453,7 @@ export default class MetamaskController extends EventEmitter { preferencesStore: this.preferencesController.store, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), getNetworkIdentifier: () => { const { type, rpcUrl } = @@ -491,7 +492,7 @@ export default class MetamaskController extends EventEmitter { // onNetworkDidChange onNetworkStateChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( @@ -609,7 +610,7 @@ export default class MetamaskController extends EventEmitter { this.networkController.store.getState().provider.chainId, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), }); @@ -621,7 +622,7 @@ export default class MetamaskController extends EventEmitter { blockTracker: this.blockTracker, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), getCurrentChainId: () => this.networkController.store.getState().provider.chainId, @@ -1007,8 +1008,15 @@ export default class MetamaskController extends EventEmitter { getDeviceModel: this.getDeviceModel.bind(this), getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this), + messenger: this.controllerMessenger.getRestricted({ + name: 'TransactionController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), }); - this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation()); this.txController.on(`tx:status-update`, async (txId, status) => { if ( @@ -1099,7 +1107,7 @@ export default class MetamaskController extends EventEmitter { }); networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, async () => { const { ticker } = this.networkController.store.getState().provider; try { @@ -1145,7 +1153,7 @@ export default class MetamaskController extends EventEmitter { networkController: this.networkController, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), provider: this.provider, getProviderConfig: () => this.networkController.store.getState().provider, @@ -1188,7 +1196,7 @@ export default class MetamaskController extends EventEmitter { // ensure accountTracker updates balances after network change networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, () => { this.accountTracker._updateAccounts(); }, @@ -1196,7 +1204,7 @@ export default class MetamaskController extends EventEmitter { // clear unapproved transactions and messages when the network will change networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkWillChange, + NetworkControllerEventType.NetworkWillChange, () => { this.txController.txStateManager.clearUnapprovedTxs(); this.encryptionPublicKeyManager.clearUnapproved(); diff --git a/app/scripts/migrations/084.test.js b/app/scripts/migrations/084.test.js new file mode 100644 index 000000000000..e93b561e5886 --- /dev/null +++ b/app/scripts/migrations/084.test.js @@ -0,0 +1,254 @@ +import { v4 } from 'uuid'; +import { migrate, version } from './084'; + +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + +describe('migration #84', () => { + beforeEach(() => { + v4.mockImplementationOnce(() => 'network-configuration-id-1') + .mockImplementationOnce(() => 'network-configuration-id-2') + .mockImplementationOnce(() => 'network-configuration-id-3') + .mockImplementationOnce(() => 'network-configuration-id-4'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { + const oldStorage = { + meta: { + version: 83, + }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version, + }); + }); + + it('should use the key of the networkConfigurations object to set the id of each network configuration', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + networkConfigurations: { + 'network-configuration-id-1': { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + 'network-configuration-id-2': { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + }, + 'network-configuration-id-3': { + chainId: '0x4e454152', + nickname: 'Aurora Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://aurorascan.dev/', + }, + rpcUrl: + 'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'Aurora ETH', + }, + 'network-configuration-id-4': { + chainId: '0x38', + nickname: + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + }, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: { + networkConfigurations: { + 'network-configuration-id-1': { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + id: 'network-configuration-id-1', + }, + 'network-configuration-id-2': { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + id: 'network-configuration-id-2', + }, + 'network-configuration-id-3': { + chainId: '0x4e454152', + nickname: 'Aurora Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://aurorascan.dev/', + }, + rpcUrl: + 'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'Aurora ETH', + id: 'network-configuration-id-3', + }, + 'network-configuration-id-4': { + chainId: '0x38', + nickname: + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + id: 'network-configuration-id-4', + }, + }, + }, + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController is undefined', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController is not an object', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: false, + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: false, + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: undefined, + }, + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: undefined, + }, + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: {}, + }, + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: {}, + }, + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); +}); diff --git a/app/scripts/migrations/084.ts b/app/scripts/migrations/084.ts new file mode 100644 index 000000000000..4ae81cdc8680 --- /dev/null +++ b/app/scripts/migrations/084.ts @@ -0,0 +1,58 @@ +import { cloneDeep } from 'lodash'; +import { isObject } from '@metamask/utils'; + +export const version = 84; + +/** + * Ensure that each networkConfigurations object in state.NetworkController.networkConfigurations has an + * `id` property which matches the key pointing that object + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if (!isObject(state.NetworkController)) { + return state; + } + const { NetworkController } = state; + + if (!isObject(NetworkController.networkConfigurations)) { + return state; + } + + const { networkConfigurations } = NetworkController; + + const newNetworkConfigurations: Record> = {}; + + for (const networkConfigurationId of Object.keys(networkConfigurations)) { + const networkConfiguration = networkConfigurations[networkConfigurationId]; + if (!isObject(networkConfiguration)) { + return state; + } + newNetworkConfigurations[networkConfigurationId] = { + ...networkConfiguration, + id: networkConfigurationId, + }; + } + + return { + ...state, + NetworkController: { + ...NetworkController, + networkConfigurations: newNetworkConfigurations, + }, + }; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index c3f8e515f610..54a09c2b4e10 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -87,6 +87,7 @@ import m080 from './080'; import * as m081 from './081'; import * as m082 from './082'; import * as m083 from './083'; +import * as m084 from './084'; const migrations = [ m002, @@ -171,6 +172,7 @@ const migrations = [ m081, m082, m083, + m084, ]; export default migrations; diff --git a/jest.config.js b/jest.config.js index ba0c5219ea64..21d41a96d670 100644 --- a/jest.config.js +++ b/jest.config.js @@ -51,7 +51,7 @@ module.exports = { '/ui/**/*.test.(js|ts|tsx)', '/development/fitness-functions/**/*.test.(js|ts|tsx)', ], - testTimeout: 2500, + testTimeout: 5500, // We have to specify the environment we are running in, which is jsdom. The // default is 'node'. This can be modified *per file* using a comment at the // head of the file. So it may be worthwhile to switch to 'node' in any diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 7d359e5227d4..ed13fb1b9a5d 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1193,7 +1193,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1201,11 +1201,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 8fc8f7bcd5fa..0d916496363f 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -1265,7 +1265,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1273,11 +1273,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 8fc8f7bcd5fa..0d916496363f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1265,7 +1265,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1273,11 +1273,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 7d359e5227d4..ed13fb1b9a5d 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1193,7 +1193,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1201,11 +1201,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/package.json b/package.json index 2b18159833f8..80df52fffead 100644 --- a/package.json +++ b/package.json @@ -242,14 +242,14 @@ "@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/etherscan-link": "^2.2.0", - "@metamask/gas-fee-controller": "^1.0.0", + "@metamask/gas-fee-controller": "^3.0.0", "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", "@metamask/message-manager": "^2.1.0", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/notification-controller": "^1.0.0", - "@metamask/obs-store": "^8.0.0", + "@metamask/obs-store": "^8.1.0", "@metamask/permission-controller": "^3.1.0", "@metamask/phishing-controller": "^2.0.0", "@metamask/post-message-stream": "^6.0.0", diff --git a/shared/constants/app.ts b/shared/constants/app.ts index 54a39496a0ea..c8be0573e075 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -53,6 +53,7 @@ export const MESSAGE_TYPE = { PERSONAL_SIGN: 'personal_sign', SEND_METADATA: 'metamask_sendDomainMetadata', SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain', + TRANSACTION: 'transaction', WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions', WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET_LEGACY: 'metamask_watchAsset', diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 1f20538c3d3b..ab5115bee8aa 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -238,7 +238,7 @@ export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, NETWORK_TYPES.GOERLI, NETWORK_TYPES.SEPOLIA, -]; +] as const; export const TEST_CHAINS = [ CHAIN_IDS.GOERLI, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 93d4515f4637..41d995b9e00c 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1535,7 +1535,18 @@ "origin": "tmashuang.github.io" } ], - "desktopEnabled": false + "desktopEnabled": false, + "pendingApprovals": { + "testApprovalId": { + "id": "testApprovalId", + "time": 1528133319641, + "origin": "metamask", + "type": "transaction", + "requestData": { "txId": "testTransactionId" }, + "requestState": { "test": "value" } + } + }, + "pendingApprovalCount": 1 }, "send": { "amountMode": "INPUT", diff --git a/test/e2e/nft/erc721-interaction.spec.js b/test/e2e/nft/erc721-interaction.spec.js index 6e9a9e959669..2d7d3abe9b43 100644 --- a/test/e2e/nft/erc721-interaction.spec.js +++ b/test/e2e/nft/erc721-interaction.spec.js @@ -62,9 +62,7 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.findElement('.list-item__title'); - const completedTxText = await completedTx.getText(); - assert.equal(completedTxText, 'Send Token'); + await driver.findElement({ text: 'Send TDC' }); }, ); }); diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index b520f1bcf73d..5e1addcc5ca1 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -119,16 +119,16 @@ describe('Create token, approve token and approve token without gas', function ( ); await driver.clickElement({ - text: 'Verify contract details', + text: 'Verify third-party details', css: '.token-allowance-container__verify-link', }); const modalTitle = await driver.waitForSelector({ - text: 'Contract details', + text: 'Third-party details', tag: 'h5', }); - assert.equal(await modalTitle.getText(), 'Contract details'); + assert.equal(await modalTitle.getText(), 'Third-party details'); await driver.clickElement({ text: 'Got it', diff --git a/test/e2e/tests/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js index 9b508d37b328..62ac5cd786c5 100644 --- a/test/e2e/tests/signature-request.spec.js +++ b/test/e2e/tests/signature-request.spec.js @@ -60,7 +60,7 @@ describe('Sign Typed Data V4 Signature Request', function () { assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); verifyContractDetailsButton.click(); - await driver.findElement({ text: 'Contract details', tag: 'h5' }); + await driver.findElement({ text: 'Third-party details', tag: 'h5' }); await driver.findElement('[data-testid="recipient"]'); await driver.clickElement({ text: 'Got it', tag: 'button' }); @@ -142,7 +142,7 @@ describe('Sign Typed Data V3 Signature Request', function () { assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); verifyContractDetailsButton.click(); - await driver.findElement({ text: 'Contract details', tag: 'h5' }); + await driver.findElement({ text: 'Third-party details', tag: 'h5' }); await driver.findElement('[data-testid="recipient"]'); await driver.clickElement({ text: 'Got it', tag: 'button' }); diff --git a/types/eth-query.d.ts b/types/eth-query.d.ts new file mode 100644 index 000000000000..726300f68176 --- /dev/null +++ b/types/eth-query.d.ts @@ -0,0 +1,50 @@ +declare module 'eth-query' { + // What it says on the tin. We omit `null` because confusingly, this is used + // for a successful response to indicate a lack of an error. + type EverythingButNull = + | string + | number + | boolean + | object + | symbol + | undefined; + + type ProviderSendAsyncResponse = { + error?: { message: string }; + result?: Result; + }; + + type ProviderSendAsyncCallback = ( + error: unknown, + response: ProviderSendAsyncResponse, + ) => void; + + type Provider = { + sendAsync( + payload: SendAsyncPayload, + callback: ProviderSendAsyncCallback, + ): void; + }; + + type SendAsyncPayload = { + id: number; + jsonrpc: '2.0'; + method: string; + params: Params; + }; + + type SendAsyncCallback = ( + ...args: + | [error: EverythingButNull, result: undefined] + | [error: null, result: Result] + ) => void; + + export default class EthQuery { + constructor(provider: Provider); + + sendAsync( + opts: Partial>, + callback: SendAsyncCallback, + ): void; + } +} diff --git a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js index 06c80a19a27a..d2fdcd7d9b54 100644 --- a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js +++ b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js @@ -1,6 +1,5 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { object } from '@storybook/addon-knobs'; import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui'; import configureStore from '../../../../store/store'; import testData from '../../../../../.storybook/test-data'; @@ -10,8 +9,13 @@ const store = configureStore(testData); export default { title: 'Components/App/SnapUIRenderer', - + component: SnapUIRenderer, decorators: [(story) => {story()}], + argTypes: { + data: { + control: 'object', + }, + }, }; const DATA = panel([ @@ -22,13 +26,18 @@ const DATA = panel([ copyable('Text you can copy'), ]); -export const DefaultStory = () => ( - +export const DefaultStory = (args) => ( + ); -export const ErrorStory = () => ( - +DefaultStory.args = { + data: DATA, +}; + +export const ErrorStory = (args) => ( + ); + +ErrorStory.args = { + data: 'foo', +}; diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js index 16e86396a497..c08df6016df1 100644 --- a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js @@ -170,8 +170,9 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) { onMouseDown={onMouseDown} onMouseUp={onMouseUp} className="hold-to-reveal-button__button-hold" + textProps={{ display: DISPLAY.FLEX, alignItems: AlignItems.center }} > - + {renderPreCompleteContent()} {renderPostCompleteContent()} diff --git a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap index d1e6c7708ee2..1892c6ec4670 100644 --- a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap +++ b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap @@ -224,7 +224,7 @@ exports[`Signature Request Component render should match snapshot when we are us
- Verify contract details + Verify third-party details
@@ -999,7 +999,7 @@ exports[`Signature Request Component render should match snapshot when we want t
- Verify contract details + Verify third-party details
diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index c9a3a939b72f..8d5f7bc3df90 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -152,7 +152,7 @@ export function useTransactionDisplayData(transactionGroup) { async function getAndSetAssetDetails() { if (isTokenCategory && !token) { const assetDetails = await getAssetDetails( - recipientAddress, + to, senderAddress, initialTransaction?.txParams?.data, knownNfts, @@ -168,6 +168,7 @@ export function useTransactionDisplayData(transactionGroup) { senderAddress, initialTransaction?.txParams?.data, knownNfts, + to, ]); if (currentAssetDetails) { token = { diff --git a/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap b/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap index e7a265e1a976..5d1310891112 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap +++ b/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap @@ -57,7 +57,7 @@ exports[`ConfirmApproveContent Component should render Confirm approve page corr role="button" tabindex="0" > - Verify contract details + Verify third-party details
- Verify contract details + Verify third-party details
- Verify contract details + Verify third-party details
- Verify contract details + Verify third-party details
{ 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', @@ -119,7 +119,7 @@ describe('ConfirmApproveContent Component', () => { 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', @@ -181,7 +181,7 @@ describe('ConfirmApproveContent Component', () => { 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', @@ -239,7 +239,7 @@ describe('ConfirmApproveContent Component', () => { 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', diff --git a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap index fbf3c10c168d..ab888b4cf5a3 100644 --- a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap +++ b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap @@ -223,7 +223,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
- Verify contract details + Verify third-party details
diff --git a/ui/pages/swaps/import-token/import-token.js b/ui/pages/swaps/import-token/import-token.js index 4e014aef0d38..eb8983262d4c 100644 --- a/ui/pages/swaps/import-token/import-token.js +++ b/ui/pages/swaps/import-token/import-token.js @@ -5,10 +5,10 @@ import UrlIcon from '../../../components/ui/url-icon'; import Popover from '../../../components/ui/popover'; import Button from '../../../components/ui/button'; import Box from '../../../components/ui/box'; -import Typography from '../../../components/ui/typography'; +import { Text } from '../../../components/component-library'; import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; import { - TypographyVariant, + TextVariant, FONT_WEIGHT, AlignItems, DISPLAY, @@ -62,21 +62,26 @@ export default function ImportToken({ fallbackClassName="import-token__token-icon" name={tokenForImport.symbol} /> - {tokenForImport.name || ''} - - {t('contract')}: - + + {t('contract')}: + + {tokenForImport.address || ''} - +
); diff --git a/ui/pages/swaps/import-token/index.scss b/ui/pages/swaps/import-token/index.scss index 5a8067ce87f8..e10106e4b0f7 100644 --- a/ui/pages/swaps/import-token/index.scss +++ b/ui/pages/swaps/import-token/index.scss @@ -14,6 +14,7 @@ border-radius: 8px; background-color: var(--color-background-alternative); padding: 5px 10px; + font-size: 0.75rem; } &__token-icon { diff --git a/ui/pages/token-allowance/index.scss b/ui/pages/token-allowance/index.scss index 057be3d10951..836625da4f00 100644 --- a/ui/pages/token-allowance/index.scss +++ b/ui/pages/token-allowance/index.scss @@ -16,8 +16,8 @@ a.token-allowance-container__verify-link { width: fit-content; - margin-inline-start: 96px; - margin-inline-end: 96px; + margin-inline-start: auto; + margin-inline-end: auto; padding: 0; } diff --git a/ui/pages/token-allowance/token-allowance.test.js b/ui/pages/token-allowance/token-allowance.test.js index a438caabbebd..a8f22dc39ef9 100644 --- a/ui/pages/token-allowance/token-allowance.test.js +++ b/ui/pages/token-allowance/token-allowance.test.js @@ -237,16 +237,16 @@ describe('TokenAllowancePage', () => { expect(getByText('Set a spending cap for your')).toBeInTheDocument(); }); - it('should click Verify contract details and show popup Contract details, then close popup', () => { + it('should click Verify third-party details and show popup Third-party details, then close popup', () => { const { getByText } = renderWithProvider( , store, ); - const verifyContractDetails = getByText('Verify contract details'); - fireEvent.click(verifyContractDetails); + const verifyThirdPartyDetails = getByText('Verify third-party details'); + fireEvent.click(verifyThirdPartyDetails); - expect(getByText('Contract details')).toBeInTheDocument(); + expect(getByText('Third-party details')).toBeInTheDocument(); const gotIt = getByText('Got it'); fireEvent.click(gotIt); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index bbe5e53e85b9..b4e04c7f611f 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -484,21 +484,14 @@ export function getCurrentCurrency(state) { export function getTotalUnapprovedCount(state) { const { - unapprovedMsgCount = 0, - unapprovedPersonalMsgCount = 0, unapprovedDecryptMsgCount = 0, unapprovedEncryptionPublicKeyMsgCount = 0, - unapprovedTypedMessagesCount = 0, pendingApprovalCount = 0, } = state.metamask; return ( - unapprovedMsgCount + - unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + - unapprovedTypedMessagesCount + - getUnapprovedTxCount(state) + pendingApprovalCount + getSuggestedAssetCount(state) ); diff --git a/yarn.lock b/yarn.lock index 6009f9a96090..0921937e9c5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3984,13 +3984,13 @@ __metadata: languageName: node linkType: hard -"@metamask/gas-fee-controller@npm:^1.0.0": - version: 1.0.0 - resolution: "@metamask/gas-fee-controller@npm:1.0.0" +"@metamask/gas-fee-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/gas-fee-controller@npm:3.0.0" dependencies: - "@metamask/base-controller": ~1.0.0 - "@metamask/controller-utils": ~1.0.0 - "@metamask/network-controller": ~1.0.0 + "@metamask/base-controller": ^1.1.2 + "@metamask/controller-utils": ^2.0.0 + "@metamask/network-controller": ^3.0.0 "@types/uuid": ^8.3.0 babel-runtime: ^6.26.0 eth-query: ^2.1.2 @@ -3998,7 +3998,9 @@ __metadata: ethjs-unit: ^0.1.6 immer: ^9.0.6 uuid: ^8.3.2 - checksum: fef5255532a6cd5325ddfbbfec11140e6629c011a8cc6b126672ef7a6e93a327d059935cdc6fc7089562f3277fb70541b5ea54cd31c0e5b350ceebbe73d5d59f + peerDependencies: + "@metamask/network-controller": ^3.0.0 + checksum: 8cdd43a265094dd5e41f0094c278cde351d290446711e6b39de26f842faa993c050e5506cafe8d1c2fb0c4ee3f0f97c5af5fa6528de10e76d071b56fb9673da8 languageName: node linkType: hard @@ -4074,9 +4076,9 @@ __metadata: languageName: node linkType: hard -"@metamask/network-controller@npm:^4.0.0": - version: 4.0.0 - resolution: "@metamask/network-controller@npm:4.0.0" +"@metamask/network-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/network-controller@npm:3.0.0" dependencies: "@metamask/base-controller": ^1.1.2 "@metamask/controller-utils": ^2.0.0 @@ -4086,23 +4088,23 @@ __metadata: eth-query: ^2.1.2 immer: ^9.0.6 web3-provider-engine: ^16.0.3 - checksum: 19dfa74cefc435f5205020c68b948956c52689cdfaa153dc37d116e866f61903396b4b19055975d6fc9ab4185b34e87a641eba8aebb864e9161ed5e561b35263 + checksum: 3ae56a252c11dbd6dc843f9db8b30768d2475afd499c99bdccdc850517031b447bab9ca4f6647da7e64c7a0efd61d029f59a89e4ec702e34a99733dd8e7f93ff languageName: node linkType: hard -"@metamask/network-controller@npm:~1.0.0": - version: 1.0.0 - resolution: "@metamask/network-controller@npm:1.0.0" +"@metamask/network-controller@npm:^4.0.0": + version: 4.0.0 + resolution: "@metamask/network-controller@npm:4.0.0" dependencies: - "@metamask/base-controller": ~1.0.0 - "@metamask/controller-utils": ~1.0.0 + "@metamask/base-controller": ^1.1.2 + "@metamask/controller-utils": ^2.0.0 async-mutex: ^0.2.6 babel-runtime: ^6.26.0 eth-json-rpc-infura: ^5.1.0 eth-query: ^2.1.2 immer: ^9.0.6 web3-provider-engine: ^16.0.3 - checksum: a138943fecc27630e6fe392b9d237405e61b55e17b9dcfc7c434ccc59582fc775aec54e765c2e98f2b1579f760c7d163156450184172128079ce3c4d8e4bc725 + checksum: 19dfa74cefc435f5205020c68b948956c52689cdfaa153dc37d116e866f61903396b4b19055975d6fc9ab4185b34e87a641eba8aebb864e9161ed5e561b35263 languageName: node linkType: hard @@ -4150,13 +4152,13 @@ __metadata: languageName: node linkType: hard -"@metamask/obs-store@npm:^8.0.0": - version: 8.0.0 - resolution: "@metamask/obs-store@npm:8.0.0" +"@metamask/obs-store@npm:^8.1.0": + version: 8.1.0 + resolution: "@metamask/obs-store@npm:8.1.0" dependencies: "@metamask/safe-event-emitter": ^2.0.0 through2: ^2.0.3 - checksum: 232362e65a3563f0bd3299cec48f5adb37e68d4f066b7de90f2b044480d3b16c2d918c12d672c825e1d9b55344ae818fb8494d91129e4613555097653b9bb887 + checksum: 92356067fa3517526d656f2f0bdfbc4d39f65e27fb30d84240cfc9c1aa9cd5d743498952df18ed8efbb8887b6cc1bc1fab37bde3fb0fc059539e0dfcc67ff86f languageName: node linkType: hard @@ -24299,14 +24301,14 @@ __metadata: "@metamask/eth-token-tracker": ^4.0.0 "@metamask/etherscan-link": ^2.2.0 "@metamask/forwarder": ^1.1.0 - "@metamask/gas-fee-controller": ^1.0.0 + "@metamask/gas-fee-controller": ^3.0.0 "@metamask/jazzicon": ^2.0.0 "@metamask/key-tree": ^7.0.0 "@metamask/logo": ^3.1.1 "@metamask/message-manager": ^2.1.0 "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/notification-controller": ^1.0.0 - "@metamask/obs-store": ^8.0.0 + "@metamask/obs-store": ^8.1.0 "@metamask/permission-controller": ^3.1.0 "@metamask/phishing-controller": ^2.0.0 "@metamask/phishing-warning": ^2.1.0 @@ -34616,14 +34618,14 @@ __metadata: linkType: hard "vm2@npm:^3.9.3": - version: 3.9.11 - resolution: "vm2@npm:3.9.11" + version: 3.9.15 + resolution: "vm2@npm:3.9.15" dependencies: acorn: ^8.7.0 acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: aab39e6e4b59146d24abacd79f490e854a6e058a8b23d93d2be5aca7720778e2605d2cc028ccc4a5f50d3d91b0c38be9a6247a80d2da1a6de09425cc437770b4 + checksum: 1df70d5a88173651c0062901aba67e5edfeeb3f699fe6c305f5efb6a5a7391e5724cbf98a6516600b65016c6824dc07cc79947ea4222f8537ae1d9ce0b730ad7 languageName: node linkType: hard