diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea350e38..a19414559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [FLASK] Fix regression in transaction confirmation tabs ([#20267](https://github.com/MetaMask/metamask-extension/pull/20267)) +## [10.35.1] +### Changed +- Store default gas settings by network ([#20576](https://github.com/MetaMask/metamask-extension/pull/20576), [#20632](https://github.com/MetaMask/metamask-extension/pull/20632)) +- Add more diagnostic information upon failure ([#20595](https://github.com/MetaMask/metamask-extension/pull/20595)) + +### Fixed +- Fix bug resulting in custom network configuration being lost upon restart ([#20586](https://github.com/MetaMask/metamask-extension/pull/20586)) +- Fix UI crash when balances are missing ([#20385](https://github.com/MetaMask/metamask-extension/pull/20385)) +- Fix infinite rerender on network change while signature request is pending ([#20473](https://github.com/MetaMask/metamask-extension/pull/20473)) +- Fix Dapp link on NFT import screen ([#19799](https://github.com/MetaMask/metamask-extension/pull/19799)) +- Fix 'View on Opensea' link for main and testnet NFTs ([#19797](https://github.com/MetaMask/metamask-extension/pull/19797)) +- Ensure chainId comparison in switchEthereumChain handler is case insensitive ([#20149](https://github.com/MetaMask/metamask-extension/pull/20149)) +- Enforce user preferences in incoming transactions controller ([#19982](https://github.com/MetaMask/metamask-extension/pull/19982)) + ## [10.35.0] ### Added - Add the ability to customize tx nonce on ERC20 approval screens ([#17945](https://github.com/MetaMask/metamask-extension/pull/17945)) @@ -3958,7 +3972,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the ability to restore accounts from seed words. [Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v11.0.0...HEAD -[11.0.0]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...v11.0.0 +[11.0.0]: https://github.com/MetaMask/metamask-extension/compare/v10.35.1...v11.0.0 +[10.35.1]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...v10.35.1 [10.35.0]: https://github.com/MetaMask/metamask-extension/compare/v10.34.5...v10.35.0 [10.34.5]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...v10.34.5 [10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index d7a90dc46..3876d50be 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Erweiterte Einstellungen" }, - "advancedGasFeeDefaultOptIn": { - "message": "Speichern Sie diese $1 als Standard für \"Erweitert\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Immer diese Werte und erweiterte Einstellung als Standard verwenden." - }, "advancedGasFeeModalTitle": { "message": "Erweiterte Gasgebühr" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Token importiert" }, - "newValues": { - "message": "neue Werte" - }, "next": { "message": "Weiter" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index af006198f..bcff6bc6f 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Προηγμένη ρύθμιση παραμέτρων" }, - "advancedGasFeeDefaultOptIn": { - "message": "Αποθηκεύστε αυτά τα $1 ως προεπιλογή μου για το \"Προηγμένο\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Να χρησιμοποιούνται πάντα αυτές τις τιμές και η ρύθμιση για προχωρημένους." - }, "advancedGasFeeModalTitle": { "message": "Προηγμένη χρέωση τελών συναλλαγής" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Τα token εισήχθησαν" }, - "newValues": { - "message": "νέες τιμές" - }, "next": { "message": "Επόμενο" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 23e4d6f20..84803dba1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -306,10 +306,8 @@ "message": "Advanced configuration" }, "advancedGasFeeDefaultOptIn": { - "message": "Save these $1 as my default for \"Advanced\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Always use these values and advanced setting as default." + "message": "Save these values as my default for the $1 network.", + "description": "$1 is the current network name." }, "advancedGasFeeModalTitle": { "message": "Advanced gas fee" @@ -2483,9 +2481,6 @@ "newTokensImportedTitle": { "message": "Token imported" }, - "newValues": { - "message": "new values" - }, "next": { "message": "Next" }, @@ -2752,6 +2747,15 @@ "notifications22Title": { "message": "Looking for your account details or the block explorer URL?" }, + "notifications24ActionText": { + "message": "Got it" + }, + "notifications24Description": { + "message": "Advanced gas fee settings are now remembered based on the network you're using. This means you can set specific advanced gas fees for each network and avoid overpaying for gas or stuck transactions." + }, + "notifications24Title": { + "message": "Advanced gas fees by network" + }, "notifications3ActionText": { "message": "Read more", "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website." diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 22f8a2071..58fae7bac 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Configuración avanzada" }, - "advancedGasFeeDefaultOptIn": { - "message": "Guarda estos 1$ como mi valor predeterminado para \"Avanzado\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Usar siempre estos valores y la configuración avanzada como valores predeterminados." - }, "advancedGasFeeModalTitle": { "message": "Tarifa de gas avanzada" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Token importado" }, - "newValues": { - "message": "nuevos valores" - }, "next": { "message": "Siguiente" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 74d738b6e..260bd4501 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -148,12 +148,6 @@ "advancedBaseGasFeeToolTip": { "message": "Cuando su transacción se incluya en el bloque, se reembolsará cualquier diferencia entre su tarifa base máxima y la tarifa base real. El importe total se calcula como tarifa base máxima (en GWEI) * límite de gas." }, - "advancedGasFeeDefaultOptIn": { - "message": "Guarda estos 1$ como mi valor predeterminado para \"Avanzado\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Usar siempre estos valores y la configuración avanzada como valores predeterminados." - }, "advancedGasFeeModalTitle": { "message": "Tarifa de gas avanzada" }, @@ -1431,9 +1425,6 @@ "newPassword": { "message": "Contraseña nueva (mín. de 8 caracteres)" }, - "newValues": { - "message": "nuevos valores" - }, "next": { "message": "Siguiente" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index a8974ea1d..3a5e98789 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Configuration avancée" }, - "advancedGasFeeDefaultOptIn": { - "message": "Enregistrer ces $1 comme valeur par défaut pour « Avancé »" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Toujours utiliser par défaut ces valeurs et les paramètres avancés." - }, "advancedGasFeeModalTitle": { "message": "Frais de carburant avancés" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Jeton importé" }, - "newValues": { - "message": "nouvelles valeurs" - }, "next": { "message": "Suivant" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index b0ffcb805..55e2cd873 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "उन्नत कंफिगुरेशन" }, - "advancedGasFeeDefaultOptIn": { - "message": "इन $1 को \"एडवांस\" के लिए मेरे डिफॉल्ट के रूप में सहेजें" - }, - "advancedGasFeeDefaultOptOut": { - "message": "हमेशा इन मूल्यों और एडवांस सेटिंग को डिफॉल्ट के रूप में उपयोग करें।" - }, "advancedGasFeeModalTitle": { "message": "एडवांस गैस शुल्क" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "टोकन इम्पोर्ट हो गया" }, - "newValues": { - "message": "नए मान" - }, "next": { "message": "अगला" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index cf38c0ad8..479345522 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Konfigurasi lanjutan" }, - "advancedGasFeeDefaultOptIn": { - "message": "Simpan $1 ini sebagai default saya untuk \"Lanjutan\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Selalu gunakan nilai ini dan pengaturan lanjutan sebagai default." - }, "advancedGasFeeModalTitle": { "message": "Biaya gas lanjutan" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Token diimpor" }, - "newValues": { - "message": "nilai baru" - }, "next": { "message": "Berikutnya" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 05b14eaaf..d6000ff93 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -213,12 +213,6 @@ "advancedBaseGasFeeToolTip": { "message": "Quando la tua transazione viene inclusa nel blocco, ogni differenza tra la tua offerta massima di gas e il gas effettivamente utilizzato viene restituita a te. Il totale viene calcolato come offerta massima di gas (in GEWI) * limite di gas." }, - "advancedGasFeeDefaultOptIn": { - "message": "Salva queste $1 come mie preferite per \"Avanzate\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Utilizzare sempre questi valori e l'impostazione avanzata come predefiniti." - }, "advancedGasFeeModalTitle": { "message": "Tariffa gas avanzata" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index f9738e5a1..bd1137b30 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "詳細設定" }, - "advancedGasFeeDefaultOptIn": { - "message": "これらの$1を「高度な設定」のデフォルトとして保存" - }, - "advancedGasFeeDefaultOptOut": { - "message": "常にこれらの値と高度な設定をデフォルトとして使用します。" - }, "advancedGasFeeModalTitle": { "message": "高度なガス代" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "トークンがインポートされました" }, - "newValues": { - "message": "新しい値" - }, "next": { "message": "次へ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f45b9eca4..5735d71c8 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "고급 옵션" }, - "advancedGasFeeDefaultOptIn": { - "message": "이 $1 옵션을 \"고급\"의 기본값으로 저장합니다" - }, - "advancedGasFeeDefaultOptOut": { - "message": "항상 이 값과 고급 설정을 기본값으로 사용합니다." - }, "advancedGasFeeModalTitle": { "message": "고급 가스 요금" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "불러온 토큰" }, - "newValues": { - "message": "새로운 가치" - }, "next": { "message": "다음" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 85e6744f6..82bec5580 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Configurações avançadas" }, - "advancedGasFeeDefaultOptIn": { - "message": "Salvar estes $1 como meu padrão para \"Avançado\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Sempre utilizar esses valores e a configuração avançada por padrão." - }, "advancedGasFeeModalTitle": { "message": "Taxa de gás avançada" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Token importado" }, - "newValues": { - "message": "novos valores" - }, "next": { "message": "Próximo" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 3ddc1aa19..1f049e28b 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -148,12 +148,6 @@ "advancedBaseGasFeeToolTip": { "message": "Quando a sua transação for incluída no bloco, qualquer diferença entre a sua taxa de base máxima e a taxa de base real será reembolsada. O cálculo do valor total é feito da seguinte forma: taxa de base máxima (em GWEI) * limite de gás." }, - "advancedGasFeeDefaultOptIn": { - "message": "Salvar estes $1 como meu padrão para \"Avançado\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Sempre utilizar esses valores e a configuração avançada por padrão." - }, "advancedGasFeeModalTitle": { "message": "Taxa de gás avançada" }, @@ -1431,9 +1425,6 @@ "newPassword": { "message": "Nova senha (no mínimo 8 caracteres)" }, - "newValues": { - "message": "novos valores" - }, "next": { "message": "Seguinte" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index a72f54a58..45e602cfd 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Расширенная конфигурация" }, - "advancedGasFeeDefaultOptIn": { - "message": "Сохранить этот $1 в качестве моего значения по умолчанию для «Дополнительной» настройки" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Всегда использовать эти значения и дополнительную настройку по умолчанию." - }, "advancedGasFeeModalTitle": { "message": "Дополнительная плата за газ" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Токен импортирован" }, - "newValues": { - "message": "новые значения" - }, "next": { "message": "Далее" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 9203fa22b..cb0a2f75a 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Advanced na pagsasaayos" }, - "advancedGasFeeDefaultOptIn": { - "message": "I-save itong mga $1bilang aking default para sa \"Advanced\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Laging gamitin ang mga value na ito at advanced setting bilang default." - }, "advancedGasFeeModalTitle": { "message": "Advanced na gas fee" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Na-import ang token" }, - "newValues": { - "message": "bagong value" - }, "next": { "message": "Susunod" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 9a6f53aad..cd04f8aff 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Gelişmiş yapılandırma" }, - "advancedGasFeeDefaultOptIn": { - "message": "\"Gelişmiş\" için şunları varsayılanım olarak kaydet: $1" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Varsayılan olarak her zaman bu değerleri ve gelişmiş ayarı kullan." - }, "advancedGasFeeModalTitle": { "message": "Gelişmiş gaz ücreti" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Token içe aktarıldı" }, - "newValues": { - "message": "yeni değerler" - }, "next": { "message": "Sonraki" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 4cee139f5..2acb4f625 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "Cấu hình nâng cao" }, - "advancedGasFeeDefaultOptIn": { - "message": "Lưu $1 này làm mặc định của tôi cho \"Nâng cao\"" - }, - "advancedGasFeeDefaultOptOut": { - "message": "Luôn sử dụng các giá trị và thiết lập nâng cao này làm mặc định." - }, "advancedGasFeeModalTitle": { "message": "Phí gas nâng cao" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "Đã nhập token" }, - "newValues": { - "message": "giá trị mới" - }, "next": { "message": "Tiếp theo" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index c36fcce77..9107f71a2 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -275,12 +275,6 @@ "advancedConfiguration": { "message": "高级配置" }, - "advancedGasFeeDefaultOptIn": { - "message": "将这些 $1 保存为“高级”默认值" - }, - "advancedGasFeeDefaultOptOut": { - "message": "始终使用这些值和高级设置作为默认值。" - }, "advancedGasFeeModalTitle": { "message": "高级燃料费" }, @@ -2101,9 +2095,6 @@ "newTokensImportedTitle": { "message": "已导入代币" }, - "newValues": { - "message": "新的值" - }, "next": { "message": "下一步" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index c5ffd2d5d..3ec253382 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -360,7 +360,7 @@ async function loadPhishingWarningPage() { } catch (error) { if (error instanceof PhishingWarningPageTimeoutError) { console.warn( - 'Phishing warning page timeout; page not guaraneteed to work offline.', + 'Phishing warning page timeout; page not guaranteed to work offline.', ); } else { console.error('Failed to initialize phishing warning page', error); diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 1bbf4a4f8..28e6d964b 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -49,6 +49,10 @@ export default class AppStateController extends EventEmitter { showProductTour: true, trezorModel: null, currentPopupId: undefined, + // This key is only used for checking if the user had set advancedGasFee + // prior to Migration 92.3 where we split out the setting to support + // multiple networks. + hadAdvancedGasFeesSetPriorToMigration92_3: false, ...initState, qrHardware: {}, nftsDropdownState: {}, diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 987f0d87b..9b0b85f33 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -135,15 +135,12 @@ export default class IncomingTransactionsController { } start() { - const { featureFlags = {} } = this.preferencesController.store.getState(); - const { showIncomingTransactions } = featureFlags; + const chainId = this.getCurrentChainId(); - if (!showIncomingTransactions) { - return; + if (this._allowedToMakeFetchIncomingTx(chainId)) { + this.blockTracker.removeListener('latest', this._onLatestBlock); + this.blockTracker.addListener('latest', this._onLatestBlock); } - - this.blockTracker.removeListener('latest', this._onLatestBlock); - this.blockTracker.addListener('latest', this._onLatestBlock); } stop() { @@ -161,13 +158,9 @@ export default class IncomingTransactionsController { * @param {number} [newBlockNumberDec] - block number to begin fetching from */ async _update(address, newBlockNumberDec) { - const { completedOnboarding } = this.onboardingController.store.getState(); const chainId = this.getCurrentChainId(); - if ( - !Object.hasOwnProperty.call(ETHERSCAN_SUPPORTED_NETWORKS, chainId) || - !address || - !completedOnboarding - ) { + + if (!address || !this._allowedToMakeFetchIncomingTx(chainId)) { return; } try { @@ -302,4 +295,26 @@ export default class IncomingTransactionsController { type: TransactionType.incoming, }; } + + /** + * @param chainId - {string} The chainId of the current network + * @returns {boolean} Whether or not the user has consented to show incoming transactions + */ + _allowedToMakeFetchIncomingTx(chainId) { + const { featureFlags = {} } = this.preferencesController.store.getState(); + const { completedOnboarding } = this.onboardingController.store.getState(); + + const hasIncomingTransactionsFeatureEnabled = Boolean( + featureFlags.showIncomingTransactions, + ); + + const isEtherscanSupportedNetwork = Boolean( + ETHERSCAN_SUPPORTED_NETWORKS[chainId], + ); + return ( + completedOnboarding && + isEtherscanSupportedNetwork && + hasIncomingTransactionsFeatureEnabled + ); + } } diff --git a/app/scripts/controllers/incoming-transactions.test.js b/app/scripts/controllers/incoming-transactions.test.js index bdfac55a7..c46c3190b 100644 --- a/app/scripts/controllers/incoming-transactions.test.js +++ b/app/scripts/controllers/incoming-transactions.test.js @@ -78,11 +78,11 @@ function getMockPreferencesController({ }; } -function getMockOnboardingController() { +function getMockOnboardingController({ completedOnboarding = true } = {}) { return { store: { getState: sinon.stub().returns({ - completedOnboarding: true, + completedOnboarding, }), subscribe: sinon.spy(), }, @@ -98,6 +98,16 @@ function getMockBlockTracker() { }; } +function getDefaultControllerOpts() { + return { + blockTracker: getMockBlockTracker(), + ...getMockNetworkControllerMethods(CHAIN_IDS.GOERLI), + preferencesController: getMockPreferencesController(), + onboardingController: getMockOnboardingController(), + initState: getEmptyInitState(), + }; +} + /** * @typedef {import( * '../../../../app/scripts/controllers/incoming-transactions' @@ -226,6 +236,7 @@ describe('IncomingTransactionsController', function () { preferencesController: getMockPreferencesController(), onboardingController: getMockOnboardingController(), initState: {}, + getCurrentChainId: () => CHAIN_IDS.GOERLI, }, ); @@ -831,6 +842,97 @@ describe('IncomingTransactionsController', function () { }); }); + describe('block explorer lookup', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + function stubFetch() { + return sandbox.stub(window, 'fetch'); + } + + function assertStubNotCalled(stub) { + assert(stub.callCount === 0); + } + + async function triggerUpdate(incomingTransactionsController) { + const subscription = + incomingTransactionsController.preferencesController.store.subscribe.getCall( + 1, + ).args[0]; + + // Sets address causing a call to _update + await subscription({ selectedAddress: MOCK_SELECTED_ADDRESS }); + } + + it('should not happen when incoming transactions feature is disabled', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + ...getDefaultControllerOpts(), + preferencesController: getMockPreferencesController({ + showIncomingTransactions: false, + }), + }, + ); + const fetchStub = stubFetch(); + await triggerUpdate(incomingTransactionsController); + assertStubNotCalled(fetchStub); + }); + + it('should not happen when onboarding is in progress', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + ...getDefaultControllerOpts(), + onboardingController: getMockOnboardingController({ + completedOnboarding: false, + }), + }, + ); + + const fetchStub = stubFetch(); + await triggerUpdate(incomingTransactionsController); + assertStubNotCalled(fetchStub); + }); + + it('should not happen when chain id is not supported', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + ...getDefaultControllerOpts(), + getCurrentChainId: () => FAKE_CHAIN_ID, + }, + ); + + const fetchStub = stubFetch(); + await triggerUpdate(incomingTransactionsController); + assertStubNotCalled(fetchStub); + }); + + it('should make api call when chain id, incoming features, and onboarding status are ok', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + ...getDefaultControllerOpts(), + getCurrentChainId: () => CHAIN_IDS.GOERLI, + onboardingController: getMockOnboardingController({ + completedOnboarding: true, + }), + preferencesController: getMockPreferencesController({ + showIncomingTransactions: true, + }), + }, + ); + + const fetchStub = stubFetch(); + await triggerUpdate(incomingTransactionsController); + assert(fetchStub.callCount === 1); + }); + }); + describe('_update', function () { describe('when state is empty (initialized)', function () { it('should use provided block number and update the latest block seen', async function () { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 9be792c49..a30d83d76 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -41,7 +41,7 @@ export default class PreferencesController { useNftDetection: false, useCurrencyRateCheck: true, openSeaEnabled: false, - advancedGasFee: null, + advancedGasFee: {}, // WARNING: Do not use feature flags for security-sensitive things. // Feature flag toggling is available in the global namespace @@ -188,10 +188,18 @@ export default class PreferencesController { /** * Setter for the `advancedGasFee` property * - * @param {object} val - holds the maxBaseFee and PriorityFee that the user set as default advanced settings. + * @param {object} options + * @param {string} options.chainId - The chainId the advancedGasFees should be set on + * @param {object} options.gasFeePreferences - The advancedGasFee options to set */ - setAdvancedGasFee(val) { - this.store.updateState({ advancedGasFee: val }); + setAdvancedGasFee({ chainId, gasFeePreferences }) { + const { advancedGasFee } = this.store.getState(); + this.store.updateState({ + advancedGasFee: { + ...advancedGasFee, + [chainId]: gasFeePreferences, + }, + }); } /** diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index af740eddc..3a1e39aac 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -2,6 +2,7 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; import { ControllerMessenger } from '@metamask/base-controller'; import { TokenListController } from '@metamask/assets-controllers'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import PreferencesController from './preferences'; describe('preferences controller', function () { @@ -250,24 +251,31 @@ describe('preferences controller', function () { }); describe('setAdvancedGasFee', function () { - it('should default to null', function () { - const state = preferencesController.store.getState(); - assert.equal(state.advancedGasFee, null); + it('should default to an empty object', function () { + assert.deepEqual( + preferencesController.store.getState().advancedGasFee, + {}, + ); }); it('should set the setAdvancedGasFee property in state', function () { const state = preferencesController.store.getState(); - assert.equal(state.advancedGasFee, null); + assert.deepEqual(state.advancedGasFee, {}); preferencesController.setAdvancedGasFee({ - maxBaseFee: '1.5', - priorityFee: '2', + chainId: CHAIN_IDS.GOERLI, + gasFeePreferences: { + maxBaseFee: '1.5', + priorityFee: '2', + }, }); assert.equal( - preferencesController.store.getState().advancedGasFee.maxBaseFee, + preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] + .maxBaseFee, '1.5', ); assert.equal( - preferencesController.store.getState().advancedGasFee.priorityFee, + preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] + .priorityFee, '2', ); }); diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 37de2830e..3388828b6 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1962,9 +1962,13 @@ export default class TransactionController extends EventEmitter { */ this.getTransactions = (opts) => this.txStateManager.getTransactions(opts); - /** @returns {object} the saved default values for advancedGasFee */ + /** + * @returns {object} the saved default values for advancedGasFee + */ this.getAdvancedGasFee = () => - this.preferencesStore.getState().advancedGasFee; + this.preferencesStore.getState().advancedGasFee[ + this._getCurrentChainId() + ]; } // called once on startup diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 8600f08dc..04a170e4e 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -59,6 +59,7 @@ describe('Transaction Controller', function () { fromAccount, fragmentExists, networkStatusStore, + preferencesStore, getCurrentChainId, messengerMock, resultCallbacksMock, @@ -81,6 +82,7 @@ describe('Transaction Controller', function () { }).provider; networkStatusStore = new ObservableStore(currentNetworkStatus); + preferencesStore = new ObservableStore({ advancedGasFee: {} }); fromAccount = getTestAccounts()[0]; const blockTrackerStub = new EventEmitter(); @@ -129,6 +131,7 @@ describe('Transaction Controller', function () { getAccountType: () => 'MetaMask', getDeviceModel: () => 'N/A', securityProviderRequest: () => undefined, + preferencesStore, messenger: messengerMock, }); 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 6d71ba602..e2137da87 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 @@ -116,7 +116,7 @@ async function switchEthereumChainHandler( if ( Object.values(BUILT_IN_INFURA_NETWORKS) .map(({ chainId: id }) => id) - .includes(chainId) + .includes(_chainId) ) { await setProviderType(approvedRequestData.type); } else { diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js new file mode 100644 index 000000000..410697214 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js @@ -0,0 +1,128 @@ +import { + CHAIN_IDS, + NETWORK_TYPES, +} from '../../../../../shared/constants/network'; +import switchEthereumChain from './switch-ethereum-chain'; + +const NON_INFURA_CHAIN_ID = '0x123456789'; + +const mockRequestUserApproval = ({ requestData }) => { + return Promise.resolve(requestData); +}; + +const MOCK_MAINNET_CONFIGURATION = { + id: 123, + chainId: CHAIN_IDS.MAINNET, + type: NETWORK_TYPES.MAINNET, +}; +const MOCK_LINEA_MAINNET_CONFIGURATION = { + id: 123, + chainId: CHAIN_IDS.LINEA_MAINNET, + type: NETWORK_TYPES.LINEA_MAINNET, +}; + +describe('switchEthereumChainHandler', () => { + it('should call setProviderType when switching to a built in infura network', async () => { + const mockSetProviderType = jest.fn(); + const mockSetActiveNetwork = jest.fn(); + const switchEthereumChainHandler = switchEthereumChain.implementation; + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: CHAIN_IDS.MAINNET }], + }, + {}, + jest.fn(), + jest.fn(), + { + getCurrentChainId: () => NON_INFURA_CHAIN_ID, + findNetworkConfigurationBy: () => MOCK_MAINNET_CONFIGURATION, + setProviderType: mockSetProviderType, + setActiveNetwork: mockSetActiveNetwork, + requestUserApproval: mockRequestUserApproval, + }, + ); + expect(mockSetProviderType).toHaveBeenCalledTimes(1); + expect(mockSetProviderType).toHaveBeenCalledWith( + MOCK_MAINNET_CONFIGURATION.type, + ); + }); + + it('should call setProviderType when switching to a built in infura network, when chainId from request is lower case', async () => { + const mockSetProviderType = jest.fn(); + const mockSetActiveNetwork = jest.fn(); + const switchEthereumChainHandler = switchEthereumChain.implementation; + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: CHAIN_IDS.LINEA_MAINNET.toLowerCase() }], + }, + {}, + jest.fn(), + jest.fn(), + { + getCurrentChainId: () => NON_INFURA_CHAIN_ID, + findNetworkConfigurationBy: () => MOCK_LINEA_MAINNET_CONFIGURATION, + setProviderType: mockSetProviderType, + setActiveNetwork: mockSetActiveNetwork, + requestUserApproval: mockRequestUserApproval, + }, + ); + expect(mockSetProviderType).toHaveBeenCalledTimes(1); + expect(mockSetProviderType).toHaveBeenCalledWith( + MOCK_LINEA_MAINNET_CONFIGURATION.type, + ); + }); + + it('should call setProviderType when switching to a built in infura network, when chainId from request is upper case', async () => { + const mockSetProviderType = jest.fn(); + const mockSetActiveNetwork = jest.fn(); + const switchEthereumChainHandler = switchEthereumChain.implementation; + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: CHAIN_IDS.LINEA_MAINNET.toUpperCase() }], + }, + {}, + jest.fn(), + jest.fn(), + { + getCurrentChainId: () => NON_INFURA_CHAIN_ID, + findNetworkConfigurationBy: () => MOCK_LINEA_MAINNET_CONFIGURATION, + setProviderType: mockSetProviderType, + setActiveNetwork: mockSetActiveNetwork, + requestUserApproval: mockRequestUserApproval, + }, + ); + expect(mockSetProviderType).toHaveBeenCalledTimes(1); + expect(mockSetProviderType).toHaveBeenCalledWith( + MOCK_LINEA_MAINNET_CONFIGURATION.type, + ); + }); + + it('should call setActiveNetwork when switching to a custom network', async () => { + const mockSetProviderType = jest.fn(); + const mockSetActiveNetwork = jest.fn(); + const switchEthereumChainHandler = switchEthereumChain.implementation; + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: NON_INFURA_CHAIN_ID }], + }, + {}, + jest.fn(), + jest.fn(), + { + getCurrentChainId: () => CHAIN_IDS.MAINNET, + findNetworkConfigurationBy: () => MOCK_MAINNET_CONFIGURATION, + setProviderType: mockSetProviderType, + setActiveNetwork: mockSetActiveNetwork, + requestUserApproval: mockRequestUserApproval, + }, + ); + expect(mockSetActiveNetwork).toHaveBeenCalledTimes(1); + expect(mockSetActiveNetwork).toHaveBeenCalledWith( + MOCK_MAINNET_CONFIGURATION.id, + ); + }); +}); diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index c6d29995e..befecf21c 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/browser'; import { Dedupe, ExtraErrorData } from '@sentry/integrations'; +import { AllProperties } from '../../../shared/modules/object.utils'; import { FilterEvents } from './sentry-filter-events'; import extractEthjsErrorMessage from './extractEthjsErrorMessage'; @@ -28,75 +29,254 @@ export const ERROR_URL_ALLOWLIST = { // debugging, and they do not contain any identifiable information. export const SENTRY_BACKGROUND_STATE = { AccountTracker: { + accounts: false, currentBlockGasLimit: true, }, + AddressBookController: { + addressBook: false, + }, AlertController: { alertEnabledness: true, + unconnectedAccountAlertShownOrigins: false, + web3ShimUsageOrigins: false, + }, + AnnouncementController: { + announcements: false, }, AppMetadataController: { currentAppVersion: true, + currentMigrationVersion: true, previousAppVersion: true, previousMigrationVersion: true, - currentMigrationVersion: true, + }, + ApprovalController: { + approvalFlows: false, + pendingApprovals: false, + pendingApprovalCount: false, }, AppStateController: { + browserEnvironment: true, connectedStatusPopoverHasBeenShown: true, + currentPopupId: false, defaultHomeActiveTabName: true, + fullScreenGasPollTokens: true, + hadAdvancedGasFeesSetPriorToMigration92_3: true, + nftsDetectionNoticeDismissed: true, + nftsDropdownState: true, + notificationGasPollTokens: true, + outdatedBrowserWarningLastShown: true, + popupGasPollTokens: true, + qrHardware: true, + recoveryPhraseReminderHasBeenShown: true, + recoveryPhraseReminderLastShown: true, + serviceWorkerLastActiveTime: true, + showBetaHeader: true, + showProductTour: true, + showTestnetMessageInDropdown: true, + snapsInstallPrivacyWarningShown: true, + termsOfUseLastAgreed: true, + timeoutMinutes: true, + trezorModel: true, + usedNetworks: true, + }, + CachedBalancesController: { + cachedBalances: false, }, CurrencyController: { conversionDate: true, conversionRate: true, currentCurrency: true, nativeCurrency: true, + pendingCurrentCurrency: true, + pendingNativeCurrency: true, + usdConversionRate: true, }, DecryptMessageController: { + unapprovedDecryptMsgs: false, unapprovedDecryptMsgCount: true, }, - DesktopController: { - desktopEnabled: true, - }, EncryptionPublicKeyController: { + unapprovedEncryptionPublicKeyMsgs: false, unapprovedEncryptionPublicKeyMsgCount: true, }, + EnsController: { + ensResolutionsByAddress: false, + }, + GasFeeController: { + estimatedGasFeeTimeBounds: true, + gasEstimateType: true, + gasFeeEstimates: true, + }, IncomingTransactionsController: { + incomingTransactions: false, incomingTxLastFetchedBlockByChainId: true, }, KeyringController: { + encryptionKey: false, isUnlocked: true, + keyrings: false, + keyringTypes: false, }, MetaMetricsController: { + eventsBeforeMetricsOptIn: false, + fragments: false, metaMetricsId: true, participateInMetaMetrics: true, + previousUserTraits: false, + segmentApiCalls: false, + traits: false, }, NetworkController: { + networkConfigurations: false, + networkDetails: false, networkId: true, networkStatus: true, providerConfig: { + chainId: true, + id: true, nickname: true, + rpcPrefs: false, + rpcUrl: false, ticker: true, type: true, }, }, + NftController: { + allNftContracts: false, + allNfts: false, + ignoredNfts: false, + }, OnboardingController: { completedOnboarding: true, firstTimeFlowType: true, + onboardingTabs: false, seedPhraseBackedUp: true, }, + PermissionController: { + subjects: false, + }, + PermissionLogController: { + permissionActivityLog: false, + permissionHistory: false, + }, + PhishingController: {}, PreferencesController: { + advancedGasFee: true, currentLocale: true, + disabledRpcMethodPreferences: true, + dismissSeedBackUpReminder: true, featureFlags: true, forgottenPassword: true, - ipfsGateway: true, - preferences: true, + identities: false, + infuraBlocked: true, + ipfsGateway: false, + isLineaMainnetReleased: true, + knownMethodData: false, + ledgerTransportType: true, + lostIdentities: false, + openSeaEnabled: true, + preferences: { + autoLockTimeLimit: true, + hideZeroBalanceTokens: true, + showFiatInTestnets: true, + showTestNetworks: true, + useNativeCurrencyAsPrimaryCurrency: true, + }, + selectedAddress: false, + snapRegistryList: false, + theme: true, + transactionSecurityCheckEnabled: true, useBlockie: true, + useCurrencyRateCheck: true, + useMultiAccountBalanceChecker: true, + useNftDetection: true, useNonceField: true, usePhishDetect: true, + useTokenDetection: true, }, SignatureController: { unapprovedMsgCount: true, + unapprovedMsgs: false, unapprovedPersonalMsgCount: true, + unapprovedPersonalMsgs: false, + unapprovedTypedMessages: false, unapprovedTypedMessagesCount: true, }, + SmartTransactionsController: { + smartTransactionsState: { + fees: { + approvalTxFees: true, + tradeTxFees: true, + }, + liveness: true, + smartTransactions: false, + userOptIn: true, + }, + }, + SubjectMetadataController: { + subjectMetadata: false, + }, + SwapsController: { + swapsState: { + approveTxId: false, + customApproveTxData: false, + customGasPrice: true, + customMaxFeePerGas: true, + customMaxGas: true, + customMaxPriorityFeePerGas: true, + errorKey: true, + fetchParams: true, + quotes: false, + quotesLastFetched: true, + quotesPollingLimitEnabled: true, + routeState: true, + saveFetchedQuotes: true, + selectedAggId: true, + swapsFeatureFlags: true, + swapsFeatureIsLive: true, + swapsQuotePrefetchingRefreshTime: true, + swapsQuoteRefreshTime: true, + swapsStxBatchStatusRefreshTime: true, + swapsStxGetTransactionsRefreshTime: true, + swapsStxMaxFeeMultiplier: true, + swapsUserFeeLevel: true, + tokens: false, + topAggId: false, + tradeTxId: false, + }, + }, + TokenListController: { + preventPollingOnNetworkRestart: true, + tokenList: false, + tokensChainsCache: { + [AllProperties]: false, + }, + }, + TokenRatesController: { + contractExchangeRates: false, + }, + TokensController: { + allDetectedTokens: { + [AllProperties]: false, + }, + allIgnoredTokens: { + [AllProperties]: false, + }, + allTokens: { + [AllProperties]: false, + }, + detectedTokens: false, + ignoredTokens: false, + tokens: false, + }, + TransactionController: { + currentNetworkTxList: false, + lastFetchedBlockNumbers: false, + }, + TxController: { + currentNetworkTxList: false, + unapprovedTxs: false, + }, }; const flattenedBackgroundStateMask = Object.values( @@ -121,7 +301,9 @@ export const SENTRY_UI_STATE = { // These properties are in the `metamask` slice but not in the background state customNonceValue: true, isAccountMenuOpen: true, + isNetworkMenuOpen: true, nextNonce: true, + pendingTokens: false, welcomeScreenSeen: true, }, unconnectedAccount: true, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b75bf896b..2ece1aae0 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1455,6 +1455,7 @@ export default class MetamaskController extends EventEmitter { this.encryptionPublicKeyController.clearUnapproved(); this.decryptMessageController.clearUnapproved(); this.signatureController.clearUnapproved(); + this.approvalController.clear(); }, ); diff --git a/app/scripts/migrations/092.2.test.ts b/app/scripts/migrations/092.2.test.ts new file mode 100644 index 000000000..513ac4f0f --- /dev/null +++ b/app/scripts/migrations/092.2.test.ts @@ -0,0 +1,99 @@ +import { NetworkType, toHex } from '@metamask/controller-utils'; +import { NetworkStatus } from '@metamask/network-controller'; +import { cloneDeep } from 'lodash'; +import { version as currentStateVersion, migrate } from './092.2'; + +const TEST_NETWORK_CONTROLLER_STATE = { + networkId: 'network-id', + networkStatus: NetworkStatus.Available, + providerConfig: { + type: NetworkType.rpc, + chainId: toHex(42), + nickname: 'Funky Town Chain', + ticker: 'ETH', + id: 'test-network-client-id', + }, + networkDetails: { EIPS: {} }, + networkConfigurations: { + 'network-configuration-id-1': { + chainId: toHex(42), + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + }, +}; + +const anyPreviousStateVersion = 91; + +describe('migration #96', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should update the state version number in the appropriate metadata field', async () => { + const originalVersionedState = { + meta: { version: anyPreviousStateVersion }, + data: {}, + }; + + const newStorage = await migrate(cloneDeep(originalVersionedState)); + + expect(newStorage.meta).toStrictEqual({ version: currentStateVersion }); + }); + + it('should return state unaltered if there is no network controller state', async () => { + const originalMetaMaskState = { + anotherController: 'another-controller-state', + }; + const originalVersionedState = { + meta: { version: anyPreviousStateVersion }, + data: originalMetaMaskState, + }; + + const updatedVersionedState = await migrate( + cloneDeep(originalVersionedState), + ); + expect(updatedVersionedState.data).toStrictEqual(originalMetaMaskState); + }); + + it('should return unaltered state if there are no obsolete network controller state properties', async () => { + const originalMetaMaskState = { + anotherController: 'another-controller-state', + NetworkController: TEST_NETWORK_CONTROLLER_STATE, + }; + const originalVersionedState = { + meta: { version: anyPreviousStateVersion }, + data: originalMetaMaskState, + }; + + const updatedVersionedState = await migrate( + cloneDeep(originalVersionedState), + ); + expect(updatedVersionedState.data).toStrictEqual(originalMetaMaskState); + }); + + it('should return updated state without obsolete network controller state properties', async () => { + const originalMetaMaskState = { + anotherController: 'another-controller-state', + NetworkController: { + ...TEST_NETWORK_CONTROLLER_STATE, + someSortOfRogueObsoleteStateProperty: 'exists', + }, + }; + const originalVersionedState = { + meta: { version: anyPreviousStateVersion }, + data: originalMetaMaskState, + }; + + const updatedVersionedState = await migrate( + cloneDeep(originalVersionedState), + ); + expect(updatedVersionedState.data).not.toStrictEqual(originalMetaMaskState); + expect(updatedVersionedState.data).toStrictEqual({ + anotherController: 'another-controller-state', + NetworkController: TEST_NETWORK_CONTROLLER_STATE, + }); + }); +}); diff --git a/app/scripts/migrations/092.2.ts b/app/scripts/migrations/092.2.ts new file mode 100644 index 000000000..ef36a9fa9 --- /dev/null +++ b/app/scripts/migrations/092.2.ts @@ -0,0 +1,75 @@ +import { hasProperty } from '@metamask/utils'; +import { captureException } from '@sentry/browser'; +import { cloneDeep, isObject, pick } from 'lodash'; + +type MetaMaskState = Record; +type VersionedState = { + meta: { version: number }; + data: MetaMaskState; +}; + +export const version = 92.2; + +/** + * This migration removes obsolete NetworkController state properties. + * + * @param originalVersionedState - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedState.meta - State metadata. + * @param originalVersionedState.meta.version - The current state version. + * @param originalVersionedState.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned of MetaMask extension state. + */ +export async function migrate( + originalVersionedState: VersionedState, +): Promise { + const updatedVersionedState = cloneDeep(originalVersionedState); + + updatedVersionedState.meta.version = version; + updatedVersionedState.data = transformState(updatedVersionedState.data); + + return updatedVersionedState; +} + +function transformState(originalState: MetaMaskState): MetaMaskState { + const updatedState = + filterOutObsoleteNetworkControllerStateProperties(originalState); + + return updatedState; +} + +function filterOutObsoleteNetworkControllerStateProperties( + state: MetaMaskState, +): MetaMaskState { + // https://github.com/MetaMask/core/blob/%40metamask/network-controller%4010.3.1/packages/network-controller/src/NetworkController.ts#L336-L342 + const CURRENT_NETWORK_CONTROLLER_STATE_PROPS = [ + 'networkId', + 'networkStatus', + 'providerConfig', + 'networkDetails', + 'networkConfigurations', + ]; + + if ( + !hasProperty(state, 'NetworkController') || + !isObject(state.NetworkController) + ) { + captureException( + `Migration ${version}: Invalid NetworkController state: ${typeof state.NetworkController}`, + ); + + return state; + } + + const networkControllerState = state.NetworkController; + + // delete network state properties that are not currently in use + const updatedNetworkController = pick( + networkControllerState, + CURRENT_NETWORK_CONTROLLER_STATE_PROPS, + ); + + return { + ...state, + NetworkController: updatedNetworkController, + }; +} diff --git a/app/scripts/migrations/092.3.test.ts b/app/scripts/migrations/092.3.test.ts new file mode 100644 index 000000000..f55ea30c6 --- /dev/null +++ b/app/scripts/migrations/092.3.test.ts @@ -0,0 +1,184 @@ +import { migrate } from './092.3'; + +const PREFERENCES_CONTROLLER_MOCK = { + useBlockie: false, + useNonceField: false, + usePhishDetect: true, + dismissSeedBackUpReminder: false, + disabledRpcMethodPreferences: { + eth_sign: false, + }, + useMultiAccountBalanceChecker: true, + useTokenDetection: false, + useNftDetection: false, + use4ByteResolution: true, + useCurrencyRateCheck: true, + openSeaEnabled: false, + advancedGasFee: null, + featureFlags: { + showIncomingTransactions: true, + }, + knownMethodData: {}, + currentLocale: 'EN', + identities: {}, + lostIdentities: {}, + forgottenPassword: false, + preferences: { + autoLockTimeLimit: undefined, + showFiatInTestnets: false, + showTestNetworks: false, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + }, + // ENS decentralized website resolution + ipfsGateway: '', + useAddressBarEnsResolution: true, + infuraBlocked: null, + ledgerTransportType: 'U2F', + snapRegistryList: {}, + transactionSecurityCheckEnabled: false, + theme: 'OS', + isLineaMainnetReleased: false, +}; + +describe('migration #92.3', () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: 92.2 }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version: 92.3 }); + }); + + it('does nothing if no PreferencesController state', async () => { + const oldData = { + some: 'data', + }; + + const oldStorage = { + meta: { version: 92.2 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('does nothing if no AppStateController state', async () => { + const oldData = { + some: 'data', + }; + + const oldStorage = { + meta: { version: 92.2 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('changes advancedGasFee from null to an empty object, and sets hadAdvancedGasFeesSetPriorToMigration92_3 to false', async () => { + const oldData = { + some: 'data', + PreferencesController: { + ...PREFERENCES_CONTROLLER_MOCK, + }, + AppStateController: {}, + }; + + const oldStorage = { + meta: { version: 92.2 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + some: oldData.some, + PreferencesController: { + ...PREFERENCES_CONTROLLER_MOCK, + advancedGasFee: {}, + }, + AppStateController: { + hadAdvancedGasFeesSetPriorToMigration92_3: false, + }, + }); + }); + + it('changes advancedGasFee from an object of values to an empty object and sets hadAdvancedGasFeesSetPriorToMigration92_3 to true', async () => { + const oldData = { + some: 'data', + PreferencesController: { + ...PREFERENCES_CONTROLLER_MOCK, + advancedGasFee: { + priorityFee: '0x1', + maxBaseFee: '0x1', + }, + }, + AppStateController: {}, + }; + + const oldStorage = { + meta: { version: 92.2 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + some: oldData.some, + PreferencesController: { + ...PREFERENCES_CONTROLLER_MOCK, + advancedGasFee: {}, + }, + AppStateController: { + hadAdvancedGasFeesSetPriorToMigration92_3: true, + }, + }); + }); + + it('does not erase advancedGasFee if it does not contain the expected data prior to this migration', async () => { + const oldData = { + some: 'data', + PreferencesController: { + ...PREFERENCES_CONTROLLER_MOCK, + advancedGasFee: { + '0x5': { + priorityFee: '0x1', + maxBaseFee: '0x1', + }, + }, + }, + AppStateController: {}, + }; + + const oldStorage = { + meta: { version: 92.2 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + some: oldData.some, + PreferencesController: { + ...PREFERENCES_CONTROLLER_MOCK, + advancedGasFee: { + '0x5': { + priorityFee: '0x1', + maxBaseFee: '0x1', + }, + }, + }, + AppStateController: { + hadAdvancedGasFeesSetPriorToMigration92_3: false, + }, + }); + }); +}); diff --git a/app/scripts/migrations/092.3.ts b/app/scripts/migrations/092.3.ts new file mode 100644 index 000000000..a2659068f --- /dev/null +++ b/app/scripts/migrations/092.3.ts @@ -0,0 +1,98 @@ +import { hasProperty, isNullOrUndefined, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import log from 'loglevel'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 92.3; + +/** + * This migration does the following: + * + * - Deletes currently stored advancedGasFee in preferences controller, + * replacing the default with an empty object + * - Sets hadAdvancedGasFeesSetPriorToMigration92_3 flag on AppStateController + * to indicate if the user had previously had advancedGasFee set in their + * preferences. This will be used to display a whats new entry to inform users + * that we wiped these settings and made them apply per network. + * + * @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: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + migrateData(versionedData.data); + return versionedData; +} + +function migrateData(state: Record): void { + changeShapeAndRemoveOldAdvancedGasFeePreference(state); +} + +function changeShapeAndRemoveOldAdvancedGasFeePreference( + state: Record, +) { + if (isNullOrUndefined(state.PreferencesController)) { + log.warn( + `Migration #${version}: preferences controller null or undefined, skipping migration`, + ); + return; + } + + if ( + hasProperty(state, 'AppStateController') && + isObject(state.AppStateController) && + hasProperty(state, 'PreferencesController') && + isObject(state.PreferencesController) + ) { + const possibleOriginalValue = state.PreferencesController?.advancedGasFee; + + // Will be false if the keys set on the object are anything other than the + // maxBaseFee or priorityFee. Essentially if the object is already keyed + // by chainId it won't show as hadFeesSet. + const hadFeesSet = + isObject(possibleOriginalValue) && + hasFeePreferenceKeys(possibleOriginalValue); + + state.AppStateController.hadAdvancedGasFeesSetPriorToMigration92_3 = + hadFeesSet; + + if ( + state.PreferencesController.advancedGasFee === null || + (isObject(state.PreferencesController.advancedGasFee) && + hasFeePreferenceKeys(state.PreferencesController.advancedGasFee)) + ) { + state.PreferencesController.advancedGasFee = {}; + } + } else if (isObject(state.AppStateController) === false) { + global.sentry?.captureException?.( + new Error( + `typeof state.AppStateController is ${typeof state.AppStateController}`, + ), + ); + } else if (isObject(state.PreferencesController) === false) { + global.sentry?.captureException?.( + new Error( + `typeof state.PreferencesController is ${typeof state.PreferencesController}`, + ), + ); + } +} + +function hasFeePreferenceKeys(objectToCheck: Record): boolean { + const keys = Object.keys(objectToCheck); + + if (keys.includes('maxBaseFee') || keys.includes('priorityFee')) { + return true; + } + return false; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index f5c169a1f..a07a71fa0 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -97,6 +97,8 @@ import * as m090 from './090'; import * as m091 from './091'; import * as m092 from './092'; import * as m092point1 from './092.1'; +import * as m092point2 from './092.2'; +import * as m092point3 from './092.3'; const migrations = [ m002, @@ -191,6 +193,8 @@ const migrations = [ m091, m092, m092point1, + m092point2, + m092point3, ]; export default migrations; diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index cffcb66d0..9169eaefd 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -2239,7 +2239,9 @@ "packages": { "@metamask/snaps-controllers-flask>concat-stream>readable-stream": true, "browserify>buffer": true, - "pumpify>inherits": true + "browserify>concat-stream>typedarray": true, + "pumpify>inherits": true, + "terser>source-map-support>buffer-from": true } }, "@metamask/snaps-controllers-flask>concat-stream>readable-stream": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index cffcb66d0..9169eaefd 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2239,7 +2239,9 @@ "packages": { "@metamask/snaps-controllers-flask>concat-stream>readable-stream": true, "browserify>buffer": true, - "pumpify>inherits": true + "browserify>concat-stream>typedarray": true, + "pumpify>inherits": true, + "terser>source-map-support>buffer-from": true } }, "@metamask/snaps-controllers-flask>concat-stream>readable-stream": { diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 180fcba8f..3c5539072 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1752,20 +1752,7 @@ "process.platform": true }, "packages": { - "browserify>browser-resolve>resolve": true - } - }, - "browserify>browser-resolve>resolve": { - "builtin": { - "fs.readFile": true, - "fs.readFileSync": true, - "fs.stat": true, - "fs.statSync": true, - "path": true - }, - "globals": { - "process.nextTick": true, - "process.platform": true + "brfs>resolve": true } }, "browserify>cached-path-relative": { @@ -1874,6 +1861,7 @@ }, "packages": { "brfs>resolve": true, + "browserify>browser-resolve": true, "browserify>cached-path-relative": true, "browserify>concat-stream": true, "browserify>duplexer2": true, @@ -1881,7 +1869,6 @@ "browserify>module-deps>stream-combiner2": true, "browserify>module-deps>through2": true, "browserify>parents": true, - "lavamoat-browserify>browser-resolve": true, "loose-envify": true, "pumpify>inherits": true, "readable-stream": true, @@ -6149,8 +6136,8 @@ }, "packages": { "@lavamoat/lavapack": true, + "browserify>browser-resolve": true, "duplexify": true, - "lavamoat-browserify>browser-resolve": true, "lavamoat-browserify>concat-stream": true, "lavamoat-browserify>readable-stream": true, "lavamoat-browserify>through2": true, @@ -6159,29 +6146,16 @@ "lavamoat>lavamoat-core": true } }, - "lavamoat-browserify>browser-resolve": { - "builtin": { - "fs.readFile": true, - "fs.readFileSync": true, - "path": true - }, - "globals": { - "__dirname": true, - "process.platform": true - }, - "packages": { - "brfs>resolve": true - } - }, "lavamoat-browserify>concat-stream": { "globals": { "Buffer.concat": true, - "Buffer.from": true, "Buffer.isBuffer": true }, "packages": { + "browserify>concat-stream>typedarray": true, "lavamoat-browserify>readable-stream": true, - "pumpify>inherits": true + "pumpify>inherits": true, + "terser>source-map-support>buffer-from": true } }, "lavamoat-browserify>readable-stream": { diff --git a/shared/modules/object.utils.js b/shared/modules/object.utils.js index ce7eb1da1..218b5389f 100644 --- a/shared/modules/object.utils.js +++ b/shared/modules/object.utils.js @@ -1,25 +1,43 @@ /** - * Return a "masked" copy of the given object. + * This symbol matches all object properties when used in a mask + */ +export const AllProperties = Symbol('*'); + +/** + * Return a "masked" copy of the given object. The returned object includes + * only the properties present in the mask. * - * The returned object includes only the properties present in the mask. The - * mask is an object that mirrors the structure of the given object, except - * the only values are `true` or a sub-mask. `true` implies the property - * should be included, and a sub-mask implies the property should be further - * masked according to that sub-mask. + * The mask is an object that mirrors the structure of the given object, except + * the only values are `true`, `false, a sub-mask, or the 'AllProperties" + * symbol. `true` implies the property should be included, and `false` will + * exclude it. A sub-mask implies the property should be further masked + * according to that sub-mask. The "AllProperties" symbol is used for objects + * with dynamic keys, and applies a rule (either `true`, `false`, or a + * sub-mask`) to every property in that object. * - * If a property is not found in the last, its type is included instead. + * If a property is excluded, its type is included instead. * * @param {object} object - The object to mask * @param {Object} mask - The mask to apply to the object */ export function maskObject(object, mask) { + let maskAllProperties = false; + if (Object.keys(mask).includes(AllProperties)) { + if (Object.keys(mask).length > 1) { + throw new Error('AllProperties mask key does not support sibling keys'); + } + maskAllProperties = true; + } return Object.keys(object).reduce((state, key) => { - if (mask[key] === true) { + const maskKey = maskAllProperties ? mask[AllProperties] : mask[key]; + if (maskKey === true) { state[key] = object[key]; - } else if (mask[key]) { - state[key] = maskObject(object[key], mask[key]); - } else { + } else if (maskKey && typeof maskKey === 'object') { + state[key] = maskObject(object[key], maskKey); + } else if (maskKey === undefined || maskKey === false) { state[key] = typeof object[key]; + } else { + throw new Error(`Unsupported mask entry: ${maskKey}`); } return state; }, {}); diff --git a/shared/notifications/index.js b/shared/notifications/index.js index f5d496b8b..f7f38ceec 100644 --- a/shared/notifications/index.js +++ b/shared/notifications/index.js @@ -121,6 +121,10 @@ export const UI_NOTIFICATIONS = { src: 'images/global-menu-block-explorer.svg', }, }, + 24: { + id: 24, + date: null, + }, }; export const getTranslatedUINotifications = (t, locale) => { @@ -331,5 +335,16 @@ export const getTranslatedUINotifications = (t, locale) => { ) : '', }, + 24: { + ...UI_NOTIFICATIONS[24], + title: t('notifications24Title'), + description: t('notifications24Description'), + actionText: t('notifications24ActionText'), + date: UI_NOTIFICATIONS[24].date + ? new Intl.DateTimeFormat(formattedLocale).format( + new Date(UI_NOTIFICATIONS[24].date), + ) + : '', + }, }; }; diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 77d17841f..6cfd84ea0 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -113,6 +113,7 @@ "networkStatus": "available", "providerConfig": { "type": "rpc", + "nickname": "goerli", "chainId": "0x5", "ticker": "ETH", "id": "chain5" @@ -339,8 +340,10 @@ "useTokenDetection": true, "useCurrencyRateCheck": true, "advancedGasFee": { - "maxBaseFee": "75", - "priorityFee": "2" + "0x5": { + "maxBaseFee": "75", + "priorityFee": "2" + } }, "nftsDropdownState": { "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { diff --git a/test/e2e/tests/errors.spec.js b/test/e2e/tests/errors.spec.js index 5c557b256..9c4927a78 100644 --- a/test/e2e/tests/errors.spec.js +++ b/test/e2e/tests/errors.spec.js @@ -4,15 +4,38 @@ const { strict: assert } = require('assert'); const { get, has, set, unset } = require('lodash'); const { Browser } = require('selenium-webdriver'); const { format } = require('prettier'); -const { convertToHexValue, withFixtures } = require('../helpers'); +const { isObject } = require('@metamask/utils'); +const { SENTRY_UI_STATE } = require('../../../app/scripts/lib/setupSentry'); const FixtureBuilder = require('../fixture-builder'); +const { convertToHexValue, withFixtures } = require('../helpers'); + +/** + * Derive a UI state field from a background state field. + * + * @param {string} backgroundField - The path of a background field. + * @returns {string} The path for the corresponding UI field. + */ +function backgroundToUiField(backgroundField) { + // The controller name is lost in the UI due to state flattening + const [, ...rest] = backgroundField.split('.'); + const flattenedBackgroundField = rest.join('.'); + // Controller state is under the 'metamask' slice in the UI + return `metamask.${flattenedBackgroundField}`; +} const maskedBackgroundFields = [ 'CurrencyController.conversionDate', // This is a timestamp that changes each run + // App metadata is masked so that we don't have to update the snapshot as + // part of the release process + 'AppMetadataController.currentAppVersion', + 'AppMetadataController.currentMigrationVersion', + 'AppStateController.browserEnvironment.browser', + 'AppStateController.browserEnvironment.os', + 'AppStateController.outdatedBrowserWarningLastShown', + 'AppStateController.recoveryPhraseReminderLastShown', + 'AppStateController.termsOfUseLastAgreed', ]; -const maskedUiFields = [ - 'metamask.conversionDate', // This is a timestamp that changes each run -]; +const maskedUiFields = maskedBackgroundFields.map(backgroundToUiField); const removedBackgroundFields = [ // This property is timing-dependent @@ -22,13 +45,7 @@ const removedBackgroundFields = [ 'AppStateController.timeoutMinutes', ]; -const removedUiFields = [ - // This property is timing-dependent - 'metamask.currentBlockGasLimit', - // These properties are set to undefined, causing inconsistencies between Chrome and Firefox - 'metamask.currentPopupId', - 'metamask.timeoutMinutes', -]; +const removedUiFields = removedBackgroundFields.map(backgroundToUiField); /** * Transform background state to make it consistent between test runs. @@ -107,6 +124,38 @@ async function matchesSnapshot({ } } +/** + * Get an object consisting of all properties in the complete + * object that are missing from the given object. + * + * @param {object} complete - The complete object to compare to. + * @param {object} object - The object to test for missing properties. + */ +function getMissingProperties(complete, object) { + const missing = {}; + for (const [key, value] of Object.entries(complete)) { + if (key in object) { + if (isObject(value) && isObject(object[key])) { + const missingNestedProperties = getMissingProperties( + value, + object[key], + ); + if (Object.keys(missingNestedProperties).length > 0) { + missing[key] = missingNestedProperties; + } else { + // no missing nested properties + } + } else { + // Skip non-object values, they are considered as present + // even if they represent masked data structures + } + } else { + missing[key] = value; + } + } + return missing; +} + describe('Sentry errors', function () { const migrationError = process.env.SELENIUM_BROWSER === Browser.CHROME @@ -312,7 +361,10 @@ describe('Sentry errors', function () { 'Invalid version state', ); await matchesSnapshot({ - data: transformBackgroundState(appState.persistedState), + data: { + ...appState.persistedState, + data: transformBackgroundState(appState.persistedState.data), + }, snapshot: 'errors-before-init-opt-in-background-state', }); }, @@ -460,7 +512,10 @@ describe('Sentry errors', function () { 'Invalid version state', ); await matchesSnapshot({ - data: transformBackgroundState(appState.persistedState), + data: { + ...appState.persistedState, + data: transformBackgroundState(appState.persistedState.data), + }, snapshot: 'errors-before-init-opt-in-ui-state', }); }, @@ -728,4 +783,80 @@ describe('Sentry errors', function () { ); }); }); + + it('should have no policy gaps for UI controller state', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.findElement('#password'); + + const fullUiState = await driver.executeScript(() => + window.stateHooks?.getCleanAppState?.(), + ); + + const missingState = getMissingProperties( + fullUiState.metamask, + SENTRY_UI_STATE.metamask, + ); + assert.deepEqual(missingState, {}); + }, + ); + }); + + it('should not have extra properties in UI state mask', async function () { + const expectedMissingState = { + currentPopupId: false, // Initialized as undefined + // Part of transaction controller store, but missing from the initial + // state + lastFetchedBlockNumbers: false, + preferences: { + autoLockTimeLimit: true, // Initialized as undefined + }, + smartTransactionsState: { + fees: { + approvalTxFees: true, // Initialized as undefined + tradeTxFees: true, // Initialized as undefined + }, + userOptIn: true, // Initialized as undefined + }, + swapsState: { + // This can get wiped out during initialization due to a bug in + // the "resetState" method + swapsFeatureFlags: true, + }, + // This can get erased due to a bug in the app state controller's + // preferences state change handler + timeoutMinutes: true, + }; + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.findElement('#password'); + + const fullUiState = await driver.executeScript(() => + window.stateHooks?.getCleanAppState?.(), + ); + + const extraMaskProperties = getMissingProperties( + SENTRY_UI_STATE.metamask, + fullUiState.metamask, + ); + const unexpectedExtraMaskProperties = getMissingProperties( + extraMaskProperties, + expectedMissingState, + ); + assert.deepEqual(unexpectedExtraMaskProperties, {}); + }, + ); + }); }); diff --git a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json index 9cf5b767a..02bd3f9da 100644 --- a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-background-state.json @@ -1,52 +1,57 @@ { "AccountTracker": { "accounts": "object" }, - "AddressBookController": "object", + "AddressBookController": { "addressBook": "object" }, "AlertController": { "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", "web3ShimUsageOrigins": "object" }, - "AnnouncementController": "object", + "AnnouncementController": { "announcements": "object" }, "AppMetadataController": { - "currentAppVersion": "11.0.0", + "currentAppVersion": "string", "previousAppVersion": "", "previousMigrationVersion": 0, - "currentMigrationVersion": 92.1 + "currentMigrationVersion": "number" }, "AppStateController": { "connectedStatusPopoverHasBeenShown": true, "defaultHomeActiveTabName": null, - "browserEnvironment": "object", - "popupGasPollTokens": "object", - "notificationGasPollTokens": "object", - "fullScreenGasPollTokens": "object", - "recoveryPhraseReminderHasBeenShown": "boolean", + "browserEnvironment": { "os": "string", "browser": "string" }, + "popupGasPollTokens": [], + "notificationGasPollTokens": [], + "fullScreenGasPollTokens": [], + "recoveryPhraseReminderHasBeenShown": true, "recoveryPhraseReminderLastShown": "number", "outdatedBrowserWarningLastShown": "number", - "nftsDetectionNoticeDismissed": "boolean", - "showTestnetMessageInDropdown": "boolean", - "showBetaHeader": "boolean", - "showProductTour": "boolean", - "trezorModel": "object", - "nftsDropdownState": "object", + "nftsDetectionNoticeDismissed": false, + "showTestnetMessageInDropdown": true, + "showBetaHeader": false, + "showProductTour": true, + "trezorModel": null, + "hadAdvancedGasFeesSetPriorToMigration92_3": false, + "nftsDropdownState": {}, "termsOfUseLastAgreed": "number", - "qrHardware": "object", - "usedNetworks": "object", - "snapsInstallPrivacyWarningShown": "boolean", - "serviceWorkerLastActiveTime": "number" + "qrHardware": {}, + "usedNetworks": { "0x1": true, "0x5": true, "0x539": true }, + "snapsInstallPrivacyWarningShown": true, + "serviceWorkerLastActiveTime": 0 + }, + "ApprovalController": { + "pendingApprovals": "object", + "pendingApprovalCount": "number", + "approvalFlows": "object" }, - "ApprovalController": "object", "BackupController": "undefined", - "CachedBalancesController": "object", + "CachedBalancesController": { "cachedBalances": "object" }, "CronjobController": "object", "CurrencyController": { "conversionDate": "number", "conversionRate": 1700, "nativeCurrency": "ETH", "currentCurrency": "usd", - "pendingCurrentCurrency": "object", - "pendingNativeCurrency": "object", - "usdConversionRate": "number" + "pendingCurrentCurrency": null, + "pendingNativeCurrency": null, + "usdConversionRate": 1700 }, "DecryptMessageController": { "unapprovedDecryptMsgs": "object", @@ -56,8 +61,12 @@ "unapprovedEncryptionPublicKeyMsgs": "object", "unapprovedEncryptionPublicKeyMsgCount": 0 }, - "EnsController": "object", - "GasFeeController": "object", + "EnsController": { "ensResolutionsByAddress": "object" }, + "GasFeeController": { + "gasFeeEstimates": {}, + "estimatedGasFeeTimeBounds": {}, + "gasEstimateType": "none" + }, "IncomingTransactionsController": { "incomingTransactions": "object", "incomingTxLastFetchedBlockByChainId": { @@ -87,18 +96,22 @@ "networkId": "1337", "networkStatus": "available", "providerConfig": { - "chainId": "string", + "chainId": "0x539", "nickname": "Localhost 8545", "rpcPrefs": "object", "rpcUrl": "string", "ticker": "ETH", "type": "rpc", - "id": "string" + "id": "networkConfigurationId" }, "networkDetails": "object", "networkConfigurations": "object" }, - "NftController": "object", + "NftController": { + "allNftContracts": "object", + "allNfts": "object", + "ignoredNfts": "object" + }, "NotificationController": "object", "OnboardingController": { "seedPhraseBackedUp": true, @@ -106,20 +119,23 @@ "completedOnboarding": true, "onboardingTabs": "object" }, - "PermissionController": "object", - "PermissionLogController": "object", + "PermissionController": { "subjects": "object" }, + "PermissionLogController": { + "permissionHistory": "object", + "permissionActivityLog": "object" + }, "PreferencesController": { "useBlockie": false, "useNonceField": false, "usePhishDetect": true, - "dismissSeedBackUpReminder": "boolean", - "disabledRpcMethodPreferences": "object", - "useMultiAccountBalanceChecker": "boolean", - "useTokenDetection": "boolean", - "useNftDetection": "boolean", - "useCurrencyRateCheck": "boolean", - "openSeaEnabled": "boolean", - "advancedGasFee": "object", + "dismissSeedBackUpReminder": true, + "disabledRpcMethodPreferences": { "eth_sign": false }, + "useMultiAccountBalanceChecker": true, + "useTokenDetection": false, + "useNftDetection": false, + "useCurrencyRateCheck": true, + "openSeaEnabled": false, + "advancedGasFee": {}, "featureFlags": { "showIncomingTransactions": true }, "knownMethodData": "object", "currentLocale": "en", @@ -132,13 +148,13 @@ "showTestNetworks": false, "useNativeCurrencyAsPrimaryCurrency": true }, - "ipfsGateway": "dweb.link", - "infuraBlocked": "boolean", - "ledgerTransportType": "string", + "ipfsGateway": "string", + "infuraBlocked": false, + "ledgerTransportType": "webhid", "snapRegistryList": "object", - "transactionSecurityCheckEnabled": "boolean", - "theme": "string", - "isLineaMainnetReleased": "boolean", + "transactionSecurityCheckEnabled": false, + "theme": "light", + "isLineaMainnetReleased": true, "selectedAddress": "string" }, "SignatureController": { @@ -149,13 +165,60 @@ "unapprovedPersonalMsgCount": 0, "unapprovedTypedMessagesCount": 0 }, - "SmartTransactionsController": "object", + "SmartTransactionsController": { + "smartTransactionsState": { + "fees": {}, + "liveness": true, + "smartTransactions": "object" + } + }, "SnapController": "object", "SnapsRegistry": "object", - "SubjectMetadataController": "object", - "SwapsController": "object", - "TokenListController": "object", - "TokenRatesController": "object", - "TokensController": "object", - "TxController": "object" + "SubjectMetadataController": { "subjectMetadata": "object" }, + "SwapsController": { + "swapsState": { + "quotes": "object", + "quotesPollingLimitEnabled": false, + "fetchParams": null, + "tokens": "object", + "tradeTxId": "object", + "approveTxId": "object", + "quotesLastFetched": null, + "customMaxGas": "", + "customGasPrice": null, + "customMaxFeePerGas": null, + "customMaxPriorityFeePerGas": null, + "swapsUserFeeLevel": "", + "selectedAggId": null, + "customApproveTxData": "string", + "errorKey": "", + "topAggId": "object", + "routeState": "", + "swapsFeatureIsLive": true, + "saveFetchedQuotes": false, + "swapsQuoteRefreshTime": 60000, + "swapsQuotePrefetchingRefreshTime": 60000, + "swapsStxBatchStatusRefreshTime": 10000, + "swapsStxGetTransactionsRefreshTime": 10000, + "swapsStxMaxFeeMultiplier": 2 + } + }, + "TokenListController": { + "tokenList": "object", + "tokensChainsCache": {}, + "preventPollingOnNetworkRestart": true + }, + "TokenRatesController": { "contractExchangeRates": "object" }, + "TokensController": { + "tokens": "object", + "ignoredTokens": "object", + "detectedTokens": "object", + "allTokens": {}, + "allIgnoredTokens": {}, + "allDetectedTokens": {} + }, + "TxController": { + "unapprovedTxs": "object", + "currentNetworkTxList": "object" + } } diff --git a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json index f2bcd7cc1..70726e17b 100644 --- a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -11,7 +11,7 @@ "isInitialized": true, "isUnlocked": false, "isAccountMenuOpen": false, - "isNetworkMenuOpen": "boolean", + "isNetworkMenuOpen": false, "identities": "object", "unapprovedTxs": "object", "networkConfigurations": "object", @@ -38,38 +38,39 @@ "nativeCurrency": "ETH", "connectedStatusPopoverHasBeenShown": true, "defaultHomeActiveTabName": null, - "browserEnvironment": "object", - "popupGasPollTokens": "object", - "notificationGasPollTokens": "object", - "fullScreenGasPollTokens": "object", - "recoveryPhraseReminderHasBeenShown": "boolean", + "browserEnvironment": { "os": "string", "browser": "string" }, + "popupGasPollTokens": [], + "notificationGasPollTokens": [], + "fullScreenGasPollTokens": [], + "recoveryPhraseReminderHasBeenShown": true, "recoveryPhraseReminderLastShown": "number", "outdatedBrowserWarningLastShown": "number", - "nftsDetectionNoticeDismissed": "boolean", - "showTestnetMessageInDropdown": "boolean", - "showBetaHeader": "boolean", - "showProductTour": "boolean", - "trezorModel": "object", - "nftsDropdownState": "object", + "nftsDetectionNoticeDismissed": false, + "showTestnetMessageInDropdown": true, + "showBetaHeader": false, + "showProductTour": true, + "trezorModel": null, + "hadAdvancedGasFeesSetPriorToMigration92_3": false, + "nftsDropdownState": {}, "termsOfUseLastAgreed": "number", - "qrHardware": "object", - "usedNetworks": "object", - "snapsInstallPrivacyWarningShown": "boolean", - "serviceWorkerLastActiveTime": "number", - "currentAppVersion": "11.0.0", + "qrHardware": {}, + "usedNetworks": { "0x1": true, "0x5": true, "0x539": true }, + "snapsInstallPrivacyWarningShown": true, + "serviceWorkerLastActiveTime": 0, + "currentAppVersion": "string", "previousAppVersion": "", "previousMigrationVersion": 0, - "currentMigrationVersion": 92.1, + "currentMigrationVersion": "number", "networkId": "1337", "networkStatus": "available", "providerConfig": { - "chainId": "string", + "chainId": "0x539", "nickname": "Localhost 8545", "rpcPrefs": "object", "rpcUrl": "string", "ticker": "ETH", "type": "rpc", - "id": "string" + "id": "networkConfigurationId" }, "networkDetails": "object", "cachedBalances": "object", @@ -78,23 +79,23 @@ "encryptionKey": "object", "useNonceField": false, "usePhishDetect": true, - "dismissSeedBackUpReminder": "boolean", - "disabledRpcMethodPreferences": "object", - "useMultiAccountBalanceChecker": "boolean", - "useTokenDetection": "boolean", - "useNftDetection": "boolean", - "useCurrencyRateCheck": "boolean", - "openSeaEnabled": "boolean", - "advancedGasFee": "object", + "dismissSeedBackUpReminder": true, + "disabledRpcMethodPreferences": { "eth_sign": false }, + "useMultiAccountBalanceChecker": true, + "useTokenDetection": false, + "useNftDetection": false, + "useCurrencyRateCheck": true, + "openSeaEnabled": false, + "advancedGasFee": {}, "lostIdentities": "object", "forgottenPassword": false, - "ipfsGateway": "dweb.link", - "infuraBlocked": "boolean", - "ledgerTransportType": "string", + "ipfsGateway": "string", + "infuraBlocked": false, + "ledgerTransportType": "webhid", "snapRegistryList": "object", - "transactionSecurityCheckEnabled": "boolean", - "theme": "string", - "isLineaMainnetReleased": "boolean", + "transactionSecurityCheckEnabled": false, + "theme": "light", + "isLineaMainnetReleased": true, "selectedAddress": "string", "metaMetricsId": "fake-metrics-id", "eventsBeforeMetricsOptIn": "object", @@ -104,9 +105,9 @@ "previousUserTraits": "object", "conversionDate": "number", "currentCurrency": "usd", - "pendingCurrentCurrency": "object", - "pendingNativeCurrency": "object", - "usdConversionRate": "number", + "pendingCurrentCurrency": null, + "pendingNativeCurrency": null, + "usdConversionRate": 1300, "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", "web3ShimUsageOrigins": "object", @@ -125,19 +126,23 @@ "permissionActivityLog": "object", "subjectMetadata": "object", "announcements": "object", - "gasFeeEstimates": "object", - "estimatedGasFeeTimeBounds": "object", - "gasEstimateType": "string", + "gasFeeEstimates": {}, + "estimatedGasFeeTimeBounds": {}, + "gasEstimateType": "none", "tokenList": "object", - "tokensChainsCache": "object", - "preventPollingOnNetworkRestart": "boolean", + "tokensChainsCache": {}, + "preventPollingOnNetworkRestart": true, "tokens": "object", "ignoredTokens": "object", "detectedTokens": "object", - "allTokens": "object", - "allIgnoredTokens": "object", - "allDetectedTokens": "object", - "smartTransactionsState": "object", + "allTokens": {}, + "allIgnoredTokens": {}, + "allDetectedTokens": {}, + "smartTransactionsState": { + "fees": {}, + "liveness": true, + "smartTransactions": "object" + }, "allNftContracts": "object", "allNfts": "object", "ignoredNfts": "object", @@ -160,7 +165,32 @@ "unapprovedMsgCount": 0, "unapprovedPersonalMsgCount": 0, "unapprovedTypedMessagesCount": 0, - "swapsState": "object", + "swapsState": { + "quotes": "object", + "quotesPollingLimitEnabled": false, + "fetchParams": null, + "tokens": "object", + "tradeTxId": "object", + "approveTxId": "object", + "quotesLastFetched": null, + "customMaxGas": "", + "customGasPrice": null, + "customMaxFeePerGas": null, + "customMaxPriorityFeePerGas": null, + "swapsUserFeeLevel": "", + "selectedAggId": null, + "customApproveTxData": "string", + "errorKey": "", + "topAggId": "object", + "routeState": "", + "swapsFeatureIsLive": true, + "saveFetchedQuotes": false, + "swapsQuoteRefreshTime": 60000, + "swapsQuotePrefetchingRefreshTime": 60000, + "swapsStxBatchStatusRefreshTime": 10000, + "swapsStxGetTransactionsRefreshTime": 10000, + "swapsStxMaxFeeMultiplier": 2 + }, "ensResolutionsByAddress": "object", "pendingApprovals": "object", "pendingApprovalCount": "number", diff --git a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json index f3adc0cd9..394e93c79 100644 --- a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-background-state.json @@ -5,33 +5,42 @@ "unconnectedAccountAlertShownOrigins": "object", "web3ShimUsageOrigins": "object" }, - "AnnouncementController": "object", + "AnnouncementController": { "announcements": "object" }, "AppStateController": { - "browserEnvironment": "object", - "nftsDropdownState": "object", + "browserEnvironment": {}, + "nftsDropdownState": {}, "connectedStatusPopoverHasBeenShown": true, "termsOfUseLastAgreed": "number", "defaultHomeActiveTabName": null, - "fullScreenGasPollTokens": "object", - "notificationGasPollTokens": "object", - "popupGasPollTokens": "object", - "qrHardware": "object", - "recoveryPhraseReminderHasBeenShown": "boolean", + "fullScreenGasPollTokens": [], + "notificationGasPollTokens": [], + "popupGasPollTokens": [], + "qrHardware": {}, + "recoveryPhraseReminderHasBeenShown": true, "recoveryPhraseReminderLastShown": "number", - "showTestnetMessageInDropdown": "boolean", - "trezorModel": "object", - "usedNetworks": "object", - "snapsInstallPrivacyWarningShown": "boolean" + "showTestnetMessageInDropdown": true, + "trezorModel": null, + "usedNetworks": { + "0x1": true, + "0xe708": true, + "0x5": true, + "0x539": true + }, + "snapsInstallPrivacyWarningShown": true }, - "CachedBalancesController": "object", + "CachedBalancesController": { "cachedBalances": "object" }, "CurrencyController": { - "conversionDate": 1665507600, + "conversionDate": "number", "conversionRate": 1300, "currentCurrency": "usd", "nativeCurrency": "ETH", - "usdConversionRate": "number" + "usdConversionRate": 1300 + }, + "GasFeeController": { + "estimatedGasFeeTimeBounds": {}, + "gasEstimateType": "none", + "gasFeeEstimates": {} }, - "GasFeeController": "object", "IncomingTransactionsController": { "incomingTransactions": "object", "incomingTxLastFetchedBlockByChainId": { @@ -54,13 +63,13 @@ "networkId": "1337", "networkStatus": "available", "providerConfig": { - "chainId": "string", + "chainId": "0x539", "nickname": "Localhost 8545", "rpcPrefs": "object", "rpcUrl": "string", "ticker": "ETH", "type": "rpc", - "id": "string" + "id": "networkConfigurationId" }, "networkConfigurations": "object" }, @@ -70,20 +79,20 @@ "onboardingTabs": "object", "seedPhraseBackedUp": true }, - "PermissionController": "object", + "PermissionController": { "subjects": "object" }, "PreferencesController": { - "advancedGasFee": "object", + "advancedGasFee": null, "currentLocale": "en", - "dismissSeedBackUpReminder": "boolean", + "dismissSeedBackUpReminder": true, "featureFlags": { "showIncomingTransactions": true }, "forgottenPassword": false, "identities": "object", - "infuraBlocked": "boolean", - "ipfsGateway": "dweb.link", + "infuraBlocked": false, + "ipfsGateway": "string", "knownMethodData": "object", - "ledgerTransportType": "string", + "ledgerTransportType": "webhid", "lostIdentities": "object", - "openSeaEnabled": "boolean", + "openSeaEnabled": false, "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, @@ -91,19 +100,32 @@ "useNativeCurrencyAsPrimaryCurrency": true }, "selectedAddress": "string", - "theme": "string", + "theme": "light", "useBlockie": false, - "useNftDetection": "boolean", + "useNftDetection": false, "useNonceField": false, "usePhishDetect": true, - "useTokenDetection": "boolean", - "useCurrencyRateCheck": "boolean", - "useMultiAccountBalanceChecker": "boolean" + "useTokenDetection": false, + "useCurrencyRateCheck": true, + "useMultiAccountBalanceChecker": true }, - "SmartTransactionsController": "object", - "SubjectMetadataController": "object", - "TokensController": "object", - "TransactionController": "object", + "SmartTransactionsController": { + "smartTransactionsState": { + "fees": {}, + "liveness": true, + "smartTransactions": "object" + } + }, + "SubjectMetadataController": { "subjectMetadata": "object" }, + "TokensController": { + "allDetectedTokens": {}, + "allIgnoredTokens": {}, + "allTokens": {}, + "detectedTokens": "object", + "ignoredTokens": "object", + "tokens": "object" + }, + "TransactionController": { "transactions": "object" }, "config": "object", "firstTimeInfo": "object" } diff --git a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json index 704026b0b..474b0205b 100644 --- a/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -5,33 +5,42 @@ "unconnectedAccountAlertShownOrigins": "object", "web3ShimUsageOrigins": "object" }, - "AnnouncementController": "object", + "AnnouncementController": { "announcements": "object" }, "AppStateController": { - "browserEnvironment": "object", - "nftsDropdownState": "object", + "browserEnvironment": {}, + "nftsDropdownState": {}, "connectedStatusPopoverHasBeenShown": true, "termsOfUseLastAgreed": "number", "defaultHomeActiveTabName": null, - "fullScreenGasPollTokens": "object", - "notificationGasPollTokens": "object", - "popupGasPollTokens": "object", - "qrHardware": "object", - "recoveryPhraseReminderHasBeenShown": "boolean", + "fullScreenGasPollTokens": [], + "notificationGasPollTokens": [], + "popupGasPollTokens": [], + "qrHardware": {}, + "recoveryPhraseReminderHasBeenShown": true, "recoveryPhraseReminderLastShown": "number", - "showTestnetMessageInDropdown": "boolean", - "trezorModel": "object", - "usedNetworks": "object", - "snapsInstallPrivacyWarningShown": "boolean" + "showTestnetMessageInDropdown": true, + "trezorModel": null, + "usedNetworks": { + "0x1": true, + "0xe708": true, + "0x5": true, + "0x539": true + }, + "snapsInstallPrivacyWarningShown": true }, - "CachedBalancesController": "object", + "CachedBalancesController": { "cachedBalances": "object" }, "CurrencyController": { - "conversionDate": 1665507600, + "conversionDate": "number", "conversionRate": 1300, "currentCurrency": "usd", "nativeCurrency": "ETH", - "usdConversionRate": "number" + "usdConversionRate": 1300 + }, + "GasFeeController": { + "estimatedGasFeeTimeBounds": {}, + "gasEstimateType": "none", + "gasFeeEstimates": {} }, - "GasFeeController": "object", "IncomingTransactionsController": { "incomingTransactions": "object", "incomingTxLastFetchedBlockByChainId": { @@ -54,13 +63,13 @@ "networkId": "1337", "networkStatus": "available", "providerConfig": { - "chainId": "string", + "chainId": "0x539", "nickname": "Localhost 8545", "rpcPrefs": "object", "rpcUrl": "string", "ticker": "ETH", "type": "rpc", - "id": "string" + "id": "networkConfigurationId" }, "networkConfigurations": "object" }, @@ -70,20 +79,20 @@ "onboardingTabs": "object", "seedPhraseBackedUp": true }, - "PermissionController": "object", + "PermissionController": { "subjects": "object" }, "PreferencesController": { - "advancedGasFee": "object", + "advancedGasFee": null, "currentLocale": "en", - "dismissSeedBackUpReminder": "boolean", + "dismissSeedBackUpReminder": true, "featureFlags": { "showIncomingTransactions": true }, "forgottenPassword": false, "identities": "object", - "infuraBlocked": "boolean", - "ipfsGateway": "dweb.link", + "infuraBlocked": false, + "ipfsGateway": "string", "knownMethodData": "object", - "ledgerTransportType": "string", + "ledgerTransportType": "webhid", "lostIdentities": "object", - "openSeaEnabled": "boolean", + "openSeaEnabled": false, "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, @@ -91,19 +100,32 @@ "useNativeCurrencyAsPrimaryCurrency": true }, "selectedAddress": "string", - "theme": "string", + "theme": "light", "useBlockie": false, - "useNftDetection": "boolean", + "useNftDetection": false, "useNonceField": false, "usePhishDetect": true, - "useTokenDetection": "boolean", - "useCurrencyRateCheck": "boolean", - "useMultiAccountBalanceChecker": "boolean" + "useTokenDetection": false, + "useCurrencyRateCheck": true, + "useMultiAccountBalanceChecker": true }, - "SmartTransactionsController": "object", - "SubjectMetadataController": "object", - "TokensController": "object", - "TransactionController": "object", + "SmartTransactionsController": { + "smartTransactionsState": { + "fees": {}, + "liveness": true, + "smartTransactions": "object" + } + }, + "SubjectMetadataController": { "subjectMetadata": "object" }, + "TokensController": { + "allDetectedTokens": {}, + "allIgnoredTokens": {}, + "allTokens": {}, + "detectedTokens": "object", + "ignoredTokens": "object", + "tokens": "object" + }, + "TransactionController": { "transactions": "object" }, "config": "object", "firstTimeInfo": "object" }, diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js index b764b2351..c4762f614 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; +import { capitalize } from 'lodash'; import { useTransactionEventFragment } from '../../../../hooks/useTransactionEventFragment'; import { EditGasModes } from '../../../../../shared/constants/gas'; import Box from '../../../ui/box'; @@ -11,7 +12,11 @@ import { TextColor, TextVariant, } from '../../../../helpers/constants/design-system'; -import { getAdvancedGasFeeValues } from '../../../../selectors'; +import { + getAdvancedGasFeeValues, + getCurrentChainId, + getNetworkIdentifier, +} from '../../../../selectors'; import { setAdvancedGasFee } from '../../../../store/actions'; import { useGasFeeContext } from '../../../../contexts/gasFee'; import { useAdvancedGasFeePopoverContext } from '../context'; @@ -24,6 +29,9 @@ const AdvancedGasFeeDefaults = () => { const { gasErrors, maxBaseFee, maxPriorityFeePerGas } = useAdvancedGasFeePopoverContext(); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); + // This will need to use a different chainId in multinetwork + const chainId = useSelector(getCurrentChainId); + const networkIdentifier = useSelector(getNetworkIdentifier); const { updateTransactionEventFragment } = useTransactionEventFragment(); const { editGasMode } = useGasFeeContext(); const [isDefaultSettingsSelected, setDefaultSettingsSelected] = useState( @@ -42,7 +50,7 @@ const AdvancedGasFeeDefaults = () => { const handleUpdateDefaultSettings = () => { if (isDefaultSettingsSelected) { - dispatch(setAdvancedGasFee(null)); + dispatch(setAdvancedGasFee({ chainId, gasFeePreferences: undefined })); setDefaultSettingsSelected(false); updateTransactionEventFragment({ properties: { @@ -53,8 +61,11 @@ const AdvancedGasFeeDefaults = () => { } else { dispatch( setAdvancedGasFee({ - maxBaseFee, - priorityFee: maxPriorityFeePerGas, + chainId, + gasFeePreferences: { + maxBaseFee, + priorityFee: maxPriorityFeePerGas, + }, }), ); updateTransactionEventFragment({ @@ -91,11 +102,7 @@ const AdvancedGasFeeDefaults = () => { as="h6" color={TextColor.textAlternative} > - {isDefaultSettingsSelected - ? t('advancedGasFeeDefaultOptOut') - : t('advancedGasFeeDefaultOptIn', [ - {t('newValues')}, - ])} + {t('advancedGasFeeDefaultOptIn', [capitalize(networkIdentifier)])} diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js index ca6253774..e93c1c44a 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js @@ -15,8 +15,11 @@ import { GasFeeContextProvider } from '../../../../contexts/gasFee'; import configureStore from '../../../../store/store'; import AdvancedGasFeeInputs from '../advanced-gas-fee-inputs'; +import { CHAIN_IDS } from '../../../../../shared/constants/network'; import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults'; +const TEXT_SELECTOR = 'Save these values as my default for the Goerli network.'; + jest.mock('../../../../store/actions', () => ({ disconnectGasFeeEstimatePoller: jest.fn(), getGasFeeEstimatesAndStartPolling: jest @@ -62,68 +65,58 @@ const render = (defaultGasParams, contextParams) => { }; describe('AdvancedGasFeeDefaults', () => { it('should renders correct message when the default is not set', () => { - render({ advancedGasFee: null }); - expect(screen.queryByText('new values')).toBeInTheDocument(); + render({ advancedGasFee: {} }); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); it('should renders correct message when the default values are set', () => { render({ - advancedGasFee: { maxBaseFee: 50, priorityFee: 2 }, + advancedGasFee: { + [CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 }, + }, }); - expect( - screen.queryByText( - 'Always use these values and advanced setting as default.', - ), - ).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); it('should renders correct message when the default values are set and the maxBaseFee values are updated', () => { render({ - advancedGasFee: { maxBaseFee: 50, priorityFee: 2 }, + advancedGasFee: { + [CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 }, + }, }); expect(document.getElementsByTagName('input')[2]).toBeChecked(); - expect( - screen.queryByText( - 'Always use these values and advanced setting as default.', - ), - ).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); fireEvent.change(document.getElementsByTagName('input')[0], { target: { value: 75 }, }); expect(document.getElementsByTagName('input')[0]).toHaveValue(75); - expect(screen.queryByText('new values')).toBeInTheDocument(); - expect( - screen.queryByText('Save these as my default for "Advanced"'), - ).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); it('should renders correct message when the default values are set and the priorityFee values are updated', () => { render({ - advancedGasFee: { maxBaseFee: 50, priorityFee: 2 }, + advancedGasFee: { + [CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 }, + }, }); expect(document.getElementsByTagName('input')[2]).toBeChecked(); - expect( - screen.queryByText( - 'Always use these values and advanced setting as default.', - ), - ).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); fireEvent.change(document.getElementsByTagName('input')[1], { target: { value: 5 }, }); expect(document.getElementsByTagName('input')[1]).toHaveValue(5); - expect(screen.queryByText('new values')).toBeInTheDocument(); - expect( - screen.queryByText('Save these as my default for "Advanced"'), - ).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); + expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); it('should call action setAdvancedGasFee when checkbox or label text is clicked', () => { render({ - advancedGasFee: { maxBaseFee: 50, priorityFee: 2 }, + advancedGasFee: { + [CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 }, + }, }); const mock = jest .spyOn(Actions, 'setAdvancedGasFee') .mockReturnValue({ type: 'test' }); - const checkboxLabel = screen.queryByText( - 'Always use these values and advanced setting as default.', - ); + const checkboxLabel = screen.queryByText(TEXT_SELECTOR); fireEvent.click(checkboxLabel); expect(mock).toHaveBeenCalledTimes(1); const checkbox = document.querySelector('input[type=checkbox]'); diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js index 83540a3cd..eab4d43ac 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js @@ -13,6 +13,7 @@ import configureStore from '../../../../../store/store'; import { AdvancedGasFeePopoverContextProvider } from '../../context'; import AdvancedGasFeeGasLimit from '../../advanced-gas-fee-gas-limit'; +import { CHAIN_IDS } from '../../../../../../shared/constants/network'; import PriorityfeeInput from './priority-fee-input'; jest.mock('../../../../../store/actions', () => ({ @@ -34,7 +35,7 @@ const render = (txProps, contextProps) => { balance: '0x1F4', }, }, - advancedGasFee: { priorityFee: 100 }, + advancedGasFee: { [CHAIN_IDS.GOERLI]: { priorityFee: 100 } }, featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js index 3e371a05d..b7382bc5d 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-fee-popover.test.js @@ -61,9 +61,13 @@ const render = ({ txProps, contextProps } = {}) => { balance: '0x1F4', }, }, + identities: { + '0xAddress': {}, + }, selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, gasFeeEstimates: MOCK_FEE_ESTIMATE, + advancedGasFee: {}, }, }); diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js index 44ac34b87..ea3191e2d 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js @@ -10,6 +10,7 @@ import { ETH } from '../../../../helpers/constants/common'; import configureStore from '../../../../store/store'; import { GasFeeContextProvider } from '../../../../contexts/gasFee'; +import { CHAIN_IDS } from '../../../../../shared/constants/network'; import EditGasItem from './edit-gas-item'; jest.mock('../../../../store/actions', () => ({ @@ -59,7 +60,9 @@ const renderComponent = ({ const store = configureStore({ metamask: { nativeCurrency: ETH, - providerConfig: {}, + providerConfig: { + chainId: CHAIN_IDS.GOERLI, + }, cachedBalances: {}, accounts: { '0xAddress': { @@ -67,13 +70,18 @@ const renderComponent = ({ balance: '0x176e5b6f173ebe66', }, }, + identities: { + '0xAddress': {}, + }, selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, gasEstimateType: 'fee-market', gasFeeEstimates: MOCK_FEE_ESTIMATE, advancedGasFee: { - maxBaseFee: '100', - priorityFee: '2', + [CHAIN_IDS.GOERLI]: { + maxBaseFee: '100', + priorityFee: '2', + }, }, }, }); diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js index dbeda43ac..97797c110 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js @@ -41,6 +41,9 @@ const renderComponent = (componentProps) => { balance: '0x176e5b6f173ebe66', }, }, + identities: { + '0xAddress': {}, + }, selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, }, diff --git a/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js b/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js index 9d8f36c4e..b57c7e32e 100644 --- a/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js +++ b/ui/components/app/gas-details-item/gas-details-item-title/gas-details-item-title.test.js @@ -28,6 +28,9 @@ const render = () => { balance: '0x176e5b6f173ebe66', }, }, + identities: { + '0xAddress': {}, + }, selectedAddress: '0xAddress', }, }); diff --git a/ui/components/app/nft-details/nft-details.js b/ui/components/app/nft-details/nft-details.js index 491506339..5a8df995e 100644 --- a/ui/components/app/nft-details/nft-details.js +++ b/ui/components/app/nft-details/nft-details.js @@ -112,12 +112,13 @@ export default function NftDetails({ nft }) { const getOpenSeaLink = () => { switch (currentNetwork) { case CHAIN_IDS.MAINNET: - return `https://opensea.io/assets/${address}/${tokenId}`; + return `https://opensea.io/assets/ethereum/${address}/${tokenId}`; case CHAIN_IDS.POLYGON: return `https://opensea.io/assets/matic/${address}/${tokenId}`; case CHAIN_IDS.GOERLI: + return `https://testnets.opensea.io/assets/goerli/${address}/${tokenId}`; case CHAIN_IDS.SEPOLIA: - return `https://testnets.opensea.io/assets/${address}/${tokenId}`; + return `https://testnets.opensea.io/assets/sepolia/${address}/${tokenId}`; default: return null; } diff --git a/ui/components/app/nft-details/nft-details.test.js b/ui/components/app/nft-details/nft-details.test.js index aab54bd60..7733e75ae 100644 --- a/ui/components/app/nft-details/nft-details.test.js +++ b/ui/components/app/nft-details/nft-details.test.js @@ -165,7 +165,7 @@ describe('NFT Details', () => { await waitFor(() => { expect(global.platform.openTab).toHaveBeenCalledWith({ - url: `https://testnets.opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`, + url: `https://testnets.opensea.io/assets/goerli/${nfts[5].address}/${nfts[5].tokenId}`, }); }); }); @@ -200,7 +200,7 @@ describe('NFT Details', () => { await waitFor(() => { expect(global.platform.openTab).toHaveBeenCalledWith({ - url: `https://opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`, + url: `https://opensea.io/assets/ethereum/${nfts[5].address}/${nfts[5].tokenId}`, }); }); }); @@ -272,7 +272,7 @@ describe('NFT Details', () => { await waitFor(() => { expect(global.platform.openTab).toHaveBeenCalledWith({ - url: `https://testnets.opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`, + url: `https://testnets.opensea.io/assets/sepolia/${nfts[5].address}/${nfts[5].tokenId}`, }); }); }); diff --git a/ui/components/app/signature-request-header/__snapshots__/signature-request-header.test.js.snap b/ui/components/app/signature-request-header/__snapshots__/signature-request-header.test.js.snap index 58642cc3e..1f9dedfed 100644 --- a/ui/components/app/signature-request-header/__snapshots__/signature-request-header.test.js.snap +++ b/ui/components/app/signature-request-header/__snapshots__/signature-request-header.test.js.snap @@ -61,7 +61,7 @@ exports[`SignatureRequestHeader should match snapshot 1`] = ` - U + G @@ -71,7 +71,7 @@ exports[`SignatureRequestHeader should match snapshot 1`] = `
- Unknown private network + goerli
- U + G @@ -147,7 +147,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = `
- Unknown private network + goerli
- U + G @@ -144,7 +144,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`]
- Unknown private network + goerli
- Private network + goerli @@ -127,7 +127,7 @@ exports[`SignatureRequestHeader renders correctly without fromAccount 1`] = ` - Private network + goerli diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index 94631434e..15ec13270 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -103,6 +103,9 @@ function getActionFunctionById(id, history) { 22: () => { updateViewedNotifications({ 22: true }); }, + 24: () => { + updateViewedNotifications({ 24: true }); + }, }; return actionFunctions[id]; @@ -364,6 +367,7 @@ export default function WhatsNewPopup({ 19: renderFirstNotification, 21: renderFirstNotification, 22: renderFirstNotification, + 24: renderFirstNotification, }; return ( diff --git a/ui/components/institutional/custody-confirm-link-modal/custody-confirm-link-modal.js b/ui/components/institutional/custody-confirm-link-modal/custody-confirm-link-modal.js index e7d0c4d5b..55747a204 100644 --- a/ui/components/institutional/custody-confirm-link-modal/custody-confirm-link-modal.js +++ b/ui/components/institutional/custody-confirm-link-modal/custody-confirm-link-modal.js @@ -11,7 +11,7 @@ import withModalProps from '../../../helpers/higher-order-components/with-modal- import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { mmiActionsFactory } from '../../../store/institutional/institution-background'; import { setSelectedAddress } from '../../../store/actions'; -import { getMetaMaskAccountsRaw } from '../../../selectors'; +import { getMetaMaskIdentities } from '../../../selectors'; import { getMMIAddressFromModalOrAddress, getCustodyAccountDetails, @@ -42,7 +42,7 @@ const CustodyConfirmLink = ({ hideModal }) => { const dispatch = useDispatch(); const mmiActions = mmiActionsFactory(); const trackEvent = useContext(MetaMetricsContext); - const mmiAccounts = useSelector(getMetaMaskAccountsRaw); + const mmiAccounts = useSelector(getMetaMaskIdentities); const address = useSelector(getMMIAddressFromModalOrAddress); const custodyAccountDetails = useSelector(getCustodyAccountDetails); const { custodians } = useSelector(getMMIConfiguration); diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index cb10c7a87..e946edefb 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -77,6 +77,8 @@ import { draftTransactionInitialState, editExistingTransaction } from '.'; const mockStore = createMockStore([thunk]); +const mockAddress1 = '0xdafea492d9c6733ae3d56b7ed1adb60692c98123'; + jest.mock('./send', () => { const actual = jest.requireActual('./send'); return { @@ -1063,7 +1065,7 @@ describe('Send Slice', () => { describe('QR Code Detected', () => { const qrCodestate = getInitialSendStateWithExistingTxState({ recipient: { - address: '0xAddress', + address: mockAddress1, }, }); @@ -1102,7 +1104,7 @@ describe('Send Slice', () => { const draftTransaction = getTestUUIDTx(result); - expect(draftTransaction.recipient.address).toStrictEqual('0xAddress'); + expect(draftTransaction.recipient.address).toStrictEqual(mockAddress1); expect(draftTransaction.recipient.error).toStrictEqual( INVALID_RECIPIENT_ADDRESS_ERROR, ); @@ -1115,7 +1117,7 @@ describe('Send Slice', () => { ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, selectedAccount: { balance: '0x0', - address: '0xAddress', + address: mockAddress1, }, }; @@ -1144,7 +1146,7 @@ describe('Send Slice', () => { ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, selectedAccount: { balance: '0x0', - address: '0xAddress', + address: mockAddress1, }, }; @@ -1158,7 +1160,7 @@ describe('Send Slice', () => { const result = sendReducer(olderState, action); expect(result.selectedAccount.balance).toStrictEqual('0x0'); - expect(result.selectedAccount.address).toStrictEqual('0xAddress'); + expect(result.selectedAccount.address).toStrictEqual(mockAddress1); }); }); @@ -1167,13 +1169,13 @@ describe('Send Slice', () => { const accountsChangedState = { ...getInitialSendStateWithExistingTxState({ fromAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, }), stage: SEND_STAGES.EDIT, selectedAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, }; @@ -1182,7 +1184,7 @@ describe('Send Slice', () => { type: 'ACCOUNT_CHANGED', payload: { account: { - address: '0xAddress', + address: mockAddress1, balance: '0x1', }, }, @@ -1201,13 +1203,13 @@ describe('Send Slice', () => { const accountsChangedState = { ...getInitialSendStateWithExistingTxState({ fromAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, }), stage: SEND_STAGES.EDIT, selectedAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, }; @@ -1231,7 +1233,7 @@ describe('Send Slice', () => { ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, stage: SEND_STAGES.EDIT, selectedAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, }; @@ -1274,23 +1276,23 @@ describe('Send Slice', () => { 1559: true, }, }, - selectedAddress: '0xAddress', - identities: { '0xAddress': { address: '0xAddress' } }, + selectedAddress: mockAddress1, + identities: { [mockAddress1]: { address: mockAddress1 } }, keyrings: [ { type: KeyringType.hdKeyTree, - accounts: ['0xAddress'], + accounts: [mockAddress1], }, ], accounts: { - '0xAddress': { - address: '0xAddress', + [mockAddress1]: { + address: mockAddress1, balance: '0x0', }, }, cachedBalances: { 0x5: { - '0xAddress': '0x0', + [mockAddress1]: '0x0', }, }, providerConfig: { @@ -1580,14 +1582,17 @@ describe('Send Slice', () => { }, cachedBalances: { [CHAIN_IDS.GOERLI]: { - '0xAddress': '0x0', + [mockAddress1]: '0x0', }, }, accounts: { - '0xAddress': { - address: '0xAddress', + [mockAddress1]: { + address: mockAddress1, }, }, + identities: { + [mockAddress1]: {}, + }, }, send: { ...getInitialSendStateWithExistingTxState({ @@ -1607,7 +1612,7 @@ describe('Send Slice', () => { userInputHexData: '', }), selectedAccount: { - address: '0xAddress', + address: mockAddress1, }, }, }; @@ -2379,16 +2384,18 @@ describe('Send Slice', () => { addressBook: { [CHAIN_IDS.GOERLI]: {}, }, - identities: {}, + identities: { + [mockAddress1]: {}, + }, accounts: { - '0xAddress': { - address: '0xAddress', + [mockAddress1]: { + address: mockAddress1, balance: '0x0', }, }, cachedBalances: { [CHAIN_IDS.GOERLI]: { - '0xAddress': '0x0', + [mockAddress1]: '0x0', }, }, tokenList: {}, @@ -2397,7 +2404,7 @@ describe('Send Slice', () => { id: 1, txParams: { data: '', - from: '0xAddress', + from: mockAddress1, to: '0xRecipientAddress', gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', // 1000000000 @@ -2414,7 +2421,7 @@ describe('Send Slice', () => { ...getInitialSendStateWithExistingTxState({ id: 1, fromAccount: { - address: '0xAddress', + address: mockAddress1, }, }), }, @@ -2443,7 +2450,7 @@ describe('Send Slice', () => { type: AssetType.native, }, fromAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, gas: { @@ -2514,16 +2521,18 @@ describe('Send Slice', () => { addressBook: { [CHAIN_IDS.GOERLI]: {}, }, - identities: {}, + identities: { + [mockAddress1]: {}, + }, accounts: { - '0xAddress': { - address: '0xAddress', + [mockAddress1]: { + address: mockAddress1, balance: '0x0', }, }, cachedBalances: { [CHAIN_IDS.GOERLI]: { - '0xAddress': '0x0', + [mockAddress1]: '0x0', }, }, tokenList: {}, @@ -2533,10 +2542,10 @@ describe('Send Slice', () => { txParams: { data: generateERC721TransferData({ toAddress: BURN_ADDRESS, - fromAddress: '0xAddress', + fromAddress: mockAddress1, tokenId: BigNumber.from(15000).toString(), }), - from: '0xAddress', + from: mockAddress1, to: '0xNftAddress', gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gasPrice: '0x3b9aca00', // 1000000000 @@ -2586,7 +2595,7 @@ describe('Send Slice', () => { type: AssetType.native, }, fromAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, gas: { @@ -2697,16 +2706,18 @@ describe('Send Slice', () => { addressBook: { [CHAIN_IDS.GOERLI]: {}, }, - identities: {}, + identities: { + [mockAddress1]: {}, + }, accounts: { - '0xAddress': { - address: '0xAddress', + [mockAddress1]: { + address: mockAddress1, balance: '0x0', }, }, cachedBalances: { [CHAIN_IDS.GOERLI]: { - '0xAddress': '0x0', + [mockAddress1]: '0x0', }, }, unapprovedTxs: { @@ -2722,7 +2733,7 @@ describe('Send Slice', () => { decimals: 18, }, }), - from: '0xAddress', + from: mockAddress1, to: '0xTokenAddress', gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gasPrice: '0x3b9aca00', // 1000000000 @@ -2740,7 +2751,7 @@ describe('Send Slice', () => { }, }), selectedAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, stage: SEND_STAGES.EDIT, @@ -2777,7 +2788,7 @@ describe('Send Slice', () => { type: AssetType.native, }, fromAccount: { - address: '0xAddress', + address: mockAddress1, balance: '0x0', }, gas: { diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js index f74dd27e9..d3e4aa6ad 100644 --- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js @@ -106,9 +106,12 @@ const ConfirmAddSuggestedNFT = () => { }, [history, mostRecentOverviewPage, suggestedNfts]); let origin; + let link; if (suggestedNfts.length) { try { - origin = new URL(suggestedNfts[0].origin)?.host; + const url = new URL(suggestedNfts[0].origin); + origin = url.host; + link = url.href; } catch { origin = 'dapp'; } @@ -138,7 +141,7 @@ const ConfirmAddSuggestedNFT = () => { {origin} diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 9cc5b74f3..e6ae9e425 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -281,28 +281,38 @@ export function deprecatedGetCurrentNetworkId(state) { return state.metamask.networkId ?? 'loading'; } +/** + * Get MetaMask accounts, including account name and balance. + */ export const getMetaMaskAccounts = createSelector( - getMetaMaskAccountsRaw, + getMetaMaskIdentities, + getMetaMaskAccountBalances, getMetaMaskCachedBalances, - (currentAccounts, cachedBalances) => - Object.entries(currentAccounts).reduce( - (selectedAccounts, [accountID, account]) => { - if (account.balance === null || account.balance === undefined) { - return { - ...selectedAccounts, - [accountID]: { - ...account, - balance: cachedBalances && cachedBalances[accountID], - }, - }; - } - return { - ...selectedAccounts, - [accountID]: account, + (identities, balances, cachedBalances) => + Object.keys(identities).reduce((accounts, address) => { + // TODO: mix in the identity state here as well, consolidating this + // selector with `accountsWithSendEtherInfoSelector` + let account = {}; + + if (balances[address]) { + account = { + ...account, + ...balances[address], }; - }, - {}, - ), + } + + if (account.balance === null || account.balance === undefined) { + account = { + ...account, + balance: cachedBalances && cachedBalances[address], + }; + } + + return { + ...accounts, + [address]: account, + }; + }, {}), ); export function getSelectedAddress(state) { @@ -325,11 +335,23 @@ export function getMetaMaskKeyrings(state) { return state.metamask.keyrings; } +/** + * Get identity state. + * + * @param {object} state - Redux state + * @returns {object} A map of account addresses to identities (which includes the account name) + */ export function getMetaMaskIdentities(state) { return state.metamask.identities; } -export function getMetaMaskAccountsRaw(state) { +/** + * Get account balances state. + * + * @param {object} state - Redux state + * @returns {object} A map of account addresses to account objects (which includes the account balance) + */ +export function getMetaMaskAccountBalances(state) { return state.metamask.accounts; } @@ -368,7 +390,7 @@ export const getMetaMaskAccountsConnected = createSelector( export function isBalanceCached(state) { const selectedAccountBalance = - state.metamask.accounts[getSelectedAddress(state)].balance; + getMetaMaskAccountBalances(state)[getSelectedAddress(state)]?.balance; const cachedBalance = getSelectedAccountCachedBalance(state); return Boolean(!selectedAccountBalance && cachedBalance); @@ -1009,6 +1031,7 @@ function getAllowedAnnouncementIds(state) { 20: currentKeyringIsLedger && isFirefox, 21: isSwapsChain, 22: true, + 24: state.metamask.hadAdvancedGasFeesSetPriorToMigration92_3 === true, }; } @@ -1309,23 +1332,26 @@ export function getIsMultiLayerFeeNetwork(state) { * To retrieve the maxBaseFee and priorityFee the user has set as default * * @param {*} state - * @returns Boolean + * @returns {{maxBaseFee: string, priorityFee: string} | undefined} */ export function getAdvancedGasFeeValues(state) { - return state.metamask.advancedGasFee; -} - -/** - * To check if the user has set advanced gas fee settings as default with a non empty maxBaseFee and priotityFee. - * - * @param {*} state - * @returns Boolean - */ -export function getIsAdvancedGasFeeDefault(state) { - const { advancedGasFee } = state.metamask; - return ( - Boolean(advancedGasFee?.maxBaseFee) && Boolean(advancedGasFee?.priorityFee) - ); + // This will not work when we switch to supporting multi-chain. + // There are four non-test files that use this selector. + // advanced-gas-fee-defaults + // base-fee-input + // priority-fee-input + // useGasItemFeeDetails + // The first three are part of the AdvancedGasFeePopover + // The latter is used by the EditGasPopover + // Both of those are used in Confirmations as well as transaction-list-item + // All of the call sites have access to the GasFeeContext, which has a + // transaction object set on it, but there are currently no guarantees that + // the transaction has a chainId associated with it. To have this method + // support multichain we'll need a reliable way for the chainId of the + // transaction being modified to be available to all callsites and either + // pass it in to the selector as a second parameter, or access it at the + // callsite. + return state.metamask.advancedGasFee[getCurrentChainId(state)]; } /** diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index e63252ee7..4c7f44df9 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -600,11 +600,6 @@ describe('Selectors', () => { priorityFee: '2', }); }); - it('#getIsAdvancedGasFeeDefault', () => { - const isAdvancedGasFeeDefault = - selectors.getIsAdvancedGasFeeDefault(mockState); - expect(isAdvancedGasFeeDefault).toStrictEqual(true); - }); it('#getAppIsLoading', () => { const appIsLoading = selectors.getAppIsLoading(mockState); expect(appIsLoading).toStrictEqual(false); diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index ebf77ff77..21157703f 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -244,6 +244,9 @@ describe('Actions', () => { '0xAnotherAddress': '0x0', }, }, + identities: { + '0xAnotherAddress': {}, + }, }), ); @@ -1876,6 +1879,9 @@ describe('Actions', () => { balance: '0x0', }, }, + identities: { + '0xFirstAddress': {}, + }, cachedBalances: { '0x1': { '0xFirstAddress': '0x0', diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 8040b01b8..a07eed427 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3035,7 +3035,7 @@ export function detectNfts(): ThunkAction< } export function setAdvancedGasFee( - val: { maxBaseFee?: Hex; priorityFee?: Hex } | null, + val: { chainId: Hex; maxBaseFee?: Hex; priorityFee?: Hex } | null, ): ThunkAction { return (dispatch: MetaMaskReduxDispatch) => { dispatch(showLoadingIndication()); diff --git a/yarn.lock b/yarn.lock index 0e81f0134..5610f527b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11304,15 +11304,6 @@ __metadata: languageName: node linkType: hard -"browser-resolve@npm:^1.11.0": - version: 1.11.3 - resolution: "browser-resolve@npm:1.11.3" - dependencies: - resolve: 1.1.7 - checksum: 431bfc1a17406362a3010a2c35503eb7d1253dbcb8081c1ce236ddb0b954a33d52dcaf0b07f64c0f20394d6eeec1be4f6551da3734ce9ed5dcc38e876c96d5d5 - languageName: node - linkType: hard - "browser-resolve@npm:^2.0.0": version: 2.0.0 resolution: "browser-resolve@npm:2.0.0" @@ -11416,13 +11407,13 @@ __metadata: linkType: hard "browserify@npm:^16.5.1": - version: 16.5.1 - resolution: "browserify@npm:16.5.1" + version: 16.5.2 + resolution: "browserify@npm:16.5.2" dependencies: JSONStream: ^1.0.3 assert: ^1.4.0 browser-pack: ^6.0.1 - browser-resolve: ^1.11.0 + browser-resolve: ^2.0.0 browserify-zlib: ~0.2.0 buffer: ~5.2.1 cached-path-relative: ^1.0.0 @@ -11443,7 +11434,7 @@ __metadata: insert-module-globals: ^7.0.0 labeled-stream-splicer: ^2.0.0 mkdirp-classic: ^0.5.2 - module-deps: ^6.0.0 + module-deps: ^6.2.3 os-browserify: ~0.3.0 parents: ^1.0.1 path-browserify: ~0.0.0 @@ -11469,7 +11460,7 @@ __metadata: xtend: ^4.0.0 bin: browserify: bin/cmd.js - checksum: 71c02959ffbcedc6364fb77db75dbf9ac7d945747414774287202327b7be9ae390257acfbf788455a90a088daa18c5134ec7b46315b747ff51ccddb73ad2e3ea + checksum: 75dacf5c82355146b49a2febb3bf9f7898893931973cf901849791827e44782afcb562be7bc3a893d9022ae528fd6fccdf24fc8812cb5aa1b081bb7ce34c46b5 languageName: node linkType: hard @@ -12962,11 +12953,13 @@ __metadata: "concat-stream@npm:^2.0.0": version: 2.0.0 - resolution: "concat-stream@https://github.com/hugomrdias/concat-stream.git#commit=057bc7b5d6d8df26c8cf00a3f151b6721a0a8034" + resolution: "concat-stream@npm:2.0.0" dependencies: + buffer-from: ^1.0.0 inherits: ^2.0.3 readable-stream: ^3.0.2 - checksum: 1cef636e7061f310088706b34fe774e3960dff60a5039158b5e5c84795f6dd8a3411659324280405b8c5f1d7e8e3d4f68fa48e55963ed14953a44fef66423329 + typedarray: ^0.0.6 + checksum: d7f75d48f0ecd356c1545d87e22f57b488172811b1181d96021c7c4b14ab8855f5313280263dca44bb06e5222f274d047da3e290a38841ef87b59719bde967c7 languageName: node linkType: hard @@ -14883,11 +14876,11 @@ __metadata: linkType: hard "dot-prop@npm:^4.1.0": - version: 5.2.0 - resolution: "dot-prop@npm:5.2.0" + version: 4.2.1 + resolution: "dot-prop@npm:4.2.1" dependencies: - is-obj: ^2.0.0 - checksum: 709a8208bff4fc4d5a11e357957a9e59ed625d7db909d14ea1e0dbeb30d26c25325a6e64ea27ed96fb17978cc13c7e38cf30bac17bb81eb9b5a740a4fe909a87 + is-obj: ^1.0.0 + checksum: 5f4f19aa440bc548670d87f2adcbd105fa6842cd1fba3165a8a2b1380568ae82862acf8ebafcc6093fa062505d7d08d7155c7ba9a88da212f7348e95ef2bdce6 languageName: node linkType: hard @@ -16386,11 +16379,11 @@ __metadata: "ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git, ethereumjs-abi@npm:0.6.8, ethereumjs-abi@npm:^0.6.4, ethereumjs-abi@npm:^0.6.8": version: 0.6.8 - resolution: "ethereumjs-abi@https://github.com/ethereumjs/ethereumjs-abi.git#commit=ee3994657fa7a427238e6ba92a84d0b529bbcde0" + resolution: "ethereumjs-abi@npm:0.6.8" dependencies: bn.js: ^4.11.8 ethereumjs-util: ^6.0.0 - checksum: ae074be0bb012857ab5d3ae644d1163b908a48dd724b7d2567cfde309dc72222d460438f2411936a70dc949dc604ce1ef7118f7273bd525815579143c907e336 + checksum: cede2a8ae7c7e04eeaec079c2f925601a25b2ef75cf9230e7c5da63b4ea27883b35447365a47e35c1e831af520973a2252af89022c292c18a09a4607821a366b languageName: node linkType: hard @@ -20834,20 +20827,13 @@ __metadata: languageName: node linkType: hard -"is-obj@npm:^1.0.1": +"is-obj@npm:^1.0.0, is-obj@npm:^1.0.1": version: 1.0.1 resolution: "is-obj@npm:1.0.1" checksum: 3ccf0efdea12951e0b9c784e2b00e77e87b2f8bd30b42a498548a8afcc11b3287342a2030c308e473e93a7a19c9ea7854c99a8832a476591c727df2a9c79796c languageName: node linkType: hard -"is-obj@npm:^2.0.0": - version: 2.0.0 - resolution: "is-obj@npm:2.0.0" - checksum: c9916ac8f4621962a42f5e80e7ffdb1d79a3fab7456ceaeea394cd9e0858d04f985a9ace45be44433bf605673c8be8810540fe4cc7f4266fc7526ced95af5a08 - languageName: node - linkType: hard - "is-object@npm:^1.0.1, is-object@npm:~1.0.1": version: 1.0.1 resolution: "is-object@npm:1.0.1" @@ -25886,7 +25872,7 @@ __metadata: languageName: node linkType: hard -"module-deps@npm:^6.0.0, module-deps@npm:^6.2.3": +"module-deps@npm:^6.2.3": version: 6.2.3 resolution: "module-deps@npm:6.2.3" dependencies: @@ -30673,13 +30659,6 @@ __metadata: languageName: node linkType: hard -"resolve@npm:1.1.7": - version: 1.1.7 - resolution: "resolve@npm:1.1.7" - checksum: afd20873fbde7641c9125efe3f940c2a99f6b1f90f1b7b743e744bdaac1cb105b2e4e0317bcc052ed7e31d57afa86b394a4dc9a1b33a297977be134fdf0250ab - languageName: node - linkType: hard - "resolve@npm:^1.1.4, resolve@npm:^1.1.5, resolve@npm:^1.1.6, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.10.1, resolve@npm:^1.11.1, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.17.0, resolve@npm:^1.18.1, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.21.0, resolve@npm:^1.22.0, resolve@npm:^1.22.1, resolve@npm:^1.22.3, resolve@npm:^1.4.0": version: 1.22.3 resolution: "resolve@npm:1.22.3" @@ -30706,13 +30685,6 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@1.1.7#~builtin": - version: 1.1.7 - resolution: "resolve@patch:resolve@npm%3A1.1.7#~builtin::version=1.1.7&hash=07638b" - checksum: e9dbca78600ae56835c43a09f1276876c883e4b4bbd43e2683fa140671519d2bdebeb1c1576ca87c8c508ae2987b3ec481645ac5d3054b0f23254cfc1ce49942 - languageName: node - linkType: hard - "resolve@patch:resolve@^1.1.4#~builtin, resolve@patch:resolve@^1.1.5#~builtin, resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.10.1#~builtin, resolve@patch:resolve@^1.11.1#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.15.1#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.18.1#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.21.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.3#~builtin, resolve@patch:resolve@^1.4.0#~builtin": version: 1.22.3 resolution: "resolve@patch:resolve@npm%3A1.22.3#~builtin::version=1.22.3&hash=07638b"