From 23ca4460cfe0f0150147e4df757efd93fcad5fe6 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 9 Mar 2023 15:00:28 -0600 Subject: [PATCH] Migrate network configurations (previously `frequentRpcListDetail`) from `PreferencesController` to `NetworkController` (#17421) --- .storybook/test-data.js | 33 +- app/scripts/background.js | 2 +- app/scripts/controllers/backup.js | 14 +- app/scripts/controllers/backup.test.js | 154 +- app/scripts/controllers/metametrics.js | 21 +- app/scripts/controllers/metametrics.test.js | 48 +- .../controllers/network/network-controller.js | 174 +- .../network/network-controller.test.js | 2014 ++++++++++++++--- app/scripts/controllers/preferences.js | 64 - app/scripts/controllers/preferences.test.js | 60 - app/scripts/first-time-state.js | 12 - .../handlers/add-ethereum-chain.js | 94 +- .../handlers/switch-ethereum-chain.js | 16 +- .../metamask-controller.actions.test.js | 21 - app/scripts/metamask-controller.js | 263 +-- app/scripts/metamask-controller.test.js | 215 +- app/scripts/migrations/082.test.js | 598 +++++ app/scripts/migrations/082.ts | 76 + app/scripts/migrations/index.js | 2 + shared/constants/metametrics.js | 1 + shared/constants/network.ts | 3 + test/data/mock-state.json | 9 +- test/e2e/fixture-builder.js | 38 +- test/e2e/tests/add-custom-network.spec.js | 19 +- test/e2e/tests/backup-restore.spec.js | 2 +- test/e2e/tests/custom-rpc-history.spec.js | 20 +- test/jest/mock-store.js | 2 +- ui/components/app/add-network/add-network.js | 37 +- .../app/add-network/add-network.test.js | 34 +- .../app/dropdowns/network-dropdown.js | 144 +- .../app/dropdowns/network-dropdown.test.js | 27 +- ui/components/app/menu-bar/menu-bar.test.js | 2 +- .../account-details-modal.test.js | 6 +- .../confirm-delete-network.component.js | 4 +- .../confirm-delete-network.container.js | 5 +- .../confirm-delete-network.test.js | 6 +- .../app/network-display/network-display.js | 5 +- ...transaction-activity-log.container.test.js | 14 +- ui/ducks/app/app.ts | 35 +- ui/ducks/metamask/metamask.js | 2 +- .../confirm-signature-request/index.test.js | 2 +- .../confirmation-network-switch.js | 6 +- .../templates/add-ethereum-chain.js | 18 +- ui/pages/confirmation/templates/index.js | 7 +- .../templates/switch-ethereum-chain.js | 2 +- ui/pages/home/home.component.js | 46 +- ui/pages/home/home.container.js | 23 +- ui/pages/import-token/import-token.stories.js | 6 +- ui/pages/import-token/import-token.test.js | 2 +- .../privacy-settings/privacy-settings.js | 6 +- .../privacy-settings/privacy-settings.test.js | 2 +- .../networks-form/networks-form.js | 93 +- .../networks-list-item/networks-list-item.js | 15 +- .../networks-list/networks-list.js | 8 +- .../networks-tab-content.js | 2 +- .../networks-tab-subheader.test.js | 2 +- .../settings/networks-tab/networks-tab.js | 50 +- .../networks-tab/networks-tab.test.js | 2 +- .../token-allowance/token-allowance.test.js | 1 - ui/pages/token-details/token-details-page.js | 4 +- ui/selectors/selectors.js | 16 +- ui/selectors/selectors.test.js | 87 + ui/store/actionConstants.ts | 5 +- ui/store/actions.test.js | 310 ++- ui/store/actions.ts | 222 +- 65 files changed, 3897 insertions(+), 1336 deletions(-) create mode 100644 app/scripts/migrations/082.test.js create mode 100644 app/scripts/migrations/082.ts diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 35e037d45..c85ab7259 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -420,7 +420,6 @@ const state = { ], }, }, - frequentRpcList: [], addressBook: { undefined: { 0: { @@ -458,20 +457,28 @@ const state = { }, ], allDetectedTokens: { - '0x5' : { + '0x5': { '0x9d0ba4ddac06032527b140912ec808ab9451b788': [ { address: '0x514910771AF9Ca656af840dff83E8264EcF986CA', decimals: 18, symbol: 'LINK', - image: 'https://crypto.com/price/coin-data/icon/LINK/color_icon.png', - aggregators: ['coinGecko', 'oneInch', 'paraswap', 'zapper', 'zerion'], + image: + 'https://crypto.com/price/coin-data/icon/LINK/color_icon.png', + aggregators: [ + 'coinGecko', + 'oneInch', + 'paraswap', + 'zapper', + 'zerion', + ], }, { address: '0xc00e94Cb662C3520282E6f5717214004A7f26888', decimals: 18, symbol: 'COMP', - image: 'https://crypto.com/price/coin-data/icon/COMP/color_icon.png', + image: + 'https://crypto.com/price/coin-data/icon/COMP/color_icon.png', aggregators: [ 'bancor', 'cmc', @@ -501,8 +508,8 @@ const state = { 'zerion', ], }, - ] - } + ], + }, }, detectedTokens: [ { @@ -1176,15 +1183,21 @@ const state = { ], }, ], - frequentRpcListDetail: [ - { + networkConfigurations: { + 'test-networkConfigurationId-1': { + rpcUrl: 'https://testrpc.com', + chainId: '0x1', + nickname: 'mainnet', + rpcPrefs: { blockExplorerUrl: 'https://etherscan.io' }, + }, + 'test-networkConfigurationId-2': { rpcUrl: 'http://localhost:8545', chainId: '0x539', ticker: 'ETH', nickname: 'Localhost 8545', rpcPrefs: {}, }, - ], + }, accountTokens: { '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { '0x1': [ diff --git a/app/scripts/background.js b/app/scripts/background.js index 236e245a4..b6d51f657 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -211,7 +211,7 @@ browser.runtime.onConnectExternal.addListener(async (...args) => { * @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed. * @property {object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys. * @property {object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions. - * @property {Array} frequentRpcList - A list of frequently used RPCs, including custom user-provided ones. + * @property {object} networkConfigurations - A list of network configurations, containing RPC provider details (eg chainId, rpcUrl, rpcPreferences). * @property {Array} addressBook - A list of previously sent to addresses. * @property {object} contractExchangeRates - Info about current token prices. * @property {Array} tokens - Tokens held by the current user, including their balances. diff --git a/app/scripts/controllers/backup.js b/app/scripts/controllers/backup.js index 1277dfe62..5873bc08c 100644 --- a/app/scripts/controllers/backup.js +++ b/app/scripts/controllers/backup.js @@ -5,17 +5,19 @@ export default class BackupController { const { preferencesController, addressBookController, + networkController, trackMetaMetricsEvent, } = opts; this.preferencesController = preferencesController; this.addressBookController = addressBookController; + this.networkController = networkController; this._trackMetaMetricsEvent = trackMetaMetricsEvent; } async restoreUserData(jsonString) { const existingPreferences = this.preferencesController.store.getState(); - const { preferences, addressBook } = JSON.parse(jsonString); + const { preferences, addressBook, network } = JSON.parse(jsonString); if (preferences) { preferences.identities = existingPreferences.identities; preferences.lostIdentities = existingPreferences.lostIdentities; @@ -28,7 +30,11 @@ export default class BackupController { this.addressBookController.update(addressBook, true); } - if (preferences && addressBook) { + if (network) { + this.networkController.store.updateState(network); + } + + if (preferences || addressBook || network) { this._trackMetaMetricsEvent({ event: 'User Data Imported', category: 'Backup', @@ -40,6 +46,10 @@ export default class BackupController { const userData = { preferences: { ...this.preferencesController.store.getState() }, addressBook: { ...this.addressBookController.state }, + network: { + networkConfigurations: + this.networkController.store.getState().networkConfigurations, + }, }; /** diff --git a/app/scripts/controllers/backup.test.js b/app/scripts/controllers/backup.test.js index 37c91f87e..fdeba7faf 100644 --- a/app/scripts/controllers/backup.test.js +++ b/app/scripts/controllers/backup.test.js @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; import BackupController from './backup'; -function getMockController() { +function getMockPreferencesController() { const mcState = { getSelectedAddress: sinon.stub().returns('0x01'), selectedAddress: '0x01', @@ -31,13 +31,135 @@ function getMockController() { return mcState; } -const jsonData = `{"preferences":{"frequentRpcListDetail":[{"chainId":"0x539","nickname":"Localhost 8545","rpcPrefs":{},"rpcUrl":"http://localhost:8545","ticker":"ETH"},{"chainId":"0x38","nickname":"Binance Smart Chain Mainnet","rpcPrefs":{"blockExplorerUrl":"https://bscscan.com"},"rpcUrl":"https://bsc-dataseed1.binance.org","ticker":"BNB"},{"chainId":"0x61","nickname":"Binance Smart Chain Testnet","rpcPrefs":{"blockExplorerUrl":"https://testnet.bscscan.com"},"rpcUrl":"https://data-seed-prebsc-1-s1.binance.org:8545","ticker":"tBNB"},{"chainId":"0x89","nickname":"Polygon Mainnet","rpcPrefs":{"blockExplorerUrl":"https://polygonscan.com"},"rpcUrl":"https://polygon-rpc.com","ticker":"MATIC"}],"useBlockie":false,"useNonceField":false,"usePhishDetect":true,"dismissSeedBackUpReminder":false,"useTokenDetection":false,"useNftDetection":false,"openSeaEnabled":false,"advancedGasFee":null,"featureFlags":{"sendHexData":true,"showIncomingTransactions":true},"knownMethodData":{},"currentLocale":"en","forgottenPassword":false,"preferences":{"hideZeroBalanceTokens":false,"showFiatInTestnets":false,"showTestNetworks":true,"useNativeCurrencyAsPrimaryCurrency":true},"ipfsGateway":"dweb.link","infuraBlocked":false,"ledgerTransportType":"webhid","theme":"light","customNetworkListEnabled":false,"textDirection":"auto"},"addressBook":{"addressBook":{"0x61":{"0x42EB768f2244C8811C63729A21A3569731535f06":{"address":"0x42EB768f2244C8811C63729A21A3569731535f06","chainId":"0x61","isEns":false,"memo":"","name":""}}}}}`; +function getMockAddressBookController() { + const mcState = { + addressBook: { + '0x61': { + '0x42EB768f2244C8811C63729A21A3569731535f06': { + address: '0x42EB768f2244C8811C63729A21A3569731535f06', + chainId: '0x61', + isEns: false, + memo: '', + name: '', + }, + }, + }, + + update: (store) => (mcState.store = store), + }; + + mcState.store = { + getState: sinon.stub().returns(mcState), + updateState: (store) => (mcState.store = store), + }; + + return mcState; +} + +function getMockNetworkController() { + const mcState = { + networkConfigurations: {}, + + update: (store) => (mcState.store = store), + }; + + mcState.store = { + getState: sinon.stub().returns(mcState), + updateState: (store) => (mcState.store = store), + }; + + return mcState; +} + +const jsonData = JSON.stringify({ + addressBook: { + addressBook: { + '0x61': { + '0x42EB768f2244C8811C63729A21A3569731535f06': { + address: '0x42EB768f2244C8811C63729A21A3569731535f06', + chainId: '0x61', + isEns: false, + memo: '', + name: '', + }, + }, + }, + }, + network: { + networkConfigurations: { + 'network-configuration-id-1': { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + 'network-configuration-id-2': { + chainId: '0x38', + nickname: 'Binance Smart Chain Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com', + }, + rpcUrl: 'https://bsc-dataseed1.binance.org', + ticker: 'BNB', + }, + 'network-configuration-id-3': { + chainId: '0x61', + nickname: 'Binance Smart Chain Testnet', + rpcPrefs: { + blockExplorerUrl: 'https://testnet.bscscan.com', + }, + rpcUrl: 'https://data-seed-prebsc-1-s1.binance.org:8545', + ticker: 'tBNB', + }, + 'network-configuration-id-4': { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://polygonscan.com', + }, + rpcUrl: 'https://polygon-rpc.com', + ticker: 'MATIC', + }, + }, + }, + preferences: { + useBlockie: false, + useNonceField: false, + usePhishDetect: true, + dismissSeedBackUpReminder: false, + useTokenDetection: false, + useCollectibleDetection: false, + openSeaEnabled: false, + advancedGasFee: null, + featureFlags: { + sendHexData: true, + showIncomingTransactions: true, + }, + knownMethodData: {}, + currentLocale: 'en', + forgottenPassword: false, + preferences: { + hideZeroBalanceTokens: false, + showFiatInTestnets: false, + showTestNetworks: true, + useNativeCurrencyAsPrimaryCurrency: true, + }, + ipfsGateway: 'dweb.link', + infuraBlocked: false, + ledgerTransportType: 'webhid', + theme: 'light', + customNetworkListEnabled: false, + textDirection: 'auto', + }, +}); describe('BackupController', function () { const getBackupController = () => { return new BackupController({ - preferencesController: getMockController(), - addressBookController: getMockController(), + preferencesController: getMockPreferencesController(), + addressBookController: getMockAddressBookController(), + networkController: getMockNetworkController(), trackMetaMetricsEvent: sinon.stub(), }); }; @@ -53,17 +175,31 @@ describe('BackupController', function () { it('should restore backup', async function () { const backupController = getBackupController(); backupController.restoreUserData(jsonData); - // check Preferences backup + // check networks backup assert.equal( - backupController.preferencesController.store.frequentRpcListDetail[0] - .chainId, + backupController.networkController.store.networkConfigurations[ + 'network-configuration-id-1' + ].chainId, '0x539', ); assert.equal( - backupController.preferencesController.store.frequentRpcListDetail[1] - .chainId, + backupController.networkController.store.networkConfigurations[ + 'network-configuration-id-2' + ].chainId, '0x38', ); + assert.equal( + backupController.networkController.store.networkConfigurations[ + 'network-configuration-id-3' + ].chainId, + '0x61', + ); + assert.equal( + backupController.networkController.store.networkConfigurations[ + 'network-configuration-id-4' + ].chainId, + '0x89', + ); // make sure identities are not lost after restore assert.equal( backupController.preferencesController.store.identities[ diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 6b663a46f..f9266a540 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -697,19 +697,14 @@ export default class MetaMetricsController { ), [TRAITS.INSTALL_DATE_EXT]: traits[TRAITS.INSTALL_DATE_EXT] || '', [TRAITS.LEDGER_CONNECTION_TYPE]: metamaskState.ledgerTransportType, - [TRAITS.NETWORKS_ADDED]: metamaskState.frequentRpcListDetail.map( - (rpc) => rpc.chainId, - ), - [TRAITS.NETWORKS_WITHOUT_TICKER]: - metamaskState.frequentRpcListDetail.reduce( - (networkList, currentNetwork) => { - if (!currentNetwork.ticker) { - networkList.push(currentNetwork.chainId); - } - return networkList; - }, - [], - ), + [TRAITS.NETWORKS_ADDED]: Object.values( + metamaskState.networkConfigurations, + ).map((networkConfiguration) => networkConfiguration.chainId), + [TRAITS.NETWORKS_WITHOUT_TICKER]: Object.values( + metamaskState.networkConfigurations, + ) + .filter(({ ticker }) => !ticker) + .map(({ chainId }) => chainId), [TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useNftDetection, [TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities) .length, diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 54e6a825d..494d4f763 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -934,11 +934,17 @@ describe('MetaMetricsController', function () { }, }, allTokens: MOCK_ALL_TOKENS, - frequentRpcListDetail: [ - { chainId: CHAIN_IDS.MAINNET, ticker: CURRENCY_SYMBOLS.ETH }, - { chainId: CHAIN_IDS.GOERLI, ticker: CURRENCY_SYMBOLS.TEST_ETH }, - { chainId: '0xaf' }, - ], + networkConfigurations: { + 'network-configuration-id-1': { + chainId: CHAIN_IDS.MAINNET, + ticker: CURRENCY_SYMBOLS.ETH, + }, + 'network-configuration-id-2': { + chainId: CHAIN_IDS.GOERLI, + ticker: CURRENCY_SYMBOLS.TEST_ETH, + }, + 'network-configuration-id-3': { chainId: '0xaf' }, + }, identities: [{}, {}], ledgerTransportType: 'web-hid', openSeaEnabled: true, @@ -975,10 +981,10 @@ describe('MetaMetricsController', function () { [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], }, allTokens: {}, - frequentRpcListDetail: [ - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.GOERLI }, - ], + networkConfigurations: { + 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, + 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, + }, ledgerTransportType: 'web-hid', openSeaEnabled: true, identities: [{}, {}], @@ -996,10 +1002,10 @@ describe('MetaMetricsController', function () { allTokens: { '0x1': { '0xabcde': [{ '0x12345': { address: '0xtestAddress' } }] }, }, - frequentRpcListDetail: [ - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.GOERLI }, - ], + networkConfigurations: { + 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, + 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, + }, ledgerTransportType: 'web-hid', openSeaEnabled: false, identities: [{}, {}, {}], @@ -1025,10 +1031,10 @@ describe('MetaMetricsController', function () { [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], }, allTokens: {}, - frequentRpcListDetail: [ - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.GOERLI }, - ], + networkConfigurations: { + 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, + 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, + }, ledgerTransportType: 'web-hid', openSeaEnabled: true, identities: [{}, {}], @@ -1044,10 +1050,10 @@ describe('MetaMetricsController', function () { [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], }, allTokens: {}, - frequentRpcListDetail: [ - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.GOERLI }, - ], + networkConfigurations: { + 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, + 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, + }, ledgerTransportType: 'web-hid', openSeaEnabled: true, identities: [{}, {}], diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js index 5c4d3d456..6cf66f5dd 100644 --- a/app/scripts/controllers/network/network-controller.js +++ b/app/scripts/controllers/network/network-controller.js @@ -14,6 +14,7 @@ import { import EthQuery from 'eth-query'; import createFilterMiddleware from 'eth-json-rpc-filters'; import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager'; +import { v4 as random } from 'uuid'; import { INFURA_PROVIDER_TYPES, BUILT_IN_NETWORKS, @@ -22,14 +23,24 @@ import { CHAIN_IDS, NETWORK_TYPES, } from '../../../../shared/constants/network'; +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import { isPrefixedFormattedHexString, isSafeChainId, } from '../../../../shared/modules/network.utils'; -import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; +import { EVENT } from '../../../../shared/constants/metametrics'; import createInfuraClient from './createInfuraClient'; import createJsonRpcClient from './createJsonRpcClient'; +/** + * @typedef {object} NetworkConfiguration + * @property {string} rpcUrl - RPC target URL. + * @property {string} chainId - Network ID as per EIP-155 + * @property {string} ticker - Currency ticker. + * @property {object} [rpcPrefs] - Personalized preferences. + * @property {string} [nickname] - Personalized network name. + */ + const env = process.env.METAMASK_ENV; const fetchWithTimeout = getFetchWithTimeout(); @@ -83,8 +94,9 @@ export default class NetworkController extends EventEmitter { * @param {object} [options] - NetworkController options. * @param {object} [options.state] - Initial controller state. * @param {string} [options.infuraProjectId] - The Infura project ID. + * @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController */ - constructor({ state = {}, infuraProjectId } = {}) { + constructor({ state = {}, infuraProjectId, trackMetaMetricsEvent } = {}) { super(); // create stores @@ -105,11 +117,17 @@ export default class NetworkController extends EventEmitter { ...defaultNetworkDetailsState, }, ); + + this.networkConfigurationsStore = new ObservableStore( + state.networkConfigurations || {}, + ); + this.store = new ComposedStore({ provider: this.providerStore, previousProviderStore: this.previousProviderStore, network: this.networkStore, networkDetails: this.networkDetails, + networkConfigurations: this.networkConfigurationsStore, }); // provider and block tracker @@ -124,6 +142,7 @@ export default class NetworkController extends EventEmitter { throw new Error('Invalid Infura project ID'); } this._infuraProjectId = infuraProjectId; + this._trackMetaMetricsEvent = trackMetaMetricsEvent; this.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { this.lookupNetwork(); @@ -224,42 +243,48 @@ export default class NetworkController extends EventEmitter { } } - setRpcTarget(rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs) { - 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.`, - ); + /** + * 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, - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, + ...targetNetwork, }); + + return targetNetwork.rpcUrl; } setProviderType(type) { assert.notStrictEqual( type, NETWORK_TYPES.RPC, - `NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setRpcTarget"`, + `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 } = BUILT_IN_NETWORKS[type]; + const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[type]; this._setProviderConfig({ type, rpcUrl: '', chainId, ticker: ticker ?? 'ETH', nickname: '', + rpcPrefs: { blockExplorerUrl }, }); } @@ -476,4 +501,117 @@ export default class NetworkController extends EventEmitter { this._provider = provider; this._blockTracker = blockTracker; } + + /** + * Network Configuration management functions + */ + + /** + * Adds a network configuration if the rpcUrl is not already present on an + * existing network configuration. Otherwise updates the entry with the matching rpcUrl. + * + * @param {NetworkConfiguration} networkConfiguration - The network configuration to add or, if rpcUrl matches an existing entry, to modify. + * @param {object} options + * @param {boolean} options.setActive - An option to set the newly added networkConfiguration as the active provider. + * @param {string} options.referrer - The site from which the call originated, or 'metamask' for internal calls - used for event metrics. + * @param {string} options.source - Where the upsertNetwork event originated (i.e. from a dapp or from the network form)- used for event metrics. + * @returns {string} id for the added or updated network configuration + */ + upsertNetworkConfiguration( + { rpcUrl, chainId, ticker, nickname, rpcPrefs }, + { setActive = false, referrer, source }, + ) { + assert.ok( + isPrefixedFormattedHexString(chainId), + `Invalid chain ID "${chainId}": invalid hex string.`, + ); + assert.ok( + isSafeChainId(parseInt(chainId, 16)), + `Invalid chain ID "${chainId}": numerical value greater than max safe value.`, + ); + + if (!rpcUrl) { + throw new Error( + 'An rpcUrl is required to add or update network configuration', + ); + } + + if (!referrer || !source) { + throw new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ); + } + + try { + // eslint-disable-next-line no-new + new URL(rpcUrl); + } catch (e) { + if (e.message.includes('Invalid URL')) { + throw new Error('rpcUrl must be a valid URL'); + } + } + + if (!ticker) { + throw new Error( + 'A ticker is required to add or update networkConfiguration', + ); + } + + const networkConfigurations = this.networkConfigurationsStore.getState(); + const newNetworkConfiguration = { + rpcUrl, + chainId, + ticker, + nickname, + rpcPrefs, + }; + + const oldNetworkConfigurationId = Object.values(networkConfigurations).find( + (networkConfiguration) => + networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(), + )?.id; + + const newNetworkConfigurationId = oldNetworkConfigurationId || random(); + this.networkConfigurationsStore.updateState({ + ...networkConfigurations, + [newNetworkConfigurationId]: { + ...newNetworkConfiguration, + id: newNetworkConfigurationId, + }, + }); + + if (!oldNetworkConfigurationId) { + this._trackMetaMetricsEvent({ + event: 'Custom Network Added', + category: EVENT.CATEGORIES.NETWORK, + referrer: { + url: referrer, + }, + properties: { + chain_id: chainId, + symbol: ticker, + source, + }, + }); + } + + if (setActive) { + this.setActiveNetwork(newNetworkConfigurationId); + } + + return newNetworkConfigurationId; + } + + /** + * Removes network configuration from state. + * + * @param {string} networkConfigurationId - the unique id for the network configuration to remove. + */ + removeNetworkConfiguration(networkConfigurationId) { + const networkConfigurations = { + ...this.networkConfigurationsStore.getState(), + }; + delete networkConfigurations[networkConfigurationId]; + this.networkConfigurationsStore.putState(networkConfigurations); + } } diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index df2a76dbd..8c887cb56 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -1,8 +1,11 @@ import { inspect, isDeepStrictEqual, promisify } from 'util'; import { isMatch } from 'lodash'; +import { v4 } from 'uuid'; import nock from 'nock'; import sinon from 'sinon'; import * as ethJsonRpcMiddlewareModule from '@metamask/eth-json-rpc-middleware'; +import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; +import { EVENT } from '../../../../shared/constants/metametrics'; import NetworkController from './network-controller'; jest.mock('@metamask/eth-json-rpc-middleware', () => { @@ -12,6 +15,15 @@ jest.mock('@metamask/eth-json-rpc-middleware', () => { }; }); +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + // Store this up front so it doesn't get lost when it is stubbed const originalSetTimeout = global.setTimeout; @@ -106,6 +118,7 @@ const DEFAULT_INFURA_PROJECT_ID = 'fake-infura-project-id'; */ const DEFAULT_CONTROLLER_OPTIONS = { infuraProjectId: DEFAULT_INFURA_PROJECT_ID, + trackMetaMetricsEvent: jest.fn(), }; /** @@ -119,21 +132,21 @@ const JSONRPC_RESPONSE_BODY_PROPERTIES = ['id', 'jsonrpc', 'result', 'error']; */ const INFURA_NETWORKS = [ { - networkName: 'Mainnet', + nickname: 'Mainnet', networkType: 'mainnet', chainId: '0x1', networkVersion: '1', ticker: 'ETH', }, { - networkName: 'Goerli', + nickname: 'Goerli', networkType: 'goerli', chainId: '0x5', networkVersion: '5', ticker: 'GoerliETH', }, { - networkName: 'Sepolia', + nickname: 'Sepolia', networkType: 'sepolia', chainId: '0xaa36a7', networkVersion: '11155111', @@ -466,6 +479,7 @@ describe('NetworkController', () => { expect(controller.store.getState()).toMatchInlineSnapshot(` { "network": "loading", + "networkConfigurations": {}, "networkDetails": { "EIPS": { "1559": false, @@ -494,6 +508,7 @@ describe('NetworkController', () => { expect(controller.store.getState()).toMatchInlineSnapshot(` { "network": "loading", + "networkConfigurations": {}, "networkDetails": { "EIPS": { "1559": undefined, @@ -568,13 +583,13 @@ describe('NetworkController', () => { }); for (const { - networkName, + nickname, networkType, chainId, networkVersion, } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { - it(`initializes a provider pointed to the ${networkName} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { await withController( { state: { @@ -631,7 +646,7 @@ describe('NetworkController', () => { ); }); - it(`persists "${networkVersion}" to state as the network version of ${networkName}`, async () => { + it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { await withController( { state: { @@ -692,7 +707,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -724,7 +749,7 @@ describe('NetworkController', () => { const { result: chainIdResult } = await promisifiedSendAsync({ method: 'eth_chainId', }); - expect(chainIdResult).toBe('0x1337'); + expect(chainIdResult).toBe('0xtest'); }, ); }); @@ -736,7 +761,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -763,7 +798,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, }, }, }, @@ -790,10 +835,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', }, - networkDetails: { - EIPS: {}, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -837,9 +889,9 @@ describe('NetworkController', () => { }); }); - for (const { networkName, networkType, chainId } of INFURA_NETWORKS) { + for (const { nickname, networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is changed to "${networkType}"`, () => { - it(`returns a provider object that was pointed to another network before the switch and is pointed to ${networkName} afterward`, async () => { + it(`returns a provider object that was pointed to another network before the switch and is pointed to ${nickname} afterward`, async () => { await withController( { state: { @@ -847,6 +899,20 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer.com', + }, + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -885,6 +951,14 @@ describe('NetworkController', () => { provider: { type: 'goerli', }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, }, }, async ({ controller, network }) => { @@ -900,14 +974,14 @@ describe('NetworkController', () => { }); expect(oldChainIdResult).toBe('0x5'); - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( provider, ); const { result: newChainIdResult } = await promisifiedSendAsync2({ method: 'eth_chainId', }); - expect(newChainIdResult).toBe('0x1337'); + expect(newChainIdResult).toBe('0xtest'); }, ); }); @@ -1119,11 +1193,7 @@ describe('NetworkController', () => { }); }); - for (const { - networkName, - networkType, - networkVersion, - } of INFURA_NETWORKS) { + for (const { nickname, networkType, networkVersion } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { describe('if the request for eth_blockNumber responds successfully', () => { it('emits infuraIsUnblocked as long as the network has not changed by the time the request ends', async () => { @@ -1291,6 +1361,14 @@ describe('NetworkController', () => { // of the network selected, it just needs to exist chainId: '0x9999999', }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -1308,9 +1386,8 @@ describe('NetworkController', () => { controller, eventName: 'networkDidChange', operation: () => { - controller.setRpcTarget( - 'http://some-rpc-url', - '0x1337', + controller.setActiveNetwork( + 'testNetworkConfigurationId', ); }, }); @@ -1390,7 +1467,7 @@ describe('NetworkController', () => { }); }); - it(`persists "${networkVersion}" to state as the network version of ${networkName}`, async () => { + it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { await withController( { state: { @@ -1592,9 +1669,9 @@ describe('NetworkController', () => { }); describe('if the network was switched after the net_version request started but before it completed', () => { - it(`persists to state the network version of the newly switched network, not the initial one for ${networkName}`, async () => { + it(`persists to state the network version of the newly switched network, not the initial one for ${nickname}`, async () => { const oldNetworkVersion = networkVersion; - const newNetworkName = 'goerli'; + const newChainName = 'goerli'; const newNetworkVersion = '5'; await withController( @@ -1621,7 +1698,7 @@ describe('NetworkController', () => { controller, propertyPath: ['network'], operation: () => { - controller.setProviderType(newNetworkName); + controller.setProviderType(newChainName); }, }) .then(() => { @@ -1708,9 +1785,9 @@ describe('NetworkController', () => { ); }); - it(`persists to state the EIP-1559 support for the newly switched network, not the initial one for ${networkName}`, async () => { + it(`persists to state the EIP-1559 support for the newly switched network, not the initial one for ${nickname}`, async () => { const oldNetworkVersion = networkVersion; - const newNetworkName = 'goerli'; + const newChainName = 'goerli'; const newNetworkVersion = '5'; await withController( @@ -1737,7 +1814,7 @@ describe('NetworkController', () => { controller, propertyPath: ['network'], operation: () => { - controller.setProviderType(newNetworkName); + controller.setProviderType(newChainName); }, }) .then(() => { @@ -1835,7 +1912,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -1869,7 +1956,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -1908,7 +2005,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -1957,10 +2064,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', }, - networkDetails: { - EIPS: {}, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, }, }, }, @@ -1990,14 +2104,25 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', + ticker: 'TEST2', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x1337', + ticker: 'TEST2', + id: 'testNetworkConfigurationId', + }, }, networkDetails: { EIPS: {}, }, }, }, + async ({ controller, network }) => { network.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -2031,6 +2156,16 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, networkDetails: { EIPS: {}, @@ -2091,7 +2226,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -2139,7 +2284,20 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + networkDetails: { + EIPS: {}, }, }, }, @@ -2191,8 +2349,27 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url-1', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, + networkDetails: { + EIPS: {}, + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x1337', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2207,9 +2384,8 @@ describe('NetworkController', () => { controller, propertyPath: ['network'], operation: () => { - controller.setRpcTarget( - 'https://mock-rpc-url-2', - '0x9999', + controller.setActiveNetwork( + 'testNetworkConfigurationId1', ); }, }); @@ -2218,7 +2394,7 @@ describe('NetworkController', () => { }); const network2 = network1.with({ networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', + customRpcUrl: 'https://mock-rpc-url-1', }, }); network2.mockEssentialRpcCalls({ @@ -2228,6 +2404,7 @@ describe('NetworkController', () => { }, }, }); + await withoutCallingLookupNetwork({ controller, operation: async () => { @@ -2249,13 +2426,33 @@ describe('NetworkController', () => { }); it('persists to state the EIP-1559 support for the newly switched network, not the initial one', async () => { + const nonEip1559RpcUrl = 'https://mock-rpc-url-1'; await withController( { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url-1', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, + networkDetails: { + EIPS: {}, + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: nonEip1559RpcUrl, + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x1337', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2268,11 +2465,10 @@ describe('NetworkController', () => { beforeCompleting: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkDetails'], operation: () => { - controller.setRpcTarget( - 'https://mock-rpc-url-2', - '0x9999', + controller.setActiveNetwork( + 'testNetworkConfigurationId1', ); }, }); @@ -2286,7 +2482,7 @@ describe('NetworkController', () => { }); const network2 = network1.with({ networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', + customRpcUrl: nonEip1559RpcUrl, }, }); network2.mockEssentialRpcCalls({ @@ -2301,21 +2497,13 @@ describe('NetworkController', () => { }, }, }); - await withoutCallingLookupNetwork({ + await waitForLookupNetworkToComplete({ controller, operation: async () => { await controller.initializeProvider(); }, }); - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - expect( controller.store.getState().networkDetails.EIPS['1559'], ).toBe(false); @@ -2326,31 +2514,33 @@ describe('NetworkController', () => { }); }); - describe('setRpcTarget', () => { - it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { - await withController(async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + describe('setActiveNetwork', () => { + it('throws if the given networkConfigurationId does not match one in networkConfigurations', async () => { + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); - expect(() => - controller.setRpcTarget('http://some.url', 'not a chain id'), - ).toThrow( - new Error('Invalid chain ID "not a chain id": invalid hex string.'), - ); - }); - }); - - it('throws if the given chain ID is greater than the maximum allowed ID', async () => { - await withController(async ({ controller, network }) => { - network.mockEssentialRpcCalls(); - - expect(() => - controller.setRpcTarget('http://some.url', '0xFFFFFFFFFFFFFFFF'), - ).toThrow( - new Error( - 'Invalid chain ID "0xFFFFFFFFFFFFFFFF": numerical value greater than max safe value.', - ), - ); - }); + expect(() => + controller.setActiveNetwork('invalid-network-configuration-id'), + ).toThrow( + new Error( + 'networkConfigurationId invalid-network-configuration-id does not match a configured networkConfiguration', + ), + ); + }, + ); }); it('captures the current provider configuration before overwriting it', async () => { @@ -2359,9 +2549,24 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x9999', - nickname: 'Test initial state', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x9999', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2369,26 +2574,27 @@ describe('NetworkController', () => { const network = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + customRpcUrl: 'https://mock-rpc-url-2', }, }); network.mockEssentialRpcCalls(); - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId1'); expect( controller.store.getState().previousProviderStore, ).toStrictEqual({ type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x9999', - nickname: 'Test initial state', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', }); }, ); }); - it('overwrites the provider configuration given a minimal set of arguments, filling in ticker, nickname, and rpcPrefs with default values', async () => { + it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { await withController( { state: { @@ -2396,7 +2602,24 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'http://example-custom-rpc.metamask.io', chainId: '0x9999', - nickname: 'Test initial state', + ticker: 'RPC', + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + type: 'rpc', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://example-custom-rpc.metamask.io', + chainId: '0x9999', + ticker: 'RPC', + type: 'rpc', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2409,56 +2632,14 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId1'); expect(controller.store.getState().provider).toStrictEqual({ type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'ETH', - nickname: '', - rpcPrefs: undefined, - }); - }, - ); - }); - - it('overwrites the provider configuration completely given a maximal set of arguments', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', - }, - }, - }, - async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, - }); - network.mockEssentialRpcCalls(); - - controller.setRpcTarget( - 'https://mock-rpc-url', - '0x1337', - 'DAI', - 'Cool network', - 'RPC prefs', - ); - - expect(controller.store.getState().provider).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'DAI', - nickname: 'Cool network', - rpcPrefs: 'RPC prefs', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', }); }, ); @@ -2471,8 +2652,24 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', + chainId: '0xtest2', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://example-custom-rpc.metamask.io', + chainId: '0xtest2', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2509,7 +2706,7 @@ describe('NetworkController', () => { controller, eventName: 'networkWillChange', operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId2'); }, beforeResolving: () => { expect(controller.store.getState().network).toBe(initialNetwork); @@ -2526,8 +2723,24 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'http://mock-rpc-url-1', - chainId: '0xFF', + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2539,19 +2752,6 @@ describe('NetworkController', () => { }, }, }); - const network2 = network1.with({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', - }, - }); - network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: "this got used when it shouldn't", - }, - }, - }); await controller.initializeProvider(); expect(controller.store.getState().network).toBe('255'); @@ -2563,7 +2763,7 @@ describe('NetworkController', () => { // before networkDidChange count: 1, operation: () => { - controller.setRpcTarget('https://mock-rpc-url-2', '0xCC'); + controller.setActiveNetwork('testNetworkConfigurationId1'); }, }); expect(controller.store.getState().network).toBe('loading'); @@ -2577,8 +2777,24 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'http://mock-rpc-url-1', - chainId: '0xFF', + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2610,7 +2826,7 @@ describe('NetworkController', () => { // before networkDidChange count: 1, operation: () => { - controller.setRpcTarget('https://mock-rpc-url-2', '0xCC'); + controller.setActiveNetwork('testNetworkConfigurationId1'); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ @@ -2623,135 +2839,205 @@ describe('NetworkController', () => { }); it('initializes a provider pointed to the given RPC URL whose chain ID matches the configured chain ID', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, - }); - network.mockEssentialRpcCalls(); - network.mockRpcCall({ - request: { + }, + async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network.mockEssentialRpcCalls(); + network.mockRpcCall({ + request: { + method: 'test', + params: [], + }, + response: { + result: 'test response', + }, + }); + + controller.setActiveNetwork('testNetworkConfigurationId'); + + const { provider } = controller.getProviderAndBlockTracker(); + const promisifiedSendAsync = promisify(provider.sendAsync).bind( + provider, + ); + const { result: testResult } = await promisifiedSendAsync({ + id: 99999, + jsonrpc: '2.0', method: 'test', params: [], - }, - response: { - result: 'test response', - }, - }); - - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); - - const { provider } = controller.getProviderAndBlockTracker(); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const { result: testResult } = await promisifiedSendAsync({ - id: 99999, - jsonrpc: '2.0', - method: 'test', - params: [], - }); - expect(testResult).toBe('test response'); - const { result: chainIdResult } = await promisifiedSendAsync({ - method: 'eth_chainId', - }); - expect(chainIdResult).toBe('0x1337'); - }); + }); + expect(testResult).toBe('test response'); + const { result: chainIdResult } = await promisifiedSendAsync({ + method: 'eth_chainId', + }); + expect(chainIdResult).toBe('0xtest'); + }, + ); }); it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, - }); - network.mockEssentialRpcCalls(); - await controller.initializeProvider(); + }, + async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); + const { provider: providerBefore } = + controller.getProviderAndBlockTracker(); + controller.setActiveNetwork('testNetworkConfigurationId'); + const { provider: providerAfter } = + controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }); + expect(providerBefore).toBe(providerAfter); + }, + ); }); it('emits networkDidChange', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, - }); - network.mockEssentialRpcCalls(); + }, + async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network.mockEssentialRpcCalls(); - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', - operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); - }, - }); + const networkDidChange = await waitForEvent({ + controller, + eventName: 'networkDidChange', + operation: () => { + controller.setActiveNetwork('testNetworkConfigurationId'); + }, + }); - expect(networkDidChange).toBe(true); - }); + expect(networkDidChange).toBe(true); + }, + ); }); it('emits infuraIsUnblocked', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, - }); - network.mockEssentialRpcCalls(); + }, + async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network.mockEssentialRpcCalls(); - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); - }, - }); + const infuraIsUnblocked = await waitForEvent({ + controller, + eventName: 'infuraIsUnblocked', + operation: () => { + controller.setActiveNetwork('testNetworkConfigurationId'); + }, + }); - expect(infuraIsUnblocked).toBe(true); - }); + expect(infuraIsUnblocked).toBe(true); + }, + ); }); it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, - }); - network.mockEssentialRpcCalls({ - net_version: { - response: { - result: '42', + await withController( + { + state: { + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, }, }, - }); + }, + async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network.mockEssentialRpcCalls({ + net_version: { + response: { + result: '42', + }, + }, + }); - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); - }, - }); + await waitForStateChanges({ + controller, + propertyPath: ['network'], + operation: () => { + controller.setActiveNetwork('testNetworkConfigurationId'); + }, + }); - expect(controller.store.getState().network).toBe('42'); - }); + expect(controller.store.getState().network).toBe('42'); + }, + ); }); it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { @@ -2761,6 +3047,14 @@ describe('NetworkController', () => { networkDetails: { EIPS: {}, }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller }) => { @@ -2779,7 +3073,7 @@ describe('NetworkController', () => { propertyPath: ['networkDetails'], count: 2, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -2793,7 +3087,7 @@ describe('NetworkController', () => { describe('setProviderType', () => { for (const { - networkName, + nickname, networkType, chainId, networkVersion, @@ -2805,10 +3099,36 @@ describe('NetworkController', () => { { state: { provider: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer.com', + }, + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2826,10 +3146,14 @@ describe('NetworkController', () => { expect( controller.store.getState().previousProviderStore, ).toStrictEqual({ - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', }); }, ); @@ -2840,10 +3164,36 @@ describe('NetworkController', () => { { state: { provider: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer.com', + }, + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -2864,6 +3214,11 @@ describe('NetworkController', () => { chainId, ticker, nickname: '', + rpcPrefs: { + blockExplorerUrl: + BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + }, + id: 'testNetworkConfigurationId2', }); }, ); @@ -2896,9 +3251,19 @@ describe('NetworkController', () => { { state: { provider: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', type: 'rpc', - rpcUrl: 'http://mock-rpc-url', - chainId: '0xFF', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -2942,8 +3307,18 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'http://mock-rpc-url-1', - chainId: '0xFF', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -2987,7 +3362,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a provider pointed to the ${networkName} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { await withController(async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', @@ -3075,7 +3450,7 @@ describe('NetworkController', () => { }); }); - it(`persists "${networkVersion}" to state as the network version of ${networkName}`, async () => { + it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { await withController(async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', @@ -3140,7 +3515,7 @@ describe('NetworkController', () => { await withController(async ({ controller }) => { expect(() => controller.setProviderType('rpc')).toThrow( new Error( - 'NetworkController - cannot call "setProviderType" with type "rpc". Use "setRpcTarget"', + 'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"', ), ); }); @@ -3160,7 +3535,7 @@ describe('NetworkController', () => { describe('resetConnection', () => { for (const { - networkName, + nickname, networkType, chainId, networkVersion, @@ -3277,7 +3652,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a new provider object pointed to the current Infura network (name: ${networkName}, chain ID: ${chainId})`, async () => { + it(`initializes a new provider object pointed to the current Infura network (name: ${nickname}, chain ID: ${chainId})`, async () => { await withController( { state: { @@ -3457,7 +3832,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3484,7 +3869,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0xFF', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3525,7 +3920,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0xFF', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3571,6 +3976,16 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3598,7 +4013,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3629,7 +4054,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3656,7 +4091,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3683,7 +4128,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3716,7 +4171,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -3742,7 +4207,7 @@ describe('NetworkController', () => { describe('rollbackToPreviousProvider', () => { for (const { - networkName, + nickname, networkType, chainId, networkVersion, @@ -3754,9 +4219,39 @@ describe('NetworkController', () => { state: { provider: { type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x999', + rpcUrl: '', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + nickname: '', + ticker: BUILT_IN_NETWORKS[networkType].ticker, + rpcPrefs: { + blockExplorerUrl: + BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + }, + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer.com', + }, + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'http://mock-rpc-url-2', + chainId: '0xtest2', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, + }, + networkDetails: { + EIPS: {}, }, }, }, @@ -3773,16 +4268,19 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId1'); }, }); expect(controller.store.getState().provider).toStrictEqual({ + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0xtest', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer.com', + }, + id: 'testNetworkConfigurationId1', type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'ETH', - nickname: '', - rpcPrefs: undefined, }); await waitForLookupNetworkToComplete({ @@ -3793,11 +4291,15 @@ describe('NetworkController', () => { }); expect(controller.store.getState().provider).toStrictEqual({ type: networkType, - rpcUrl: 'https://mock-rpc-url', - chainId: '0x999', - ticker: 'ETH', + rpcUrl: '', + chainId: BUILT_IN_NETWORKS[networkType].chainId, + ticker: BUILT_IN_NETWORKS[networkType].ticker, nickname: '', - rpcPrefs: undefined, + id: 'testNetworkConfigurationId1', + rpcPrefs: { + blockExplorerUrl: + BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + }, }); }, ); @@ -3813,6 +4315,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -3827,7 +4342,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -3859,6 +4374,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -3880,7 +4408,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); expect(controller.store.getState().network).toBe('255'); @@ -3915,6 +4443,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -3932,7 +4473,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ @@ -3967,7 +4508,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a provider pointed to the ${networkName} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { await withController( { state: { @@ -3977,6 +4518,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -3991,7 +4545,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -4024,6 +4578,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -4038,7 +4605,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -4068,6 +4635,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -4083,7 +4663,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -4114,6 +4694,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -4129,7 +4722,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); @@ -4151,7 +4744,7 @@ describe('NetworkController', () => { ); }); - it(`persists "${networkVersion}" to state as the network version of ${networkName}`, async () => { + it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { await withController( { state: { @@ -4161,6 +4754,19 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -4182,7 +4788,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); expect(controller.store.getState().network).toBe('255'); @@ -4209,6 +4815,14 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + }, + }, }, }, async ({ controller, network: network1 }) => { @@ -4233,7 +4847,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setRpcTarget('https://mock-rpc-url', '0x1337'); + controller.setActiveNetwork('testNetworkConfigurationId'); }, }); expect( @@ -4263,8 +4877,41 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + nickname: 'test-chain', + ticker: 'TEST', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer.com', + }, + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x1337', + nickname: 'test-chain-2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -4290,6 +4937,10 @@ describe('NetworkController', () => { chainId: '0x5', ticker: 'GoerliETH', nickname: '', + rpcPrefs: { + blockExplorerUrl: 'https://goerli.etherscan.io', + }, + id: 'testNetworkConfigurationId2', }); await waitForLookupNetworkToComplete({ @@ -4300,10 +4951,14 @@ describe('NetworkController', () => { }); expect(controller.store.getState().provider).toStrictEqual({ type: 'rpc', - rpcUrl: 'https://mock-rpc-url', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', - ticker: 'GoerliETH', - nickname: '', + ticker: 'TEST2', + nickname: 'test-chain-2', + rpcPrefs: { + blockExplorerUrl: 'test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', }); }, ); @@ -4315,8 +4970,24 @@ describe('NetworkController', () => { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url', + rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, + networkConfigurations: { + testNetworkConfigurationId1: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', + }, + testNetworkConfigurationId2: { + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x1337', + ticker: 'TEST2', + id: 'testNetworkConfigurationId2', + }, }, }, }, @@ -4361,7 +5032,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4410,7 +5091,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4472,6 +5163,16 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4517,7 +5218,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4560,7 +5271,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4605,7 +5326,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4651,7 +5382,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4697,7 +5438,17 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, }, }, }, @@ -4744,6 +5495,609 @@ describe('NetworkController', () => { }); }); }); + describe('upsertNetworkConfiguration', () => { + it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { + const invalidChainId = '1'; + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: invalidChainId, + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'rpc_url', + ticker: 'RPC', + }, + { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }, + ), + ).toThrow( + new Error( + `Invalid chain ID "${invalidChainId}": invalid hex string.`, + ), + ); + }); + }); + + it('throws if the given chain ID is greater than the maximum allowed ID', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: '0xFFFFFFFFFFFFFFFF', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'rpc_url', + ticker: 'RPC', + }, + { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }, + ), + ).toThrow( + new Error( + 'Invalid chain ID "0xFFFFFFFFFFFFFFFF": numerical value greater than max safe value.', + ), + ); + }); + }); + + it('throws if the no (or a falsy) rpcUrl is passed', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: '0x9999', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + ticker: 'RPC', + }, + { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }, + ), + ).toThrow( + new Error( + 'An rpcUrl is required to add or update network configuration', + ), + ); + }); + }); + + it('throws if rpcUrl passed is not a valid Url', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: '0x9999', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + ticker: 'RPC', + rpcUrl: 'test', + }, + { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }, + ), + ).toThrow(new Error('rpcUrl must be a valid URL')); + }); + }); + + it('throws if the no (or a falsy) ticker is passed', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration( + { + chainId: '0x5', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'https://mock-rpc-url', + }, + { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }, + ), + ).toThrow( + new Error( + 'A ticker is required to add or update networkConfiguration', + ), + ); + }); + }); + + it('throws if an options object is not passed as a second argument', async () => { + await withController(async ({ controller }) => { + expect(() => + controller.upsertNetworkConfiguration({ + chainId: '0x5', + nickname: 'RPC', + rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, + rpcUrl: 'https://mock-rpc-url', + }), + ).toThrow( + new Error( + "Cannot read properties of undefined (reading 'setActive')", + ), + ); + }); + }); + + it('should add the given network if all required properties are present but nither rpcPrefs nor nickname properties are passed', async () => { + v4.mockImplementationOnce(() => 'networkConfigurationId'); + await withController( + { + state: { + networkConfigurations: {}, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + ...rpcUrlNetwork, + nickname: undefined, + rpcPrefs: undefined, + id: 'networkConfigurationId', + }, + ]), + ); + }, + ); + }); + + it('adds new networkConfiguration to networkController store, but only adds valid properties (rpcUrl, chainId, ticker, nickname, rpcPrefs) and fills any missing properties from this list as undefined', async function () { + v4.mockImplementationOnce(() => 'networkConfigurationId'); + await withController( + { + state: { + networkConfigurations: {}, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + invalidKey: 'new-chain', + invalidKey2: {}, + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + nickname: undefined, + rpcPrefs: undefined, + id: 'networkConfigurationId', + }, + ]), + ); + }, + ); + }); + + it('should add the given network configuration if its rpcURL does not match an existing configuration without changing or overwriting other configurations', async () => { + v4.mockImplementationOnce(() => 'networkConfigurationId2'); + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId: { + rpcUrl: 'https://test-rpc-url', + ticker: 'ticker', + nickname: 'nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + nickname: 'RPC', + rpcPrefs: undefined, + rpcUrl: 'https://test-rpc-url-2', + ticker: 'RPC', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual( + expect.arrayContaining([ + { + rpcUrl: 'https://test-rpc-url', + ticker: 'ticker', + nickname: 'nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + { ...rpcUrlNetwork, id: 'networkConfigurationId2' }, + ]), + ); + }, + ); + }); + + it('should use the given configuration to update an existing network configuration that has a matching rpcUrl', async () => { + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId: { + rpcUrl: 'https://test-rpc-url', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_chainName', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + }, + }, + }, + + async ({ controller }) => { + const updatedConfiguration = { + rpcUrl: 'https://test-rpc-url', + ticker: 'new_rpc_ticker', + nickname: 'new_rpc_chainName', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + }; + controller.upsertNetworkConfiguration(updatedConfiguration, { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual([ + { + rpcUrl: 'https://test-rpc-url', + nickname: 'new_rpc_chainName', + ticker: 'new_rpc_ticker', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + ]); + }, + ); + }); + + it('should use the given configuration to update an existing network configuration that has a matching rpcUrl without changing or overwriting other networkConfigurations', async () => { + await withController( + { + state: { + networkConfigurations: { + networkConfigurationId: { + rpcUrl: 'https://test-rpc-url', + ticker: 'ticker', + nickname: 'nickname', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + networkConfigurationId2: { + rpcUrl: 'https://test-rpc-url-2', + ticker: 'ticker-2', + nickname: 'nickname-2', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x9999', + id: 'networkConfigurationId2', + }, + }, + }, + }, + async ({ controller }) => { + controller.upsertNetworkConfiguration( + { + rpcUrl: 'https://test-rpc-url', + ticker: 'new-ticker', + nickname: 'new-nickname', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + }, + { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }, + ); + + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual([ + { + rpcUrl: 'https://test-rpc-url', + ticker: 'new-ticker', + nickname: 'new-nickname', + rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, + chainId: '0x1', + id: 'networkConfigurationId', + }, + { + rpcUrl: 'https://test-rpc-url-2', + ticker: 'ticker-2', + nickname: 'nickname-2', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '0x9999', + id: 'networkConfigurationId2', + }, + ]); + }, + ); + }); + + it('should add the given network and not set it to active if the setActive option is not passed (or a falsy value is passed)', async () => { + v4.mockImplementationOnce(() => 'networkConfigurationId'); + const originalProvider = { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }; + await withController( + { + state: { + provider: originalProvider, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + + expect(controller.store.getState().provider).toStrictEqual( + originalProvider, + ); + }, + ); + }); + + it('should add the given network and set it to active if the setActive option is passed as true', async () => { + v4.mockImplementationOnce(() => 'networkConfigurationId'); + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller }) => { + const rpcUrlNetwork = { + chainId: '0x1', + rpcUrl: 'https://test-rpc-url', + ticker: 'test_ticker', + }; + + controller.upsertNetworkConfiguration(rpcUrlNetwork, { + setActive: true, + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + + expect(controller.store.getState().provider).toStrictEqual({ + ...rpcUrlNetwork, + nickname: undefined, + rpcPrefs: undefined, + type: 'rpc', + id: 'networkConfigurationId', + }); + }, + ); + }); + + it('adds new networkConfiguration to networkController store and calls to the metametrics event tracking with the correct values', async () => { + v4.mockImplementationOnce(() => 'networkConfigurationId'); + const trackEventSpy = jest.fn(); + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + trackMetaMetricsEvent: trackEventSpy, + }, + async ({ controller }) => { + const newNetworkConfiguration = { + rpcUrl: 'https://new-chain-rpc-url', + chainId: '0x9999', + ticker: 'NEW', + nickname: 'new-chain', + rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, + }; + + controller.upsertNetworkConfiguration(newNetworkConfiguration, { + referrer: 'https://test-dapp.com', + source: EVENT.SOURCE.NETWORK.DAPP, + }); + + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual([ + { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + { + ...newNetworkConfiguration, + id: 'networkConfigurationId', + }, + ]); + expect(trackEventSpy).toHaveBeenCalledWith({ + event: 'Custom Network Added', + category: 'Network', + referrer: { + url: 'https://test-dapp.com', + }, + properties: { + chain_id: '0x9999', + symbol: 'NEW', + source: 'dapp', + }, + }); + }, + ); + }); + + it('throws if referrer and source arguments are not passed', async () => { + v4.mockImplementationOnce(() => 'networkConfigurationId'); + const trackEventSpy = jest.fn(); + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + trackMetaMetricsEvent: trackEventSpy, + }, + async ({ controller }) => { + const newNetworkConfiguration = { + rpcUrl: 'https://new-chain-rpc-url', + chainId: '0x9999', + ticker: 'NEW', + nickname: 'new-chain', + rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, + }; + + expect(() => + controller.upsertNetworkConfiguration(newNetworkConfiguration, {}), + ).toThrow( + 'referrer and source are required arguments for adding or updating a network configuration', + ); + }, + ); + }); + }); + + describe('removeNetworkConfigurations', () => { + it('should remove a network configuration', async () => { + const networkConfigurationId = 'networkConfigurationId'; + await withController( + { + state: { + networkConfigurations: { + [networkConfigurationId]: { + rpcUrl: 'https://test-rpc-url', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_chainName', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '1', + }, + }, + }, + }, + async ({ controller }) => { + expect( + Object.values(controller.store.getState().networkConfigurations), + ).toStrictEqual([ + { + rpcUrl: 'https://test-rpc-url', + ticker: 'old_rpc_ticker', + nickname: 'old_rpc_chainName', + rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, + chainId: '1', + }, + ]); + controller.removeNetworkConfiguration(networkConfigurationId); + expect( + controller.store.getState().networkConfigurations, + ).toStrictEqual({}); + }, + ); + }); + }); }); /** diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index b0afa083b..455fcdc1e 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,7 +1,6 @@ import { ObservableStore } from '@metamask/obs-store'; import { normalize as normalizeAddress } from 'eth-sig-util'; import { IPFS_DEFAULT_GATEWAY_URL } from '../../../shared/constants/network'; -import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets'; import { ThemeType } from '../../../shared/constants/preferences'; import { NETWORK_EVENTS } from './network'; @@ -12,7 +11,6 @@ export default class PreferencesController { * @typedef {object} PreferencesController * @param {object} opts - Overrides the defaults for the initial state of this.store * @property {object} store The stored object containing a users preferences, stored in local storage - * @property {Array} store.frequentRpcList A list of custom rpcs to provide the user * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {boolean} store.useNonceField The users preference for nonce field within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the @@ -25,7 +23,6 @@ export default class PreferencesController { */ constructor(opts = {}) { const initState = { - frequentRpcListDetail: [], useBlockie: false, useNonceField: false, usePhishDetect: true, @@ -399,67 +396,6 @@ export default class PreferencesController { return label; } - /** - * Adds custom RPC url to state. - * - * @param {string} rpcUrl - The RPC url to add to frequentRpcList. - * @param {string} chainId - The chainId of the selected network. - * @param {string} [ticker] - Ticker symbol of the selected network. - * @param {string} [nickname] - Nickname of the selected network. - * @param {object} [rpcPrefs] - Optional RPC preferences, such as the block explorer URL - */ - upsertToFrequentRpcList( - rpcUrl, - chainId, - ticker = 'ETH', - nickname = '', - rpcPrefs = {}, - ) { - const rpcList = this.getFrequentRpcListDetail(); - - const index = rpcList.findIndex((element) => { - return element.rpcUrl === rpcUrl; - }); - if (index !== -1) { - rpcList.splice(index, 1, { rpcUrl, chainId, ticker, nickname, rpcPrefs }); - return; - } - - if (!isPrefixedFormattedHexString(chainId)) { - throw new Error(`Invalid chainId: "${chainId}"`); - } - - rpcList.push({ rpcUrl, chainId, ticker, nickname, rpcPrefs }); - this.store.updateState({ frequentRpcListDetail: rpcList }); - } - - /** - * Removes custom RPC url from state. - * - * @param {string} url - The RPC url to remove from frequentRpcList. - * @returns {Promise} Promise resolving to updated frequentRpcList. - */ - async removeFromFrequentRpcList(url) { - const rpcList = this.getFrequentRpcListDetail(); - const index = rpcList.findIndex((element) => { - return element.rpcUrl === url; - }); - if (index !== -1) { - rpcList.splice(index, 1); - } - this.store.updateState({ frequentRpcListDetail: rpcList }); - return rpcList; - } - - /** - * Getter for the `frequentRpcListDetail` property. - * - * @returns {Array} An array of rpc urls. - */ - getFrequentRpcListDetail() { - return this.store.getState().frequentRpcListDetail; - } - /** * Updates the `featureFlags` property, which is an object. One property within that object will be set to a boolean. * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 1679cab45..f129de940 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -182,66 +182,6 @@ describe('preferences controller', function () { }); }); - describe('adding and removing from frequentRpcListDetail', function () { - it('should add custom RPC url to state', function () { - preferencesController.upsertToFrequentRpcList('rpc_url', '0x1'); - assert.deepEqual( - preferencesController.store.getState().frequentRpcListDetail, - [ - { - rpcUrl: 'rpc_url', - chainId: '0x1', - ticker: 'ETH', - nickname: '', - rpcPrefs: {}, - }, - ], - ); - preferencesController.upsertToFrequentRpcList('rpc_url', '0x1'); - assert.deepEqual( - preferencesController.store.getState().frequentRpcListDetail, - [ - { - rpcUrl: 'rpc_url', - chainId: '0x1', - ticker: 'ETH', - nickname: '', - rpcPrefs: {}, - }, - ], - ); - }); - - it('should throw if chainId is invalid', function () { - assert.throws(() => { - preferencesController.upsertToFrequentRpcList('rpc_url', '1'); - }, 'should throw on invalid chainId'); - }); - - it('should remove custom RPC url from state', function () { - preferencesController.upsertToFrequentRpcList('rpc_url', '0x1'); - assert.deepEqual( - preferencesController.store.getState().frequentRpcListDetail, - [ - { - rpcUrl: 'rpc_url', - chainId: '0x1', - ticker: 'ETH', - nickname: '', - rpcPrefs: {}, - }, - ], - ); - preferencesController.removeFromFrequentRpcList('other_rpc_url'); - preferencesController.removeFromFrequentRpcList('http://localhost:8545'); - preferencesController.removeFromFrequentRpcList('rpc_url'); - assert.deepEqual( - preferencesController.store.getState().frequentRpcListDetail, - [], - ); - }); - }); - describe('setUsePhishDetect', function () { it('should default to true', function () { const state = preferencesController.store.getState(); diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index b5f906862..920ea8254 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -9,17 +9,5 @@ */ const initialState = { config: {}, - PreferencesController: { - frequentRpcListDetail: [ - { - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - ticker: 'ETH', - nickname: 'Localhost 8545', - rpcPrefs: {}, - }, - ], - }, }; - export default initialState; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index 150c7d351..b35fe1e5c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -5,23 +5,22 @@ import { MESSAGE_TYPE, UNKNOWN_TICKER_SYMBOL, } from '../../../../../shared/constants/app'; -import { EVENT } from '../../../../../shared/constants/metametrics'; import { isPrefixedFormattedHexString, isSafeChainId, } from '../../../../../shared/modules/network.utils'; +import { EVENT } from '../../../../../shared/constants/metametrics'; const addEthereumChain = { methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], implementation: addEthereumChainHandler, hookNames: { - addCustomRpc: true, + upsertNetworkConfiguration: true, getCurrentChainId: true, getCurrentRpcUrl: true, - findCustomRpcBy: true, - updateRpcTarget: true, + findNetworkConfigurationBy: true, + setActiveNetwork: true, requestUserApproval: true, - sendMetrics: true, }, }; export default addEthereumChain; @@ -32,13 +31,12 @@ async function addEthereumChainHandler( _next, end, { - addCustomRpc, + upsertNetworkConfiguration, getCurrentChainId, getCurrentRpcUrl, - findCustomRpcBy, - updateRpcTarget, + findNetworkConfigurationBy, + setActiveNetwork, requestUserApproval, - sendMetrics, }, ) { if (!req.params?.[0] || typeof req.params[0] !== 'object') { @@ -138,7 +136,7 @@ async function addEthereumChainHandler( ); } - const existingNetwork = findCustomRpcBy({ chainId: _chainId }); + const existingNetwork = findNetworkConfigurationBy({ chainId: _chainId }); // if the request is to add a network that is already added and configured // with the same RPC gateway we shouldn't try to add it again. @@ -158,18 +156,18 @@ async function addEthereumChainHandler( // If this network is already added with but is not the currently selected network // Ask the user to switch the network try { - await updateRpcTarget( - await requestUserApproval({ - origin, - type: MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN, - requestData: { - rpcUrl: existingNetwork.rpcUrl, - chainId: existingNetwork.chainId, - nickname: existingNetwork.nickname, - ticker: existingNetwork.ticker, - }, - }), - ); + await requestUserApproval({ + origin, + type: MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN, + requestData: { + rpcUrl: existingNetwork.rpcUrl, + chainId: existingNetwork.chainId, + nickname: existingNetwork.nickname, + ticker: existingNetwork.ticker, + }, + }); + + await setActiveNetwork(existingNetwork.id); res.result = null; } catch (error) { // For the purposes of this method, it does not matter if the user @@ -242,32 +240,30 @@ async function addEthereumChainHandler( }), ); } - + let networkConfigurationId; try { - const customRpc = await requestUserApproval({ + await requestUserApproval({ origin, type: MESSAGE_TYPE.ADD_ETHEREUM_CHAIN, requestData: { chainId: _chainId, - blockExplorerUrl: firstValidBlockExplorerUrl, + rpcPrefs: { blockExplorerUrl: firstValidBlockExplorerUrl }, chainName: _chainName, rpcUrl: firstValidRPCUrl, ticker, }, }); - await addCustomRpc(customRpc); - sendMetrics({ - event: 'Custom Network Added', - category: EVENT.CATEGORIES.NETWORK, - referrer: { - url: origin, + + networkConfigurationId = await upsertNetworkConfiguration( + { + chainId: _chainId, + rpcPrefs: { blockExplorerUrl: firstValidBlockExplorerUrl }, + nickname: _chainName, + rpcUrl: firstValidRPCUrl, + ticker, }, - properties: { - chain_id: _chainId, - symbol: ticker, - source: EVENT.SOURCE.TRANSACTION.DAPP, - }, - }); + { source: EVENT.SOURCE.NETWORK.DAPP, referrer: origin }, + ); // Once the network has been added, the requested is considered successful res.result = null; @@ -277,18 +273,18 @@ async function addEthereumChainHandler( // Ask the user to switch the network try { - await updateRpcTarget( - await requestUserApproval({ - origin, - type: MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN, - requestData: { - rpcUrl: firstValidRPCUrl, - chainId: _chainId, - nickname: _chainName, - ticker, - }, - }), - ); + await requestUserApproval({ + origin, + type: MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN, + requestData: { + rpcUrl: firstValidRPCUrl, + chainId: _chainId, + nickname: _chainName, + ticker, + networkConfigurationId, + }, + }); + await setActiveNetwork(networkConfigurationId); } catch (error) { // For the purposes of this method, it does not matter if the user // declines to switch the selected network. However, other errors indicate diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js index 7c5edc155..65ba5a2b1 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js @@ -18,15 +18,15 @@ const switchEthereumChain = { implementation: switchEthereumChainHandler, hookNames: { getCurrentChainId: true, - findCustomRpcBy: true, + findNetworkConfigurationBy: true, setProviderType: true, - updateRpcTarget: true, + setActiveNetwork: true, requestUserApproval: true, }, }; export default switchEthereumChain; -function findExistingNetwork(chainId, findCustomRpcBy) { +function findExistingNetwork(chainId, findNetworkConfigurationBy) { if (chainId in CHAIN_ID_TO_TYPE_MAP) { return { chainId, @@ -37,7 +37,7 @@ function findExistingNetwork(chainId, findCustomRpcBy) { }; } - return findCustomRpcBy({ chainId }); + return findNetworkConfigurationBy({ chainId }); } async function switchEthereumChainHandler( @@ -47,9 +47,9 @@ async function switchEthereumChainHandler( end, { getCurrentChainId, - findCustomRpcBy, + findNetworkConfigurationBy, setProviderType, - updateRpcTarget, + setActiveNetwork, requestUserApproval, }, ) { @@ -95,7 +95,7 @@ async function switchEthereumChainHandler( ); } - const requestData = findExistingNetwork(_chainId, findCustomRpcBy); + const requestData = findExistingNetwork(_chainId, findNetworkConfigurationBy); if (requestData) { const currentChainId = getCurrentChainId(); if (currentChainId === _chainId) { @@ -114,7 +114,7 @@ async function switchEthereumChainHandler( ) { setProviderType(approvedRequestData.type); } else { - await updateRpcTarget(approvedRequestData); + await setActiveNetwork(approvedRequestData); } res.result = null; } catch (error) { diff --git a/app/scripts/metamask-controller.actions.test.js b/app/scripts/metamask-controller.actions.test.js index e15a84580..b23351233 100644 --- a/app/scripts/metamask-controller.actions.test.js +++ b/app/scripts/metamask-controller.actions.test.js @@ -222,27 +222,6 @@ describe('MetaMaskController', function () { }); }); - describe('#addCustomNetwork', function () { - const customRpc = { - chainId: '0x1', - chainName: 'DUMMY_CHAIN_NAME', - rpcUrl: 'DUMMY_RPCURL', - ticker: 'DUMMY_TICKER', - blockExplorerUrl: 'DUMMY_EXPLORER', - }; - it('two successive calls with custom RPC details give same result', async function () { - await metamaskController.addCustomNetwork(customRpc); - const rpcList1Length = - metamaskController.preferencesController.store.getState() - .frequentRpcListDetail.length; - await metamaskController.addCustomNetwork(customRpc); - const rpcList2Length = - metamaskController.preferencesController.store.getState() - .frequentRpcListDetail.length; - assert.equal(rpcList1Length, rpcList2Length); - }); - }); - describe('#updateTransactionSendFlowHistory', function () { it('two sequential calls with same history give same result', async function () { const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 983ee5119..61c5c8aa5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3,9 +3,9 @@ import pump from 'pump'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; import { JsonRpcEngine } from 'json-rpc-engine'; -import { debounce } from 'lodash'; import { createEngineStream } from 'json-rpc-middleware-stream'; import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware'; +import { debounce } from 'lodash'; import { KeyringController, keyringBuilderFactory, @@ -258,6 +258,8 @@ export default class MetamaskController extends EventEmitter { this.networkController = new NetworkController({ state: initState.NetworkController, infuraProjectId: opts.infuraProjectId, + trackMetaMetricsEvent: (...args) => + this.metaMetricsController.trackEvent(...args), }); this.networkController.initializeProvider(); this.provider = @@ -897,6 +899,7 @@ export default class MetamaskController extends EventEmitter { this.backupController = new BackupController({ preferencesController: this.preferencesController, addressBookController: this.addressBookController, + networkController: this.networkController, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), @@ -961,14 +964,17 @@ export default class MetamaskController extends EventEmitter { status === TransactionStatus.failed ) { const txMeta = this.txController.txStateManager.getTransaction(txId); - const frequentRpcListDetail = - this.preferencesController.getFrequentRpcListDetail(); let rpcPrefs = {}; if (txMeta.chainId) { - const rpcSettings = frequentRpcListDetail.find( - (rpc) => txMeta.chainId === rpc.chainId, + const { networkConfigurations } = + this.networkController.store.getState(); + const matchingNetworkConfig = Object.values( + networkConfigurations, + ).find( + (networkConfiguration) => + networkConfiguration.chainId === txMeta.chainId, ); - rpcPrefs = rpcSettings?.rpcPrefs ?? {}; + rpcPrefs = matchingNetworkConfig?.rpcPrefs ?? {}; } this.platform.showTransactionNotification(txMeta, rpcPrefs); @@ -1712,6 +1718,7 @@ export default class MetamaskController extends EventEmitter { txController, assetsContractController, backupController, + approvalController, } = this; return { @@ -1763,6 +1770,10 @@ export default class MetamaskController extends EventEmitter { markNotificationPopupAsAutomaticallyClosed: () => this.notificationManager.markAsAutomaticallyClosed(), + // approval + requestUserApproval: + approvalController.addAndShowApprovalRequest.bind(approvalController), + // primary HD keyring management addNewAccount: this.addNewAccount.bind(this), verifySeedPhrase: this.verifySeedPhrase.bind(this), @@ -1806,11 +1817,14 @@ export default class MetamaskController extends EventEmitter { networkController.setProviderType.bind(networkController), rollbackToPreviousProvider: networkController.rollbackToPreviousProvider.bind(networkController), - setCustomRpc: this.setCustomRpc.bind(this), - updateAndSetCustomRpc: this.updateAndSetCustomRpc.bind(this), - delCustomRpc: this.delCustomRpc.bind(this), - addCustomNetwork: this.addCustomNetwork.bind(this), - requestAddNetworkApproval: this.requestAddNetworkApproval.bind(this), + removeNetworkConfiguration: + networkController.removeNetworkConfiguration.bind(networkController), + setActiveNetwork: + networkController.setActiveNetwork.bind(networkController), + upsertNetworkConfiguration: + this.networkController.upsertNetworkConfiguration.bind( + this.networkController, + ), // PreferencesController setSelectedAddress: preferencesController.setSelectedAddress.bind( preferencesController, @@ -2304,57 +2318,6 @@ export default class MetamaskController extends EventEmitter { } } - async requestAddNetworkApproval(customRpc, originIsMetaMask) { - try { - await this.approvalController.addAndShowApprovalRequest({ - origin: 'metamask', - type: 'wallet_addEthereumChain', - requestData: { - chainId: customRpc.chainId, - blockExplorerUrl: customRpc.rpcPrefs.blockExplorerUrl, - chainName: customRpc.nickname, - rpcUrl: customRpc.rpcUrl, - ticker: customRpc.ticker, - imageUrl: customRpc.rpcPrefs.imageUrl, - }, - }); - } catch (error) { - if ( - !(originIsMetaMask && error.message === 'User rejected the request.') - ) { - throw error; - } - } - } - - async addCustomNetwork(customRpc, actionId) { - const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc; - - this.preferencesController.upsertToFrequentRpcList( - rpcUrl, - chainId, - ticker, - chainName, - { - blockExplorerUrl, - }, - ); - - this.metaMetricsController.trackEvent({ - event: 'Custom Network Added', - category: EVENT.CATEGORIES.NETWORK, - referrer: { - url: ORIGIN_METAMASK, - }, - properties: { - chain_id: chainId, - symbol: ticker, - source: EVENT.SOURCE.NETWORK.POPULAR_NETWORK_LIST, - }, - actionId, - }); - } - /** * Create a new Vault and restore an existent keyring. * @@ -2479,18 +2442,16 @@ export default class MetamaskController extends EventEmitter { */ async fetchInfoToSync() { // Preferences - const { - currentLocale, - frequentRpcList, - identities, - selectedAddress, - useTokenDetection, - } = this.preferencesController.store.getState(); + const { currentLocale, identities, selectedAddress, useTokenDetection } = + this.preferencesController.store.getState(); const isTokenDetectionInactiveInMainnet = !useTokenDetection && this.networkController.store.getState().provider.chainId === CHAIN_IDS.MAINNET; + + const { networkConfigurations } = this.networkController.store.getState(); + const { tokenList } = this.tokenListController.state; const caseInSensitiveTokenList = isTokenDetectionInactiveInMainnet ? STATIC_MAINNET_TOKEN_LIST @@ -2498,7 +2459,6 @@ export default class MetamaskController extends EventEmitter { const preferences = { currentLocale, - frequentRpcList, identities, selectedAddress, }; @@ -2574,6 +2534,7 @@ export default class MetamaskController extends EventEmitter { transactions, tokens: { allTokens: allERC20Tokens, allIgnoredTokens }, network: this.networkController.store.getState(), + networkConfigurations, }; } @@ -4039,40 +4000,24 @@ export default class MetamaskController extends EventEmitter { { origin }, ), - // Custom RPC-related - addCustomRpc: async ({ - chainId, - blockExplorerUrl, - ticker, - chainName, - rpcUrl, - } = {}) => { - await this.preferencesController.upsertToFrequentRpcList( - rpcUrl, - chainId, - ticker, - chainName, - { - blockExplorerUrl, - }, - ); - }, - findCustomRpcBy: this.findCustomRpcBy.bind(this), getCurrentChainId: () => this.networkController.store.getState().provider.chainId, - getCurrentRpcUrl: + getCurrentRpcUrl: () => this.networkController.store.getState().provider.rpcUrl, + // network configuration-related + getNetworkConfigurations: () => + this.networkController.store.getState().networkConfigurations, + upsertNetworkConfiguration: + this.networkController.upsertNetworkConfiguration.bind( + this.networkController, + ), + setActiveNetwork: this.networkController.setActiveNetwork.bind( + this.networkController, + ), + findNetworkConfigurationBy: this.findNetworkConfigurationBy.bind(this), setProviderType: this.networkController.setProviderType.bind( this.networkController, ), - updateRpcTarget: ({ rpcUrl, chainId, ticker, nickname }) => { - this.networkController.setRpcTarget( - rpcUrl, - chainId, - ticker, - nickname, - ); - }, // Web3 shim-related getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( @@ -4428,120 +4373,24 @@ export default class MetamaskController extends EventEmitter { // CONFIG //============================================================================= - // Log blocks - /** - * A method for selecting a custom URL for an ethereum RPC provider and updating it - * - * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. - * @param {string} chainId - The chainId of the selected network. - * @param {string} ticker - The ticker symbol of the selected network. - * @param {string} [nickname] - Nickname of the selected network. - * @param {object} [rpcPrefs] - RPC preferences. - * @param {string} [rpcPrefs.blockExplorerUrl] - URL of block explorer for the chain. - * @returns {Promise} The RPC Target URL confirmed. - */ - async updateAndSetCustomRpc( - rpcUrl, - chainId, - ticker = 'ETH', - nickname, - rpcPrefs, - ) { - this.networkController.setRpcTarget( - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - ); - await this.preferencesController.upsertToFrequentRpcList( - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - ); - return rpcUrl; - } - - /** - * A method for selecting a custom URL for an ethereum RPC provider. - * - * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. - * @param {string} chainId - The chainId of the selected network. - * @param {string} ticker - The ticker symbol of the selected network. - * @param {string} nickname - Optional nickname of the selected network. - * @param rpcPrefs - * @returns {Promise} The RPC Target URL confirmed. - */ - async setCustomRpc( - rpcUrl, - chainId, - ticker = 'ETH', - nickname = '', - rpcPrefs = {}, - ) { - const frequentRpcListDetail = - this.preferencesController.getFrequentRpcListDetail(); - const rpcSettings = frequentRpcListDetail.find( - (rpc) => rpcUrl === rpc.rpcUrl, - ); - - if (rpcSettings) { - this.networkController.setRpcTarget( - rpcSettings.rpcUrl, - rpcSettings.chainId, - rpcSettings.ticker, - rpcSettings.nickname, - rpcPrefs, - ); - } else { - this.networkController.setRpcTarget( - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - ); - await this.preferencesController.upsertToFrequentRpcList( - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - ); - } - return rpcUrl; - } - - /** - * A method for deleting a selected custom URL. - * - * @param {string} rpcUrl - A RPC URL to delete. - */ - async delCustomRpc(rpcUrl) { - await this.preferencesController.removeFromFrequentRpcList(rpcUrl); - } - - /** - * Returns the first RPC info object that matches at least one field of the + * Returns the first network configuration object that matches at least one field of the * provided search criteria. Returns null if no match is found * * @param {object} rpcInfo - The RPC endpoint properties and values to check. - * @returns {object} rpcInfo found in the frequentRpcList + * @returns {object} rpcInfo found in the network configurations list */ - findCustomRpcBy(rpcInfo) { - const frequentRpcListDetail = - this.preferencesController.getFrequentRpcListDetail(); - for (const existingRpcInfo of frequentRpcListDetail) { - for (const key of Object.keys(rpcInfo)) { - if (existingRpcInfo[key] === rpcInfo[key]) { - return existingRpcInfo; - } - } - } - return null; + findNetworkConfigurationBy(rpcInfo) { + const { networkConfigurations } = this.networkController.store.getState(); + const networkConfiguration = Object.values(networkConfigurations).find( + (configuration) => { + return Object.keys(rpcInfo).some((key) => { + return configuration[key] === rpcInfo[key]; + }); + }, + ); + + return networkConfiguration || null; } /** diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index f8ecf03e5..43d0e5b20 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -19,35 +19,6 @@ import { deferredPromise } from './lib/util'; const Ganache = require('../../test/e2e/ganache'); -const NOTIFICATION_ID = 'NHL8f2eSSTn9TKBamRLiU'; - -const firstTimeState = { - config: {}, - NetworkController: { - provider: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, - }, - NotificationController: { - notifications: { - [NOTIFICATION_ID]: { - id: NOTIFICATION_ID, - origin: 'local:http://localhost:8086/', - createdDate: 1652967897732, - readDate: null, - message: 'Hello, http://localhost:8086!', - }, - }, - }, -}; - const ganacheServer = new Ganache(); const browserPolyfillMock = { @@ -120,8 +91,78 @@ const TEST_ADDRESS_3 = '0xeb9e64b93097bc15f01f13eae97015c57ab64823'; const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'; const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; -const CUSTOM_RPC_URL = 'http://localhost:8545'; -const CUSTOM_RPC_CHAIN_ID = '0x539'; + +const NOTIFICATION_ID = 'NHL8f2eSSTn9TKBamRLiU'; + +const ALT_MAINNET_RPC_URL = 'http://localhost:8545'; +const POLYGON_RPC_URL = 'https://polygon.llamarpc.com'; +const POLYGON_RPC_URL_2 = 'https://polygon-rpc.com'; + +const NETWORK_CONFIGURATION_ID_1 = 'networkConfigurationId1'; +const NETWORK_CONFIGURATION_ID_2 = 'networkConfigurationId2'; +const NETWORK_CONFIGURATION_ID_3 = 'networkConfigurationId3'; + +const ETH = 'ETH'; +const MATIC = 'MATIC'; + +const POLYGON_CHAIN_ID = '0x89'; +const MAINNET_CHAIN_ID = '0x1'; + +const firstTimeState = { + config: {}, + NetworkController: { + provider: { + type: NETWORK_TYPES.RPC, + rpcUrl: ALT_MAINNET_RPC_URL, + chainId: MAINNET_CHAIN_ID, + ticker: ETH, + nickname: 'Alt Mainnet', + id: NETWORK_CONFIGURATION_ID_1, + }, + networkConfigurations: { + [NETWORK_CONFIGURATION_ID_1]: { + rpcUrl: ALT_MAINNET_RPC_URL, + type: NETWORK_TYPES.RPC, + chainId: MAINNET_CHAIN_ID, + ticker: ETH, + nickname: 'Alt Mainnet', + id: NETWORK_CONFIGURATION_ID_1, + }, + [NETWORK_CONFIGURATION_ID_2]: { + rpcUrl: POLYGON_RPC_URL, + type: NETWORK_TYPES.RPC, + chainId: POLYGON_CHAIN_ID, + ticker: MATIC, + nickname: 'Polygon', + id: NETWORK_CONFIGURATION_ID_2, + }, + [NETWORK_CONFIGURATION_ID_3]: { + rpcUrl: POLYGON_RPC_URL_2, + type: NETWORK_TYPES.RPC, + chainId: POLYGON_CHAIN_ID, + ticker: MATIC, + nickname: 'Alt Polygon', + id: NETWORK_CONFIGURATION_ID_1, + }, + }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + }, + NotificationController: { + notifications: { + [NOTIFICATION_ID]: { + id: NOTIFICATION_ID, + origin: 'local:http://localhost:8086/', + createdDate: 1652967897732, + readDate: null, + message: 'Hello, http://localhost:8086!', + }, + }, + }, +}; describe('MetaMaskController', function () { let metamaskController; @@ -700,26 +741,6 @@ describe('MetaMaskController', function () { }); }); - describe('#setCustomRpc', function () { - it('returns custom RPC that when called', async function () { - const rpcUrl = await metamaskController.setCustomRpc( - CUSTOM_RPC_URL, - CUSTOM_RPC_CHAIN_ID, - ); - assert.equal(rpcUrl, CUSTOM_RPC_URL); - }); - - it('changes the network controller rpc', async function () { - await metamaskController.setCustomRpc( - CUSTOM_RPC_URL, - CUSTOM_RPC_CHAIN_ID, - ); - const networkControllerState = - metamaskController.networkController.store.getState(); - assert.equal(networkControllerState.provider.rpcUrl, CUSTOM_RPC_URL); - }); - }); - describe('#addNewAccount', function () { it('errors when an primary keyring is does not exist', async function () { const addNewAccount = metamaskController.addNewAccount(); @@ -1610,5 +1631,97 @@ describe('MetaMaskController', function () { 'tokenDetails should include a balance', ); }); + + describe('findNetworkConfigurationBy', function () { + it('returns null if passed an object containing a valid networkConfiguration key but no matching value is found', function () { + assert.strictEqual( + metamaskController.findNetworkConfigurationBy({ + chainId: '0xnone', + }), + null, + ); + }); + it('returns null if passed an object containing an invalid networkConfiguration key', function () { + assert.strictEqual( + metamaskController.findNetworkConfigurationBy({ + invalidKey: '0xnone', + }), + null, + ); + }); + + it('returns matching networkConfiguration when passed a chainId that matches an existing configuration', function () { + assert.deepStrictEqual( + metamaskController.findNetworkConfigurationBy({ + chainId: MAINNET_CHAIN_ID, + }), + { + chainId: MAINNET_CHAIN_ID, + nickname: 'Alt Mainnet', + id: NETWORK_CONFIGURATION_ID_1, + rpcUrl: ALT_MAINNET_RPC_URL, + ticker: ETH, + type: NETWORK_TYPES.RPC, + }, + ); + }); + + it('returns matching networkConfiguration when passed a ticker that matches an existing configuration', function () { + assert.deepStrictEqual( + metamaskController.findNetworkConfigurationBy({ + ticker: MATIC, + }), + { + rpcUrl: POLYGON_RPC_URL, + type: NETWORK_TYPES.RPC, + chainId: POLYGON_CHAIN_ID, + ticker: MATIC, + nickname: 'Polygon', + id: NETWORK_CONFIGURATION_ID_2, + }, + ); + }); + + it('returns matching networkConfiguration when passed a nickname that matches an existing configuration', function () { + assert.deepStrictEqual( + metamaskController.findNetworkConfigurationBy({ + nickname: 'Alt Mainnet', + }), + { + chainId: MAINNET_CHAIN_ID, + nickname: 'Alt Mainnet', + id: NETWORK_CONFIGURATION_ID_1, + rpcUrl: ALT_MAINNET_RPC_URL, + ticker: ETH, + type: NETWORK_TYPES.RPC, + }, + ); + }); + + it('returns null if passed an object containing mismatched networkConfiguration key/value combination', function () { + assert.deepStrictEqual( + metamaskController.findNetworkConfigurationBy({ + nickname: MAINNET_CHAIN_ID, + }), + null, + ); + }); + + it('returns the first networkConfiguration added if passed an key/value combination for which there are multiple matching configurations', function () { + assert.deepStrictEqual( + metamaskController.findNetworkConfigurationBy({ + chainId: POLYGON_CHAIN_ID, + }), + { + rpcUrl: POLYGON_RPC_URL, + type: NETWORK_TYPES.RPC, + chainId: POLYGON_CHAIN_ID, + ticker: MATIC, + nickname: 'Polygon', + id: NETWORK_CONFIGURATION_ID_2, + }, + ); + }); + }); }); }); diff --git a/app/scripts/migrations/082.test.js b/app/scripts/migrations/082.test.js new file mode 100644 index 000000000..6f221c5c6 --- /dev/null +++ b/app/scripts/migrations/082.test.js @@ -0,0 +1,598 @@ +import { v4 } from 'uuid'; +import { migrate, version } from './082'; + +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + +describe('migration #82', () => { + 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: 81, + }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version, + }); + }); + + it('should migrate the network configurations from an array on the PreferencesController to an object on the NetworkController', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + frequentRpcListDetail: [ + { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + }, + { + chainId: '0x4e454152', + nickname: 'Aurora Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://aurorascan.dev/', + }, + rpcUrl: + 'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'Aurora ETH', + }, + { + chainId: '0x38', + nickname: + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + }, + ], + }, + NetworkController: {}, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version, + }, + data: { + PreferencesController: {}, + 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', + }, + }, + }, + }, + }); + }); + + it('should not change data other than removing `frequentRpcListDetail` and adding `networkConfigurations` on the PreferencesController and NetworkController respectively', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + transactionSecurityCheckEnabled: false, + useBlockie: false, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: false, + useNonceField: false, + frequentRpcListDetail: [ + { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + }, + { + chainId: '0x4e454152', + nickname: 'Aurora Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://aurorascan.dev/', + }, + rpcUrl: + 'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'Aurora ETH', + }, + { + chainId: '0x38', + nickname: + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + }, + ], + }, + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version, + }, + data: { + PreferencesController: { + transactionSecurityCheckEnabled: false, + useBlockie: false, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: false, + useNonceField: false, + }, + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + 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', + }, + }, + }, + }, + }); + }); + + it('should migrate the network configurations from an array on the PreferencesController to an object on the NetworkController and not include any properties on the frequentRpcListDetail objects that are not included in the list: [chainId, nickname, rpcPrefs, rpcUrl, ticker]', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + frequentRpcListDetail: [ + { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + invalidKey: 'invalidKey', + anotherInvalidKey: 'anotherInvalidKey', + }, + { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + randomInvalidKey: 'randomInvalidKey', + randomInvalidKey2: 'randomInvalidKey2', + }, + ], + }, + NetworkController: {}, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version, + }, + data: { + PreferencesController: {}, + 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', + }, + }, + }, + }, + }); + }); + + it('should migrate the network configurations from an array on the PreferencesController to an object on the NetworkController even if frequentRpcListDetail entries do not include all members of list [chainId, nickname, rpcPrefs, rpcUrl, ticker]', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + frequentRpcListDetail: [ + { + chainId: '0x539', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + { + chainId: '0xa4b1', + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + }, + ], + }, + NetworkController: {}, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version, + }, + data: { + PreferencesController: {}, + NetworkController: { + networkConfigurations: { + 'network-configuration-id-1': { + chainId: '0x539', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + nickname: undefined, + rpcPrefs: undefined, + }, + 'network-configuration-id-2': { + chainId: '0xa4b1', + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + nickname: undefined, + rpcPrefs: undefined, + }, + }, + }, + }, + }); + }); + + it('should not change anything if any PreferencesController.frequentRpcListDetail entries are not objects', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + transactionSecurityCheckEnabled: false, + useBlockie: false, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: false, + useNonceField: false, + frequentRpcListDetail: [ + { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + 'invalid entry type', + 1, + ], + }, + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('should not change anything if there is no frequentRpcListDetail property on PreferencesController', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + PreferencesController: { + transactionSecurityCheckEnabled: false, + useBlockie: false, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: false, + useNonceField: false, + }, + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + 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); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('should change nothing if PreferencesController is undefined', async () => { + const oldStorage = { + meta: { + version: 81, + }, + data: { + NetworkController: { + network: '1', + networkDetails: { + EIPS: { + 1559: true, + }, + }, + previousProviderStore: { + chainId: '0x89', + nickname: 'Polygon Mainnet', + rpcPrefs: {}, + rpcUrl: + 'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'MATIC', + type: 'rpc', + }, + provider: { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); +}); diff --git a/app/scripts/migrations/082.ts b/app/scripts/migrations/082.ts new file mode 100644 index 000000000..fdda1fb8d --- /dev/null +++ b/app/scripts/migrations/082.ts @@ -0,0 +1,76 @@ +import { cloneDeep } from 'lodash'; +import { hasProperty, isObject } from '@metamask/utils'; +import { v4 } from 'uuid'; + +export const version = 82; + +/** + * Migrate the frequentRpcListDetail from the PreferencesController to the NetworkController, convert it from an array to an object + * keyed by random uuids. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if ( + !hasProperty(state, 'PreferencesController') || + !isObject(state.PreferencesController) || + !isObject(state.NetworkController) || + !hasProperty(state.PreferencesController, 'frequentRpcListDetail') || + !Array.isArray(state.PreferencesController.frequentRpcListDetail) || + !state.PreferencesController.frequentRpcListDetail.every(isObject) + ) { + return state; + } + const { PreferencesController, NetworkController } = state; + const { frequentRpcListDetail } = PreferencesController; + if (!Array.isArray(frequentRpcListDetail)) { + return state; + } + + const networkConfigurations = frequentRpcListDetail.reduce( + ( + networkConfigurationsAcc, + { rpcUrl, chainId, ticker, nickname, rpcPrefs }, + ) => { + const networkConfigurationId = v4(); + return { + ...networkConfigurationsAcc, + [networkConfigurationId]: { + rpcUrl, + chainId, + ticker, + rpcPrefs, + nickname, + }, + }; + }, + {}, + ); + + delete PreferencesController.frequentRpcListDetail; + + return { + ...state, + NetworkController: { + ...NetworkController, + networkConfigurations, + }, + PreferencesController: { + ...PreferencesController, + }, + }; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 6bc0d0576..fdd29924c 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -85,6 +85,7 @@ import * as m078 from './078'; import m079 from './079'; import m080 from './080'; import * as m081 from './081'; +import * as m082 from './082'; const migrations = [ m002, @@ -167,6 +168,7 @@ const migrations = [ m079, m080, m081, + m082, ]; export default migrations; diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index 62423b94f..4471457a4 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -426,6 +426,7 @@ export const EVENT = { NETWORK: { CUSTOM_NETWORK_FORM: 'custom_network_form', POPULAR_NETWORK_LIST: 'popular_network_list', + DAPP: 'dapp', }, SWAPS: { MAIN_VIEW: 'Main View', diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 7f0804685..58ca0ce1c 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -266,15 +266,18 @@ export const BUILT_IN_NETWORKS = { networkId: NETWORK_IDS.GOERLI, chainId: CHAIN_IDS.GOERLI, ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], + blockExplorerUrl: `https://${NETWORK_TYPES.GOERLI}.etherscan.io`, }, [NETWORK_TYPES.SEPOLIA]: { networkId: NETWORK_IDS.SEPOLIA, chainId: CHAIN_IDS.SEPOLIA, ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA], + blockExplorerUrl: `https://${NETWORK_TYPES.SEPOLIA}.etherscan.io`, }, [NETWORK_TYPES.MAINNET]: { networkId: NETWORK_IDS.MAINNET, chainId: CHAIN_IDS.MAINNET, + blockExplorerUrl: `https://etherscan.io`, }, [NETWORK_TYPES.LOCALHOST]: { networkId: NETWORK_IDS.LOCALHOST, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 64c4c6e61..8f08aee96 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -76,7 +76,14 @@ "provider": { "type": "rpc", "chainId": "0x5", - "ticker": "ETH" + "ticker": "ETH", + "id": "testNetworkConfigurationId" + }, + "networkConfigurations": { + "testNetworkConfigurationId": { + "rpcUrl": "https://testrpc.com", + "chainId": "0x1" + } }, "keyrings": [ { diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index eff689cc2..1c45d4992 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -183,6 +183,16 @@ function defaultFixture() { ticker: 'ETH', type: 'rpc', }, + networkConfigurations: { + networkConfigurationId: { + chainId: CHAIN_IDS.LOCALHOST, + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + networkConfigurationId: 'networkConfigurationId', + }, + }, }, OnboardingController: { completedOnboarding: true, @@ -201,15 +211,6 @@ function defaultFixture() { showIncomingTransactions: true, }, forgottenPassword: false, - frequentRpcListDetail: [ - { - chainId: CHAIN_IDS.LOCALHOST, - nickname: 'Localhost 8545', - rpcPrefs: {}, - rpcUrl: 'http://localhost:8545', - ticker: 'ETH', - }, - ], identities: { '0x5cfe73b6021e818b776b421b1c4db2474086a7e1': { address: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', @@ -313,6 +314,16 @@ function onboardingFixture() { chainId: CHAIN_IDS.LOCALHOST, nickname: 'Localhost 8545', }, + networkConfigurations: { + networkConfigurationId: { + chainId: CHAIN_IDS.LOCALHOST, + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + networkConfigurationId: 'networkConfigurationId', + }, + }, }, PreferencesController: { advancedGasFee: null, @@ -322,15 +333,6 @@ function onboardingFixture() { showIncomingTransactions: true, }, forgottenPassword: false, - frequentRpcListDetail: [ - { - chainId: CHAIN_IDS.LOCALHOST, - nickname: 'Localhost 8545', - rpcPrefs: {}, - rpcUrl: 'http://localhost:8545', - ticker: 'ETH', - }, - ], identities: {}, infuraBlocked: false, ipfsGateway: 'dweb.link', diff --git a/test/e2e/tests/add-custom-network.spec.js b/test/e2e/tests/add-custom-network.spec.js index c46ca2694..3564f9f2e 100644 --- a/test/e2e/tests/add-custom-network.spec.js +++ b/test/e2e/tests/add-custom-network.spec.js @@ -263,22 +263,22 @@ describe('Custom network', function () { assert.equal( await networkName.getText(), networkNAME, - 'Network name is not correct displayed', + 'Network name is not correctly displayed', ); assert.equal( await networkUrl.getText(), networkURL, - 'Network Url is not correct displayed', + 'Network Url is not correctly displayed', ); assert.equal( await chainIdElement.getText(), chainID.toString(), - 'Chain Id is not correct displayed', + 'Chain Id is not correctly displayed', ); assert.equal( await currencySymbol.getText(), currencySYMBOL, - 'Currency symbol is not correct displayed', + 'Currency symbol is not correctly displayed', ); await driver.clickElement({ tag: 'a', text: 'View all details' }); @@ -353,20 +353,21 @@ describe('Custom network', function () { }, ); }); + it('Add a custom network and then delete that same network', async function () { await withFixtures( { fixtures: new FixtureBuilder() - .withPreferencesController({ - frequentRpcListDetail: [ - { + .withNetworkController({ + networkConfigurations: { + networkConfigurationId: { rpcUrl: networkURL, chainId: chainID, - ticker: currencySYMBOL, nickname: networkNAME, + ticker: currencySYMBOL, rpcPrefs: {}, }, - ], + }, }) .build(), ganacheOptions, diff --git a/test/e2e/tests/backup-restore.spec.js b/test/e2e/tests/backup-restore.spec.js index 767e3fbcc..cf90718a5 100644 --- a/test/e2e/tests/backup-restore.spec.js +++ b/test/e2e/tests/backup-restore.spec.js @@ -87,7 +87,7 @@ describe('Backup and Restore', function () { assert.notEqual(info, null); // Verify Json assert.equal( - info?.preferences?.frequentRpcListDetail[0].chainId, + Object.values(info?.network?.networkConfigurations)?.[0].chainId, '0x539', ); }, diff --git a/test/e2e/tests/custom-rpc-history.spec.js b/test/e2e/tests/custom-rpc-history.spec.js index 34a0abc01..8696d2f7f 100644 --- a/test/e2e/tests/custom-rpc-history.spec.js +++ b/test/e2e/tests/custom-rpc-history.spec.js @@ -190,23 +190,23 @@ describe('Stores custom RPC history', function () { await withFixtures( { fixtures: new FixtureBuilder() - .withPreferencesController({ - frequentRpcListDetail: [ - { + .withNetworkController({ + networkConfigurations: { + networkConfigurationId: { rpcUrl: 'http://127.0.0.1:8545/1', chainId: '0x539', ticker: 'ETH', nickname: 'http://127.0.0.1:8545/1', rpcPrefs: {}, }, - { + networkConfigurationId2: { rpcUrl: 'http://127.0.0.1:8545/2', chainId: '0x539', ticker: 'ETH', nickname: 'http://127.0.0.1:8545/2', rpcPrefs: {}, }, - ], + }, }) .build(), ganacheOptions, @@ -238,23 +238,23 @@ describe('Stores custom RPC history', function () { await withFixtures( { fixtures: new FixtureBuilder() - .withPreferencesController({ - frequentRpcListDetail: [ - { + .withNetworkController({ + networkConfigurations: { + networkConfigurationId: { rpcUrl: 'http://127.0.0.1:8545/1', chainId: '0x539', ticker: 'ETH', nickname: 'http://127.0.0.1:8545/1', rpcPrefs: {}, }, - { + networkConfigurationId2: { rpcUrl: 'http://127.0.0.1:8545/2', chainId: '0x539', ticker: 'ETH', nickname: 'http://127.0.0.1:8545/2', rpcPrefs: {}, }, - ], + }, }) .build(), ganacheOptions, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 2c10677f3..b0dd8eaee 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -270,7 +270,7 @@ export const createSwapsMockStore = () => { accounts: ['0xd85a4b6a394794842887b8284293d69163007bbb'], }, ], - frequentRpcListDetail: [], + networkConfigurations: {}, tokens: [ { erc20: true, diff --git a/ui/components/app/add-network/add-network.js b/ui/components/app/add-network/add-network.js index e4cfc23d9..165359087 100644 --- a/ui/components/app/add-network/add-network.js +++ b/ui/components/app/add-network/add-network.js @@ -21,7 +21,7 @@ import Tooltip from '../../ui/tooltip'; import IconWithFallback from '../../ui/icon-with-fallback'; import IconBorder from '../../ui/icon-border'; import { - getFrequentRpcListDetail, + getNetworkConfigurations, getUnapprovedConfirmations, } from '../../../selectors'; @@ -29,8 +29,9 @@ import { ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_POPUP, MESSAGE_TYPE, + ORIGIN_METAMASK, } from '../../../../shared/constants/app'; -import { requestAddNetworkApproval } from '../../../store/actions'; +import { requestUserApproval } from '../../../store/actions'; import Popover from '../../ui/popover'; import ConfirmationPage from '../../../pages/confirmation/confirmation'; import { FEATURED_RPCS } from '../../../../shared/constants/network'; @@ -38,14 +39,15 @@ import { ADD_NETWORK_ROUTE } from '../../../helpers/constants/routes'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { Icon, ICON_NAMES, ICON_SIZES } from '../../component-library'; +import { EVENT } from '../../../../shared/constants/metametrics'; const AddNetwork = () => { const t = useContext(I18nContext); const dispatch = useDispatch(); const history = useHistory(); - const frequentRpcList = useSelector(getFrequentRpcListDetail); + const networkConfigurations = useSelector(getNetworkConfigurations); - const frequentRpcListChainIds = Object.values(frequentRpcList).map( + const networkConfigurationChainIds = Object.values(networkConfigurations).map( (net) => net.chainId, ); @@ -55,8 +57,8 @@ const AddNetwork = () => { a.nickname > b.nickname ? 1 : -1, ).slice(0, FEATURED_RPCS.length); - const notFrequentRpcNetworks = nets.filter( - (net) => frequentRpcListChainIds.indexOf(net.chainId) === -1, + const notExistingNetworkConfigurations = nets.filter( + (net) => networkConfigurationChainIds.indexOf(net.chainId) === -1, ); const unapprovedConfirmations = useSelector(getUnapprovedConfirmations); const [showPopover, setShowPopover] = useState(false); @@ -80,7 +82,7 @@ const AddNetwork = () => { return ( <> - {Object.keys(notFrequentRpcNetworks).length === 0 ? ( + {Object.keys(notExistingNetworkConfigurations).length === 0 ? ( { > {t('popularCustomNetworks')} - {notFrequentRpcNetworks.map((item, index) => ( + {notExistingNetworkConfigurations.map((item, index) => ( { @@ -250,7 +252,22 @@ const AddNetwork = () => { type="inline" className="add-network__add-button" onClick={async () => { - await dispatch(requestAddNetworkApproval(item, true)); + await dispatch( + requestUserApproval({ + origin: ORIGIN_METAMASK, + type: MESSAGE_TYPE.ADD_ETHEREUM_CHAIN, + requestData: { + chainId: item.chainId, + rpcUrl: item.rpcUrl, + ticker: item.ticker, + rpcPrefs: item.rpcPrefs, + imageUrl: item.rpcPrefs?.imageUrl, + chainName: item.nickname, + referrer: ORIGIN_METAMASK, + source: EVENT.SOURCE.NETWORK.POPULAR_NETWORK_LIST, + }, + }), + ); }} > {t('add')} diff --git a/ui/components/app/add-network/add-network.test.js b/ui/components/app/add-network/add-network.test.js index d0272608f..1bfd6dc26 100644 --- a/ui/components/app/add-network/add-network.test.js +++ b/ui/components/app/add-network/add-network.test.js @@ -6,24 +6,22 @@ import mockState from '../../../../test/data/mock-state.json'; import AddNetwork from './add-network'; jest.mock('../../../selectors', () => ({ - getFrequentRpcListDetail: () => ({ - frequentRpcList: [ - { - chainId: '0x539', - nickname: 'Localhost 8545', - rpcPrefs: {}, - rpcUrl: 'http://localhost:8545', - ticker: 'ETH', - }, - { - chainId: '0xA4B1', - nickname: 'Arbitrum One', - rpcPrefs: { blockExplorerUrl: 'https://explorer.arbitrum.io' }, - rpcUrl: - 'https://arbitrum-mainnet.infura.io/v3/7e127583378c4732a858df2550aff333', - ticker: 'AETH', - }, - ], + getNetworkConfigurations: () => ({ + networkConfigurationId: { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + networkConfigurationId2: { + chainId: '0xA4B1', + nickname: 'Arbitrum One', + rpcPrefs: { blockExplorerUrl: 'https://explorer.arbitrum.io' }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/7e127583378c4732a858df2550aff333', + ticker: 'AETH', + }, }), getUnapprovedConfirmations: jest.fn(), getTheme: () => 'light', diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index 41bcc2435..f8c7e6e97 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { compose } from 'redux'; +import { pickBy } from 'lodash'; import Button from '../../ui/button'; import * as actions from '../../../store/actions'; import { openAlert as displayInvalidCustomNetworkAlert } from '../../../ducks/alerts/invalid-custom-network'; @@ -50,7 +51,7 @@ function mapStateToProps(state) { return { provider: state.metamask.provider, shouldShowTestNetworks: getShowTestNetworks(state), - frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], + networkConfigurations: state.metamask.networkConfigurations, networkDropdownOpen: state.appState.networkDropdownOpen, showTestnetMessageInDropdown: state.metamask.showTestnetMessageInDropdown, }; @@ -61,8 +62,8 @@ function mapDispatchToProps(dispatch) { setProviderType: (type) => { dispatch(actions.setProviderType(type)); }, - setRpcTarget: (target, chainId, ticker, nickname) => { - dispatch(actions.setRpcTarget(target, chainId, ticker, nickname)); + setActiveNetwork: (networkConfigurationId) => { + dispatch(actions.setActiveNetwork(networkConfigurationId)); }, hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), displayInvalidCustomNetworkAlert: (networkName) => { @@ -95,9 +96,9 @@ class NetworkDropdown extends Component { ticker: PropTypes.string, }).isRequired, setProviderType: PropTypes.func.isRequired, - setRpcTarget: PropTypes.func.isRequired, + setActiveNetwork: PropTypes.func.isRequired, hideNetworkDropdown: PropTypes.func.isRequired, - frequentRpcListDetail: PropTypes.array.isRequired, + networkConfigurations: PropTypes.object.isRequired, shouldShowTestNetworks: PropTypes.bool, networkDropdownOpen: PropTypes.bool.isRequired, displayInvalidCustomNetworkAlert: PropTypes.func.isRequired, @@ -153,70 +154,69 @@ class NetworkDropdown extends Component { ); } - renderCustomRpcList(rpcListDetail, provider, opts = {}) { - const reversedRpcListDetail = rpcListDetail.slice().reverse(); - - return reversedRpcListDetail.map((entry) => { - const { rpcUrl, chainId, ticker = 'ETH', nickname = '' } = entry; - const isCurrentRpcTarget = - provider.type === NETWORK_TYPES.RPC && rpcUrl === provider.rpcUrl; - - return ( - this.props.hideNetworkDropdown()} - onClick={() => { - if (isPrefixedFormattedHexString(chainId)) { - this.props.setRpcTarget(rpcUrl, chainId, ticker, nickname); - } else { - this.props.displayInvalidCustomNetworkAlert(nickname || rpcUrl); - } - }} - style={{ - fontSize: '16px', - lineHeight: '20px', - padding: '16px', - }} - > - {isCurrentRpcTarget ? ( - - ) : ( -
✓
- )} - - { + const { rpcUrl, chainId, nickname = '', id } = networkConfiguration; + const isCurrentRpcTarget = + provider.type === NETWORK_TYPES.RPC && rpcUrl === provider.rpcUrl; + return ( + this.props.hideNetworkDropdown()} + onClick={() => { + if (isPrefixedFormattedHexString(chainId)) { + this.props.setActiveNetwork(networkConfigurationId); + } else { + this.props.displayInvalidCustomNetworkAlert(nickname || rpcUrl); + } + }} style={{ - color: isCurrentRpcTarget - ? 'var(--color-text-default)' - : 'var(--color-text-alternative)', + fontSize: '16px', + lineHeight: '20px', + padding: '16px', }} > - {nickname || rpcUrl} - - {isCurrentRpcTarget ? null : ( - { - e.stopPropagation(); - this.props.showConfirmDeleteNetworkModal({ - target: rpcUrl, - onConfirm: () => undefined, - }); - }} + {isCurrentRpcTarget ? ( + + ) : ( +
✓
+ )} + - )} -
- ); - }); + + {nickname || rpcUrl} + + {isCurrentRpcTarget ? null : ( + { + e.stopPropagation(); + this.props.showConfirmDeleteNetworkModal({ + target: id, + onConfirm: () => undefined, + }); + }} + /> + )} + + ); + }, + ); } getNetworkName() { @@ -283,14 +283,18 @@ class NetworkDropdown extends Component { shouldShowTestNetworks, showTestnetMessageInDropdown, hideTestNetMessage, + networkConfigurations, } = this.props; - const rpcListDetail = this.props.frequentRpcListDetail; - const rpcListDetailWithoutLocalHost = rpcListDetail.filter( - (rpc) => rpc.rpcUrl !== LOCALHOST_RPC_URL, + + const rpcListDetailWithoutLocalHost = pickBy( + networkConfigurations, + (config) => config.rpcUrl !== LOCALHOST_RPC_URL, ); - const rpcListDetailForLocalHost = rpcListDetail.filter( - (rpc) => rpc.rpcUrl === LOCALHOST_RPC_URL, + const rpcListDetailForLocalHost = pickBy( + networkConfigurations, + (config) => config.rpcUrl === LOCALHOST_RPC_URL, ); + const isOpen = this.props.networkDropdownOpen; const { t } = this.context; diff --git a/ui/components/app/dropdowns/network-dropdown.test.js b/ui/components/app/dropdowns/network-dropdown.test.js index 727b8fadc..6fc126da0 100644 --- a/ui/components/app/dropdowns/network-dropdown.test.js +++ b/ui/components/app/dropdowns/network-dropdown.test.js @@ -54,11 +54,17 @@ describe('Network Dropdown', () => { preferences: { showTestNetworks: true, }, - frequentRpcListDetail: [ - { chainId: '0x1a', rpcUrl: 'http://localhost:7545' }, - { rpcUrl: 'http://localhost:7546' }, - { rpcUrl: LOCALHOST_RPC_URL, nickname: 'localhost' }, - ], + networkConfigurations: { + networkConfigurationId1: { + chainId: '0x1a', + rpcUrl: 'http://localhost:7545', + }, + networkConfigurationId2: { rpcUrl: 'http://localhost:7546' }, + networkConfigurationId3: { + rpcUrl: LOCALHOST_RPC_URL, + nickname: 'localhost', + }, + }, }, appState: { networkDropdownOpen: true, @@ -120,10 +126,13 @@ describe('Network Dropdown', () => { preferences: { showTestNetworks: false, }, - frequentRpcListDetail: [ - { chainId: '0x1a', rpcUrl: 'http://localhost:7545' }, - { rpcUrl: 'http://localhost:7546' }, - ], + networkConfigurations: { + networkConfigurationId1: { + chainId: '0x1a', + rpcUrl: 'http://localhost:7545', + }, + networkConfigurationId2: { rpcUrl: 'http://localhost:7546' }, + }, }, appState: { networkDropdownOpen: true, diff --git a/ui/components/app/menu-bar/menu-bar.test.js b/ui/components/app/menu-bar/menu-bar.test.js index 2a4037ab8..c834c6076 100644 --- a/ui/components/app/menu-bar/menu-bar.test.js +++ b/ui/components/app/menu-bar/menu-bar.test.js @@ -25,7 +25,7 @@ const initState = { accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], }, ], - frequentRpcListDetail: [], + networkConfigurations: {}, }, }; const mockStore = configureStore(); diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.test.js b/ui/components/app/modals/account-details-modal/account-details-modal.test.js index dbd7a1a66..a66c4fc12 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.test.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.test.js @@ -78,14 +78,14 @@ describe('Account Details Modal', () => { ...mockState, metamask: { ...mockState.metamask, - frequentRpcListDetail: [ - { + networkConfigurations: { + networkConfigurationId: { chainId: '0x99', rpcPrefs: { blockExplorerUrl, }, }, - ], + }, provider: { chainId: '0x99', }, diff --git a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.component.js b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.component.js index 74b48bb85..a4c9f9c23 100644 --- a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.component.js +++ b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.component.js @@ -5,7 +5,7 @@ import Modal, { ModalContent } from '../../modal'; export default class ConfirmDeleteNetwork extends PureComponent { static propTypes = { hideModal: PropTypes.func.isRequired, - delRpcTarget: PropTypes.func.isRequired, + removeNetworkConfiguration: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired, target: PropTypes.string.isRequired, }; @@ -15,7 +15,7 @@ export default class ConfirmDeleteNetwork extends PureComponent { }; handleDelete = () => { - this.props.delRpcTarget(this.props.target).then(() => { + this.props.removeNetworkConfiguration(this.props.target).then(() => { this.props.onConfirm(); this.props.hideModal(); }); diff --git a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js index 5133e7824..8a74f0d31 100644 --- a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js +++ b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.container.js @@ -1,12 +1,13 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; -import { delRpcTarget } from '../../../../store/actions'; +import { removeNetworkConfiguration } from '../../../../store/actions'; import ConfirmDeleteNetwork from './confirm-delete-network.component'; const mapDispatchToProps = (dispatch) => { return { - delRpcTarget: (target) => dispatch(delRpcTarget(target)), + removeNetworkConfiguration: (target) => + dispatch(removeNetworkConfiguration(target)), }; }; diff --git a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.test.js b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.test.js index d6c3ec7cd..6d1357828 100644 --- a/ui/components/app/modals/confirm-delete-network/confirm-delete-network.test.js +++ b/ui/components/app/modals/confirm-delete-network/confirm-delete-network.test.js @@ -9,7 +9,7 @@ describe('Confirm Delete Network', () => { const props = { hideModal: jest.fn(), onConfirm: jest.fn(), - delRpcTarget: jest.fn().mockResolvedValue(), + removeNetworkConfiguration: jest.fn().mockResolvedValue(), target: 'target', }; @@ -30,7 +30,7 @@ describe('Confirm Delete Network', () => { fireEvent.click(queryByText('[cancel]')); - expect(props.delRpcTarget).not.toHaveBeenCalled(); + expect(props.removeNetworkConfiguration).not.toHaveBeenCalled(); expect(props.onConfirm).not.toHaveBeenCalled(); expect(props.hideModal).toHaveBeenCalled(); @@ -44,7 +44,7 @@ describe('Confirm Delete Network', () => { fireEvent.click(queryByText('[delete]')); await waitFor(() => { - expect(props.delRpcTarget).toHaveBeenCalled(); + expect(props.removeNetworkConfiguration).toHaveBeenCalled(); expect(props.onConfirm).toHaveBeenCalled(); expect(props.hideModal).toHaveBeenCalled(); }); diff --git a/ui/components/app/network-display/network-display.js b/ui/components/app/network-display/network-display.js index 2b7e53aec..39ed11f9b 100644 --- a/ui/components/app/network-display/network-display.js +++ b/ui/components/app/network-display/network-display.js @@ -34,8 +34,7 @@ export default function NetworkDisplay({ })); const t = useI18nContext(); - const { nickname: networkNickname, type: networkType } = - targetNetwork ?? currentNetwork; + const { nickname, type: networkType } = targetNetwork ?? currentNetwork; return ( { metamask: { conversionRate: 280.45, nativeCurrency: 'ETH', - frequentRpcListDetail: [], + networkConfigurations: {}, provider: { ticker: 'ETH', }, @@ -36,17 +36,17 @@ describe('TransactionActivityLog container', () => { metamask: { conversionRate: 280.45, nativeCurrency: 'ETH', - frequentRpcListDetail: [ - { + networkConfigurations: { + networkConfigurationId: { rpcUrl: 'https://customnetwork.com/', - rpcPrefs: { - blockExplorerUrl: 'https://customblockexplorer.com/', - }, }, - ], + }, provider: { rpcUrl: 'https://customnetwork.com/', ticker: 'ETH', + rpcPrefs: { + blockExplorerUrl: 'https://customblockexplorer.com/', + }, }, }, }; diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index 955f63273..a2578ec13 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -4,7 +4,6 @@ import { WebHIDConnectedStatuses, HardwareTransportStates, } from '../../../shared/constants/hardware-wallets'; -import { RPCDefinition } from '../../../shared/constants/network'; import * as actionConstants from '../../store/actionConstants'; interface AppState { @@ -56,12 +55,13 @@ interface AppState { smartTransactionsErrorMessageDismissed: boolean; ledgerWebHidConnectedStatus: WebHIDConnectedStatuses; ledgerTransportStatus: HardwareTransportStates; - newNetworkAdded: string; newNftAddedMessage: string; removeNftMessage: string; + newNetworkAddedName: string; + newNetworkAddedConfigurationId: string; + selectedNetworkConfigurationId: string; sendInputCurrencySwitched: boolean; newTokensImported: string; - newCustomNetworkAdded: RPCDefinition | Record; onboardedInThisUISession: boolean; customTokenAmount: string; txId: number | null; @@ -117,12 +117,13 @@ const initialState: AppState = { smartTransactionsErrorMessageDismissed: false, ledgerWebHidConnectedStatus: WebHIDConnectedStatuses.unknown, ledgerTransportStatus: HardwareTransportStates.none, - newNetworkAdded: '', newNftAddedMessage: '', removeNftMessage: '', + newNetworkAddedName: '', + newNetworkAddedConfigurationId: '', + selectedNetworkConfigurationId: '', sendInputCurrencySwitched: false, newTokensImported: '', - newCustomNetworkAdded: {}, onboardedInThisUISession: false, customTokenAmount: '', scrollToBottom: true, @@ -330,18 +331,20 @@ export default function reduceApp( isMouseUser: action.payload, }; - case actionConstants.SET_SELECTED_SETTINGS_RPC_URL: + case actionConstants.SET_SELECTED_NETWORK_CONFIGURATION_ID: return { ...appState, - networksTabSelectedRpcUrl: action.payload, + selectedNetworkConfigurationId: action.payload, }; - case actionConstants.SET_NEW_NETWORK_ADDED: + case actionConstants.SET_NEW_NETWORK_ADDED: { + const { networkConfigurationId, nickname } = action.payload; return { ...appState, - newNetworkAdded: action.payload, + newNetworkAddedName: nickname, + newNetworkAddedConfigurationId: networkConfigurationId, }; - + } case actionConstants.SET_NEW_TOKENS_IMPORTED: return { ...appState, @@ -409,11 +412,6 @@ export default function reduceApp( ...appState, sendInputCurrencySwitched: !appState.sendInputCurrencySwitched, }; - case actionConstants.SET_NEW_CUSTOM_NETWORK_ADDED: - return { - ...appState, - newCustomNetworkAdded: action.payload, - }; case actionConstants.ONBOARDED_IN_THIS_UI_SESSION: return { ...appState, @@ -458,13 +456,6 @@ export function toggleCurrencySwitch(): Action { return { type: actionConstants.TOGGLE_CURRENCY_INPUT_SWITCH }; } -export function setNewCustomNetworkAdded( - // can pass in a valid network or empty one - payload: RPCDefinition | Record, -): PayloadAction> { - return { type: actionConstants.SET_NEW_CUSTOM_NETWORK_ADDED, payload }; -} - export function setOnBoardedInThisUISession( payload: boolean, ): PayloadAction { diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 5b9dbf40b..2fa9a3ca4 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -28,7 +28,7 @@ const initialState = { isAccountMenuOpen: false, identities: {}, unapprovedTxs: {}, - frequentRpcList: [], + networkConfigurations: {}, addressBook: [], contractExchangeRates: {}, pendingTokens: {}, diff --git a/ui/pages/confirm-signature-request/index.test.js b/ui/pages/confirm-signature-request/index.test.js index 417437a97..fa3a8b006 100644 --- a/ui/pages/confirm-signature-request/index.test.js +++ b/ui/pages/confirm-signature-request/index.test.js @@ -33,7 +33,7 @@ const mockState = { unapprovedTypedMessagesCount: 1, provider: { chainId: '0x5', type: 'goerli' }, keyrings: [], - frequentRpcListDetail: [], + networkConfigurations: {}, subjectMetadata: {}, cachedBalances: { '0x5': {}, diff --git a/ui/pages/confirmation/components/confirmation-network-switch/confirmation-network-switch.js b/ui/pages/confirmation/components/confirmation-network-switch/confirmation-network-switch.js index 33ef3fc4c..3596a3be3 100644 --- a/ui/pages/confirmation/components/confirmation-network-switch/confirmation-network-switch.js +++ b/ui/pages/confirmation/components/confirmation-network-switch/confirmation-network-switch.js @@ -78,7 +78,7 @@ export default function ConfirmationNetworkSwitch({ newNetwork }) { {newNetwork.chainId in CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP ? ( ) : ( @@ -96,7 +96,7 @@ export default function ConfirmationNetworkSwitch({ newNetwork }) { justifyContent: JustifyContent.center, }} > - {newNetwork.name} + {newNetwork.nickname}
@@ -106,6 +106,6 @@ export default function ConfirmationNetworkSwitch({ newNetwork }) { ConfirmationNetworkSwitch.propTypes = { newNetwork: PropTypes.shape({ chainId: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, + nickname: PropTypes.string.isRequired, }), }; diff --git a/ui/pages/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmation/templates/add-ethereum-chain.js index d2cd97814..e5656d625 100644 --- a/ui/pages/confirmation/templates/add-ethereum-chain.js +++ b/ui/pages/confirmation/templates/add-ethereum-chain.js @@ -348,7 +348,7 @@ function getValues(pendingApproval, t, actions, history) { [t('chainId')]: parseInt(pendingApproval.requestData.chainId, 16), [t('currencySymbol')]: pendingApproval.requestData.ticker, [t('blockExplorerUrl')]: - pendingApproval.requestData.blockExplorerUrl, + pendingApproval.requestData.rpcPrefs.blockExplorerUrl, }, prefaceKeys: [ t('networkName'), @@ -385,7 +385,21 @@ function getValues(pendingApproval, t, actions, history) { pendingApproval.requestData, ); if (originIsMetaMask) { - actions.addCustomNetwork(pendingApproval.requestData); + const networkConfigurationId = await actions.upsertNetworkConfiguration( + { + ...pendingApproval.requestData, + nickname: pendingApproval.requestData.chainName, + }, + { + setActive: false, + source: pendingApproval.requestData.source, + }, + ); + await actions.setNewNetworkAdded({ + networkConfigurationId, + nickname: pendingApproval.requestData.chainName, + }); + history.push(DEFAULT_ROUTE); } return []; diff --git a/ui/pages/confirmation/templates/index.js b/ui/pages/confirmation/templates/index.js index 12a042462..77cf9031d 100644 --- a/ui/pages/confirmation/templates/index.js +++ b/ui/pages/confirmation/templates/index.js @@ -3,7 +3,8 @@ import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { rejectPendingApproval, resolvePendingApproval, - addCustomNetwork, + setNewNetworkAdded, + upsertNetworkConfiguration, } from '../../../store/actions'; import addEthereumChain from './add-ethereum-chain'; import switchEthereumChain from './switch-ethereum-chain'; @@ -111,7 +112,9 @@ function getAttenuatedDispatch(dispatch) { dispatch(rejectPendingApproval(...args)), resolvePendingApproval: (...args) => dispatch(resolvePendingApproval(...args)), - addCustomNetwork: (...args) => dispatch(addCustomNetwork(...args)), + upsertNetworkConfiguration: (...args) => + dispatch(upsertNetworkConfiguration(...args)), + setNewNetworkAdded: (...args) => dispatch(setNewNetworkAdded(...args)), }; } diff --git a/ui/pages/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmation/templates/switch-ethereum-chain.js index 8eec72d8c..47d3e85ce 100644 --- a/ui/pages/confirmation/templates/switch-ethereum-chain.js +++ b/ui/pages/confirmation/templates/switch-ethereum-chain.js @@ -66,7 +66,7 @@ function getValues(pendingApproval, t, actions) { props: { newNetwork: { chainId: pendingApproval.requestData.chainId, - name: pendingApproval.requestData.nickname, + nickname: pendingApproval.requestData.nickname, }, }, }, diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 76678fbba..00426cf53 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -133,8 +133,7 @@ export default class Home extends PureComponent { ); } }, - newNetworkAdded: PropTypes.string, - setNewNetworkAdded: PropTypes.func.isRequired, + newNetworkAddedName: PropTypes.string, // This prop is used in the `shouldCloseNotificationPopup` function // eslint-disable-next-line react/no-unused-prop-types isSigningQRHardwareTransaction: PropTypes.bool.isRequired, @@ -145,9 +144,9 @@ export default class Home extends PureComponent { closeNotificationPopup: PropTypes.func.isRequired, newTokensImported: PropTypes.string, setNewTokensImported: PropTypes.func.isRequired, - newCustomNetworkAdded: PropTypes.object, - clearNewCustomNetworkAdded: PropTypes.func, - setRpcTarget: PropTypes.func, + newNetworkAddedConfigurationId: PropTypes.string, + clearNewNetworkAdded: PropTypes.func, + setActiveNetwork: PropTypes.func, onboardedInThisUISession: PropTypes.bool, }; @@ -268,17 +267,16 @@ export default class Home extends PureComponent { ///: END:ONLY_INCLUDE_IN infuraBlocked, showOutdatedBrowserWarning, - newNetworkAdded, - setNewNetworkAdded, newNftAddedMessage, setNewNftAddedMessage, + newNetworkAddedName, removeNftMessage, setRemoveNftMessage, newTokensImported, setNewTokensImported, - newCustomNetworkAdded, - clearNewCustomNetworkAdded, - setRpcTarget, + newNetworkAddedConfigurationId, + clearNewNetworkAdded, + setActiveNetwork, } = this.props; const onAutoHide = () => { @@ -378,7 +376,7 @@ export default class Home extends PureComponent { } /> ) : null} - {newNetworkAdded ? ( + {newNetworkAddedName ? ( - {t('newNetworkAdded', [newNetworkAdded])} + {t('newNetworkAdded', [newNetworkAddedName])} setNewNetworkAdded('')} + onClick={() => clearNewNetworkAdded()} className="home__new-network-notification-close" />
@@ -508,7 +506,7 @@ export default class Home extends PureComponent { key="home-outdatedBrowserNotification" /> ) : null} - {Object.keys(newCustomNetworkAdded).length !== 0 && ( + {newNetworkAddedConfigurationId && ( { - setRpcTarget( - newCustomNetworkAdded.rpcUrl, - newCustomNetworkAdded.chainId, - newCustomNetworkAdded.ticker, - newCustomNetworkAdded.chainName, - ); - clearNewCustomNetworkAdded(); + setActiveNetwork(newNetworkAddedConfigurationId); + clearNewNetworkAdded(); }} > - {t('switchToNetwork', [newCustomNetworkAdded.chainName])} + {t('switchToNetwork', [newNetworkAddedName])} -