diff --git a/.circleci/config.yml b/.circleci/config.yml index e25e89fa5..68a1c199a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -701,7 +701,7 @@ jobs: test-e2e-firefox-snaps: executor: node-browsers - parallelism: 2 + parallelism: 4 steps: - checkout - run: @@ -738,7 +738,7 @@ jobs: test-e2e-chrome-snaps: executor: node-browsers - parallelism: 2 + parallelism: 4 steps: - checkout - run: diff --git a/.eslintrc.js b/.eslintrc.js index c842db401..6851d9566 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -81,6 +81,7 @@ module.exports = { files: [ 'app/**/*.js', 'shared/**/*.js', + 'shared/**/*.ts', 'ui/**/*.js', '**/*.test.js', 'test/lib/**/*.js', @@ -272,6 +273,7 @@ module.exports = { 'app/scripts/platforms/*.test.js', 'development/**/*.test.js', 'shared/**/*.test.js', + 'shared/**/*.test.ts', 'test/helpers/*.js', 'test/jest/*.js', 'ui/**/*.test.js', diff --git a/.metamaskrc.dist b/.metamaskrc.dist index d5b8114a8..d430e090f 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -3,10 +3,9 @@ PASSWORD=METAMASK PASSWORD INFURA_PROJECT_ID=00000000000 SEGMENT_WRITE_KEY= SWAPS_USE_DEV_APIS= -PUBNUB_PUB_KEY= -PUBNUB_SUB_KEY= PORTFOLIO_URL= TRANSACTION_SECURITY_PROVIDER= +MULTICHAIN= ; Set this to test changes to the phishing warning page. PHISHING_WARNING_PAGE_URL= diff --git a/.storybook/preview.js b/.storybook/preview.js index 4a74eb33d..fb15f8569 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -38,6 +38,9 @@ addParameters({ ], }, }, + controls: { + expanded: true, + }, }); export const globalTypes = { diff --git a/.storybook/test-data.js b/.storybook/test-data.js index c85ab7259..3f133fcba 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1,5 +1,5 @@ import { draftTransactionInitialState } from '../ui/ducks/send'; -import { HardwareKeyringTypes } from '../shared/constants/hardware-wallets'; +import { KeyringType } from '../shared/constants/keyring'; const state = { invalidCustomNetwork: { @@ -1168,20 +1168,23 @@ const state = { unapprovedTypedMessages: {}, unapprovedTypedMessagesCount: 0, keyringTypes: [ - HardwareKeyringTypes.imported, - HardwareKeyringTypes.hdKeyTree, - HardwareKeyringTypes.trezor, - HardwareKeyringTypes.ledger, + KeyringType.imported, + KeyringType.hdKeyTree, + KeyringType.trezor, + KeyringType.ledger, ], keyrings: [ { - type: HardwareKeyringTypes.hdKeyTree, + type: KeyringType.hdKeyTree, accounts: [ '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - '0x9d0ba4ddac06032527b140912ec808ab9451b788', ], }, + { + type: KeyringType.ledger, + accounts: ['0x9d0ba4ddac06032527b140912ec808ab9451b788'], + }, ], networkConfigurations: { 'test-networkConfigurationId-1': { diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be32abe7..22e11a337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.29.0] +### Added +- [FLASK] Redesign snaps permission screens ([#18372](https://github.com/MetaMask/metamask-extension/pull/18372)) +- [FLASK] Add tooltips to show info about a permission ([#17685](https://github.com/MetaMask/metamask-extension/pull/17685)) + +### Changed +- Add Ledger instructions to the Sign In With Ethereum page ([#18589](https://github.com/MetaMask/metamask-extension/pull/18589)) +- Removed advanced gas toggle from the settings ([#18138](https://github.com/MetaMask/metamask-extension/pull/18138)) +- Improve security provider warning messaging, to give users more info about transactions security providers flag as potentially suspicious ([#18545](https://github.com/MetaMask/metamask-extension/pull/18545)) +- Update wording on token allowance screen: replace "contract" with "third party" ([#18101](https://github.com/MetaMask/metamask-extension/pull/18101)) +- Update wording on token allowance screen: change the review spending cap header text ([#18214](https://github.com/MetaMask/metamask-extension/pull/18214)) +- Added fallback copy for when we're not able to retrieve a erc721 or erc1155 name in the setApprovalForAll screen ([#17992](https://github.com/MetaMask/metamask-extension/pull/17992)) +- Bump contract-metadata version, so that tokens added ([v2.3.0](https://github.com/MetaMask/contract-metadata/pull/1169)) and ([v2.3.1](https://github.com/MetaMask/contract-metadata/pull/1173)) are included in the default MetaMask token lists ([#18589](https://github.com/MetaMask/metamask-extension/pull/18589)) +- [FLASK] Redesign snap content delineator ([#18385](https://github.com/MetaMask/metamask-extension/pull/18385)) +- [FLASK] Redesign key management modal ([#18263](https://github.com/MetaMask/metamask-extension/pull/18263)) +- [FLASK] Redesign snap authorship component ([#18262](https://github.com/MetaMask/metamask-extension/pull/18262)) +- [FLASK] Improve design of snaps settings page when no snaps are installed ([#18172](https://github.com/MetaMask/metamask-extension/pull/18172)) +- [FLASK] Remove permission footer in snap install/update flow ([#18240](https://github.com/MetaMask/metamask-extension/pull/18240)) +- [FLASK] **BREAKING:** Snaps are now required to request permission for at least one handler permission (e.g. `onRpcRequest`) ([#18371](https://github.com/MetaMask/metamask-extension/pull/18371)) +- [FLASK] Fix issues with using `atob` and `btoa` in snaps ([#18371](https://github.com/MetaMask/metamask-extension/pull/18371)) +- [FLASK] Combine the snap installation popups into a single popup ([#18142](https://github.com/MetaMask/metamask-extension/pull/18142)) +- [FLASK] **BREAKING:** Disallow snaps requesting `eth_requestAccounts` and `wallet_requestSnaps` RPC methods ([#18142](https://github.com/MetaMask/metamask-extension/pull/18142)) + +### Fixed +- Add a title to the security provider "What's New" notification ([#18526](https://github.com/MetaMask/metamask-extension/pull/18526)) +- Fix cursor styling on Sign Typed Data screen to use the 'pointer' cursor ([#18046](https://github.com/MetaMask/metamask-extension/pull/18046)) +- Fix layout/styling of the "Hold to reveal" button in the SRP reveal flow([#18496](https://github.com/MetaMask/metamask-extension/pull/18496)) +- Fixed hardware wallet info popup on token allowance screen ([#17881](https://github.com/MetaMask/metamask-extension/pull/17881)) +- Fix send flow on Optimism Goerli network ([#18478](https://github.com/MetaMask/metamask-extension/pull/18478)) +- Disabled button for Import Tokens Modal when no token is selected ([#18396](https://github.com/MetaMask/metamask-extension/pull/18396)) +- [FLASK] Fix crash when requesting unknown snap permission ([#18447](https://github.com/MetaMask/metamask-extension/pull/18447)) +- [FLASK] Fix overflow issues with text coming from snap UI ([#18169](https://github.com/MetaMask/metamask-extension/pull/18169)) +- [FLASK] Snaps e2e test stability improvements ([#18090](https://github.com/MetaMask/metamask-extension/pull/18090)) + ## [10.28.3] ### Fixed - Fix network switching prompted by dapps for users that added the network prior to v10.28.0. ([#18513](https://github.com/MetaMask/metamask-extension/pull/18513)) @@ -15,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix network switching prompted by dapps by fixing the `wallet_switchEthereumChain` handler. ([#18483](https://github.com/MetaMask/metamask-extension/pull/18483)) - Fix to ensure all users see the NFT and transaction security notifications ([#18460](https://github.com/MetaMask/metamask-extension/pull/18460)) - Fix issue blocking Hindi, Japanese and Turkish language users from installing from the Chrome store ([#18487](https://github.com/MetaMask/metamask-extension/pull/18487)) +- [FLASK] Fix window overflow issues with snap UI text ([#18169](https://github.com/MetaMask/metamask-extension/pull/18169)) ## [10.28.1] ### Changed @@ -3653,7 +3688,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.28.3...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.29.0...HEAD +[10.29.0]: https://github.com/MetaMask/metamask-extension/compare/v10.28.3...v10.29.0 [10.28.3]: https://github.com/MetaMask/metamask-extension/compare/v10.28.2...v10.28.3 [10.28.2]: https://github.com/MetaMask/metamask-extension/compare/v10.28.1...v10.28.2 [10.28.1]: https://github.com/MetaMask/metamask-extension/compare/v10.28.0...v10.28.1 diff --git a/README.md b/README.md index 48dbe9f3f..bf2671159 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack - [How to use the TREZOR emulator](./docs/trezor-emulator.md) - [Developing on MetaMask](./development/README.md) - [How to generate a visualization of this repository's development](./development/gource-viz.sh) +- [How to add new confirmations](./docs/confirmations.md) ## Dapp Developer Resources diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 1d11f9a35..c7bb730e5 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -663,12 +663,6 @@ "settings": { "message": "ቅንብሮች" }, - "showAdvancedGasInline": { - "message": "የላቁ የነዳጅ ቁጥጥሮች" - }, - "showAdvancedGasInlineDescription": { - "message": "በላክ እና አረጋግጥ ማያዎች ላይ የነዳጅ ዋጋን ለማሳየትና ቁጥጥሮችን ለመገደብ ይህን ይምረጡ።" - }, "showFiatConversionInTestnets": { "message": "ልወጣን በ Testnets ላይ አሳይ" }, @@ -732,27 +726,6 @@ "symbolBetweenZeroTwelve": { "message": "ምልክቱ 11 ቁምፊዎች ወይም ከዚያ ያነሰ መሆን አለበት።" }, - "syncWithMobile": { - "message": "ከሞባይል ጋር አሳምር" - }, - "syncWithMobileBeCareful": { - "message": "ይህን ኮድ ስካን ሲያደርጉ ሌላ ሰው የእርስዎን ማያ እየተመለከተ አለመሆኑን ያረጋግጡ" - }, - "syncWithMobileComplete": { - "message": "ውሂብዎ በሚገባ ተሳምሯል። በ MetaMask የሞባይል መተግበሪያ ይደሰቱ!" - }, - "syncWithMobileDesc": { - "message": "መለያዎችዎንና መረጃዎችዎን ከሞባይል መሳሪያዎ ጋር ያሳምሩ። የ MetaMask የሞባይል መተግበሪያን ይክፈቱ፣ ወደ \"ቅንብሮች\" ይሂዱና \"ከማሰሺያ ቅጥያ አሳምር\" የሚለውን ይንኩ" - }, - "syncWithMobileDescNewUsers": { - "message": "የ MetaMask የሞባይል መተግበሪያን ለመጀመሪያ ጊዜ ገና እየከፈቱ ከሆነ፣ በስልክዎ ላይ ያሉትን ቅደም ተከተሎች ብቻ ይከተሉ።" - }, - "syncWithMobileScanThisCode": { - "message": "ይህን ኮድ በ MetaMask የሞባይል መተግበሪያዎ ስካን ያድርጉ" - }, - "syncWithMobileTitle": { - "message": "ከሞባይል ጋር አሳምር" - }, "terms": { "message": "የአጠቃቀም ደንቦች" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index aac31f246..392d7fdfa 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -675,12 +675,6 @@ "settings": { "message": "الإعدادات" }, - "showAdvancedGasInline": { - "message": "أدوات التحكم المتقدمة للغاز" - }, - "showAdvancedGasInlineDescription": { - "message": "حدد هذا لإظهار سعر عملة جاس والحد من الضوابط مباشرة على شاشات الإرسال والتأكيد." - }, "showFiatConversionInTestnets": { "message": "عرض التحويل على Testnets" }, @@ -744,27 +738,6 @@ "symbolBetweenZeroTwelve": { "message": "يجب أن يكون الرمز 11 حرفًا أو أقل." }, - "syncWithMobile": { - "message": "مزامنة مع الهاتف المحمول" - }, - "syncWithMobileBeCareful": { - "message": "تأكد من عدم قيام أي شخص آخر بالنظر إلى شاشتك عند مسح هذا الرمز" - }, - "syncWithMobileComplete": { - "message": "تمت مزامنة بياناتك بنجاح. استمتع بتطبيق MetaMask للهاتف المحمول!" - }, - "syncWithMobileDesc": { - "message": "يمكنك مزامنة حساباتك ومعلوماتك مع جهازك المحمول. افتح تطبيق MetaMask على الجهاز المحمول وانتقل إلى \"الإعدادات\" ثم انقر على \"المزامنة من إضافة المتصفح\"" - }, - "syncWithMobileDescNewUsers": { - "message": "إذا قمت للتوّ بفتح تطبيق MetaMask للهواتف المحمولة لأول مرة، فاتبع الخطوات الموجودة في هاتفك." - }, - "syncWithMobileScanThisCode": { - "message": "امسح هذا الرمز ضوئياً باستخدام تطبيق MetaMask للهواتف المحمولة" - }, - "syncWithMobileTitle": { - "message": "المزامنة مع الجهاز المحمول" - }, "terms": { "message": "شروط الاستخدام" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index eea674ea6..e4ce863f5 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -674,12 +674,6 @@ "settings": { "message": "Настройки" }, - "showAdvancedGasInline": { - "message": "Разширено управление на газа" - }, - "showAdvancedGasInlineDescription": { - "message": "Изберете това, за да покажете цените на газа и ограничите контрола директно на екраните за изпращане и потвърждение." - }, "showFiatConversionInTestnets": { "message": "Показване на преобразуването на Testnets" }, @@ -743,27 +737,6 @@ "symbolBetweenZeroTwelve": { "message": "Символът трябва да е 11 символа или по-малко." }, - "syncWithMobile": { - "message": "Синхронизиране с мобилни устройства" - }, - "syncWithMobileBeCareful": { - "message": "Уверете се, че никой друг не гледа екрана ви, когато сканирате този код" - }, - "syncWithMobileComplete": { - "message": "Вашите данни са синхронизирани успешно. Насладете се на мобилното приложение MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Можете да синхронизирате вашите акаунти и информация с мобилното си устройство. Отворете мобилното приложение MetaMask, отидете на „Настройки“ и докоснете „Синхронизиране от разширението на браузъра“" - }, - "syncWithMobileDescNewUsers": { - "message": "Ако отворите приложението MetaMask Mobile за първи път, просто следвайте стъпките в телефона си." - }, - "syncWithMobileScanThisCode": { - "message": "Сканирайте този код с вашето мобилно приложение MetaMask" - }, - "syncWithMobileTitle": { - "message": "Синхронизиране с мобилни устройства" - }, "terms": { "message": "Условия за ползване" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 736c1f72f..6ba3e132d 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -672,12 +672,6 @@ "settings": { "message": "সেটিংস" }, - "showAdvancedGasInline": { - "message": "উন্নত গ্যাস নিয়ন্ত্রণসমূহ" - }, - "showAdvancedGasInlineDescription": { - "message": "গ্যাসের মূল্য দেখাতে এটি নির্বাচন করুন এবং পাঠানোর এবং নিশ্চিতকরণের স্ক্রিনগুলিতে নিয়ন্ত্রণগুলি সরাসরি সীমিত করুন।" - }, "showFiatConversionInTestnets": { "message": "Testnets এ রূপান্তর দেখান" }, @@ -741,27 +735,6 @@ "symbolBetweenZeroTwelve": { "message": "প্রতীকটি 11 টি অক্ষর বা তার চেয়ে কম হতে হবে।" }, - "syncWithMobile": { - "message": "মোবাইল দিয়ে সিঙ্ক করুন" - }, - "syncWithMobileBeCareful": { - "message": "এই কোডটি স্ক্যান করার সময় কেউ আপনার স্ক্রিনটি দেখছে না তা নিশ্চিত করুন" - }, - "syncWithMobileComplete": { - "message": "আপনার ডেটা সফলভাবে সিঙ্ক করা হয়েছে। MetaMask মোবাইল অ্যাপ উপভোগ করুন!" - }, - "syncWithMobileDesc": { - "message": "আপনি আপনার অ্যাকাউন্ট এবং তথ্য আপনার মোবাইল ডিভাইস দিয়ে সিঙ্ক করতে পারেন। MetaMask মোবাইল অ্যাপটি খুলুন, \"সেটিংস\" এ যান এবং \"ব্রাউজার এক্সটেনশন থেকে সিঙ্ক করুন\" এ ট্যাপ করুন " - }, - "syncWithMobileDescNewUsers": { - "message": "আপনি প্রথমবারের জন্য MetaMask খোলার পরে, শুধু আপনার ফোনে পদক্ষেপগুলি অনুসরণ করুন। " - }, - "syncWithMobileScanThisCode": { - "message": "আপনার MetaMask মোবাইল অ্যাপ দিয়ে এই কোডটি স্ক্যান করুন" - }, - "syncWithMobileTitle": { - "message": "মোবাইল দিয়ে সিঙ্ক করুন" - }, "terms": { "message": "ব্যবহারের শর্তাবলী" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index c382f3619..ab1da7841 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -656,12 +656,6 @@ "settings": { "message": "Configuració" }, - "showAdvancedGasInline": { - "message": "Controls de gas avançats" - }, - "showAdvancedGasInlineDescription": { - "message": "Selecciona això per a mostrar el preu del gas i els controls de límit directament a les pantalles d'enviament i confirmació." - }, "showFiatConversionInTestnets": { "message": "Mostra Conversió a Testnets" }, @@ -725,27 +719,6 @@ "symbolBetweenZeroTwelve": { "message": "El símbol ha de tenir com a mínim 11 caràcters." }, - "syncWithMobile": { - "message": "Sincronitza amb el mòbil" - }, - "syncWithMobileBeCareful": { - "message": "Assegura't que no hi ha ningú mirant la teva pantalla quan escanegis aquest codi" - }, - "syncWithMobileComplete": { - "message": "Les teves dades s'han sincronitzat amb èxit. Disfruta de l'app mòbil de MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Pots sincronitzar els teus comptes i la teva informació amb el teu dispositiu mòbil. Obre l'app mòbil de MetaTask, ves a \"Configuració\" i \"Sincronitzar desde l" - }, - "syncWithMobileDescNewUsers": { - "message": "Si acabes d'obrir l'app mòbil MetaMask per primer cop, tan sols has de seguir els passos del teu telèfon." - }, - "syncWithMobileScanThisCode": { - "message": "Escaneja aquest codi amb la teva aplicació mòbil de MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sincronitzar amb mòbil" - }, "terms": { "message": "Condicions d'ús" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 68974f4e7..733b31dab 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -656,12 +656,6 @@ "settings": { "message": "Indstillinger " }, - "showAdvancedGasInline": { - "message": "Avanceret Gas-styring" - }, - "showAdvancedGasInlineDescription": { - "message": "Vælg dette for at vise brændstofprisen og begræns styringen direkte på afsendelses- og bekræftelseskærmene." - }, "showFiatConversionInTestnets": { "message": "Vis konvertering på testnet" }, @@ -722,27 +716,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbolet skal være mindst 11 tegn." }, - "syncWithMobile": { - "message": "Synkronisér med mobil" - }, - "syncWithMobileBeCareful": { - "message": "Sørg for at der ikke er nogen der kigger på din skærm, når du scanner denne kode" - }, - "syncWithMobileComplete": { - "message": "Dine data er blevet synkroniseret korrekt. Nyd MetaMask-mobilappen!" - }, - "syncWithMobileDesc": { - "message": "Du kan synkronisere dine konti og informationer med din mobilenhed. Åbn MetaMask-mobilappen, gå til \"Indstillinger\" og tryk på \"Synkroniser fra browserudvidelse\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Hvis du netop har åbnet MetaMask-mobilappen for første gang, skal du blot følge trinnene på din telefon." - }, - "syncWithMobileScanThisCode": { - "message": "Scan denne kode med din MetaMask-mobilapp" - }, - "syncWithMobileTitle": { - "message": "Synkroniser med mobil" - }, "terms": { "message": "Brugsbetingelser" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 6f4cf6e25..652874ed2 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Auf Etherscan anzeigen" }, - "expandExperience": { - "message": "Erweitern Sie Ihre Web3-Erfahrung" - }, "expandView": { "message": "Ansicht erweitern" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Fehlerhafte Daten" }, - "manageSnaps": { - "message": "Verwalten Sie Ihre installierten Snaps" - }, "max": { "message": "Max." }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Sie sehen Ihren Token nicht?" }, - "mobileSyncWarning": { - "message": "Die Funktion \"Mit Erweiterung synchronisieren\" ist vorübergehend deaktiviert. Wenn Sie Ihre Erweiterungs-Wallet auf MetaMask mobile verwenden möchten, dann gehen Sie in Ihrer mobilen App zurück zu den Einrichtungsoptionen für die Wallet und wählen Sie die Option \"Import mit Geheime Wiederherstellungsphrase\". Verwenden Sie die Geheime Wiederherstellungsphrase Ihrer Erweiterungs-Wallet, um Ihre Wallet in Mobile zu importieren." - }, "moreComingSoon": { "message": "Mehr in Kürze ..." }, @@ -2188,9 +2179,6 @@ "message": "Nonce ist höher als vorgeschlagen nonce von $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "NFT kann nicht hinzugefügt werden, da die Eigentumsangaben nicht übereinstimmen. Stellen Sie sicher, dass Sie die richtigen Informationen eingegeben haben." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Zeigen" }, - "showAdvancedGasInline": { - "message": "Erweiterte Gaskontrollen" - }, - "showAdvancedGasInlineDescription": { - "message": "Wählen Sie dies aus, um den Gaspreis und die Limitkontrollen direkt auf den Senden- und Bestätigen-Bildschirmen anzuzeigen." - }, "showFiatConversionInTestnets": { "message": "Umwandlung auf Testnets anzeigen" }, @@ -3358,23 +3340,12 @@ "message": "Sie gewähren dem Snap „$1“ wichtige $2-Zugriffsrechte. Dies kann nicht rückgängig gemacht werden und gibt „$1“ Kontrolle über Ihre $2-Konten und Vermögenswerte. Stellen Sie sicher, dass Sie „$1“ vertrauen, bevor Sie fortfahren.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Für diesen Snap werden die folgenden Berechtigungen beantragt:" - }, "snapUpdate": { "message": "Snap aktualisieren" }, - "snapUpdateExplanation": { - "message": "$1 benötigt eine neuere Version Ihres Snaps.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snaps" }, - "snapsInsightError": { - "message": "Ein Fehler ist mit $1: $2 aufgetreten", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Transaktions-Einsicht wird geladen ..." }, @@ -3391,7 +3362,8 @@ "message": "Ein Snap wird nur ausgeführt, wenn er aktiviert ist" }, "snapsUIError": { - "message": "Die vom Snap spezifizierte UI ist ungültig." + "message": "Die vom Snap spezifizierte UI ist ungültig.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Einige Netzwerke können Sicherheits- und/oder Datenschutzrisiken bergen. Informieren Sie sich über die Risiken, bevor Sie ein Netzwerk hinzufügen und nutzen." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Das Symbol darf maximal 11 Zeichen lang sein." }, - "syncFailed": { - "message": "Sync fehlgeschlagen" - }, - "syncInProgress": { - "message": "Synchronisation läuft" - }, - "syncWithMobile": { - "message": "Mit Mobilgerät synchronisieren" - }, - "syncWithMobileBeCareful": { - "message": "Stellen Sie sicher, dass niemand sonst auf Ihren Bildschirm blickt, wenn Sie diesen Code scannen" - }, - "syncWithMobileComplete": { - "message": "Ihre Daten wurden erfolgreich synchronisiert. Viel Spaß mit der MetaMask-Handy-App!" - }, - "syncWithMobileDesc": { - "message": "Sie können Ihre Konten und Informationen mit Ihrem Mobilgerät synchronisieren. Öffnen Sie die MetaMask-Mobilapp, gehen Sie zu \"Einstellungen\" und tippen Sie auf \"Von Browsererweiterung aus synchronisieren\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Wenn Sie die MetaMask-Mobilapp gerade zum ersten Mal öffnen, folgen Sie einfach den Schritten auf Ihrem Telefon." - }, - "syncWithMobileScanThisCode": { - "message": "Scannen Sie diesen Code mit Ihrer MetaMask-Mobilapp" - }, - "syncWithMobileTitle": { - "message": "Mit Mobilgerät synchronisieren" - }, "tenPercentIncreased": { "message": "10% Erhöhung" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 50f603838..b1862ae0e 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Προβολή στην Etherscan" }, - "expandExperience": { - "message": "Επεκτείνετε την εμπειρία σας στο web3 με τα Snap του MetaMask" - }, "expandView": { "message": "Ανάπτυξη Προβολής" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Παραμορφωμένα δεδομένα" }, - "manageSnaps": { - "message": "Διαχειριστείτε τα εγκατεστημένα Snap σας" - }, "max": { "message": "Μέγ." }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Δεν βλέπετε το token σας;" }, - "mobileSyncWarning": { - "message": "Η λειτουργία 'Συγχρονισμός με επέκταση' είναι προσωρινά απενεργοποιημένη. Αν θέλετε να χρησιμοποιήσετε το πορτοφόλι της επέκτασής σας στο MetaMask mobile, τότε στην εφαρμογή για το κινητό σας: επιστρέψτε στις επιλογές εγκατάστασης του πορτοφολιού και επιλέξτε την επιλογή 'Εισαγωγή με Μυστική Φράση Ανάκτησης'. Χρησιμοποιήστε τη μυστική φράση του πορτοφολιού της επέκτασής σας για να εισαγάγετε το πορτοφόλι σας στο κινητό." - }, "moreComingSoon": { "message": "Περισσότερα έρχονται σύντομα..." }, @@ -2188,9 +2179,6 @@ "message": "Το Nonce είναι υψηλότερο από το προτεινόμενο nonce του $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "Τα NFT δεν μπορούν να προστεθούν, διότι τα στοιχεία της κυριότητας δεν ταυτίζονται. Σιγουρευτείτε ότι έχετε εισαγάγει τα σωστά στοιχεία." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Εμφάνιση" }, - "showAdvancedGasInline": { - "message": "Προωθημένος έλεγχος gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Επιλέξτε αυτό για να εμφανίσετε τις τιμές αερίου και να περιορίσετε τα στοιχεία ελέγχου απευθείας στις οθόνες αποστολής και επιβεβαίωσης." - }, "showFiatConversionInTestnets": { "message": "Εμφάνιση Μετατροπής σε Δοκιμαστικά Δίκτυα" }, @@ -3358,23 +3340,12 @@ "message": "Εκχωρείτε στο $2 βασική πρόσβαση στο snap \"$1\". Αυτό είναι αμετάκλητο και παρέχει στο \"$1\" τον έλεγχο των λογαριασμών και των περιουσιακών σας στοιχείων $2. Βεβαιωθείτε ότι εμπιστεύεστε το \"$1\" προτού συνεχίσετε.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Αυτό το snap αιτείται τις παρακάτω άδειες:" - }, "snapUpdate": { "message": "Ενημέρωση Snap" }, - "snapUpdateExplanation": { - "message": "Το $1 χρειάζεται μια νεότερη έκδοση του snap σας.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snaps" }, - "snapsInsightError": { - "message": "Παρουσιάστηκε ένα σφάλμα με $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Φόρτωση πληροφοριών συναλλαγών..." }, @@ -3391,7 +3362,8 @@ "message": "Ένα snap θα εκτελεστεί μόνο εάν είναι ενεργοποιημένο" }, "snapsUIError": { - "message": "Η Διεπαφή Χρήστη (UI) που καθορίζεται από το στιγμιότυπο δεν είναι έγκυρη." + "message": "Η Διεπαφή Χρήστη (UI) που καθορίζεται από το στιγμιότυπο δεν είναι έγκυρη.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Ορισμένα δίκτυα ενδέχεται να ενέχουν κινδύνους για την ασφάλεια ή/και το απόρρητο. Ενημερωθείτε για τους κινδύνους πριν προσθέσετε και χρησιμοποιήσετε ένα δίκτυο." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Το σύμβολο πρέπει να είναι τουλάχιστον 11 χαρακτήρες." }, - "syncFailed": { - "message": "Ο συγχρονισμός απέτυχε" - }, - "syncInProgress": { - "message": "Συγχρονισμός σε εξέλιξη" - }, - "syncWithMobile": { - "message": "Συγχρονισμός με κινητό" - }, - "syncWithMobileBeCareful": { - "message": "Σιγουρευτείτε ότι κανένας δεν κοιτάζει στην οθόνη σας όταν κάνετε σάρωση αυτού του κωδικού" - }, - "syncWithMobileComplete": { - "message": "Τα δεδομένα σας έχουν συγχρονιστεί με επιτυχία. Απολαύστε την εφαρμογή MetaMask για κινητά!" - }, - "syncWithMobileDesc": { - "message": "Μπορείτε να συγχρονίσετε τους λογαριασμούς και τις πληροφορίες σας με την κινητή συσκευή σας. Ανοίξτε την εφαρμογή MetaMask για κινητά, μεταβείτε στην ενότητα \"Ρυθμίσεις\" και πατήστε \"Συγχρονισμός από Επέκταση Προγράμματος Περιήγησης\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Εάν απλά ανοίξετε την εφαρμογή MetaMask Mobile για πρώτη φορά, απλώς ακολουθήστε τα βήματα στο τηλέφωνό σας." - }, - "syncWithMobileScanThisCode": { - "message": "Σαρώστε αυτόν τον κώδικα με την εφαρμογή MetaMask για κινητά" - }, - "syncWithMobileTitle": { - "message": "Συγχρονισμός με κινητό" - }, "tenPercentIncreased": { "message": "10% αύξηση" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 9846fad1c..0b78c914f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -103,6 +103,9 @@ "SIWEWarningTitle": { "message": "Are you sure?" }, + "ShowMore": { + "message": "Show more" + }, "about": { "message": "About" }, @@ -150,6 +153,9 @@ "accountSelectionRequired": { "message": "You need to select an account!" }, + "activated": { + "message": "Active" + }, "active": { "message": "Active" }, @@ -171,6 +177,9 @@ "addANickname": { "message": "Add a nickname" }, + "addAccount": { + "message": "Add account" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, @@ -257,6 +266,9 @@ "message": "This network connection relies on third parties. This connection may be less reliable or enable third-parties to track activity. $1", "description": "$1 is Learn more link" }, + "addNewToken": { + "message": "Add new token" + }, "addSuggestedTokens": { "message": "Add suggested tokens" }, @@ -318,6 +330,10 @@ "message": "All of your $1", "description": "$1 is the symbol or name of the token that the user is approving spending" }, + "allYourNFTsOf": { + "message": "All of your NFTs from $1", + "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" + }, "allowExternalExtensionTo": { "message": "Allow this external extension to:" }, @@ -335,6 +351,9 @@ "amount": { "message": "Amount" }, + "apiUrl": { + "message": "API URL" + }, "appDescription": { "message": "An Ethereum Wallet in your Browser", "description": "The description of the application" @@ -362,6 +381,10 @@ "message": "Allow access to and transfer of all your $1?", "description": "$1 is the symbol of the token for which the user is granting approval" }, + "approveAllTokensTitleWithoutSymbol": { + "message": "Allow access to and transfer all of your NFTs from $1?", + "description": "$1 a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" + }, "approveAndInstall": { "message": "Approve & install" }, @@ -378,6 +401,10 @@ "approveTokenDescription": { "message": "This allows a third party to access and transfer the following NFTs without further notice until you revoke its access." }, + "approveTokenDescriptionWithoutSymbol": { + "message": "This allows a third party to access and transfer all of your NFTs from $1 without further notice until you revoke its access.", + "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" + }, "approveTokenTitle": { "message": "Allow access to and transfer of your $1?", "description": "$1 is the symbol of the token for which the user is granting approval" @@ -621,9 +648,45 @@ "close": { "message": "Close" }, + "codefiCompliance": { + "message": "Codefi Compliance" + }, "coingecko": { "message": "CoinGecko" }, + "complianceActivatedDesc": { + "message": "You can now use compliance in MetaMask Institutional. Receiving AML/CFT analysis within the confirmation screen on all the addresses you interact with." + }, + "complianceActivatedTitle": { + "message": "Your compliance feature is activated" + }, + "complianceBlurb0": { + "message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties." + }, + "complianceBlurb1": { + "message": "Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting." + }, + "complianceBlurbStep1": { + "message": "Sign up to Codefi Compliance below" + }, + "complianceBlurbStep2": { + "message": "Create an organisation" + }, + "complianceBlurbStep3": { + "message": "Create a project" + }, + "complianceBlurbStep4": { + "message": "Set your compliance settings" + }, + "complianceBlurbStep5": { + "message": "Click the \"Enable Compliance in MMI\" button" + }, + "complianceBlurpStep0": { + "message": "Steps to enable AML/CFT Compliance:" + }, + "complianceSettingsExplanation": { + "message": "Change your settings or view reports by opening up Codefi Compliance or disconnect below." + }, "confirm": { "message": "Confirm" }, @@ -723,6 +786,9 @@ "connectingToSepolia": { "message": "Connecting to Sepolia test network" }, + "connectionError": { + "message": "Connection error" + }, "contactUs": { "message": "Contact us" }, @@ -749,7 +815,7 @@ "message": "Contract deployment" }, "contractDescription": { - "message": "To protect yourself against scammers, take a moment to verify contract details." + "message": "To protect yourself against scammers, take a moment to verify third-party details." }, "contractInteraction": { "message": "Contract interaction" @@ -764,10 +830,10 @@ "message": "Contract requesting signature" }, "contractRequestingSpendingCap": { - "message": "Contract requesting spending cap" + "message": "Third party requesting spending cap" }, "contractTitle": { - "message": "Contract details" + "message": "Third-party details" }, "contractToken": { "message": "Token contract" @@ -857,6 +923,12 @@ "curveMediumGasEstimate": { "message": "Market gas estimate graph" }, + "custodian": { + "message": "Custodian" + }, + "custodianAccount": { + "message": "Custodian account" + }, "custom": { "message": "Advanced" }, @@ -1311,9 +1383,17 @@ "errorWhileConnectingToRPC": { "message": "Error while connecting to the custom network." }, + "errorWithSnap": { + "message": "Error with $1", + "description": "$1 represents the name of the snap" + }, "ethGasPriceFetchWarning": { "message": "Backup gas price is provided as the main gas estimation service is unavailable right now." }, + "ethereumProviderAccess": { + "message": "Grant Ethereum provider access to $1", + "description": "The parameter is the name of the requesting origin" + }, "ethereumPublicAddress": { "message": "Ethereum public address" }, @@ -1326,18 +1406,21 @@ "etherscanViewOn": { "message": "View on Etherscan" }, - "expandExperience": { - "message": "Expand your web3 experience with MetaMask Snaps" - }, "expandView": { "message": "Expand view" }, "experimental": { "message": "Experimental" }, + "exploreMetaMaskSnaps": { + "message": "Explore MetaMask Snaps" + }, "exportPrivateKey": { "message": "Export private key" }, + "extendWalletWithSnaps": { + "message": "Extend the wallet experience." + }, "externalExtension": { "message": "External extension" }, @@ -1367,6 +1450,9 @@ "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" }, + "fileTooBig": { + "message": "The dropped file is too big." + }, "flaskSnapSettingsCardButtonCta": { "message": "See details", "description": "Call to action a user can take to see more information about the snap that is installed" @@ -1543,6 +1629,9 @@ "hardware": { "message": "Hardware" }, + "hardwareWallet": { + "message": "Hardware wallet" + }, "hardwareWalletConnected": { "message": "Hardware wallet connected" }, @@ -1627,6 +1716,9 @@ "holdToRevealTitle": { "message": "Keep your SRP safe" }, + "id": { + "message": "Id" + }, "ignoreAll": { "message": "Ignore all" }, @@ -1716,18 +1808,25 @@ "message": "Your initial transaction was confirmed by the network. Click OK to go back." }, "inputLogicEmptyState": { - "message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later." + "message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later." }, "inputLogicEqualOrSmallerNumber": { - "message": "This allows the contract to spend $1 from your current balance.", + "message": "This allows the third party to spend $1 from your current balance.", "description": "$1 is the current token balance in the account and the name of the current token" }, "inputLogicHigherNumber": { - "message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap." + "message": "This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap." + }, + "insightsFromSnap": { + "message": "Insights from $1", + "description": "$1 represents the name of the snap" }, "install": { "message": "Install" }, + "institutionalFeatures": { + "message": "Institutional Features" + }, "insufficientBalance": { "message": "Insufficient balance." }, @@ -1940,6 +2039,9 @@ "lock": { "message": "Lock" }, + "lockMetaMask": { + "message": "Lock MetaMask" + }, "lockTimeTooGreat": { "message": "Lock time is too great" }, @@ -1976,9 +2078,6 @@ "malformedData": { "message": "Malformed data" }, - "manageSnaps": { - "message": "Manage your installed snaps" - }, "max": { "message": "Max" }, @@ -2054,8 +2153,11 @@ "missingToken": { "message": "Don't see your token?" }, - "mobileSyncWarning": { - "message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile." + "mmiAddToken": { + "message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional" + }, + "mmiAuthenticate": { + "message": "The page at $1 would like to authorise the following project’s compliance settings in MetaMask Institutional" }, "moreComingSoon": { "message": "More coming soon..." @@ -2105,6 +2207,9 @@ "networkIsBusy": { "message": "Network is busy. Gas prices are high and estimates are less accurate." }, + "networkMenuHeading": { + "message": "Select a network" + }, "networkName": { "message": "Network name" }, @@ -2215,9 +2320,6 @@ "message": "Nonce is higher than suggested nonce of $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "NFT can’t be added as the ownership details do not match. Make sure you have entered correct information." }, @@ -2270,7 +2372,7 @@ "message": "No NFTs yet" }, "noSnaps": { - "message": "No Snaps installed" + "message": "You don't have any snaps installed." }, "noThanksVariant2": { "message": "No, thanks." @@ -2381,6 +2483,9 @@ "message": "OpenSea is the first provider for this feature. More providers coming soon!", "description": "Description of a notification in the 'See What's New' popup. Describes Opensea Security Provider feature." }, + "notifications18Title": { + "message": "Stay safe with security alerts" + }, "notifications19ActionText": { "message": "Enable NFT autodetection" }, @@ -2407,6 +2512,18 @@ "message": "Swapping on mobile is here!", "description": "Title for a notification in the 'See What's New' popup. Tells users that they can now use MetaMask Swaps on Mobile." }, + "notifications20ActionText": { + "message": "Learn more", + "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a ledger page to resolve the U2F connection issue." + }, + "notifications20Description": { + "message": "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support.", + "description": "Description of a notification in the 'See What's New' popup. Describes the U2F support being dropped by firefox and that it affects ledger users." + }, + "notifications20Title": { + "message": "Ledger and Firefox Users Experiencing Connection Issues", + "description": "Title for a notification in the 'See What's New' popup. Tells users that latest firefox users using U2F may experience connection issues." + }, "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." @@ -2652,6 +2769,9 @@ "onlyConnectTrust": { "message": "Only connect with sites you trust." }, + "openCodefiCompliance": { + "message": "Open Codefi Compliance" + }, "openFullScreenForLedgerWebHid": { "message": "Open MetaMask in full screen to connect your ledger via WebHID.", "description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid." @@ -2761,22 +2881,42 @@ "message": "Access the internet.", "description": "The description of the `endowment:network-access` permission." }, + "permission_accessNetworkDescription": { + "message": "Allow the snap to access the internet. This can be used to both send and receive data with third-party servers.", + "description": "An extended description of the `endowment:network-access` permission." + }, "permission_accessSnap": { "message": "Connect to the $1 snap.", "description": "The description for the `wallet_snap` permission. $1 is the name of the snap." }, + "permission_accessSnapDescription": { + "message": "Allow the website or snap to interact with $1.", + "description": "The description for the `wallet_snap_*` permission. $1 is the name of the Snap." + }, "permission_cronjob": { "message": "Schedule and execute periodic actions.", "description": "The description for the `snap_cronjob` permission" }, + "permission_cronjobDescription": { + "message": "Allow the snap to perform actions that run periodically at fixed times, dates, or intervals. This can be used to trigger time-sensitive interactions or notifications.", + "description": "An extended description for the `snap_cronjob` permission" + }, "permission_customConfirmation": { "message": "Display a confirmation in MetaMask.", "description": "The description for the `snap_confirm` permission" }, + "permission_customConfirmationDescription": { + "message": "Allow the snap to display MetaMask popups with custom text, and buttons to approve or reject an action.", + "description": "An extended description for the `snap_confirm` permission" + }, "permission_dialog": { "message": "Display dialog windows in MetaMask.", "description": "The description for the `snap_dialog` permission" }, + "permission_dialogDescription": { + "message": "Allow the snap to display MetaMask popups with custom text, input field, and buttons to approve or reject an action.\nCan be used to create e.g. alerts, confirmations, and opt-in flows for a snap.", + "description": "An extended description for the `snap_dialog` permission" + }, "permission_ethereumAccounts": { "message": "See address, account balance, activity and suggest transactions to approve", "description": "The description for the `eth_accounts` permission" @@ -2785,22 +2925,42 @@ "message": "Access the Ethereum provider.", "description": "The description for the `endowment:ethereum-provider` permission" }, + "permission_ethereumProviderDescription": { + "message": "Allow the snap to communicate with MetaMask directly, in order for it to read data from the blockchain and suggest messages and transactions.", + "description": "An extended description for the `endowment:ethereum-provider` permission" + }, "permission_getEntropy": { "message": "Derive arbitrary keys unique to this snap.", "description": "The description for the `snap_getEntropy` permission" }, + "permission_getEntropyDescription": { + "message": "Allow the snap to derive arbitrary keys unique to this snap, without exposing them. These keys are separate from your MetaMask account(s) and not related to your private keys or Secret Recovery Phrase. Other snaps cannot access this information.", + "description": "An extended description for the `snap_getEntropy` permission" + }, "permission_longRunning": { "message": "Run indefinitely.", "description": "The description for the `endowment:long-running` permission" }, + "permission_longRunningDescription": { + "message": "Allow the snap to run indefinitely while, for example, processing large amounts of data.", + "description": "An extended description for the `endowment:long-running` permission" + }, "permission_manageBip32Keys": { "message": "Control your accounts and assets under $1 ($2).", "description": "The description for the `snap_getBip32Entropy` permission. $1 is a derivation path, e.g. 'm/44'/0'/0''. $2 is the elliptic curve name, e.g. 'secp256k1'." }, + "permission_manageBip32KeysDescription": { + "message": "Allow the snap to derive BIP-32 key pairs based on your Secret Recovery Phrase without exposing it. This grants full access to all accounts and assets on $1.\nWith the power to manage keys, the snap can support a variety of blockchain protocols beyond Ethereum (EVMs).", + "description": "An extended description for the `snap_getBip32Entropy` permission. $1 is a derivation path (name)" + }, "permission_manageBip44Keys": { - "message": "Control your \"$1\" accounts and assets.", + "message": "Control your $1 accounts and assets.", "description": "The description for the `snap_getBip44Entropy` permission. $1 is the name of a protocol, e.g. 'Filecoin'." }, + "permission_manageBip44KeysDescription": { + "message": "Allow the snap to derive BIP-44 key pairs based on your Secret Recovery Phrase without exposing it. This grants full access to all accounts and assets on $1.\nWith the power to manage keys, the snap can support a variety of blockchain protocols beyond Ethereum (EVMs).", + "description": "An extended description for the `snap_getBip44Entropy` permission. $1 is the name of a protocol, e.g., 'Filecoin'." + }, "permission_manageNamedBip32Keys": { "message": "Control your $1 accounts and assets.", "description": "The description for the `snap_getBip32Entropy` permission. $1 is a name for the derivation path, e.g., 'Ethereum accounts'. $2 is the plain derivation path, e.g. 'm/44'/0'/0''." @@ -2809,22 +2969,42 @@ "message": "Store and manage its data on your device.", "description": "The description for the `snap_manageState` permission" }, + "permission_manageStateDescription": { + "message": "Allow the snap to store, update, and retrieve data securely with encryption. Other snaps cannot access this information.", + "description": "An extended description for the `snap_manageState` permission" + }, "permission_notifications": { "message": "Show notifications.", "description": "The description for the `snap_notify` permission" }, + "permission_notificationsDescription": { + "message": "Allow the snap to display notifications within MetaMask. A short notification text can be triggered by a snap for actionable or time-sensitive information.", + "description": "An extended description for the `snap_notify` permission" + }, "permission_rpc": { "message": "Allow $1 to communicate directly with this snap.", "description": "The description for the `endowment:rpc` permission. $1 is 'other snaps' or 'websites'." }, + "permission_rpcDescription": { + "message": "Allow $1 to send messages to the snap and receive a response from the snap.", + "description": "An extended description for the `endowment:rpc` permission. $1 is 'other snaps' or 'websites'." + }, "permission_transactionInsight": { "message": "Fetch and display transaction insights.", "description": "The description for the `endowment:transaction-insight` permission" }, + "permission_transactionInsightDescription": { + "message": "Allow the snap to decode transactions and show insights within the MetaMask UI. This can be used for anti-phishing and security solutions.", + "description": "An extended description for the `endowment:transaction-insight` permission" + }, "permission_transactionInsightOrigin": { "message": "See the origins of websites that suggest transactions", "description": "The description for the `transactionOrigin` caveat, to be used with the `endowment:transaction-insight` permission" }, + "permission_transactionInsightOriginDescription": { + "message": "Allow the snap to see the origin (URI) of websites that suggest transactions. This can be used for anti-phishing and security solutions.", + "description": "An extended description for the `transactionOrigin` caveat, to be used with the `endowment:transaction-insight` permission" + }, "permission_unknown": { "message": "Unknown permission: $1", "description": "$1 is the name of a requested permission that is not recognized." @@ -2833,6 +3013,10 @@ "message": "View your public key for $1 ($2).", "description": "The description for the `snap_getBip32PublicKey` permission. $1 is a derivation path, e.g. 'm/44'/0'/0''. $2 is the elliptic curve name, e.g. 'secp256k1'." }, + "permission_viewBip32PublicKeysDescription": { + "message": "Allow the snap to view your public keys (and addresses) for $1. This does not grant any control of accounts or assets.", + "description": "An extended description for the `snap_getBip32PublicKey` permission. $1 is a derivation path (name)" + }, "permission_viewNamedBip32PublicKeys": { "message": "View your public key for $1.", "description": "The description for the `snap_getBip32PublicKey` permission. $1 is a name for the derivation path, e.g., 'Ethereum accounts'." @@ -2841,6 +3025,10 @@ "message": "Support for WebAssembly.", "description": "The description of the `endowment:webassembly` permission." }, + "permission_webAssemblyDescription": { + "message": "Allow the snap to access low-level execution environments via WebAssembly.", + "description": "An extended description of the `endowment:webassembly` permission." + }, "permissions": { "message": "Permissions" }, @@ -2860,6 +3048,9 @@ "portfolio": { "message": "Portfolio" }, + "portfolioView": { + "message": "Portfolio view" + }, "preferredLedgerConnectionType": { "message": "Preferred Ledger connection type", "description": "A header for a dropdown in Settings > Advanced. Appears above the ledgerConnectionPreferenceDescription message" @@ -2901,6 +3092,12 @@ "proceedWithTransaction": { "message": "I want to proceed anyway" }, + "projectIdInvalid": { + "message": "Provided Project ID is invalid" + }, + "projectName": { + "message": "Project Name" + }, "proposedApprovalLimit": { "message": "Proposed approval limit" }, @@ -3016,6 +3213,9 @@ "replace": { "message": "replace" }, + "requestFailed": { + "message": "Request failed" + }, "requestFlaggedAsMaliciousFallbackCopyReason": { "message": "The security provider has not shared additional details" }, @@ -3117,22 +3317,30 @@ "message": "Reveal seed phrase" }, "reviewSpendingCap": { - "message": "Review your spending cap" + "message": "Review the spending cap for your" }, "revokeAllTokensTitle": { "message": "Revoke permission to access and transfer all of your $1?", "description": "$1 is the symbol of the token for which the user is revoking approval" }, + "revokeAllTokensTitleWithoutSymbol": { + "message": "Revoke permission to access and transfer all of your NFTs from $1?", + "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" + }, "revokeApproveForAllDescription": { "message": "This revokes the permission for a third party to access and transfer all of your $1 without further notice.", "description": "$1 is either a string or link of a given token symbol or name" }, + "revokeApproveForAllDescriptionWithoutSymbol": { + "message": "This revokes the permission for a third party to access and transfer all of your NFTs from $1 without further notice.", + "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" + }, "revokeSpendingCap": { "message": "Revoke spending cap for your $1", "description": "$1 is a token symbol" }, "revokeSpendingCapTooltipText": { - "message": "This contract will be unable to spend any more of your current or future tokens." + "message": "This third party will be unable to spend any more of your current or future tokens." }, "rpcUrl": { "message": "New RPC URL" @@ -3248,6 +3456,9 @@ "selectHdPath": { "message": "Select HD path" }, + "selectJWT": { + "message": "Select token" + }, "selectNFTPrivacyPreference": { "message": "Turn on NFT detection in Settings" }, @@ -3320,12 +3531,6 @@ "show": { "message": "Show" }, - "showAdvancedGasInline": { - "message": "Advanced gas controls" - }, - "showAdvancedGasInlineDescription": { - "message": "Select this to show gas price and limit controls directly on the send and confirm screens." - }, "showFiatConversionInTestnets": { "message": "Show conversion on test networks" }, @@ -3415,36 +3620,60 @@ "snapInstall": { "message": "Install snap" }, + "snapInstallRequest": { + "message": "$1 wants to install $2. Make sure you trust the authors before you proceed.", + "description": "$1 is the dApp origin requesting the snap and $2 is the snap name" + }, + "snapInstallRequestsPermission": { + "message": "$1 wants to install $2, which is requesting the following permissions. Make sure you trust the authors before you proceed.", + "description": "$1 is the dApp origin requesting the snap and $2 is the snap name" + }, "snapInstallWarningCheck": { - "message": "To confirm that you understand, check the box." + "message": "Ensure that the permission below align with your intended actions. Only proceed with authors you trust." }, "snapInstallWarningCheckPlural": { - "message": "To confirm that you understand, check all the boxes." + "message": "Ensure that the permissions below align with your intended actions. Only proceed with authors you trust." + }, + "snapInstallWarningHeading": { + "message": "Proceed with caution" }, "snapInstallWarningKeyAccess": { - "message": "You are granting $2 key access to the snap \"$1\". This is irrevocable and grants \"$1\" control of your $2 accounts and assets. Make sure you trust \"$1\" before proceeding.", + "message": "Grant $2 account control to $1", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "This snap is requesting the following permissions:" + "snapInstallWarningPublicKeyAccess": { + "message": "Grant $2 public key access to $1", + "description": "The first parameter is the name of the snap and the second one is the protocol" + }, + "snapResultError": { + "message": "Error" + }, + "snapResultSuccess": { + "message": "Success" + }, + "snapResultSuccessDescription": { + "message": "$1 is now available to use." }, "snapUpdate": { "message": "Update snap" }, - "snapUpdateExplanation": { - "message": "$1 needs a newer version of your snap.", - "description": "$1 is the dapp that is requesting an update to the snap." + "snapUpdateRequest": { + "message": "$1 wants to update $2. Make sure you trust the authors before you proceed.", + "description": "$1 is the dApp origin requesting the snap and $2 is the snap name" + }, + "snapUpdateRequestsPermission": { + "message": "$1 wants to update $2, which is requesting the following permissions. Make sure you trust the authors before you proceed.", + "description": "$1 is the dApp origin requesting the snap and $2 is the snap name" }, "snaps": { "message": "Snaps" }, - "snapsInsightError": { - "message": "An error occured with $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Loading transaction insight..." }, + "snapsInvalidUIError": { + "message": "The UI specified by the snap is invalid." + }, "snapsNoInsight": { "message": "The snap didn't return any insight" }, @@ -3458,7 +3687,8 @@ "message": "A snap will only run if it is enabled" }, "snapsUIError": { - "message": "The UI specified by the snap is invalid." + "message": "Contact the creators of $1 for further support.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network." @@ -3567,6 +3797,9 @@ "statusNotConnected": { "message": "Not connected" }, + "statusNotConnectedAccount": { + "message": "No accounts connected" + }, "step1LatticeWallet": { "message": "Connect your Lattice1" }, @@ -3896,7 +4129,7 @@ "message": "Select a quote" }, "swapSelectAToken": { - "message": "Select a token" + "message": "Select token" }, "swapSelectQuotePopoverDescription": { "message": "Below are all the quotes gathered from multiple liquidity sources." @@ -4032,33 +4265,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbol must be 11 characters or fewer." }, - "syncFailed": { - "message": "Sync failed" - }, - "syncInProgress": { - "message": "Sync in progress" - }, - "syncWithMobile": { - "message": "Sync with mobile" - }, - "syncWithMobileBeCareful": { - "message": "Make sure nobody else is looking at your screen when you scan this code" - }, - "syncWithMobileComplete": { - "message": "Your data has been synced successfully. Enjoy the MetaMask mobile app!" - }, - "syncWithMobileDesc": { - "message": "You can sync your accounts and information with your mobile device. Open the MetaMask mobile app, go to \"Settings\" and tap on \"Sync from Browser Extension\"" - }, - "syncWithMobileDescNewUsers": { - "message": "If you just open the MetaMask Mobile app for the first time, just follow the steps in your phone." - }, - "syncWithMobileScanThisCode": { - "message": "Scan this code with your MetaMask mobile app" - }, - "syncWithMobileTitle": { - "message": "Sync with mobile" - }, "tenPercentIncreased": { "message": "10% increase" }, @@ -4083,6 +4289,9 @@ "thingsToKeep": { "message": "Things to keep in mind:" }, + "thisCollection": { + "message": "this collection" + }, "thisIsBasedOn": { "message": "This is based on information from " }, @@ -4162,6 +4371,12 @@ "tooltipApproveButton": { "message": "I understand" }, + "tooltipSatusConnected": { + "message": "connected" + }, + "tooltipSatusNotConnected": { + "message": "not connected" + }, "total": { "message": "Total" }, @@ -4285,6 +4500,22 @@ "transferFrom": { "message": "Transfer from" }, + "troubleConnectingToLedgerU2FOnFirefox": { + "message": "We're having trouble connecting your Ledger. $1", + "description": "$1 is a link to the wallet connection guide;" + }, + "troubleConnectingToLedgerU2FOnFirefox2": { + "message": "Review our hardware wallet connection guide and try again.", + "description": "$1 of the ledger wallet connection guide" + }, + "troubleConnectingToLedgerU2FOnFirefoxLedgerSolution": { + "message": "If you're on the latest version of Firefox, you might be experiencing an issue related to Firefox dropping U2F support. Learn how to fix this issue $1.", + "description": "It is a link to the ledger website for the workaround." + }, + "troubleConnectingToLedgerU2FOnFirefoxLedgerSolution2": { + "message": "here", + "description": "Second part of the error message; It is a link to the ledger website for the workaround." + }, "troubleConnectingToWallet": { "message": "We had trouble connecting to your $1, try reviewing $2 and try again.", "description": "$1 is the wallet device name; $2 is a link to wallet connection guide" @@ -4375,6 +4606,9 @@ "upArrow": { "message": "up arrow" }, + "update": { + "message": "Update" + }, "updatedWithDate": { "message": "Updated $1" }, @@ -4427,7 +4661,7 @@ "message": "Username" }, "verifyContractDetails": { - "message": "Verify contract details" + "message": "Verify third-party details" }, "verifyThisTokenDecimalOn": { "message": "Token decimal can be found on $1", @@ -4470,6 +4704,9 @@ "message": "View $1 on Etherscan", "description": "$1 is the action type. e.g (Account, Transaction, Swap)" }, + "viewOnExplorer": { + "message": "View on explorer" + }, "viewOnOpensea": { "message": "View on Opensea" }, @@ -4510,7 +4747,7 @@ "message": "Warning" }, "warningTooltipText": { - "message": "$1 The contract could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", + "message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, "weak": { diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 3ff6332bc..74866edaf 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Ver en Etherscan" }, - "expandExperience": { - "message": "Amplíe su experiencia web3 con complementos de MetaMask" - }, "expandView": { "message": "Expandir vista" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Datos con formato incorrecto" }, - "manageSnaps": { - "message": "Administre sus complementos instalados" - }, "max": { "message": "Máx." }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "¿No ve su token?" }, - "mobileSyncWarning": { - "message": "La función 'Sincronizar con la extensión' está temporalmente desactivada. Si desea utilizar su cartera de extensión en MetaMask móvil, haga lo siguiente en la aplicación móvil: vuelva a las opciones de configuración de la cartera y seleccione la opción 'Importar con frase secreta de recuperación'. Use la frase secreta de su cartera de extensión para importar su cartera al móvil." - }, "moreComingSoon": { "message": "Más próximamente..." }, @@ -2188,9 +2179,6 @@ "message": "El nonce es superior al nonce sugerido de $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "No se puede agregar el NFT porque los detalles de propiedad no coinciden. Asegúrese de haber ingresado la información correcta." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Mostrar" }, - "showAdvancedGasInline": { - "message": "Controles avanzados de gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación." - }, "showFiatConversionInTestnets": { "message": "Mostrar conversión en redes de prueba" }, @@ -3358,23 +3340,12 @@ "message": "Está otorgando acceso clave de $2 al complemento \"$1\". Esto es irrevocable y le otorga a \"$1\" el control de sus cuentas y activos de $2. Asegúrese de que confía en \"$1\" antes de continuar.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Este complemento solicita los siguientes permisos:" - }, "snapUpdate": { "message": "Actualizar complemento" }, - "snapUpdateExplanation": { - "message": "$1 necesita una versión más reciente de su complemento.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Complementos" }, - "snapsInsightError": { - "message": "Ocurrió un error con $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Cargando información de transacción..." }, @@ -3391,7 +3362,8 @@ "message": "Un complemento solo se ejecutará si está habilitado" }, "snapsUIError": { - "message": "La IU especificada por el complemento no es válida." + "message": "La IU especificada por el complemento no es válida.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Algunas redes pueden presentar riesgos de seguridad y/o privacidad. Comprenda los riesgos antes de agregar y utilizar una red." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "El símbolo debe tener 11 caracteres o menos." }, - "syncFailed": { - "message": "Error al sincronizar" - }, - "syncInProgress": { - "message": "Sincronización en progreso" - }, - "syncWithMobile": { - "message": "Sincronizar con dispositivo móvil" - }, - "syncWithMobileBeCareful": { - "message": "Asegúrese de que nadie vea su pantalla cuando escanee este código" - }, - "syncWithMobileComplete": { - "message": "Los datos se sincronizaron correctamente. ¡Disfrute de la aplicación móvil de MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Puede sincronizar sus cuentas y su información con el dispositivo móvil. Abra la aplicación móvil de MetaMask, vaya a \"Configuración\" y presione \"Sincronizar desde la extensión del explorador\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Si acaba de abrir la aplicación móvil de MetaMask por primera vez, siga los pasos que aparecen en el teléfono." - }, - "syncWithMobileScanThisCode": { - "message": "Escanear este código con la aplicación móvil de MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sincronizar con dispositivo móvil" - }, "tenPercentIncreased": { "message": "10% de aumento" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 5e90a0c91..e46218f14 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1369,9 +1369,6 @@ "missingToken": { "message": "¿No ve su token?" }, - "mobileSyncWarning": { - "message": "La función 'Sincronizar con la extensión' está temporalmente desactivada. Si desea utilizar su cartera de extensión en MetaMask móvil, haga lo siguiente en la aplicación móvil: vuelva a las opciones de configuración de la cartera y seleccione la opción 'Importar con frase secreta de recuperación'. Use la frase secreta de su cartera de extensión para importar su cartera al móvil." - }, "mustSelectOne": { "message": "Debe seleccionar al menos 1 token." }, @@ -2070,12 +2067,6 @@ "show": { "message": "Mostrar" }, - "showAdvancedGasInline": { - "message": "Controles avanzados de gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación." - }, "showFiatConversionInTestnets": { "message": "Mostrar conversión en redes de prueba" }, @@ -2555,33 +2546,6 @@ "symbolBetweenZeroTwelve": { "message": "El símbolo debe tener 11 caracteres o menos." }, - "syncFailed": { - "message": "Error al sincronizar" - }, - "syncInProgress": { - "message": "Sincronización en progreso" - }, - "syncWithMobile": { - "message": "Sincronizar con dispositivo móvil" - }, - "syncWithMobileBeCareful": { - "message": "Asegúrese de que nadie vea su pantalla cuando escanee este código" - }, - "syncWithMobileComplete": { - "message": "Los datos se sincronizaron correctamente. ¡Disfrute de la aplicación móvil de MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Puede sincronizar sus cuentas y su información con el dispositivo móvil. Abra la aplicación móvil de MetaMask, vaya a \"Configuración\" y presione \"Sincronizar desde la extensión del explorador\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Si acaba de abrir la aplicación móvil de MetaMask por primera vez, siga los pasos que aparecen en el teléfono." - }, - "syncWithMobileScanThisCode": { - "message": "Escanear este código con la aplicación móvil de MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sincronizar con dispositivo móvil" - }, "tenPercentIncreased": { "message": "10% de aumento" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 1efe138a8..f9a64457e 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -668,12 +668,6 @@ "settings": { "message": "Seaded" }, - "showAdvancedGasInline": { - "message": "Täiustatud gaasijuhikud" - }, - "showAdvancedGasInlineDescription": { - "message": "Valige see, et kuvada gaasi hinda ja piirangut otse saatmise ning kinnitamise kuval." - }, "showFiatConversionInTestnets": { "message": "Kuva teisendus Testnetsis" }, @@ -737,27 +731,6 @@ "symbolBetweenZeroTwelve": { "message": "Sümbol peab olema 11 tähemärki või vähem." }, - "syncWithMobile": { - "message": "Mobiiliga sünkroonimine" - }, - "syncWithMobileBeCareful": { - "message": "Veenduge, et keegi teine ei vaata selle koodi skannimisel teie ekraani" - }, - "syncWithMobileComplete": { - "message": "Teie andmed on edukalt sünkroonitud. Nautige MetaMaski mobiilirakendust!" - }, - "syncWithMobileDesc": { - "message": "Saate sünkroonida oma kontod ja teabe oma mobiiliseadmega. Avage MetaMaski mobiilirakendus, avage \"Settings\" (Seaded) ja puudutage valikut \"Sync from Browser Extension\" (Sünkroonimine lehitseja laiendusest)" - }, - "syncWithMobileDescNewUsers": { - "message": "Järgige MetaMaski mobiilirakenduse esmakordsel avamisel telefonis esitatud samme." - }, - "syncWithMobileScanThisCode": { - "message": "Skanneerige see kood MetaMaski mobiilirakendusega" - }, - "syncWithMobileTitle": { - "message": "Mobiiliga sünkroonimine" - }, "terms": { "message": "Teenusetingimused" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index ccf627d31..6d61ed44d 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -678,12 +678,6 @@ "settings": { "message": "تنظیمات" }, - "showAdvancedGasInline": { - "message": "کنترول های پیشرفته گاز" - }, - "showAdvancedGasInlineDescription": { - "message": "این را انتخاب نمایید تا قیمت گاز را نشان داده و کنترول ها را بصورت مستقیم در صفحات ارسال و تأیید محدود نماید." - }, "showFiatConversionInTestnets": { "message": "نمایش تغییرات Testnets" }, @@ -747,27 +741,6 @@ "symbolBetweenZeroTwelve": { "message": "نماد باید 11 کاراکتر یا کمتر باشد." }, - "syncWithMobile": { - "message": "همگام سازی با موبایل" - }, - "syncWithMobileBeCareful": { - "message": "مطمئن شوید که هیچکس هنگامیکه این کود را سکن میکنید، به صفحه شما نمیبیند" - }, - "syncWithMobileComplete": { - "message": "دیتای شما موفقانه همگام سازی شد. از اپلیکیشن موبایل MetaMask لذت ببرید!" - }, - "syncWithMobileDesc": { - "message": "شما میتوانید حساب ها و معلومات خویش را با دستگاه موبایل تان همگام بسازید. اپلیکیشن MetaMask را باز نموده به \"Settings\" رفته و بالای \"Sync from Browser Extension\" کلیک کنید" - }, - "syncWithMobileDescNewUsers": { - "message": "در صورتیکه شما اپلیکیشن موبایل MetaMask را برای بار اول باز کرده اید، فقط گام ها را در موبایل تان دنبال نمایید." - }, - "syncWithMobileScanThisCode": { - "message": "این کود را با اپلیکیشن موبایل MetaMask سکن نمایید" - }, - "syncWithMobileTitle": { - "message": "همگام سازی با موبایل" - }, "terms": { "message": "شرایط استفاده" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index b43b0bc46..0cea114d6 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -675,12 +675,6 @@ "settings": { "message": "Asetukset" }, - "showAdvancedGasInline": { - "message": "Bensan lisävalvonta" - }, - "showAdvancedGasInlineDescription": { - "message": "Valitse tämä näyttääksesi gas-hinta ja rajoittaaksesi säätimiä suoraan lähetä- ja vahvista-ruuduissa." - }, "showFiatConversionInTestnets": { "message": "Näytä vaihtokurssi koeverkoissa" }, @@ -744,27 +738,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbolin on oltava 11 merkkiä tai vähemmän." }, - "syncWithMobile": { - "message": "Synkronoi mobiililaitteelle" - }, - "syncWithMobileBeCareful": { - "message": "Varmista, ettei kukaan muu katsele näyttöäsi, kun skannaat koodin" - }, - "syncWithMobileComplete": { - "message": "Tietojesi synkronoiminen onnistui. Nauti MetaMask-mobiilisovelluksesta!" - }, - "syncWithMobileDesc": { - "message": "Voit synkronoida tilisi ja tietosi mobiililaitteidesi kesken. Avaa MetaMaskin mobiilisovellus, siirry \"Asetukset\"-osioon ja napauta \"Synkronoi selainlaajennuksesta\" -vaihtoehtoa" - }, - "syncWithMobileDescNewUsers": { - "message": "Jos avaat MetaMaskin mobiilisovelluksen vasta ensimmäistä kertaa, noudata vain puhelimesi ilmoittamia vaiheita." - }, - "syncWithMobileScanThisCode": { - "message": "Lue tämä koodi MetaMask-mobiilisovelluksellasi" - }, - "syncWithMobileTitle": { - "message": "Synkronoi mobiililaitteen kanssa" - }, "terms": { "message": "Käyttöehdot" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 158a9b6dc..3f7d43347 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -602,12 +602,6 @@ "settings": { "message": "Mga Setting" }, - "showAdvancedGasInline": { - "message": "Mga advanced na kontrol sa gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Piliin ito para ipakita ang presyo ng gas at limitahan ang mga kontrol nang direkta sa screen ng pagpapadala at pagkumpirma." - }, "showFiatConversionInTestnets": { "message": "Ipakita ang Conversion sa mga Testnet" }, @@ -665,27 +659,6 @@ "symbolBetweenZeroTwelve": { "message": "Ang simbolo ay dapat na 11 character o mas kaunti." }, - "syncWithMobile": { - "message": "I-sync sa mobile" - }, - "syncWithMobileBeCareful": { - "message": "Tiyaking walang ibang taong tumitingin sa iyong screen kapag sina-scan mo ang code na ito" - }, - "syncWithMobileComplete": { - "message": "Matagumpay na na-sync ang iyong data. I-enjoy ang MetaMask mobile app!" - }, - "syncWithMobileDesc": { - "message": "Maaari mong i-sync ang iyong mga account at impormasyon sa iyong mobile device. Buksan ang MetaMask mobile app, pumunta sa \"Settings\" at mag-tap sa \"Sync from Browser Extension\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Kung bubuksan mo ang MetaMask Mobile app sa unang pagkakataon, sundin lang ang mga hakbang sa iyong telepono." - }, - "syncWithMobileScanThisCode": { - "message": "I-scan ang code na ito sa iyong MetaMask mobile app" - }, - "syncWithMobileTitle": { - "message": "I-sync sa mobile" - }, "terms": { "message": "Mga Tuntunin ng Paggamit" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 90a701e7a..b0240b965 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Afficher sur Etherscan" }, - "expandExperience": { - "message": "Développez votre expérience web3 avec les Snaps MetaMask" - }, "expandView": { "message": "Agrandir la vue" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Données malformées" }, - "manageSnaps": { - "message": "Gérez vos Snaps installés" - }, "max": { "message": "Max." }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Vous ne voyez pas votre jeton ?" }, - "mobileSyncWarning": { - "message": "La fonction « Synchronisation avec l’extension » est temporairement désactivée. Si vous souhaitez utiliser votre portefeuille d’extension sur MetaMask mobile : sur votre application mobile, revenez aux options de configuration du portefeuille et sélectionnez l’option « Importation avec la phrase secrète de récupération ». Utilisez la phrase secrète de votre portefeuille d’extension pour importer celui-ci sur votre mobile." - }, "moreComingSoon": { "message": "D’autres à venir..." }, @@ -2188,9 +2179,6 @@ "message": "Le nonce est supérieur au nonce suggéré de $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "Ce NFT ne peut pas être ajouté, car les informations de propriété ne correspondent pas. Vérifiez que votre saisie est correcte." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Afficher" }, - "showAdvancedGasInline": { - "message": "Contrôles de carburant avancés" - }, - "showAdvancedGasInlineDescription": { - "message": "Sélectionnez cette option pour afficher le prix du carburant et les contrôles des limites directement sur les écrans d’envoi et de confirmation." - }, "showFiatConversionInTestnets": { "message": "Afficher la conversion sur les testnets" }, @@ -3358,23 +3340,12 @@ "message": "Vous autorisez $2 à accéder à la clé du snap « $1 ». Cette action est irréversible et accorde à « $1 » le contrôle de vos comptes et actifs $2. Assurez-vous que vous faites confiance à « $1 » avant de continuer.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Ce snap demande les autorisations suivantes :" - }, "snapUpdate": { "message": "Mettre à jour Snap" }, - "snapUpdateExplanation": { - "message": "$1 a besoin d’une version plus récente de votre snap.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snaps" }, - "snapsInsightError": { - "message": "Une erreur s’est produite avec $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Chargement de l’aperçu de transaction…" }, @@ -3391,7 +3362,8 @@ "message": "Un snap ne s’exécute que s’il est activé" }, "snapsUIError": { - "message": "L’interface utilisateur (IU) spécifiée par le snap n’est pas valide." + "message": "L’interface utilisateur (IU) spécifiée par le snap n’est pas valide.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Certains réseaux peuvent présenter des risques pour la sécurité et/ou la vie privée. Informez-vous sur les risques avant d’ajouter et d’utiliser un réseau." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Le symbole doit comporter 11 caractères ou moins." }, - "syncFailed": { - "message": "Échec de la synchronisation" - }, - "syncInProgress": { - "message": "Synchronisation en cours" - }, - "syncWithMobile": { - "message": "Synchroniser avec le mobile" - }, - "syncWithMobileBeCareful": { - "message": "Assurez-vous que personne d’autre ne regarde votre écran lorsque vous scannez ce code." - }, - "syncWithMobileComplete": { - "message": "Vos données ont été synchronisées avec succès. Profitez de l’application mobile MetaMask !" - }, - "syncWithMobileDesc": { - "message": "Vous pouvez synchroniser vos comptes et vos informations avec votre appareil mobile. Ouvrez l’application mobile MetaMask, allez dans « Paramètres » et appuyez sur « Synchroniser depuis l’extension de navigateur »" - }, - "syncWithMobileDescNewUsers": { - "message": "Si vous ouvrez l’application MetaMask Mobile pour la première fois, suivez simplement les étapes dans votre téléphone." - }, - "syncWithMobileScanThisCode": { - "message": "Scannez ce code avec votre application mobile MetaMask" - }, - "syncWithMobileTitle": { - "message": "Synchroniser avec le mobile" - }, "tenPercentIncreased": { "message": "Augmentation de 10 %" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index ac0251010..4e3ab6b64 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -675,12 +675,6 @@ "settings": { "message": "הגדרות" }, - "showAdvancedGasInline": { - "message": "אמצעי שליטה מתקדמים בדלק" - }, - "showAdvancedGasInlineDescription": { - "message": "בחר/י באפשרות זו כדי להציג אמצעי שליטה במחיר הדלק וההגבלה (limit) ישירות במסכי השליחה והאישור." - }, "showFiatConversionInTestnets": { "message": "הצג המרה -Testnets" }, @@ -744,27 +738,6 @@ "symbolBetweenZeroTwelve": { "message": "הסמל חייב להיות 11 תווים או פחות." }, - "syncWithMobile": { - "message": "סנכרן עם הנייד" - }, - "syncWithMobileBeCareful": { - "message": "ודא/י כי איש אינו מסתכל על המסך שלך בזמן סריקת קוד זה" - }, - "syncWithMobileComplete": { - "message": "הנתונים שלך סונכרנו בהצלחה. תיהנה/י מאפליקציית MetaMask לטלפונים ניידים! " - }, - "syncWithMobileDesc": { - "message": "באפשרותך לסנכרן את החשבונות והמידע שלך עם המכשיר הנייד שלך. יש לפתוח את האפליקציה לנייד של MetaMask, לעבור אל \"הגדרות\" וללחוץ על \"סנכרון מהרחבה לדפדפן\"" - }, - "syncWithMobileDescNewUsers": { - "message": "אם את/ה פותח/ת את אפליקציית MetaMask Mobile בפעם הראשונה, פשוט בצע/י את השלבים בטלפון שלך." - }, - "syncWithMobileScanThisCode": { - "message": "יש לסרוק קוד QR זה באמצעות אפליקציית MetaMask לטלפון נייד" - }, - "syncWithMobileTitle": { - "message": "סנכרן עם הנייד" - }, "terms": { "message": "תנאי שימוש" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 500bc48fc..597828e1a 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Etherscan पर देखें" }, - "expandExperience": { - "message": "MetaMask स्नैप्स के साथ अपने web3 अनुभव का विस्तार करें" - }, "expandView": { "message": "दृश्य का विस्तार करें" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "विकृत डेटा" }, - "manageSnaps": { - "message": "अपने इंस्टाल किए गए स्नैप्स मैनेज करें" - }, "max": { "message": "अधिकतम" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "क्या अपना टोकन नहीं देख रहे हैं?" }, - "mobileSyncWarning": { - "message": "'एक्सटेंशन के साथ सिंक' फीचर अस्थायी रूप से अक्षम है। यदि आप MetaMask मोबाइल पर अपने एक्सटेंशन वॉलेट का उपयोग करना चाहते हैं, तो अपने मोबाइल ऐप पर: वॉलेट सेटअप ऑप्शन पर वापस जाएं और 'सीक्रेट रिकवरी फ्रेज के साथ इम्पोर्ट करें' विकल्प चुनें। फिर अपने वॉलेट को मोबाइल में इम्पोर्ट करने के लिए अपने एक्सटेंशन वॉलेट के सीक्रेट फ्रेज का उपयोग करें।" - }, "moreComingSoon": { "message": "और अधिक जल्द ही आ रहा..." }, @@ -2188,9 +2179,6 @@ "message": "नॉन्स $1 के सुझाए गए नॉन्स से अधिक है", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "एनएफटी" - }, "nftAddFailedMessage": { "message": "एनएफटी जोड़ा नहीं जा सकता क्योंकि स्वामित्व विवरण मेल नहीं खा रहे हैं। सुनिश्चित करें कि आपने सही जानकारी दर्ज की है।" }, @@ -3253,12 +3241,6 @@ "show": { "message": "दिखाएं" }, - "showAdvancedGasInline": { - "message": "उन्नत गैस नियंत्रण" - }, - "showAdvancedGasInlineDescription": { - "message": "गैस मूल्य और सीमा नियंत्रण को सीधे भेजने और पुष्टि करने की स्क्रीन पर दिखाने के लिए इसका चयन करें।" - }, "showFiatConversionInTestnets": { "message": "टेस्ट नेटवर्क पर रूपांतरण दिखाएं" }, @@ -3358,23 +3340,12 @@ "message": "आप स्नैप \"$1\" के लिए $2 कुंजी का एक्सेस प्रदान कर रहे हैं। यह अपरिवर्तनीय है और आपके $2 खातों और संपत्तियों पर \"$1\" नियंत्रण प्रदान करता है। आगे बढ़ने से पहले सुनिश्चित करें कि आप \"$1\" पर भरोसा करते हैं।", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "ये स्नैप निम्नलिखित अनुमतियों हेतु अनुरोध कर रहा है:" - }, "snapUpdate": { "message": "स्नैप अपडेट करें" }, - "snapUpdateExplanation": { - "message": "$1 को आपके स्नैप के नए वर्जन की जरूरत है।", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "स्नैप्स" }, - "snapsInsightError": { - "message": "$1: $2 के साथ त्रुटि हुई", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "ट्रांजैक्शन इनसाइट लोड हो रही है..." }, @@ -3391,7 +3362,8 @@ "message": "कोई स्नैप तभी चलेगा जब उसे सक्षम किया गया हो" }, "snapsUIError": { - "message": "स्नैप द्वारा विनिर्दिष्टत UI अमान्य है।" + "message": "स्नैप द्वारा विनिर्दिष्टत UI अमान्य है।", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "कुछ नेटवर्क सुरक्षा और/या गोपनीयता संबंधी जोखिम पैदा कर सकते हैं। नेटवर्क जोड़ने और उपयोग करने से पहले जोखिमों को समझें।" @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "प्रतीक 11 वर्ण या उससे कम का होना चाहिए।" }, - "syncFailed": { - "message": "सिंक विफल" - }, - "syncInProgress": { - "message": "सिंक प्रगति पर है" - }, - "syncWithMobile": { - "message": "मोबाइल के साथ सिंक करें" - }, - "syncWithMobileBeCareful": { - "message": "सुनिश्चित करें कि जब आप इस कोड को स्कैन कर रहे हों, तो आपकी स्क्रीन को कोई और न देख रहा हो" - }, - "syncWithMobileComplete": { - "message": "आपका डेटा सफलतापूर्वक सिंक कर लिया गया है। MetaMask मोबाइल ऐप का आनंद लें!" - }, - "syncWithMobileDesc": { - "message": "आप अपने खाते और जानकारी को अपने मोबाइल डिवाइस के साथ सिंक कर सकते हैं। MetaMask मोबाइल ऐप खोलें, \"सेटिंग\" पर जाएं और \"ब्राउजर एक्सटेंशन से सिंक करें\" पर टैप करें" - }, - "syncWithMobileDescNewUsers": { - "message": "यदि आप पहली बार MetaMask मोबाइल ऐप खोलते हैं, तो बस अपने फोन में दिए गए चरणों का पालन करें।" - }, - "syncWithMobileScanThisCode": { - "message": "इस कोड को अपने MetaMask मोबाइल ऐप से स्कैन करें" - }, - "syncWithMobileTitle": { - "message": "मोबाइल के साथ सिंक करें" - }, "tenPercentIncreased": { "message": "10% बढ़ोत्तरी" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index decdfbb9c..72c63d315 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -671,12 +671,6 @@ "settings": { "message": "Postavke" }, - "showAdvancedGasInline": { - "message": "Napredno upravljanje gorivom" - }, - "showAdvancedGasInlineDescription": { - "message": "Odaberite ovu stavku za prikaz cijene goriva i izravno ograničite kontrole prilikom slanja i potvrđivanja zaslona." - }, "showFiatConversionInTestnets": { "message": "Prikaži konverziju na usluzi Testnets" }, @@ -740,27 +734,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbol mora biti 11 znakova ili manje." }, - "syncWithMobile": { - "message": "Sinkroniziraj s mobilnim telefonom" - }, - "syncWithMobileBeCareful": { - "message": "Pazite da nitko ne gleda u vaš zaslon dok skenirate ovaj kôd" - }, - "syncWithMobileComplete": { - "message": "Vaši su podatci uspješno sinkronizirani. Uživajte u mobilnoj aplikaciji MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Možete sinkronizirati svoje račune i informacije s vašim mobilnim telefonom. Otvorite mobilnu aplikaciju MetaMask, idite u stavku „Postavke” i dodirnite „Sinkroniziraj iz dodatka preglednika”" - }, - "syncWithMobileDescNewUsers": { - "message": "Kad otvorite mobilnu aplikaciju MetaMask po prvi puta, pridržavajte se koraka koji se prikazuju na telefonu." - }, - "syncWithMobileScanThisCode": { - "message": "Skenirajte ovaj kôd uporabom mobilne aplikacije MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sinkroniziraj s mobilnim telefonom" - }, "terms": { "message": "Odredbe uporabe" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 340e7e7c1..51270215d 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -671,12 +671,6 @@ "settings": { "message": "Beállítások" }, - "showAdvancedGasInline": { - "message": "Speciális gázszabályzók" - }, - "showAdvancedGasInlineDescription": { - "message": "Jelöld meg ezt a gázárak és korlátozásellenőrzés mutatásához közvetlenül a küldési és megerősítési képernyőkön." - }, "showFiatConversionInTestnets": { "message": "Konverzió mutatása Testnetsen" }, @@ -740,27 +734,6 @@ "symbolBetweenZeroTwelve": { "message": "A szimbólum 0 és 12 karakter között kell legyen." }, - "syncWithMobile": { - "message": "Szinkronizálás telefonnal" - }, - "syncWithMobileBeCareful": { - "message": "Győződjön meg arról, hogy senki nem látja a képernyőt, amikor beolvassa ezt a kódot" - }, - "syncWithMobileComplete": { - "message": "Adatai szinkronizálása sikerült. Élvezze a MetaMask mobilalkalmazást!" - }, - "syncWithMobileDesc": { - "message": "Szinkronizálhatja fiókjait és adatait a mobilkészülékkel. Nyissa meg a MetaMask mobilalkalmazást, lépjen a \"Beállítások\" elemre, majd koppintson a \"Szinkronizálás a böngésző bővítményéből\" elemre." - }, - "syncWithMobileDescNewUsers": { - "message": "Ha most először nyitja meg a MetaMask mobilalkalmazást, kövesse a telefonon megadott lépéseket." - }, - "syncWithMobileScanThisCode": { - "message": "Olvasd be ezt a kódot MetaMask mobilalkalmazásoddal" - }, - "syncWithMobileTitle": { - "message": "Szinkronizálás mobillal" - }, "terms": { "message": "Használati feltételek" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 9a6997cab..c3d74b4ec 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Lihat di Etherscan" }, - "expandExperience": { - "message": "Perluas pengalaman web3 Anda dengan MetaMask Snaps" - }, "expandView": { "message": "Perluas tampilan" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Format data salah" }, - "manageSnaps": { - "message": "Kelola Snap yang Anda instal" - }, "max": { "message": "Maks" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Tidak melihat token Anda?" }, - "mobileSyncWarning": { - "message": "Fitur 'Sinkronkan dengan ekstensi' dinonaktifkan untuk sementara waktu. Jika Anda ingin menggunakan dompet ekstensi Anda di ponsel MetaMask, maka pada aplikasi seluler Anda: kembali ke opsi pengaturan dompet dan pilih opsi 'Impor dengan Frasa Pemulihan Rahasia'. Gunakan frasa rahasia dompet ekstensi Anda untuk mengimpor dompet Anda ke ponsel nantinya." - }, "moreComingSoon": { "message": "Selanjutnya akan segera hadir..." }, @@ -2188,9 +2179,6 @@ "message": "Nonce lebih tinggi dari nonce $1 yang disarankan", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "NFT tidak dapat ditambahkan karena detail kepemilikan tidak cocok. Pastikan Anda telah memasukkan informasi yang benar." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Tampil" }, - "showAdvancedGasInline": { - "message": "Kontrol gas lanjutan" - }, - "showAdvancedGasInlineDescription": { - "message": "Pilih ini untuk menampilkan biaya gas dan kontrol batas secara langsung di layar kirim dan konfirmasi." - }, "showFiatConversionInTestnets": { "message": "Tampilkan konversi di jaringan uji" }, @@ -3358,23 +3340,12 @@ "message": "Anda memberikan $2 akses kunci ke snap \"$1\". Tindakan ini tidak dapat dibatalkan dan memberikan kendali \"$1\" atas akun dan aset $2 Anda. Sebelum melanjutkan, pastikan \"$1\" aman.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Snap ini meminta izin berikut:" - }, "snapUpdate": { "message": "Perbarui Snap" }, - "snapUpdateExplanation": { - "message": "$1 memerlukan versi snap yang lebih baru.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snap" }, - "snapsInsightError": { - "message": "Terjadi kesalahan dengan $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Memuat wawasan transaksi..." }, @@ -3391,7 +3362,8 @@ "message": "Snap hanya akan beroperasi jika diaktifkan" }, "snapsUIError": { - "message": "UI yang ditentukan oleh snap tidak valid." + "message": "UI yang ditentukan oleh snap tidak valid.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Beberapa jaringan dapat menimbulkan risiko keamanan dan/atau privasi. Pahami risikonya sebelum menambahkan & menggunakan jaringan." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbol harus terdiri dari 11 karakter atau kurang." }, - "syncFailed": { - "message": "Sinkronisasi gagal" - }, - "syncInProgress": { - "message": "Sinkronisasi sedang berlangsung" - }, - "syncWithMobile": { - "message": "Sinkronkan dengan seluler" - }, - "syncWithMobileBeCareful": { - "message": "Pastikan tidak ada orang lain yang melihat layar Anda saat memindai kode ini" - }, - "syncWithMobileComplete": { - "message": "Data Anda telah berhasil disinkronkan. Nikmati aplikasi seluler MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Anda dapat menyinkronkan akun dan informasi dengan perangkat seluler Anda. Buka aplikasi seluler MetaMask, buka \"Pengaturan\" dan ketuk \"Sinkronkan dari Ekstensi Peramban\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Jika Anda baru membuka aplikasi seluler MetaMask untuk pertama kali, cukup ikuti langkah-langkah yang ada di ponsel Anda." - }, - "syncWithMobileScanThisCode": { - "message": "Pindai kode ini dengan aplikasi seluler MetaMask Anda" - }, - "syncWithMobileTitle": { - "message": "Sinkronkan dengan seluler" - }, "tenPercentIncreased": { "message": "Meningkat 10%" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 8e6e39683..6ed51025d 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1470,12 +1470,6 @@ "settings": { "message": "Impostazioni" }, - "showAdvancedGasInline": { - "message": "Controlli gas avanzati" - }, - "showAdvancedGasInlineDescription": { - "message": "Seleziona per visualizzare i controlli su prezzo e limite del gas nelle schermate di invio e conferma." - }, "showFiatConversionInTestnets": { "message": "Mostra conversione nelle reti di test" }, @@ -1776,27 +1770,6 @@ "symbolBetweenZeroTwelve": { "message": "Il simbolo deve essere lungo tra 0 e 12 caratteri." }, - "syncWithMobile": { - "message": "Sincronizza con dispositivo mobile" - }, - "syncWithMobileBeCareful": { - "message": "Assicurati che nessun'altro stia guardando al tuo schermo quando scansioni questo codice" - }, - "syncWithMobileComplete": { - "message": "I tuoi dati sono stati sincronizzati con successo. Goditi l'app di MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Puoi sincronizzare i tuoi account e le tue informazioni con il tuo dispositivo mobile. Apri l'app di MetaMask, vai su \"Impostazioni\" e tocca \"Sincronizza da Estensione sul Browser\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Se hai appena aperto l'app di MetaMask per la prima volta, segui i passaggi sul tuo telefono." - }, - "syncWithMobileScanThisCode": { - "message": "Scansiona questo codice con l'app di MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sincronizza con dispositivo mobile" - }, "terms": { "message": "Termini di Uso" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 6e1a716a9..0c9ee72c3 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Etherscanで表示" }, - "expandExperience": { - "message": "MetaMask Snaps で web3 エクスペリエンスを拡張" - }, "expandView": { "message": "ビューを展開" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "不正な形式のデータ" }, - "manageSnaps": { - "message": "インストールされたスナップの管理" - }, "max": { "message": "最大" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "トークンが見当たりませんか?" }, - "mobileSyncWarning": { - "message": "「拡張機能と同期」機能は一時的に無効になっています。拡張ウォレットをMetaMaskモバイルで使用する場合は、モバイルアプリでウォレットの設定オプションに戻り、「シークレットリカバリーフレーズでインポート」オプションを選択します。拡張ウォレットのシークレットフレーズを使用して、ウォレットをモバイルにインポートします。" - }, "moreComingSoon": { "message": "さらに近日追加予定..." }, @@ -2188,9 +2179,6 @@ "message": "ナンスが提案され$1よりも大きいです", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "所有者情報が一致していないため、NFT を追加できません。入力された情報が正しいことを確認してください。" }, @@ -3253,12 +3241,6 @@ "show": { "message": "表示" }, - "showAdvancedGasInline": { - "message": "高度なガスコントロール" - }, - "showAdvancedGasInlineDescription": { - "message": "これを選択すると、ガス代と限度額のコントロールが送金画面と確認画面に直接表示されます。" - }, "showFiatConversionInTestnets": { "message": "テストネット上に変換を表示" }, @@ -3358,23 +3340,12 @@ "message": "スナップ「$1」に $2 へのキーアクセスを許可しようとしています。この操作は取り消し不能であり、$2 アカウントとアセットのコントロールを「$1」に許可することになります。続行する前に、必ず「$1」が信頼できることを確認してください。", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "このスナップが次のパーミッションをリクエストしています:" - }, "snapUpdate": { "message": "スナップを更新" }, - "snapUpdateExplanation": { - "message": "$1 に新しいバージョンのスナップが必要です。", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "スナップ" }, - "snapsInsightError": { - "message": "$1 でエラーが発生しました: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "トランザクションインサイトを読み込み中..." }, @@ -3391,7 +3362,8 @@ "message": "スナップは有効になっている場合にのみ実行されます" }, "snapsUIError": { - "message": "スナップにより指定された UI が無効です。" + "message": "スナップにより指定された UI が無効です。", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "ネットワークによっては、セキュリティやプライバシーの面でリスクが伴う可能性があります。ネットワークを追加・使用する前にリスクを理解するようにしてください。" @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "シンボルは11文字以下にする必要があります。" }, - "syncFailed": { - "message": "同期に失敗しました" - }, - "syncInProgress": { - "message": "同期中" - }, - "syncWithMobile": { - "message": "モバイルと同期" - }, - "syncWithMobileBeCareful": { - "message": "このコードをスキャンするとき、画面を誰にも見られていないことを確認してください" - }, - "syncWithMobileComplete": { - "message": "データの同期に成功しました。MetaMaskモバイルアプリをご活用ください!" - }, - "syncWithMobileDesc": { - "message": "アカウントと情報を、モバイルデバイスと同期させることができます。MetaMaskモバイルアプリを開き、「設定」に進み、「ブラウザ拡張機能から同期」をタップします。" - }, - "syncWithMobileDescNewUsers": { - "message": "MetaMaskモバイルアプリを初めて開く場合は、スマートフォンを以下のステップに従って操作してください。" - }, - "syncWithMobileScanThisCode": { - "message": "MetaMaskモバイルアプリでこのコードをスキャンします" - }, - "syncWithMobileTitle": { - "message": "モバイルと同期" - }, "tenPercentIncreased": { "message": "10% の増加" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index b1ab67888..77e919931 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -678,12 +678,6 @@ "settings": { "message": "ಸೆಟ್ಟಿಂಗ್‌ಗಳು" }, - "showAdvancedGasInline": { - "message": "ಸುಧಾರಿತ ಗ್ಯಾಸ್ ನಿಯಂತ್ರಣಗಳು" - }, - "showAdvancedGasInlineDescription": { - "message": "ಕಳುಹಿಸುವ ಮತ್ತು ಖಚಿತಪಡಿಸುವ ಪರದೆಯ ಮೇಲೆ ನೇರವಾಗಿ ಗ್ಯಾಸ್ ಬೆಲೆ ಮತ್ತು ಮಿತಿಯ ನಿಯಂತ್ರಣಗಳನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ." - }, "showFiatConversionInTestnets": { "message": "Testnets ನಲ್ಲಿ ಪರಿವರ್ತನೆಯನ್ನು ತೋರಿಸಿ" }, @@ -747,27 +741,6 @@ "symbolBetweenZeroTwelve": { "message": "ಚಿಹ್ನೆಯು 0 ಮತ್ತು 12 ಅಕ್ಷರಗಳ ನಡುವೆ ಇರಬೇಕು." }, - "syncWithMobile": { - "message": "ಮೊಬೈಲ್‌ನೊಂದಿಗೆ ಸಿಂಕ್ ಮಾಡಿ" - }, - "syncWithMobileBeCareful": { - "message": "ನೀವು ಈ ಕೋಡ್ ಅನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತಿರುವಾಗ ಯಾರೂ ನಿಮ್ಮ ಪರದೆಯ ಕಡೆಗೆ ನೋಡುತ್ತಿಲ್ಲವೇ ಎಂಬುದನ್ನು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ." - }, - "syncWithMobileComplete": { - "message": "ನಿಮ್ಮ ಡೇಟಾ ಯಶಸ್ವಿಯಾಗಿ ಸಿಂಕ್ ಆಗಿದೆ. MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಆನಂದಿಸಿ!" - }, - "syncWithMobileDesc": { - "message": "ನಿಮ್ಮ ಮೊಬೈಲ್ ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಖಾತೆಗಳು ಮತ್ತು ಮಾಹಿತಿಯನ್ನು ನೀವು ಸಿಂಕ್ ಮಾಡಬಹುದಾಗಿದೆ. MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ತೆರೆಯಿರಿ, \"ಸೆಟ್ಟಿಂಗ್‌ಗಳಿಗೆ\" ಹೋಗಿ ಮತ್ತು \"ಬ್ರೌಸರ್ ವಿಸ್ತರಣೆಯಿಂದ ಸಿಂಕ್ ಮಾಡಿ\" ಟ್ಯಾಪ್ ಮಾಡಿ" - }, - "syncWithMobileDescNewUsers": { - "message": "ನೀವು ಮೊದಲ ಬಾರಿಗೆ MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ತೆರೆದರೆ, ನಿಮ್ಮ ಫೋನ್‌ನಲ್ಲಿರುವ ಹಂತಗಳನ್ನು ಅನುಸರಿಸಿ." - }, - "syncWithMobileScanThisCode": { - "message": "ನಿಮ್ಮ MetaMask ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಮೂಲಕ ಈ ಕೋಡ್ ಅನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿ" - }, - "syncWithMobileTitle": { - "message": "ಮೊಬೈಲ್‌ನೊಂದಿಗೆ ಸಿಂಕ್ ಮಾಡಿ" - }, "terms": { "message": "ಬಳಕೆಯ ನಿಯಮಗಳು" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f46b0ff36..d54e6be44 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Etherscan에서 보기" }, - "expandExperience": { - "message": "MetaMask 스냅으로 web3 경험을 확대하세요" - }, "expandView": { "message": "보기 확장" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "잘못된 데이터" }, - "manageSnaps": { - "message": "설치된 스냅을 관리하세요" - }, "max": { "message": "최대" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "토큰이 보이지 않나요?" }, - "mobileSyncWarning": { - "message": "'확장 프로그램과 동기화' 기능이 일시적으로 비활성화됩니다. MetaMask 모바일에서 확장 지갑을 사용하려면 모바일 앱에서 지갑 설정 옵션으로 돌아가 '비밀 복구 구문 가져오기' 옵션을 선택하세요. 확장 지갑의 비밀 구문을 사용하시면 지갑을 모바일로 가져올 수 있습니다." - }, "moreComingSoon": { "message": "더 추가 예정..." }, @@ -2188,9 +2179,6 @@ "message": "임시값이 권장 임시값인 $1보다 큽니다.", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT\n" - }, "nftAddFailedMessage": { "message": "소유권 정보가 일치하지 않아 NFT를 추가할 수 없습니다. 올바른 정보를 입력했는지 확인하세요." }, @@ -3253,12 +3241,6 @@ "show": { "message": "보기" }, - "showAdvancedGasInline": { - "message": "고급 가스 제어 기능" - }, - "showAdvancedGasInlineDescription": { - "message": "이 항목을 선택하면 보내기 및 확인 화면에서 바로 가스 가격과 한도 조절을 확인할 수 있습니다." - }, "showFiatConversionInTestnets": { "message": "테스트넷에 전환 표시" }, @@ -3358,23 +3340,12 @@ "message": "'$1' 스냅 이용에 필요한 $2 키 액세스 권한을 부여하고 있습니다. 이 작업은 사용자의 $2 계정과 자산에 '$1' 제어 권한을 부여하며 취소가 불가능합니다. '$1의 신뢰성을 확인한 후에 진행하세요.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "이 스냅이 다음 권한을 요청하고 있습니다." - }, "snapUpdate": { "message": "스냅 업데이트" }, - "snapUpdateExplanation": { - "message": "$1에는 스냅의 새 버전이 필요합니다", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "스냅" }, - "snapsInsightError": { - "message": "$1 관련 오류 발생: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "거래 인사이트를 가져오는 중..." }, @@ -3391,7 +3362,8 @@ "message": "스냅은 활성화된 상태에서만 작동합니다." }, "snapsUIError": { - "message": "스냅에서 지정한 UI가 올바르지 않습니다." + "message": "스냅에서 지정한 UI가 올바르지 않습니다.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "네트워크에 따라 보안이나 개인 정보 유출의 위험이 있을 수 있습니다. 네트워크 추가 및 사용 이전에 위험 요소를 파악하세요." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "기호는 11자 이하여야 합니다." }, - "syncFailed": { - "message": "동기화 실패" - }, - "syncInProgress": { - "message": "동기화 진행 중" - }, - "syncWithMobile": { - "message": "모바일과 동기화" - }, - "syncWithMobileBeCareful": { - "message": "이 코드를 스캔할 때는 다른 사람이 화면을 보지 못하게 하세요" - }, - "syncWithMobileComplete": { - "message": "데이터가 동기화되었습니다. MetaMask 모바일 앱을 마음껏 이용하세요!" - }, - "syncWithMobileDesc": { - "message": "계정과 정보를 모바일 장치와 동기화할 수 있습니다. MetaMask 모바일 앱을 열고 \"설정\"으로 이동하여 \"브라우저 확장에서 동기화\"를 탭합니다." - }, - "syncWithMobileDescNewUsers": { - "message": "MetaMask 모바일 앱을 처음 여는 경우라면 휴대폰에 나타나는 지시사항을 따르세요." - }, - "syncWithMobileScanThisCode": { - "message": "MetaMask 모바일 앱으로 이 코드를 스캔하세요" - }, - "syncWithMobileTitle": { - "message": "모바일과 동기화" - }, "tenPercentIncreased": { "message": "10% 인상" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index d39141fac..3b0831431 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -678,12 +678,6 @@ "settings": { "message": "Nustatymai" }, - "showAdvancedGasInline": { - "message": "Išplėstiniai dujų valdikliai" - }, - "showAdvancedGasInlineDescription": { - "message": "Pasirinkite tai, kad būtų rodoma dujų kaina, ir ribokite valdymo elementus tiesiogiai siuntimo ir patvirtinimo ekranuose." - }, "showFiatConversionInTestnets": { "message": "Rodyti keitimą „Testnet“" }, @@ -747,27 +741,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbolis turi būti ne ilgesnis nei 11 simbolių." }, - "syncWithMobile": { - "message": "Sinchronizuoti su mobiliuoju" - }, - "syncWithMobileBeCareful": { - "message": "Pasirūpinkite, kad jums nuskaitant šį kodą niekas nežiūrėtų į jūsų ekraną." - }, - "syncWithMobileComplete": { - "message": "Jūsų duomenys sėkmingai sinchronizuoti. Mėgaukitės „MetaMask“ mobiliąja programa! " - }, - "syncWithMobileDesc": { - "message": "Galite sinchronizuoti paskyrą ir informaciją su savo mobiliuoju įrenginiu. Atverkite „MetaMask“ mobiliąją programą, eikite į „Nuostatos“ ir palieskite „Sinchronizuoti iš naršyklės plėtinio“" - }, - "syncWithMobileDescNewUsers": { - "message": "Jeigu „MetaMask“ mobiliąją programą atveriate tik pirmą kartą, tiesiog sekite veiksmus telefone." - }, - "syncWithMobileScanThisCode": { - "message": "Nuskaitykite šį kodą su savo „MetaMask“ mobiliąja programa" - }, - "syncWithMobileTitle": { - "message": "Sinchronizuoti su mobiliuoju" - }, "terms": { "message": "Naudojimo sąlygos" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index d174f8442..2ffc66e5c 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -674,12 +674,6 @@ "settings": { "message": "Iestatījumi" }, - "showAdvancedGasInline": { - "message": "Papildu Gas vadīklas" - }, - "showAdvancedGasInlineDescription": { - "message": "Atlasiet šo, lai parādītu Gas cenu un ierobežotu kontroles iespējas tieši sūtīšanas un apstiprināšanas ekrānos." - }, "showFiatConversionInTestnets": { "message": "Rādīt konversiju testa tīklos" }, @@ -743,27 +737,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbolā nedrīkst būt vairāk par 11 rakstzīmēm." }, - "syncWithMobile": { - "message": "Sinhronizēt ar tālruni" - }, - "syncWithMobileBeCareful": { - "message": "Pārliecinieties, ka neviens cits neskatās jūsu ekrānā, kad skenējat šo kodu." - }, - "syncWithMobileComplete": { - "message": "Jūsu dati sekmīgi sinhronizēti. Patīkamu MetaMask mobilās lietotnes lietošanu!" - }, - "syncWithMobileDesc": { - "message": "Jūs varat sinhronizēt savus kontus un informāciju ar mobilo ierīci. Atveriet MetaMask mobilo lietotni, ejiet uz \"Iestatījumi\" un pieskarieties pie \"Sinhronizēt no pārlūka paplašinājuma\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Ja esat pirmoreiz atvēris MetaMask mobilo lietotni, vienkārši sekojiet norādēm tālrunī." - }, - "syncWithMobileScanThisCode": { - "message": "Noskenējiet šo kodu ar MetaMask mobilo lietotni" - }, - "syncWithMobileTitle": { - "message": "Sinhronizēt ar tālruni" - }, "terms": { "message": "Lietošanas noteikumi" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 5dd5a8216..5c304cd47 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -658,12 +658,6 @@ "settings": { "message": "Tetapan" }, - "showAdvancedGasInline": { - "message": "Kawalan gas lanjutan" - }, - "showAdvancedGasInlineDescription": { - "message": "Pilih ini untuk menunjukkan harga gas dan kawalan had terus di skrin hantar dan sahkan." - }, "showFiatConversionInTestnets": { "message": "Tunjukkan Penukaran di Testnets" }, @@ -727,27 +721,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbol mestilah 11 aksara atau kurang." }, - "syncWithMobile": { - "message": "Segerakkan dengan telefon mudah alih" - }, - "syncWithMobileBeCareful": { - "message": "Pastikan tiada orang lain melihat skrin anda ketika anda mengimbas kod ini" - }, - "syncWithMobileComplete": { - "message": "Data anda berjaya disegerakkan. Nikmati aplikasi mudah alih MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Anda boleh menyegerakkan akaun dan maklumat anda dengan peranti mudah alih anda. Buka ap mudah alih MetaMask, pergi ke \"Tetapan\" dan ketik \"Segerakkan daripada Sambungan Pelayar\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Jika ini kali pertama anda membuka aplikasi mudah alih MetaMask, anda cuma perlu ikuti langkah-langkah di telefon anda." - }, - "syncWithMobileScanThisCode": { - "message": "Imbas kod ini dengan aplikasi mudah alih MetaMask anda" - }, - "syncWithMobileTitle": { - "message": "Segerakkan dengan mudah alih" - }, "terms": { "message": "Syarat-syarat Penggunaan" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 5e904cf86..2df0252ef 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -659,12 +659,6 @@ "settings": { "message": "Innstillinger" }, - "showAdvancedGasInline": { - "message": "Avanserte datakraftskontroller" - }, - "showAdvancedGasInlineDescription": { - "message": "Velg dette for å vise bensinpris og begrensningskontroller direkte på send- og bekreftskjermbildene." - }, "showFiatConversionInTestnets": { "message": "Vis konvertering på Testnets " }, @@ -725,27 +719,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbolet må være 11 tegn eller færre." }, - "syncWithMobile": { - "message": "Synkroniser med mobil " - }, - "syncWithMobileBeCareful": { - "message": "Pass på at ingen andre ser på skjermen din mens du skanner denne koden " - }, - "syncWithMobileComplete": { - "message": "Dataene dine er blitt synkronisert med suksess. Kos deg med mobilappen for MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Du kan synkronisere kontoene og informasjonen din med den mobile enheten din. Åpne mobilappen for MetaMask, gå til \"Innstillinger\" og trykk på \"Synkronisering fra nettleserutvidelse\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Hvis du åpner mobilappen for MetaMask for første gang, følger du bare trinnene på telefonen." - }, - "syncWithMobileScanThisCode": { - "message": "Skann denne koden med din mobilapp for MetaMask" - }, - "syncWithMobileTitle": { - "message": "Synkronisér med mobil" - }, "terms": { "message": "Brukervilkår" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 48f21c3c4..ca47dc905 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1320,12 +1320,6 @@ "settings": { "message": "Mga Setting" }, - "showAdvancedGasInline": { - "message": "Mga advanced na kontrol sa gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma." - }, "showFiatConversionInTestnets": { "message": "Ipakita ang Conversion sa Testnets" }, @@ -1717,27 +1711,6 @@ "symbolBetweenZeroTwelve": { "message": "Dapat ay 11 character o mas kaunti ang simbolo." }, - "syncWithMobile": { - "message": "I-sync sa mobile" - }, - "syncWithMobileBeCareful": { - "message": "Tiyaking walang ibang nakakakita sa iyong screen kapag na-scan mo ang code na ito" - }, - "syncWithMobileComplete": { - "message": "Matagumpay na na-sync ang iyong data. I-enjoy ang MetaMask mobile app!" - }, - "syncWithMobileDesc": { - "message": "Puwede mong i-sync ang iyong mga account at impormasyon sa mobile device mo. Buksan ang MetaMask mobile app, pumunta sa \"Mga Setting\" at mag-tap sa \"I-sync mula sa Browser Extension\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Kung unang pagkakataon mong bubuksan ang MetaMask Mobile app, sundin lang ang mga hakbang sa iyong telepono." - }, - "syncWithMobileScanThisCode": { - "message": "I-scan ang code na ito gamit ang iyong MetaMask mobile app" - }, - "syncWithMobileTitle": { - "message": "I-sync sa mobile" - }, "terms": { "message": "Mga Tuntunin ng Paggamit" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 531164a02..348798703 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -672,12 +672,6 @@ "settings": { "message": "Ustawienia" }, - "showAdvancedGasInline": { - "message": "Zaawansowana kontrola gazu" - }, - "showAdvancedGasInlineDescription": { - "message": "Wybierz tę opcję, aby móc zmienić cenę i limit gazu bezpośrednio na ekranach wysyłania i potwierdzania." - }, "showFiatConversionInTestnets": { "message": "Pokaż przeliczanie w sieciach testowych" }, @@ -738,27 +732,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbol musi mieć maksymalnie 11 znaków." }, - "syncWithMobile": { - "message": "Synchronizuj z telefonem" - }, - "syncWithMobileBeCareful": { - "message": "Upewnij się, że nikt inny nie patrzy na Twój ekran podczas skanowania tego kodu" - }, - "syncWithMobileComplete": { - "message": "Twoje dane zostały zsynchronizowane. Miłego korzystania z aplikacji mobilnej MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Możesz synchronizować swoje konta i informacje z urządzeniem mobilnym. Otwórz aplikację mobilną MetaMask, przejdź do „Ustawień” i wybierz opcję „Synchronizuj z rozszerzeniem przeglądarki”." - }, - "syncWithMobileDescNewUsers": { - "message": "Jeśli po raz pierwszy otwierasz aplikację MetaMask Mobile, postępuj zgodnie z instrukcjami w telefonie." - }, - "syncWithMobileScanThisCode": { - "message": "Zeskanuj ten kod za pomocą aplikacji mobilnej MetaMask" - }, - "syncWithMobileTitle": { - "message": "Synchronizuj z telefonem" - }, "terms": { "message": "Regulamin" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 5b73c1ab8..c49ec5791 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Ver no Etherscan" }, - "expandExperience": { - "message": "Expanda sua experiência web3 com os snaps da MetaMask" - }, "expandView": { "message": "Expandir exibição" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Dados inválidos" }, - "manageSnaps": { - "message": "Gerencie seus snaps instalados" - }, "max": { "message": "Máximo" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Não está vendo seu token?" }, - "mobileSyncWarning": { - "message": "A funcionalidade \"Sincronizar com a extensão\" está temporariamente desativada. Se você quer usar sua carteira de extensão na MetaMask mobile, então, no seu app mobile: volte às opções de configuração da carteira e selecione a opção \"Importar com frase de recuperação secreta\". Use a frase secreta da sua carteira de extensão para, então, importar a sua carteira no celular." - }, "moreComingSoon": { "message": "Mais em breve..." }, @@ -2188,9 +2179,6 @@ "message": "Nonce é maior que o nonce sugerido de $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "O NFT não pôde ser adicionado, pois os dados de propriedade não coincidem. Certifique-se de ter inserido as informações corretas." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Exibir" }, - "showAdvancedGasInline": { - "message": "Controles avançados de gás" - }, - "showAdvancedGasInlineDescription": { - "message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação." - }, "showFiatConversionInTestnets": { "message": "Mostrar conversão nas redes de teste" }, @@ -3358,23 +3340,12 @@ "message": "Você está concedendo ao snap \"$1\" acesso à sua chave $2. Isso é irrevogável e concede a \"$1\" controle de suas contas e ativos $2. Certifique-se de que confia em \"$1\" antes de prosseguir.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Esse snap está solicitando as seguintes permissões:" - }, "snapUpdate": { "message": "Atualizar snap" }, - "snapUpdateExplanation": { - "message": "$1 precisa de uma versão mais nova do seu snap.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snaps" }, - "snapsInsightError": { - "message": "Ocorreu um erro com $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Carregando insight da transação..." }, @@ -3391,7 +3362,8 @@ "message": "O snap só será executado se estiver ativado" }, "snapsUIError": { - "message": "A IU especificada pelo snap é inválida." + "message": "A IU especificada pelo snap é inválida.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Algumas redes podem representar riscos de segurança e/ou privacidade. Tenha os riscos em mente antes de adicionar e usar uma rede." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "O símbolo deve ter 11 caracteres ou menos." }, - "syncFailed": { - "message": "Falha na sincronização" - }, - "syncInProgress": { - "message": "Sincronização em andamento" - }, - "syncWithMobile": { - "message": "Sincronizar com dispositivo móvel" - }, - "syncWithMobileBeCareful": { - "message": "Ao escanear esse código, verifique se não há mais ninguém olhando para a sua tela" - }, - "syncWithMobileComplete": { - "message": "Seus dados foram sincronizados. Curta o app da MetaMask para dispositivos móveis!" - }, - "syncWithMobileDesc": { - "message": "Você pode sincronizar suas contas e informações com o seu dispositivo móvel. Abra o aplicativo da MetaMask para dispositivos móveis, acesse \"Configurações\" e toque em \"Sincronizar pela extensão do navegador\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Se você tiver acabado de abrir o app da MetaMask para dispositivos móveis pela primeira vez, basta seguir as etapas no seu telefone." - }, - "syncWithMobileScanThisCode": { - "message": "Escaneie esse código com seu app da MetaMask para dispositivos móveis" - }, - "syncWithMobileTitle": { - "message": "Sincronizar com dispositivo móvel" - }, "tenPercentIncreased": { "message": "10% de aumento" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index e051519b8..73ec8f5c3 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1369,9 +1369,6 @@ "missingToken": { "message": "Não está vendo o seu token?" }, - "mobileSyncWarning": { - "message": "A funcionalidade \"Sincronizar com a extensão\" está temporariamente desativada. Se você quer usar sua carteira de extensão na MetaMask mobile, então, no seu app mobile: volte às opções de configuração da carteira e selecione a opção \"Importar com frase de recuperação secreta\". Use a frase secreta da sua carteira de extensão para, então, importar a sua carteira no celular." - }, "mustSelectOne": { "message": "Selecione pelo menos 1 token." }, @@ -2070,12 +2067,6 @@ "show": { "message": "Mostrar" }, - "showAdvancedGasInline": { - "message": "Controles avançados de gás" - }, - "showAdvancedGasInlineDescription": { - "message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação." - }, "showFiatConversionInTestnets": { "message": "Mostrar conversão nas redes de teste" }, @@ -2555,33 +2546,6 @@ "symbolBetweenZeroTwelve": { "message": "O símbolo deve ter até 11 caracteres." }, - "syncFailed": { - "message": "Falha na sincronização" - }, - "syncInProgress": { - "message": "Sincronização em andamento" - }, - "syncWithMobile": { - "message": "Sincronizar com dispositivo móvel" - }, - "syncWithMobileBeCareful": { - "message": "Ao escanear esse código, verifique se não há mais ninguém olhando para a sua tela" - }, - "syncWithMobileComplete": { - "message": "Seus dados foram sincronizados. Curta o app da MetaMask para dispositivos móveis!" - }, - "syncWithMobileDesc": { - "message": "Você pode sincronizar suas contas e informações com o seu dispositivo móvel. Abra o aplicativo da MetaMask para dispositivos móveis, acesse \"Configurações\" e toque em \"Sincronizar pela extensão do navegador\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Se você tiver acabado de abrir o app da MetaMask para dispositivos móveis pela primeira vez, basta seguir as etapas no seu telefone." - }, - "syncWithMobileScanThisCode": { - "message": "Escaneie esse código com seu app da MetaMask para dispositivos móveis" - }, - "syncWithMobileTitle": { - "message": "Sincronizar com dispositivo móvel" - }, "tenPercentIncreased": { "message": "10% de aumento" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 4fc356286..2ecf4a992 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -665,12 +665,6 @@ "settings": { "message": "Setări" }, - "showAdvancedGasInline": { - "message": "Controale avansate pentru gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Selectați aceasta pentru a arăta prețul gasului și comenzile de limitare direct pe ecranele de trimitere și confirmare." - }, "showFiatConversionInTestnets": { "message": "Afișează conversiile pe rețelele de test (testnets)" }, @@ -734,27 +728,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbolul trebuie să fie de 11 caractere sau mai puțin." }, - "syncWithMobile": { - "message": "Sincronizați cu dispozitivul mobil" - }, - "syncWithMobileBeCareful": { - "message": "Asigurați-vă că nimeni altcineva nu poate vedea ecranul dvs. când scanați acest cod" - }, - "syncWithMobileComplete": { - "message": "Datele dvs. au fost sincronizate cu succes. Bucurați-vă de aplicația mobilă MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Vă puteți sincroniza conturile și informațiile cu dispozitivul dvs. mobil. Deschideți aplicația mobilă MetaMask, mergeți la „Setări” și atingeți „Sincronizare de la extensia de browser”" - }, - "syncWithMobileDescNewUsers": { - "message": "Dacă deschideți aplicația pentru mobil MetaMask pentru prima oară, urmați pașii afișați pe telefonul dvs." - }, - "syncWithMobileScanThisCode": { - "message": "Scanați acest cod folosind aplicația dvs. mobilă MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sincronizați cu mobilul" - }, "terms": { "message": "Termeni și condiții" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 37f138a27..059014e4d 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Посмотреть на Etherscan" }, - "expandExperience": { - "message": "Расширьте свои возможности web3 с помощью MetaMask Snaps" - }, "expandView": { "message": "Развернуть представление" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Искаженные данные" }, - "manageSnaps": { - "message": "Управляйте установленными снапами" - }, "max": { "message": "Макс." }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Не видите свой токен?" }, - "mobileSyncWarning": { - "message": "Функция «Синхронизация с расширением» временно отключена. Если вы хотите использовать свой кошелек из расширения браузера в мобильной версии MetaMask, тогда в мобильном приложении вернитесь к параметрам настройки кошелька и выберите параметр «Импортировать с помощью секретной фразы для восстановления». Используйте секретную фразу своего кошелька из расширения, чтобы импортировать кошелек на мобильное устройство." - }, "moreComingSoon": { "message": "Скоро появится больше..." }, @@ -2188,9 +2179,6 @@ "message": "Одноразовый номер больше, чем предложенный одноразовый номер $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "Невозможно добавить NFT, так как сведения о владельце не совпадают. Убедитесь, что вы ввели правильную информацию." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Показать" }, - "showAdvancedGasInline": { - "message": "Расширенное управление газом" - }, - "showAdvancedGasInlineDescription": { - "message": "Выберите это, чтобы отображать цену газа и управление лимитами непосредственно на экранах отправки и подтверждения." - }, "showFiatConversionInTestnets": { "message": "Показывать конвертацию в тестовых сетях" }, @@ -3358,23 +3340,12 @@ "message": "Вы предоставляете ключ доступа $2 к привязке \"$1\". Это действие нельзя отменить, и оно предоставляет \"$1\" управление всеми счетами и активами $2. Перед тем как продолжить, убедитесь, что доверяете \"$1\".", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Этот снап запрашивает следующие разрешения:" - }, "snapUpdate": { "message": "Обновить снап" }, - "snapUpdateExplanation": { - "message": "$1 нужна более новая версия вашей привязки.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Снапы" }, - "snapsInsightError": { - "message": "Произошла ошибка с $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Загрузка аналитики по транзакции..." }, @@ -3391,7 +3362,8 @@ "message": "Снап будет работать только в том случае, если он включен" }, "snapsUIError": { - "message": "Пользовательский интерфейс, указанный привязкой, недействителен." + "message": "Пользовательский интерфейс, указанный привязкой, недействителен.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Некоторые сети могут представлять угрозу безопасности и/или конфиденциальности. Прежде чем добавлять и использовать сеть, ознакомьтесь с рисками." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Символ должен состоять из 11 или менее знаков." }, - "syncFailed": { - "message": "Ошибка синхронизации" - }, - "syncInProgress": { - "message": "Выполняется синхронизация" - }, - "syncWithMobile": { - "message": "Синхронизировать с мобильным устройством" - }, - "syncWithMobileBeCareful": { - "message": "Убедитесь, что никто не смотрит на ваш экран, когда вы сканируете этот код" - }, - "syncWithMobileComplete": { - "message": "Ваши данные успешно синхронизированы. Наслаждайтесь мобильным приложением MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Вы можете синхронизировать свои счета и информацию со своим мобильным устройством. Откройте мобильное приложение MetaMask, перейдите в раздел «Настройки» и нажмите «Синхронизировать из расширения браузера»." - }, - "syncWithMobileDescNewUsers": { - "message": "Если вы открываете приложение MetaMask Mobile в первый раз, просто следуйте инструкциям на телефоне." - }, - "syncWithMobileScanThisCode": { - "message": "Отсканируйте этот код с помощью мобильного приложения MetaMask" - }, - "syncWithMobileTitle": { - "message": "Синхронизировать с мобильным устройством" - }, "tenPercentIncreased": { "message": "Увеличение на 10%" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 7ab10cf1e..6b0057045 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -650,12 +650,6 @@ "settings": { "message": "Nastavení" }, - "showAdvancedGasInline": { - "message": "Pokročilé ovládacie prvky GAS" - }, - "showAdvancedGasInlineDescription": { - "message": "Vyberte túto možnosť vtedy, keď chcete priamo na obrazovke odosielania a potvrdenia zobraziť ceny za GAS a ovládacie prvky limitov." - }, "showFiatConversionInTestnets": { "message": "Zobraziť konverziu na Testnets" }, @@ -716,27 +710,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbol musí být mezi 0 a 12 znaky." }, - "syncWithMobile": { - "message": "Synchronizácia s mobilom" - }, - "syncWithMobileBeCareful": { - "message": "Pri skenovaní tohto kódu sa uistite, že sa nikto iný nedíva na vašu obrazovku" - }, - "syncWithMobileComplete": { - "message": "Vaše údaje boli úspešne synchronizované. Užite si mobilnú aplikáciu MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Svoje účty a informácie môžete synchronizovať so svojim mobilným zariadením. Otvorte mobilnú aplikáciu MetaMask, prejdite na „Nastavenia“ a kliknite na „Synchronizovať z rozšírenia prehliadača“." - }, - "syncWithMobileDescNewUsers": { - "message": "Ak otvoríte mobilnú aplikáciu MetaMask prvýkrát, postupujte podľa pokynov v telefóne." - }, - "syncWithMobileScanThisCode": { - "message": "Naskenujte tento kód pomocou mobilnej aplikácie MetaMask" - }, - "syncWithMobileTitle": { - "message": "Synchronizácia s mobilom" - }, "terms": { "message": "Podmínky použití" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 2c774019e..bbc88645e 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -666,12 +666,6 @@ "settings": { "message": "Nastavitve" }, - "showAdvancedGasInline": { - "message": "Napredno krmiljenje plina" - }, - "showAdvancedGasInlineDescription": { - "message": "Izberite to možnost, če želite prikazati ceno plina in omejiti nadzor neposredno na zaslonih za pošiljanje in potrditev." - }, "showFiatConversionInTestnets": { "message": "Pokažite pretvorbo na testnih omrežjih" }, @@ -735,27 +729,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbol mora biti največ 11 znakov ali manj." }, - "syncWithMobile": { - "message": "Sinhroniziraj z mobilnimi telefonom" - }, - "syncWithMobileBeCareful": { - "message": "Ko skenirate to kodo, naj nihče ne kuka na vaš zaslon" - }, - "syncWithMobileComplete": { - "message": "Vaši podatki so uspešno sinhronizirani. Uživajte v mobilni aplikaciji MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Račune in podatke lahko sinhronizirate s svojo mobilno napravo. Odprite mobilno aplikacijo MetaMask, pojdite na \"Nastavitve\" in tapnite \"Sinhroniziraj z razširitvijo brskalnika\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Če ste sedaj prvič odprli aplikacijo MetaMask Mobile, samo sledite korakom v telefonu." - }, - "syncWithMobileScanThisCode": { - "message": "Skenirajte to kodo z mobilno aplikacijo MetaMask" - }, - "syncWithMobileTitle": { - "message": "Sinhroniziraj z mobilnimi telefonom" - }, "terms": { "message": "Pogoji uporabe" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index c87c6972c..b95229b50 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -669,12 +669,6 @@ "settings": { "message": "Podešavanja" }, - "showAdvancedGasInline": { - "message": "Napredne kontrole gasa" - }, - "showAdvancedGasInlineDescription": { - "message": "Izaberite ovo kako biste prikazali cenu gasa i kontrole limita direktno na ekranima za slanje i potvrđivanje." - }, "showFiatConversionInTestnets": { "message": "Prikažite konverzije na Testnet" }, @@ -738,27 +732,6 @@ "symbolBetweenZeroTwelve": { "message": "Simbol mora biti 11 znakova ili manje." }, - "syncWithMobile": { - "message": "Sinhronizacija sa mobilnim telefonom" - }, - "syncWithMobileBeCareful": { - "message": "Obratite pažnju da niko drugi ne gleda u vaš ekran kad skenirate ovaj kod" - }, - "syncWithMobileComplete": { - "message": "Vaši podaci su uspešno sinhronizovani. Uživajte u mobilnoj aplikaciji MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Možete sinhronizovati vaše naloge i informacije sa svojim mobilnim uređajem. Otvorite mobilnu aplikaciju MetaMask, idite na \"Podešavanja\" i pritisnite \"Sinhronizuj iz ekstenzije za pregledač\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Ako prvi put otvorite aplikaciju MetaMask Mobile, samo sledite korake u svom telefonu." - }, - "syncWithMobileScanThisCode": { - "message": "Skenirajte ovaj kod uz pomoć svoje MetaMask mobilne aplikacije" - }, - "syncWithMobileTitle": { - "message": "Sinhronizujte sa mobilnim uređajem" - }, "terms": { "message": "Uslovi korišćenja" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index f8912daa6..75a70e467 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -662,12 +662,6 @@ "settings": { "message": "Inställningar" }, - "showAdvancedGasInline": { - "message": "Avancerade gaskontroller" - }, - "showAdvancedGasInlineDescription": { - "message": "Välj detta för att visa gas-pris och gränskontroller direkt på skicka och bekräfta-skärmarna." - }, "showFiatConversionInTestnets": { "message": "Visa omvandling på testnätverk" }, @@ -728,27 +722,6 @@ "symbolBetweenZeroTwelve": { "message": "Symbolen måste vara 11 tecken eller färre." }, - "syncWithMobile": { - "message": "Synka med mobil" - }, - "syncWithMobileBeCareful": { - "message": "Försäkra dig om att ingen tittar på din skärm när du skannar denna kod" - }, - "syncWithMobileComplete": { - "message": "Din data har nu synkats. Ha det så kul med MetaMasks mobil-app!" - }, - "syncWithMobileDesc": { - "message": "Du kan nu synka dina konton och din information med din mobil-enhet. Öppna MetaMasks mobil-app, gå till \"Inställningar\" och tryck på \"Synka från webbläsartillägg\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Om du öppnar MetaMask mobilapplikation för första gången behöver du bara följa stegen som visas i telefonen." - }, - "syncWithMobileScanThisCode": { - "message": "Skanna den här koden med din MetaMask mobilapplikation" - }, - "syncWithMobileTitle": { - "message": "Synka med mobil" - }, "terms": { "message": "Användarvillkor" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index c50e6e0bc..5ec90f52f 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -656,12 +656,6 @@ "settings": { "message": "Mipangilio" }, - "showAdvancedGasInline": { - "message": "Udhibiti wa juu wa gesi" - }, - "showAdvancedGasInlineDescription": { - "message": "Chagua hii ili uonyeshe bei ya gesi na punguza vidhibiti moja kwa moja kwenye skrini za tuma na thibitisha." - }, "showFiatConversionInTestnets": { "message": "Onyesha Ubadilishaji kwenye Testnets" }, @@ -725,27 +719,6 @@ "symbolBetweenZeroTwelve": { "message": "Alama lazima iwe na herufi 11 au chache." }, - "syncWithMobile": { - "message": "Oanisha na simu" - }, - "syncWithMobileBeCareful": { - "message": "Hakikisha hakuna mtu mwingine anayeangalia kwenye skrini yako unapokuwa unakagua msimbo huu." - }, - "syncWithMobileComplete": { - "message": "Umefanikiwa kuoanisha data yako. Furahia programu yako ya simu ya MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Unaweza kuoanisha akaunti zako na taarifa kwa kifaa chako cha simu ya mkononi. Fungua programu ya simu ya MetaMask, kisha nenda kwenye \"Mipangilio\" na bofya kwenye \"Oanisha kutoka kwenye Kiendelezi cha Kivinjari\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Ikiwa ndio umefungua tu programu ya simu ya MetaMask kwa mara ya kwanza, fuata hatua kwenye simu yako." - }, - "syncWithMobileScanThisCode": { - "message": "Kagua msimbo huu kwa kutumia programu yako ya simu ya MetaMask" - }, - "syncWithMobileTitle": { - "message": "Oanisha na simu" - }, "terms": { "message": "Masharti ya Matumizi" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 42b71a318..23e312863 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -344,9 +344,6 @@ "settings": { "message": "การตั้งค่า" }, - "showAdvancedGasInline": { - "message": "การควบคุม Gas ขั้นสูง" - }, "showPrivateKeys": { "message": "แสดงคีย์ส่วนตัว" }, @@ -380,9 +377,6 @@ "symbolBetweenZeroTwelve": { "message": "สัญลักษณ์จะต้องมีความยาว 11 ตัวอักษร" }, - "syncWithMobileComplete": { - "message": "ซิงค์ข้อมูลของคุณเรียบร้อยแล้ว ใช้แอพ MetaMask ให้สนุกนะ!" - }, "terms": { "message": "ข้อตกลงในการใช้งาน" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index a6394c2ed..b9a8ec2ef 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Tingnan sa Etherscan" }, - "expandExperience": { - "message": "Palawakin ang iyong karanasan sa web3 gamit ang MetaMask Snaps" - }, "expandView": { "message": "I-expand ang view" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Pangit na datos" }, - "manageSnaps": { - "message": "Pamahalaan ang iyong mga naka-install na snap" - }, "max": { "message": "Max" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Hindi mo ba nakikita ang iyong mga token?" }, - "mobileSyncWarning": { - "message": "Ang feature na 'I-sync gamit ang extension' ay pansamantalang hindi gumagana. Kung gusto mong gamitin ang iyong extension wallet sa MetaMask mobile, pagkatapos ay sa iyong mobile app: bumalik sa mga opsyon sa pag-setup ng wallet at piliin ang opsyong 'Mag-import gamit ang Secret Recovery Phrase'. Gamitin ang lihim na parirala ng iyong extension wallet upang pagkatapos ay i-import ang iyong wallet sa mobile." - }, "moreComingSoon": { "message": "Marami pang parating..." }, @@ -2188,9 +2179,6 @@ "message": "Mas mataas ang noncesa iminumungkahing nonce na $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "Hindi maidaragdag ang NFT dahil hindi tumutugma ang mga detalye ng pagmamay-ari. Siguraduhing tamang impormasyon ang iyong nailagay." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Ipakita" }, - "showAdvancedGasInline": { - "message": "Mga advanced na kontrol sa gas" - }, - "showAdvancedGasInlineDescription": { - "message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma." - }, "showFiatConversionInTestnets": { "message": "Ipakita ang Conversion sa Testnets" }, @@ -3358,23 +3340,12 @@ "message": "Binibigyan mo ang $2 ng key access sa snap na \"$1\". Hindi na ito mababawi at nagbibigay ito sa \"$1\" ng kontrol sa iyong mga $2 account at asset. Tiyaking pinagkakatiwalaan mo ang \"$1\" bago magpatuloy.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Hinihiling ng snap na ito ang mga sumusunod na pahintulot:" - }, "snapUpdate": { "message": "I-update ang snap" }, - "snapUpdateExplanation": { - "message": "Kailangan ng $1 ng bagong bersyon ng iyong snap.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Mga Snap" }, - "snapsInsightError": { - "message": "Nagkaroon ng error sa $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Naglo-load ng insight sa transaksyon..." }, @@ -3391,7 +3362,8 @@ "message": "Tatakbo lamang ang snap kapag pinagana ito" }, "snapsUIError": { - "message": "Ang UI na tinukoy sa pamamagitan ng snap ay hindi wasto." + "message": "Ang UI na tinukoy sa pamamagitan ng snap ay hindi wasto.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Maaaring magdulot ang ilang network ng mga panganib sa seguridad at/o pagkapribado. Unawain ang mga panganib bago idagdag o gamitin ang isang network." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Dapat ay 11 character o mas kaunti ang simbolo." }, - "syncFailed": { - "message": "Bigong ma-sync" - }, - "syncInProgress": { - "message": "Kasalukuyang nagsi-sync" - }, - "syncWithMobile": { - "message": "I-sync sa mobile" - }, - "syncWithMobileBeCareful": { - "message": "Tiyaking walang ibang nakakakita sa iyong screen kapag na-scan mo ang code na ito" - }, - "syncWithMobileComplete": { - "message": "Matagumpay na na-sync ang iyong data. I-enjoy ang MetaMask mobile app!" - }, - "syncWithMobileDesc": { - "message": "Puwede mong i-sync ang iyong mga account at impormasyon sa mobile device mo. Buksan ang MetaMask mobile app, pumunta sa \"Mga Setting\" at i-tap ang \"I-sync mula sa Browser Extension\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Kung unang pagkakataon mong bubuksan ang MetaMask Mobile app, sundin lang ang mga hakbang sa iyong telepono." - }, - "syncWithMobileScanThisCode": { - "message": "I-scan ang code na ito gamit ang iyong MetaMask mobile app" - }, - "syncWithMobileTitle": { - "message": "I-sync sa mobile" - }, "tenPercentIncreased": { "message": "10% na dagdag" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index f8a4b63cd..a757a743f 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Etherscan'de görüntüle" }, - "expandExperience": { - "message": "MetaMask Snap'leri ile web3 deneyimini genişlet" - }, "expandView": { "message": "Görünümü genişlet" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Hatalı biçimlendirilmiş veri" }, - "manageSnaps": { - "message": "Yüklü snap'lerini yönet" - }, "max": { "message": "Maksimum" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Tokeninizi görmüyor musunuz?" }, - "mobileSyncWarning": { - "message": "\"Uzantı ile senkronize et\" özelliği geçici olarak devre dışı bırakılmış. MetaMask mobilde uzantı cüzdanınızı kullanmak istiyorsanız mobil uygulamaya gidin: cüzdan kurulum ayarlarına geri dönün ve \"Gizli Kurtarma İfadesi ile İçe Aktar\" seçeneğini seçin. Ardından cüzdanınızı mobil uygulamada içe aktarmak için uzantı cüzdanınızın gizli ifadesini kullanın." - }, "moreComingSoon": { "message": "Daha fazlası çok yakında..." }, @@ -2188,9 +2179,6 @@ "message": "Geçici anahtar, önerilen $1 geçici anahtarından daha büyük", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "Sahiplik bilgileri eşleşmediği için NFT eklenemiyor. Doğru bilgileri girdiğinizden emin olun." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Göster" }, - "showAdvancedGasInline": { - "message": "Gelişmiş gaz kontrolleri" - }, - "showAdvancedGasInlineDescription": { - "message": "Gaz fiyatı ve limit kontrollerini doğrudan gönder ve onayla ekranlarında göstermek için bunu seçin." - }, "showFiatConversionInTestnets": { "message": "Test ağlarında dönüşümü göster" }, @@ -3358,23 +3340,12 @@ "message": "\"$1\" için $2 anahtar erişimi veriyorsunuz. Bu iptal edilemez ve $2 hesaplarınıza ve varlıklarınıza \"$1\" kontrolü verir. İlerlemeden önce \"$1\" alanına güvendiğinizden emin olun.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Bu ek, aşağıdaki izinleri istiyor:" - }, "snapUpdate": { "message": "Snap'i güncelle" }, - "snapUpdateExplanation": { - "message": "$1 için daha yeni bir snap sürümü gerekli.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snap'ler" }, - "snapsInsightError": { - "message": "$1: $2 ile ilgili bir hata oldu", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "İşlem ayrıntıları yükleniyor..." }, @@ -3391,7 +3362,8 @@ "message": "Bir snap yalnızca etkinleştirilmişse çalışır" }, "snapsUIError": { - "message": "Snap tarafından belirtilen Kullanıcı Arayüzü geçersiz." + "message": "Snap tarafından belirtilen Kullanıcı Arayüzü geçersiz.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Bazı ağlar güvenlik ve/veya gizlilik riskleri teşkil edebilir. Bir ağ eklemeden ve kullanmadan önce riskleri anlayın." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Sembol en fazla 11 karakter olmalıdır." }, - "syncFailed": { - "message": "Senkronizasyon başarısız oldu" - }, - "syncInProgress": { - "message": "Senkronizasyon sürüyor" - }, - "syncWithMobile": { - "message": "Mobil ile senkronize et" - }, - "syncWithMobileBeCareful": { - "message": "Bu kodu tararken hiç kimsenin ekranınıza bakmadığından emin olun" - }, - "syncWithMobileComplete": { - "message": "Verileriniz başarılı bir şekilde senkronize edildi. MetaMask mobil uygulamasının tadını çıkarın!" - }, - "syncWithMobileDesc": { - "message": "Hesaplarınızı ve bilgilerinizi mobil cihazınızla senkronize edebilirsiniz. MetaMask mobil uygulamasını açın, \"Ayarlar\" kısmına gidin ve \"Tarayıcı Uzantısından Senkronize Et\" seçeneğine dokunun" - }, - "syncWithMobileDescNewUsers": { - "message": "Metamask Mobil uygulamasını ilk defa açıyorsanız telefonunuzdaki adımları izleyin." - }, - "syncWithMobileScanThisCode": { - "message": "MetaMask mobil uygulamanızla bu kodu tarayın" - }, - "syncWithMobileTitle": { - "message": "Mobil ile senkronize et" - }, "tenPercentIncreased": { "message": "%10 artış" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 83f6857de..acfaf09ab 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -678,12 +678,6 @@ "settings": { "message": "Налаштування" }, - "showAdvancedGasInline": { - "message": "Розширене керування газом" - }, - "showAdvancedGasInlineDescription": { - "message": "Виберіть цей параметр, щоб відображати регулятори ціни й ліміту газу на екранах надсилання й підтвердження." - }, "showFiatConversionInTestnets": { "message": "Показати бесіду у Testnet" }, @@ -747,27 +741,6 @@ "symbolBetweenZeroTwelve": { "message": "Символ повинен містити 11 символів або менше." }, - "syncWithMobile": { - "message": "Синхронізувати з мобільним пристроєм" - }, - "syncWithMobileBeCareful": { - "message": "Переконайтесь, що ніхто не дивиться на ваш екран, коли скануєте цей код" - }, - "syncWithMobileComplete": { - "message": "Ваші дані були успішно синхронізовані. Насолоджуйтесь мобільним застосунком MetaMask!" - }, - "syncWithMobileDesc": { - "message": "Ви можете синхронізувати ваші облікові записи та інформацію з вашим мобільним пристроєм. Відкрийте мобільний застосунок MetaMask, перейдіть до \"Налаштування\" та клацніть на \"Синхронізувати з розширення браузера\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Якщо ви відкрили мобільний застосунок MetaMask вперше, просто слідуйте крокам у вашому телефоні." - }, - "syncWithMobileScanThisCode": { - "message": "Відскануйте цей код за допомогою мобільної програми MetaMask" - }, - "syncWithMobileTitle": { - "message": "Синхронізувати з мобільним" - }, "terms": { "message": "Умови використання" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index a3acb4703..983a50366 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "Xem trên Etherscan" }, - "expandExperience": { - "message": "Mở rộng trải nghiệm web3 của bạn với MetaMask Snap" - }, "expandView": { "message": "Mở rộng cửa sổ xem" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "Dữ liệu không đúng định dạng" }, - "manageSnaps": { - "message": "Quản lý các Snap đã cài đặt" - }, "max": { "message": "Tối đa" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "Không thấy token của mình?" }, - "mobileSyncWarning": { - "message": "Tính năng 'Đồng bộ với tiện ích' tạm thời bị tắt. Nếu bạn muốn sử dụng ví tiện ích trên thiết bị di động MetaMask, thì trên ứng dụng di động: hãy quay lại các tùy chọn thiết lập ví và chọn phương án 'Nhập bằng Cụm Mật Khẩu Khôi Phục Bí Mật'. Sử dụng cụm mật khẩu bí mật của ví tiện ích để nhập ví của bạn vào thiết bị di động." - }, "moreComingSoon": { "message": "Sắp có thêm..." }, @@ -2188,9 +2179,6 @@ "message": "Số chỉ dùng một lần lớn hơn số chỉ dùng một lần gợi ý là $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "NFT" - }, "nftAddFailedMessage": { "message": "Không thể thêm NFT vì thông tin quyền sở hữu không trùng khớp. Đảm bảo bạn đã nhập đúng thông tin." }, @@ -3253,12 +3241,6 @@ "show": { "message": "Hiển thị" }, - "showAdvancedGasInline": { - "message": "Quyền kiểm soát gas nâng cao" - }, - "showAdvancedGasInlineDescription": { - "message": "Chọn tùy chọn này để hiển thị các quyền kiểm soát giá gas và giới hạn ngay trên màn hình gửi và xác nhận." - }, "showFiatConversionInTestnets": { "message": "Hiển thị tỷ lệ quy đổi trên các mạng thử nghiệm" }, @@ -3358,23 +3340,12 @@ "message": "Bạn đang cấp quyền truy cập khóa $2 cho Snap \"$1\". Hành động này không thể hủy bỏ và sẽ cấp quyền kiểm soát tài khoản và tài sản $2 của bạn cho \"$1\". Đảm bảo bạn tin tưởng \"$1\" trước khi tiếp tục.", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "Snap này đang yêu cầu các quyền sau:" - }, "snapUpdate": { "message": "Cập nhật Snap" }, - "snapUpdateExplanation": { - "message": "$1 cần một phiên bản Snap mới hơn.", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snap" }, - "snapsInsightError": { - "message": "Đã xảy ra lỗi với $1: $2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "Đang tải thông tin chi tiết về giao dịch..." }, @@ -3391,7 +3362,8 @@ "message": "Snap chỉ hoạt động khi đã bật" }, "snapsUIError": { - "message": "Giao diện người dùng được chỉ định bởi snap không hợp lệ." + "message": "Giao diện người dùng được chỉ định bởi snap không hợp lệ.", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "Một số mạng có thể gây ra rủi ro về bảo mật và/hoặc quyền riêng tư. Bạn cần hiểu rõ các rủi ro này trước khi thêm và sử dụng mạng." @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "Ký hiệu không được dài quá 11 ký tự." }, - "syncFailed": { - "message": "Đồng bộ thất bại" - }, - "syncInProgress": { - "message": "Đang đồng bộ" - }, - "syncWithMobile": { - "message": "Đồng bộ với thiết bị di động" - }, - "syncWithMobileBeCareful": { - "message": "Đảm bảo rằng không có ai nhìn vào màn hình của bạn khi quét mã này" - }, - "syncWithMobileComplete": { - "message": "Đã đồng bộ thành công dữ liệu của bạn. Tận hưởng ứng dụng MetaMask trên thiết bị di động!" - }, - "syncWithMobileDesc": { - "message": "Bạn có thể đồng bộ tài khoản và thông tin của mình với thiết bị di động. Mở ứng dụng MetaMask trên thiết bị di động, chuyển đến phần \"Cài đặt\" và nhấn vào \"Đồng bộ với tiện ích trình duyệt\"" - }, - "syncWithMobileDescNewUsers": { - "message": "Nếu mới mở ứng dụng MetaMask trên thiết bị di động lần đầu tiên, bạn chỉ cần làm theo các bước hướng dẫn trên điện thoại." - }, - "syncWithMobileScanThisCode": { - "message": "Quét mã này bằng ứng dụng MetaMask trên thiết bị di động" - }, - "syncWithMobileTitle": { - "message": "Đồng bộ với thiết bị di động" - }, "tenPercentIncreased": { "message": "Tăng 10%" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index b6b057c41..a90f47e1a 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1317,9 +1317,6 @@ "etherscanViewOn": { "message": "在 Etherscan 上查看" }, - "expandExperience": { - "message": "扩展MetaMask Snap的web3体验" - }, "expandView": { "message": "展开视图" }, @@ -1952,9 +1949,6 @@ "malformedData": { "message": "格式错误的数据" }, - "manageSnaps": { - "message": "管理已安装的Snap" - }, "max": { "message": "最大" }, @@ -2027,9 +2021,6 @@ "missingToken": { "message": "没有看到您的代币?" }, - "mobileSyncWarning": { - "message": "“与扩展程序同步”功能暂时被禁用。如果您想要在 MetaMask 移动设备上使用您的扩展程序钱包,那么在您的移动应用程序上:返回钱包设置选项并选择“使用账户助记词导入”选项。使用您的扩展程序钱包的助记词来将您的钱包导入移动设备。" - }, "moreComingSoon": { "message": "更多即将到来……" }, @@ -2188,9 +2179,6 @@ "message": "Nonce 高于建议的 nouce 值 $1", "description": "The next nonce according to MetaMask's internal logic" }, - "nft": { - "message": "非同质化代币(NFT)" - }, "nftAddFailedMessage": { "message": "由于所有权信息不匹配,无法添加NFT。请确保所输入的信息正确无误。" }, @@ -3253,12 +3241,6 @@ "show": { "message": "显示" }, - "showAdvancedGasInline": { - "message": "高级燃料控制" - }, - "showAdvancedGasInlineDescription": { - "message": "选择此项可直接在发送和确认界面显示燃料价格和上限控制。" - }, "showFiatConversionInTestnets": { "message": "在测试网络上显示转换" }, @@ -3358,23 +3340,12 @@ "message": "您正在向snap \"$1\"授予$2的密钥访问权限。此操作不可撤销,并会向\"$1\"授予对您的$2账户和资产的控制权。在继续之前,请确保您信任\"$1\"。", "description": "The first parameter is the name of the snap and the second one is the protocol" }, - "snapRequestsPermission": { - "message": "此Snap正在请求以下权限:" - }, "snapUpdate": { "message": "更新Snap" }, - "snapUpdateExplanation": { - "message": "$1需要更新版本的snap。", - "description": "$1 is the dapp that is requesting an update to the snap." - }, "snaps": { "message": "Snap" }, - "snapsInsightError": { - "message": "$1 发生错误:$2", - "description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message." - }, "snapsInsightLoading": { "message": "正在加载交易洞察……" }, @@ -3391,7 +3362,8 @@ "message": "Snap仅在启用后才会运行" }, "snapsUIError": { - "message": "Snap指定的用户界面无效。" + "message": "Snap指定的用户界面无效。", + "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, "someNetworksMayPoseSecurity": { "message": "某些网络可能会带来安全和/或隐私风险。在添加和使用网络之前,请先了解风险。" @@ -3965,33 +3937,6 @@ "symbolBetweenZeroTwelve": { "message": "符号不得超过11个字符。" }, - "syncFailed": { - "message": "同步失败" - }, - "syncInProgress": { - "message": "同步进行中" - }, - "syncWithMobile": { - "message": "与移动设备同步" - }, - "syncWithMobileBeCareful": { - "message": "扫描此代码时,请确保附近没有其他人在看您的屏幕" - }, - "syncWithMobileComplete": { - "message": "您的数据已同步成功。尽情体验 MetaMask 移动应用程序吧!" - }, - "syncWithMobileDesc": { - "message": "您可以将您的账户和信息与您的移动设备同步。打开 MetaMask 移动应用程序,进入“设置”,点击“从浏览器扩展程序同步”" - }, - "syncWithMobileDescNewUsers": { - "message": "如果您是首次启用 MetaMask 移动应用程序,请通过个人手机完成如下操作。" - }, - "syncWithMobileScanThisCode": { - "message": "使用 MetaMask 移动应用程序扫描此代码" - }, - "syncWithMobileTitle": { - "message": "与移动设备同步" - }, "tenPercentIncreased": { "message": "增加10%" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index ccb72664b..486ea922d 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -1245,12 +1245,6 @@ "settings": { "message": "設定" }, - "showAdvancedGasInline": { - "message": "顯示進階 gas 控制選項" - }, - "showAdvancedGasInlineDescription": { - "message": "選擇此項會在傳送或確認畫面顯示可微調 gas 價格以及 gas 上限的功能" - }, "showFiatConversionInTestnets": { "message": "在測試網上顯示匯率" }, @@ -1393,27 +1387,6 @@ "symbolBetweenZeroTwelve": { "message": "符號不得超過 11 個字元。" }, - "syncWithMobile": { - "message": "和行動裝置同步" - }, - "syncWithMobileBeCareful": { - "message": "掃描代碼時確保沒有其他人在看你的螢幕" - }, - "syncWithMobileComplete": { - "message": "你的資料已成功同步。開始活用 MetaMask 行動應用程式!" - }, - "syncWithMobileDesc": { - "message": "你可以用行動裝置同步帳戶與資訊。開啟 MetaMask 行動應用程式,至「設定」並點擊「自瀏覽器擴充功能同步」" - }, - "syncWithMobileDescNewUsers": { - "message": "如果您是第一次開啟 MetaMask 行動應用程式,只要跟著手機上的指示操作即可。" - }, - "syncWithMobileScanThisCode": { - "message": "用您的 MetaMask 行動應用程式 掃描此條碼" - }, - "syncWithMobileTitle": { - "message": "和行動裝置同步" - }, "terms": { "message": "使用條款" }, diff --git a/app/build-types/mmi/images/bitgo.svg b/app/build-types/mmi/images/bitgo.svg deleted file mode 100644 index 21e237c3a..000000000 --- a/app/build-types/mmi/images/bitgo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/build-types/mmi/images/cactus.svg b/app/build-types/mmi/images/cactus.svg deleted file mode 100644 index 5abde4485..000000000 --- a/app/build-types/mmi/images/cactus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/build-types/mmi/images/curv-logo-horizontal-black.svg b/app/build-types/mmi/images/curv-logo-horizontal-black.svg deleted file mode 100644 index 45e128b12..000000000 --- a/app/build-types/mmi/images/curv-logo-horizontal-black.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/build-types/mmi/images/curv-logo.svg b/app/build-types/mmi/images/curv-logo.svg deleted file mode 100644 index 37c06025b..000000000 --- a/app/build-types/mmi/images/curv-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/build-types/mmi/images/parfin.svg b/app/build-types/mmi/images/parfin.svg deleted file mode 100644 index bf09f39b7..000000000 --- a/app/build-types/mmi/images/parfin.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/build-types/mmi/images/qredo.svg b/app/build-types/mmi/images/qredo.svg deleted file mode 100644 index 4f3d182c9..000000000 --- a/app/build-types/mmi/images/qredo.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/images/icons/ethereum.svg b/app/images/icons/ethereum.svg new file mode 100644 index 000000000..f3e509708 --- /dev/null +++ b/app/images/icons/ethereum.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/scripts/app-init.js b/app/scripts/app-init.js index 0f61d9888..450c59597 100644 --- a/app/scripts/app-init.js +++ b/app/scripts/app-init.js @@ -127,10 +127,6 @@ chrome.runtime.onMessage.addListener(() => { return false; }); -chrome.runtime.onStartup.addListener(() => { - globalThis.isFirstTimeProfileLoaded = true; -}); - /* * This content script is injected programmatically because * MAIN world injection does not work properly via manifest diff --git a/app/scripts/background.js b/app/scripts/background.js index b6d51f657..d92fddb81 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -24,11 +24,11 @@ import { } from '../../shared/constants/app'; import { SECOND } from '../../shared/constants/time'; import { - REJECT_NOTFICIATION_CLOSE, - REJECT_NOTFICIATION_CLOSE_SIG, - EVENT, - EVENT_NAMES, - TRAITS, + REJECT_NOTIFICATION_CLOSE, + REJECT_NOTIFICATION_CLOSE_SIG, + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsUserTrait, } from '../../shared/constants/metametrics'; import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils'; import { isManifestV3 } from '../../shared/modules/mv3.utils'; @@ -223,7 +223,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => { * @property {object} provider - The current selected network provider. * @property {string} provider.rpcUrl - The address for the RPC API, if using an RPC API. * @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks. - * @property {string} network - A stringified number of the current network ID. + * @property {string} networkId - The stringified number of the current network ID. + * @property {string} networkStatus - Either "unknown", "available", "unavailable", or "blocked", depending on the status of the currently selected network. * @property {object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values. * @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string. * @property {TransactionMeta[]} currentNetworkTxList - An array of transactions associated with the currently selected network. @@ -267,7 +268,23 @@ async function initialize() { await DesktopManager.init(platform.getVersion()); ///: END:ONLY_INCLUDE_IN - setupController(initState, initLangCode); + let isFirstMetaMaskControllerSetup; + if (isManifestV3) { + const sessionData = await browser.storage.session.get([ + 'isFirstMetaMaskControllerSetup', + ]); + + isFirstMetaMaskControllerSetup = + sessionData?.isFirstMetaMaskControllerSetup === undefined; + await browser.storage.session.set({ isFirstMetaMaskControllerSetup }); + } + + setupController( + initState, + initLangCode, + {}, + isFirstMetaMaskControllerSetup, + ); if (!isManifestV3) { await loadPhishingWarningPage(); } @@ -409,8 +426,14 @@ export async function loadStateFromPersistence() { * @param {object} initState - The initial state to start the controller with, matches the state that is emitted from the controller. * @param {string} initLangCode - The region code for the language preferred by the current user. * @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app) + * @param isFirstMetaMaskControllerSetup */ -export function setupController(initState, initLangCode, overrides) { +export function setupController( + initState, + initLangCode, + overrides, + isFirstMetaMaskControllerSetup, +) { // // MetaMask Controller // @@ -436,6 +459,7 @@ export function setupController(initState, initLangCode, overrides) { }, localStore, overrides, + isFirstMetaMaskControllerSetup, }); setupEnsIpfsResolver({ @@ -555,6 +579,10 @@ export function setupController(initState, initLangCode, overrides) { if (message.name === WORKER_KEEP_ALIVE_MESSAGE) { // To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI. remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE }); + + controller.appStateController.setServiceWorkerLastActiveTime( + Date.now(), + ); } }); } @@ -656,14 +684,6 @@ export function setupController(initState, initLangCode, overrides) { METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); - controller.messageManager.on( - METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, - updateBadge, - ); - controller.personalMessageManager.on( - METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, - updateBadge, - ); controller.decryptMessageManager.on( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, @@ -672,7 +692,7 @@ export function setupController(initState, initLangCode, overrides) { METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); - controller.typedMessageManager.on( + controller.signController.hub.on( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); @@ -708,23 +728,17 @@ export function setupController(initState, initLangCode, overrides) { function getUnapprovedTransactionCount() { const unapprovedTxCount = controller.txController.getUnapprovedTxCount(); - const { unapprovedMsgCount } = controller.messageManager; - const { unapprovedPersonalMsgCount } = controller.personalMessageManager; const { unapprovedDecryptMsgCount } = controller.decryptMessageManager; const { unapprovedEncryptionPublicKeyMsgCount } = controller.encryptionPublicKeyManager; - const { unapprovedTypedMessagesCount } = controller.typedMessageManager; const pendingApprovalCount = controller.approvalController.getTotalApprovalCount(); const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length; return ( unapprovedTxCount + - unapprovedMsgCount + - unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + - unapprovedTypedMessagesCount + pendingApprovalCount + waitingForUnlockCount ); @@ -747,36 +761,13 @@ export function setupController(initState, initLangCode, overrides) { ).forEach((txId) => controller.txController.txStateManager.setTxStatusRejected(txId), ); - controller.messageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.messageManager.rejectMsg( - tx.id, - REJECT_NOTFICIATION_CLOSE_SIG, - ), - ); - controller.personalMessageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.personalMessageManager.rejectMsg( - tx.id, - REJECT_NOTFICIATION_CLOSE_SIG, - ), - ); - controller.typedMessageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.typedMessageManager.rejectMsg( - tx.id, - REJECT_NOTFICIATION_CLOSE_SIG, - ), - ); + controller.signController.rejectUnapproved(REJECT_NOTIFICATION_CLOSE_SIG); controller.decryptMessageManager.messages .filter((msg) => msg.status === 'unapproved') .forEach((tx) => controller.decryptMessageManager.rejectMsg( tx.id, - REJECT_NOTFICIATION_CLOSE, + REJECT_NOTIFICATION_CLOSE, ), ); controller.encryptionPublicKeyManager.messages @@ -784,7 +775,7 @@ export function setupController(initState, initLangCode, overrides) { .forEach((tx) => controller.encryptionPublicKeyManager.rejectMsg( tx.id, - REJECT_NOTFICIATION_CLOSE, + REJECT_NOTIFICATION_CLOSE, ), ); @@ -880,11 +871,13 @@ async function openPopup() { const addAppInstalledEvent = () => { if (controller) { controller.metaMetricsController.updateTraits({ - [TRAITS.INSTALL_DATE_EXT]: new Date().toISOString().split('T')[0], // yyyy-mm-dd + [MetaMetricsUserTrait.InstallDateExt]: new Date() + .toISOString() + .split('T')[0], // yyyy-mm-dd }); controller.metaMetricsController.addEventBeforeMetricsOptIn({ - category: EVENT.CATEGORIES.APP, - event: EVENT_NAMES.APP_INSTALLED, + category: MetaMetricsEventCategory.App, + event: MetaMetricsEventName.AppInstalled, properties: {}, }); return; diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 7d1c83a4e..f7669e055 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -51,6 +51,7 @@ export default class AppStateController extends EventEmitter { '0x5': true, '0x539': true, }, + serviceWorkerLastActiveTime: 0, }); this.timer = null; @@ -362,4 +363,10 @@ export default class AppStateController extends EventEmitter { getCurrentPopupId() { return this.store.getState().currentPopupId; } + + setServiceWorkerLastActiveTime(serviceWorkerLastActiveTime) { + this.store.updateState({ + serviceWorkerLastActiveTime, + }); + } } diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index a53bee096..2b23dd71b 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -8,7 +8,10 @@ import { AssetType, TokenStandard, } from '../../../shared/constants/transaction'; -import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; // By default, poll every 3 minutes const DEFAULT_INTERVAL = MINUTE * 3; @@ -167,8 +170,8 @@ export default class DetectTokensController { if (tokensWithBalance.length > 0) { this._trackMetaMetricsEvent({ - event: EVENT_NAMES.TOKEN_DETECTED, - category: EVENT.CATEGORIES.WALLET, + event: MetaMetricsEventName.TokenDetected, + category: MetaMetricsEventCategory.Wallet, properties: { tokens: eventTokensDetails, token_standard: TokenStandard.ERC20, diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index 8a3685f22..c90ebc590 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -13,12 +13,12 @@ import { convertHexToDecimal } from '@metamask/controller-utils'; import { NETWORK_TYPES } from '../../../shared/constants/network'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; -import NetworkController, { NETWORK_EVENTS } from './network'; +import NetworkController, { NetworkControllerEventTypes } from './network'; import PreferencesController from './preferences'; describe('DetectTokensController', function () { - const sandbox = sinon.createSandbox(); - let assetsContractController, + let sandbox, + assetsContractController, keyringMemStore, network, preferences, @@ -32,78 +32,94 @@ describe('DetectTokensController', function () { getAccounts: noop, }; + const infuraProjectId = 'infura-project-id'; + beforeEach(async function () { - keyringMemStore = new ObservableStore({ isUnlocked: false }); - network = new NetworkController({ infuraProjectId: 'foo' }); - network.initializeProvider(networkControllerProviderConfig); - provider = network.getProviderAndBlockTracker().provider; - - const tokenListMessenger = new ControllerMessenger().getRestricted({ - name: 'TokenListController', - }); - tokenListController = new TokenListController({ - chainId: '1', - preventPollingOnNetworkRestart: false, - onNetworkStateChange: sinon.spy(), - onPreferencesStateChange: sinon.spy(), - messenger: tokenListMessenger, - }); - await tokenListController.start(); - - preferences = new PreferencesController({ - network, - provider, - tokenListController, - }); - preferences.setAddresses([ - '0x7e57e2', - '0xbc86727e770de68b1060c91f6bb6945c73e10388', - ]); - preferences.setUseTokenDetection(true); - - tokensController = new TokensController({ - onPreferencesStateChange: preferences.store.subscribe.bind( - preferences.store, - ), - onNetworkStateChange: (cb) => - network.store.subscribe((networkState) => { - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.provider, + sandbox = sinon.createSandbox(); + // Disable all requests, even those to localhost + nock.disableNetConnect(); + nock('https://mainnet.infura.io') + .post(`/v3/${infuraProjectId}`) + .reply(200, (_uri, requestBody) => { + if (requestBody.method === 'eth_getBlockByNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: { + number: '0x42', }, }; - return cb(modifiedNetworkState); - }), - }); + } - assetsContractController = new AssetsContractController({ - onPreferencesStateChange: preferences.store.subscribe.bind( - preferences.store, - ), - onNetworkStateChange: (cb) => - network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - const networkState = network.store.getState(); - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.provider, - chainId: convertHexToDecimal(networkState.provider.chainId), + if (requestBody.method === 'eth_blockNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '0x42', + }; + } + + throw new Error(`(Infura) Mock not defined for ${requestBody.method}`); + }) + .persist(); + nock('https://sepolia.infura.io') + .post(`/v3/${infuraProjectId}`) + .reply(200, (_uri, requestBody) => { + if (requestBody.method === 'eth_getBlockByNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: { + number: '0x42', }, }; - return cb(modifiedNetworkState); - }), - }); + } - sandbox - .stub(network, '_getLatestBlock') - .callsFake(() => Promise.resolve({})); - sandbox - .stub(tokensController, '_instantiateNewEthersProvider') - .returns(null); - sandbox - .stub(tokensController, '_detectIsERC721') - .returns(Promise.resolve(false)); + if (requestBody.method === 'eth_blockNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '0x42', + }; + } + + throw new Error(`(Infura) Mock not defined for ${requestBody.method}`); + }) + .persist(); + nock('http://localhost:8545') + .post('/') + .reply(200, (_uri, requestBody) => { + if (requestBody.method === 'eth_getBlockByNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: { + number: '0x42', + }, + }; + } + + if (requestBody.method === 'eth_blockNumber') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '0x42', + }; + } + + if (requestBody.method === 'net_version') { + return { + id: requestBody.id, + jsonrpc: '2.0', + result: '1337', + }; + } + + throw new Error( + `(localhost) Mock not defined for ${requestBody.method}`, + ); + }) + .persist(); nock('https://token-api.metaswap.codefi.network') .get(`/tokens/1`) .reply(200, [ @@ -174,9 +190,82 @@ describe('DetectTokensController', function () { .get(`/tokens/3`) .reply(200, { error: 'ChainId 3 is not supported' }) .persist(); + + keyringMemStore = new ObservableStore({ isUnlocked: false }); + const networkControllerMessenger = new ControllerMessenger(); + network = new NetworkController({ + messenger: networkControllerMessenger, + infuraProjectId, + }); + await network.initializeProvider(networkControllerProviderConfig); + provider = network.getProviderAndBlockTracker().provider; + + const tokenListMessenger = new ControllerMessenger().getRestricted({ + name: 'TokenListController', + }); + tokenListController = new TokenListController({ + chainId: '1', + preventPollingOnNetworkRestart: false, + onNetworkStateChange: sinon.spy(), + onPreferencesStateChange: sinon.spy(), + messenger: tokenListMessenger, + }); + await tokenListController.start(); + + preferences = new PreferencesController({ + network, + provider, + tokenListController, + onInfuraIsBlocked: sinon.stub(), + onInfuraIsUnblocked: sinon.stub(), + }); + preferences.setAddresses([ + '0x7e57e2', + '0xbc86727e770de68b1060c91f6bb6945c73e10388', + ]); + preferences.setUseTokenDetection(true); + + tokensController = new TokensController({ + config: { provider }, + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: (cb) => + network.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + providerConfig: { + ...networkState.provider, + }, + }; + return cb(modifiedNetworkState); + }), + }); + + assetsContractController = new AssetsContractController({ + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: (cb) => + networkControllerMessenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + () => { + const networkState = network.store.getState(); + const modifiedNetworkState = { + ...networkState, + providerConfig: { + ...networkState.provider, + chainId: convertHexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }, + ), + }); }); - after(function () { + afterEach(function () { + nock.enableNetConnect('localhost'); sandbox.restore(); }); diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index a949f3cc7..c433b3f5e 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -15,7 +15,7 @@ import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { METAMETRICS_ANONYMOUS_ID, METAMETRICS_BACKGROUND_PAGE_OBJECT, - TRAITS, + MetaMetricsUserTrait, } from '../../../shared/constants/metametrics'; import { SECOND } from '../../../shared/constants/time'; import { isManifestV3 } from '../../../shared/modules/mv3.utils'; @@ -692,38 +692,44 @@ export default class MetaMetricsController { const { traits, previousUserTraits } = this.store.getState(); /** @type {MetaMetricsTraits} */ const currentTraits = { - [TRAITS.ADDRESS_BOOK_ENTRIES]: sum( + [MetaMetricsUserTrait.AddressBookEntries]: sum( Object.values(metamaskState.addressBook).map(size), ), - [TRAITS.INSTALL_DATE_EXT]: traits[TRAITS.INSTALL_DATE_EXT] || '', - [TRAITS.LEDGER_CONNECTION_TYPE]: metamaskState.ledgerTransportType, - [TRAITS.NETWORKS_ADDED]: Object.values( + [MetaMetricsUserTrait.InstallDateExt]: + traits[MetaMetricsUserTrait.InstallDateExt] || '', + [MetaMetricsUserTrait.LedgerConnectionType]: + metamaskState.ledgerTransportType, + [MetaMetricsUserTrait.NetworksAdded]: Object.values( metamaskState.networkConfigurations, ).map((networkConfiguration) => networkConfiguration.chainId), - [TRAITS.NETWORKS_WITHOUT_TICKER]: Object.values( + [MetaMetricsUserTrait.NetworksWithoutTicker]: Object.values( metamaskState.networkConfigurations, ) .filter(({ ticker }) => !ticker) .map(({ chainId }) => chainId), - [TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useNftDetection, - [TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities) - .length, - [TRAITS.NUMBER_OF_NFT_COLLECTIONS]: this._getAllUniqueNFTAddressesLength( + [MetaMetricsUserTrait.NftAutodetectionEnabled]: + metamaskState.useNftDetection, + [MetaMetricsUserTrait.NumberOfAccounts]: Object.values( + metamaskState.identities, + ).length, + [MetaMetricsUserTrait.NumberOfNftCollections]: + this._getAllUniqueNFTAddressesLength(metamaskState.allNfts), + [MetaMetricsUserTrait.NumberOfNfts]: this._getAllNFTsFlattened( metamaskState.allNfts, - ), - [TRAITS.NUMBER_OF_NFTS]: this._getAllNFTsFlattened(metamaskState.allNfts) - .length, - [TRAITS.NUMBER_OF_TOKENS]: this._getNumberOfTokens(metamaskState), - [TRAITS.OPENSEA_API_ENABLED]: metamaskState.openSeaEnabled, - [TRAITS.THREE_BOX_ENABLED]: false, // deprecated, hard-coded as false - [TRAITS.THEME]: metamaskState.theme || 'default', - [TRAITS.TOKEN_DETECTION_ENABLED]: metamaskState.useTokenDetection, + ).length, + [MetaMetricsUserTrait.NumberOfTokens]: + this._getNumberOfTokens(metamaskState), + [MetaMetricsUserTrait.OpenseaApiEnabled]: metamaskState.openSeaEnabled, + [MetaMetricsUserTrait.ThreeBoxEnabled]: false, // deprecated, hard-coded as false + [MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default', + [MetaMetricsUserTrait.TokenDetectionEnabled]: + metamaskState.useTokenDetection, ///: BEGIN:ONLY_INCLUDE_IN(flask) - [TRAITS.DESKTOP_ENABLED]: metamaskState.desktopEnabled || false, + [MetaMetricsUserTrait.DesktopEnabled]: + metamaskState.desktopEnabled || false, ///: END:ONLY_INCLUDE_IN - [TRAITS.SECURITY_PROVIDERS]: metamaskState.transactionSecurityCheckEnabled - ? ['opensea'] - : [], + [MetaMetricsUserTrait.SecurityProviders]: + metamaskState.transactionSecurityCheckEnabled ? ['opensea'] : [], }; if (!previousUserTraits) { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index f3969a7b3..dbd0e56c8 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -5,7 +5,7 @@ import { createSegmentMock } from '../lib/segment'; import { METAMETRICS_ANONYMOUS_ID, METAMETRICS_BACKGROUND_PAGE_OBJECT, - TRAITS, + MetaMetricsUserTrait, } from '../../../shared/constants/metametrics'; import waitUntilCalled from '../../../test/lib/wait-until-called'; import { @@ -15,7 +15,6 @@ import { } from '../../../shared/constants/network'; import * as Utils from '../lib/util'; import MetaMetricsController from './metametrics'; -import { NETWORK_EVENTS } from './network'; const segment = createSegmentMock(2, 10000); @@ -72,17 +71,17 @@ function getMockNetworkController() { }, network: 'loading', }; - const on = sinon.stub().withArgs(NETWORK_EVENTS.NETWORK_DID_CHANGE); + const onNetworkDidChange = sinon.stub(); const updateState = (newState) => { state = { ...state, ...newState }; - on.getCall(0).args[1](); + onNetworkDidChange.getCall(0).args[0](); }; return { store: { getState: () => state, updateState, }, - on, + onNetworkDidChange, }; } @@ -136,10 +135,8 @@ function getMetaMetricsController({ segment: segmentInstance || segment, getCurrentChainId: () => networkController.store.getState().provider.chainId, - onNetworkDidChange: networkController.on.bind( - networkController, - NETWORK_EVENTS.NETWORK_DID_CHANGE, - ), + onNetworkDidChange: + networkController.onNetworkDidChange.bind(networkController), preferencesStore, version: '0.0.1', environment: 'test', @@ -956,22 +953,26 @@ describe('MetaMetricsController', function () { }); assert.deepEqual(traits, { - [TRAITS.ADDRESS_BOOK_ENTRIES]: 3, - [TRAITS.INSTALL_DATE_EXT]: '', - [TRAITS.LEDGER_CONNECTION_TYPE]: 'web-hid', - [TRAITS.NETWORKS_ADDED]: [CHAIN_IDS.MAINNET, CHAIN_IDS.GOERLI, '0xaf'], - [TRAITS.NETWORKS_WITHOUT_TICKER]: ['0xaf'], - [TRAITS.NFT_AUTODETECTION_ENABLED]: false, - [TRAITS.NUMBER_OF_ACCOUNTS]: 2, - [TRAITS.NUMBER_OF_NFT_COLLECTIONS]: 3, - [TRAITS.NUMBER_OF_NFTS]: 4, - [TRAITS.NUMBER_OF_TOKENS]: 5, - [TRAITS.OPENSEA_API_ENABLED]: true, - [TRAITS.THREE_BOX_ENABLED]: false, - [TRAITS.THEME]: 'default', - [TRAITS.TOKEN_DETECTION_ENABLED]: true, - [TRAITS.DESKTOP_ENABLED]: false, - [TRAITS.SECURITY_PROVIDERS]: [], + [MetaMetricsUserTrait.AddressBookEntries]: 3, + [MetaMetricsUserTrait.InstallDateExt]: '', + [MetaMetricsUserTrait.LedgerConnectionType]: 'web-hid', + [MetaMetricsUserTrait.NetworksAdded]: [ + CHAIN_IDS.MAINNET, + CHAIN_IDS.GOERLI, + '0xaf', + ], + [MetaMetricsUserTrait.NetworksWithoutTicker]: ['0xaf'], + [MetaMetricsUserTrait.NftAutodetectionEnabled]: false, + [MetaMetricsUserTrait.NumberOfAccounts]: 2, + [MetaMetricsUserTrait.NumberOfNftCollections]: 3, + [MetaMetricsUserTrait.NumberOfNfts]: 4, + [MetaMetricsUserTrait.NumberOfTokens]: 5, + [MetaMetricsUserTrait.OpenseaApiEnabled]: true, + [MetaMetricsUserTrait.ThreeBoxEnabled]: false, + [MetaMetricsUserTrait.Theme]: 'default', + [MetaMetricsUserTrait.TokenDetectionEnabled]: true, + [MetaMetricsUserTrait.DesktopEnabled]: false, + [MetaMetricsUserTrait.SecurityProviders]: [], }); }); @@ -1018,10 +1019,10 @@ describe('MetaMetricsController', function () { }); assert.deepEqual(updatedTraits, { - [TRAITS.ADDRESS_BOOK_ENTRIES]: 4, - [TRAITS.NUMBER_OF_ACCOUNTS]: 3, - [TRAITS.NUMBER_OF_TOKENS]: 1, - [TRAITS.OPENSEA_API_ENABLED]: false, + [MetaMetricsUserTrait.AddressBookEntries]: 4, + [MetaMetricsUserTrait.NumberOfAccounts]: 3, + [MetaMetricsUserTrait.NumberOfTokens]: 1, + [MetaMetricsUserTrait.OpenseaApiEnabled]: false, }); }); diff --git a/app/scripts/controllers/network/create-network-client.test.js b/app/scripts/controllers/network/create-network-client.test.js new file mode 100644 index 000000000..7e674392b --- /dev/null +++ b/app/scripts/controllers/network/create-network-client.test.js @@ -0,0 +1,7 @@ +import { NetworkClientType } from './create-network-client'; +import { testsForProviderType } from './provider-api-tests/shared-tests'; + +describe('createNetworkClient', () => { + testsForProviderType(NetworkClientType.Infura); + testsForProviderType(NetworkClientType.Custom); +}); diff --git a/app/scripts/controllers/network/create-network-client.ts b/app/scripts/controllers/network/create-network-client.ts new file mode 100644 index 000000000..6e96b71f5 --- /dev/null +++ b/app/scripts/controllers/network/create-network-client.ts @@ -0,0 +1,191 @@ +import { + createAsyncMiddleware, + createScaffoldMiddleware, + JsonRpcEngine, + mergeMiddleware, + JsonRpcMiddleware, +} from 'json-rpc-engine'; +import { + createBlockCacheMiddleware, + createBlockRefMiddleware, + createBlockRefRewriteMiddleware, + createBlockTrackerInspectorMiddleware, + createInflightCacheMiddleware, + createFetchMiddleware, + createRetryOnEmptyMiddleware, +} from '@metamask/eth-json-rpc-middleware'; +import { + providerFromEngine, + providerFromMiddleware, + SafeEventEmitterProvider, +} from '@metamask/eth-json-rpc-provider'; +import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; +import type { Hex } from '@metamask/utils/dist'; +import { PollingBlockTracker } from 'eth-block-tracker/dist'; +import { SECOND } from '../../../../shared/constants/time'; +import { + BUILT_IN_INFURA_NETWORKS, + BuiltInInfuraNetwork, +} from '../../../../shared/constants/network'; + +export enum NetworkClientType { + Custom = 'custom', + Infura = 'infura', +} + +type CustomNetworkConfiguration = { + chainId: Hex; + rpcUrl: string; + type: NetworkClientType.Custom; +}; + +type InfuraNetworkConfiguration = { + network: BuiltInInfuraNetwork; + infuraProjectId: string; + type: NetworkClientType.Infura; +}; + +/** + * Create a JSON RPC network client for a specific network. + * + * @param networkConfig - The network configuration. + * @returns + */ +export function createNetworkClient( + networkConfig: CustomNetworkConfiguration | InfuraNetworkConfiguration, +): { provider: SafeEventEmitterProvider; blockTracker: PollingBlockTracker } { + const rpcApiMiddleware = + networkConfig.type === NetworkClientType.Infura + ? createInfuraMiddleware({ + network: networkConfig.network, + projectId: networkConfig.infuraProjectId, + maxAttempts: 5, + source: 'metamask', + }) + : createFetchMiddleware({ + btoa: global.btoa, + fetch: global.fetch, + rpcUrl: networkConfig.rpcUrl, + }); + + const rpcProvider = providerFromMiddleware(rpcApiMiddleware); + + const blockTrackerOpts = + process.env.IN_TEST && networkConfig.type === 'custom' + ? { pollingInterval: SECOND } + : {}; + const blockTracker = new PollingBlockTracker({ + ...blockTrackerOpts, + provider: rpcProvider, + }); + + const networkMiddleware = + networkConfig.type === NetworkClientType.Infura + ? createInfuraNetworkMiddleware({ + blockTracker, + network: networkConfig.network, + rpcProvider, + rpcApiMiddleware, + }) + : createCustomNetworkMiddleware({ + blockTracker, + chainId: networkConfig.chainId, + rpcApiMiddleware, + }); + + const engine = new JsonRpcEngine(); + + engine.push(networkMiddleware); + + const provider = providerFromEngine(engine); + + return { provider, blockTracker }; +} + +function createInfuraNetworkMiddleware({ + blockTracker, + network, + rpcProvider, + rpcApiMiddleware, +}: { + blockTracker: PollingBlockTracker; + network: BuiltInInfuraNetwork; + rpcProvider: SafeEventEmitterProvider; + rpcApiMiddleware: JsonRpcMiddleware; +}) { + return mergeMiddleware([ + createNetworkAndChainIdMiddleware({ network }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ]); +} + +function createNetworkAndChainIdMiddleware({ + network, +}: { + network: BuiltInInfuraNetwork; +}) { + if (!BUILT_IN_INFURA_NETWORKS[network]) { + throw new Error(`createInfuraClient - unknown network "${network}"`); + } + + const { chainId, networkId } = BUILT_IN_INFURA_NETWORKS[network]; + + return createScaffoldMiddleware({ + eth_chainId: chainId, + net_version: networkId, + }); +} + +const createChainIdMiddleware = ( + chainId: string, +): JsonRpcMiddleware => { + return (req, res, next, end) => { + if (req.method === 'eth_chainId') { + res.result = chainId; + return end(); + } + return next(); + }; +}; + +function createCustomNetworkMiddleware({ + blockTracker, + chainId, + rpcApiMiddleware, +}: { + blockTracker: PollingBlockTracker; + chainId: string; + rpcApiMiddleware: any; +}) { + const testMiddlewares = process.env.IN_TEST + ? [createEstimateGasDelayTestMiddleware()] + : []; + + return mergeMiddleware([ + ...testMiddlewares, + createChainIdMiddleware(chainId), + createBlockRefRewriteMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ]); +} + +/** + * For use in tests only. + * Adds a delay to `eth_estimateGas` calls. + */ +function createEstimateGasDelayTestMiddleware() { + return createAsyncMiddleware(async (req, _, next) => { + if (req.method === 'eth_estimateGas') { + await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); + } + return next(); + }); +} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js deleted file mode 100644 index 76461eb62..000000000 --- a/app/scripts/controllers/network/createInfuraClient.js +++ /dev/null @@ -1,49 +0,0 @@ -import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import { - createBlockRefMiddleware, - createRetryOnEmptyMiddleware, - createBlockCacheMiddleware, - createInflightCacheMiddleware, - createBlockTrackerInspectorMiddleware, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-middleware'; - -import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; -import { PollingBlockTracker } from 'eth-block-tracker'; - -import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; - -export default function createInfuraClient({ network, projectId }) { - const infuraMiddleware = createInfuraMiddleware({ - network, - projectId, - maxAttempts: 5, - source: 'metamask', - }); - const infuraProvider = providerFromMiddleware(infuraMiddleware); - const blockTracker = new PollingBlockTracker({ provider: infuraProvider }); - - const networkMiddleware = mergeMiddleware([ - createNetworkAndChainIdMiddleware({ network }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: infuraProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), - infuraMiddleware, - ]); - return { networkMiddleware, blockTracker }; -} - -function createNetworkAndChainIdMiddleware({ network }) { - if (!BUILT_IN_NETWORKS[network]) { - throw new Error(`createInfuraClient - unknown network "${network}"`); - } - - const { chainId, networkId } = BUILT_IN_NETWORKS[network]; - - return createScaffoldMiddleware({ - eth_chainId: chainId, - net_version: networkId, - }); -} diff --git a/app/scripts/controllers/network/createInfuraClient.test.js b/app/scripts/controllers/network/createInfuraClient.test.js deleted file mode 100644 index d1b1a7ccf..000000000 --- a/app/scripts/controllers/network/createInfuraClient.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createInfuraClient', () => { - testsForProviderType('infura'); -}); diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js deleted file mode 100644 index b8cf0e7aa..000000000 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ /dev/null @@ -1,61 +0,0 @@ -import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import { - createFetchMiddleware, - createBlockRefRewriteMiddleware, - createBlockCacheMiddleware, - createInflightCacheMiddleware, - createBlockTrackerInspectorMiddleware, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-middleware'; -import { PollingBlockTracker } from 'eth-block-tracker'; -import { SECOND } from '../../../../shared/constants/time'; - -export default function createJsonRpcClient({ rpcUrl, chainId }) { - const blockTrackerOpts = process.env.IN_TEST - ? { pollingInterval: SECOND } - : {}; - const fetchMiddleware = createFetchMiddleware({ rpcUrl }); - const blockProvider = providerFromMiddleware(fetchMiddleware); - const blockTracker = new PollingBlockTracker({ - ...blockTrackerOpts, - provider: blockProvider, - }); - const testMiddlewares = process.env.IN_TEST - ? [createEstimateGasDelayTestMiddleware()] - : []; - - const networkMiddleware = mergeMiddleware([ - ...testMiddlewares, - createChainIdMiddleware(chainId), - createBlockRefRewriteMiddleware({ blockTracker }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), - fetchMiddleware, - ]); - - return { networkMiddleware, blockTracker }; -} - -function createChainIdMiddleware(chainId) { - return (req, res, next, end) => { - if (req.method === 'eth_chainId') { - res.result = chainId; - return end(); - } - return next(); - }; -} - -/** - * For use in tests only. - * Adds a delay to `eth_estimateGas` calls. - */ -function createEstimateGasDelayTestMiddleware() { - return createAsyncMiddleware(async (req, _, next) => { - if (req.method === 'eth_estimateGas') { - await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); - } - return next(); - }); -} diff --git a/app/scripts/controllers/network/createJsonRpcClient.test.js b/app/scripts/controllers/network/createJsonRpcClient.test.js deleted file mode 100644 index 1c3443d25..000000000 --- a/app/scripts/controllers/network/createJsonRpcClient.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createJsonRpcClient', () => { - testsForProviderType('custom'); -}); diff --git a/app/scripts/controllers/network/index.js b/app/scripts/controllers/network/index.js index dbb003ff1..b91e16698 100644 --- a/app/scripts/controllers/network/index.js +++ b/app/scripts/controllers/network/index.js @@ -1 +1 @@ -export { default, NETWORK_EVENTS } from './network-controller'; +export { default, NetworkControllerEventTypes } from './network-controller'; diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js index 6f0e08cbe..4449bc683 100644 --- a/app/scripts/controllers/network/network-controller.js +++ b/app/scripts/controllers/network/network-controller.js @@ -1,15 +1,18 @@ import { strict as assert } from 'assert'; import EventEmitter from 'events'; import { ComposedStore, ObservableStore } from '@metamask/obs-store'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import { providerFromEngine } from '@metamask/eth-json-rpc-middleware'; import log from 'loglevel'; import { createSwappableProxy, createEventEmitterProxy, -} from 'swappable-obj-proxy'; +} from '@metamask/swappable-obj-proxy'; import EthQuery from 'eth-query'; +// ControllerMessenger is referred to in the JSDocs +// eslint-disable-next-line no-unused-vars +import { ControllerMessenger } from '@metamask/base-controller'; import { v4 as random } from 'uuid'; +import { hasProperty, isPlainObject } from '@metamask/utils'; +import { errorCodes } from 'eth-rpc-errors'; import { INFURA_PROVIDER_TYPES, BUILT_IN_NETWORKS, @@ -17,15 +20,14 @@ import { TEST_NETWORK_TICKER_MAP, CHAIN_IDS, NETWORK_TYPES, + NetworkStatus, } from '../../../../shared/constants/network'; -import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import { isPrefixedFormattedHexString, isSafeChainId, } from '../../../../shared/modules/network.utils'; -import { EVENT } from '../../../../shared/constants/metametrics'; -import createInfuraClient from './createInfuraClient'; -import createJsonRpcClient from './createJsonRpcClient'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; +import { createNetworkClient } from './create-network-client'; /** * @typedef {object} NetworkConfiguration @@ -36,91 +38,133 @@ import createJsonRpcClient from './createJsonRpcClient'; * @property {string} [nickname] - Personalized network name. */ -const env = process.env.METAMASK_ENV; -const fetchWithTimeout = getFetchWithTimeout(); +function buildDefaultProviderConfigState() { + if (process.env.IN_TEST) { + return { + type: NETWORK_TYPES.RPC, + rpcUrl: 'http://localhost:8545', + chainId: '0x539', + nickname: 'Localhost 8545', + ticker: 'ETH', + }; + } else if ( + process.env.METAMASK_DEBUG || + process.env.METAMASK_ENV === 'test' + ) { + return { + type: NETWORK_TYPES.GOERLI, + chainId: CHAIN_IDS.GOERLI, + ticker: TEST_NETWORK_TICKER_MAP.GOERLI, + }; + } -let defaultProviderConfigOpts; -if (process.env.IN_TEST) { - defaultProviderConfigOpts = { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - nickname: 'Localhost 8545', - }; -} else if (process.env.METAMASK_DEBUG || env === 'test') { - defaultProviderConfigOpts = { - type: NETWORK_TYPES.GOERLI, - chainId: CHAIN_IDS.GOERLI, - ticker: TEST_NETWORK_TICKER_MAP.GOERLI, - }; -} else { - defaultProviderConfigOpts = { + return { type: NETWORK_TYPES.MAINNET, chainId: CHAIN_IDS.MAINNET, + ticker: 'ETH', }; } -const defaultProviderConfig = { - ticker: 'ETH', - ...defaultProviderConfigOpts, -}; +function buildDefaultNetworkIdState() { + return null; +} -const defaultNetworkDetailsState = { - EIPS: { 1559: undefined }, -}; +function buildDefaultNetworkStatusState() { + return NetworkStatus.Unknown; +} -export const NETWORK_EVENTS = { - // Fired after the actively selected network is changed - NETWORK_DID_CHANGE: 'networkDidChange', - // Fired when the actively selected network *will* change - NETWORK_WILL_CHANGE: 'networkWillChange', - // Fired when Infura returns an error indicating no support - INFURA_IS_BLOCKED: 'infuraIsBlocked', - // Fired when not using an Infura network or when Infura returns no error, indicating support - INFURA_IS_UNBLOCKED: 'infuraIsUnblocked', +function buildDefaultNetworkDetailsState() { + return { + EIPS: { + 1559: undefined, + }, + }; +} + +function buildDefaultNetworkConfigurationsState() { + return {}; +} + +/** + * The name of the controller. + */ +const name = 'NetworkController'; + +/** + * The set of event types that this controller can publish via its messenger. + */ +export const NetworkControllerEventTypes = { + /** + * Fired after the current network is changed. + */ + NetworkDidChange: `${name}:networkDidChange`, + /** + * Fired when there is a request to change the current network, but no state + * changes have occurred yet. + */ + NetworkWillChange: `${name}:networkWillChange`, + /** + * Fired after the network is changed to an Infura network, but when Infura + * returns an error denying support for the user's location. + */ + InfuraIsBlocked: `${name}:infuraIsBlocked`, + /** + * Fired after the network is changed to an Infura network and Infura does not + * return an error denying support for the user's location, or after the + * network is changed to a custom network. + */ + InfuraIsUnblocked: `${name}:infuraIsUnblocked`, }; export default class NetworkController extends EventEmitter { - static defaultProviderConfig = defaultProviderConfig; - /** * Construct a NetworkController. * - * @param {object} [options] - NetworkController options. + * @param {object} options - Options for this controller. + * @param {ControllerMessenger} options.messenger - The controller messenger. * @param {object} [options.state] - Initial controller state. * @param {string} [options.infuraProjectId] - The Infura project ID. * @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController */ - constructor({ state = {}, infuraProjectId, trackMetaMetricsEvent } = {}) { + constructor({ + messenger, + state = {}, + infuraProjectId, + trackMetaMetricsEvent, + } = {}) { super(); + this.messenger = messenger; + // create stores this.providerStore = new ObservableStore( - state.provider || { ...defaultProviderConfig }, + state.provider || buildDefaultProviderConfigState(), ); this.previousProviderStore = new ObservableStore( this.providerStore.getState(), ); - this.networkStore = new ObservableStore('loading'); - // We need to keep track of a few details about the current network - // Ideally we'd merge this.networkStore with this new store, but doing so - // will require a decent sized refactor of how we're accessing network - // state. Currently this is only used for detecting EIP 1559 support but - // can be extended to track other network details. + this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState()); + this.networkStatusStore = new ObservableStore( + buildDefaultNetworkStatusState(), + ); + // We need to keep track of a few details about the current network. + // Ideally we'd merge this.networkStatusStore with this new store, but doing + // so will require a decent sized refactor of how we're accessing network + // state. Currently this is only used for detecting EIP-1559 support but can + // be extended to track other network details. this.networkDetails = new ObservableStore( - state.networkDetails || { - ...defaultNetworkDetailsState, - }, + state.networkDetails || buildDefaultNetworkDetailsState(), ); this.networkConfigurationsStore = new ObservableStore( - state.networkConfigurations || {}, + state.networkConfigurations || buildDefaultNetworkConfigurationsState(), ); this.store = new ComposedStore({ provider: this.providerStore, previousProviderStore: this.previousProviderStore, - network: this.networkStore, + networkId: this.networkIdStore, + networkStatus: this.networkStatusStore, networkDetails: this.networkDetails, networkConfigurations: this.networkConfigurationsStore, }); @@ -137,11 +181,8 @@ export default class NetworkController extends EventEmitter { throw new Error('Invalid Infura project ID'); } this._infuraProjectId = infuraProjectId; - this._trackMetaMetricsEvent = trackMetaMetricsEvent; - this.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - this.lookupNetwork(); - }); + this._trackMetaMetricsEvent = trackMetaMetricsEvent; } /** @@ -167,10 +208,12 @@ export default class NetworkController extends EventEmitter { } /** - * Method to check if the block header contains fields that indicate EIP 1559 - * support (baseFeePerGas). + * Determines whether the network supports EIP-1559 by checking whether the + * latest block has a `baseFeePerGas` property, then updates state + * appropriately. * - * @returns {Promise} true if current network supports EIP 1559 + * @returns {Promise} A promise that resolves to true if the network + * supports EIP-1559 and false otherwise. */ async getEIP1559Compatibility() { const { EIPS } = this.networkDetails.getState(); @@ -179,15 +222,28 @@ export default class NetworkController extends EventEmitter { if (EIPS[1559] !== undefined) { return EIPS[1559]; } - const latestBlock = await this._getLatestBlock(); - const supportsEIP1559 = - latestBlock && latestBlock.baseFeePerGas !== undefined; - this._setNetworkEIPSupport(1559, supportsEIP1559); + const supportsEIP1559 = await this._determineEIP1559Compatibility(); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); return supportsEIP1559; } + /** + * Captures information about the currently selected network — namely, + * the network ID and whether the network supports EIP-1559 — and then uses + * the results of these requests to determine the status of the network. + */ async lookupNetwork() { - // Prevent firing when provider is not defined. + const { chainId, type } = this.providerStore.getState(); + let networkChanged = false; + let networkId; + let supportsEIP1559; + let networkStatus; + if (!this._provider) { log.warn( 'NetworkController - lookupNetwork aborted due to missing provider', @@ -195,46 +251,102 @@ export default class NetworkController extends EventEmitter { return; } - const { chainId } = this.providerStore.getState(); if (!chainId) { log.warn( 'NetworkController - lookupNetwork aborted due to missing chainId', ); - this._setNetworkState('loading'); - this._clearNetworkDetails(); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); return; } - // Ping the RPC endpoint so we can confirm that it works - const initialNetwork = this.networkStore.getState(); - const { type } = this.providerStore.getState(); const isInfura = INFURA_PROVIDER_TYPES.includes(type); - if (isInfura) { - this._checkInfuraAvailability(type); - } else { - this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED); + const listener = () => { + networkChanged = true; + this.messenger.unsubscribe( + NetworkControllerEventTypes.NetworkDidChange, + listener, + ); + }; + this.messenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + listener, + ); + + try { + const results = await Promise.all([ + this._getNetworkId(), + this._determineEIP1559Compatibility(), + ]); + networkId = results[0]; + supportsEIP1559 = results[1]; + networkStatus = NetworkStatus.Available; + } catch (error) { + if (hasProperty(error, 'code')) { + let responseBody; + try { + responseBody = JSON.parse(error.message); + } catch { + // error.message must not be JSON + } + + if ( + isPlainObject(responseBody) && + responseBody.error === INFURA_BLOCKED_KEY + ) { + networkStatus = NetworkStatus.Blocked; + } else if (error.code === errorCodes.rpc.internal) { + networkStatus = NetworkStatus.Unknown; + } else { + networkStatus = NetworkStatus.Unavailable; + } + } else { + log.warn( + 'NetworkController - could not determine network status', + error, + ); + networkStatus = NetworkStatus.Unknown; + } } - let networkVersion; - let networkVersionError; - try { - networkVersion = await this._getNetworkId(); - } catch (error) { - networkVersionError = error; - } - if (initialNetwork !== this.networkStore.getState()) { + if (networkChanged) { + // If the network has changed, then `lookupNetwork` either has been or is + // in the process of being called, so we don't need to go further. return; } + this.messenger.unsubscribe( + NetworkControllerEventTypes.NetworkDidChange, + listener, + ); - if (networkVersionError) { - this._setNetworkState('loading'); - // keep network details in sync with network state - this._clearNetworkDetails(); + this.networkStatusStore.putState(networkStatus); + + if (networkStatus === NetworkStatus.Available) { + this.networkIdStore.putState(networkId); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); } else { - this._setNetworkState(networkVersion); - // look up EIP-1559 support - await this.getEIP1559Compatibility(); + this._resetNetworkId(); + this._resetNetworkDetails(); + } + + if (isInfura) { + if (networkStatus === NetworkStatus.Available) { + this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); + } else if (networkStatus === NetworkStatus.Blocked) { + this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked); + } + } else { + // Always publish infuraIsUnblocked regardless of network status to + // prevent consumers from being stuck in a blocked state if they were + // previously connected to an Infura network that was blocked + this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); } } @@ -289,7 +401,7 @@ export default class NetworkController extends EventEmitter { rollbackToPreviousProvider() { const config = this.previousProviderStore.getState(); - this.providerStore.updateState(config); + this.providerStore.putState(config); this._switchNetwork(config); } @@ -297,13 +409,38 @@ export default class NetworkController extends EventEmitter { // Private // + /** + * Method to return the latest block for the current network + * + * @returns {object} Block header + */ + _getLatestBlock() { + const { provider } = this.getProviderAndBlockTracker(); + const ethQuery = new EthQuery(provider); + + return new Promise((resolve, reject) => { + ethQuery.sendAsync( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }, + ); + }); + } + /** * Get the network ID for the current selected network * * @returns {string} The network ID for the current network. */ async _getNetworkId() { - const ethQuery = new EthQuery(this._provider); + const { provider } = this.getProviderAndBlockTracker(); + const ethQuery = new EthQuery(provider); + return await new Promise((resolve, reject) => { ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { if (error) { @@ -316,49 +453,24 @@ export default class NetworkController extends EventEmitter { } /** - * Method to return the latest block for the current network - * - * @returns {object} Block header + * Clears the stored network ID. */ - _getLatestBlock() { - return new Promise((resolve, reject) => { - const { provider } = this.getProviderAndBlockTracker(); - const ethQuery = new EthQuery(provider); - ethQuery.sendAsync( - { method: 'eth_getBlockByNumber', params: ['latest', false] }, - (err, block) => { - if (err) { - return reject(err); - } - return resolve(block); - }, - ); - }); - } - - _setNetworkState(network) { - this.networkStore.putState(network); + _resetNetworkId() { + this.networkIdStore.putState(buildDefaultNetworkIdState()); } /** - * Set EIP support indication in the networkDetails store - * - * @param {number} EIPNumber - The number of the EIP to mark support for - * @param {boolean} isSupported - True if the EIP is supported + * Resets network status to the default ("unknown"). */ - _setNetworkEIPSupport(EIPNumber, isSupported) { - this.networkDetails.updateState({ - EIPS: { - [EIPNumber]: isSupported, - }, - }); + _resetNetworkStatus() { + this.networkStatusStore.putState(buildDefaultNetworkStatusState()); } /** - * Reset EIP support to default (no support) + * Clears details previously stored for the network. */ - _clearNetworkDetails() { - this.networkDetails.putState({ ...defaultNetworkDetailsState }); + _resetNetworkDetails() { + this.networkDetails.putState(buildDefaultNetworkDetailsState()); } /** @@ -367,68 +479,42 @@ export default class NetworkController extends EventEmitter { * @param config */ _setProviderConfig(config) { - this.previousProviderStore.updateState(this.providerStore.getState()); - this.providerStore.updateState(config); + this.previousProviderStore.putState(this.providerStore.getState()); + this.providerStore.putState(config); this._switchNetwork(config); } - async _checkInfuraAvailability(network) { - const rpcUrl = `https://${network}.infura.io/v3/${this._infuraProjectId}`; - - let networkChanged = false; - this.once(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - networkChanged = true; - }); - - try { - const response = await fetchWithTimeout(rpcUrl, { - method: 'POST', - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_blockNumber', - params: [], - id: 1, - }), - }); - - if (networkChanged) { - return; - } - - if (response.ok) { - this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED); - } else { - const responseMessage = await response.json(); - if (networkChanged) { - return; - } - if (responseMessage.error === INFURA_BLOCKED_KEY) { - this.emit(NETWORK_EVENTS.INFURA_IS_BLOCKED); - } - } - } catch (err) { - log.warn(`MetaMask - Infura availability check failed`, err); - } + /** + * Retrieves the latest block from the currently selected network; if the + * block has a `baseFeePerGas` property, then we know that the network + * supports EIP-1559; otherwise it doesn't. + * + * @returns {Promise} A promise that resolves to true if the network + * supports EIP-1559 and false otherwise. + */ + async _determineEIP1559Compatibility() { + const latestBlock = await this._getLatestBlock(); + return latestBlock && latestBlock.baseFeePerGas !== undefined; } _switchNetwork(opts) { - // Indicate to subscribers that network is about to change - this.emit(NETWORK_EVENTS.NETWORK_WILL_CHANGE); - // Set loading state - this._setNetworkState('loading'); - // Reset network details - this._clearNetworkDetails(); - // Configure the provider appropriately + this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); this._configureProvider(opts); - // Notify subscribers that network has changed - this.emit(NETWORK_EVENTS.NETWORK_DID_CHANGE, opts.type); + this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange); + this.lookupNetwork(); } _configureProvider({ type, rpcUrl, chainId }) { // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type); if (isInfura) { - this._configureInfuraProvider(type, this._infuraProjectId); + this._configureInfuraProvider({ + type, + infuraProjectId: this._infuraProjectId, + }); // url-based rpc endpoints } else if (type === NETWORK_TYPES.RPC) { this._configureStandardProvider(rpcUrl, chainId); @@ -439,25 +525,23 @@ export default class NetworkController extends EventEmitter { } } - _configureInfuraProvider(type, projectId) { + _configureInfuraProvider({ type, infuraProjectId }) { log.info('NetworkController - configureInfuraProvider', type); - const networkClient = createInfuraClient({ + const { provider, blockTracker } = createNetworkClient({ network: type, - projectId, + infuraProjectId, + type: 'infura', }); - this._setNetworkClient(networkClient); + this._setProviderAndBlockTracker({ provider, blockTracker }); } _configureStandardProvider(rpcUrl, chainId) { log.info('NetworkController - configureStandardProvider', rpcUrl); - const networkClient = createJsonRpcClient({ rpcUrl, chainId }); - this._setNetworkClient(networkClient); - } - - _setNetworkClient({ networkMiddleware, blockTracker }) { - const engine = new JsonRpcEngine(); - engine.push(networkMiddleware); - const provider = providerFromEngine(engine); + const { provider, blockTracker } = createNetworkClient({ + chainId, + rpcUrl, + type: 'custom', + }); this._setProviderAndBlockTracker({ provider, blockTracker }); } @@ -550,7 +634,7 @@ export default class NetworkController extends EventEmitter { )?.id; const newNetworkConfigurationId = oldNetworkConfigurationId || random(); - this.networkConfigurationsStore.updateState({ + this.networkConfigurationsStore.putState({ ...networkConfigurations, [newNetworkConfigurationId]: { ...newNetworkConfiguration, @@ -561,7 +645,7 @@ export default class NetworkController extends EventEmitter { if (!oldNetworkConfigurationId) { this._trackMetaMetricsEvent({ event: 'Custom Network Added', - category: EVENT.CATEGORIES.NETWORK, + category: MetaMetricsEventCategory.Network, referrer: { url: referrer, }, diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index 8c887cb56..a58c563a8 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -3,18 +3,11 @@ import { isMatch } from 'lodash'; import { v4 } from 'uuid'; import nock from 'nock'; import sinon from 'sinon'; -import * as ethJsonRpcMiddlewareModule from '@metamask/eth-json-rpc-middleware'; +import { ControllerMessenger } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { MetaMetricsNetworkEventSource } from '../../../../shared/constants/metametrics'; import NetworkController from './network-controller'; -jest.mock('@metamask/eth-json-rpc-middleware', () => { - return { - __esModule: true, - ...jest.requireActual('@metamask/eth-json-rpc-middleware'), - }; -}); - jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -58,35 +51,7 @@ const originalSetTimeout = global.setTimeout; * `baseFeePerGas` property). */ const PRE_1559_BLOCK = { - difficulty: '0x0', - extraData: '0x', - gasLimit: '0x1c9c380', - gasUsed: '0x598c9b', - hash: '0xfb2086eb924ffce4061f94c3b65f303e0351f8e7deff185fe1f5e9001ff96f63', - logsBloom: - '0x7034820113921800018e8070900006316040002225c04a0624110010841018a2109040401004112a4c120f00220a2119020000714b143a04004106120130a8450080433129401068ed22000a54a48221a1020202524204045421b883882530009a1800b08a1309408008828403010d530440001a40003c0006240291008c0404c211610c690b00f1985e000009c02503240040010989c01cf2806840043815498e90012103e06084051542c0094002494008044c24a0a13281e0009601481073010800130402464202212202a8088210442a8ec81b080430075629e60a00a082005a3988400940a4009012a204011a0018a00903222a60420428888144210802', - miner: '0xffee087852cb4898e6c3532e776e68bc68b1143b', - mixHash: '0xb17ba50cd7261e77a213fb75704dcfd8a28fbcd78d100691a112b7cc2893efa2', - nonce: '0x0000000000000000', - number: '0x2', // number set to "2" to simplify tests - parentHash: - '0x31406d1bf1a2ca12371ce5b3ecb20568d6a8b9bf05b49b71b93ba33f317d5a82', - receiptsRoot: - '0x5ba97ece1afbac2a8fe0344f9022fe808342179b26ea3ecc2e0b8c4b46b7f8cd', - sha3Uncles: - '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - size: '0x70f4', - stateRoot: - '0x36bfb7ca106d41c4458292669126e091011031c5af612dee1c2e6424ef92b080', - timestamp: '0x639b6d9b', - totalDifficulty: '0xc70d815d562d3cfa955', - transactions: [ - // reduced to a single transaction to make fixture less verbose - '0x2761e939dc822f64141bd00bc7ef8cee16201af10e862469212396664cee81ce', - ], - transactionsRoot: - '0x98bbdfbe1074bc3aa72a77a281f16d6ba7e723d68f15937d80954fb34d323369', - uncles: [], + number: '0x42', }; /** @@ -111,16 +76,6 @@ const BLOCK = POST_1559_BLOCK; */ const DEFAULT_INFURA_PROJECT_ID = 'fake-infura-project-id'; -/** - * Despite the signature of its constructor, NetworkController must take an - * Infura project ID. This object is mixed into the options first when a - * NetworkController is instantiated in tests. - */ -const DEFAULT_CONTROLLER_OPTIONS = { - infuraProjectId: DEFAULT_INFURA_PROJECT_ID, - trackMetaMetricsEvent: jest.fn(), -}; - /** * The set of properties allowed in a valid JSON-RPC response object. */ @@ -132,28 +87,62 @@ const JSONRPC_RESPONSE_BODY_PROPERTIES = ['id', 'jsonrpc', 'result', 'error']; */ const INFURA_NETWORKS = [ { - nickname: 'Mainnet', networkType: 'mainnet', chainId: '0x1', - networkVersion: '1', + networkId: '1', ticker: 'ETH', }, { - nickname: 'Goerli', networkType: 'goerli', chainId: '0x5', - networkVersion: '5', + networkId: '5', ticker: 'GoerliETH', }, { - nickname: 'Sepolia', networkType: 'sepolia', chainId: '0xaa36a7', - networkVersion: '11155111', + networkId: '11155111', ticker: 'SepoliaETH', }, ]; +/** + * A response object for a successful request to `eth_getBlockByNumber`. It is + * assumed that the block number here is insignificant to the test. + */ +const SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE = { + result: BLOCK, + error: null, +}; + +/** + * A response object for a request that has been geoblocked by Infura. + */ +const BLOCKED_INFURA_RESPONSE = { + result: null, + error: 'countryBlocked', + httpStatus: 500, +}; + +/** + * A response object for a successful request to `net_version`. It is assumed + * that the network ID here is insignificant to the test. + */ +const SUCCESSFUL_NET_VERSION_RESPONSE = { + result: '42', + error: null, +}; + +/** + * A response object for a unsuccessful request to any RPC method. It is assumed + * that the error here is insignificant to the test. + */ +const UNSUCCESSFUL_JSON_RPC_RESPONSE = { + result: null, + error: 'oops', + httpStatus: 500, +}; + /** * Handles mocking provider requests for a particular network. */ @@ -192,6 +181,9 @@ class NetworkCommunications { customRpcUrl, }; this.networkClientType = networkClientType; + if (networkClientType !== 'infura' && networkClientType !== 'custom') { + throw new Error("networkClientType must be 'infura' or 'custom'"); + } this.#networkClientOptions = networkClientOptions; this.infuraProjectId = infuraProjectId; const rpcUrl = @@ -248,23 +240,6 @@ class NetworkCommunications { } const defaultMocksByRpcMethod = { - eth_blockNumber: { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: latestBlockNumber, - }, - // When the provider is configured for an Infura network, - // NetworkController makes a sentinel request for `eth_blockNumber`, so - // we ensure that it is mocked by default. Conversely, when the provider - // is configured for a custom RPC endpoint, we don't mock - // `eth_blockNumber` at all unless specified. Admittedly, this is a bit - // magical, but it saves us from having to think about this in tests - // if we don't have to. - times: this.networkClientType === 'infura' ? 1 : 0, - }, eth_getBlockByNumber: { request: { method: 'eth_getBlockByNumber', @@ -283,11 +258,29 @@ class NetworkCommunications { result: '1', }, }, + // The request that the block tracker makes always occurs after any + // request that the network controller makes (because such a request goes + // through the block cache middleware and that is what spawns the block + // tracker). + eth_blockNumber: { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: latestBlockNumber, + }, + // If there is no latest block number then the request that spawned the + // block tracker won't be cached inside of the block tracker, so the + // block tracker makes another request when it is asked for the latest + // block. + times: latestBlock === null ? 2 : 1, + }, }; const providedMocksByRpcMethod = { - eth_blockNumber: ethBlockNumberMocks, eth_getBlockByNumber: ethGetBlockByNumberMocks, net_version: netVersionMocks, + eth_blockNumber: ethBlockNumberMocks, }; const allMocks = []; @@ -307,23 +300,6 @@ class NetworkCommunications { } }); - // The request that the block tracker makes always occurs after any request - // that the network controller makes (because such a request goes through - // the block cache middleware and that is what spawns the block tracker). We - // don't need to customize the block tracker request; we just need to ensure - // that the block number it returns matches the same block number that - // `eth_getBlockByNumber` uses. - allMocks.push({ - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: latestBlockNumber, - }, - times: latestBlock === null ? 2 : 1, - }); - allMocks.forEach((mock) => { this.mockRpcCall(mock); }); @@ -457,34 +433,33 @@ describe('NetworkController', () => { }); it('accepts initial state', async () => { - const exampleInitialState = { - provider: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', - nickname: 'Test initial state', - }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, - }; - await withController( { - state: exampleInitialState, + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + chainId: '0x9999', + nickname: 'Test initial state', + }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, + }, }, ({ controller }) => { expect(controller.store.getState()).toMatchInlineSnapshot(` { - "network": "loading", "networkConfigurations": {}, "networkDetails": { "EIPS": { "1559": false, }, }, + "networkId": null, + "networkStatus": "unknown", "previousProviderStore": { "chainId": "0x9999", "nickname": "Test initial state", @@ -507,13 +482,14 @@ describe('NetworkController', () => { await withController(({ controller }) => { expect(controller.store.getState()).toMatchInlineSnapshot(` { - "network": "loading", "networkConfigurations": {}, "networkDetails": { "EIPS": { "1559": undefined, }, }, + "networkId": null, + "networkStatus": "unknown", "previousProviderStore": { "chainId": "0x539", "nickname": "Localhost 8545", @@ -536,7 +512,9 @@ describe('NetworkController', () => { describe('destroy', () => { it('does not throw if called before the provider is initialized', async () => { - const controller = new NetworkController(DEFAULT_CONTROLLER_OPTIONS); + const controller = new NetworkController( + buildDefaultNetworkControllerOptions(), + ); expect(await controller.destroy()).toBeUndefined(); }); @@ -545,7 +523,7 @@ describe('NetworkController', () => { await withController(async ({ controller, network }) => { network.mockEssentialRpcCalls({ eth_blockNumber: { - times: 1, + times: 2, }, }); await controller.initializeProvider(); @@ -554,7 +532,7 @@ describe('NetworkController', () => { blockTracker.addListener('latest', () => { // do nothing }); - expect(blockTracker.isRunning()).toBe(true); + expect(blockTracker.isRunning()).toBeTruthy(); await controller.destroy(); @@ -582,14 +560,9 @@ describe('NetworkController', () => { ); }); - for (const { - nickname, - networkType, - chainId, - networkVersion, - } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { - it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController( { state: { @@ -618,9 +591,12 @@ describe('NetworkController', () => { ); }); - it('emits infuraIsUnblocked (assuming that the request to eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -633,20 +609,20 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', operation: async () => { await controller.initializeProvider(); }, }); - expect(infuraIsUnblocked).toBe(true); + expect(infuraIsUnblocked).toBeTruthy(); }, ); }); - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { + it('determines the status of the network, storing it in state', async () => { await withController( { state: { @@ -660,15 +636,18 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls(); + expect(controller.store.getState().networkStatus).toBe('unknown'); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); }, ); }); - it(`persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)`, async () => { + it('determines whether the network supports EIP-1559 and stores the result in state without overwriting other state in the networkDetails store', async () => { await withController( { state: { @@ -678,9 +657,10 @@ describe('NetworkController', () => { // of the network selected, it just needs to exist chainId: '0x9999999', }, - }, - networkDetails: { - EIPS: {}, + networkDetails: { + EIPS: {}, + other: 'details', + }, }, }, async ({ controller, network }) => { @@ -690,9 +670,12 @@ describe('NetworkController', () => { await controller.initializeProvider(); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + other: 'details', + }); }, ); }); @@ -755,8 +738,11 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -778,20 +764,61 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', operation: async () => { await controller.initializeProvider(); }, }); - expect(infuraIsUnblocked).toBe(true); + expect(infuraIsUnblocked).toBeTruthy(); }, ); }); - it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + networkConfigurations: { + testNetworkConfigurationId: { + rpcUrl: 'https://mock-rpc-url', + chainId: '0xtest', + ticker: 'TEST', + id: 'testNetworkConfigurationId', + }, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('determines the status of the network, storing it in state', async () => { await withController( { state: { @@ -815,20 +842,19 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); + expect(controller.store.getState().networkStatus).toBe('unknown'); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('available'); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('determines whether the network supports EIP-1559, storing it in state', async () => { await withController( { state: { @@ -847,6 +873,10 @@ describe('NetworkController', () => { id: 'testNetworkConfigurationId', }, }, + networkDetails: { + EIPS: {}, + other: 'details', + }, }, }, async ({ controller, network }) => { @@ -856,9 +886,12 @@ describe('NetworkController', () => { await controller.initializeProvider(); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + other: 'details', + }); }, ); }); @@ -889,9 +922,9 @@ describe('NetworkController', () => { }); }); - for (const { nickname, networkType, chainId } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is changed to "${networkType}"`, () => { - it(`returns a provider object that was pointed to another network before the switch and is pointed to ${nickname} afterward`, async () => { + it(`returns a provider object that was pointed to another network before the switch and is pointed to "${networkType}" afterward`, async () => { await withController( { state: { @@ -899,25 +932,18 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', - }, - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, }, }, }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: networkType, + }, + }); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); @@ -954,15 +980,21 @@ describe('NetworkController', () => { networkConfigurations: { testNetworkConfigurationId: { rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', + chainId: '0x1337', id: 'testNetworkConfigurationId', }, }, }, }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); @@ -981,7 +1013,7 @@ describe('NetworkController', () => { const { result: newChainIdResult } = await promisifiedSendAsync2({ method: 'eth_chainId', }); - expect(newChainIdResult).toBe('0xtest'); + expect(newChainIdResult).toBe('0x1337'); }, ); }); @@ -990,7 +1022,7 @@ describe('NetworkController', () => { describe('getEIP1559Compatibility', () => { describe('when the latest block has a baseFeePerGas property', () => { - it('persists to state that the network supports EIP-1559', async () => { + it('stores the fact that the network supports EIP-1559', async () => { await withController( { state: { @@ -1023,13 +1055,13 @@ describe('NetworkController', () => { const supportsEIP1559 = await controller.getEIP1559Compatibility(); - expect(supportsEIP1559).toBe(true); + expect(supportsEIP1559).toBeTruthy(); }); }); }); describe('when the latest block does not have a baseFeePerGas property', () => { - it('persists to state that the network does not support EIP-1559', async () => { + it('stores the fact that the network does not support EIP-1559', async () => { await withController( { state: { @@ -1068,7 +1100,7 @@ describe('NetworkController', () => { }); describe('when the request for the latest block responds with null', () => { - it('persists null to state as whether the network supports EIP-1559', async () => { + it('stores null as whether the network supports EIP-1559', async () => { await withController( { state: { @@ -1108,8 +1140,11 @@ describe('NetworkController', () => { it('does not make multiple requests to eth_getBlockByNumber when called multiple times and the request to eth_getBlockByNumber succeeded the first time', async () => { await withController(async ({ controller, network }) => { - // This mocks eth_getBlockByNumber once by default - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + times: 1, + }, + }); await withoutCallingGetEIP1559Compatibility({ controller, operation: async () => { @@ -1120,7 +1155,7 @@ describe('NetworkController', () => { await controller.getEIP1559Compatibility(); await controller.getEIP1559Compatibility(); - expect(network.nockScope.isDone()).toBe(true); + expect(network.nockScope.isDone()).toBeTruthy(); }); }); }); @@ -1161,42 +1196,354 @@ describe('NetworkController', () => { }); it('does not emit infuraIsUnblocked', async () => { - await withController(async ({ controller, network }) => { + const messenger = buildMessenger(); + + await withController({ messenger }, async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const promiseForInfuraIsUnblocked = waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, operation: async () => { await controller.lookupNetwork(); }, }); - await expect(promiseForInfuraIsUnblocked).toNeverResolve(); + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); }); }); it('does not emit infuraIsBlocked', async () => { - await withController(async ({ controller, network }) => { + const messenger = buildMessenger(); + + await withController({ messenger }, async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const promiseForInfuraIsBlocked = waitForEvent({ - controller, - eventName: 'infuraIsBlocked', + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, operation: async () => { await controller.lookupNetwork(); }, }); - await expect(promiseForInfuraIsBlocked).toNeverResolve(); + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); }); }); }); - for (const { nickname, networkType, networkVersion } of INFURA_NETWORKS) { + describe('if the provider has initialized, but the current network has no chainId', () => { + it('does not update state in any way', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); + const stateAfterInitialization = controller.store.getState(); + + await controller.lookupNetwork(); + + expect(controller.store.getState()).toStrictEqual( + stateAfterInitialization, + ); + }, + ); + }); + + it('does not emit infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); + + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); + }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'http://example-custom-rpc.metamask.io', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); + await controller.initializeProvider(); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + }); + + INFURA_NETWORKS.forEach(({ networkType, networkId }) => { describe(`when the type in the provider configuration is "${networkType}"`, () => { - describe('if the request for eth_blockNumber responds successfully', () => { - it('emits infuraIsUnblocked as long as the network has not changed by the time the request ends', async () => { + describe('if the request for eth_getBlockByNumber responds successfully', () => { + it('stores the fact that the network is available', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + }, + ); + }); + + it('stores the ID of the network', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkId).toBe(networkId); + }, + ); + }); + + it('stores the fact that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: {}, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBeTruthy(); + }, + ); + }); + + it('stores the fact that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: {}, + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBe(false); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for eth_blockNumber responds with a "countryBlocked" error', () => { + it('stores the fact that the network is blocked', async () => { await withController( { state: { @@ -1210,9 +1557,781 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ - eth_blockNumber: { + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkStatus).toBe( + 'blocked', + ); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // Ensure that each call to eth_blockNumber returns a + // different block number, otherwise the first + // eth_getBlockByNumber response will get cached under the + // first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: BLOCKED_INFURA_RESPONSE, + }, + ], + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe(networkId); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect( + controller.store.getState().networkDetails, + ).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsBlocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsBlocked).toBeTruthy(); + }, + ); + }); + + it('does not emit infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for eth_getBlockByNumber responds with a generic error', () => { + it('stores the network status as unavailable if the error does not translate to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: { + error: 'some error', + httpStatus: 405, + }, + }, + ], + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unavailable', + ); + }, + ); + }); + + it('stores the network status as unknown if the error translates to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: { + error: 'some error', + httpStatus: 500, + }, + }, + ], + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + // Ensure that each call to eth_blockNumber returns a + // different block number, otherwise the first + // eth_getBlockByNumber response will get cached under the + // first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe(networkId); + + // Advance block tracker loop to force a fresh call to + // eth_getBlockByNumber + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + const intentionalErrorMessage = + 'intentional error from eth_getBlockByNumber'; + + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + try { + await controller.lookupNetwork(); + } catch (error) { + if (error !== intentionalErrorMessage) { + console.error(error); + } + } + }, + }); + expect( + controller.store.getState().networkDetails, + ).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); + }, + ); + }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsBlockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('does not emit infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); + }); + }); + + describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: { + result: { + ...BLOCK, + number: '0x1', + }, + }, + }, + { + request: { + params: ['0x2', false], + }, + response: { + result: { + ...BLOCK, + number: '0x2', + }, + }, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); + }, + }); + }, + }, + ], + }); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls({ + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); + }, + ); + }); + + it('stores the ID of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); + }, + }); + }, + net_version: { + response: { + result: '111', + }, + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls({ + net_version: { response: { - result: '0x42', + result: '222', }, }, }); @@ -1223,20 +2342,92 @@ describe('NetworkController', () => { }, }); - const infuraIsUnblocked = await waitForEvent({ + await waitForStateChanges({ controller, - eventName: 'infuraIsUnblocked', + propertyPath: ['networkId'], operation: async () => { await controller.lookupNetwork(); }, }); - expect(infuraIsUnblocked).toBe(true); + expect(controller.store.getState().networkId).toBe('222'); }, ); }); - it('does not emit infuraIsUnblocked if the network has changed by the time the request ends', async () => { + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }, + networkConfigurations: { + testNetworkConfigurationId: { + id: 'testNetworkConfigurationId', + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork( + 'testNetworkConfigurationId', + ); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + network2.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails, + ).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { const anotherNetwork = INFURA_NETWORKS.find( (network) => network.networkType !== networkType, ); @@ -1247,8 +2438,11 @@ describe('NetworkController', () => { ); } + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -1260,17 +2454,15 @@ describe('NetworkController', () => { }, async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - result: '0x42', - }, + eth_getBlockByNumber: { beforeCompleting: async () => { - await waitForEvent({ - controller, - eventName: 'networkDidChange', + await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: async () => { - await withoutCallingLookupNetwork({ + await waitForStateChanges({ controller, + propertyPath: ['networkStatus'], operation: () => { controller.setProviderType( anotherNetwork.networkType, @@ -1283,695 +2475,105 @@ describe('NetworkController', () => { }, }); const network2 = network1.with({ + networkClientType: 'infura', networkClientOptions: { infuraNetwork: anotherNetwork.networkType, }, }); - network2.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const promiseForInfuraIsUnblocked = waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsUnblocked).toNeverResolve(); - }, - ); - }); - }); - - describe('if the request for eth_blockNumber responds with a "countryBlocked" error', () => { - it('emits infuraIsBlocked as long as the network has not changed by the time the request ends', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - httpStatus: 500, - error: 'countryBlocked', - }, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const infuraIsBlocked = await waitForEvent({ - controller, - eventName: 'infuraIsBlocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(infuraIsBlocked).toBe(true); - }, - ); - }); - - it('does not emit infuraIsBlocked if the network has changed by the time the request ends', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - }, - }, - }, - }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - httpStatus: 500, - error: 'countryBlocked', - }, - beforeCompleting: async () => { - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await waitForEvent({ - controller, - eventName: 'networkDidChange', - operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }); - }, - }); - }, - }, - }); - const network2 = new NetworkCommunications({ - networkClientType: 'rpc', - networkClientOptions: { - customRpcUrl: 'http://some-rpc-url', - }, - }); - network2.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const promiseForInfuraIsBlocked = waitForEvent({ - controller, - eventName: 'infuraIsBlocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsBlocked).toNeverResolve(); - }, - ); - }); - }); - - describe('if the request for eth_blockNumber responds with a generic error', () => { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID - // of the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_blockNumber: { - response: { - httpStatus: 500, - error: 'oops', - }, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const promiseForInfuraIsUnblocked = waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsUnblocked).toNeverResolve(); - }, - ); - }); - }); - - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().network).toBe(networkVersion); - }, - ); - }); - - it(`does not update the network state if it is already set to "${networkVersion}"`, async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_blockNumber: { - times: 2, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - const promiseForStateChanges = waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForStateChanges).toNeverResolve(); - }, - ); - }); - - describe('if the request for eth_getBlockByNumber responds successfully', () => { - it('persists to state that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - }); - await controller.initializeProvider(); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(true); - }, - ); - }); - - it('persists to state that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: PRE_1559_BLOCK, - }); - await controller.initializeProvider(); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(false); - }, - ); - }); - }); - - describe('if the request for eth_getBlockByNumber responds with an error', () => { - it('does not update the network details in any way', async () => { - const intentionalErrorMessage = - 'intentional error from eth_getBlockByNumber'; - - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ + network2.mockEssentialRpcCalls({ eth_getBlockByNumber: { - response: { - error: intentionalErrorMessage, - }, + response: BLOCKED_INFURA_RESPONSE, }, }); - await withoutCallingGetEIP1559Compatibility({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - count: 0, - operation: async () => { - try { - await controller.getEIP1559Compatibility(); - } catch (error) { - if (error !== intentionalErrorMessage) { - console.error(error); - } - } - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - }, - ); - }); - }); - - describe('if the network was switched after the net_version request started but before it completed', () => { - it(`persists to state the network version of the newly switched network, not the initial one for ${nickname}`, async () => { - const oldNetworkVersion = networkVersion; - const newChainName = 'goerli'; - const newNetworkVersion = '5'; - - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller }) => { - let netVersionCallCount = 0; - - const fakeProviders = [ - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - netVersionCallCount += 1; - if (netVersionCallCount === 1) { - waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: () => { - controller.setProviderType(newChainName); - }, - }) - .then(() => { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: oldNetworkVersion, - }); - }) - .catch((error) => { - throw error; - }); - return; - } - - throw new Error( - "net_version shouldn't be called more than once", - ); - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: newNetworkVersion, - }); - return; - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - ]; - jest - .spyOn(ethJsonRpcMiddlewareModule, 'providerFromEngine') - .mockImplementationOnce(() => fakeProviders[0]) - .mockImplementationOnce(() => fakeProviders[1]); await withoutCallingLookupNetwork({ controller, operation: async () => { await controller.initializeProvider(); }, }); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', }); - expect(controller.store.getState().network).toBe( - newNetworkVersion, - ); - }, - ); - }); - - it(`persists to state the EIP-1559 support for the newly switched network, not the initial one for ${nickname}`, async () => { - const oldNetworkVersion = networkVersion; - const newChainName = 'goerli'; - const newNetworkVersion = '5'; - - await withController( - { - state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - }, - }, - async ({ controller }) => { - let netVersionCallCount = 0; - - const fakeProviders = [ - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - netVersionCallCount += 1; - if (netVersionCallCount === 1) { - waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: () => { - controller.setProviderType(newChainName); - }, - }) - .then(() => { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: oldNetworkVersion, - }); - }) - .catch((error) => { - throw error; - }); - return; - } - - throw new Error( - "net_version shouldn't be called more than once", - ); - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: POST_1559_BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - { - sendAsync(request, callback) { - if (request.method === 'net_version') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: newNetworkVersion, - }); - return; - } - - if (request.method === 'eth_getBlockByNumber') { - callback(null, { - id: request.id, - jsonrpc: '2.0', - result: PRE_1559_BLOCK, - }); - return; - } - - throw new Error( - `Mock not found for method ${request.method}`, - ); - }, - }, - ]; - jest - .spyOn(ethJsonRpcMiddlewareModule, 'providerFromEngine') - .mockImplementationOnce(() => fakeProviders[0]) - .mockImplementationOnce(() => fakeProviders[1]); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); - }, - ); - }); - }); - }); - } - - describe(`when the type in the provider configuration is "rpc"`, () => { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls(); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: async () => { await controller.lookupNetwork(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, - }); - - expect(infuraIsUnblocked).toBe(true); - }, - ); + ); + }); + }); }); + }); - describe('if the request for net_version responds successfully', () => { - it('persists the network version to state', async () => { + describe('when the type in the provider configuration is "rpc"', () => { + describe('if both net_version and eth_getBlockByNumber respond successfully', () => { + it('stores the fact the network is available', async () => { await withController( { state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, + chainId: '0x1337', }, }, }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + eth_getBlockByNumber: { + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + }, + ); + }); + + it('stores the ID of the network', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, net_version: { response: { result: '42', @@ -1984,48 +2586,39 @@ describe('NetworkController', () => { await controller.initializeProvider(); }, }); + expect(controller.store.getState().networkId).toBe(null); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkId'], operation: async () => { await controller.lookupNetwork(); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkId).toBe('42'); }, ); }); - it('does not persist the result of net_version if it matches what is already in state', async () => { + it('stores the fact that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { await withController( { state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, + chainId: '0x1337', }, }, }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, net_version: { - times: 2, - }, - eth_blockNumber: { - times: 2, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); await withoutCallingLookupNetwork({ @@ -2035,295 +2628,6 @@ describe('NetworkController', () => { }, }); - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - const promiseForStateChanges = waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForStateChanges).toNeverResolve(); - }, - ); - }); - - describe('if the request for eth_getBlockByNumber responds successfully', () => { - it('persists to state that the network supports EIP-1559 when baseFeePerGas is in the block header', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await controller.lookupNetwork(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(true); - }, - ); - }); - - it('persists to state that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'TEST2', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'TEST2', - id: 'testNetworkConfigurationId', - }, - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: PRE_1559_BLOCK, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - - await controller.lookupNetwork(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(false); - }, - ); - }); - }); - - describe('if the request for eth_getBlockByNumber responds with an error', () => { - it('does not update the network details in any way', async () => { - const intentionalErrorMessage = - 'intentional error from eth_getBlockByNumber'; - - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - eth_getBlockByNumber: { - response: { - error: intentionalErrorMessage, - }, - }, - }); - await withoutCallingLookupNetwork({ - controller, - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - count: 0, - operation: async () => { - try { - await controller.lookupNetwork(); - } catch (error) { - if ( - !('data' in error) || - error.data !== intentionalErrorMessage - ) { - console.error(error); - } - } - }, - }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); - }, - ); - }); - }); - }); - - describe('if the request for net_version responds with an error', () => { - it('resets the network status to "loading"', async () => { - const intentionalErrorMessage = 'intentional error from net_version'; - - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - net_version: [ - { - response: { - result: '42', - }, - }, - { - response: { - error: intentionalErrorMessage, - }, - }, - ], - }); - await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('42'); - - await waitForStateChanges({ - controller, - propertyPath: ['network'], - operation: async () => { - try { - await controller.lookupNetwork(); - } catch (error) { - if (error !== intentionalErrorMessage) { - console.error(error); - } - } - }, - }); - - expect(controller.store.getState().network).toBe('loading'); - }, - ); - }); - - it('removes from state whether the network supports EIP-1559', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - networkDetails: { - EIPS: {}, - }, - }, - }, - async ({ controller, network }) => { - network.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - net_version: [ - { - response: { - result: '42', - }, - }, - { - response: { - error: 'oops', - }, - }, - ], - }); - await controller.initializeProvider(); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - await waitForStateChanges({ controller, propertyPath: ['networkDetails'], @@ -2332,6 +2636,306 @@ describe('NetworkController', () => { }, }); + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBeTruthy(); + }, + ); + }); + + it('stores the fact that the network does not support EIP-1559 when baseFeePerGas is not in the block header', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: PRE_1559_BLOCK, + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect( + controller.store.getState().networkDetails.EIPS[1559], + ).toBe(false); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: PRE_1559_BLOCK, + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for eth_getBlockByNumber responds successfully, but the request for net_version responds with a generic error', () => { + it('stores the network status as available if the error does not translate to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: [ + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + { + response: { + error: 'some error', + httpStatus: 405, + }, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unavailable', + ); + }, + ); + }); + + it('stores the network status as unknown if the error translates to an internal RPC error', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: [ + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: [ + { + response: { + result: '42', + }, + }, + { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe('42'); + + // Advance block tracker loop to force a fresh call to + // eth_getBlockByNumber + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); expect(controller.store.getState().networkDetails).toStrictEqual({ EIPS: { 1559: undefined, @@ -2340,10 +2944,467 @@ describe('NetworkController', () => { }, ); }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the request for net_version responds successfully, but the request for eth_getBlockByNumber responds with a generic error', () => { + it('stores the fact that the network is unavailable', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, + }, + { + request: { + params: ['0x2', false], + }, + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('clears the ID of the network from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + latestBlock: BLOCK, + net_version: { + response: { + result: '42', + }, + }, + eth_getBlockByNumber: [ + { + response: { + result: BLOCK, + }, + }, + { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + ], + eth_blockNumber: { + times: 2, + }, + }); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkId).toBe('42'); + + // Advance block tracker loop to force a fresh call to + // eth_getBlockByNumber + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkId).toBeNull(); + }, + ); + }); + + it('clears whether the network supports EIP-1559 from state', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + networkDetails: { + EIPS: { + 1559: true, + }, + other: 'details', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); + }, + ); + }); + + it('does not emit infuraIsBlocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); + }); + + it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(infuraIsUnblocked).toBeTruthy(); + }, + ); + }); }); describe('if the network was switched after the net_version request started but before it completed', () => { - it('persists to state the network version of the newly switched network, not the initial network', async () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: [ + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + ], + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: { + result: { + ...BLOCK, + number: '0x1', + }, + }, + }, + { + request: { + params: ['0x2', false], + }, + response: { + result: { + ...BLOCK, + number: '0x2', + }, + }, + }, + ], + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('stores the ID of the second network, not the first', async () => { await withController( { state: { @@ -2352,24 +3413,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, - networkDetails: { - EIPS: {}, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, - testNetworkConfigurationId2: { - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, }, }, }, @@ -2382,28 +3425,21 @@ describe('NetworkController', () => { beforeCompleting: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId1', - ); + controller.setProviderType('goerli'); }, }); }, }, }); - const network2 = network1.with({ + const network2 = new NetworkCommunications({ + networkClientType: 'infura', networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-1', - }, - }); - network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: '222', - }, + infuraNetwork: 'goerli', }, }); + network2.mockEssentialRpcCalls(); await withoutCallingLookupNetwork({ controller, @@ -2414,19 +3450,261 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkId'], operation: async () => { await controller.lookupNetwork(); }, }); - expect(controller.store.getState().network).toBe('222'); + expect(controller.store.getState().networkId).toBe('5'); }, ); }); - it('persists to state the EIP-1559 support for the newly switched network, not the initial one', async () => { - const nonEip1559RpcUrl = 'https://mock-rpc-url-1'; + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + networkDetails: { + EIPS: {}, + other: 'details', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + // setProviderType clears networkDetails first, and then updates + // it to what we expect it to be + count: 2, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); + + await controller.lookupNetwork(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); + }, + ); + }); + }); + + describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { + it('stores the network status of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x1337', + ticker: 'RPC', + }, + networkDetails: { + EIPS: {}, + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + net_version: { + times: 2, + }, + // Ensure that each call to eth_blockNumber returns a different + // block number, otherwise the first eth_getBlockByNumber + // response will get cached under the first block number + eth_blockNumber: [ + { + response: { + result: '0x1', + }, + }, + { + response: { + result: '0x2', + }, + }, + ], + eth_getBlockByNumber: [ + { + request: { + params: ['0x1', false], + }, + response: { + result: { + ...BLOCK, + number: '0x1', + }, + }, + }, + { + request: { + params: ['0x2', false], + }, + response: { + result: { + ...BLOCK, + number: '0x2', + }, + }, + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + ], + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.initializeProvider(); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); + + // Force the block tracker to request a new block to clear the + // block cache + clock.runAll(); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + expect(controller.store.getState().networkStatus).toBe('unknown'); + }, + ); + }); + + it('stores the network ID of the second network, not the first', async () => { await withController( { state: { @@ -2435,78 +3713,184 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, - networkDetails: { - EIPS: {}, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: nonEip1559RpcUrl, - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, - testNetworkConfigurationId2: { - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x1337', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', - }, }, }, }, async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ - net_version: { - response: { - result: '111', - }, + eth_getBlockByNumber: { beforeCompleting: async () => { await waitForStateChanges({ controller, - propertyPath: ['networkDetails'], + propertyPath: ['networkStatus'], operation: () => { - controller.setActiveNetwork( - 'testNetworkConfigurationId1', - ); + controller.setProviderType('goerli'); }, }); }, }, - eth_getBlockByNumber: { - response: { - result: POST_1559_BLOCK, - }, - }, - }); - const network2 = network1.with({ - networkClientOptions: { - customRpcUrl: nonEip1559RpcUrl, - }, - }); - network2.mockEssentialRpcCalls({ net_version: { response: { - result: '222', - }, - }, - eth_getBlockByNumber: { - response: { - result: PRE_1559_BLOCK, + result: '111', }, }, }); - await waitForLookupNetworkToComplete({ + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls(); + + await withoutCallingLookupNetwork({ controller, operation: async () => { await controller.initializeProvider(); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); + await waitForStateChanges({ + controller, + propertyPath: ['networkId'], + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkId).toBe('5'); + }, + ); + }); + + it('stores the EIP-1559 support of the second network, not the first', async () => { + await withController( + { + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + networkDetails: { + EIPS: {}, + other: 'details', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + // setProviderType clears networkDetails first, and then updates + // it to what we expect it to be + count: 2, + operation: async () => { + await controller.lookupNetwork(); + }, + }); + + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); + }, + ); + }); + + it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { + const messenger = buildMessenger(); + + await withController( + { + messenger, + state: { + provider: { + type: 'rpc', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'RPC', + }, + }, + }, + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + beforeCompleting: async () => { + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.setProviderType('goerli'); + }, + }); + }, + }, + }); + const network2 = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: 'goerli', + }, + }); + network2.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + await withoutCallingLookupNetwork({ + controller, + operation: async () => { + await controller.initializeProvider(); + }, + }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); + + await controller.lookupNetwork(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, ); }); @@ -2543,28 +3927,21 @@ describe('NetworkController', () => { ); }); - it('captures the current provider configuration before overwriting it', async () => { + it('stores the current provider configuration before overwriting it', async () => { await withController( { state: { provider: { type: 'rpc', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x9999', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - ticker: 'TEST', - id: 'testNetworkConfigurationId1', - }, testNetworkConfigurationId2: { rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x9999', - ticker: 'RPC', + chainId: '0x222', id: 'testNetworkConfigurationId2', }, }, @@ -2579,16 +3956,16 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - controller.setActiveNetwork('testNetworkConfigurationId1'); + controller.setActiveNetwork('testNetworkConfigurationId2'); expect( controller.store.getState().previousProviderStore, ).toStrictEqual({ type: 'rpc', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x9999', - ticker: 'RPC', - id: 'testNetworkConfigurationId2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST', + id: 'testNetworkConfigurationId1', }); }, ); @@ -2645,28 +4022,30 @@ describe('NetworkController', () => { ); }); - it('emits networkWillChange before making any changes to the network store', async () => { + it('emits networkWillChange before making any changes to the network status', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0xtest2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', ticker: 'TEST2', - id: 'testNetworkConfigurationId2', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST1', id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0xtest2', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', ticker: 'TEST2', id: 'testNetworkConfigurationId2', }, @@ -2676,22 +4055,16 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); const network2 = network1.with({ networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + customRpcUrl: 'https://mock-rpc-url-2', }, }); network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: '99', - }, - }, + net_version: UNSUCCESSFUL_JSON_RPC_RESPONSE, }); await waitForLookupNetworkToComplete({ controller, @@ -2699,25 +4072,28 @@ describe('NetworkController', () => { await controller.initializeProvider(); }, }); - const initialNetwork = controller.store.getState().network; - expect(initialNetwork).toBe('42'); + const initialNetworkStatus = + controller.store.getState().networkStatus; + expect(initialNetworkStatus).toBe('available'); - const networkWillChange = await waitForEvent({ - controller, - eventName: 'networkWillChange', + const networkWillChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', operation: () => { controller.setActiveNetwork('testNetworkConfigurationId2'); }, beforeResolving: () => { - expect(controller.store.getState().network).toBe(initialNetwork); + expect(controller.store.getState().networkStatus).toBe( + initialNetworkStatus, + ); }, }); - expect(networkWillChange).toBe(true); + expect(networkWillChange).toBeTruthy(); }, ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -2726,7 +4102,7 @@ describe('NetworkController', () => { rpcUrl: 'http://mock-rpc-url-2', chainId: '0xtest2', ticker: 'TEST2', - id: 'testNetworkConfigurationId2', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -2747,18 +4123,27 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ net_version: { - response: { - result: '255', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, + }, + }); + const network2 = network1.with({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url-1', + }, + }); + network2.mockEssentialRpcCalls({ + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it happens // before networkDidChange count: 1, @@ -2766,32 +4151,32 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId1'); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { provider: { type: 'rpc', - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - ticker: 'TEST2', - id: 'testNetworkConfigurationId2', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + ticker: 'TEST1', + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - ticker: 'TEST', + chainId: '0x1111', + ticker: 'TEST1', id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', ticker: 'TEST2', id: 'testNetworkConfigurationId2', }, @@ -2826,7 +4211,7 @@ describe('NetworkController', () => { // before networkDidChange count: 1, operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId1'); + controller.setActiveNetwork('testNetworkConfigurationId2'); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ @@ -2899,20 +4284,21 @@ describe('NetworkController', () => { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', + chainId: '0x1337', ticker: 'TEST', }, }, }, }, - async ({ controller }) => { - const network = new NetworkCommunications({ + async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = network1.with({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network.mockEssentialRpcCalls(); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider: providerBefore } = @@ -2927,8 +4313,11 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { networkConfigurations: { testNetworkConfigurationId: { @@ -2949,22 +4338,25 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', + const networkDidChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(networkDidChange).toBe(true); + expect(networkDidChange).toBeTruthy(); }, ); }); it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { networkConfigurations: { testNetworkConfigurationId: { @@ -2985,20 +4377,20 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(infuraIsUnblocked).toBe(true); + expect(infuraIsUnblocked).toBeTruthy(); }, ); }); - it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { + it('determines the status of the network, storing it in state', async () => { await withController( { state: { @@ -3021,31 +4413,29 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); - await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('available'); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('determines whether the network supports EIP-1559, storing it in state', async () => { await withController( { state: { networkDetails: { EIPS: {}, + other: 'details', }, networkConfigurations: { testNetworkConfigurationId: { @@ -3071,30 +4461,28 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, propertyPath: ['networkDetails'], + // setActiveNetwork clears networkDetails first, and then updates it + // to what we expect it to be count: 2, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(controller.store.getState().networkDetails.EIPS['1559']).toBe( - true, - ); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); }); describe('setProviderType', () => { - for (const { - nickname, - networkType, - chainId, - networkVersion, - ticker, - } of INFURA_NETWORKS) { + for (const { networkType, chainId, ticker } of INFURA_NETWORKS) { describe(`given a type of "${networkType}"`, () => { - it('captures the current provider configuration before overwriting it', async () => { + it('stores the current provider configuration before overwriting it', async () => { await withController( { state: { @@ -3218,14 +4606,15 @@ describe('NetworkController', () => { blockExplorerUrl: BUILT_IN_NETWORKS[networkType].blockExplorerUrl, }, - id: 'testNetworkConfigurationId2', }); }, ); }); it('emits networkWillChange', async () => { - await withController(async ({ controller }) => { + const messenger = buildMessenger(); + + await withController({ messenger }, async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { @@ -3234,19 +4623,19 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - const networkWillChange = await waitForEvent({ - controller, - eventName: 'networkWillChange', + const networkWillChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', operation: () => { controller.setProviderType(networkType); }, }); - expect(networkWillChange).toBe(true); + expect(networkWillChange).toBeTruthy(); }); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -3270,9 +4659,7 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls({ net_version: { - response: { - result: '255', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); const network2 = network1.with({ @@ -3284,11 +4671,13 @@ describe('NetworkController', () => { network2.mockEssentialRpcCalls(); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -3296,12 +4685,12 @@ describe('NetworkController', () => { controller.setProviderType(networkType); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -3362,7 +4751,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController(async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', @@ -3386,14 +4775,15 @@ describe('NetworkController', () => { }); it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ + await withController(async ({ controller, network: network1 }) => { + network1.mockEssentialRpcCalls(); + const network2 = network1.with({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: networkType, }, }); - network.mockEssentialRpcCalls(); + network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider: providerBefore } = @@ -3407,7 +4797,9 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - await withController(async ({ controller }) => { + const messenger = buildMessenger(); + + await withController({ messenger }, async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { @@ -3416,19 +4808,51 @@ describe('NetworkController', () => { }); network.mockEssentialRpcCalls(); - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', + const networkDidChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: () => { controller.setProviderType(networkType); }, }); - expect(networkDidChange).toBe(true); + expect(networkDidChange).toBeTruthy(); }); }); - it('emits infuraIsUnblocked (assuming that the request for eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { + const messenger = buildMessenger(); + + await withController({ messenger }, async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'infura', + networkClientOptions: { + infuraNetwork: networkType, + }, + }); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); + + controller.setProviderType(networkType); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); + }); + }); + + it('determines the status of the network, storing it in state', async () => { await withController(async ({ controller }) => { const network = new NetworkCommunications({ networkClientType: 'infura', @@ -3436,48 +4860,31 @@ describe('NetworkController', () => { infuraNetwork: networkType, }, }); - network.mockEssentialRpcCalls(); - - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: () => { - controller.setProviderType(networkType); - }, + network.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, }); - expect(infuraIsUnblocked).toBe(true); - }); - }); - - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { - await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, - }); - network.mockEssentialRpcCalls(); - await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.setProviderType(networkType); }, }); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe('available'); }); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('determines whether the network supports EIP-1559, storing it in state', async () => { await withController( { state: { networkDetails: { EIPS: {}, + other: 'details', }, }, }, @@ -3495,15 +4902,19 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, propertyPath: ['networkDetails'], + // setProviderType clears networkDetails first, and then updates + // it to what we expect it to be count: 2, operation: () => { controller.setProviderType(networkType); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -3534,16 +4945,14 @@ describe('NetworkController', () => { }); describe('resetConnection', () => { - for (const { - nickname, - networkType, - chainId, - networkVersion, - } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { it('emits networkWillChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -3556,20 +4965,20 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const networkWillChange = await waitForEvent({ - controller, - eventName: 'networkWillChange', + const networkWillChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', operation: () => { controller.resetConnection(); }, }); - expect(networkWillChange).toBe(true); + expect(networkWillChange).toBeTruthy(); }, ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -3589,11 +4998,13 @@ describe('NetworkController', () => { }); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -3601,12 +5012,12 @@ describe('NetworkController', () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -3652,7 +5063,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a new provider object pointed to the current Infura network (name: ${nickname}, chain ID: ${chainId})`, async () => { + it(`initializes a new provider object pointed to the current Infura network (type: "${networkType}", chain ID: ${chainId})`, async () => { await withController( { state: { @@ -3694,7 +5105,11 @@ describe('NetworkController', () => { }, }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + eth_blockNumber: { + times: 2, + }, + }); await controller.initializeProvider(); const { provider: providerBefore } = @@ -3709,8 +5124,11 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -3723,22 +5141,25 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', + const networkDidChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: () => { controller.resetConnection(); }, }); - expect(networkDidChange).toBe(true); + expect(networkDidChange).toBeTruthy(); }, ); }); - it('emits infuraIsUnblocked (assuming that the request for eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -3749,22 +5170,31 @@ describe('NetworkController', () => { }, }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); - - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: () => { - controller.resetConnection(); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, }, }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); - expect(infuraIsUnblocked).toBe(true); + controller.resetConnection(); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, ); }); - it(`ensures that the network version in state is set to "${networkVersion}"`, async () => { + it('checks the status of the network again, updating state appropriately', async () => { await withController( { state: { @@ -3781,18 +5211,20 @@ describe('NetworkController', () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); }, ); }); - it('does not ensure that EIP-1559 support for the current network is up to date', async () => { + it('checks whether the network supports EIP-1559 again, updating state appropriately', async () => { await withController( { state: { @@ -3809,15 +5241,25 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); - controller.resetConnection(); + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.resetConnection(); + }, + }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -3826,8 +5268,11 @@ describe('NetworkController', () => { describe(`when the type in the provider configuration is "rpc"`, () => { it('emits networkWillChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -3849,20 +5294,20 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const networkWillChange = await waitForEvent({ - controller, - eventName: 'networkWillChange', + const networkWillChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', operation: () => { controller.resetConnection(); }, }); - expect(networkWillChange).toBe(true); + expect(networkWillChange).toBeTruthy(); }, ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -3885,22 +5330,21 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ - eth_blockNumber: { + net_version: { + response: SUCCESSFUL_NET_VERSION_RESPONSE, times: 2, }, - net_version: { - response: { - result: '255', - }, + eth_blockNumber: { + times: 2, }, }); await controller.initializeProvider(); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it happens // before networkDidChange count: 1, @@ -3908,12 +5352,12 @@ describe('NetworkController', () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -4028,7 +5472,11 @@ describe('NetworkController', () => { }, }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + network.mockEssentialRpcCalls({ + eth_blockNumber: { + times: 2, + }, + }); await controller.initializeProvider(); const { provider: providerBefore } = @@ -4048,8 +5496,11 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -4071,22 +5522,25 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', + const networkDidChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: () => { controller.resetConnection(); }, }); - expect(networkDidChange).toBe(true); + expect(networkDidChange).toBeTruthy(); }, ); }); it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -4108,20 +5562,20 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls(); - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', operation: () => { controller.resetConnection(); }, }); - expect(infuraIsUnblocked).toBe(true); + expect(infuraIsUnblocked).toBeTruthy(); }, ); }); - it('ensures that the network version in state is up to date', async () => { + it('checks the status of the network again, updating state appropriately', async () => { await withController( { state: { @@ -4145,26 +5599,25 @@ describe('NetworkController', () => { async ({ controller, network }) => { network.mockEssentialRpcCalls({ net_version: { - response: { - result: '42', - }, + response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); + expect(controller.store.getState().networkStatus).toBe('unknown'); await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], operation: () => { controller.resetConnection(); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('available'); }, ); }); - it('does not ensure that EIP-1559 support for the current network is up to date', async () => { + it('ensures that EIP-1559 support for the current network is up to date', async () => { await withController( { state: { @@ -4190,15 +5643,25 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: undefined, + }, + }); - controller.resetConnection(); + await waitForStateChanges({ + controller, + propertyPath: ['networkDetails'], + operation: () => { + controller.resetConnection(); + }, + }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBeUndefined(); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -4206,81 +5669,73 @@ describe('NetworkController', () => { }); describe('rollbackToPreviousProvider', () => { - for (const { - nickname, - networkType, - chainId, - networkVersion, - } of INFURA_NETWORKS) { + for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`if the previous provider configuration had a type of "${networkType}"`, () => { - it('merges the previous configuration into the current provider configuration', async () => { + it('overwrites the the current provider configuration with the previous provider configuration', async () => { await withController( { state: { provider: { type: networkType, - rpcUrl: '', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - nickname: '', - ticker: BUILT_IN_NETWORKS[networkType].ticker, + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + nickname: 'network 1', + ticker: 'TEST1', rpcPrefs: { - blockExplorerUrl: - BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + blockExplorerUrl: 'https://test-block-explorer-1.com', }, + id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - nickname: 'test-chain', - ticker: 'TEST', + chainId: '0x111', + nickname: 'network 1', + ticker: 'TEST1', rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', + blockExplorerUrl: 'https://test-block-explorer-1.com', }, id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', + nickname: 'network 2', ticker: 'TEST2', rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', + blockExplorerUrl: 'https://test-block-explorer-2.com', }, id: 'testNetworkConfigurationId2', }, }, - networkDetails: { - EIPS: {}, - }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', + customRpcUrl: 'https://mock-rpc-url-2', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId1'); + controller.setActiveNetwork('testNetworkConfigurationId2'); }, }); expect(controller.store.getState().provider).toStrictEqual({ - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', - }, - id: 'testNetworkConfigurationId1', type: 'rpc', + rpcUrl: 'https://mock-rpc-url-2', + chainId: '0x222', + nickname: 'network 2', + ticker: 'TEST2', + rpcPrefs: { + blockExplorerUrl: 'https://test-block-explorer-2.com', + }, + id: 'testNetworkConfigurationId2', }); await waitForLookupNetworkToComplete({ @@ -4291,23 +5746,25 @@ describe('NetworkController', () => { }); expect(controller.store.getState().provider).toStrictEqual({ type: networkType, - rpcUrl: '', - chainId: BUILT_IN_NETWORKS[networkType].chainId, - ticker: BUILT_IN_NETWORKS[networkType].ticker, - nickname: '', - id: 'testNetworkConfigurationId1', + rpcUrl: 'https://mock-rpc-url-1', + chainId: '0x111', + nickname: 'network 1', + ticker: 'TEST1', rpcPrefs: { - blockExplorerUrl: - BUILT_IN_NETWORKS[networkType].blockExplorerUrl, + blockExplorerUrl: 'https://test-block-explorer-1.com', }, + id: 'testNetworkConfigurationId1', }); }, ); }); it('emits networkWillChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -4330,15 +5787,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -4349,22 +5806,22 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - const networkWillChange = await waitForEvent({ - controller, - eventName: 'networkWillChange', + const networkWillChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(networkWillChange).toBe(true); + expect(networkWillChange).toBeTruthy(); }, }); }, ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network status to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -4389,21 +5846,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls({ - net_version: { - response: { - result: '255', - }, - }, - }); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4411,14 +5862,16 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect(controller.store.getState().network).toBe('255'); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); await waitForLookupNetworkToComplete({ controller, operation: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -4426,14 +5879,16 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); }, }); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -4458,17 +5913,17 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4508,7 +5963,7 @@ describe('NetworkController', () => { ); }); - it(`initializes a provider pointed to the ${nickname} Infura network (chainId: ${chainId})`, async () => { + it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController( { state: { @@ -4533,15 +5988,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -4593,15 +6048,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -4626,8 +6081,11 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -4650,15 +6108,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4670,23 +6128,26 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', + const networkDidChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(networkDidChange).toBe(true); + expect(networkDidChange).toBeTruthy(); }, }); }, ); }); - it('emits infuraIsUnblocked (assuming that the request for eth_blockNumber responds successfully)', async () => { + it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: networkType, @@ -4694,11 +6155,6 @@ describe('NetworkController', () => { // the network selected, it just needs to exist chainId: '0x9999999', }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', @@ -4709,103 +6165,117 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls(); - + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); await waitForLookupNetworkToComplete({ controller, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsBlocked', + }); await waitForLookupNetworkToComplete({ controller, operation: async () => { - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', - operation: () => { - controller.rollbackToPreviousProvider(); - }, - }); - - expect(infuraIsUnblocked).toBe(true); + controller.rollbackToPreviousProvider(); }, }); + + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); }, ); }); - it(`persists "${networkVersion}" to state as the network version of ${nickname}`, async () => { + it('checks the status of the previous network again and updates state accordingly', async () => { + const previousProvider = { + type: networkType, + // NOTE: This doesn't need to match the logical chain ID of + // the network selected, it just needs to exist + chainId: '0x9999999', + }; + const currentNetworkConfiguration = { + id: 'currentNetworkConfiguration', + rpcUrl: 'https://mock-rpc-url', + chainId: '0x1337', + ticker: 'TEST', + }; await withController( { state: { - provider: { - type: networkType, - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkDetails: { - EIPS: { - 1559: false, - }, - }, + provider: previousProvider, networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', - ticker: 'TEST', - }, + currentNetworkConfiguration, }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'custom', networkClientOptions: { customRpcUrl: 'https://mock-rpc-url', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ net_version: { response: { - result: '255', + error: 'some error', + httpStatus: 405, }, }, }); - - await waitForLookupNetworkToComplete({ - controller, - operation: () => { - controller.setActiveNetwork('testNetworkConfigurationId'); + previousNetwork.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, }, }); - expect(controller.store.getState().network).toBe('255'); + + await waitForStateChanges({ + controller, + propertyPath: ['networkStatus'], + operation: () => { + controller.setActiveNetwork('currentNetworkConfiguration'); + }, + }); + expect(controller.store.getState().networkStatus).toBe( + 'unavailable', + ); await waitForLookupNetworkToComplete({ controller, - numberOfNetworkDetailsChanges: 2, operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe(networkVersion); + expect(controller.store.getState().networkStatus).toBe( + 'available', + ); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { await withController( { state: { @@ -4825,24 +6295,19 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://mock-rpc-url', + }, + }); + currentNetwork.mockEssentialRpcCalls({ + latestBlock: PRE_1559_BLOCK, + }); + previousNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); - const network2 = network1.with({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, - }); - network2.mockEssentialRpcCalls({ - latestBlock: PRE_1559_BLOCK, - net_version: { - response: { - result: '99999', - }, - }, - }); await waitForLookupNetworkToComplete({ controller, @@ -4850,9 +6315,11 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); await waitForLookupNetworkToComplete({ controller, @@ -4861,9 +6328,11 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); @@ -4871,7 +6340,7 @@ describe('NetworkController', () => { } describe(`if the previous provider configuration had a type of "rpc"`, () => { - it('merges the previous configuration into the current provider configuration', async () => { + it('overwrites the the current provider configuration with the previous provider configuration', async () => { await withController( { state: { @@ -4915,15 +6384,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -4940,7 +6409,6 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'https://goerli.etherscan.io', }, - id: 'testNetworkConfigurationId2', }); await waitForLookupNetworkToComplete({ @@ -4953,8 +6421,8 @@ describe('NetworkController', () => { type: 'rpc', rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', - ticker: 'TEST2', nickname: 'test-chain-2', + ticker: 'TEST2', rpcPrefs: { blockExplorerUrl: 'test-block-explorer-2.com', }, @@ -4965,8 +6433,11 @@ describe('NetworkController', () => { }); it('emits networkWillChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -4991,15 +6462,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -5010,22 +6481,22 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - const networkWillChange = await waitForEvent({ - controller, - eventName: 'networkWillChange', + const networkWillChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkWillChange', operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(networkWillChange).toBe(true); + expect(networkWillChange).toBeTruthy(); }, }); }, ); }); - it('resets the network state to "loading" before emitting networkDidChange', async () => { + it('resets the network state to "unknown" before emitting networkDidChange', async () => { await withController( { state: { @@ -5046,15 +6517,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5062,14 +6533,14 @@ describe('NetworkController', () => { controller.setProviderType('goerli'); }, }); - expect(controller.store.getState().network).toBe('5'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForLookupNetworkToComplete({ controller, operation: async () => { await waitForStateChanges({ controller, - propertyPath: ['network'], + propertyPath: ['networkStatus'], // We only care about the first state change, because it // happens before networkDidChange count: 1, @@ -5077,14 +6548,16 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe('loading'); + expect(controller.store.getState().networkStatus).toBe( + 'unknown', + ); }, }); }, ); }); - it('resets EIP support for the network before emitting networkDidChange', async () => { + it('clears EIP-1559 support for the network from state before emitting networkDidChange', async () => { await withController( { state: { @@ -5105,17 +6578,17 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5176,15 +6649,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -5232,15 +6705,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, operation: () => { @@ -5265,8 +6738,11 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -5285,15 +6761,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5305,14 +6781,14 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - const networkDidChange = await waitForEvent({ - controller, - eventName: 'networkDidChange', + const networkDidChange = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:networkDidChange', operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(networkDidChange).toBe(true); + expect(networkDidChange).toBeTruthy(); }, }); }, @@ -5320,8 +6796,11 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { + const messenger = buildMessenger(); + await withController( { + messenger, state: { provider: { type: 'rpc', @@ -5340,15 +6819,15 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls(); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls(); + previousNetwork.mockEssentialRpcCalls(); await waitForLookupNetworkToComplete({ controller, @@ -5360,22 +6839,22 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - const infuraIsUnblocked = await waitForEvent({ - controller, - eventName: 'infuraIsUnblocked', + const infuraIsUnblocked = await waitForPublishedEvents({ + messenger, + eventType: 'NetworkController:infuraIsUnblocked', operation: () => { controller.rollbackToPreviousProvider(); }, }); - expect(infuraIsUnblocked).toBe(true); + expect(infuraIsUnblocked).toBeTruthy(); }, }); }, ); }); - it('persists the network version to state (assuming that the request for net_version responds successfully)', async () => { + it('checks the status of the previous network again and updates state accordingly', async () => { await withController( { state: { @@ -5396,21 +6875,23 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ - net_version: { - response: { - result: '42', - }, - }, - }); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls(); + currentNetwork.mockEssentialRpcCalls({ + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: BLOCK, + }); + previousNetwork.mockEssentialRpcCalls({ + net_version: { + response: UNSUCCESSFUL_JSON_RPC_RESPONSE, + }, + }); await waitForLookupNetworkToComplete({ controller, @@ -5418,7 +6899,7 @@ describe('NetworkController', () => { controller.setProviderType('goerli'); }, }); - expect(controller.store.getState().network).toBe('5'); + expect(controller.store.getState().networkStatus).toBe('available'); await waitForLookupNetworkToComplete({ controller, @@ -5426,12 +6907,12 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect(controller.store.getState().network).toBe('42'); + expect(controller.store.getState().networkStatus).toBe('unknown'); }, ); }); - it('persists to state whether the network supports EIP-1559 (assuming that the request for eth_getBlockByNumber responds successfully)', async () => { + it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { await withController( { state: { @@ -5452,24 +6933,19 @@ describe('NetworkController', () => { }, }, }, - async ({ controller, network: network1 }) => { - network1.mockEssentialRpcCalls({ - latestBlock: POST_1559_BLOCK, - net_version: { - response: { - result: '99999', - }, - }, - }); - const network2 = network1.with({ + async ({ controller, network: previousNetwork }) => { + const currentNetwork = new NetworkCommunications({ networkClientType: 'infura', networkClientOptions: { infuraNetwork: 'goerli', }, }); - network2.mockEssentialRpcCalls({ + currentNetwork.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, }); + previousNetwork.mockEssentialRpcCalls({ + latestBlock: POST_1559_BLOCK, + }); await waitForLookupNetworkToComplete({ controller, @@ -5477,9 +6953,11 @@ describe('NetworkController', () => { controller.setProviderType('goerli'); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(false); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: false, + }, + }); await waitForLookupNetworkToComplete({ controller, @@ -5487,14 +6965,17 @@ describe('NetworkController', () => { controller.rollbackToPreviousProvider(); }, }); - expect( - controller.store.getState().networkDetails.EIPS['1559'], - ).toBe(true); + expect(controller.store.getState().networkDetails).toStrictEqual({ + EIPS: { + 1559: true, + }, + }); }, ); }); }); }); + describe('upsertNetworkConfiguration', () => { it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { const invalidChainId = '1'; @@ -5510,7 +6991,7 @@ describe('NetworkController', () => { }, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }, ), ).toThrow( @@ -5534,7 +7015,7 @@ describe('NetworkController', () => { }, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }, ), ).toThrow( @@ -5557,7 +7038,7 @@ describe('NetworkController', () => { }, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }, ), ).toThrow( @@ -5581,7 +7062,7 @@ describe('NetworkController', () => { }, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }, ), ).toThrow(new Error('rpcUrl must be a valid URL')); @@ -5600,7 +7081,7 @@ describe('NetworkController', () => { }, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }, ), ).toThrow( @@ -5645,7 +7126,7 @@ describe('NetworkController', () => { controller.upsertNetworkConfiguration(rpcUrlNetwork, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect( @@ -5683,7 +7164,7 @@ describe('NetworkController', () => { controller.upsertNetworkConfiguration(rpcUrlNetwork, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect( @@ -5732,7 +7213,7 @@ describe('NetworkController', () => { controller.upsertNetworkConfiguration(rpcUrlNetwork, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect( @@ -5781,7 +7262,7 @@ describe('NetworkController', () => { }; controller.upsertNetworkConfiguration(updatedConfiguration, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect( Object.values(controller.store.getState().networkConfigurations), @@ -5834,7 +7315,7 @@ describe('NetworkController', () => { }, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }, ); @@ -5894,7 +7375,7 @@ describe('NetworkController', () => { controller.upsertNetworkConfiguration(rpcUrlNetwork, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect(controller.store.getState().provider).toStrictEqual( @@ -5927,6 +7408,13 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { + const network = new NetworkCommunications({ + networkClientType: 'custom', + networkClientOptions: { + customRpcUrl: 'https://test-rpc-url', + }, + }); + network.mockEssentialRpcCalls(); const rpcUrlNetwork = { chainId: '0x1', rpcUrl: 'https://test-rpc-url', @@ -5936,7 +7424,7 @@ describe('NetworkController', () => { controller.upsertNetworkConfiguration(rpcUrlNetwork, { setActive: true, referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect(controller.store.getState().provider).toStrictEqual({ @@ -5985,7 +7473,7 @@ describe('NetworkController', () => { controller.upsertNetworkConfiguration(newNetworkConfiguration, { referrer: 'https://test-dapp.com', - source: EVENT.SOURCE.NETWORK.DAPP, + source: MetaMetricsNetworkEventSource.Dapp, }); expect( @@ -6100,6 +7588,40 @@ describe('NetworkController', () => { }); }); +/** + * Builds the controller messenger that NetworkController is designed to work + * with. + * + * @returns The controller messenger. + */ +function buildMessenger() { + return new ControllerMessenger().getRestricted({ + name: 'NetworkController', + allowedActions: [], + allowedEvents: [ + 'NetworkController:networkDidChange', + 'NetworkController:networkWillChange', + 'NetworkController:infuraIsBlocked', + 'NetworkController:infuraIsUnblocked', + ], + }); +} + +/** + * Despite the signature of its constructor, NetworkController must take an + * Infura project ID. The object that this function returns is mixed into the + * options first when a NetworkController is instantiated in tests. + * + * @returns {object} The controller options. + */ +function buildDefaultNetworkControllerOptions() { + return { + messenger: buildMessenger(), + infuraProjectId: DEFAULT_INFURA_PROJECT_ID, + trackMetaMetricsEvent: jest.fn(), + }; +} + /** * Builds a controller based on the given options, and calls the given function * with that controller. @@ -6111,11 +7633,11 @@ describe('NetworkController', () => { * @returns Whatever the callback returns. */ async function withController(...args) { - const [givenConstructorOptions, fn] = + const [givenNetworkControllerOptions, fn] = args.length === 2 ? args : [{}, args[0]]; const constructorOptions = { - ...DEFAULT_CONTROLLER_OPTIONS, - ...givenConstructorOptions, + ...buildDefaultNetworkControllerOptions(), + ...givenNetworkControllerOptions, }; const controller = new NetworkController(constructorOptions); @@ -6326,43 +7848,118 @@ async function waitForStateChanges({ } /** - * Waits for an event to occur on the controller before proceeding. + * Waits for controller events to be emitted before proceeding. * - * @param {{controller: NetworkController, eventName: string, operation: (() => void | Promise), beforeResolving?: (() => void | Promise)}} args - The arguments. - * @param {NetworkController} args.controller - The network controller - * @param {string} args.eventName - The name of the event. - * @param {() => void | Promise} args.operation - A function that will - * presumably produce the event in question. - * @param {() => void | Promise} [args.beforeResolving] - In some tests, - * state updates happen so fast, we need to make an assertion immediately after - * the event in question occurs. However, if we wait until the promise this - * function returns resolves to do so, some other state update to the same + * @param {object} options - An options bag. + * @param {ControllerMessenger} options.messenger - The messenger suited for + * NetworkController. + * @param {string} options.eventType - The type of NetworkController event you + * want to wait for. + * @param {number} options.count - The number of events you expect to occur + * (default: 1). + * @param {(payload: any) => boolean} options.filter - A function used to + * discard events that are not of interest. + * @param {number} options.wait - The amount of time in milliseconds to wait for + * the expected number of filtered events to occur before resolving the promise + * that this function returns (default: 150). + * @param {() => void | Promise} options.operation - A function to run + * that will presumably produce the events in question. + * @param {() => void | Promise} [options.beforeResolving] - In some + * tests, state updates happen so fast, we need to make an assertion immediately + * after the event in question occurs. However, if we wait until the promise + * this function returns resolves to do so, some other state update to the same * property may have happened. This option allows you to make an assertion * _before_ the promise resolves. This has the added benefit of allowing you to - * maintain the "arrange, act, assert" ordering in your test, meaning that - * you can still call the method that kicks off the event and then make the + * maintain the "arrange, act, assert" ordering in your test, meaning that you + * can still call the method that kicks off the event and then make the * assertion afterward instead of the other way around. - * @returns {Promise} + * @returns A promise that resolves to the list of payloads for the set of + * events, optionally filtered, when a specific number of them have occurred. */ -async function waitForEvent({ - controller, - eventName, - operation, +async function waitForPublishedEvents({ + messenger, + eventType, + count: expectedNumberOfEvents = 1, + filter: isEventPayloadInteresting = () => true, + wait: timeBeforeAssumingNoMoreEvents = 150, + operation = () => { + // do nothing + }, beforeResolving = async () => { // do nothing }, }) { - const promise = new Promise((resolve) => { - controller.once(eventName, () => { - Promise.resolve(beforeResolving()).then(() => { - resolve(true); - }); - }); + const promiseForEventPayloads = new Promise((resolve, reject) => { + // We need to declare this variable first, then assign it later, so that + // ESLint won't complain that resetTimer is referring to this variable + // before it's declared. And we need to use let so that we can assign it + // below. + /* eslint-disable-next-line prefer-const */ + let eventListener; + let timer; + const allEventPayloads = []; + const interestingEventPayloads = []; + let alreadyEnded = false; + + const end = () => { + if (!alreadyEnded) { + alreadyEnded = true; + messenger.unsubscribe(eventType.toString(), eventListener); + Promise.resolve(beforeResolving()).then(() => { + if (interestingEventPayloads.length === expectedNumberOfEvents) { + resolve(interestingEventPayloads); + } else { + // Using a string instead of an Error leads to better backtraces. + /* eslint-disable-next-line prefer-promise-reject-errors */ + reject( + `Expected to receive ${expectedNumberOfEvents} ${eventType} event(s), but received ${ + interestingEventPayloads.length + } after ${timeBeforeAssumingNoMoreEvents}ms.\n\nAll payloads:\n\n${inspect( + allEventPayloads, + { depth: null }, + )}`, + ); + } + }); + } + }; + + const stopTimer = () => { + if (timer) { + clearTimeout(timer); + } + }; + + const resetTimer = () => { + stopTimer(); + timer = originalSetTimeout(() => { + end(); + }, timeBeforeAssumingNoMoreEvents); + }; + + eventListener = (...payload) => { + allEventPayloads.push(payload); + + if (isEventPayloadInteresting(payload)) { + interestingEventPayloads.push(payload); + if (interestingEventPayloads.length === expectedNumberOfEvents) { + stopTimer(); + end(); + } else { + resetTimer(); + } + } + }; + + messenger.subscribe(eventType.toString(), eventListener); + resetTimer(); }); - await operation(); + if (operation) { + await operation(); + } - return await promise; + return await promiseForEventPayloads; } /** diff --git a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js new file mode 100644 index 000000000..c1778f628 --- /dev/null +++ b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js @@ -0,0 +1,272 @@ +/* eslint-disable jest/require-top-level-describe, jest/no-export */ + +import { withMockedCommunications, withNetworkClient } from './helpers'; + +/** + * Defines tests which exercise the behavior exhibited by an RPC method that + * use `blockHash` in the response data to determine whether the response is + * cacheable. + * + * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.numberOfParameters - The number of parameters supported + * by the method under test. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom` (default: "infura"). + */ +export function testsForRpcMethodsThatCheckForBlockHashInResponse( + method, + { numberOfParameters, providerType }, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } + + it('does not hit the RPC endpoint more than once for identical requests and it has a valid blockHash', async () => { + const requests = [{ method }, { method }]; + const mockResult = { blockHash: '0x1' }; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResult }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResult, mockResult]); + }); + }); + + it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { + const requests = [{ method }, { method }]; + const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; + + await withMockedCommunications({ providerType }, async (comms) => { + // Note that we have to mock these requests in a specific order. The + // first block tracker request occurs because of the first RPC + // request. The second block tracker request, however, does not occur + // because of the second RPC request, but rather because we call + // `clock.runAll()` below. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a new + // block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + + it('does not reuse the result of a previous request if result.blockHash was null', async () => { + const requests = [{ method }, { method }]; + const mockResults = [ + { blockHash: null, extra: 'some value' }, + { blockHash: '0x100', extra: 'some other value' }, + ]; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + + it('does not reuse the result of a previous request if result.blockHash was undefined', async () => { + const requests = [{ method }, { method }]; + const mockResults = [ + { extra: 'some value' }, + { blockHash: '0x100', extra: 'some other value' }, + ]; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + + it('does not reuse the result of a previous request if result.blockHash was "0x0000000000000000000000000000000000000000000000000000000000000000"', async () => { + const requests = [{ method }, { method }]; + const mockResults = [ + { + blockHash: + '0x0000000000000000000000000000000000000000000000000000000000000000', + extra: 'some value', + }, + { blockHash: '0x100', extra: 'some other value' }, + ]; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + + for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + it(`does not retry an empty response of "${emptyValue}"`, async () => { + const request = { method }; + const mockResult = emptyValue; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { result: mockResult }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual(mockResult); + }); + }); + + it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { + const requests = [{ method }, { method }]; + const mockResults = [emptyValue, { blockHash: '0x100' }]; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + } + + for (const paramIndex of [...Array(numberOfParameters).keys()]) { + it(`does not reuse the result of a previous request with a valid blockHash if parameter at index "${paramIndex}" differs`, async () => { + const firstMockParams = [ + ...new Array(numberOfParameters).fill('some value'), + ]; + const secondMockParams = firstMockParams.slice(); + secondMockParams[paramIndex] = 'another value'; + const requests = [ + { + method, + params: firstMockParams, + }, + { method, params: secondMockParams }, + ]; + const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[1]]); + }); + }); + } +} diff --git a/app/scripts/controllers/network/provider-api-tests/block-param.js b/app/scripts/controllers/network/provider-api-tests/block-param.js new file mode 100644 index 000000000..49bd6e772 --- /dev/null +++ b/app/scripts/controllers/network/provider-api-tests/block-param.js @@ -0,0 +1,2059 @@ +/* eslint-disable jest/require-top-level-describe, jest/no-export */ + +import { + buildMockParams, + buildRequestWithReplacedBlockParam, + waitForPromiseToBeFulfilledAfterRunningAllTimers, + withMockedCommunications, + withNetworkClient, +} from './helpers'; +import { + buildFetchFailedErrorMessage, + buildInfuraClientRetriesExhaustedErrorMessage, + buildJsonRpcEngineEmptyResponseErrorMessage, +} from './shared-tests'; + +/** + * Defines tests which exercise the behavior exhibited by an RPC method that + * takes a block parameter. The value of this parameter can be either a block + * number or a block tag ("latest", "earliest", or "pending") and affects how + * the method is cached. + * + * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.blockParamIndex - The index of the block parameter. + * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. + * @param additionalArgs.providerType - The type of provider being tested. + * either `infura` or `custom` (default: "infura"). + */ +/* eslint-disable-next-line jest/no-export */ +export function testsForRpcMethodSupportingBlockParam( + method, + { blockParamIndex, numberOfParameters, providerType }, +) { + describe.each([ + ['given no block tag', undefined], + ['given a block tag of "latest"', 'latest'], + ])('%s', (_desc, blockParam) => { + it('does not hit the RPC endpoint more than once for identical requests', async () => { + const requests = [ + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the block-cache + // middleware will request the latest block number through the block + // tracker to determine the cache key. Later, the block-ref + // middleware will request the latest block number again to resolve + // the value of "latest", but the block number is cached once made, + // so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[0]]); + }); + }); + + for (const paramIndex of [...Array(numberOfParameters).keys()]) { + if (paramIndex === blockParamIndex) { + // testing changes in block param is covered under later tests + continue; + } + + it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { + const firstMockParams = [ + ...new Array(numberOfParameters).fill('some value'), + ]; + firstMockParams[blockParamIndex] = blockParam; + const secondMockParams = firstMockParams.slice(); + secondMockParams[paramIndex] = 'another value'; + const requests = [ + { + method, + params: firstMockParams, + }, + { method, params: secondMockParams }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the block-cache + // middleware will request the latest block number through the block + // tracker to determine the cache key. Later, the block-ref + // middleware will request the latest block number again to resolve + // the value of "latest", but the block number is cached once made, + // so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[1], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[1]]); + }); + }); + } + + it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { + const requests = [ + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // Note that we have to mock these requests in a specific order. + // The first block tracker request occurs because of the first RPC + // request. The second block tracker request, however, does not + // occur because of the second RPC request, but rather because we + // call `clock.runAll()` below. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[1], + blockParamIndex, + '0x200', + ), + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a + // new block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + + for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + it(`does not retry an empty response of "${emptyValue}"`, async () => { + const request = { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }; + const mockResult = emptyValue; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { result: mockResult }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual(mockResult); + }); + }); + + it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { + const requests = [ + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + ]; + const mockResults = [emptyValue, 'some result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[1], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + } + + it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => { + const requests = [ + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + { method, params: buildMockParams({ blockParamIndex, blockParam }) }, + ]; + const mockResults = ['first result', 'second result', 'third result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number, and we delay it. + comms.mockRpcCall({ + delay: 100, + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + // The previous two requests will happen again, in the same order. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[1], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[1] }, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[2], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[2] }, + }); + + const results = await withNetworkClient( + { providerType }, + async (client) => { + const resultPromises = [ + client.makeRpcCall(requests[0]), + client.makeRpcCall(requests[1]), + client.makeRpcCall(requests[2]), + ]; + const firstResult = await resultPromises[0]; + // The inflight cache middleware uses setTimeout to run the + // handlers, so run them now + client.clock.runAll(); + const remainingResults = await Promise.all(resultPromises.slice(1)); + return [firstResult, ...remainingResults]; + }, + ); + + expect(results).toStrictEqual([ + mockResults[0], + mockResults[0], + mockResults[0], + ]); + }); + }); + + it('throws an error with a custom message if the request to the RPC endpoint returns a 405 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 405, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'The method does not exist / is not available', + ); + }); + }); + + // There is a difference in how we are testing the Infura middleware vs. the + // custom RPC middleware (or, more specifically, the fetch middleware) + // because of what both middleware treat as rate limiting errors. In this + // case, the fetch middleware treats a 418 response from the RPC endpoint as + // such an error, whereas to the Infura middleware, it is a 429 response. + if (providerType === 'infura') { + it('throws a generic, undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + id: 123, + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + '{"id":123,"jsonrpc":"2.0"}', + ); + }); + }); + + it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited', + ); + }); + }); + } else { + it('throws an error with a custom message if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited.', + ); + }); + }); + + it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + "Non-200 status code: '429'", + ); + }); + }); + } + + it('throws an undescriptive error message if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + id: 12345, + jsonrpc: '2.0', + error: 'some error', + httpStatus: 420, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + const msg = + providerType === 'infura' + ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' + : "Non-200 status code: '420'"; + await expect(promiseForResult).rejects.toThrow(msg); + }); + }); + + [503, 504].forEach((httpStatus) => { + it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + error: 'some error', + httpStatus, + }, + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + // Both the Infura middleware and custom RPC middleware detect a 503 or + // 504 response and retry the request to the RPC endpoint automatically + // but differ in what sort of response is returned when the number of + // retries is exhausted. + if (providerType === 'infura') { + it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + error: 'Some error', + httpStatus, + }, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout'), + ); + }); + }); + } else { + it(`produces an empty response if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + error: 'Some error', + httpStatus, + }, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + }); + + it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'ETIMEDOUT: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + // Both the Infura and fetch middleware detect ETIMEDOUT errors and will + // automatically retry the request to the RPC endpoint in question, but each + // produces a different error if the number of retries is exhausted. + if (providerType === 'infura') { + it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + } else { + it('produces an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'ETIMEDOUT: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // The Infura middleware treats a response that contains an ECONNRESET + // message as an innocuous error that is likely to disappear on a retry. The + // custom RPC middleware, on the other hand, does not specially handle this + // error. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'ECONNRESET: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + } + + // Both the Infura and fetch middleware will attempt to parse the response + // body as JSON, and if this step produces an error, both middleware will + // also attempt to retry the request. However, this error handling code is + // slightly different between the two. As the error in this case is a + // SyntaxError, the Infura middleware will catch it immediately, whereas the + // custom RPC middleware will catch it and re-throw a separate error, which + // it then catches later. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if a "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'SyntaxError: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('causes a request to fail with a custom error if a "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + + it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'failed to parse response body: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + + it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'failed to parse response body: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('produces an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'failed to parse response body: some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // Only the custom RPC middleware will detect a "Failed to fetch" error and + // attempt to retry the request to the RPC endpoint; the Infura middleware + // does not. + if (providerType === 'infura') { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + const errorMessage = 'Failed to fetch: Some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + }); + + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + params: buildMockParams({ blockParam, blockParamIndex }), + }; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + // + // Here we have the request fail for the first 4 tries, then + // succeed on the 5th try. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: 'Failed to fetch: Some message', + times: 4, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('produces an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: some message'; + + // The first time a block-cacheable request is made, the + // block-cache middleware will request the latest block number + // through the block tracker to determine the cache key. Later, + // the block-ref middleware will request the latest block number + // again to resolve the value of "latest", but the block number is + // cached once made, so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + }); + + describe.each([ + ['given a block tag of "earliest"', 'earliest', 'earliest'], + ['given a block number', 'block number', '0x100'], + ])('%s', (_desc, blockParamType, blockParam) => { + it('does not hit the RPC endpoint more than once for identical requests', async () => { + const requests = [ + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the block-cache + // middleware will request the latest block number through the block + // tracker to determine the cache key. This block number doesn't + // matter. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[0]]); + }); + }); + + for (const paramIndex of [...Array(numberOfParameters).keys()]) { + if (paramIndex === blockParamIndex) { + // testing changes in block param is covered under later tests + continue; + } + it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { + const firstMockParams = [ + ...new Array(numberOfParameters).fill('some value'), + ]; + firstMockParams[blockParamIndex] = blockParam; + const secondMockParams = firstMockParams.slice(); + secondMockParams[paramIndex] = 'another value'; + const requests = [ + { + method, + params: firstMockParams, + }, + { method, params: secondMockParams }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the block-cache + // middleware will request the latest block number through the block + // tracker to determine the cache key. Later, the block-ref + // middleware will request the latest block number again to resolve + // the value of "latest", but the block number is cached once made, + // so we only need to mock the request once. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); + // The block-ref middleware will make the request as specified + // except that the block param is replaced with the latest block + // number. + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[1]]); + }); + }); + } + + it('reuses the result of a previous request even if the latest block number was updated since', async () => { + const requests = [ + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // Note that we have to mock these requests in a specific order. The + // first block tracker request occurs because of the first RPC + // request. The second block tracker request, however, does not + // occur because of the second RPC request, but rather because we + // call `clock.runAll()` below. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a + // new block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[0]]); + }); + }); + + if (blockParamType === 'earliest') { + it('treats "0x00" as a synonym for "earliest"', async () => { + const requests = [ + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { + method, + params: buildMockParams({ blockParamIndex, blockParam: '0x00' }), + }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest + // block number is retrieved through the block tracker first. It + // doesn't matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[0]]); + }); + }); + + for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + it(`does not retry an empty response of "${emptyValue}"`, async () => { + const request = { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }; + const mockResult = emptyValue; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { result: mockResult }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual(mockResult); + }); + }); + + it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { + const requests = [ + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { + method, + params: buildMockParams({ blockParamIndex, blockParam }), + }, + ]; + const mockResults = [emptyValue, 'some result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + } + } + + if (blockParamType === 'block number') { + it('does not reuse the result of a previous request if it was made with different arguments than this one', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const requests = [ + { + method, + params: buildMockParams({ blockParamIndex, blockParam: '0x100' }), + }, + { + method, + params: buildMockParams({ blockParamIndex, blockParam: '0x200' }), + }, + ]; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: 'first result' }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: 'second result' }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(['first result', 'second result']); + }); + }); + + describe.each( + [ + ['less than the current block number', '0x200'], + ['equal to the curent block number', '0x100'], + ], + '%s', + (_nestedDesc, currentBlockNumber) => { + it('makes an additional request to the RPC endpoint', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }; + + // The first time a block-cacheable request is made, the latest + // block number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ + blockNumber: currentBlockNumber, + }); + comms.mockRpcCall({ + request, + response: { result: 'the result' }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + if (providerType === 'infura') { + it(`retries up to 10 times if a "${emptyValue}" response is returned, returning successful non-empty response if there is one on the 10th try`, async () => { + const request = { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }; + + await withMockedCommunications( + { providerType }, + async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ + blockNumber: currentBlockNumber, + }); + comms.mockRpcCall({ + request, + response: { result: emptyValue }, + times: 9, + }); + comms.mockRpcCall({ + request, + response: { result: 'some value' }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall, clock }) => + waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ), + ); + + expect(result).toStrictEqual('some value'); + }, + ); + }); + + it(`retries up to 10 times if a "${emptyValue}" response is returned, failing after the 10th try`, async () => { + const request = { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }; + const mockResult = emptyValue; + + await withMockedCommunications( + { providerType }, + async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ + blockNumber: currentBlockNumber, + }); + comms.mockRpcCall({ + request, + response: { result: mockResult }, + times: 10, + }); + + const promiseForResult = withNetworkClient( + { providerType }, + ({ makeRpcCall, clock }) => + waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ), + ); + + await expect(promiseForResult).rejects.toThrow( + 'RetryOnEmptyMiddleware - retries exhausted', + ); + }, + ); + }); + } else { + it(`does not retry an empty response of "${emptyValue}"`, async () => { + const request = { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }; + const mockResult = emptyValue; + + await withMockedCommunications( + { providerType }, + async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ + blockNumber: currentBlockNumber, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { result: mockResult }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual(mockResult); + }, + ); + }); + + it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { + const requests = [ + { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }, + ]; + const mockResults = [emptyValue, { blockHash: '0x100' }]; + + await withMockedCommunications( + { providerType }, + async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ + blockNumber: currentBlockNumber, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[1], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => + makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }, + ); + }); + } + } + }, + ); + + describe('greater than the current block number', () => { + it('makes an additional request to the RPC endpoint', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }; + + // The first time a block-cacheable request is made, the latest + // block number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); + comms.mockRpcCall({ + request, + response: { result: 'the result' }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + it(`does not retry an empty response of "${emptyValue}"`, async () => { + const request = { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }; + const mockResult = emptyValue; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + request, + blockParamIndex, + '0x100', + ), + response: { result: mockResult }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual(mockResult); + }); + }); + + it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { + const requests = [ + { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }, + { + method, + // Note that `blockParam` is `0x100` here + params: buildMockParams({ blockParamIndex, blockParam }), + }, + ]; + const mockResults = [emptyValue, { blockHash: '0x100' }]; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[0], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: buildRequestWithReplacedBlockParam( + requests[1], + blockParamIndex, + '0x100', + ), + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + } + }); + } + }); + + describe('given a block tag of "pending"', () => { + const params = buildMockParams({ blockParamIndex, blockParam: 'pending' }); + + it('hits the RPC endpoint on all calls and does not cache anything', async () => { + const requests = [ + { method, params }, + { method, params }, + ]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest + // block number is retrieved through the block tracker first. It + // doesn't matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + }); +} diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.js b/app/scripts/controllers/network/provider-api-tests/helpers.js index f8deb72f8..afa91a7bb 100644 --- a/app/scripts/controllers/network/provider-api-tests/helpers.js +++ b/app/scripts/controllers/network/provider-api-tests/helpers.js @@ -1,10 +1,7 @@ import nock from 'nock'; import sinon from 'sinon'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import { providerFromEngine } from '@metamask/eth-json-rpc-middleware'; import EthQuery from 'eth-query'; -import createInfuraClient from '../createInfuraClient'; -import createJsonRpcClient from '../createJsonRpcClient'; +import { createNetworkClient } from '../create-network-client'; /** * @typedef {import('nock').Scope} NockScope @@ -13,55 +10,6 @@ import createJsonRpcClient from '../createJsonRpcClient'; * base URL. */ -/** - * @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial) => Promise, makeRpcCallsInSeries: (requests: Partial[]) => Promise}} Client - * - * Provides methods to interact with the suite of middleware that - * `createInfuraClient` or `createJsonRpcClient` exposes. - */ - -/** - * @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions - * - * The options bag that `withNetworkClient` takes. - */ - -/** - * @typedef {(client: Client) => Promise} WithClientCallback - * - * The callback that `withNetworkClient` takes. - */ - -/** - * @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions - * - * The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`. - */ - -/** - * @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions - * - * The options to `mockRpcCall`. - */ - -/** - * @typedef {{mockNextBlockTrackerRequest: (options: Omit) => void, mockAllBlockTrackerRequests: (options: Omit) => void, mockRpcCall: (options: Omit) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications - * - * Provides methods to mock different kinds of requests to the provider. - */ - -/** - * @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions - * - * The options bag that `Communications` takes. - */ - -/** - * @typedef {(comms: Communications) => Promise} WithMockedCommunicationsCallback - * - * The callback that `mockingCommunications` takes. - */ - /** * A dummy value for the `infuraProjectId` option that `createInfuraClient` * needs. (Infura should not be hit during tests, but just in case, this should @@ -82,6 +30,12 @@ const MOCK_RPC_URL = 'http://foo.com'; */ const DEFAULT_LATEST_BLOCK_NUMBER = '0x42'; +/** + * A reference to the original `setTimeout` function so that we can use it even + * when using fake timers. + */ +const originalSetTimeout = setTimeout; + /** * If you're having trouble writing a test and you're wondering why the test * keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This @@ -103,14 +57,17 @@ function debug(...args) { */ function buildScopeForMockingRequests(rpcUrl) { return nock(rpcUrl).filteringRequestBody((body) => { - const copyOfBody = JSON.parse(body); - // Some IDs are random, so remove them entirely from the request to make it - // possible to mock these requests - delete copyOfBody.id; - return JSON.stringify(copyOfBody); + debug('Nock Received Request: ', body); + return body; }); } +/** + * @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions + * + * The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`. + */ + /** * Mocks the next request for the latest block that the block tracker will make. * @@ -151,6 +108,12 @@ async function mockAllBlockTrackerRequests({ }).persist(); } +/** + * @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions + * + * The options to `mockRpcCall`. + */ + /** * Mocks a JSON-RPC request sent to the provider with the given response. * Provider type is inferred from the base url set on the nockScope. @@ -177,24 +140,38 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { // eth-query always passes `params`, so even if we don't supply this property, // for consistency with makeRpcCall, assume that the `body` contains it const { method, params = [], ...rest } = request; - const httpStatus = response?.httpStatus ?? 200; - let completeResponse; + let httpStatus = 200; + let completeResponse = { id: 2, jsonrpc: '2.0' }; if (response !== undefined) { - if (response.body === undefined) { - completeResponse = { id: 1, jsonrpc: '2.0' }; - ['id', 'jsonrpc', 'result', 'error'].forEach((prop) => { - if (response[prop] !== undefined) { - completeResponse[prop] = response[prop]; - } - }); - } else { + if ('body' in response) { completeResponse = response.body; + } else { + if (response.error) { + completeResponse.error = response.error; + } else { + completeResponse.result = response.result; + } + if (response.httpStatus) { + httpStatus = response.httpStatus; + } } } const url = nockScope.basePath.includes('infura.io') ? `/v3/${MOCK_INFURA_PROJECT_ID}` : '/'; + + debug('Mocking request:', { + url, + method, + params, + response, + error, + ...rest, + times, + }); + let nockRequest = nockScope.post(url, { + id: /\d*/u, jsonrpc: '2.0', method, params, @@ -212,7 +189,17 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { if (error !== undefined) { return nockRequest.replyWithError(error); } else if (completeResponse !== undefined) { - return nockRequest.reply(httpStatus, completeResponse); + return nockRequest.reply(httpStatus, (_, requestBody) => { + if (response !== undefined && !('body' in response)) { + if (response.id === undefined) { + completeResponse.id = requestBody.id; + } else { + completeResponse.id = response.id; + } + } + debug('Nock returning Response', completeResponse); + return completeResponse; + }); } return nockRequest; } @@ -240,6 +227,24 @@ function makeRpcCall(ethQuery, request) { }); } +/** + * @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions + * + * The options bag that `Communications` takes. + */ + +/** + * @typedef {{mockNextBlockTrackerRequest: (options: Omit) => void, mockAllBlockTrackerRequests: (options: Omit) => void, mockRpcCall: (options: Omit) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications + * + * Provides methods to mock different kinds of requests to the provider. + */ + +/** + * @typedef {(comms: Communications) => Promise} WithMockedCommunicationsCallback + * + * The callback that `mockingCommunications` takes. + */ + /** * Sets up request mocks for requests to the provider. * @@ -275,6 +280,7 @@ export async function withMockedCommunications( mockAllBlockTrackerRequests({ nockScope, ...localOptions }); const curriedMockRpcCall = (localOptions) => mockRpcCall({ nockScope, ...localOptions }); + const comms = { mockNextBlockTrackerRequest: curriedMockNextBlockTrackerRequest, mockAllBlockTrackerRequests: curriedMockAllBlockTrackerRequests, @@ -291,6 +297,71 @@ export async function withMockedCommunications( } } +/** + * @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial) => Promise, makeRpcCallsInSeries: (requests: Partial[]) => Promise}} MockNetworkClient + * + * Provides methods to interact with the suite of middleware that + * `createInfuraClient` or `createJsonRpcClient` exposes. + */ + +/** + * Some middleware contain logic which retries the request if some condition + * applies. This retrying always happens out of band via `setTimeout`, and + * because we are stubbing time via Jest's fake timers, we have to manually + * advance the clock so that the `setTimeout` handlers get fired. We don't know + * when these timers will get created, however, so we have to keep advancing + * timers until the request has been made an appropriate number of times. + * Unfortunately we don't have a good way to know how many times a request has + * been retried, but the good news is that the middleware won't end, and thus + * the promise which the RPC call returns won't get fulfilled, until all retries + * have been made. + * + * @param promise - The promise which is returned by the RPC call. + * @param clock - A Sinon clock object which can be used to advance to the next + * `setTimeout` handler. + */ +export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( + promise, + clock, +) { + let hasPromiseBeenFulfilled = false; + let numTimesClockHasBeenAdvanced = 0; + + promise + .catch((error) => { + // This is used to silence Node.js warnings about the rejection + // being handled asynchronously. The error is handled later when + // `promise` is awaited, but we log it here anyway in case it gets + // swallowed. + debug(error); + }) + .finally(() => { + hasPromiseBeenFulfilled = true; + }); + + // `hasPromiseBeenFulfilled` is modified asynchronously. + /* eslint-disable-next-line no-unmodified-loop-condition */ + while (!hasPromiseBeenFulfilled && numTimesClockHasBeenAdvanced < 15) { + clock.runAll(); + await new Promise((resolve) => originalSetTimeout(resolve, 10)); + numTimesClockHasBeenAdvanced += 1; + } + + return promise; +} + +/** + * @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions + * + * The options bag that `withNetworkClient` takes. + */ + +/** + * @typedef {(client: MockNetworkClient) => Promise} WithClientCallback + * + * The callback that `withNetworkClient` takes. + */ + /** * Builds a provider from the middleware (for the provider type) along with a * block tracker, runs the given function with those two things, and then @@ -325,6 +396,13 @@ export async function withNetworkClient( ); } + // Faking timers ends up doing two things: + // 1. Halting the block tracker (which depends on `setTimeout` to periodically + // request the latest block) set up in `eth-json-rpc-middleware` + // 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which also + // depends on `setTimeout`) + const clock = sinon.useFakeTimers(); + // The JSON-RPC client wraps `eth_estimateGas` so that it takes 2 seconds longer // than it usually would to complete. Or at least it should — this doesn't // appear to be working correctly. Unset `IN_TEST` on `process.env` to prevent @@ -333,20 +411,21 @@ export async function withNetworkClient( delete process.env.IN_TEST; const clientUnderTest = providerType === 'infura' - ? createInfuraClient({ + ? createNetworkClient({ network: infuraNetwork, - projectId: MOCK_INFURA_PROJECT_ID, + infuraProjectId: MOCK_INFURA_PROJECT_ID, + type: 'infura', }) - : createJsonRpcClient({ rpcUrl: customRpcUrl, chainId: customChainId }); + : createNetworkClient({ + chainId: customChainId, + rpcUrl: customRpcUrl, + type: 'custom', + }); process.env.IN_TEST = inTest; - const { networkMiddleware, blockTracker } = clientUnderTest; + const { provider, blockTracker } = clientUnderTest; - const engine = new JsonRpcEngine(); - engine.push(networkMiddleware); - const provider = providerFromEngine(engine); const ethQuery = new EthQuery(provider); - const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); const makeRpcCallsInSeries = async (requests) => { const responses = []; @@ -355,12 +434,7 @@ export async function withNetworkClient( } return responses; }; - // Faking timers ends up doing two things: - // 1. Halting the block tracker (which depends on `setTimeout` to periodically - // request the latest block) set up in `eth-json-rpc-middleware` - // 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which also - // depends on `setTimeout`) - const clock = sinon.useFakeTimers(); + const client = { blockTracker, clock, diff --git a/app/scripts/controllers/network/provider-api-tests/no-block-param.js b/app/scripts/controllers/network/provider-api-tests/no-block-param.js new file mode 100644 index 000000000..08ae7edd0 --- /dev/null +++ b/app/scripts/controllers/network/provider-api-tests/no-block-param.js @@ -0,0 +1,968 @@ +/* eslint-disable jest/require-top-level-describe, jest/no-export */ + +import { + waitForPromiseToBeFulfilledAfterRunningAllTimers, + withMockedCommunications, + withNetworkClient, +} from './helpers'; +import { + buildFetchFailedErrorMessage, + buildInfuraClientRetriesExhaustedErrorMessage, + buildJsonRpcEngineEmptyResponseErrorMessage, +} from './shared-tests'; + +/** + * Defines tests which exercise the behavior exhibited by an RPC method which is + * assumed to not take a block parameter. Even if it does, the value of this + * parameter will not be used in determining how to cache the method. + * + * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom` (default: "infura"). + */ +export function testsForRpcMethodAssumingNoBlockParam( + method, + { numberOfParameters, providerType }, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } + + it('does not hit the RPC endpoint more than once for identical requests', async () => { + const requests = [{ method }, { method }]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[0]]); + }); + }); + + for (const paramIndex of [...Array(numberOfParameters).keys()]) { + it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { + const firstMockParams = [ + ...new Array(numberOfParameters).fill('some value'), + ]; + const secondMockParams = firstMockParams.slice(); + secondMockParams[paramIndex] = 'another value'; + const requests = [ + { + method, + params: firstMockParams, + }, + { method, params: secondMockParams }, + ]; + const mockResults = ['some result', 'another result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual([mockResults[0], mockResults[1]]); + }); + }); + } + + it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { + const requests = [{ method }, { method }]; + const mockResults = ['first result', 'second result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // Note that we have to mock these requests in a specific order. The + // first block tracker request occurs because of the first RPC request. + // The second block tracker request, however, does not occur because of + // the second RPC request, but rather because we call `clock.runAll()` + // below. + comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + async (client) => { + const firstResult = await client.makeRpcCall(requests[0]); + // Proceed to the next iteration of the block tracker so that a new + // block is fetched and the current block is updated. + client.clock.runAll(); + const secondResult = await client.makeRpcCall(requests[1]); + return [firstResult, secondResult]; + }, + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + + for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + it(`does not retry an empty response of "${emptyValue}"`, async () => { + const request = { method }; + const mockResult = emptyValue; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { result: mockResult }, + }); + + const result = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(result).toStrictEqual(mockResult); + }); + }); + + it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { + const requests = [{ method }, { method }]; + const mockResults = [emptyValue, 'some result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + }); + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + const results = await withNetworkClient( + { providerType }, + ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), + ); + + expect(results).toStrictEqual(mockResults); + }); + }); + } + + it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => { + const requests = [{ method }, { method }, { method }]; + const mockResults = ['first result', 'second result', 'third result']; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request: requests[0], + response: { result: mockResults[0] }, + delay: 100, + }); + + comms.mockRpcCall({ + request: requests[1], + response: { result: mockResults[1] }, + }); + + comms.mockRpcCall({ + request: requests[2], + response: { result: mockResults[2] }, + }); + + const results = await withNetworkClient( + { providerType }, + async (client) => { + const resultPromises = [ + client.makeRpcCall(requests[0]), + client.makeRpcCall(requests[1]), + client.makeRpcCall(requests[2]), + ]; + const firstResult = await resultPromises[0]; + // The inflight cache middleware uses setTimeout to run the handlers, + // so run them now + client.clock.runAll(); + const remainingResults = await Promise.all(resultPromises.slice(1)); + return [firstResult, ...remainingResults]; + }, + ); + + expect(results).toStrictEqual([ + mockResults[0], + mockResults[0], + mockResults[0], + ]); + }); + }); + + it('throws a custom error if the request to the RPC endpoint returns a 405 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 405, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'The method does not exist / is not available', + ); + }); + }); + + // There is a difference in how we are testing the Infura middleware vs. the + // custom RPC middleware (or, more specifically, the fetch middleware) because + // of what both middleware treat as rate limiting errors. In this case, the + // fetch middleware treats a 418 response from the RPC endpoint as such an + // error, whereas to the Infura middleware, it is a 429 response. + if (providerType === 'infura') { + it('throws an undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { id: 123, method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + '{"id":123,"jsonrpc":"2.0"}', + ); + }); + }); + + it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited', + ); + }); + }); + } else { + it('throws a custom error if the request to the RPC endpoint returns a 418 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 418, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + 'Request is being rate limited.', + ); + }); + }); + + it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + httpStatus: 429, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + "Non-200 status code: '429'", + ); + }); + }); + } + + it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + id: 12345, + jsonrpc: '2.0', + error: 'some error', + httpStatus: 420, + }, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + const errorMessage = + providerType === 'infura' + ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' + : "Non-200 status code: '420'"; + await expect(promiseForResult).rejects.toThrow(errorMessage); + }); + }); + + [503, 504].forEach((httpStatus) => { + it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + response: { + error: 'Some error', + httpStatus, + }, + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { + error: 'Some error', + httpStatus, + }, + times: 5, + }); + comms.mockNextBlockTrackerRequest(); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + const err = + providerType === 'infura' + ? buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout') + : buildJsonRpcEngineEmptyResponseErrorMessage(method); + await expect(promiseForResult).rejects.toThrow(err); + }); + }); + }); + + it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'ETIMEDOUT: Some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + // Both the Infura and fetch middleware detect ETIMEDOUT errors and will + // automatically retry the request to the RPC endpoint in question, but both + // produce a different error if the number of retries is exhausted. + if (providerType === 'infura') { + it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + } else { + it('returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ETIMEDOUT: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // The Infura middleware treats a response that contains an ECONNRESET message + // as an innocuous error that is likely to disappear on a retry. The custom + // RPC middleware, on the other hand, does not specially handle this error. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'ECONNRESET: Some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { method }; + const errorMessage = 'ECONNRESET: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + } + + // Both the Infura and fetch middleware will attempt to parse the response + // body as JSON, and if this step produces an error, both middleware will also + // attempt to retry the request. However, this error handling code is slightly + // different between the two. As the error in this case is a SyntaxError, the + // Infura middleware will catch it immediately, whereas the custom RPC + // middleware will catch it and re-throw a separate error, which it then + // catches later. + if (providerType === 'infura') { + it('retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'SyntaxError: Some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), + ); + }); + }); + + it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'failed to parse response body: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { + const customRpcUrl = 'http://example.com'; + + await withMockedCommunications( + { providerType, customRpcUrl }, + async (comms) => { + const request = { method }; + const errorMessage = 'SyntaxError: Some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, customRpcUrl }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(customRpcUrl, errorMessage), + ); + }, + ); + }); + + it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'failed to parse response body: some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'failed to parse response body: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } + + // Only the custom RPC middleware will detect a "Failed to fetch" error and + // attempt to retry the request to the RPC endpoint; the Infura middleware + // does not. + if (providerType === 'infura') { + it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + }); + const promiseForResult = withNetworkClient( + { providerType, infuraNetwork: comms.infuraNetwork }, + async ({ makeRpcCall }) => makeRpcCall(request), + ); + + await expect(promiseForResult).rejects.toThrow( + buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), + ); + }); + }); + } else { + it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + // Here we have the request fail for the first 4 tries, then succeed + // on the 5th try. + comms.mockRpcCall({ + request, + error: 'Failed to fetch: some message', + times: 4, + }); + comms.mockRpcCall({ + request, + response: { + result: 'the result', + httpStatus: 200, + }, + }); + + const result = await withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + expect(result).toStrictEqual('the result'); + }); + }); + + it('returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { + await withMockedCommunications({ providerType }, async (comms) => { + const request = { method }; + const errorMessage = 'Failed to fetch: some message'; + + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + error: errorMessage, + times: 5, + }); + const promiseForResult = withNetworkClient( + { providerType }, + async ({ makeRpcCall, clock }) => { + return await waitForPromiseToBeFulfilledAfterRunningAllTimers( + makeRpcCall(request), + clock, + ); + }, + ); + + await expect(promiseForResult).rejects.toThrow( + buildJsonRpcEngineEmptyResponseErrorMessage(method), + ); + }); + }); + } +} diff --git a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js new file mode 100644 index 000000000..693d9f779 --- /dev/null +++ b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js @@ -0,0 +1,51 @@ +/* eslint-disable jest/require-top-level-describe, jest/no-export */ + +import { fill } from 'lodash'; +import { withMockedCommunications, withNetworkClient } from './helpers'; + +/** + * Defines tests which exercise the behavior exhibited by an RPC method that + * is not handled specially by the network client middleware. + * + * @param method - The name of the RPC method under test. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.providerType - The type of provider being tested; + * either `infura` or `custom`. + * @param additionalArgs.numberOfParameters - The number of parameters that this + * RPC method takes. + */ +export function testsForRpcMethodNotHandledByMiddleware( + method, + { providerType, numberOfParameters }, +) { + if (providerType !== 'infura' && providerType !== 'custom') { + throw new Error( + `providerType must be either "infura" or "custom", was "${providerType}" instead`, + ); + } + + it('attempts to pass the request off to the RPC endpoint', async () => { + const request = { + method, + params: fill(Array(numberOfParameters), 'some value'), + }; + const expectedResult = 'the result'; + + await withMockedCommunications({ providerType }, async (comms) => { + // The first time a block-cacheable request is made, the latest block + // number is retrieved through the block tracker first. It doesn't + // matter what this is — it's just used as a cache key. + comms.mockNextBlockTrackerRequest(); + comms.mockRpcCall({ + request, + response: { result: expectedResult }, + }); + const actualResult = await withNetworkClient( + { providerType }, + ({ makeRpcCall }) => makeRpcCall(request), + ); + + expect(actualResult).toStrictEqual(expectedResult); + }); + }); +} diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.js b/app/scripts/controllers/network/provider-api-tests/shared-tests.js index 4fb997915..04412d3f0 100644 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.js +++ b/app/scripts/controllers/network/provider-api-tests/shared-tests.js @@ -1,14 +1,10 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export, jest/no-identical-title */ -import { fill } from 'lodash'; -import { - withMockedCommunications, - withNetworkClient, - buildMockParams, - buildRequestWithReplacedBlockParam, -} from './helpers'; - -const originalSetTimeout = setTimeout; +import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response'; +import { testsForRpcMethodSupportingBlockParam } from './block-param'; +import { withMockedCommunications, withNetworkClient } from './helpers'; +import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param'; +import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middleware'; /** * Constructs an error message that the Infura client would produce in the event @@ -17,7 +13,7 @@ const originalSetTimeout = setTimeout; * @param reason - The exact reason for failure. * @returns The error message. */ -function buildInfuraClientRetriesExhaustedErrorMessage(reason) { +export function buildInfuraClientRetriesExhaustedErrorMessage(reason) { return new RegExp( `^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`, 'us', @@ -31,7 +27,7 @@ function buildInfuraClientRetriesExhaustedErrorMessage(reason) { * @param method - The RPC method. * @returns The error message. */ -function buildJsonRpcEngineEmptyResponseErrorMessage(method) { +export function buildJsonRpcEngineEmptyResponseErrorMessage(method) { return new RegExp( `^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`, 'us', @@ -46,57 +42,13 @@ function buildJsonRpcEngineEmptyResponseErrorMessage(method) { * @param reason - The reason. * @returns The error message. */ -function buildFetchFailedErrorMessage(url, reason) { +export function buildFetchFailedErrorMessage(url, reason) { return new RegExp( `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, 'us', ); } -/** - * Some middleware contain logic which retries the request if some condition - * applies. This retrying always happens out of band via `setTimeout`, and - * because we are stubbing time via Jest's fake timers, we have to manually - * advance the clock so that the `setTimeout` handlers get fired. We don't know - * when these timers will get created, however, so we have to keep advancing - * timers until the request has been made an appropriate number of times. - * Unfortunately we don't have a good way to know how many times a request has - * been retried, but the good news is that the middleware won't end, and thus - * the promise which the RPC call returns won't get fulfilled, until all retries - * have been made. - * - * @param promise - The promise which is returned by the RPC call. - * @param clock - A Sinon clock object which can be used to advance to the next - * `setTimeout` handler. - */ -async function waitForPromiseToBeFulfilledAfterRunningAllTimers( - promise, - clock, -) { - let hasPromiseBeenFulfilled = false; - let numTimesClockHasBeenAdvanced = 0; - - promise - .catch(() => { - // This is used to silence Node.js warnings about the rejection - // being handled asynchronously. The error is handled later when - // `promise` is awaited. - }) - .finally(() => { - hasPromiseBeenFulfilled = true; - }); - - // `isPromiseFulfilled` is modified asynchronously. - /* eslint-disable-next-line no-unmodified-loop-condition */ - while (!hasPromiseBeenFulfilled && numTimesClockHasBeenAdvanced < 15) { - clock.runAll(); - await new Promise((resolve) => originalSetTimeout(resolve, 10)); - numTimesClockHasBeenAdvanced += 1; - } - - return promise; -} - /** * Defines tests that are common to both the Infura and JSON-RPC network client. * @@ -105,7 +57,6 @@ async function waitForPromiseToBeFulfilledAfterRunningAllTimers( * exposed by `createInfuraClient` is tested; if `custom`, then the middleware * exposed by `createJsonRpcClient` will be tested. */ -/* eslint-disable-next-line jest/no-export */ export function testsForProviderType(providerType) { // Ethereum JSON-RPC spec: // Infura documentation: @@ -113,33 +64,40 @@ export function testsForProviderType(providerType) { describe('methods included in the Ethereum JSON-RPC spec', () => { describe('methods not handled by middleware', () => { const notHandledByMiddleware = [ - { name: 'eth_accounts', numberOfParameters: 0 }, - { name: 'eth_coinbase', numberOfParameters: 0 }, - { name: 'eth_createAccessList', numberOfParameters: 2 }, - { name: 'eth_feeHistory', numberOfParameters: 3 }, + // This list is presented in the same order as in the network client + // tests on the core side. + + { name: 'eth_newFilter', numberOfParameters: 1 }, { name: 'eth_getFilterChanges', numberOfParameters: 1 }, + { name: 'eth_newBlockFilter', numberOfParameters: 0 }, + { name: 'eth_newPendingTransactionFilter', numberOfParameters: 0 }, + { name: 'eth_uninstallFilter', numberOfParameters: 1 }, + + { name: 'eth_sendRawTransaction', numberOfParameters: 1 }, + { name: 'eth_sendTransaction', numberOfParameters: 1 }, + { name: 'eth_sign', numberOfParameters: 2 }, + + { name: 'eth_createAccessList', numberOfParameters: 2 }, { name: 'eth_getLogs', numberOfParameters: 1 }, { name: 'eth_getProof', numberOfParameters: 3 }, { name: 'eth_getWork', numberOfParameters: 0 }, - { name: 'eth_hashrate', numberOfParameters: 0 }, { name: 'eth_maxPriorityFeePerGas', numberOfParameters: 0 }, - { name: 'eth_mining', numberOfParameters: 0 }, - { name: 'eth_newBlockFilter', numberOfParameters: 0 }, - { name: 'eth_newFilter', numberOfParameters: 1 }, - { name: 'eth_newPendingTransactionFilter', numberOfParameters: 0 }, - { name: 'eth_sendRawTransaction', numberOfParameters: 1 }, - { name: 'eth_signTransaction', numberOfParameters: 1 }, - { name: 'eth_sendTransaction', numberOfParameters: 1 }, - { name: 'eth_sign', numberOfParameters: 2 }, { name: 'eth_submitHashRate', numberOfParameters: 2 }, { name: 'eth_submitWork', numberOfParameters: 3 }, { name: 'eth_syncing', numberOfParameters: 0 }, - { name: 'eth_uninstallFilter', numberOfParameters: 1 }, + { name: 'eth_feeHistory', numberOfParameters: 3 }, { name: 'debug_getRawHeader', numberOfParameters: 1 }, { name: 'debug_getRawBlock', numberOfParameters: 1 }, { name: 'debug_getRawTransaction', numberOfParameters: 1 }, { name: 'debug_getRawReceipts', numberOfParameters: 1 }, { name: 'debug_getBadBlocks', numberOfParameters: 0 }, + + { name: 'eth_accounts', numberOfParameters: 0 }, + { name: 'eth_coinbase', numberOfParameters: 0 }, + { name: 'eth_hashrate', numberOfParameters: 0 }, + { name: 'eth_mining', numberOfParameters: 0 }, + + { name: 'eth_signTransaction', numberOfParameters: 1 }, ]; notHandledByMiddleware.forEach(({ name, numberOfParameters }) => { describe(`method name: ${name}`, () => { @@ -151,6 +109,63 @@ export function testsForProviderType(providerType) { }); }); + describe('methods with block hashes in their result', () => { + const methodsWithBlockHashInResponse = [ + { name: 'eth_getTransactionByHash', numberOfParameters: 1 }, + { name: 'eth_getTransactionReceipt', numberOfParameters: 1 }, + ]; + methodsWithBlockHashInResponse.forEach(({ name, numberOfParameters }) => { + describe(`method name: ${name}`, () => { + testsForRpcMethodsThatCheckForBlockHashInResponse(name, { + numberOfParameters, + providerType, + }); + }); + }); + }); + + describe('methods that assume there is no block param', () => { + const assumingNoBlockParam = [ + { name: 'eth_getFilterLogs', numberOfParameters: 1 }, + { name: 'eth_blockNumber', numberOfParameters: 0 }, + { name: 'eth_estimateGas', numberOfParameters: 2 }, + { name: 'eth_gasPrice', numberOfParameters: 0 }, + { name: 'eth_getBlockByHash', numberOfParameters: 2 }, + { + name: 'eth_getBlockTransactionCountByHash', + numberOfParameters: 1, + }, + { + name: 'eth_getTransactionByBlockHashAndIndex', + numberOfParameters: 2, + }, + { name: 'eth_getUncleByBlockHashAndIndex', numberOfParameters: 2 }, + { name: 'eth_getUncleCountByBlockHash', numberOfParameters: 1 }, + ]; + const blockParamIgnored = [ + { name: 'eth_getUncleCountByBlockNumber', numberOfParameters: 1 }, + { name: 'eth_getUncleByBlockNumberAndIndex', numberOfParameters: 2 }, + { + name: 'eth_getTransactionByBlockNumberAndIndex', + numberOfParameters: 2, + }, + { + name: 'eth_getBlockTransactionCountByNumber', + numberOfParameters: 1, + }, + ]; + assumingNoBlockParam + .concat(blockParamIgnored) + .forEach(({ name, numberOfParameters }) => + describe(`method name: ${name}`, () => { + testsForRpcMethodAssumingNoBlockParam(name, { + providerType, + numberOfParameters, + }); + }), + ); + }); + describe('methods that have a param to specify the block', () => { const supportingBlockParam = [ { @@ -193,70 +208,6 @@ export function testsForProviderType(providerType) { ); }); - describe('methods that assume there is no block param', () => { - const assumingNoBlockParam = [ - { name: 'eth_blockNumber', numberOfParameters: 0 }, - { name: 'eth_estimateGas', numberOfParameters: 2 }, - { name: 'eth_gasPrice', numberOfParameters: 0 }, - { name: 'eth_getBlockByHash', numberOfParameters: 2 }, - // NOTE: eth_getBlockTransactionCountByNumber does take a block param at - // the 0th index, but this is not handled by our cache middleware - // currently - { - name: 'eth_getBlockTransactionCountByNumber', - numberOfParameters: 1, - }, - // NOTE: eth_getTransactionByBlockNumberAndIndex does take a block param - // at the 0th index, but this is not handled by our cache middleware - // currently - { - name: 'eth_getTransactionByBlockNumberAndIndex', - numberOfParameters: 2, - }, - { - name: 'eth_getBlockTransactionCountByHash', - numberOfParameters: 1, - }, - { name: 'eth_getFilterLogs', numberOfParameters: 1 }, - { - name: 'eth_getTransactionByBlockHashAndIndex', - numberOfParameters: 2, - }, - { name: 'eth_getUncleByBlockHashAndIndex', numberOfParameters: 2 }, - // NOTE: eth_getUncleByBlockNumberAndIndex does take a block param at - // the 0th index, but this is not handled by our cache middleware - // currently - { name: 'eth_getUncleByBlockNumberAndIndex', numberOfParameters: 2 }, - { name: 'eth_getUncleCountByBlockHash', numberOfParameters: 1 }, - // NOTE: eth_getUncleCountByBlockNumber does take a block param at the - // 0th index, but this is not handled by our cache middleware currently - { name: 'eth_getUncleCountByBlockNumber', numberOfParameters: 1 }, - ]; - assumingNoBlockParam.forEach(({ name, numberOfParameters }) => - describe(`method name: ${name}`, () => { - testsForRpcMethodAssumingNoBlockParam(name, { - providerType, - numberOfParameters, - }); - }), - ); - }); - - describe('methods with block hashes in their result', () => { - const methodsWithBlockHashInResponse = [ - { name: 'eth_getTransactionByHash', numberOfParameters: 1 }, - { name: 'eth_getTransactionReceipt', numberOfParameters: 1 }, - ]; - methodsWithBlockHashInResponse.forEach(({ name, numberOfParameters }) => { - describe(`method name: ${name}`, () => { - testsForRpcMethodsThatCheckForBlockHashInResponse(name, { - numberOfParameters, - providerType, - }); - }); - }); - }); - describe('other methods', () => { describe('eth_getTransactionByHash', () => { it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => { @@ -336,10 +287,14 @@ export function testsForProviderType(providerType) { describe('methods not included in the Ethereum JSON-RPC spec', () => { describe('methods not handled by middleware', () => { const notHandledByMiddleware = [ - { name: 'custom_rpc_method', numberOfParameters: 1 }, - { name: 'eth_subscribe', numberOfParameters: 1 }, - { name: 'eth_unsubscribe', numberOfParameters: 1 }, + // This list is presented in the same order as in the network client + // tests on the core side. + { name: 'net_listening', numberOfParameters: 0 }, + // TODO: Methods to add back when we add testing for subscribe middleware + // { name: 'eth_subscribe', numberOfParameters: 1 }, + // { name: 'eth_unsubscribe', numberOfParameters: 1 }, + { name: 'custom_rpc_method', numberOfParameters: 1 }, { name: 'net_peerCount', numberOfParameters: 0 }, { name: 'parity_nextNonce', numberOfParameters: 1 }, ]; @@ -355,8 +310,8 @@ export function testsForProviderType(providerType) { describe('methods that assume there is no block param', () => { const assumingNoBlockParam = [ - { name: 'eth_protocolVersion', numberOfParameters: 0 }, { name: 'web3_clientVersion', numberOfParameters: 0 }, + { name: 'eth_protocolVersion', numberOfParameters: 0 }, ]; assumingNoBlockParam.forEach(({ name, numberOfParameters }) => describe(`method name: ${name}`, () => { @@ -412,3252 +367,3 @@ export function testsForProviderType(providerType) { }); }); } - -/** - * Defines tests which exercise the behavior exhibited by an RPC method that - * is not handled specially by the network client middleware. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.providerType - The type of provider being tested; - * either `infura` or `custom`. - * @param additionalArgs.numberOfParameters - The number of parameters that this - * RPC method takes. - */ -/* eslint-disable-next-line jest/no-export */ -export function testsForRpcMethodNotHandledByMiddleware( - method, - { providerType, numberOfParameters }, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - it('attempts to pass the request off to the RPC endpoint', async () => { - const request = { - method, - params: fill(Array(numberOfParameters), 'some value'), - }; - const expectedResult = 'the result'; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: expectedResult }, - }); - const actualResult = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(actualResult).toStrictEqual(expectedResult); - }); - }); -} - -/** - * Defines tests which exercise the behavior exhibited by an RPC method which is - * assumed to not take a block parameter. Even if it does, the value of this - * parameter will not be used in determining how to cache the method. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. - * @param additionalArgs.providerType - The type of provider being tested; - * either `infura` or `custom` (default: "infura"). - */ -export function testsForRpcMethodAssumingNoBlockParam( - method, - { numberOfParameters, providerType }, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - it('does not hit the RPC endpoint more than once for identical requests', async () => { - const requests = [{ method }, { method }]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = ['some result', 'another result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { - const requests = [{ method }, { method }]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. The - // first block tracker request occurs because of the first RPC request. - // The second block tracker request, however, does not occur because of - // the second RPC request, but rather because we call `clock.runAll()` - // below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a new - // block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { method }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [{ method }, { method }]; - const mockResults = [emptyValue, 'some result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - - it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => { - const requests = [{ method }, { method }, { method }]; - const mockResults = ['first result', 'second result', 'third result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - delay: 100, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - comms.mockRpcCall({ - request: requests[2], - response: { result: mockResults[2] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const resultPromises = [ - client.makeRpcCall(requests[0]), - client.makeRpcCall(requests[1]), - client.makeRpcCall(requests[2]), - ]; - const firstResult = await resultPromises[0]; - // The inflight cache middleware uses setTimeout to run the handlers, - // so run them now - client.clock.runAll(); - const remainingResults = await Promise.all(resultPromises.slice(1)); - return [firstResult, ...remainingResults]; - }, - ); - - expect(results).toStrictEqual([ - mockResults[0], - mockResults[0], - mockResults[0], - ]); - }); - }); - - it('throws a custom error if the request to the RPC endpoint returns a 405 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 405, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'The method does not exist / is not available', - ); - }); - }); - - // There is a difference in how we are testing the Infura middleware vs. the - // custom RPC middleware (or, more specifically, the fetch middleware) because - // of what both middleware treat as rate limiting errors. In this case, the - // fetch middleware treats a 418 response from the RPC endpoint as such an - // error, whereas to the Infura middleware, it is a 429 response. - if (providerType === 'infura') { - it('throws an undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - '{"id":1,"jsonrpc":"2.0"}', - ); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); - }); - }); - } else { - it('throws a custom error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited.', - ); - }); - }); - - it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - "Non-200 status code: '429'", - ); - }); - }); - } - - it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - id: 12345, - jsonrpc: '2.0', - error: 'some error', - httpStatus: 420, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - const errorMessage = - providerType === 'infura' - ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' - : "Non-200 status code: '420'"; - await expect(promiseForResult).rejects.toThrow(errorMessage); - }); - }); - - [503, 504].forEach((httpStatus) => { - it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - response: { - error: 'Some error', - httpStatus, - }, - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - comms.mockNextBlockTrackerRequest(); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - const err = - providerType === 'infura' - ? buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout') - : buildJsonRpcEngineEmptyResponseErrorMessage(method); - await expect(promiseForResult).rejects.toThrow(err); - }); - }); - }); - - it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'ETIMEDOUT: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - // Both the Infura and fetch middleware detect ETIMEDOUT errors and will - // automatically retry the request to the RPC endpoint in question, but both - // produce a different error if the number of retries is exhausted. - if (providerType === 'infura') { - it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // The Infura middleware treats a response that contains an ECONNRESET message - // as an innocuous error that is likely to disappear on a retry. The custom - // RPC middleware, on the other hand, does not specially handle this error. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'ECONNRESET: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - } - - // Both the Infura and fetch middleware will attempt to parse the response - // body as JSON, and if this step produces an error, both middleware will also - // attempt to retry the request. However, this error handling code is slightly - // different between the two. As the error in this case is a SyntaxError, the - // Infura middleware will catch it immediately, whereas the custom RPC - // middleware will catch it and re-throw a separate error, which it then - // catches later. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'SyntaxError: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - - it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - - it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'failed to parse response body: some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // Only the custom RPC middleware will detect a "Failed to fetch" error and - // attempt to retry the request to the RPC endpoint; the Infura middleware - // does not. - if (providerType === 'infura') { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'Failed to fetch: some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } -} - -/** - * Defines tests which exercise the behavior exhibited by an RPC method that - * use `blockHash` in the response data to determine whether the response is - * cacheable. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. - * @param additionalArgs.providerType - The type of provider being tested; - * either `infura` or `custom` (default: "infura"). - */ -export function testsForRpcMethodsThatCheckForBlockHashInResponse( - method, - { numberOfParameters, providerType }, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - it('does not hit the RPC endpoint more than once for identical requests and it has a valid blockHash', async () => { - const requests = [{ method }, { method }]; - const mockResult = { blockHash: '0x100' }; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResult }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResult, mockResult]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - it(`does not reuse the result of a previous request with a valid blockHash if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { - const requests = [{ method }, { method }]; - const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. The - // first block tracker request occurs because of the first RPC - // request. The second block tracker request, however, does not occur - // because of the second RPC request, but rather because we call - // `clock.runAll()` below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a new - // block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { method }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [{ method }, { method }]; - const mockResults = [emptyValue, { blockHash: '0x100' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - - it('does not reuse the result of a previous request if result.blockHash was null', async () => { - const requests = [{ method }, { method }]; - const mockResults = [ - { blockHash: null, extra: 'some value' }, - { blockHash: '0x100', extra: 'some other value' }, - ]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - it('does not reuse the result of a previous request if result.blockHash was undefined', async () => { - const requests = [{ method }, { method }]; - const mockResults = [ - { extra: 'some value' }, - { blockHash: '0x100', extra: 'some other value' }, - ]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - it('does not reuse the result of a previous request if result.blockHash was "0x0000000000000000000000000000000000000000000000000000000000000000"', async () => { - const requests = [{ method }, { method }]; - const mockResults = [ - { - blockHash: - '0x0000000000000000000000000000000000000000000000000000000000000000', - extra: 'some value', - }, - { blockHash: '0x100', extra: 'some other value' }, - ]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); -} - -/** - * Defines tests which exercise the behavior exhibited by an RPC method that - * takes a block parameter. The value of this parameter can be either a block - * number or a block tag ("latest", "earliest", or "pending") and affects how - * the method is cached. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.blockParamIndex - The index of the block parameter. - * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. - * @param additionalArgs.providerType - The type of provider being tested. - * either `infura` or `custom` (default: "infura"). - */ -/* eslint-disable-next-line jest/no-export */ -export function testsForRpcMethodSupportingBlockParam( - method, - { blockParamIndex, numberOfParameters, providerType }, -) { - describe.each([ - ['given no block tag', undefined], - ['given a block tag of "latest"', 'latest'], - ])('%s', (_desc, blockParam) => { - it('does not hit the RPC endpoint more than once for identical requests', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. Later, the block-ref - // middleware will request the latest block number again to resolve - // the value of "latest", but the block number is cached once made, - // so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - if (paramIndex === blockParamIndex) { - // testing changes in block param is covered under later tests - continue; - } - it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - firstMockParams[blockParamIndex] = blockParam; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. Later, the block-ref - // middleware will request the latest block number again to resolve - // the value of "latest", but the block number is cached once made, - // so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { - const requests = [ - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. - // The first block tracker request occurs because of the first RPC - // request. The second block tracker request, however, does not - // occur because of the second RPC request, but rather because we - // call `clock.runAll()` below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x200', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a - // new block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = [emptyValue, 'some result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - - it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => { - const requests = [{ method }, { method }, { method }]; - const mockResults = ['first result', 'second result', 'third result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number, and we delay it. - comms.mockRpcCall({ - delay: 100, - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - // The previous two requests will happen again, in the same order. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[2], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[2] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const resultPromises = [ - client.makeRpcCall(requests[0]), - client.makeRpcCall(requests[1]), - client.makeRpcCall(requests[2]), - ]; - const firstResult = await resultPromises[0]; - // The inflight cache middleware uses setTimeout to run the - // handlers, so run them now - client.clock.runAll(); - const remainingResults = await Promise.all(resultPromises.slice(1)); - return [firstResult, ...remainingResults]; - }, - ); - - expect(results).toStrictEqual([ - mockResults[0], - mockResults[0], - mockResults[0], - ]); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 405 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 405, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'The method does not exist / is not available', - ); - }); - }); - - // There is a difference in how we are testing the Infura middleware vs. the - // custom RPC middleware (or, more specifically, the fetch middleware) - // because of what both middleware treat as rate limiting errors. In this - // case, the fetch middleware treats a 418 response from the RPC endpoint as - // such an error, whereas to the Infura middleware, it is a 429 response. - if (providerType === 'infura') { - it('throws a generic, undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - '{"id":1,"jsonrpc":"2.0"}', - ); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); - }); - }); - } else { - it('throws an error with a custom message if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited.', - ); - }); - }); - - it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - "Non-200 status code: '429'", - ); - }); - }); - } - - it('throws an undescriptive error message if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - id: 12345, - jsonrpc: '2.0', - error: 'some error', - httpStatus: 420, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - const msg = - providerType === 'infura' - ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' - : "Non-200 status code: '420'"; - await expect(promiseForResult).rejects.toThrow(msg); - }); - }); - - [503, 504].forEach((httpStatus) => { - it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'some error', - httpStatus, - }, - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - // Both the Infura middleware and custom RPC middleware detect a 503 or 504 - // response and retry the request to the RPC endpoint automatically but - // differ in what sort of response is returned when the number of retries is - // exhausted. - if (providerType === 'infura') { - it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout'), - ); - }); - }); - } else { - it(`produces an empty response if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - }); - - it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'ETIMEDOUT: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - // Both the Infura and fetch middleware detect ETIMEDOUT errors and will - // automatically retry the request to the RPC endpoint in question, but each - // produces a different error if the number of retries is exhausted. - if (providerType === 'infura') { - it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('produces an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // The Infura middleware treats a response that contains an ECONNRESET - // message as an innocuous error that is likely to disappear on a retry. The - // custom RPC middleware, on the other hand, does not specially handle this - // error. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'ECONNRESET: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - } - - // Both the Infura and fetch middleware will attempt to parse the response - // body as JSON, and if this step produces an error, both middleware will - // also attempt to retry the request. However, this error handling code is - // slightly different between the two. As the error in this case is a - // SyntaxError, the Infura middleware will catch it immediately, whereas the - // custom RPC middleware will catch it and re-throw a separate error, which - // it then catches later. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if a "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'SyntaxError: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if a "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - - it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - - it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'failed to parse response body: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('produces an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // Only the custom RPC middleware will detect a "Failed to fetch" error and - // attempt to retry the request to the RPC endpoint; the Infura middleware - // does not. - if (providerType === 'infura') { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'Failed to fetch: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('produces an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - }); - - describe.each([ - ['given a block tag of "earliest"', 'earliest', 'earliest'], - ['given a block number', 'block number', '0x100'], - ])('%s', (_desc, blockParamType, blockParam) => { - it('does not hit the RPC endpoint more than once for identical requests', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. This block number doesn't - // matter. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - if (paramIndex === blockParamIndex) { - // testing changes in block param is covered under later tests - continue; - } - it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - firstMockParams[blockParamIndex] = blockParam; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. Later, the block-ref - // middleware will request the latest block number again to resolve - // the value of "latest", but the block number is cached once made, - // so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('reuses the result of a previous request even if the latest block number was updated since', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. The - // first block tracker request occurs because of the first RPC - // request. The second block tracker request, however, does not - // occur because of the second RPC request, but rather because we - // call `clock.runAll()` below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a - // new block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - if (blockParamType === 'earliest') { - it('treats "0x00" as a synonym for "earliest"', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam: '0x00' }), - }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. It - // doesn't matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = [emptyValue, 'some result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - } - - if (blockParamType === 'block number') { - it('does not reuse the result of a previous request if it was made with different arguments than this one', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam: '0x100' }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam: '0x200' }), - }, - ]; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: 'first result' }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: 'second result' }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(['first result', 'second result']); - }); - }); - - describe.each( - [ - ['less than the current block number', '0x200'], - ['equal to the curent block number', '0x100'], - ], - '%s', - (_nestedDesc, currentBlockNumber) => { - it('makes an additional request to the RPC endpoint', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request, - response: { result: 'the result' }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - if (providerType === 'infura') { - it(`retries up to 10 times if a "${emptyValue}" response is returned, returning successful non-empty response if there is one on the 10th try`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request, - response: { result: emptyValue }, - times: 9, - }); - comms.mockRpcCall({ - request, - response: { result: 'some value' }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall, clock }) => - waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ), - ); - - expect(result).toStrictEqual('some value'); - }, - ); - }); - - it(`retries up to 10 times if a "${emptyValue}" response is returned, failing after the 10th try`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - times: 10, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - ({ makeRpcCall, clock }) => - waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ), - ); - - await expect(promiseForResult).rejects.toThrow( - 'RetryOnEmptyMiddleware - retries exhausted', - ); - }, - ); - }); - } else { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }, - ); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = [emptyValue, { blockHash: '0x100' }]; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }, - ); - }); - } - } - }, - ); - - describe('greater than the current block number', () => { - it('makes an additional request to the RPC endpoint', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); - comms.mockRpcCall({ - request, - response: { result: 'the result' }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = [emptyValue, { blockHash: '0x100' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - }); - } - }); - - describe('given a block tag of "pending"', () => { - const params = buildMockParams({ blockParamIndex, blockParam: 'pending' }); - - it('hits the RPC endpoint on all calls and does not cache anything', async () => { - const requests = [ - { method, params }, - { method, params }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. It - // doesn't matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - }); -} diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index c33e0f12a..64075a154 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -3,7 +3,6 @@ import { normalize as normalizeAddress } from 'eth-sig-util'; import { IPFS_DEFAULT_GATEWAY_URL } from '../../../shared/constants/network'; import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets'; import { ThemeType } from '../../../shared/constants/preferences'; -import { NETWORK_EVENTS } from './network'; export default class PreferencesController { /** @@ -70,10 +69,10 @@ export default class PreferencesController { ...opts.initState, }; - this.network = opts.network; + this._onInfuraIsBlocked = opts.onInfuraIsBlocked; + this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked; this.store = new ObservableStore(initState); this.store.setMaxListeners(13); - this.openPopup = opts.openPopup; this.tokenListController = opts.tokenListController; this._subscribeToInfuraAvailability(); @@ -511,10 +510,11 @@ export default class PreferencesController { // _subscribeToInfuraAvailability() { - this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => { + this._onInfuraIsBlocked(() => { this._setInfuraBlocked(true); }); - this.network.on(NETWORK_EVENTS.INFURA_IS_UNBLOCKED, () => { + + this._onInfuraIsUnblocked(() => { this._setInfuraBlocked(false); }); } diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 21267195c..51f2ba5a0 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -19,8 +19,10 @@ describe('preferences controller', function () { const networkControllerProviderConfig = { getAccounts: () => undefined, }; + const networkControllerMessenger = new ControllerMessenger(); network = new NetworkController({ infuraProjectId: 'foo', + messenger: networkControllerMessenger, state: { provider: { type: 'mainnet', @@ -50,6 +52,8 @@ describe('preferences controller', function () { network, provider, tokenListController, + onInfuraIsBlocked: sinon.spy(), + onInfuraIsUnblocked: sinon.spy(), }); }); diff --git a/app/scripts/controllers/sign.test.ts b/app/scripts/controllers/sign.test.ts new file mode 100644 index 000000000..dac749466 --- /dev/null +++ b/app/scripts/controllers/sign.test.ts @@ -0,0 +1,587 @@ +import { + MessageManager, + PersonalMessageManager, + TypedMessageManager, +} from '@metamask/message-manager'; +import { + AbstractMessage, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import SignController, { + SignControllerMessenger, + SignControllerOptions, +} from './sign'; + +jest.mock('@metamask/message-manager', () => ({ + MessageManager: jest.fn(), + PersonalMessageManager: jest.fn(), + TypedMessageManager: jest.fn(), +})); + +const messageIdMock = '123'; +const messageIdMock2 = '456'; +const versionMock = '1'; +const signatureMock = '0xAABBCC'; +const stateMock = { test: 123 }; +const securityProviderResponseMock = { test2: 345 }; + +const messageParamsMock = { + from: '0x123', + origin: 'http://test.com', + data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + metamaskId: messageIdMock, + version: 'V1', +}; + +const messageParamsMock2 = { + from: '0x124', + origin: 'http://test4.com', + data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA', + metamaskId: messageIdMock, +}; + +const messageMock = { + id: messageIdMock, + time: 123, + status: 'unapproved', + type: 'testType', + rawSig: undefined, +} as any as AbstractMessage; + +const coreMessageMock = { + ...messageMock, + messageParams: messageParamsMock, +}; + +const stateMessageMock = { + ...messageMock, + msgParams: messageParamsMock, + securityProviderResponse: securityProviderResponseMock, +}; + +const requestMock = { + origin: 'http://test2.com', +} as OriginalRequest; + +const createMessengerMock = () => + ({ + registerActionHandler: jest.fn(), + publish: jest.fn(), + call: jest.fn(), + } as any as jest.Mocked); + +const createMessageManagerMock = () => + ({ + getUnapprovedMessages: jest.fn(), + getUnapprovedMessagesCount: jest.fn(), + addUnapprovedMessageAsync: jest.fn(), + approveMessage: jest.fn(), + setMessageStatusSigned: jest.fn(), + rejectMessage: jest.fn(), + subscribe: jest.fn(), + update: jest.fn(), + hub: { + on: jest.fn(), + }, + } as any as jest.Mocked); + +const createPreferencesControllerMock = () => ({ + store: { + getState: jest.fn(), + }, +}); + +const createKeyringControllerMock = () => ({ + signMessage: jest.fn(), + signPersonalMessage: jest.fn(), + signTypedMessage: jest.fn(), +}); + +describe('SignController', () => { + let signController: SignController; + + const messageManagerConstructorMock = MessageManager as jest.MockedClass< + typeof MessageManager + >; + const personalMessageManagerConstructorMock = + PersonalMessageManager as jest.MockedClass; + const typedMessageManagerConstructorMock = + TypedMessageManager as jest.MockedClass; + const messageManagerMock = createMessageManagerMock(); + const personalMessageManagerMock = + createMessageManagerMock(); + const typedMessageManagerMock = + createMessageManagerMock(); + const messengerMock = createMessengerMock(); + const preferencesControllerMock = createPreferencesControllerMock(); + const keyringControllerMock = createKeyringControllerMock(); + const getStateMock = jest.fn(); + const securityProviderRequestMock = jest.fn(); + const metricsEventMock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + + messageManagerConstructorMock.mockReturnValue(messageManagerMock); + personalMessageManagerConstructorMock.mockReturnValue( + personalMessageManagerMock, + ); + + typedMessageManagerConstructorMock.mockReturnValue(typedMessageManagerMock); + + preferencesControllerMock.store.getState.mockReturnValue({ + disabledRpcMethodPreferences: { eth_sign: true }, + }); + + signController = new SignController({ + messenger: messengerMock as any, + preferencesController: preferencesControllerMock as any, + keyringController: keyringControllerMock as any, + getState: getStateMock as any, + securityProviderRequest: securityProviderRequestMock as any, + metricsEvent: metricsEventMock as any, + } as SignControllerOptions); + }); + + describe('unapprovedMsgCount', () => { + it('returns value from message manager getter', () => { + messageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(10); + expect(signController.unapprovedMsgCount).toBe(10); + }); + }); + + describe('unapprovedPersonalMessagesCount', () => { + it('returns value from personal message manager getter', () => { + personalMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce( + 11, + ); + expect(signController.unapprovedPersonalMessagesCount).toBe(11); + }); + }); + + describe('unapprovedTypedMessagesCount', () => { + it('returns value from typed message manager getter', () => { + typedMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce( + 12, + ); + expect(signController.unapprovedTypedMessagesCount).toBe(12); + }); + }); + + describe('resetState', () => { + it('sets state to initial state', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + signController.update(() => ({ + unapprovedMsgs: { [messageIdMock]: messageMock } as any, + unapprovedPersonalMsgs: { [messageIdMock]: messageMock } as any, + unapprovedTypedMessages: { [messageIdMock]: messageMock } as any, + unapprovedMsgCount: 1, + unapprovedPersonalMsgCount: 2, + unapprovedTypedMessagesCount: 3, + })); + + signController.resetState(); + + expect(signController.state).toEqual({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 0, + }); + }); + }); + + describe('rejectUnapproved', () => { + beforeEach(() => { + const messages = { + [messageIdMock]: messageMock, + [messageIdMock2]: messageMock, + }; + + messageManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + personalMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + typedMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + signController.update(() => ({ + unapprovedMsgs: messages as any, + unapprovedPersonalMsgs: messages as any, + unapprovedTypedMessages: messages as any, + })); + }); + + it('rejects all messages in all message managers', () => { + signController.rejectUnapproved('Test Reason'); + + expect(messageManagerMock.rejectMessage).toHaveBeenCalledTimes(2); + expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + + expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2); + expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + + expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2); + expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + }); + + it('fires metrics event with reject reason', () => { + signController.rejectUnapproved('Test Reason'); + + expect(metricsEventMock).toHaveBeenCalledTimes(6); + expect(metricsEventMock).toHaveBeenLastCalledWith({ + event: 'Test Reason', + category: MetaMetricsEventCategory.Transactions, + properties: { + action: 'Sign Request', + type: messageMock.type, + }, + }); + }); + }); + + describe('clearUnapproved', () => { + it('resets state in all message managers', () => { + signController.clearUnapproved(); + + const defaultState = { + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }; + + expect(messageManagerMock.update).toHaveBeenCalledTimes(1); + expect(messageManagerMock.update).toHaveBeenCalledWith(defaultState); + + expect(personalMessageManagerMock.update).toHaveBeenCalledTimes(1); + expect(personalMessageManagerMock.update).toHaveBeenCalledWith( + defaultState, + ); + + expect(typedMessageManagerMock.update).toHaveBeenCalledTimes(1); + expect(typedMessageManagerMock.update).toHaveBeenCalledWith(defaultState); + }); + }); + + describe('newUnsignedMessage', () => { + it('throws if eth_sign disabled in preferences', async () => { + preferencesControllerMock.store.getState.mockReturnValueOnce({ + disabledRpcMethodPreferences: { eth_sign: false }, + }); + + await expect( + signController.newUnsignedMessage(messageParamsMock, requestMock), + ).rejects.toThrowError( + 'eth_sign has been disabled. You must enable it in the advanced settings', + ); + }); + + it('throws if data has wrong length', async () => { + await expect( + signController.newUnsignedMessage( + { ...messageParamsMock, data: '0xFF' }, + requestMock, + ), + ).rejects.toThrowError('eth_sign requires 32 byte message hash'); + }); + + it('adds message to message manager', async () => { + await signController.newUnsignedMessage(messageParamsMock, requestMock); + + expect( + messageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + expect(messageManagerMock.addUnapprovedMessageAsync).toHaveBeenCalledWith( + messageParamsMock, + requestMock, + ); + }); + }); + + describe('newUnsignedPersonalMessage', () => { + it('adds message to personal message manager', async () => { + await signController.newUnsignedPersonalMessage( + messageParamsMock, + requestMock, + ); + + expect( + personalMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + + expect( + personalMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledWith( + expect.objectContaining(messageParamsMock), + requestMock, + ); + }); + }); + + describe('newUnsignedTypedMessage', () => { + it('adds message to typed message manager', async () => { + signController.newUnsignedTypedMessage( + messageParamsMock, + requestMock, + versionMock, + ); + + expect( + typedMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + expect( + typedMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledWith(messageParamsMock, versionMock, requestMock); + }); + }); + + describe.each([ + ['signMessage', messageManagerMock], + ['signPersonalMessage', personalMessageManagerMock], + ['signTypedMessage', typedMessageManagerMock], + ])('%s', (signMethodName, messageManager) => { + beforeEach(() => { + messageManager.approveMessage.mockResolvedValueOnce(messageParamsMock2); + + keyringControllerMock[signMethodName].mockResolvedValueOnce( + signatureMock, + ); + }); + + it('approves message and signs', async () => { + await signController[signMethodName](messageParamsMock); + + const keyringControllerExtraArgs = + signMethodName === 'signTypedMessage' + ? [{ version: messageParamsMock.version }] + : []; + + expect(keyringControllerMock[signMethodName]).toHaveBeenCalledTimes(1); + expect(keyringControllerMock[signMethodName]).toHaveBeenCalledWith( + messageParamsMock2, + ...keyringControllerExtraArgs, + ); + + expect(messageManager.setMessageStatusSigned).toHaveBeenCalledTimes(1); + expect(messageManager.setMessageStatusSigned).toHaveBeenCalledWith( + messageParamsMock2.metamaskId, + signatureMock, + ); + }); + + it('returns current state', async () => { + getStateMock.mockReturnValueOnce(stateMock); + expect(await signController[signMethodName](messageParamsMock)).toEqual( + stateMock, + ); + }); + + it('accepts approval', async () => { + await signController[signMethodName](messageParamsMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + messageParamsMock.metamaskId, + ); + }); + + it('does not throw if accepting approval throws', async () => { + messengerMock.call.mockImplementation(() => { + throw new Error('Test Error'); + }); + + await signController[signMethodName](messageParamsMock); + }); + + it('rejects message on error', async () => { + keyringControllerMock[signMethodName].mockReset(); + keyringControllerMock[signMethodName].mockRejectedValue( + new Error('Test Error'), + ); + + await expect( + signController[signMethodName](messageParamsMock), + ).rejects.toThrow('Test Error'); + + expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1); + expect(messageManager.rejectMessage).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + ); + }); + + it('rejects approval on error', async () => { + keyringControllerMock[signMethodName].mockReset(); + keyringControllerMock[signMethodName].mockRejectedValue( + new Error('Test Error'), + ); + + await expect( + signController[signMethodName](messageParamsMock), + ).rejects.toThrow('Test Error'); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:rejectRequest', + messageParamsMock.metamaskId, + 'Cancel', + ); + }); + }); + + describe.each([ + ['cancelMessage', messageManagerMock], + ['cancelPersonalMessage', personalMessageManagerMock], + ['cancelTypedMessage', typedMessageManagerMock], + ])('%s', (cancelMethodName, messageManager) => { + it('rejects message using message manager', async () => { + signController[cancelMethodName](messageIdMock); + + expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1); + expect(messageManager.rejectMessage).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + ); + }); + + it('rejects approval using approval controller', async () => { + signController[cancelMethodName](messageIdMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:rejectRequest', + messageParamsMock.metamaskId, + 'Cancel', + ); + }); + + it('does not throw if rejecting approval throws', async () => { + messengerMock.call.mockImplementation(() => { + throw new Error('Test Error'); + }); + + await signController[cancelMethodName](messageParamsMock); + }); + }); + + describe('message manager events', () => { + it.each([ + ['message manager', messageManagerMock], + ['personal message manager', personalMessageManagerMock], + ['typed message manager', typedMessageManagerMock], + ])('bubbles update badge event from %s', (_, messageManager) => { + const mockListener = jest.fn(); + + signController.hub.on('updateBadge', mockListener); + messageManager.hub.on.mock.calls[0][1](); + + expect(mockListener).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['message manager', messageManagerMock, 'eth_sign'], + ['personal message manager', personalMessageManagerMock, 'personal_sign'], + ['typed message manager', typedMessageManagerMock, 'eth_signTypedData'], + ])( + 'requires approval on unapproved message event from %s', + (_, messageManager, methodName) => { + messengerMock.call.mockResolvedValueOnce({}); + + messageManager.hub.on.mock.calls[1][1](messageParamsMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: messageIdMock, + origin: messageParamsMock.origin, + type: methodName, + }, + true, + ); + }, + ); + + it('updates state on message manager state change', async () => { + securityProviderRequestMock.mockResolvedValue( + securityProviderResponseMock, + ); + + await messageManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 3, + }); + + expect(await signController.state).toEqual({ + unapprovedMsgs: { [messageIdMock]: stateMessageMock as any }, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 3, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 0, + }); + }); + + it('updates state on personal message manager state change', async () => { + securityProviderRequestMock.mockResolvedValue( + securityProviderResponseMock, + ); + + await personalMessageManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 4, + }); + + expect(await signController.state).toEqual({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: { [messageIdMock]: stateMessageMock as any }, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 4, + unapprovedTypedMessagesCount: 0, + }); + }); + + it('updates state on typed message manager state change', async () => { + securityProviderRequestMock.mockResolvedValue( + securityProviderResponseMock, + ); + + await typedMessageManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 5, + }); + + expect(await signController.state).toEqual({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: { [messageIdMock]: stateMessageMock as any }, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 5, + }); + }); + }); +}); diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts new file mode 100644 index 000000000..4b8970051 --- /dev/null +++ b/app/scripts/controllers/sign.ts @@ -0,0 +1,662 @@ +import EventEmitter from 'events'; +import log from 'loglevel'; +import { + MessageManager, + MessageParams, + MessageParamsMetamask, + PersonalMessageManager, + PersonalMessageParams, + PersonalMessageParamsMetamask, + TypedMessageManager, + TypedMessageParams, + TypedMessageParamsMetamask, +} from '@metamask/message-manager'; +import { ethErrors } from 'eth-rpc-errors'; +import { bufferToHex } from 'ethereumjs-util'; +import { KeyringController } from '@metamask/eth-keyring-controller'; +import { + AbstractMessageManager, + AbstractMessage, + MessageManagerState, + AbstractMessageParams, + AbstractMessageParamsMetamask, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { Patch } from 'immer'; +import { + AcceptRequest, + AddApprovalRequest, + RejectRequest, +} from '@metamask/approval-controller'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import { MESSAGE_TYPE } from '../../../shared/constants/app'; +import PreferencesController from './preferences'; + +const controllerName = 'SignController'; +const methodNameSign = MESSAGE_TYPE.ETH_SIGN; +const methodNamePersonalSign = MESSAGE_TYPE.PERSONAL_SIGN; +const methodNameTypedSign = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA; + +const stateMetadata = { + unapprovedMsgs: { persist: false, anonymous: false }, + unapprovedPersonalMsgs: { persist: false, anonymous: false }, + unapprovedTypedMessages: { persist: false, anonymous: false }, + unapprovedMsgCount: { persist: false, anonymous: false }, + unapprovedPersonalMsgCount: { persist: false, anonymous: false }, + unapprovedTypedMessagesCount: { persist: false, anonymous: false }, +}; + +const getDefaultState = () => ({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 0, +}); + +export type CoreMessage = AbstractMessage & { + messageParams: AbstractMessageParams; +}; + +export type StateMessage = Required & { + msgParams: Required; + securityProviderResponse: any; +}; + +export type SignControllerState = { + unapprovedMsgs: Record; + unapprovedPersonalMsgs: Record; + unapprovedTypedMessages: Record; + unapprovedMsgCount: number; + unapprovedPersonalMsgCount: number; + unapprovedTypedMessagesCount: number; +}; + +export type GetSignState = { + type: `${typeof controllerName}:getState`; + handler: () => SignControllerState; +}; + +export type SignStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [SignControllerState, Patch[]]; +}; + +export type SignControllerActions = GetSignState; + +export type SignControllerEvents = SignStateChange; + +type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest; + +export type SignControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + SignControllerActions | AllowedActions, + SignControllerEvents, + AllowedActions['type'], + never +>; + +export type SignControllerOptions = { + messenger: SignControllerMessenger; + keyringController: KeyringController; + preferencesController: PreferencesController; + sendUpdate: () => void; + getState: () => any; + metricsEvent: (payload: any, options?: any) => void; + securityProviderRequest: ( + requestData: any, + methodName: string, + ) => Promise; +}; + +/** + * Controller for creating signing requests requiring user approval. + */ +export default class SignController extends BaseControllerV2< + typeof controllerName, + SignControllerState, + SignControllerMessenger +> { + hub: EventEmitter; + + private _keyringController: KeyringController; + + private _preferencesController: PreferencesController; + + private _getState: () => any; + + private _messageManager: MessageManager; + + private _personalMessageManager: PersonalMessageManager; + + private _typedMessageManager: TypedMessageManager; + + private _messageManagers: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >[]; + + private _metricsEvent: (payload: any, options?: any) => void; + + private _securityProviderRequest: ( + requestData: any, + methodName: string, + ) => Promise; + + /** + * Construct a Sign controller. + * + * @param options - The controller options. + * @param options.messenger - The restricted controller messenger for the sign controller. + * @param options.keyringController - An instance of a keyring controller used to perform the signing operations. + * @param options.preferencesController - An instance of a preferences controller to limit operations based on user configuration. + * @param options.getState - Callback to retrieve all user state. + * @param options.metricsEvent - A function for emitting a metric event. + * @param options.securityProviderRequest - A function for verifying a message, whether it is malicious or not. + */ + constructor({ + messenger, + keyringController, + preferencesController, + getState, + metricsEvent, + securityProviderRequest, + }: SignControllerOptions) { + super({ + name: controllerName, + metadata: stateMetadata, + messenger, + state: getDefaultState(), + }); + + this._keyringController = keyringController; + this._preferencesController = preferencesController; + this._getState = getState; + this._metricsEvent = metricsEvent; + this._securityProviderRequest = securityProviderRequest; + + this.hub = new EventEmitter(); + this._messageManager = new MessageManager(); + this._personalMessageManager = new PersonalMessageManager(); + this._typedMessageManager = new TypedMessageManager(); + + this._messageManagers = [ + this._messageManager, + this._personalMessageManager, + this._typedMessageManager, + ]; + + const methodNames = [ + methodNameSign, + methodNamePersonalSign, + methodNameTypedSign, + ]; + + this._messageManagers.forEach((messageManager, index) => { + this._bubbleEvents(messageManager); + + messageManager.hub.on( + 'unapprovedMessage', + (msgParams: AbstractMessageParamsMetamask) => { + this._requestApproval(msgParams, methodNames[index]); + }, + ); + }); + + this._subscribeToMessageState( + this._messageManager, + (state, newMessages, messageCount) => { + state.unapprovedMsgs = newMessages; + state.unapprovedMsgCount = messageCount; + }, + ); + + this._subscribeToMessageState( + this._personalMessageManager, + (state, newMessages, messageCount) => { + state.unapprovedPersonalMsgs = newMessages; + state.unapprovedPersonalMsgCount = messageCount; + }, + ); + + this._subscribeToMessageState( + this._typedMessageManager, + (state, newMessages, messageCount) => { + state.unapprovedTypedMessages = newMessages; + state.unapprovedTypedMessagesCount = messageCount; + }, + ); + } + + /** + * A getter for the number of 'unapproved' Messages in this.messages + * + * @returns The number of 'unapproved' Messages in this.messages + */ + get unapprovedMsgCount(): number { + return this._messageManager.getUnapprovedMessagesCount(); + } + + /** + * A getter for the number of 'unapproved' PersonalMessages in this.messages + * + * @returns The number of 'unapproved' PersonalMessages in this.messages + */ + get unapprovedPersonalMessagesCount(): number { + return this._personalMessageManager.getUnapprovedMessagesCount(); + } + + /** + * A getter for the number of 'unapproved' TypedMessages in this.messages + * + * @returns The number of 'unapproved' TypedMessages in this.messages + */ + get unapprovedTypedMessagesCount(): number { + return this._typedMessageManager.getUnapprovedMessagesCount(); + } + + /** + * Reset the controller state to the initial state. + */ + resetState() { + this.update(() => getDefaultState()); + } + + /** + * Called when a Dapp uses the eth_sign method, to request user approval. + * eth_sign is a pure signature of arbitrary data. It is on a deprecation + * path, since this data can be a transaction, or can leak private key + * information. + * + * @param msgParams - The params passed to eth_sign. + * @param [req] - The original request, containing the origin. + */ + async newUnsignedMessage( + msgParams: MessageParams, + req: OriginalRequest, + ): Promise { + const { + // eslint-disable-next-line camelcase + disabledRpcMethodPreferences: { eth_sign }, + } = this._preferencesController.store.getState() as any; + + // eslint-disable-next-line camelcase + if (!eth_sign) { + throw ethErrors.rpc.methodNotFound( + 'eth_sign has been disabled. You must enable it in the advanced settings', + ); + } + + const data = this._normalizeMsgData(msgParams.data); + + // 64 hex + "0x" at the beginning + // This is needed because Ethereum's EcSign works only on 32 byte numbers + // For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607 + if (data.length !== 66 && data.length !== 67) { + throw ethErrors.rpc.invalidParams( + 'eth_sign requires 32 byte message hash', + ); + } + + return this._messageManager.addUnapprovedMessageAsync(msgParams, req); + } + + /** + * Called when a dapp uses the personal_sign method. + * This is identical to the Geth eth_sign method, and may eventually replace + * eth_sign. + * + * We currently define our eth_sign and personal_sign mostly for legacy Dapps. + * + * @param msgParams - The params of the message to sign & return to the Dapp. + * @param req - The original request, containing the origin. + */ + async newUnsignedPersonalMessage( + msgParams: PersonalMessageParams, + req: OriginalRequest, + ): Promise { + return this._personalMessageManager.addUnapprovedMessageAsync( + msgParams, + req, + ); + } + + /** + * Called when a dapp uses the eth_signTypedData method, per EIP 712. + * + * @param msgParams - The params passed to eth_signTypedData. + * @param req - The original request, containing the origin. + * @param version + */ + async newUnsignedTypedMessage( + msgParams: TypedMessageParams, + req: OriginalRequest, + version: string, + ): Promise { + return this._typedMessageManager.addUnapprovedMessageAsync( + msgParams, + version, + req, + ); + } + + /** + * Signifies user intent to complete an eth_sign method. + * + * @param msgParams - The params passed to eth_call. + * @returns Full state update. + */ + async signMessage(msgParams: MessageParamsMetamask) { + return await this._signAbstractMessage( + this._messageManager, + methodNameSign, + msgParams, + async (cleanMsgParams) => + await this._keyringController.signMessage(cleanMsgParams), + ); + } + + /** + * Signifies a user's approval to sign a personal_sign message in queue. + * Triggers signing, and the callback function from newUnsignedPersonalMessage. + * + * @param msgParams - The params of the message to sign & return to the Dapp. + * @returns A full state update. + */ + async signPersonalMessage(msgParams: PersonalMessageParamsMetamask) { + return await this._signAbstractMessage( + this._personalMessageManager, + methodNamePersonalSign, + msgParams, + async (cleanMsgParams) => + await this._keyringController.signPersonalMessage(cleanMsgParams), + ); + } + + /** + * The method for a user approving a call to eth_signTypedData, per EIP 712. + * Triggers the callback in newUnsignedTypedMessage. + * + * @param msgParams - The params passed to eth_signTypedData. + * @returns Full state update. + */ + async signTypedMessage(msgParams: TypedMessageParamsMetamask) { + const { version } = msgParams; + + return await this._signAbstractMessage( + this._typedMessageManager, + methodNameTypedSign, + msgParams, + async (cleanMsgParams) => { + // For some reason every version after V1 used stringified params. + if (version !== 'V1') { + // But we don't have to require that. We can stop suggesting it now: + if (typeof cleanMsgParams.data === 'string') { + cleanMsgParams.data = JSON.parse(cleanMsgParams.data); + } + } + + return await this._keyringController.signTypedMessage(cleanMsgParams, { + version, + }); + }, + ); + } + + /** + * Used to cancel a message submitted via eth_sign. + * + * @param msgId - The id of the message to cancel. + * @returns A full state update. + */ + cancelMessage(msgId: string) { + return this._cancelAbstractMessage(this._messageManager, msgId); + } + + /** + * Used to cancel a personal_sign type message. + * + * @param msgId - The ID of the message to cancel. + * @returns A full state update. + */ + cancelPersonalMessage(msgId: string) { + return this._cancelAbstractMessage(this._personalMessageManager, msgId); + } + + /** + * Used to cancel a eth_signTypedData type message. + * + * @param msgId - The ID of the message to cancel. + * @returns A full state update. + */ + cancelTypedMessage(msgId: string) { + return this._cancelAbstractMessage(this._typedMessageManager, msgId); + } + + /** + * Reject all unapproved messages of any type. + * + * @param reason - A message to indicate why. + */ + rejectUnapproved(reason?: string) { + this._messageManagers.forEach((messageManager) => { + Object.keys(messageManager.getUnapprovedMessages()).forEach( + (messageId) => { + this._cancelAbstractMessage(messageManager, messageId, reason); + }, + ); + }); + } + + /** + * Clears all unapproved messages from memory. + */ + clearUnapproved() { + this._messageManagers.forEach((messageManager) => { + messageManager.update({ + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }); + }); + } + + private async _signAbstractMessage

( + messageManager: AbstractMessageManager< + AbstractMessage, + P, + AbstractMessageParamsMetamask + >, + methodName: string, + msgParams: AbstractMessageParamsMetamask, + getSignature: (cleanMessageParams: P) => Promise, + ) { + log.info(`MetaMaskController - ${methodName}`); + + const messageId = msgParams.metamaskId as string; + + try { + const cleanMessageParams = await messageManager.approveMessage(msgParams); + const signature = await getSignature(cleanMessageParams); + + messageManager.setMessageStatusSigned(messageId, signature); + + this._acceptApproval(messageId); + + return this._getState(); + } catch (error) { + log.info(`MetaMaskController - ${methodName} failed.`, error); + this._cancelAbstractMessage(messageManager, messageId); + throw error; + } + } + + private _cancelAbstractMessage( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + messageId: string, + reason?: string, + ) { + if (reason) { + const message = this._getMessage(messageId); + + this._metricsEvent({ + event: reason, + category: MetaMetricsEventCategory.Transactions, + properties: { + action: 'Sign Request', + type: message.type, + }, + }); + } + + messageManager.rejectMessage(messageId); + this._rejectApproval(messageId); + + return this._getState(); + } + + private _bubbleEvents( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + ) { + messageManager.hub.on('updateBadge', () => { + this.hub.emit('updateBadge'); + }); + } + + private _subscribeToMessageState( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + updateState: ( + state: SignControllerState, + newMessages: Record, + messageCount: number, + ) => void, + ) { + messageManager.subscribe( + async (state: MessageManagerState) => { + const newMessages = await this._migrateMessages( + state.unapprovedMessages as any, + ); + + this.update((draftState) => { + updateState(draftState, newMessages, state.unapprovedMessagesCount); + }); + }, + ); + } + + private async _migrateMessages( + coreMessages: Record, + ): Promise> { + const stateMessages: Record = {}; + + for (const messageId of Object.keys(coreMessages)) { + const coreMessage = coreMessages[messageId]; + const stateMessage = await this._migrateMessage(coreMessage); + + stateMessages[messageId] = stateMessage; + } + + return stateMessages; + } + + private async _migrateMessage( + coreMessage: CoreMessage, + ): Promise { + const { messageParams, ...coreMessageData } = coreMessage; + + // Core message managers use messageParams but frontend uses msgParams with lots of references + const stateMessage = { + ...coreMessageData, + rawSig: coreMessage.rawSig as string, + msgParams: { + ...messageParams, + origin: messageParams.origin as string, + }, + }; + + const messageId = coreMessage.id; + const existingMessage = this._getMessage(messageId); + + const securityProviderResponse = existingMessage + ? existingMessage.securityProviderResponse + : await this._securityProviderRequest(stateMessage, stateMessage.type); + + return { ...stateMessage, securityProviderResponse }; + } + + private _normalizeMsgData(data: string) { + if (data.slice(0, 2) === '0x') { + // data is already hex + return data; + } + // data is unicode, convert to hex + return bufferToHex(Buffer.from(data, 'utf8')); + } + + private _getMessage(messageId: string): StateMessage { + return { + ...this.state.unapprovedMsgs, + ...this.state.unapprovedPersonalMsgs, + ...this.state.unapprovedTypedMessages, + }[messageId]; + } + + private _requestApproval( + msgParams: AbstractMessageParamsMetamask, + type: string, + ) { + const id = msgParams.metamaskId as string; + const origin = msgParams.origin || controllerName; + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + private _acceptApproval(messageId: string) { + try { + this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + } catch (error) { + log.info('Failed to accept signature approval request', error); + } + } + + private _rejectApproval(messageId: string) { + try { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + messageId, + 'Cancel', + ); + } catch (error) { + log.info('Failed to reject signature approval request', error); + } + } +} diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 534aa54b7..b44e66a21 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -17,7 +17,7 @@ import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, } from '../../../shared/constants/swaps'; import { GasEstimateTypes } from '../../../shared/constants/gas'; -import { CHAIN_IDS } from '../../../shared/constants/network'; +import { CHAIN_IDS, NetworkStatus } from '../../../shared/constants/network'; import { FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, FALLBACK_SMART_TRANSACTIONS_DEADLINE, @@ -41,7 +41,6 @@ import fetchEstimatedL1Fee from '../../../ui/helpers/utils/optimism/fetchEstimat import { Numeric } from '../../../shared/modules/Numeric'; import { EtherDenomination } from '../../../shared/constants/common'; -import { NETWORK_EVENTS } from './network'; // The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator const MAX_GAS_LIMIT = 2500000; @@ -114,6 +113,7 @@ export default class SwapsController { fetchTradesInfo = defaultFetchTradesInfo, getCurrentChainId, getEIP1559GasFeeEstimates, + onNetworkStateChange, }) { this.store = new ObservableStore({ swapsState: { ...initialState.swapsState }, @@ -136,10 +136,14 @@ export default class SwapsController { this.indexOfNewestCallInFlight = 0; this.ethersProvider = new Web3Provider(provider); - this._currentNetwork = networkController.store.getState().network; - networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, (network) => { - if (network !== 'loading' && network !== this._currentNetwork) { - this._currentNetwork = network; + this._currentNetworkId = networkController.store.getState().networkId; + onNetworkStateChange(() => { + const { networkId, networkStatus } = networkController.store.getState(); + if ( + networkStatus === NetworkStatus.Available && + networkId !== this._currentNetworkId + ) { + this._currentNetworkId = networkId; this.ethersProvider = new Web3Provider(provider); } }); @@ -300,6 +304,7 @@ export default class SwapsController { Object.values(newQuotes).map(async (quote) => { if (quote.trade) { const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee( + chainId, { txParams: quote.trade, chainId, diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index 065540985..e3dbaafda 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -4,7 +4,11 @@ import sinon from 'sinon'; import { BigNumber } from '@ethersproject/bignumber'; import { mapValues } from 'lodash'; import BigNumberjs from 'bignumber.js'; -import { CHAIN_IDS, NETWORK_IDS } from '../../../shared/constants/network'; +import { + CHAIN_IDS, + NETWORK_IDS, + NetworkStatus, +} from '../../../shared/constants/network'; import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; import { createTestProviderTools } from '../../../test/stub/provider'; import { SECOND } from '../../../shared/constants/time'; @@ -14,7 +18,6 @@ import { FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER, } from '../../../shared/constants/smartTransactions'; import SwapsController, { utils } from './swaps'; -import { NETWORK_EVENTS } from './network'; const MOCK_FETCH_PARAMS = { slippage: 3, @@ -98,16 +101,11 @@ const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({ function getMockNetworkController() { return { store: { - getState: () => { - return { - network: NETWORK_IDS.GOERLI, - }; - }, + getState: sinon.stub().returns({ + networkId: NETWORK_IDS.GOERLI, + networkStatus: NetworkStatus.Available, + }), }, - on: sinon - .stub() - .withArgs(NETWORK_EVENTS.NETWORK_DID_CHANGE) - .callsArgAsync(1), }; } @@ -158,11 +156,12 @@ const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => { describe('SwapsController', function () { let provider; - const getSwapsController = () => { + const getSwapsController = (_provider = provider) => { return new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController: getMockNetworkController(), - provider, + onNetworkStateChange: sinon.stub(), + provider: _provider, getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getTokenRatesState: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, @@ -209,9 +208,11 @@ describe('SwapsController', function () { it('should replace ethers instance when network changes', function () { const networkController = getMockNetworkController(); + const onNetworkStateChange = sinon.stub(); const swapsController = new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController, + onNetworkStateChange, provider, getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getTokenRatesState: MOCK_TOKEN_RATES_STORE, @@ -219,9 +220,13 @@ describe('SwapsController', function () { getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; - const onNetworkDidChange = networkController.on.getCall(0).args[1]; + const changeNetwork = onNetworkStateChange.getCall(0).args[0]; - onNetworkDidChange(NETWORK_IDS.MAINNET); + networkController.store.getState.returns({ + networkId: NETWORK_IDS.MAINNET, + networkStatus: NetworkStatus.Available, + }); + changeNetwork(NETWORK_IDS.MAINNET); const newEthersInstance = swapsController.ethersProvider; assert.notStrictEqual( @@ -233,9 +238,11 @@ describe('SwapsController', function () { it('should not replace ethers instance when network changes to loading', function () { const networkController = getMockNetworkController(); + const onNetworkStateChange = sinon.stub(); const swapsController = new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController, + onNetworkStateChange, provider, getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getTokenRatesState: MOCK_TOKEN_RATES_STORE, @@ -243,9 +250,13 @@ describe('SwapsController', function () { getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; - const onNetworkDidChange = networkController.on.getCall(0).args[1]; + const changeNetwork = onNetworkStateChange.getCall(0).args[0]; - onNetworkDidChange('loading'); + networkController.store.getState.returns({ + networkId: null, + networkStatus: NetworkStatus.Unknown, + }); + changeNetwork('loading'); const newEthersInstance = swapsController.ethersProvider; assert.strictEqual( @@ -257,9 +268,11 @@ describe('SwapsController', function () { it('should not replace ethers instance when network changes to the same network', function () { const networkController = getMockNetworkController(); + const onNetworkStateChange = sinon.stub(); const swapsController = new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController, + onNetworkStateChange, provider, getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getTokenRatesState: MOCK_TOKEN_RATES_STORE, @@ -267,9 +280,13 @@ describe('SwapsController', function () { getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; - const onNetworkDidChange = networkController.on.getCall(0).args[1]; + const changeNetwork = onNetworkStateChange.getCall(0).args[0]; - onNetworkDidChange(NETWORK_IDS.GOERLI); + networkController.store.getState.returns({ + networkId: NETWORK_IDS.GOERLI, + networkStatus: NetworkStatus.Available, + }); + changeNetwork(NETWORK_IDS.GOERLI); const newEthersInstance = swapsController.ethersProvider; assert.strictEqual( @@ -705,6 +722,72 @@ describe('SwapsController', function () { ); }); + it('calls returns the correct quotes on the optimism chain', async function () { + fetchTradesInfoStub.resetHistory(); + const OPTIMISM_MOCK_FETCH_METADATA = { + ...MOCK_FETCH_METADATA, + chainId: CHAIN_IDS.OPTIMISM, + }; + const optimismProviderResultStub = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + eth_call: + '0x000000000000000000000000000000000000000000000000000103c18816d4e8', + }; + const optimismProvider = createTestProviderTools({ + scaffold: optimismProviderResultStub, + networkId: 10, + chainId: 10, + }).provider; + + swapsController = getSwapsController(optimismProvider); + + fetchTradesInfoStub.resolves(getMockQuotes()); + + // Make it so approval is not required + sandbox + .stub(swapsController, '_getERC20Allowance') + .resolves(BigNumber.from(1)); + + const [newQuotes] = await swapsController.fetchAndSetQuotes( + MOCK_FETCH_PARAMS, + OPTIMISM_MOCK_FETCH_METADATA, + ); + + assert.deepStrictEqual(newQuotes[TEST_AGG_ID_BEST], { + ...getMockQuotes()[TEST_AGG_ID_BEST], + sourceTokenInfo: undefined, + destinationTokenInfo: { + symbol: 'FOO', + decimals: 18, + }, + isBestQuote: true, + // TODO: find a way to calculate these values dynamically + gasEstimate: 2000000, + gasEstimateWithRefund: '0xb8cae', + savings: { + fee: '-0.061067', + metaMaskFee: '0.5050505050505050505', + performance: '6', + total: '5.4338824949494949495', + medianMetaMaskFee: '0.44444444444444444444', + }, + ethFee: '0.113822', + multiLayerL1TradeFeeTotal: '0x0103c18816d4e8', + overallValueOfQuote: '49.886178', + metaMaskFeeInEth: '0.5050505050505050505', + ethValueOfTokens: '50', + }); + assert.strictEqual( + fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS, { + ...OPTIMISM_MOCK_FETCH_METADATA, + }), + true, + ); + }); + it('performs the allowance check', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 112071ffd..c3026e043 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1,4 +1,4 @@ -import EventEmitter from 'safe-event-emitter'; +import EventEmitter from '@metamask/safe-event-emitter'; import { ObservableStore } from '@metamask/obs-store'; import { bufferToHex, keccak, toBuffer, isHexString } from 'ethereumjs-util'; import EthQuery from 'ethjs-query'; @@ -39,11 +39,12 @@ import { hexWEIToDecGWEI, } from '../../../../shared/modules/conversion.utils'; import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { HARDFORKS, CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, NETWORK_TYPES, + NetworkStatus, } from '../../../../shared/constants/network'; import { determineTransactionAssetType, @@ -115,7 +116,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain'; * * @param {object} opts * @param {object} opts.initState - initial transaction list default is an empty array - * @param {Function} opts.getNetworkState - Get the current network state. + * @param {Function} opts.getNetworkId - Get the current network ID. + * @param {Function} opts.getNetworkStatus - Get the current network status. * @param {Function} opts.onNetworkStateChange - Subscribe to network state change events. * @param {object} opts.blockTracker - An instance of eth-blocktracker * @param {object} opts.provider - A network provider. @@ -129,7 +131,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain'; export default class TransactionController extends EventEmitter { constructor(opts) { super(); - this.getNetworkState = opts.getNetworkState; + this.getNetworkId = opts.getNetworkId; + this.getNetworkStatus = opts.getNetworkStatus; this._getCurrentChainId = opts.getCurrentChainId; this.getProviderConfig = opts.getProviderConfig; this._getCurrentNetworkEIP1559Compatibility = @@ -167,7 +170,8 @@ export default class TransactionController extends EventEmitter { this.txStateManager = new TransactionStateManager({ initState: opts.initState, txHistoryLimit: opts.txHistoryLimit, - getNetworkState: this.getNetworkState, + getNetworkId: this.getNetworkId, + getNetworkStatus: this.getNetworkStatus, getCurrentChainId: opts.getCurrentChainId, }); @@ -226,10 +230,13 @@ export default class TransactionController extends EventEmitter { * @returns {number} The numerical chainId. */ getChainId() { - const networkState = this.getNetworkState(); + const networkStatus = this.getNetworkStatus(); const chainId = this._getCurrentChainId(); const integerChainId = parseInt(chainId, 16); - if (networkState === 'loading' || Number.isNaN(integerChainId)) { + if ( + networkStatus !== NetworkStatus.Available || + Number.isNaN(integerChainId) + ) { return 0; } return integerChainId; @@ -272,12 +279,13 @@ export default class TransactionController extends EventEmitter { }); } - // For 'rpc' we need to use the same basic configuration as mainnet, - // since we only support EVM compatible chains, and then override the + // For 'rpc' we need to use the same basic configuration as mainnet, since + // we only support EVM compatible chains, and then override the // name, chainId and networkId properties. This is done using the // `forCustomChain` static method on the Common class. const chainId = parseInt(this._getCurrentChainId(), 16); - const networkId = this.getNetworkState(); + const networkStatus = this.getNetworkStatus(); + const networkId = this.getNetworkId(); const customChainParams = { name, @@ -291,7 +299,8 @@ export default class TransactionController extends EventEmitter { // on a custom network that requires valid network id. I have not ran // into this limitation on any network I have attempted, even when // hardcoding networkId to 'loading'. - networkId: networkId === 'loading' ? 0 : parseInt(networkId, 10), + networkId: + networkStatus === NetworkStatus.Available ? parseInt(networkId, 10) : 0, }; return Common.forCustomChain( @@ -2018,7 +2027,7 @@ export default class TransactionController extends EventEmitter { this._trackMetaMetricsEvent({ event: 'Swap Failed', sensitiveProperties: { ...txMeta.swapMetaData }, - category: EVENT.CATEGORIES.SWAPS, + category: MetaMetricsEventCategory.Swaps, }); } else { const tokensReceived = getSwapsTokensReceivedFromTxMeta( @@ -2053,7 +2062,7 @@ export default class TransactionController extends EventEmitter { this._trackMetaMetricsEvent({ event: 'Swap Completed', - category: EVENT.CATEGORIES.SWAPS, + category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { ...txMeta.swapMetaData, token_to_amount_received: tokensReceived, @@ -2405,7 +2414,7 @@ export default class TransactionController extends EventEmitter { // occur. case TransactionMetaMetricsEvent.added: this.createEventFragment({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, initialEvent: TransactionMetaMetricsEvent.added, successEvent: TransactionMetaMetricsEvent.approved, failureEvent: TransactionMetaMetricsEvent.rejected, @@ -2427,7 +2436,7 @@ export default class TransactionController extends EventEmitter { case TransactionMetaMetricsEvent.approved: case TransactionMetaMetricsEvent.rejected: this.createEventFragment({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, successEvent: TransactionMetaMetricsEvent.approved, failureEvent: TransactionMetaMetricsEvent.rejected, properties, @@ -2449,7 +2458,7 @@ export default class TransactionController extends EventEmitter { // properties to the transaction event. case TransactionMetaMetricsEvent.submitted: this.createEventFragment({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, initialEvent: TransactionMetaMetricsEvent.submitted, successEvent: TransactionMetaMetricsEvent.finalized, properties, @@ -2469,7 +2478,7 @@ export default class TransactionController extends EventEmitter { // fragment does not exist. case TransactionMetaMetricsEvent.finalized: this.createEventFragment({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, successEvent: TransactionMetaMetricsEvent.finalized, properties, sensitiveProperties, diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 096783fae..c16f1fa2c 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -10,7 +10,10 @@ import { getTestAccounts, } from '../../../../test/stub/provider'; import mockEstimates from '../../../../test/data/mock-estimates.json'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsTransactionEventSource, +} from '../../../../shared/constants/metametrics'; import { TransactionStatus, TransactionType, @@ -27,12 +30,14 @@ import { } from '../../../../shared/constants/gas'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { NetworkStatus } from '../../../../shared/constants/network'; import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils'; import TransactionController from '.'; const noop = () => true; const currentNetworkId = '5'; const currentChainId = '0x5'; +const currentNetworkStatus = NetworkStatus.Available; const providerConfig = { type: 'goerli', }; @@ -46,7 +51,8 @@ describe('Transaction Controller', function () { providerResultStub, fromAccount, fragmentExists, - networkStore; + networkStatusStore, + getCurrentChainId; beforeEach(function () { fragmentExists = false; @@ -59,22 +65,27 @@ describe('Transaction Controller', function () { provider = createTestProviderTools({ scaffold: providerResultStub, networkId: currentNetworkId, - chainId: currentNetworkId, + chainId: parseInt(currentChainId, 16), }).provider; - networkStore = new ObservableStore(currentNetworkId); + networkStatusStore = new ObservableStore(currentNetworkStatus); fromAccount = getTestAccounts()[0]; const blockTrackerStub = new EventEmitter(); blockTrackerStub.getCurrentBlock = noop; blockTrackerStub.getLatestBlock = noop; + + getCurrentChainId = sinon.stub().callsFake(() => currentChainId); + txController = new TransactionController({ provider, getGasPrice() { return '0xee6b2800'; }, - getNetworkState: () => networkStore.getState(), - onNetworkStateChange: (listener) => networkStore.subscribe(listener), + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => networkStatusStore.getState(), + onNetworkStateChange: (listener) => + networkStatusStore.subscribe(listener), getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(false), getCurrentAccountEIP1559Compatibility: () => false, txHistoryLimit: 10, @@ -85,7 +96,7 @@ describe('Transaction Controller', function () { }), getProviderConfig: () => providerConfig, getPermittedAccounts: () => undefined, - getCurrentChainId: () => currentChainId, + getCurrentChainId, getParticipateInMetrics: () => false, trackMetaMetricsEvent: () => undefined, createEventFragment: () => undefined, @@ -467,8 +478,8 @@ describe('Transaction Controller', function () { ); }); - it('should fail if netId is loading', async function () { - networkStore.putState('loading'); + it('should fail if the network status is not "available"', async function () { + networkStatusStore.putState(NetworkStatus.Unknown); await assert.rejects( () => txController.addUnapprovedTransaction(undefined, { @@ -1079,8 +1090,19 @@ describe('Transaction Controller', function () { }); describe('#getChainId', function () { - it('returns 0 when the chainId is NaN', function () { - networkStore.putState('loading'); + it('returns the chain ID of the network when it is available', function () { + networkStatusStore.putState(NetworkStatus.Available); + assert.equal(txController.getChainId(), 5); + }); + + it('returns 0 when the network is not available', function () { + networkStatusStore.putState('asdflsfadf'); + assert.equal(txController.getChainId(), 0); + }); + + it('returns 0 when the chain ID cannot be parsed as a hex string', function () { + networkStatusStore.putState(NetworkStatus.Available); + getCurrentChainId.returns('$fdsjfldf'); assert.equal(txController.getChainId(), 0); }); }); @@ -1753,7 +1775,7 @@ describe('Transaction Controller', function () { successEvent: 'Transaction Approved', failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, persist: true, properties: { chain_id: '0x5', @@ -1762,7 +1784,7 @@ describe('Transaction Controller', function () { gas_edit_type: 'none', network: '5', referrer: ORIGIN_METAMASK, - source: EVENT.SOURCE.TRANSACTION.USER, + source: MetaMetricsTransactionEventSource.User, transaction_type: TransactionType.simpleSend, account_type: 'MetaMask', asset_type: AssetType.native, @@ -1840,7 +1862,7 @@ describe('Transaction Controller', function () { initialEvent: 'Transaction Submitted', successEvent: 'Transaction Finalized', uniqueIdentifier: 'transaction-submitted-1', - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, persist: true, properties: { chain_id: '0x5', @@ -1849,7 +1871,7 @@ describe('Transaction Controller', function () { gas_edit_type: 'none', network: '5', referrer: ORIGIN_METAMASK, - source: EVENT.SOURCE.TRANSACTION.USER, + source: MetaMetricsTransactionEventSource.User, transaction_type: TransactionType.simpleSend, account_type: 'MetaMask', asset_type: AssetType.native, @@ -1939,7 +1961,7 @@ describe('Transaction Controller', function () { successEvent: 'Transaction Approved', failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, persist: true, properties: { chain_id: '0x5', @@ -1948,7 +1970,7 @@ describe('Transaction Controller', function () { gas_edit_type: 'none', network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, account_type: 'MetaMask', asset_type: AssetType.native, @@ -2028,7 +2050,7 @@ describe('Transaction Controller', function () { initialEvent: 'Transaction Submitted', successEvent: 'Transaction Finalized', uniqueIdentifier: 'transaction-submitted-1', - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, persist: true, properties: { chain_id: '0x5', @@ -2037,7 +2059,7 @@ describe('Transaction Controller', function () { gas_edit_type: 'none', network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, account_type: 'MetaMask', asset_type: AssetType.native, @@ -2119,7 +2141,7 @@ describe('Transaction Controller', function () { successEvent: 'Transaction Approved', failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, persist: true, properties: { chain_id: '0x5', @@ -2128,7 +2150,7 @@ describe('Transaction Controller', function () { gas_edit_type: 'none', network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, account_type: 'MetaMask', asset_type: AssetType.native, @@ -2192,11 +2214,11 @@ describe('Transaction Controller', function () { failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', persist: true, - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, properties: { network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, chain_id: '0x5', eip_1559_version: '0', @@ -2266,11 +2288,11 @@ describe('Transaction Controller', function () { failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', persist: true, - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, properties: { network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, chain_id: '0x5', eip_1559_version: '0', @@ -2340,11 +2362,11 @@ describe('Transaction Controller', function () { failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', persist: true, - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, properties: { network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, chain_id: '0x5', eip_1559_version: '0', @@ -2422,7 +2444,7 @@ describe('Transaction Controller', function () { failureEvent: 'Transaction Rejected', uniqueIdentifier: 'transaction-added-1', persist: true, - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, properties: { chain_id: '0x5', eip_1559_version: '2', @@ -2430,7 +2452,7 @@ describe('Transaction Controller', function () { gas_edit_type: 'none', network: '5', referrer: 'other', - source: EVENT.SOURCE.TRANSACTION.DAPP, + source: MetaMetricsTransactionEventSource.Dapp, transaction_type: TransactionType.simpleSend, account_type: 'MetaMask', asset_type: AssetType.native, diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 6252727c3..93b1de252 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,4 +1,4 @@ -import EventEmitter from 'safe-event-emitter'; +import EventEmitter from '@metamask/safe-event-emitter'; import log from 'loglevel'; import EthQuery from 'ethjs-query'; import { TransactionStatus } from '../../../../shared/constants/transaction'; diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index bd8e38051..20e142c6c 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,4 +1,4 @@ -import EventEmitter from 'safe-event-emitter'; +import EventEmitter from '@metamask/safe-event-emitter'; import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import { values, keyBy, mapValues, omitBy, pickBy, sortBy } from 'lodash'; @@ -7,6 +7,7 @@ import { TransactionStatus } from '../../../../shared/constants/transaction'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { NetworkStatus } from '../../../../shared/constants/network'; import { generateHistoryEntry, replayHistory, @@ -54,13 +55,15 @@ export const ERROR_SUBMITTING = * transactions list keyed by id * @param {number} [opts.txHistoryLimit] - limit for how many finished * transactions can hang around in state - * @param {Function} opts.getNetworkState - Get the current network state. + * @param {Function} opts.getNetworkId - Get the current network Id. + * @param {Function} opts.getNetworkStatus - Get the current network status. */ export default class TransactionStateManager extends EventEmitter { constructor({ initState, txHistoryLimit, - getNetworkState, + getNetworkId, + getNetworkStatus, getCurrentChainId, }) { super(); @@ -70,7 +73,8 @@ export default class TransactionStateManager extends EventEmitter { ...initState, }); this.txHistoryLimit = txHistoryLimit; - this.getNetworkState = getNetworkState; + this.getNetworkId = getNetworkId; + this.getNetworkStatus = getNetworkStatus; this.getCurrentChainId = getCurrentChainId; } @@ -86,9 +90,10 @@ export default class TransactionStateManager extends EventEmitter { * @returns {TransactionMeta} the default txMeta object */ generateTxMeta(opts = {}) { - const netId = this.getNetworkState(); + const networkId = this.getNetworkId(); + const networkStatus = this.getNetworkStatus(); const chainId = this.getCurrentChainId(); - if (netId === 'loading') { + if (networkStatus !== NetworkStatus.Available) { throw new Error('MetaMask is having trouble connecting to the network'); } @@ -128,7 +133,7 @@ export default class TransactionStateManager extends EventEmitter { id: createId(), time: new Date().getTime(), status: TransactionStatus.unapproved, - metamaskNetworkId: netId, + metamaskNetworkId: networkId, originalGasEstimate: opts.txParams?.gas, userEditedGasLimit: false, chainId, @@ -149,12 +154,12 @@ export default class TransactionStateManager extends EventEmitter { */ getUnapprovedTxList() { const chainId = this.getCurrentChainId(); - const network = this.getNetworkState(); + const networkId = this.getNetworkId(); return pickBy( this.store.getState().transactions, (transaction) => transaction.status === TransactionStatus.unapproved && - transactionMatchesNetwork(transaction, chainId, network), + transactionMatchesNetwork(transaction, chainId, networkId), ); } @@ -413,7 +418,7 @@ export default class TransactionStateManager extends EventEmitter { limit, } = {}) { const chainId = this.getCurrentChainId(); - const network = this.getNetworkState(); + const networkId = this.getNetworkId(); // searchCriteria is an object that might have values that aren't predicate // methods. When providing any other value type (string, number, etc), we // consider this shorthand for "check the value at key for strict equality @@ -442,7 +447,7 @@ export default class TransactionStateManager extends EventEmitter { // when filterToCurrentNetwork is true. if ( filterToCurrentNetwork && - transactionMatchesNetwork(transaction, chainId, network) === false + transactionMatchesNetwork(transaction, chainId, networkId) === false ) { return false; } @@ -596,8 +601,7 @@ export default class TransactionStateManager extends EventEmitter { } /** - * Removes all transactions for the given address on the current network, - * preferring chainId for comparison over networkId. + * Removes all transactions for the given address on the current network. * * @param {string} address - hex string of the from address on the txParams * to remove @@ -605,8 +609,8 @@ export default class TransactionStateManager extends EventEmitter { wipeTransactions(address) { // network only tx const { transactions } = this.store.getState(); - const network = this.getNetworkState(); const chainId = this.getCurrentChainId(); + const networkId = this.getNetworkId(); // Update state this.store.updateState({ @@ -614,7 +618,7 @@ export default class TransactionStateManager extends EventEmitter { transactions, (transaction) => transaction.txParams.from === address && - transactionMatchesNetwork(transaction, chainId, network), + transactionMatchesNetwork(transaction, chainId, networkId), ), }); } diff --git a/app/scripts/controllers/transactions/tx-state-manager.test.js b/app/scripts/controllers/transactions/tx-state-manager.test.js index 677f59334..a3b420e10 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.test.js +++ b/app/scripts/controllers/transactions/tx-state-manager.test.js @@ -4,7 +4,11 @@ import { TransactionStatus, TransactionType, } from '../../../../shared/constants/transaction'; -import { CHAIN_IDS, NETWORK_IDS } from '../../../../shared/constants/network'; +import { + CHAIN_IDS, + NETWORK_IDS, + NetworkStatus, +} from '../../../../shared/constants/network'; import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import TxStateManager, { ERROR_SUBMITTING } from './tx-state-manager'; @@ -45,6 +49,7 @@ function generateTransactions( describe('TransactionStateManager', function () { let txStateManager; const currentNetworkId = NETWORK_IDS.GOERLI; + const currentNetworkStatus = NetworkStatus.Available; const currentChainId = CHAIN_IDS.MAINNET; const otherNetworkId = '2'; @@ -54,7 +59,8 @@ describe('TransactionStateManager', function () { transactions: {}, }, txHistoryLimit: 10, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); }); @@ -181,7 +187,8 @@ describe('TransactionStateManager', function () { [confirmedTx.id]: confirmedTx, }, }, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); @@ -246,7 +253,8 @@ describe('TransactionStateManager', function () { [confirmedTx3.id]: confirmedTx3, }, }, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); @@ -355,7 +363,8 @@ describe('TransactionStateManager', function () { [failedTx3Dupe.id]: failedTx3Dupe, }, }, - getNetworkState: () => currentNetworkId, + getNetworkId: () => currentNetworkId, + getNetworkStatus: () => currentNetworkStatus, getCurrentChainId: () => currentChainId, }); diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index eed68abae..cbc87aef7 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -62,7 +62,7 @@ export default class AccountTracker { accounts: {}, currentBlockGasLimit: '', }; - this.store = new ObservableStore(initState); + this.store = new ObservableStore({ ...initState, ...opts.initState }); this.resetState = () => { this.store.updateState(initState); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index 8a5186d42..d34404beb 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,12 +1,15 @@ import { errorCodes } from 'eth-rpc-errors'; +import { detectSIWE } from '@metamask/controller-utils'; +import { isValidAddress } from 'ethereumjs-util'; + import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; +import { TransactionStatus } from '../../../shared/constants/transaction'; import { SECOND } from '../../../shared/constants/time'; -import { detectSIWE } from '../../../shared/modules/siwe'; + import { - EVENT, - EVENT_NAMES, - METAMETRIC_KEY_OPTIONS, - METAMETRIC_KEY, + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsEventUiCustomization, } from '../../../shared/constants/metametrics'; /** @@ -41,55 +44,55 @@ const RATE_LIMIT_MAP = { /** * For events with user interaction (approve / reject | cancel) this map will - * return an object with APPROVED, REJECTED and REQUESTED keys that map to the + * return an object with APPROVED, REJECTED, REQUESTED, and FAILED keys that map to the * appropriate event names. */ const EVENT_NAME_MAP = { [MESSAGE_TYPE.ETH_SIGN]: { - APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, - FAILED: EVENT_NAMES.SIGNATURE_FAILED, - REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, - REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, + APPROVED: MetaMetricsEventName.SignatureApproved, + FAILED: MetaMetricsEventName.SignatureFailed, + REJECTED: MetaMetricsEventName.SignatureRejected, + REQUESTED: MetaMetricsEventName.SignatureRequested, }, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: { - APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, - REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, - REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, + APPROVED: MetaMetricsEventName.SignatureApproved, + REJECTED: MetaMetricsEventName.SignatureRejected, + REQUESTED: MetaMetricsEventName.SignatureRequested, }, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: { - APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, - REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, - REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, + APPROVED: MetaMetricsEventName.SignatureApproved, + REJECTED: MetaMetricsEventName.SignatureRejected, + REQUESTED: MetaMetricsEventName.SignatureRequested, }, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: { - APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, - REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, - REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, + APPROVED: MetaMetricsEventName.SignatureApproved, + REJECTED: MetaMetricsEventName.SignatureRejected, + REQUESTED: MetaMetricsEventName.SignatureRequested, }, [MESSAGE_TYPE.PERSONAL_SIGN]: { - APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, - REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, - REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, + APPROVED: MetaMetricsEventName.SignatureApproved, + REJECTED: MetaMetricsEventName.SignatureRejected, + REQUESTED: MetaMetricsEventName.SignatureRequested, }, [MESSAGE_TYPE.ETH_DECRYPT]: { - APPROVED: EVENT_NAMES.DECRYPTION_APPROVED, - REJECTED: EVENT_NAMES.DECRYPTION_REJECTED, - REQUESTED: EVENT_NAMES.DECRYPTION_REQUESTED, + APPROVED: MetaMetricsEventName.DecryptionApproved, + REJECTED: MetaMetricsEventName.DecryptionRejected, + REQUESTED: MetaMetricsEventName.DecryptionRequested, }, [MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: { - APPROVED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_APPROVED, - REJECTED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REJECTED, - REQUESTED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED, + APPROVED: MetaMetricsEventName.EncryptionPublicKeyApproved, + REJECTED: MetaMetricsEventName.EncryptionPublicKeyRejected, + REQUESTED: MetaMetricsEventName.EncryptionPublicKeyRequested, }, [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: { - APPROVED: EVENT_NAMES.PERMISSIONS_APPROVED, - REJECTED: EVENT_NAMES.PERMISSIONS_REJECTED, - REQUESTED: EVENT_NAMES.PERMISSIONS_REQUESTED, + APPROVED: MetaMetricsEventName.PermissionsApproved, + REJECTED: MetaMetricsEventName.PermissionsRejected, + REQUESTED: MetaMetricsEventName.PermissionsRequested, }, [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: { - APPROVED: EVENT_NAMES.PERMISSIONS_APPROVED, - REJECTED: EVENT_NAMES.PERMISSIONS_REJECTED, - REQUESTED: EVENT_NAMES.PERMISSIONS_REQUESTED, + APPROVED: MetaMetricsEventName.PermissionsApproved, + REJECTED: MetaMetricsEventName.PermissionsRejected, + REQUESTED: MetaMetricsEventName.PermissionsRequested, }, }; @@ -142,6 +145,8 @@ export default function createRPCMethodTrackingMiddleware({ // keys for the various events in the flow. const eventType = EVENT_NAME_MAP[method]; + const eventProperties = {}; + // Boolean variable that reduces code duplication and increases legibility const shouldTrackEvent = // Don't track if the request came from our own UI or background @@ -160,29 +165,32 @@ export default function createRPCMethodTrackingMiddleware({ // 'Provider Method Called'. const event = eventType ? eventType.REQUESTED - : EVENT_NAMES.PROVIDER_METHOD_CALLED; + : MetaMetricsEventName.ProviderMethodCalled; - const properties = {}; + if (event === MetaMetricsEventName.SignatureRequested) { + eventProperties.signature_type = method; - let msgParams; - - if (event === EVENT_NAMES.SIGNATURE_REQUESTED) { - properties.signature_type = method; - - const data = req?.params?.[0]; - const from = req?.params?.[1]; + // In personal messages the first param is data while in typed messages second param is data + // if condition below is added to ensure that the right params are captured as data and address. + let data; + let from; + if (isValidAddress(req?.params?.[1])) { + data = req?.params?.[0]; + from = req?.params?.[1]; + } else { + data = req?.params?.[1]; + from = req?.params?.[0]; + } const paramsExamplePassword = req?.params?.[2]; - msgParams = { - ...paramsExamplePassword, - from, - data, - origin, - }; - const msgData = { - msgParams, - status: 'unapproved', + msgParams: { + ...paramsExamplePassword, + from, + data, + origin, + }, + status: TransactionStatus.unapproved, type: req.method, }; @@ -193,25 +201,21 @@ export default function createRPCMethodTrackingMiddleware({ ); if (securityProviderResponse?.flagAsDangerous === 1) { - properties.ui_customizations = ['flagged_as_malicious']; + eventProperties.ui_customizations = [ + MetaMetricsEventUiCustomization.FlaggedAsMalicious, + ]; } else if (securityProviderResponse?.flagAsDangerous === 2) { - properties.ui_customizations = ['flagged_as_safety_unknown']; - } else { - properties.ui_customizations = null; + eventProperties.ui_customizations = [ + MetaMetricsEventUiCustomization.FlaggedAsSafetyUnknown, + ]; } if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const { isSIWEMessage } = detectSIWE({ data }); if (isSIWEMessage) { - properties.ui_customizations === null - ? (properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ]) - : properties.ui_customizations.push( - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ); + eventProperties.ui_customizations = ( + eventProperties.ui_customizations || [] + ).concat(MetaMetricsEventUiCustomization.Siwe); } } } catch (e) { @@ -220,16 +224,16 @@ export default function createRPCMethodTrackingMiddleware({ ); } } else { - properties.method = method; + eventProperties.method = method; } trackEvent({ event, - category: EVENT.CATEGORIES.INPAGE_PROVIDER, + category: MetaMetricsEventCategory.InpageProvider, referrer: { url: origin, }, - properties, + properties: eventProperties, }); rateLimitTimeouts[method] = setTimeout(() => { @@ -242,8 +246,6 @@ export default function createRPCMethodTrackingMiddleware({ return callback(); } - const properties = {}; - // The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings const isDisabledEthSignAdvancedSetting = method === MESSAGE_TYPE.ETH_SIGN && @@ -254,79 +256,20 @@ export default function createRPCMethodTrackingMiddleware({ let event; if (isDisabledRPCMethod) { event = eventType.FAILED; - properties.error = res.error; - } else if (res.error?.code === 4001) { + eventProperties.error = res.error; + } else if (res.error?.code === errorCodes.provider.userRejectedRequest) { event = eventType.REJECTED; } else { event = eventType.APPROVED; } - let msgParams; - - if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) { - properties.signature_type = method; - - const data = req?.params?.[0]; - const from = req?.params?.[1]; - const paramsExamplePassword = req?.params?.[2]; - - msgParams = { - ...paramsExamplePassword, - from, - data, - origin, - }; - - const msgData = { - msgParams, - status: 'unapproved', - type: req.method, - }; - - try { - const securityProviderResponse = await securityProviderRequest( - msgData, - req.method, - ); - - if (securityProviderResponse?.flagAsDangerous === 1) { - properties.ui_customizations = ['flagged_as_malicious']; - } else if (securityProviderResponse?.flagAsDangerous === 2) { - properties.ui_customizations = ['flagged_as_safety_unknown']; - } else { - properties.ui_customizations = null; - } - - if (method === MESSAGE_TYPE.PERSONAL_SIGN) { - const { isSIWEMessage } = detectSIWE({ data }); - if (isSIWEMessage) { - properties.ui_customizations === null - ? (properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ]) - : properties.ui_customizations.push( - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ); - } - } - } catch (e) { - console.warn( - `createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`, - ); - } - } else { - properties.method = method; - } - trackEvent({ event, - category: EVENT.CATEGORIES.INPAGE_PROVIDER, + category: MetaMetricsEventCategory.InpageProvider, referrer: { url: origin, }, - properties, + properties: eventProperties, }); return callback(); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index af910279e..5da8206fa 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -1,6 +1,10 @@ import { errorCodes } from 'eth-rpc-errors'; +import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { EVENT_NAMES } from '../../../shared/constants/metametrics'; +import { + MetaMetricsEventName, + MetaMetricsEventUiCustomization, +} from '../../../shared/constants/metametrics'; import { SECOND } from '../../../shared/constants/time'; import createRPCMethodTrackingMiddleware from './createRPCMethodTrackingMiddleware'; @@ -52,6 +56,12 @@ function getNext(timeout = 500) { const waitForSeconds = async (seconds) => await new Promise((resolve) => setTimeout(resolve, SECOND * seconds)); +jest.mock('@metamask/controller-utils', () => ({ + detectSIWE: jest.fn().mockImplementation(() => { + return { isSIWEMessage: false }; + }), +})); + describe('createRPCMethodTrackingMiddleware', () => { afterEach(() => { jest.resetAllMocks(); @@ -101,7 +111,7 @@ describe('createRPCMethodTrackingMiddleware', () => { metricsState.participateInMetaMetrics = true; }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => { + it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => { const req = { method: MESSAGE_TYPE.ETH_SIGN, origin: 'some.dapp', @@ -115,7 +125,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(1); expect(trackEvent.mock.calls[0][0]).toMatchObject({ category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, + event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, }, @@ -123,7 +133,7 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); - it(`should track a ${EVENT_NAMES.SIGNATURE_APPROVED} event if the user approves`, async () => { + it(`should track a ${MetaMetricsEventName.SignatureApproved} event if the user approves`, async () => { const req = { method: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, origin: 'some.dapp', @@ -138,7 +148,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_APPROVED, + event: MetaMetricsEventName.SignatureApproved, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, }, @@ -146,14 +156,14 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); - it(`should track a ${EVENT_NAMES.SIGNATURE_REJECTED} event if the user approves`, async () => { + it(`should track a ${MetaMetricsEventName.SignatureRejected} event if the user approves`, async () => { const req = { method: MESSAGE_TYPE.PERSONAL_SIGN, origin: 'some.dapp', }; const res = { - error: { code: 4001 }, + error: { code: errorCodes.provider.userRejectedRequest }, }; const { next, executeMiddlewareStack } = getNext(); await handler(req, res, next); @@ -161,7 +171,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REJECTED, + event: MetaMetricsEventName.SignatureRejected, properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN, }, @@ -169,7 +179,7 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); - it(`should track a ${EVENT_NAMES.PERMISSIONS_APPROVED} event if the user approves`, async () => { + it(`should track a ${MetaMetricsEventName.PermissionsApproved} event if the user approves`, async () => { const req = { method: MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS, origin: 'some.dapp', @@ -182,7 +192,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ category: 'inpage_provider', - event: EVENT_NAMES.PERMISSIONS_APPROVED, + event: MetaMetricsEventName.PermissionsApproved, properties: { method: MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS }, referrer: { url: 'some.dapp' }, }); @@ -230,8 +240,38 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent.mock.calls[1][0].properties.method).toBe('eth_chainId'); }); + it('should track Sign-in With Ethereum (SIWE) message if detected', async () => { + const req = { + method: MESSAGE_TYPE.PERSONAL_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next, executeMiddlewareStack } = getNext(); + + detectSIWE.mockImplementation(() => { + return { isSIWEMessage: true }; + }); + + await handler(req, res, next); + await executeMiddlewareStack(); + + expect(trackEvent).toHaveBeenCalledTimes(2); + + expect(trackEvent.mock.calls[1][0]).toMatchObject({ + category: 'inpage_provider', + event: MetaMetricsEventName.SignatureApproved, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + ui_customizations: [MetaMetricsEventUiCustomization.Siwe], + }, + referrer: { url: 'some.dapp' }, + }); + }); + describe(`when '${MESSAGE_TYPE.ETH_SIGN}' is disabled in advanced settings`, () => { - it(`should track ${EVENT_NAMES.SIGNATURE_FAILED} and include error property`, async () => { + it(`should track ${MetaMetricsEventName.SignatureFailed} and include error property`, async () => { const mockError = { code: errorCodes.rpc.methodNotFound }; const req = { method: MESSAGE_TYPE.ETH_SIGN, @@ -249,7 +289,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent.mock.calls[1][0]).toMatchObject({ category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_FAILED, + event: MetaMetricsEventName.SignatureFailed, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, error: mockError, @@ -258,93 +298,150 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); }); - }); - describe('participateInMetaMetrics is set to true with a request flagged as safe', () => { - beforeEach(() => { - metricsState.participateInMetaMetrics = true; - }); + describe('when request is flagged as safe by security provider', () => { + it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next } = getNext(); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safe`, async () => { - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; + await handler(req, res, next); - const res = { - error: null, - }; - const { next } = getNext(); - await handler(req, res, next); - expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - ui_customizations: null, - }, - referrer: { url: 'some.dapp' }, + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: MetaMetricsEventName.SignatureRequested, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + }, + referrer: { url: 'some.dapp' }, + }); }); }); - }); - describe('participateInMetaMetrics is set to true with a request flagged as malicious', () => { - beforeEach(() => { - metricsState.participateInMetaMetrics = true; - flagAsDangerous = 1; - }); + describe('when request is flagged as malicious by security provider', () => { + beforeEach(() => { + flagAsDangerous = 1; + }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => { - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; + it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event which is flagged as malicious`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next } = getNext(); - const res = { - error: null, - }; - const { next } = getNext(); - await handler(req, res, next); - expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - ui_customizations: ['flagged_as_malicious'], - }, - referrer: { url: 'some.dapp' }, + await handler(req, res, next); + + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: MetaMetricsEventName.SignatureRequested, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: ['flagged_as_malicious'], + }, + referrer: { url: 'some.dapp' }, + }); }); }); - }); - describe('participateInMetaMetrics is set to true with a request flagged as safety unknown', () => { - beforeEach(() => { - metricsState.participateInMetaMetrics = true; - flagAsDangerous = 2; + describe('when request flagged as safety unknown by security provider', () => { + beforeEach(() => { + flagAsDangerous = 2; + }); + + it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event which is flagged as safety unknown`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next } = getNext(); + + await handler(req, res, next); + + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: MetaMetricsEventName.SignatureRequested, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: ['flagged_as_safety_unknown'], + }, + referrer: { url: 'some.dapp' }, + }); + }); }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => { - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; + describe('when signature requests are received', () => { + let securityProviderReq, fnHandler; + beforeEach(() => { + securityProviderReq = jest.fn().mockReturnValue(() => + Promise.resolve({ + flagAsDangerous: 0, + }), + ); - const res = { - error: null, - }; - const { next } = getNext(); - await handler(req, res, next); - expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - ui_customizations: ['flagged_as_safety_unknown'], - }, - referrer: { url: 'some.dapp' }, + fnHandler = createRPCMethodTrackingMiddleware({ + trackEvent, + getMetricsState, + rateLimitSeconds: 1, + securityProviderRequest: securityProviderReq, + }); + }); + it(`should pass correct data for personal sign`, async () => { + const req = { + method: 'personal_sign', + params: [ + '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765', + '0x8eeee1781fd885ff5ddef7789486676961873d12', + 'Example password', + ], + jsonrpc: '2.0', + id: 1142196570, + origin: 'https://metamask.github.io', + tabId: 1048582817, + }; + const res = { id: 1142196570, jsonrpc: '2.0' }; + const { next } = getNext(); + + await fnHandler(req, res, next); + + expect(securityProviderReq).toHaveBeenCalledTimes(1); + const call = securityProviderReq.mock.calls[0][0]; + expect(call.msgParams.data).toStrictEqual(req.params[0]); + }); + it(`should pass correct data for typed sign`, async () => { + const req = { + method: 'eth_signTypedData_v4', + params: [ + '0x8eeee1781fd885ff5ddef7789486676961873d12', + '{"domain":{"chainId":"5","name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Group":[{"name":"name","type":"string"},{"name":"members","type":"Person[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}', + ], + jsonrpc: '2.0', + id: 1142196571, + origin: 'https://metamask.github.io', + tabId: 1048582817, + }; + const res = { id: 1142196571, jsonrpc: '2.0' }; + const { next } = getNext(); + + await fnHandler(req, res, next); + + expect(securityProviderReq).toHaveBeenCalledTimes(1); + const call = securityProviderReq.mock.calls[0][0]; + expect(call.msgParams.data).toStrictEqual(req.params[1]); }); }); }); diff --git a/app/scripts/lib/decrypt-message-manager.js b/app/scripts/lib/decrypt-message-manager.js index 197b66763..9249c907e 100644 --- a/app/scripts/lib/decrypt-message-manager.js +++ b/app/scripts/lib/decrypt-message-manager.js @@ -4,7 +4,7 @@ import { bufferToHex } from 'ethereumjs-util'; import { ethErrors } from 'eth-rpc-errors'; import log from 'loglevel'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { EVENT } from '../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import createId from '../../../shared/modules/random-id'; import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; @@ -237,7 +237,7 @@ export default class DecryptMessageManager extends EventEmitter { if (reason) { this.metricsEvent({ event: reason, - category: EVENT.CATEGORIES.MESSAGES, + category: MetaMetricsEventCategory.Messages, properties: { action: 'Decrypt Message Request', }, diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js index 6230c9224..9791e0378 100644 --- a/app/scripts/lib/encryption-public-key-manager.js +++ b/app/scripts/lib/encryption-public-key-manager.js @@ -3,7 +3,7 @@ import { ObservableStore } from '@metamask/obs-store'; import { ethErrors } from 'eth-rpc-errors'; import log from 'loglevel'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { EVENT } from '../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import createId from '../../../shared/modules/random-id'; @@ -225,7 +225,7 @@ export default class EncryptionPublicKeyManager extends EventEmitter { if (reason) { this.metricsEvent({ event: reason, - category: EVENT.CATEGORIES.MESSAGES, + category: MetaMetricsEventCategory.Messages, properties: { action: 'Encryption public key Request', }, diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js deleted file mode 100644 index b070e65a0..000000000 --- a/app/scripts/lib/message-manager.js +++ /dev/null @@ -1,329 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex } from 'ethereumjs-util'; -import { ethErrors } from 'eth-rpc-errors'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; - -/** - * Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for - * an eth_sign call is requested. - * - * @see {@link https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign} - * @typedef {object} Message - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the eth_sign method once the signature request is approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' with - * always have a 'eth_sign' type. - */ - -export default class MessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - Messages. - * - * @param {object} opts - Controller options - * @param {Function} opts.metricsEvent - A function for emitting a metric event. - * @param {Function} opts.securityProviderRequest - A function for verifying a message, whether it is malicious or not. - */ - constructor({ metricsEvent, securityProviderRequest }) { - super(); - this.memStore = new ObservableStore({ - unapprovedMsgs: {}, - unapprovedMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedMsgs: {}, - unapprovedMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' Messages in this.messages - * - * @returns {number} The number of 'unapproved' Messages in this.messages - */ - get unapprovedMsgCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' Messages in this.messages - * - * @returns {object} An index of Message ids to Messages, for all 'unapproved' Messages in this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the - * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {promise} after signature has been - */ - async addUnapprovedMessageAsync(msgParams, req) { - const msgId = await this.addUnapprovedMessage(msgParams, req); - return await new Promise((resolve, reject) => { - // await finished - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return resolve(data.rawSig); - case 'rejected': - return reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - case 'errored': - return reject( - new Error(`MetaMask Message Signature: ${data.error}`), - ); - default: - return reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - } - - /** - * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the - * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object where the origin may be specified - * @returns {number} The id of the newly created message. - */ - async addUnapprovedMessage(msgParams, req) { - // add origin from request - if (req) { - msgParams.origin = req.origin; - } - msgParams.data = normalizeMsgData(msgParams.data); - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.ETH_SIGN, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Adds a passed Message to this.messages, and calls this._saveMsgList() to save the unapproved Messages from that - * list to this.memStore. - * - * @param {Message} msg - The Message to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified Message. - * - * @param {number} msgId - The id of the Message to get - * @returns {Message|undefined} The Message with the id that matches the passed msgId, or undefined if no Message has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a Message. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise with - * any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a Message status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a Message status to 'signed' via a call to this._setMsgStatus and updates that Message in this.messages by - * adding the raw signature data of the signature request to the Message - * - * @param {number} msgId - The id of the Message to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a Message status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a Message status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * Updates the status of a Message in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the Message to update. - * @param {string} status - The new status of the Message. - * @throws A 'MessageManager - Message not found for id: "${msgId}".' if there is no Message in this.messages with an - * id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The Message is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along with the message - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error(`MessageManager - Message not found for id: "${msgId}".`); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a Message in this.messages to the passed Message if the ids are equal. Then saves the unapprovedMsg list to - * storage via this._saveMsgList - * - * @private - * @param {Message} msg - A Message that will replace an existing Message (with the same id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved messages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedMsgs = this.getUnapprovedMsgs(); - const unapprovedMsgCount = Object.keys(unapprovedMsgs).length; - this.memStore.updateState({ unapprovedMsgs, unapprovedMsgCount }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } -} - -/** - * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. - * - * @param {any} data - The buffer data to convert to a hex - * @returns {string} A hex string conversion of the buffer data - */ -export function normalizeMsgData(data) { - if (data.slice(0, 2) === '0x') { - // data is already hex - return data; - } - // data is unicode, convert to hex - return bufferToHex(Buffer.from(data, 'utf8')); -} diff --git a/app/scripts/lib/message-manager.test.js b/app/scripts/lib/message-manager.test.js deleted file mode 100644 index 9a326d000..000000000 --- a/app/scripts/lib/message-manager.test.js +++ /dev/null @@ -1,128 +0,0 @@ -import { TransactionStatus } from '../../../shared/constants/transaction'; -import MessageManager from './message-manager'; - -describe('Message Manager', () => { - let messageManager; - - beforeEach(() => { - messageManager = new MessageManager({ - metricsEvent: jest.fn(), - }); - }); - - describe('#getMsgList', () => { - it('when new should return empty array', () => { - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(0); - }); - }); - - describe('#addMsg', () => { - it('adds a Msg returned in getMsgList', () => { - const Msg = { - id: 1, - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].id).toStrictEqual(1); - }); - }); - - describe('#setMsgStatusApproved', () => { - it('sets the Msg status to approved', () => { - const Msg = { - id: 1, - status: 'unapproved', - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.setMsgStatusApproved(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.approved); - }); - }); - - describe('#rejectMsg', () => { - it('sets the Msg status to rejected', () => { - const Msg = { - id: 1, - status: 'unapproved', - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.rejectMsg(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.rejected); - }); - }); - - describe('#_updateMsg', () => { - it('replaces the Msg with the same id', () => { - messageManager.addMsg({ - id: '1', - status: 'unapproved', - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - messageManager._updateMsg({ - id: '1', - status: 'blah', - hash: 'foo', - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getMsg('1'); - expect(result.hash).toStrictEqual('foo'); - }); - }); - - describe('#getUnapprovedMsgs', () => { - it('returns unapproved Msgs in a hash', () => { - messageManager.addMsg({ - id: '1', - status: 'unapproved', - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getUnapprovedMsgs(); - expect(typeof result).toStrictEqual('object'); - expect(result['1'].status).toStrictEqual('unapproved'); - expect(result['2']).toBeUndefined(); - }); - }); - - describe('#getMsg', () => { - it('returns a Msg with the requested id', () => { - messageManager.addMsg({ - id: '1', - status: 'unapproved', - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - expect(messageManager.getMsg('1').status).toStrictEqual('unapproved'); - expect(messageManager.getMsg('2').status).toStrictEqual( - TransactionStatus.approved, - ); - }); - }); -}); diff --git a/app/scripts/lib/metaRPCClientFactory.js b/app/scripts/lib/metaRPCClientFactory.js index c09f7e38d..3aae9962d 100644 --- a/app/scripts/lib/metaRPCClientFactory.js +++ b/app/scripts/lib/metaRPCClientFactory.js @@ -1,5 +1,5 @@ import { EthereumRpcError } from 'eth-rpc-errors'; -import SafeEventEmitter from 'safe-event-emitter'; +import SafeEventEmitter from '@metamask/safe-event-emitter'; import createRandomId from '../../../shared/modules/random-id'; import { TEN_SECONDS_IN_MILLISECONDS } from '../../../shared/lib/transactions-controller-utils'; diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 6ee4e3c12..2e4d45ee2 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -1,4 +1,4 @@ -import EventEmitter from 'safe-event-emitter'; +import EventEmitter from '@metamask/safe-event-emitter'; import ExtensionPlatform from '../platforms/extension'; const NOTIFICATION_HEIGHT = 620; diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js deleted file mode 100644 index bb0a2b0c1..000000000 --- a/app/scripts/lib/personal-message-manager.js +++ /dev/null @@ -1,370 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex } from 'ethereumjs-util'; -import { ethErrors } from 'eth-rpc-errors'; -import log from 'loglevel'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; -import { detectSIWE } from '../../../shared/modules/siwe'; -import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; -import { addHexPrefix } from './util'; - -const hexRe = /^[0-9A-Fa-f]+$/gu; - -/** - * Represents, and contains data about, an 'personal_sign' type signature request. These are created when a - * signature for an personal_sign call is requested. - * - * @see {@link https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html#sign} - * @typedef {object} PersonalMessage - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the personal_sign method once the signature request is - * approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will - * always have a 'personal_sign' type. - */ - -export default class PersonalMessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - PersonalMessage. - * - * @param options - * @param options.metricsEvent - * @param options.securityProviderRequest - */ - constructor({ metricsEvent, securityProviderRequest }) { - super(); - this.memStore = new ObservableStore({ - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' PersonalMessages in this.messages - * - * @returns {number} The number of 'unapproved' PersonalMessages in this.messages - */ - get unapprovedPersonalMsgCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' PersonalMessages in this.messages - * - * @returns {object} An index of PersonalMessage ids to PersonalMessages, for all 'unapproved' PersonalMessages in - * this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to - * this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {promise} When the message has been signed or rejected - */ - addUnapprovedMessageAsync(msgParams, req) { - return new Promise((resolve, reject) => { - if (!msgParams.from) { - reject( - new Error('MetaMask Message Signature: from field is required.'), - ); - return; - } - this.addUnapprovedMessage(msgParams, req).then((msgId) => { - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - resolve(data.rawSig); - return; - case 'rejected': - reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - return; - case 'errored': - reject(new Error(`MetaMask Message Signature: ${data.error}`)); - return; - default: - reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - }); - } - - /** - * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to - * this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {number} The id of the newly created PersonalMessage. - */ - async addUnapprovedMessage(msgParams, req) { - log.debug( - `PersonalMessageManager addUnapprovedMessage: ${JSON.stringify( - msgParams, - )}`, - ); - // add origin from request - if (req) { - msgParams.origin = req.origin; - } - msgParams.data = this.normalizeMsgData(msgParams.data); - - // check for SIWE message - const siwe = detectSIWE(msgParams); - msgParams.siwe = siwe; - - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.PERSONAL_SIGN, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Adds a passed PersonalMessage to this.messages, and calls this._saveMsgList() to save the unapproved PersonalMessages from that - * list to this.memStore. - * - * @param {Message} msg - The PersonalMessage to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified PersonalMessage. - * - * @param {number} msgId - The id of the PersonalMessage to get - * @returns {PersonalMessage|undefined} The PersonalMessage with the id that matches the passed msgId, or undefined - * if no PersonalMessage has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a PersonalMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise - * with any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a PersonalMessage status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the PersonalMessage to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a PersonalMessage status to 'signed' via a call to this._setMsgStatus and updates that PersonalMessage in - * this.messages by adding the raw signature data of the signature request to the PersonalMessage - * - * @param {number} msgId - The id of the PersonalMessage to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a PersonalMessage status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the PersonalMessage to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a Message status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * Updates the status of a PersonalMessage in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the PersonalMessage to update. - * @param {string} status - The new status of the PersonalMessage. - * @throws A 'PersonalMessageManager - PersonalMessage not found for id: "${msgId}".' if there is no PersonalMessage - * in this.messages with an id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The PersonalMessage is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along - * with the PersonalMessage - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error( - `PersonalMessageManager - Message not found for id: "${msgId}".`, - ); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a PersonalMessage in this.messages to the passed PersonalMessage if the ids are equal. Then saves the - * unapprovedPersonalMsgs index to storage via this._saveMsgList - * - * @private - * @param {PersonalMessage} msg - A PersonalMessage that will replace an existing PersonalMessage (with the same - * id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved PersonalMessages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedPersonalMsgs = this.getUnapprovedMsgs(); - const unapprovedPersonalMsgCount = Object.keys( - unapprovedPersonalMsgs, - ).length; - this.memStore.updateState({ - unapprovedPersonalMsgs, - unapprovedPersonalMsgCount, - }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } - - /** - * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. - * - * @param {any} data - The buffer data to convert to a hex - * @returns {string} A hex string conversion of the buffer data - */ - normalizeMsgData(data) { - try { - const stripped = stripHexPrefix(data); - if (stripped.match(hexRe)) { - return addHexPrefix(stripped); - } - } catch (e) { - log.debug(`Message was not hex encoded, interpreting as utf8.`); - } - - return bufferToHex(Buffer.from(data, 'utf8')); - } -} diff --git a/app/scripts/lib/personal-message-manager.test.js b/app/scripts/lib/personal-message-manager.test.js deleted file mode 100644 index e565c0229..000000000 --- a/app/scripts/lib/personal-message-manager.test.js +++ /dev/null @@ -1,182 +0,0 @@ -import { TransactionStatus } from '../../../shared/constants/transaction'; -import PersonalMessageManager from './personal-message-manager'; - -describe('Personal Message Manager', () => { - let messageManager; - - beforeEach(() => { - messageManager = new PersonalMessageManager({ - metricsEvent: jest.fn(), - securityProviderRequest: jest.fn(), - }); - }); - - describe('#getMsgList', () => { - it('when new should return empty array', () => { - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(0); - }); - }); - - describe('#addMsg', () => { - it('adds a Msg returned in getMsgList', () => { - const Msg = { - id: 1, - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].id).toStrictEqual(1); - }); - }); - - describe('#setMsgStatusApproved', () => { - it('sets the Msg status to approved', () => { - const Msg = { - id: 1, - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.setMsgStatusApproved(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.approved); - }); - }); - - describe('#rejectMsg', () => { - it('sets the Msg status to rejected', () => { - const Msg = { - id: 1, - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.rejectMsg(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.rejected); - }); - }); - - describe('#_updateMsg', () => { - it('replaces the Msg with the same id', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - messageManager._updateMsg({ - id: '1', - status: 'blah', - hash: 'foo', - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getMsg('1'); - expect(result.hash).toStrictEqual('foo'); - }); - }); - - describe('#getUnapprovedMsgs', () => { - it('returns unapproved Msgs in a hash', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getUnapprovedMsgs(); - expect(typeof result).toStrictEqual('object'); - expect(result['1'].status).toStrictEqual(TransactionStatus.unapproved); - expect(result['2']).toBeUndefined(); - }); - }); - - describe('#getMsg', () => { - it('returns a Msg with the requested id', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - expect(messageManager.getMsg('1').status).toStrictEqual( - TransactionStatus.unapproved, - ); - expect(messageManager.getMsg('2').status).toStrictEqual( - TransactionStatus.approved, - ); - }); - }); - - describe('#normalizeMsgData', () => { - it('converts text to a utf8 hex string', () => { - const input = 'hello'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x68656c6c6f'); - }); - - it('tolerates a hex prefix', () => { - const input = '0x12'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x12'); - }); - - it('tolerates normal hex', () => { - const input = '12'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x12'); - }); - }); - - describe('#addUnapprovedMessage', () => { - const origin = 'http://localhost:8080'; - const from = '0xFb2C15004343904e5f4082578c4e8e11105cF7e3'; - const msgParams = { - from, - data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', - }; - - it('should detect SIWE messages', async () => { - const request = { origin }; - const nonSiweMsgParams = { - from, - data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', - }; - // siwe message - const msgId = await messageManager.addUnapprovedMessage( - msgParams, - request, - ); - const result = messageManager.getMsg(msgId); - expect(result.msgParams.siwe.isSIWEMessage).toStrictEqual(true); - // non-siwe message - const msgId2 = await messageManager.addUnapprovedMessage( - nonSiweMsgParams, - request, - ); - const result2 = messageManager.getMsg(msgId2); - expect(result2.msgParams.siwe.isSIWEMessage).toStrictEqual(false); - }); - }); -}); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index b35fe1e5c..f63412b61 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -9,7 +9,7 @@ import { isPrefixedFormattedHexString, isSafeChainId, } from '../../../../../shared/modules/network.utils'; -import { EVENT } from '../../../../../shared/constants/metametrics'; +import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics'; const addEthereumChain = { methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], @@ -262,7 +262,7 @@ async function addEthereumChainHandler( rpcUrl: firstValidRPCUrl, ticker, }, - { source: EVENT.SOURCE.NETWORK.DAPP, referrer: origin }, + { source: MetaMetricsNetworkEventSource.Dapp, referrer: origin }, ); // Once the network has been added, the requested is considered successful diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js index b829e16fa..5ea476a4f 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js @@ -1,5 +1,5 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; -import { EVENT } from '../../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics'; /** * This RPC method is called by the inpage provider whenever it detects the @@ -49,7 +49,7 @@ function logWeb3ShimUsageHandler( sendMetrics( { event: `Website Accessed window.web3 Shim`, - category: EVENT.CATEGORIES.INPAGE_PROVIDER, + category: MetaMetricsEventCategory.InpageProvider, referrer: { url: origin, }, diff --git a/app/scripts/lib/seed-phrase-verifier.js b/app/scripts/lib/seed-phrase-verifier.js index 5fcaf4481..28c0b856d 100644 --- a/app/scripts/lib/seed-phrase-verifier.js +++ b/app/scripts/lib/seed-phrase-verifier.js @@ -1,7 +1,7 @@ import { KeyringController } from '@metamask/eth-keyring-controller'; import log from 'loglevel'; -import { HardwareKeyringTypes } from '../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../shared/constants/keyring'; const seedPhraseVerifier = { /** @@ -23,7 +23,7 @@ const seedPhraseVerifier = { const keyringController = new KeyringController({}); const keyringBuilder = keyringController.getKeyringBuilderForType( - HardwareKeyringTypes.hdKeyTree, + KeyringType.hdKeyTree, ); const keyring = keyringBuilder(); const opts = { diff --git a/app/scripts/lib/seed-phrase-verifier.test.js b/app/scripts/lib/seed-phrase-verifier.test.js index bf888f79b..be549034c 100644 --- a/app/scripts/lib/seed-phrase-verifier.test.js +++ b/app/scripts/lib/seed-phrase-verifier.test.js @@ -6,13 +6,13 @@ import { cloneDeep } from 'lodash'; import { KeyringController } from '@metamask/eth-keyring-controller'; import firstTimeState from '../first-time-state'; import mockEncryptor from '../../../test/lib/mock-encryptor'; -import { HardwareKeyringTypes } from '../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../shared/constants/keyring'; import seedPhraseVerifier from './seed-phrase-verifier'; describe('SeedPhraseVerifier', () => { describe('verifyAccounts', () => { const password = 'passw0rd1'; - const { hdKeyTree } = HardwareKeyringTypes; + const { hdKeyTree } = KeyringType; let keyringController; let primaryKeyring; diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 633ac770d..bc0150b84 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -52,7 +52,8 @@ export const SENTRY_STATE = { isUnlocked: true, metaMetricsId: true, nativeCurrency: true, - network: true, + networkId: true, + networkStatus: true, nextNonce: true, participateInMetaMetrics: true, preferences: true, diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js deleted file mode 100644 index 231aeb470..000000000 --- a/app/scripts/lib/typed-message-manager.js +++ /dev/null @@ -1,421 +0,0 @@ -import EventEmitter from 'events'; -import { strict as assert } from 'assert'; -import { ObservableStore } from '@metamask/obs-store'; -import { ethErrors } from 'eth-rpc-errors'; -import { typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util'; -import log from 'loglevel'; -import jsonschema from 'jsonschema'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; -import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; - -/** - * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a - * signature for an eth_signTypedData call is requested. - * - * @typedef {object} TypedMessage - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the eth_signTypedData method once the signature request is - * approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {object} msgParams.from The address that is making the signature request. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will - * always have a 'eth_signTypedData' type. - */ - -export default class TypedMessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - TypedMessage. - * - * @param options - * @param options.getCurrentChainId - * @param options.metricsEvent - * @param options.securityProviderRequest - */ - constructor({ getCurrentChainId, metricsEvent, securityProviderRequest }) { - super(); - this._getCurrentChainId = getCurrentChainId; - this.memStore = new ObservableStore({ - unapprovedTypedMessages: {}, - unapprovedTypedMessagesCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedTypedMessages: {}, - unapprovedTypedMessagesCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' TypedMessages in this.messages - * - * @returns {number} The number of 'unapproved' TypedMessages in this.messages - */ - get unapprovedTypedMessagesCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' TypedMessages in this.messages - * - * @returns {object} An index of TypedMessage ids to TypedMessages, for all 'unapproved' TypedMessages in - * this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to - * this.memStore. Before any of this is done, msgParams are validated - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @param version - * @returns {promise} When the message has been signed or rejected - */ - async addUnapprovedMessageAsync(msgParams, req, version) { - return new Promise((resolve, reject) => { - this.addUnapprovedMessage(msgParams, req, version).then((msgId) => { - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return resolve(data.rawSig); - case 'rejected': - return reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - case 'errored': - return reject( - new Error(`MetaMask Message Signature: ${data.error}`), - ); - default: - return reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - }); - } - - /** - * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to - * this.memStore. Before any of this is done, msgParams are validated - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @param version - * @returns {number} The id of the newly created TypedMessage. - */ - async addUnapprovedMessage(msgParams, req, version) { - msgParams.version = version; - if (req) { - msgParams.origin = req.origin; - } - this.validateParams(msgParams); - - log.debug( - `TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`, - ); - - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Helper method for this.addUnapprovedMessage. Validates that the passed params have the required properties. - * - * @param {object} params - The params to validate - */ - validateParams(params) { - assert.ok( - params && typeof params === 'object', - 'Params must be an object.', - ); - assert.ok('data' in params, 'Params must include a "data" field.'); - assert.ok('from' in params, 'Params must include a "from" field.'); - assert.ok( - typeof params.from === 'string' && - isValidHexAddress(params.from, { allowNonPrefixed: false }), - '"from" field must be a valid, lowercase, hexadecimal Ethereum address string.', - ); - - switch (params.version) { - case 'V1': - assert.ok( - Array.isArray(params.data), - '"params.data" must be an array.', - ); - assert.doesNotThrow(() => { - typedSignatureHash(params.data); - }, 'Signing data must be valid EIP-712 typed data.'); - break; - case 'V3': - case 'V4': { - assert.equal( - typeof params.data, - 'string', - '"params.data" must be a string.', - ); - let data; - assert.doesNotThrow(() => { - data = JSON.parse(params.data); - }, '"data" must be a valid JSON string.'); - const validation = jsonschema.validate(data, TYPED_MESSAGE_SCHEMA); - if (validation.errors.length !== 0) { - throw ethErrors.rpc.invalidParams({ - message: - 'Signing data must conform to EIP-712 schema. See https://git.io/fNtcx.', - data: validation.errors.map((v) => v.message.toString()), - }); - } - assert.ok( - data.primaryType in data.types, - `Primary type of "${data.primaryType}" has no type definition.`, - ); - let { chainId } = data.domain; - if (chainId) { - const activeChainId = parseInt(this._getCurrentChainId(), 16); - assert.ok( - !Number.isNaN(activeChainId), - `Cannot sign messages for chainId "${chainId}", because MetaMask is switching networks.`, - ); - if (typeof chainId === 'string') { - chainId = parseInt(chainId, chainId.startsWith('0x') ? 16 : 10); - } - assert.equal( - chainId, - activeChainId, - `Provided chainId "${chainId}" must match the active chainId "${activeChainId}"`, - ); - } - break; - } - default: - assert.fail(`Unknown typed data version "${params.version}"`); - } - } - - /** - * Adds a passed TypedMessage to this.messages, and calls this._saveMsgList() to save the unapproved TypedMessages from that - * list to this.memStore. - * - * @param {Message} msg - The TypedMessage to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified TypedMessage. - * - * @param {number} msgId - The id of the TypedMessage to get - * @returns {TypedMessage|undefined} The TypedMessage with the id that matches the passed msgId, or undefined - * if no TypedMessage has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a TypedMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise - * with any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a TypedMessage status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a TypedMessage status to 'signed' via a call to this._setMsgStatus and updates that TypedMessage in - * this.messages by adding the raw signature data of the signature request to the TypedMessage - * - * @param {number} msgId - The id of the TypedMessage to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - delete msgParams.version; - return msgParams; - } - - /** - * Sets a TypedMessage status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - version: msg.msgParams.version, - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - // - // PRIVATE METHODS - // - - /** - * Updates the status of a TypedMessage in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the TypedMessage to update. - * @param {string} status - The new status of the TypedMessage. - * @throws A 'TypedMessageManager - TypedMessage not found for id: "${msgId}".' if there is no TypedMessage - * in this.messages with an id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The TypedMessage is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along - * with the TypedMessage - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error( - `TypedMessageManager - Message not found for id: "${msgId}".`, - ); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed' || status === 'errored') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a TypedMessage in this.messages to the passed TypedMessage if the ids are equal. Then saves the - * unapprovedTypedMsgs index to storage via this._saveMsgList - * - * @private - * @param {TypedMessage} msg - A TypedMessage that will replace an existing TypedMessage (with the same - * id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved TypedMessages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedTypedMessages = this.getUnapprovedMsgs(); - const unapprovedTypedMessagesCount = Object.keys( - unapprovedTypedMessages, - ).length; - this.memStore.updateState({ - unapprovedTypedMessages, - unapprovedTypedMessagesCount, - }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } -} diff --git a/app/scripts/lib/typed-message-manager.test.js b/app/scripts/lib/typed-message-manager.test.js deleted file mode 100644 index b20b8ab09..000000000 --- a/app/scripts/lib/typed-message-manager.test.js +++ /dev/null @@ -1,126 +0,0 @@ -import sinon from 'sinon'; -import { TransactionStatus } from '../../../shared/constants/transaction'; -import TypedMessageManager from './typed-message-manager'; - -describe('Typed Message Manager', () => { - let typedMessageManager, - msgParamsV1, - msgParamsV3, - typedMsgs, - messages, - msgId, - numberMsgId; - - const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - - beforeEach(async () => { - typedMessageManager = new TypedMessageManager({ - getCurrentChainId: sinon.fake.returns('0x1'), - metricsEvent: sinon.fake(), - securityProviderRequest: sinon.fake(), - }); - - msgParamsV1 = { - from: address, - data: [ - { type: 'string', name: 'unit test', value: 'hello there' }, - { - type: 'uint32', - name: 'A number, but not really a number', - value: '$$$', - }, - ], - }; - - msgParamsV3 = { - from: address, - data: JSON.stringify({ - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - Person: [ - { name: 'name', type: 'string' }, - { name: 'wallet', type: 'address' }, - ], - Mail: [ - { name: 'from', type: 'Person' }, - { name: 'to', type: 'Person' }, - { name: 'contents', type: 'string' }, - ], - }, - primaryType: 'Mail', - domain: { - name: 'Ether Mainl', - version: '1', - chainId: 1, - verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', - }, - message: { - from: { - name: 'Cow', - wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', - }, - to: { - name: 'Bob', - wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', - }, - contents: 'Hello, Bob!', - }, - }), - }; - - await typedMessageManager.addUnapprovedMessage(msgParamsV3, null, 'V3'); - typedMsgs = typedMessageManager.getUnapprovedMsgs(); - messages = typedMessageManager.messages; - msgId = Object.keys(typedMsgs)[0]; - messages[0].msgParams.metamaskId = parseInt(msgId, 10); - numberMsgId = parseInt(msgId, 10); - }); - - it('supports version 1 of signedTypedData', async () => { - await typedMessageManager.addUnapprovedMessage(msgParamsV1, null, 'V1'); - expect(messages[messages.length - 1].msgParams.data).toStrictEqual( - msgParamsV1.data, - ); - }); - - it('has params address', () => { - expect(typedMsgs[msgId].msgParams.from).toStrictEqual(address); - }); - - it('adds to unapproved messages and sets status to unapproved', () => { - expect(typedMsgs[msgId].status).toStrictEqual(TransactionStatus.unapproved); - }); - - it('validates params', async () => { - await expect(() => { - typedMessageManager.validateParams(messages[0].msgParams); - }).not.toThrow(); - }); - - it('gets unapproved by id', () => { - const getMsg = typedMessageManager.getMsg(numberMsgId); - expect(getMsg.id).toStrictEqual(numberMsgId); - }); - - it('approves messages', async () => { - const messageMetaMaskId = messages[0].msgParams; - typedMessageManager.approveMessage(messageMetaMaskId); - expect(messages[0].status).toStrictEqual(TransactionStatus.approved); - }); - - it('sets msg status to signed and adds a raw sig to message details', () => { - typedMessageManager.setMsgStatusSigned(numberMsgId, 'raw sig'); - expect(messages[0].status).toStrictEqual(TransactionStatus.signed); - expect(messages[0].rawSig).toStrictEqual('raw sig'); - }); - - it('rejects message', () => { - typedMessageManager.rejectMsg(numberMsgId); - expect(messages[0].status).toStrictEqual(TransactionStatus.rejected); - }); -}); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bd1fe8300..7b46e25cd 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -12,11 +12,7 @@ import { } from '@metamask/eth-keyring-controller'; import createFilterMiddleware from 'eth-json-rpc-filters'; import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager'; -import { - errorCodes as rpcErrorCodes, - EthereumRpcError, - ethErrors, -} from 'eth-rpc-errors'; +import { errorCodes as rpcErrorCodes, EthereumRpcError } from 'eth-rpc-errors'; import { Mutex } from 'await-semaphore'; import log from 'loglevel'; import TrezorKeyring from 'eth-trezor-keyring'; @@ -60,12 +56,12 @@ import SmartTransactionsController from '@metamask/smart-transactions-controller ///: BEGIN:ONLY_INCLUDE_IN(flask) import { CronjobController, + JsonSnapsRegistry, SnapController, IframeExecutionService, } from '@metamask/snaps-controllers'; ///: END:ONLY_INCLUDE_IN -import browser from 'webextension-polyfill'; import { AssetType, TransactionStatus, @@ -77,11 +73,13 @@ import { GAS_DEV_API_BASE_URL, SWAPS_CLIENT_ID, } from '../../shared/constants/swaps'; -import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; import { - HardwareDeviceNames, - HardwareKeyringTypes, -} from '../../shared/constants/hardware-wallets'; + CHAIN_IDS, + NETWORK_TYPES, + NetworkStatus, +} from '../../shared/constants/network'; +import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../shared/constants/keyring'; import { CaveatTypes, RestrictedMethods, @@ -92,20 +90,20 @@ import { ///: END:ONLY_INCLUDE_IN } from '../../shared/constants/permissions'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; -import { - toChecksumHexAddress, - stripHexPrefix, -} from '../../shared/modules/hexstring-utils'; +import { stripHexPrefix } from '../../shared/modules/hexstring-utils'; import { MILLISECOND, SECOND } from '../../shared/constants/time'; import { ORIGIN_METAMASK, - ///: BEGIN:ONLY_INCLUDE_IN(flask) MESSAGE_TYPE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) SNAP_DIALOG_TYPES, ///: END:ONLY_INCLUDE_IN POLLING_TOKEN_ENVIRONMENT_TYPES, } from '../../shared/constants/app'; -import { EVENT, EVENT_NAMES } from '../../shared/constants/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../shared/constants/metametrics'; import { getTokenIdParam, @@ -122,6 +120,7 @@ import { isMain, isFlask } from '../../shared/constants/environment'; // eslint-disable-next-line import/order import { DesktopController } from '@metamask/desktop/dist/controllers/desktop'; ///: END:ONLY_INCLUDE_IN +import { ACTION_QUEUE_METRICS_E2E_TEST } from '../../shared/constants/test-flags'; import { onMessageReceived, checkForMultipleVersionsRunning, @@ -141,7 +140,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { setupMultiplex } from './lib/stream-utils'; import EnsController from './controllers/ens'; -import NetworkController, { NETWORK_EVENTS } from './controllers/network'; +import NetworkController, { + NetworkControllerEventTypes, +} from './controllers/network'; import PreferencesController from './controllers/preferences'; import AppStateController from './controllers/app-state'; import CachedBalancesController from './controllers/cached-balances'; @@ -149,11 +150,8 @@ import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; import BackupController from './controllers/backup'; import IncomingTransactionsController from './controllers/incoming-transactions'; -import MessageManager, { normalizeMsgData } from './lib/message-manager'; import DecryptMessageManager from './lib/decrypt-message-manager'; import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'; -import PersonalMessageManager from './lib/personal-message-manager'; -import TypedMessageManager from './lib/typed-message-manager'; import TransactionController from './controllers/transactions'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; @@ -164,6 +162,7 @@ import { segment } from './lib/segment'; import createMetaRPCHandler from './lib/createMetaRPCHandler'; import { previousValueComparator } from './lib/util'; import createMetamaskMiddleware from './lib/createMetamaskMiddleware'; +import SignController from './controllers/sign'; import { CaveatMutatorFactories, @@ -201,6 +200,8 @@ export default class MetamaskController extends EventEmitter { constructor(opts) { super(); + const { isFirstMetaMaskControllerSetup } = opts; + this.defaultMaxListeners = 20; this.sendUpdate = debounce( @@ -255,9 +256,19 @@ export default class MetamaskController extends EventEmitter { name: 'ApprovalController', }), showApprovalRequest: opts.showUserConfirmation, + typesExcludedFromRateLimiting: [ + MESSAGE_TYPE.ETH_SIGN, + MESSAGE_TYPE.PERSONAL_SIGN, + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, + ], }); + const networkControllerMessenger = this.controllerMessenger.getRestricted({ + name: 'NetworkController', + allowedEvents: Object.values(NetworkControllerEventTypes), + }); this.networkController = new NetworkController({ + messenger: networkControllerMessenger, state: initState.NetworkController, infuraProjectId: opts.infuraProjectId, trackMetaMetricsEvent: (...args) => @@ -299,8 +310,14 @@ export default class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, - openPopup: opts.openPopup, - network: this.networkController, + onInfuraIsBlocked: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + NetworkControllerEventTypes.InfuraIsBlocked, + ), + onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + NetworkControllerEventTypes.InfuraIsUnblocked, + ), tokenListController: this.tokenListController, provider: this.provider, }); @@ -327,18 +344,29 @@ export default class MetamaskController extends EventEmitter { { onPreferencesStateChange: (listener) => this.preferencesController.store.subscribe(listener), + // This handler is misnamed, and is a known issue that will be resolved + // by planned refactors. It should be onNetworkDidChange which happens + // AFTER the provider in the network controller is updated to reflect + // the new state of the network controller. In #18041 we changed this + // handler to be triggered by the change in the network state because + // that is what the handler name implies, but this triggers too soon + // causing the provider of the AssetsContractController to trail the + // network provider by one update. onNetworkStateChange: (cb) => - this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - const networkState = this.networkController.store.getState(); - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.provider, - chainId: hexToDecimal(networkState.provider.chainId), - }, - }; - return cb(modifiedNetworkState); - }), + networkControllerMessenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + () => { + const networkState = this.networkController.store.getState(); + const modifiedNetworkState = { + ...networkState, + providerConfig: { + ...networkState.provider, + chainId: hexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }, + ), }, { provider: this.provider, @@ -387,8 +415,8 @@ export default class MetamaskController extends EventEmitter { ), onNftAdded: ({ address, symbol, tokenId, standard, source }) => this.metaMetricsController.trackEvent({ - event: EVENT_NAMES.NFT_ADDED, - category: EVENT.CATEGORIES.WALLET, + event: MetaMetricsEventName.NftAdded, + category: MetaMetricsEventCategory.Wallet, properties: { token_contract_address: address, token_symbol: symbol, @@ -435,9 +463,9 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsController = new MetaMetricsController({ segment, preferencesStore: this.preferencesController.store, - onNetworkDidChange: this.networkController.on.bind( - this.networkController, - NETWORK_EVENTS.NETWORK_DID_CHANGE, + onNetworkDidChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + NetworkControllerEventTypes.NetworkDidChange, ), getNetworkIdentifier: () => { const { type, rpcUrl } = @@ -472,9 +500,11 @@ export default class MetamaskController extends EventEmitter { clientId: SWAPS_CLIENT_ID, getProvider: () => this.networkController.getProviderAndBlockTracker().provider, - onNetworkStateChange: this.networkController.on.bind( - this.networkController, - NETWORK_EVENTS.NETWORK_DID_CHANGE, + // NOTE: This option is inaccurately named; it should be called + // onNetworkDidChange + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + NetworkControllerEventTypes.NetworkDidChange, ), getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( @@ -586,9 +616,9 @@ export default class MetamaskController extends EventEmitter { provider: this.provider, getCurrentChainId: () => this.networkController.store.getState().provider.chainId, - onNetworkDidChange: this.networkController.on.bind( - this.networkController, - NETWORK_EVENTS.NETWORK_DID_CHANGE, + onNetworkDidChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + NetworkControllerEventTypes.NetworkDidChange, ), }); @@ -598,9 +628,9 @@ export default class MetamaskController extends EventEmitter { this.incomingTransactionsController = new IncomingTransactionsController({ blockTracker: this.blockTracker, - onNetworkDidChange: this.networkController.on.bind( - this.networkController, - NETWORK_EVENTS.NETWORK_DID_CHANGE, + onNetworkDidChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + NetworkControllerEventTypes.NetworkDidChange, ), getCurrentChainId: () => this.networkController.store.getState().provider.chainId, @@ -622,6 +652,12 @@ export default class MetamaskController extends EventEmitter { }, preferencesController: this.preferencesController, onboardingController: this.onboardingController, + initState: + isManifestV3 && + isFirstMetaMaskControllerSetup === false && + initState.AccountTracker?.accounts + ? { accounts: initState.AccountTracker.accounts } + : { accounts: {} }, }); // start and stop polling for balances based on activeControllerConnections @@ -682,6 +718,7 @@ export default class MetamaskController extends EventEmitter { this.keyringController.memStore.subscribe((state) => this._onKeyringControllerUpdate(state), ); + this.keyringController.on('unlock', () => this._onUnlock()); this.keyringController.on('lock', () => this._onLock()); @@ -696,6 +733,8 @@ export default class MetamaskController extends EventEmitter { `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, `${this.approvalController.name}:rejectRequest`, + `SnapController:getPermitted`, + `SnapController:install`, ], }), state: initState.PermissionController, @@ -755,9 +794,7 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IN(flask) const snapExecutionServiceArgs = { - iframeUrl: new URL( - 'https://metamask.github.io/iframe-execution-environment/0.13.0', - ), + iframeUrl: new URL('https://execution.metamask.io/0.15.1/index.html'), messenger: this.controllerMessenger.getRestricted({ name: 'ExecutionService', }), @@ -787,6 +824,7 @@ export default class MetamaskController extends EventEmitter { `${this.permissionController.name}:getSubjectNames`, `${this.permissionController.name}:updateCaveat`, `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:updateRequestState`, `${this.permissionController.name}:grantPermissions`, `${this.subjectMetadataController.name}:getSubjectMetadata`, 'ExecutionService:executeSnap', @@ -794,6 +832,8 @@ export default class MetamaskController extends EventEmitter { 'ExecutionService:terminateSnap', 'ExecutionService:terminateAllSnaps', 'ExecutionService:handleRpcRequest', + 'SnapsRegistry:get', + 'SnapsRegistry:getMetadata', ], }); @@ -852,7 +892,6 @@ export default class MetamaskController extends EventEmitter { }, }, }); - // --- Snaps Cronjob Controller configuration const cronjobControllerMessenger = this.controllerMessenger.getRestricted({ name: 'CronjobController', allowedEvents: [ @@ -871,6 +910,24 @@ export default class MetamaskController extends EventEmitter { messenger: cronjobControllerMessenger, }); + const snapsRegistryMessenger = this.controllerMessenger.getRestricted({ + name: 'SnapsRegistry', + allowedEvents: [], + allowedActions: [], + }); + this.snapsRegistry = new JsonSnapsRegistry({ + state: initState.SnapsRegistry, + messenger: snapsRegistryMessenger, + refetchOnAllowlistMiss: isMain, + failOnUnavailableRegistry: isMain, + url: { + registry: 'https://acl.execution.metamask.io/latest/registry.json', + signature: 'https://acl.execution.metamask.io/latest/signature.json', + }, + publicKey: + '0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6', + }); + this.desktopController = new DesktopController({ initState: initState.DesktopController, }); @@ -918,9 +975,11 @@ export default class MetamaskController extends EventEmitter { ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(this), - getNetworkState: () => this.networkController.store.getState().network, + getNetworkId: () => this.networkController.store.getState().networkId, + getNetworkStatus: () => + this.networkController.store.getState().networkStatus, onNetworkStateChange: (listener) => - this.networkController.networkStore.subscribe(listener), + this.networkController.networkIdStore.subscribe(listener), getCurrentChainId: () => this.networkController.store.getState().provider.chainId, preferencesStore: this.preferencesController.store, @@ -1032,7 +1091,7 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsController.trackEvent( { event: 'Tx Status Update: On-Chain Failure', - category: EVENT.CATEGORIES.BACKGROUND, + category: MetaMetricsEventCategory.Background, properties: { action: 'Transactions', errorMessage: txMeta.simulationFails?.reason, @@ -1048,29 +1107,20 @@ export default class MetamaskController extends EventEmitter { } }); - this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => { - const { ticker } = this.networkController.store.getState().provider; - try { - await this.currencyRateController.setNativeCurrency(ticker); - } catch (error) { - // TODO: Handle failure to get conversion rate more gracefully - console.error(error); - } - }); + networkControllerMessenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + async () => { + const { ticker } = this.networkController.store.getState().provider; + try { + await this.currencyRateController.setNativeCurrency(ticker); + } catch (error) { + // TODO: Handle failure to get conversion rate more gracefully + console.error(error); + } + }, + ); this.networkController.lookupNetwork(); - this.messageManager = new MessageManager({ - metricsEvent: this.metaMetricsController.trackEvent.bind( - this.metaMetricsController, - ), - securityProviderRequest: this.securityProviderRequest.bind(this), - }); - this.personalMessageManager = new PersonalMessageManager({ - metricsEvent: this.metaMetricsController.trackEvent.bind( - this.metaMetricsController, - ), - securityProviderRequest: this.securityProviderRequest.bind(this), - }); this.decryptMessageManager = new DecryptMessageManager({ metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, @@ -1081,12 +1131,19 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsController, ), }); - this.typedMessageManager = new TypedMessageManager({ - getCurrentChainId: () => - this.networkController.store.getState().provider.chainId, - metricsEvent: this.metaMetricsController.trackEvent.bind( - this.metaMetricsController, - ), + + this.signController = new SignController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'SignController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), + keyringController: this.keyringController, + preferencesController: this.preferencesController, + getState: this.getState.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this), }); @@ -1095,6 +1152,8 @@ export default class MetamaskController extends EventEmitter { this.txController.txGasUtil, ), networkController: this.networkController, + onNetworkStateChange: (listener) => + this.networkController.store.subscribe(listener), provider: this.provider, getProviderConfig: () => this.networkController.store.getState().provider, getTokenRatesState: () => this.tokenRatesController.state, @@ -1116,7 +1175,8 @@ export default class MetamaskController extends EventEmitter { return cb(modifiedNetworkState); }); }, - getNetwork: () => this.networkController.store.getState().network, + getNetwork: () => + this.networkController.store.getState().networkId ?? 'loading', getNonceLock: this.txController.nonceTracker.getNonceLock.bind( this.txController.nonceTracker, ), @@ -1134,19 +1194,42 @@ export default class MetamaskController extends EventEmitter { ); // ensure accountTracker updates balances after network change - this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - this.accountTracker._updateAccounts(); - }); + networkControllerMessenger.subscribe( + NetworkControllerEventTypes.NetworkDidChange, + () => { + this.accountTracker._updateAccounts(); + }, + ); // clear unapproved transactions and messages when the network will change - this.networkController.on(NETWORK_EVENTS.NETWORK_WILL_CHANGE, () => { - this.txController.txStateManager.clearUnapprovedTxs(); - this.encryptionPublicKeyManager.clearUnapproved(); - this.personalMessageManager.clearUnapproved(); - this.typedMessageManager.clearUnapproved(); - this.decryptMessageManager.clearUnapproved(); - this.messageManager.clearUnapproved(); - }); + networkControllerMessenger.subscribe( + NetworkControllerEventTypes.NetworkWillChange, + () => { + this.txController.txStateManager.clearUnapprovedTxs(); + this.encryptionPublicKeyManager.clearUnapproved(); + this.decryptMessageManager.clearUnapproved(); + this.signController.clearUnapproved(); + }, + ); + + if (isManifestV3 && globalThis.isFirstTimeProfileLoaded === false) { + const { serviceWorkerLastActiveTime } = + this.appStateController.store.getState(); + const metametricsPayload = { + category: MetaMetricsEventCategory.ServiceWorkers, + event: MetaMetricsEventName.ServiceWorkerRestarted, + properties: { + service_worker_restarted_time: + Date.now() - serviceWorkerLastActiveTime, + }, + }; + + try { + this.metaMetricsController.trackEvent(metametricsPayload); + } catch (e) { + log.warn('Failed to track service worker restart metric:', e); + } + } this.metamaskMiddleware = createMetamaskMiddleware({ static: { @@ -1173,11 +1256,22 @@ export default class MetamaskController extends EventEmitter { // tx signing processTransaction: this.newUnapprovedTransaction.bind(this), // msg signing - processEthSignMessage: this.newUnsignedMessage.bind(this), - processTypedMessage: this.newUnsignedTypedMessage.bind(this), - processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), - processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), - processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), + processEthSignMessage: this.signController.newUnsignedMessage.bind( + this.signController, + ), + processTypedMessage: this.signController.newUnsignedTypedMessage.bind( + this.signController, + ), + processTypedMessageV3: this.signController.newUnsignedTypedMessage.bind( + this.signController, + ), + processTypedMessageV4: this.signController.newUnsignedTypedMessage.bind( + this.signController, + ), + processPersonalMessage: + this.signController.newUnsignedPersonalMessage.bind( + this.signController, + ), processDecryptMessage: this.newRequestDecryptMessage.bind(this), processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), getPendingNonce: this.getPendingNonce.bind(this), @@ -1201,11 +1295,9 @@ export default class MetamaskController extends EventEmitter { AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, TokenRatesController: this.tokenRatesController, - MessageManager: this.messageManager.memStore, - PersonalMessageManager: this.personalMessageManager.memStore, DecryptMessageManager: this.decryptMessageManager.memStore, EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, - TypesMessageManager: this.typedMessageManager.memStore, + SignController: this.signController, SwapsController: this.swapsController.store, EnsController: this.ensController.store, ApprovalController: this.approvalController, @@ -1238,6 +1330,7 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IN(flask) SnapController: this.snapController, CronjobController: this.cronjobController, + SnapsRegistry: this.snapsRegistry, NotificationController: this.notificationController, DesktopController: this.desktopController.store, ///: END:ONLY_INCLUDE_IN @@ -1271,6 +1364,7 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IN(flask) SnapController: this.snapController, CronjobController: this.cronjobController, + SnapsRegistry: this.snapsRegistry, NotificationController: this.notificationController, DesktopController: this.desktopController.store, ///: END:ONLY_INCLUDE_IN @@ -1283,11 +1377,9 @@ export default class MetamaskController extends EventEmitter { const resetMethods = [ this.accountTracker.resetState, this.txController.resetState, - this.messageManager.resetState, - this.personalMessageManager.resetState, this.decryptMessageManager.resetState, this.encryptionPublicKeyManager.resetState, - this.typedMessageManager.resetState, + this.signController.resetState.bind(this.signController), this.swapsController.resetState, this.ensController.resetState, this.approvalController.clear.bind(this.approvalController), @@ -1295,8 +1387,11 @@ export default class MetamaskController extends EventEmitter { ]; if (isManifestV3) { - if (globalThis.isFirstTimeProfileLoaded === true) { + if (isFirstMetaMaskControllerSetup === true) { this.resetStates(resetMethods); + this.extension.storage.session.set({ + isFirstMetaMaskControllerSetup: false, + }); } } else { // it's always the first time in MV2 @@ -1371,8 +1466,6 @@ export default class MetamaskController extends EventEmitter { console.error(err); } }); - - globalThis.isFirstTimeProfileLoaded = false; } ///: BEGIN:ONLY_INCLUDE_IN(flask) @@ -1547,7 +1640,7 @@ export default class MetamaskController extends EventEmitter { (truncatedSnap) => { this.metaMetricsController.trackEvent({ event: 'Snap Installed', - category: EVENT.CATEGORIES.SNAPS, + category: MetaMetricsEventCategory.Snaps, properties: { snap_id: truncatedSnap.id, version: truncatedSnap.version, @@ -1561,7 +1654,7 @@ export default class MetamaskController extends EventEmitter { (newSnap, oldVersion) => { this.metaMetricsController.trackEvent({ event: 'Snap Updated', - category: EVENT.CATEGORIES.SNAPS, + category: MetaMetricsEventCategory.Snaps, properties: { snap_id: newSnap.id, old_version: oldVersion, @@ -1625,16 +1718,16 @@ export default class MetamaskController extends EventEmitter { function updatePublicConfigStore(memState) { const { chainId } = networkController.store.getState().provider; - if (memState.network !== 'loading') { + if (memState.networkStatus === NetworkStatus.Available) { publicConfigStore.putState(selectPublicState(chainId, memState)); } } - function selectPublicState(chainId, { isUnlocked, network }) { + function selectPublicState(chainId, { isUnlocked, networkId }) { return { isUnlocked, chainId, - networkVersion: network, + networkVersion: networkId ?? 'loading', }; } @@ -1645,12 +1738,7 @@ export default class MetamaskController extends EventEmitter { * Gets relevant state for the provider of an external origin. * * @param {string} origin - The origin to get the provider state for. - * @returns {Promise<{ - * isUnlocked: boolean, - * networkVersion: string, - * chainId: string, - * accounts: string[], - * }>} An object with relevant state properties. + * @returns {Promise<{ isUnlocked: boolean, networkVersion: string, chainId: string, accounts: string[] }>} An object with relevant state properties. */ async getProviderState(origin) { return { @@ -1668,10 +1756,10 @@ export default class MetamaskController extends EventEmitter { * @returns {object} An object with relevant network state properties. */ getProviderNetworkState(memState) { - const { network } = memState || this.getState(); + const { networkId } = memState || this.getState(); return { chainId: this.networkController.store.getState().provider.chainId, - networkVersion: network, + networkVersion: networkId ?? 'loading', }; } @@ -1812,9 +1900,6 @@ export default class MetamaskController extends EventEmitter { cancelQRHardwareSignRequest: qrHardwareKeyring.cancelSignRequest.bind(qrHardwareKeyring), - // mobile - fetchInfoToSync: this.fetchInfoToSync.bind(this), - // vault management submitPassword: this.submitPassword.bind(this), verifyPassword: this.verifyPassword.bind(this), @@ -1977,17 +2062,24 @@ export default class MetamaskController extends EventEmitter { updatePreviousGasParams: txController.updatePreviousGasParams.bind(txController), - // messageManager - signMessage: this.signMessage.bind(this), - cancelMessage: this.cancelMessage.bind(this), - // personalMessageManager - signPersonalMessage: this.signPersonalMessage.bind(this), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this), - - // typedMessageManager - signTypedMessage: this.signTypedMessage.bind(this), - cancelTypedMessage: this.cancelTypedMessage.bind(this), + // signController + signMessage: this.signController.signMessage.bind(this.signController), + cancelMessage: this.signController.cancelMessage.bind( + this.signController, + ), + signPersonalMessage: this.signController.signPersonalMessage.bind( + this.signController, + ), + cancelPersonalMessage: this.signController.cancelPersonalMessage.bind( + this.signController, + ), + signTypedMessage: this.signController.signTypedMessage.bind( + this.signController, + ), + cancelTypedMessage: this.signController.cancelTypedMessage.bind( + this.signController, + ), // decryptMessageManager decryptMessage: this.decryptMessage.bind(this), @@ -2373,7 +2465,7 @@ export default class MetamaskController extends EventEmitter { ); const [primaryKeyring] = keyringController.getKeyringsByType( - HardwareKeyringTypes.hdKeyTree, + KeyringType.hdKeyTree, ); if (!primaryKeyring) { throw new Error('MetamaskController - No HD Key Tree found'); @@ -2437,110 +2529,6 @@ export default class MetamaskController extends EventEmitter { }); } - /** - * Collects all the information that we want to share - * with the mobile client for syncing purposes - * - * @returns {Promise} Parts of the state that we want to syncx - */ - async fetchInfoToSync() { - // Preferences - const { currentLocale, identities, selectedAddress, useTokenDetection } = - this.preferencesController.store.getState(); - - const isTokenDetectionInactiveInMainnet = - !useTokenDetection && - this.networkController.store.getState().provider.chainId === - CHAIN_IDS.MAINNET; - - const { networkConfigurations } = this.networkController.store.getState(); - - const { tokenList } = this.tokenListController.state; - const caseInSensitiveTokenList = isTokenDetectionInactiveInMainnet - ? STATIC_MAINNET_TOKEN_LIST - : tokenList; - - const preferences = { - currentLocale, - identities, - selectedAddress, - }; - - // Tokens - const { allTokens, allIgnoredTokens } = this.tokensController.state; - - // Filter ERC20 tokens - const allERC20Tokens = {}; - - Object.keys(allTokens).forEach((chainId) => { - allERC20Tokens[chainId] = {}; - Object.keys(allTokens[chainId]).forEach((accountAddress) => { - const checksummedAccountAddress = toChecksumHexAddress(accountAddress); - allERC20Tokens[chainId][checksummedAccountAddress] = allTokens[chainId][ - checksummedAccountAddress - ].filter((asset) => { - if (asset.isERC721 === undefined) { - // the tokenList will be holding only erc20 tokens - if ( - caseInSensitiveTokenList[asset.address?.toLowerCase()] !== - undefined - ) { - return true; - } - } else if (asset.isERC721 === false) { - return true; - } - return false; - }); - }); - }); - - // Accounts - const [hdKeyring] = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.hdKeyTree, - ); - const simpleKeyPairKeyrings = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.hdKeyTree, - ); - const hdAccounts = await hdKeyring.getAccounts(); - const simpleKeyPairKeyringAccounts = await Promise.all( - simpleKeyPairKeyrings.map((keyring) => keyring.getAccounts()), - ); - const simpleKeyPairAccounts = simpleKeyPairKeyringAccounts.reduce( - (acc, accounts) => [...acc, ...accounts], - [], - ); - const accounts = { - hd: hdAccounts - .filter((item, pos) => hdAccounts.indexOf(item) === pos) - .map((address) => toChecksumHexAddress(address)), - simpleKeyPair: simpleKeyPairAccounts - .filter((item, pos) => simpleKeyPairAccounts.indexOf(item) === pos) - .map((address) => toChecksumHexAddress(address)), - ledger: [], - trezor: [], - lattice: [], - }; - - // transactions - - let { transactions } = this.txController.store.getState(); - // delete tx for other accounts that we're not importing - transactions = Object.values(transactions).filter((tx) => { - const checksummedTxFrom = toChecksumHexAddress(tx.txParams.from); - return accounts.hd.includes(checksummedTxFrom); - }); - - return { - accounts, - preferences, - transactions, - tokens: { allTokens: allERC20Tokens, allIgnoredTokens }, - network: this.networkController.store.getState(), - networkConfigurations, - }; - } - /** * Submits the user's password and attempts to unlock the vault. * Also synchronizes the preferencesController, to ensure its schema @@ -2574,7 +2562,7 @@ export default class MetamaskController extends EventEmitter { try { // Automatic login via config password const password = process.env.CONF?.PASSWORD; - if (password) { + if (password && !process.env.IN_TEST) { await this.submitPassword(password); } // Automatic login via storage encryption key @@ -2603,10 +2591,8 @@ export default class MetamaskController extends EventEmitter { */ async submitEncryptionKey() { try { - const { loginToken, loginSalt } = await browser.storage.session.get([ - 'loginToken', - 'loginSalt', - ]); + const { loginToken, loginSalt } = + await this.extension.storage.session.get(['loginToken', 'loginSalt']); if (loginToken && loginSalt) { const { vault } = this.keyringController.store.getState(); @@ -2631,7 +2617,7 @@ export default class MetamaskController extends EventEmitter { } async clearLoginArtifacts() { - await browser.storage.session.remove(['loginToken', 'loginSalt']); + await this.extension.storage.session.remove(['loginToken', 'loginSalt']); } /** @@ -2664,7 +2650,7 @@ export default class MetamaskController extends EventEmitter { */ getPrimaryKeyringMnemonic() { const [keyring] = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.hdKeyTree, + KeyringType.hdKeyTree, ); if (!keyring.mnemonic) { throw new Error('Primary keyring mnemonic unavailable.'); @@ -2804,12 +2790,12 @@ export default class MetamaskController extends EventEmitter { async getAccountType(address) { const keyring = await this.keyringController.getKeyringForAccount(address); switch (keyring.type) { - case HardwareKeyringTypes.trezor: - case HardwareKeyringTypes.lattice: - case HardwareKeyringTypes.qr: - case HardwareKeyringTypes.ledger: + case KeyringType.trezor: + case KeyringType.lattice: + case KeyringType.qr: + case KeyringType.ledger: return 'hardware'; - case HardwareKeyringTypes.imported: + case KeyringType.imported: return 'imported'; default: return 'MetaMask'; @@ -2827,14 +2813,14 @@ export default class MetamaskController extends EventEmitter { async getDeviceModel(address) { const keyring = await this.keyringController.getKeyringForAccount(address); switch (keyring.type) { - case HardwareKeyringTypes.trezor: + case KeyringType.trezor: return keyring.getModel(); - case HardwareKeyringTypes.qr: + case KeyringType.qr: return keyring.getName(); - case HardwareKeyringTypes.ledger: + case KeyringType.ledger: // TODO: get model after ledger keyring exposes method return HardwareDeviceNames.ledger; - case HardwareKeyringTypes.lattice: + case KeyringType.lattice: // TODO: get model after lattice keyring exposes method return HardwareDeviceNames.lattice; default: @@ -2907,8 +2893,15 @@ export default class MetamaskController extends EventEmitter { * @returns {} keyState */ async addNewAccount(accountCount) { + const isActionMetricsQueueE2ETest = + this.appStateController.store.getState()[ACTION_QUEUE_METRICS_E2E_TEST]; + + if (process.env.IN_TEST && isActionMetricsQueueE2ETest) { + await new Promise((resolve) => setTimeout(resolve, 5_000)); + } + const [primaryKeyring] = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.hdKeyTree, + KeyringType.hdKeyTree, ); if (!primaryKeyring) { throw new Error('MetamaskController - No HD Key Tree found'); @@ -2953,7 +2946,7 @@ export default class MetamaskController extends EventEmitter { */ async verifySeedPhrase() { const [primaryKeyring] = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.hdKeyTree, + KeyringType.hdKeyTree, ); if (!primaryKeyring) { throw new Error('MetamaskController - No HD Key Tree found'); @@ -3075,7 +3068,7 @@ export default class MetamaskController extends EventEmitter { async importAccountWithStrategy(strategy, args) { const privateKey = await accountImporter.importAccount(strategy, args); const keyring = await this.keyringController.addNewKeyring( - HardwareKeyringTypes.imported, + KeyringType.imported, [privateKey], ); const [firstAccount] = await keyring.getAccounts(); @@ -3101,46 +3094,6 @@ export default class MetamaskController extends EventEmitter { return await this.txController.newUnapprovedTransaction(txParams, req); } - // eth_sign methods: - - /** - * Called when a Dapp uses the eth_sign method, to request user approval. - * eth_sign is a pure signature of arbitrary data. It is on a deprecation - * path, since this data can be a transaction, or can leak private key - * information. - * - * @param {object} msgParams - The params passed to eth_sign. - * @param {object} [req] - The original request, containing the origin. - */ - async newUnsignedMessage(msgParams, req) { - const { disabledRpcMethodPreferences } = - this.preferencesController.store.getState(); - const { eth_sign } = disabledRpcMethodPreferences; // eslint-disable-line camelcase - const data = normalizeMsgData(msgParams.data); - let promise; - - // eslint-disable-next-line camelcase - if (!eth_sign) { - throw ethErrors.rpc.methodNotFound( - 'eth_sign has been disabled. You must enable it in the advanced settings', - ); - } - - // 64 hex + "0x" at the beginning - // This is needed because Ethereum's EcSign works only on 32 byte numbers - // For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607 - if (data.length === 66 || data.length === 67) { - promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req); - this.sendUpdate(); - this.opts.showUserConfirmation(); - } else { - throw ethErrors.rpc.invalidParams( - 'eth_sign requires 32 byte message hash', - ); - } - return await promise; - } - ///: BEGIN:ONLY_INCLUDE_IN(flask) /** * Gets an "app key" corresponding to an Ethereum address. An app key is more @@ -3167,105 +3120,6 @@ export default class MetamaskController extends EventEmitter { } ///: END:ONLY_INCLUDE_IN - /** - * Signifies user intent to complete an eth_sign method. - * - * @param {object} msgParams - The params passed to eth_call. - * @returns {Promise} Full state update. - */ - async signMessage(msgParams) { - log.info('MetaMaskController - signMessage'); - const msgId = msgParams.metamaskId; - try { - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - const cleanMsgParams = await this.messageManager.approveMessage( - msgParams, - ); - const rawSig = await this.keyringController.signMessage(cleanMsgParams); - this.messageManager.setMsgStatusSigned(msgId, rawSig); - return this.getState(); - } catch (error) { - log.info('MetaMaskController - eth_sign failed', error); - this.messageManager.errorMessage(msgId, error); - throw error; - } - } - - /** - * Used to cancel a message submitted via eth_sign. - * - * @param {string} msgId - The id of the message to cancel. - */ - cancelMessage(msgId) { - const { messageManager } = this; - messageManager.rejectMsg(msgId); - return this.getState(); - } - - // personal_sign methods: - - /** - * Called when a dapp uses the personal_sign method. - * This is identical to the Geth eth_sign method, and may eventually replace - * eth_sign. - * - * We currently define our eth_sign and personal_sign mostly for legacy Dapps. - * - * @param {object} msgParams - The params of the message to sign & return to the Dapp. - * @param {object} [req] - The original request, containing the origin. - */ - async newUnsignedPersonalMessage(msgParams, req) { - const promise = this.personalMessageManager.addUnapprovedMessageAsync( - msgParams, - req, - ); - this.sendUpdate(); - this.opts.showUserConfirmation(); - return promise; - } - - /** - * Signifies a user's approval to sign a personal_sign message in queue. - * Triggers signing, and the callback function from newUnsignedPersonalMessage. - * - * @param {object} msgParams - The params of the message to sign & return to the Dapp. - * @returns {Promise} A full state update. - */ - async signPersonalMessage(msgParams) { - log.info('MetaMaskController - signPersonalMessage'); - const msgId = msgParams.metamaskId; - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - try { - const cleanMsgParams = await this.personalMessageManager.approveMessage( - msgParams, - ); - const rawSig = await this.keyringController.signPersonalMessage( - cleanMsgParams, - ); - // tells the listener that the message has been signed - // and can be returned to the dapp - this.personalMessageManager.setMsgStatusSigned(msgId, rawSig); - return this.getState(); - } catch (error) { - log.info('MetaMaskController - eth_personalSign failed', error); - this.personalMessageManager.errorMessage(msgId, error); - throw error; - } - } - - /** - * Used to cancel a personal_sign type message. - * - * @param {string} msgId - The ID of the message to cancel. - */ - cancelPersonalMessage(msgId) { - const messageManager = this.personalMessageManager; - messageManager.rejectMsg(msgId); - return this.getState(); - } - // eth_decrypt methods /** @@ -3369,7 +3223,7 @@ export default class MetamaskController extends EventEmitter { const keyring = await this.keyringController.getKeyringForAccount(address); switch (keyring.type) { - case HardwareKeyringTypes.ledger: { + case KeyringType.ledger: { return new Promise((_, reject) => { reject( new Error('Ledger does not support eth_getEncryptionPublicKey.'), @@ -3377,7 +3231,7 @@ export default class MetamaskController extends EventEmitter { }); } - case HardwareKeyringTypes.trezor: { + case KeyringType.trezor: { return new Promise((_, reject) => { reject( new Error('Trezor does not support eth_getEncryptionPublicKey.'), @@ -3385,7 +3239,7 @@ export default class MetamaskController extends EventEmitter { }); } - case HardwareKeyringTypes.lattice: { + case KeyringType.lattice: { return new Promise((_, reject) => { reject( new Error('Lattice does not support eth_getEncryptionPublicKey.'), @@ -3393,7 +3247,7 @@ export default class MetamaskController extends EventEmitter { }); } - case HardwareKeyringTypes.qr: { + case KeyringType.qr: { return Promise.reject( new Error('QR hardware does not support eth_getEncryptionPublicKey.'), ); @@ -3458,74 +3312,6 @@ export default class MetamaskController extends EventEmitter { return this.getState(); } - // eth_signTypedData methods - - /** - * Called when a dapp uses the eth_signTypedData method, per EIP 712. - * - * @param {object} msgParams - The params passed to eth_signTypedData. - * @param {object} [req] - The original request, containing the origin. - * @param version - */ - async newUnsignedTypedMessage(msgParams, req, version) { - const promise = this.typedMessageManager.addUnapprovedMessageAsync( - msgParams, - req, - version, - ); - this.sendUpdate(); - this.opts.showUserConfirmation(); - return promise; - } - - /** - * The method for a user approving a call to eth_signTypedData, per EIP 712. - * Triggers the callback in newUnsignedTypedMessage. - * - * @param {object} msgParams - The params passed to eth_signTypedData. - * @returns {object} Full state update. - */ - async signTypedMessage(msgParams) { - log.info('MetaMaskController - eth_signTypedData'); - const msgId = msgParams.metamaskId; - const { version } = msgParams; - try { - const cleanMsgParams = await this.typedMessageManager.approveMessage( - msgParams, - ); - - // For some reason every version after V1 used stringified params. - if (version !== 'V1') { - // But we don't have to require that. We can stop suggesting it now: - if (typeof cleanMsgParams.data === 'string') { - cleanMsgParams.data = JSON.parse(cleanMsgParams.data); - } - } - - const signature = await this.keyringController.signTypedMessage( - cleanMsgParams, - { version }, - ); - this.typedMessageManager.setMsgStatusSigned(msgId, signature); - return this.getState(); - } catch (error) { - log.info('MetaMaskController - eth_signTypedData failed.', error); - this.typedMessageManager.errorMessage(msgId, error); - throw error; - } - } - - /** - * Used to cancel a eth_signTypedData type message. - * - * @param {string} msgId - The ID of the message to cancel. - */ - cancelTypedMessage(msgId) { - const messageManager = this.typedMessageManager; - messageManager.rejectMsg(msgId); - return this.getState(); - } - /** * @returns {boolean} true if the keyring type supports EIP-1559 */ @@ -3666,8 +3452,8 @@ export default class MetamaskController extends EventEmitter { if (usePhishDetect && phishingTestResponse?.result) { this.sendPhishingWarning(connectionStream, hostname); this.metaMetricsController.trackEvent({ - event: EVENT_NAMES.PHISHING_PAGE_DISPLAYED, - category: EVENT.CATEGORIES.PHISHING, + event: MetaMetricsEventName.PhishingPageDisplayed, + category: MetaMetricsEventCategory.Phishing, properties: { url: hostname, }, @@ -3987,7 +3773,6 @@ export default class MetamaskController extends EventEmitter { sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), - // Permission-related getAccounts: this.getPermittedAccounts.bind(this, origin), getPermissionsForOrigin: this.permissionController.getPermissions.bind( @@ -4052,15 +3837,11 @@ export default class MetamaskController extends EventEmitter { 'SnapController:getPermitted', origin, ), - requestPermissions: async (requestedPermissions) => { - const [approvedPermissions] = - await this.permissionController.requestPermissions( - { origin }, - requestedPermissions, - ); - - return Object.values(approvedPermissions); - }, + requestPermissions: async (requestedPermissions) => + await this.permissionController.requestPermissions( + { origin }, + requestedPermissions, + ), getPermissions: this.permissionController.getPermissions.bind( this.permissionController, origin, @@ -4254,7 +4035,7 @@ export default class MetamaskController extends EventEmitter { ); if (isManifestV3) { - await browser.storage.session.set({ loginToken, loginSalt }); + await this.extension.storage.session.set({ loginToken, loginSalt }); } if (!addresses.length) { @@ -4515,14 +4296,14 @@ export default class MetamaskController extends EventEmitter { */ setLocked() { const [trezorKeyring] = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.trezor, + KeyringType.trezor, ); if (trezorKeyring) { trezorKeyring.dispose(); } const [ledgerKeyring] = this.keyringController.getKeyringsByType( - HardwareKeyringTypes.ledger, + KeyringType.ledger, ); ledgerKeyring?.destroy?.(); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 43d0e5b20..af2e5c62f 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -11,10 +11,8 @@ import { TransactionStatus } from '../../shared/constants/transaction'; import createTxMeta from '../../test/lib/createTxMeta'; import { NETWORK_TYPES } from '../../shared/constants/network'; import { createTestProviderTools } from '../../test/stub/provider'; -import { - HardwareDeviceNames, - HardwareKeyringTypes, -} from '../../shared/constants/hardware-wallets'; +import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../shared/constants/keyring'; import { deferredPromise } from './lib/util'; const Ganache = require('../../test/e2e/ganache'); @@ -32,6 +30,9 @@ const browserPolyfillMock = { }, getPlatformInfo: async () => 'mac', }, + storage: { + session: {}, + }, }; let loggerMiddlewareMock; @@ -81,6 +82,10 @@ const MetaMaskController = proxyquire('./metamask-controller', { 'ethjs-contract': MockEthContract, }).default; +const MetaMaskControllerMV3 = proxyquire('./metamask-controller', { + '../../shared/modules/mv3.utils': { isManifestV3: true }, +}).default; + const currentNetworkId = '5'; const DEFAULT_LABEL = 'Account 1'; const TEST_SEED = @@ -170,10 +175,13 @@ describe('MetaMaskController', function () { const sandbox = sinon.createSandbox(); const noop = () => undefined; + browserPolyfillMock.storage.session.set = sandbox.spy(); + before(async function () { globalThis.isFirstTimeProfileLoaded = true; await ganacheServer.start(); sinon.spy(MetaMaskController.prototype, 'resetStates'); + sinon.spy(MetaMaskControllerMV3.prototype, 'resetStates'); }); beforeEach(function () { @@ -226,6 +234,7 @@ describe('MetaMaskController', function () { }, browser: browserPolyfillMock, infuraProjectId: 'foo', + isFirstMetaMaskControllerSetup: true, }); // add sinon method spies @@ -249,14 +258,70 @@ describe('MetaMaskController', function () { }); describe('should reset states on first time profile load', function () { - it('should reset state', function () { - assert(metamaskController.resetStates.calledOnce); - assert.equal(globalThis.isFirstTimeProfileLoaded, false); + it('in mv2, it should reset state without attempting to call browser storage', function () { + assert.equal(metamaskController.resetStates.callCount, 1); + assert.equal(browserPolyfillMock.storage.session.set.callCount, 0); }); - it('should not reset states if already set', function () { - // global.isFirstTime should also remain false - assert.equal(globalThis.isFirstTimeProfileLoaded, false); + it('in mv3, it should reset state', function () { + MetaMaskControllerMV3.prototype.resetStates.resetHistory(); + const metamaskControllerMV3 = new MetaMaskControllerMV3({ + showUserConfirmation: noop, + encryptor: { + encrypt(_, object) { + this.object = object; + return Promise.resolve('mock-encrypted'); + }, + decrypt() { + return Promise.resolve(this.object); + }, + }, + initState: cloneDeep(firstTimeState), + initLangCode: 'en_US', + platform: { + showTransactionNotification: () => undefined, + getVersion: () => 'foo', + }, + browser: browserPolyfillMock, + infuraProjectId: 'foo', + isFirstMetaMaskControllerSetup: true, + }); + assert.equal(metamaskControllerMV3.resetStates.callCount, 1); + assert.equal(browserPolyfillMock.storage.session.set.callCount, 1); + assert.deepEqual( + browserPolyfillMock.storage.session.set.getCall(0).args[0], + { + isFirstMetaMaskControllerSetup: false, + }, + ); + }); + + it('in mv3, it should not reset states if isFirstMetaMaskControllerSetup is false', function () { + MetaMaskControllerMV3.prototype.resetStates.resetHistory(); + browserPolyfillMock.storage.session.set.resetHistory(); + const metamaskControllerMV3 = new MetaMaskControllerMV3({ + showUserConfirmation: noop, + encryptor: { + encrypt(_, object) { + this.object = object; + return Promise.resolve('mock-encrypted'); + }, + decrypt() { + return Promise.resolve(this.object); + }, + }, + initState: cloneDeep(firstTimeState), + initLangCode: 'en_US', + platform: { + showTransactionNotification: () => undefined, + getVersion: () => 'foo', + }, + browser: browserPolyfillMock, + infuraProjectId: 'foo', + isFirstMetaMaskControllerSetup: false, + }); + assert.equal(metamaskControllerMV3.resetStates.callCount, 0); + assert.equal(browserPolyfillMock.storage.session.set.callCount, 0); }); }); @@ -275,7 +340,7 @@ describe('MetaMaskController', function () { it('adds private key to keyrings in KeyringController', async function () { const simpleKeyrings = metamaskController.keyringController.getKeyringsByType( - HardwareKeyringTypes.imported, + KeyringType.imported, ); const pubAddressHexArr = await simpleKeyrings[0].getAccounts(); const privKeyHex = await simpleKeyrings[0].exportAccount( @@ -555,11 +620,11 @@ describe('MetaMaskController', function () { .catch(() => null); const keyrings = await metamaskController.keyringController.getKeyringsByType( - HardwareKeyringTypes.trezor, + KeyringType.trezor, ); assert.deepEqual( metamaskController.keyringController.addNewKeyring.getCall(0).args, - [HardwareKeyringTypes.trezor], + [KeyringType.trezor], ); assert.equal(keyrings.length, 1); }); @@ -571,11 +636,11 @@ describe('MetaMaskController', function () { .catch(() => null); const keyrings = await metamaskController.keyringController.getKeyringsByType( - HardwareKeyringTypes.ledger, + KeyringType.ledger, ); assert.deepEqual( metamaskController.keyringController.addNewKeyring.getCall(0).args, - [HardwareKeyringTypes.ledger], + [KeyringType.ledger], ); assert.equal(keyrings.length, 1); }); @@ -651,7 +716,7 @@ describe('MetaMaskController', function () { await metamaskController.forgetDevice(HardwareDeviceNames.trezor); const keyrings = await metamaskController.keyringController.getKeyringsByType( - HardwareKeyringTypes.trezor, + KeyringType.trezor, ); assert.deepEqual(keyrings[0].accounts, []); @@ -711,7 +776,7 @@ describe('MetaMaskController', function () { it('should set unlockedAccount in the keyring', async function () { const keyrings = await metamaskController.keyringController.getKeyringsByType( - HardwareKeyringTypes.trezor, + KeyringType.trezor, ); assert.equal(keyrings[0].unlockedAccount, accountToUnlock); }); @@ -781,13 +846,13 @@ describe('MetaMaskController', function () { metamaskController.preferencesController, 'getSelectedAddress', ); - const getNetworkstub = sinon.stub( + const getNetworkIdStub = sinon.stub( metamaskController.txController.txStateManager, - 'getNetworkState', + 'getNetworkId', ); selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'); - getNetworkstub.returns(42); + getNetworkIdStub.returns(42); metamaskController.txController.txStateManager._addTransactionsToState([ createTxMeta({ @@ -897,177 +962,6 @@ describe('MetaMaskController', function () { }); }); - describe('#newUnsignedMessage', function () { - let msgParams, metamaskMsgs, messages, msgId; - - const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - const data = - '0x0000000000000000000000000000000000000043727970746f6b697474696573'; - - beforeEach(async function () { - sandbox.stub(metamaskController, 'getBalance'); - metamaskController.getBalance.callsFake(() => { - return Promise.resolve('0x0'); - }); - - await metamaskController.createNewVaultAndRestore( - 'foobar1337', - TEST_SEED_ALT, - ); - - msgParams = { - from: address, - data, - }; - - metamaskController.preferencesController.setDisabledRpcMethodPreference( - 'eth_sign', - true, - ); - const promise = metamaskController.newUnsignedMessage(msgParams); - // handle the promise so it doesn't throw an unhandledRejection - promise.then(noop).catch(noop); - - metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs(); - messages = metamaskController.messageManager.messages; - msgId = Object.keys(metamaskMsgs)[0]; - messages[0].msgParams.metamaskId = parseInt(msgId, 10); - }); - - it('persists address from msg params', function () { - assert.equal(metamaskMsgs[msgId].msgParams.from, address); - }); - - it('persists data from msg params', function () { - assert.equal(metamaskMsgs[msgId].msgParams.data, data); - }); - - it('sets the status to unapproved', function () { - assert.equal(metamaskMsgs[msgId].status, TransactionStatus.unapproved); - }); - - it('sets the type to eth_sign', function () { - assert.equal(metamaskMsgs[msgId].type, 'eth_sign'); - }); - - it('rejects the message', function () { - const msgIdInt = parseInt(msgId, 10); - metamaskController.cancelMessage(msgIdInt, noop); - assert.equal(messages[0].status, TransactionStatus.rejected); - }); - - it('checks message length', async function () { - msgParams = { - from: address, - data: '0xDEADBEEF', - }; - - try { - await metamaskController.newUnsignedMessage(msgParams); - } catch (error) { - assert.equal(error.message, 'eth_sign requires 32 byte message hash'); - } - }); - - it('errors when signing a message', async function () { - try { - await metamaskController.signMessage(messages[0].msgParams); - } catch (error) { - assert.equal( - error.message, - 'Expected message to be an Uint8Array with length 32', - ); - } - }); - }); - - describe('#newUnsignedPersonalMessage', function () { - let msgParams, metamaskPersonalMsgs, personalMessages, msgId; - - const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - const data = '0x43727970746f6b697474696573'; - - beforeEach(async function () { - sandbox.stub(metamaskController, 'getBalance'); - metamaskController.getBalance.callsFake(() => { - return Promise.resolve('0x0'); - }); - - await metamaskController.createNewVaultAndRestore( - 'foobar1337', - TEST_SEED_ALT, - ); - - msgParams = { - from: address, - data, - }; - - const promise = metamaskController.newUnsignedPersonalMessage(msgParams); - // handle the promise so it doesn't throw an unhandledRejection - promise.then(noop).catch(noop); - - metamaskPersonalMsgs = - metamaskController.personalMessageManager.getUnapprovedMsgs(); - personalMessages = metamaskController.personalMessageManager.messages; - msgId = Object.keys(metamaskPersonalMsgs)[0]; - personalMessages[0].msgParams.metamaskId = parseInt(msgId, 10); - }); - - it('errors with no from in msgParams', async function () { - try { - await metamaskController.newUnsignedPersonalMessage({ - data, - }); - assert.fail('should have thrown'); - } catch (error) { - assert.equal( - error.message, - 'MetaMask Message Signature: from field is required.', - ); - } - }); - - it('persists address from msg params', function () { - assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address); - }); - - it('persists data from msg params', function () { - assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data); - }); - - it('sets the status to unapproved', function () { - assert.equal( - metamaskPersonalMsgs[msgId].status, - TransactionStatus.unapproved, - ); - }); - - it('sets the type to personal_sign', function () { - assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign'); - }); - - it('rejects the message', function () { - const msgIdInt = parseInt(msgId, 10); - metamaskController.cancelPersonalMessage(msgIdInt, noop); - assert.equal(personalMessages[0].status, TransactionStatus.rejected); - }); - - it('errors when signing a message', async function () { - await metamaskController.signPersonalMessage( - personalMessages[0].msgParams, - ); - assert.equal( - metamaskPersonalMsgs[msgId].status, - TransactionStatus.signed, - ); - assert.equal( - metamaskPersonalMsgs[msgId].rawSig, - '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b', - ); - }); - }); - describe('#setupUntrustedCommunication', function () { const mockTxParams = { from: TEST_ADDRESS }; diff --git a/app/scripts/migrations/084.test.js b/app/scripts/migrations/084.test.js new file mode 100644 index 000000000..138bfacb6 --- /dev/null +++ b/app/scripts/migrations/084.test.js @@ -0,0 +1,103 @@ +import { migrate } from './084'; + +describe('migration #84', () => { + it('updates the version metadata', async () => { + const originalVersionedData = buildOriginalVersionedData({ + meta: { + version: 9999999, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.meta).toStrictEqual({ + version: 84, + }); + }); + + it('does not change the state if the network controller state does not exist', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + test: '123', + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); + }); + + const nonObjects = [undefined, null, 'test', 1, ['test']]; + for (const invalidState of nonObjects) { + it(`does not change the state if the network controller state is ${invalidState}`, async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: invalidState, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); + }); + } + + it('does not change the state if the network controller state does not include "network"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + test: '123', + }, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual(originalVersionedData.data); + }); + + it('replaces "network" in the network controller state with "networkId": null, "networkStatus": "unknown" if it is "loading"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + network: 'loading', + }, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual({ + NetworkController: { + networkId: null, + networkStatus: 'unknown', + }, + }); + }); + + it('replaces "network" in the network controller state with "networkId": network, "networkStatus": "available" if it is not "loading"', async () => { + const originalVersionedData = buildOriginalVersionedData({ + data: { + NetworkController: { + network: '12345', + }, + }, + }); + + const newVersionedData = await migrate(originalVersionedData); + + expect(newVersionedData.data).toStrictEqual({ + NetworkController: { + networkId: '12345', + networkStatus: 'available', + }, + }); + }); +}); + +function buildOriginalVersionedData({ meta = {}, data = {} } = {}) { + return { + meta: { version: 999999, ...meta }, + data: { ...data }, + }; +} diff --git a/app/scripts/migrations/084.ts b/app/scripts/migrations/084.ts new file mode 100644 index 000000000..66a2f45ae --- /dev/null +++ b/app/scripts/migrations/084.ts @@ -0,0 +1,47 @@ +import { cloneDeep } from 'lodash'; +import { hasProperty, isObject } from '@metamask/utils'; + +export const version = 84; + +/** + * The `network` property in state was replaced with `networkId` and `networkStatus`. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if ( + !hasProperty(state, 'NetworkController') || + !isObject(state.NetworkController) || + !hasProperty(state.NetworkController, 'network') + ) { + return state; + } + + const NetworkController = { ...state.NetworkController }; + + if (NetworkController.network === 'loading') { + NetworkController.networkId = null; + NetworkController.networkStatus = 'unknown'; + } else { + NetworkController.networkId = NetworkController.network; + NetworkController.networkStatus = 'available'; + } + + delete NetworkController.network; + + return { ...state, NetworkController }; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index c3f8e515f..54a09c2b4 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -87,6 +87,7 @@ import m080 from './080'; import * as m081 from './081'; import * as m082 from './082'; import * as m083 from './083'; +import * as m084 from './084'; const migrations = [ m002, @@ -171,6 +172,7 @@ const migrations = [ m081, m082, m083, + m084, ]; export default migrations; diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 94f7482d7..1ac0c853e 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -191,9 +191,14 @@ export default class ExtensionPlatform { async _showFailedTransaction(txMeta, errorMessage) { const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Failed transaction'; - const message = `Transaction ${nonce} failed! ${ + let message = `Transaction ${nonce} failed! ${ errorMessage || txMeta.err.message }`; + ///: BEGIN:ONLY_INCLUDE_IN(mmi) + if (isNaN(nonce)) { + message = `Transaction failed! ${errorMessage || txMeta.err.message}`; + } + ///: END:ONLY_INCLUDE_IN await this._showNotification(title, message); } diff --git a/coverage-targets.js b/coverage-targets.js index 8bd0ef280..041b322aa 100644 --- a/coverage-targets.js +++ b/coverage-targets.js @@ -6,10 +6,10 @@ // subset of files to check against these targets. module.exports = { global: { - lines: 64.5, - branches: 53.15, - statements: 63.57, - functions: 56.58, + lines: 66.7, + branches: 54.83, + statements: 65.99, + functions: 59.3, }, transforms: { branches: 100, diff --git a/development/build/config.js b/development/build/config.js index 292a5cc0b..b7b0f984c 100644 --- a/development/build/config.js +++ b/development/build/config.js @@ -3,10 +3,8 @@ const { readFile } = require('fs/promises'); const ini = require('ini'); const { BuildType } = require('../lib/build-type'); -const commonConfigurationPropertyNames = ['PUBNUB_PUB_KEY', 'PUBNUB_SUB_KEY']; - const configurationPropertyNames = [ - ...commonConfigurationPropertyNames, + 'MULTICHAIN', 'INFURA_PROJECT_ID', 'PHISHING_WARNING_PAGE_URL', 'PORTFOLIO_URL', @@ -19,10 +17,10 @@ const configurationPropertyNames = [ 'DISABLE_WEB_SOCKET_ENCRYPTION', 'METAMASK_DEBUG', 'SKIP_OTP_PAIRING_FLOW', + 'ENABLE_MV3', ]; const productionConfigurationPropertyNames = [ - ...commonConfigurationPropertyNames, 'INFURA_BETA_PROJECT_ID', 'INFURA_FLASK_PROJECT_ID', 'INFURA_PROD_PROJECT_ID', @@ -99,7 +97,7 @@ async function getProductionConfig(buildType) { }; const requiredEnvironmentVariables = { - all: ['PUBNUB_PUB_KEY', 'PUBNUB_SUB_KEY', 'SENTRY_DSN'], + all: ['SENTRY_DSN'], [BuildType.beta]: ['INFURA_BETA_PROJECT_ID', 'SEGMENT_BETA_WRITE_KEY'], [BuildType.flask]: ['INFURA_FLASK_PROJECT_ID', 'SEGMENT_FLASK_WRITE_KEY'], [BuildType.main]: ['INFURA_PROD_PROJECT_ID', 'SEGMENT_PROD_WRITE_KEY'], diff --git a/development/build/scripts.js b/development/build/scripts.js index 3bb01ef66..3be5a912f 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -52,7 +52,7 @@ const { } = require('./transforms/remove-fenced-code'); // map dist files to bag of needed native APIs against LM scuttling -const scuttlingConfig = { +const scuttlingConfigBase = { 'sentry-install.js': { // globals sentry need to function window: '', @@ -70,9 +70,9 @@ const scuttlingConfig = { Number: '', Request: '', Date: '', - document: '', JSON: '', encodeURIComponent: '', + console: '', crypto: '', // {clear/set}Timeout are "this sensitive" clearTimeout: 'window', @@ -87,6 +87,16 @@ const scuttlingConfig = { }, }; +const mv3ScuttlingConfig = { ...scuttlingConfigBase }; + +const standardScuttlingConfig = { + ...scuttlingConfigBase, + 'sentry-install.js': { + ...scuttlingConfigBase['sentry-install.js'], + document: '', + }, +}; + /** * Get the appropriate Infura project ID. * @@ -907,6 +917,9 @@ function setupBundlerDefaults( bundlerOpts.manualIgnore.push('remote-redux-devtools'); } + // This dependency uses WASM which we cannot execute in accordance with our CSP + bundlerOpts.manualIgnore.push('@chainsafe/as-sha256'); + // Inject environment variables via node-style `process.env` if (envVars) { bundlerOpts.transform.push([envify(envVars), { global: true }]); @@ -929,9 +942,8 @@ function setupBundlerDefaults( // Setup source maps setupSourcemaps(buildConfiguration, { buildTarget }); - // Setup wrapping of code against scuttling (before sourcemaps generation) - setupScuttlingWrapping(buildConfiguration, applyLavaMoat); + setupScuttlingWrapping(buildConfiguration, applyLavaMoat, envVars); } } @@ -985,7 +997,11 @@ function setupMinification(buildConfiguration) { }); } -function setupScuttlingWrapping(buildConfiguration, applyLavaMoat) { +function setupScuttlingWrapping(buildConfiguration, applyLavaMoat, envVars) { + const scuttlingConfig = + envVars.ENABLE_MV3 === 'true' + ? mv3ScuttlingConfig + : standardScuttlingConfig; const { events } = buildConfiguration; events.on('configurePipeline', ({ pipeline }) => { pipeline.get('scuttle').push( @@ -1106,7 +1122,9 @@ async function getEnvironmentVariables({ buildTarget, buildType, version }) { const iconNames = await generateIconNames(); return { ICON_NAMES: iconNames, + MULTICHAIN: config.MULTICHAIN === '1', CONF: devMode ? config : {}, + ENABLE_MV3: config.ENABLE_MV3, IN_TEST: testing, INFURA_PROJECT_ID: getInfuraProjectId({ buildType, @@ -1121,8 +1139,6 @@ async function getEnvironmentVariables({ buildTarget, buildType, version }) { NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION, PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({ config, testing }), PORTFOLIO_URL: config.PORTFOLIO_URL || 'https://portfolio.metamask.io', - PUBNUB_PUB_KEY: config.PUBNUB_PUB_KEY || '', - PUBNUB_SUB_KEY: config.PUBNUB_SUB_KEY || '', SEGMENT_HOST: config.SEGMENT_HOST, SEGMENT_WRITE_KEY: getSegmentWriteKey({ buildType, config, environment }), SENTRY_DSN: config.SENTRY_DSN, diff --git a/development/generate-lavamoat-policies.js b/development/generate-lavamoat-policies.js index a4004e0b8..9266091b7 100755 --- a/development/generate-lavamoat-policies.js +++ b/development/generate-lavamoat-policies.js @@ -4,6 +4,11 @@ const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const { BuildType } = require('./lib/build-type'); +const stableBuildTypes = Object.values(BuildType).filter( + // Skip generating policy for MMI until that build has stabilized + (buildType) => buildType !== BuildType.mmi, +); + start().catch((error) => { console.error('Policy generation failed.', error); process.exitCode = 1; @@ -20,7 +25,7 @@ async function start() { .option('build-types', { alias: ['t'], choices: Object.values(BuildType), - default: Object.values(BuildType), + default: stableBuildTypes, demandOption: true, description: 'The build type(s) to generate policy files for.', }) diff --git a/development/static-server.js b/development/static-server.js index facdc915b..bb15133d6 100755 --- a/development/static-server.js +++ b/development/static-server.js @@ -1,14 +1,13 @@ #!/usr/bin/env node -const fs = require('fs'); +const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); -const pify = require('pify'); const createStaticServer = require('./create-static-server'); const { parsePort } = require('./lib/parse-port'); -const fsStat = pify(fs.stat); +const fsStat = fs.stat; const DEFAULT_PORT = 9080; const onResponse = (request, response) => { diff --git a/development/ts-migration-dashboard/app/components/App.tsx b/development/ts-migration-dashboard/app/components/App.tsx index 317856a1f..e5ee1f3d0 100644 --- a/development/ts-migration-dashboard/app/components/App.tsx +++ b/development/ts-migration-dashboard/app/components/App.tsx @@ -1,7 +1,9 @@ -import React from 'react'; -import classnames from 'classnames'; -import { Tooltip as ReactTippy } from 'react-tippy'; +import React, { useState } from 'react'; import { readPartitionsFile } from '../../common/partitions-file'; +import type { ModulePartitionChild } from '../../common/build-module-partitions'; +import Box from './Box'; +import Connections from './Connections'; +import type { BoxRect, BoxModel } from './types'; type Summary = { numConvertedModules: number; @@ -12,16 +14,85 @@ function calculatePercentageComplete(summary: Summary) { return ((summary.numConvertedModules / summary.numModules) * 100).toFixed(1); } -export default function App() { - const partitions = readPartitionsFile(); +const partitions = readPartitionsFile(); +const allModules = partitions.flatMap((partition) => { + return partition.children; +}); +const modulesById = allModules.reduce>( + (obj, module) => { + return { ...obj, [module.id]: module }; + }, + {}, +); +const overallTotal = { + numConvertedModules: allModules.filter((module) => module.hasBeenConverted) + .length, + numModules: allModules.length, +}; - const allModules = partitions.flatMap((partition) => { - return partition.children; - }); - const overallTotal = { - numConvertedModules: allModules.filter((module) => module.hasBeenConverted) - .length, - numModules: allModules.length, +export default function App() { + const [boxRectsByModuleId, setBoxRectsById] = useState | null>(null); + const boxesByModuleId = + boxRectsByModuleId === null + ? {} + : Object.values(boxRectsByModuleId).reduce>( + (obj, boxRect) => { + const module = modulesById[boxRect.moduleId]; + + const dependencyBoxRects = module.dependencyIds.reduce( + (dependencyBoxes, dependencyId) => { + if (boxRectsByModuleId[dependencyId] === undefined) { + return dependencyBoxes; + } + return [...dependencyBoxes, boxRectsByModuleId[dependencyId]]; + }, + [], + ); + + const dependentBoxRects = module.dependentIds.reduce( + (dependentBoxes, dependentId) => { + if (boxRectsByModuleId[dependentId] === undefined) { + return dependentBoxes; + } + return [...dependentBoxes, boxRectsByModuleId[dependentId]]; + }, + [], + ); + + return { + ...obj, + [boxRect.moduleId]: { + rect: boxRect, + dependencyBoxRects, + dependentBoxRects, + }, + }; + }, + {}, + ); + const [activeBoxRectId, setActiveBoxRectId] = useState(null); + const activeBoxRect = + boxRectsByModuleId === null || activeBoxRectId === null + ? null + : boxRectsByModuleId[activeBoxRectId]; + + const registerBox = (id: string, boxRect: BoxRect) => { + setBoxRectsById((existingBoxRectsById) => { + if (existingBoxRectsById === undefined) { + return { [id]: boxRect }; + } + return { ...existingBoxRectsById, [id]: boxRect }; + }); + }; + const toggleConnectionsFor = (id: string) => { + if (activeBoxRectId !== undefined && activeBoxRectId === id) { + setActiveBoxRectId(null); + } else { + setActiveBoxRectId(id); + } }; return ( @@ -50,17 +121,35 @@ export default function App() {

- Each box -

+ Each box on this page represents a file in the codebase. Gray boxes +   -
- on this page represents a file that either we want to convert or - we've already converted to TypeScript (hover over a box to see the - filename). Boxes that are -
+ + represent files that need to be converted to TypeScript. Green boxes +   -
- greyed out are test or Storybook files. + + are files that have already been converted. Faded boxes + +   + + +   + + are test or Storybook files. +

+ +

+ You can hover over a box to see the name of the file that it + represents. You can also click on a box to see connections between + other files;{' '} + red lines + lead to dependencies (other files that import the file);{' '} + blue lines + lead to dependents (other files that are imported by the file).

@@ -103,36 +192,25 @@ export default function App() {

level {partition.level}
- {partition.children.map(({ name, hasBeenConverted }) => { - const isTest = /\.test\.(?:js|tsx?)/u.test(name); - const isStorybookModule = /\.stories\.(?:js|tsx?)/u.test( - name, - ); + {partition.children.map((module) => { + const areConnectionsVisible = activeBoxRectId === module.id; return ( - -
- + ); })}
); })} + {activeBoxRect === null ? null : ( + + )}
); diff --git a/development/ts-migration-dashboard/app/components/Box.tsx b/development/ts-migration-dashboard/app/components/Box.tsx new file mode 100644 index 000000000..675cb63db --- /dev/null +++ b/development/ts-migration-dashboard/app/components/Box.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useRef } from 'react'; +import classnames from 'classnames'; +import { Tooltip as ReactTippy } from 'react-tippy'; +import type { ModulePartitionChild } from '../../common/build-module-partitions'; +import type { BoxRect } from './types'; + +export default function Box({ + module, + register, + toggleConnectionsFor, + areConnectionsVisible, +}: { + module: ModulePartitionChild; + register: (id: string, boxRect: BoxRect) => void; + toggleConnectionsFor: (id: string) => void; + areConnectionsVisible: boolean; +}) { + const isTest = /\.test\.(?:js|tsx?)/u.test(module.id); + const isStorybookModule = /\.stories\.(?:js|tsx?)/u.test(module.id); + const ref = useRef(null); + + useEffect(() => { + if (ref.current?.offsetParent) { + const rect = ref.current.getBoundingClientRect(); + const offsetParentRect = ref.current.offsetParent.getBoundingClientRect(); + const top = rect.top - offsetParentRect.top; + const left = rect.left - offsetParentRect.left; + const centerX = left + rect.width / 2; + const centerY = top + rect.height / 2; + register(module.id, { + moduleId: module.id, + top, + left, + width: rect.width, + height: rect.height, + centerX, + centerY, + }); + } + }, [ref]); + + const onClick = (event: React.MouseEvent) => { + event.preventDefault(); + toggleConnectionsFor(module.id); + }; + + return ( + +
+ + ); +} diff --git a/development/ts-migration-dashboard/app/components/Connections.tsx b/development/ts-migration-dashboard/app/components/Connections.tsx new file mode 100644 index 000000000..329bfb3e5 --- /dev/null +++ b/development/ts-migration-dashboard/app/components/Connections.tsx @@ -0,0 +1,143 @@ +import React from 'react'; +import type { BoxModel } from './types'; + +function buildShapePoints(coordinates: [number, number][]): string { + return coordinates.map(([x, y]) => `${x},${y}`).join(' '); +} + +function buildPathD(coordinates: [number, number][]): string { + return coordinates + .map(([x, y], index) => { + if (index === 0) { + return `M ${x},${y}`; + } + return `L ${x},${y}`; + }) + .join(' '); +} + +function Arrowhead({ + type, + x, + y, +}: { + type: 'dependency' | 'dependent'; + x: number; + y: number; +}) { + return ( + + ); +} + +function Line({ + type, + originX, + originY, + originYOffset = 0, + destinationX, + destinationY, + destinationYOffset = 0, +}: { + type: 'dependency' | 'dependent'; + originX: number; + originY: number; + originYOffset?: number; + destinationX: number; + destinationY: number; + destinationYOffset?: number; +}) { + const coordinates: [number, number][] = + type === 'dependency' + ? [ + [originX, originY], + [originX, originY + originYOffset], + [destinationX, originY + originYOffset], + [destinationX, destinationY], + ] + : [ + [originX, originY], + [originX, destinationY - destinationYOffset], + [destinationX, destinationY - destinationYOffset], + [destinationX, destinationY], + ]; + return ( + + ); +} + +function LineStart({ + type, + x, + y, +}: { + type: 'dependency' | 'dependent'; + x: number; + y: number; +}) { + return ( + + ); +} + +export default function Connections({ activeBox }: { activeBox: BoxModel }) { + return ( + + {activeBox.dependencyBoxRects.length === 0 ? null : ( + + )} + {activeBox.dependencyBoxRects.map((dependencyBoxRect) => { + return ( + + + + + ); + })} + {activeBox.dependentBoxRects.map((dependentBoxRect) => { + return ( + + + + + ); + })} + + ); +} diff --git a/development/ts-migration-dashboard/app/components/types.ts b/development/ts-migration-dashboard/app/components/types.ts new file mode 100644 index 000000000..65b2349da --- /dev/null +++ b/development/ts-migration-dashboard/app/components/types.ts @@ -0,0 +1,15 @@ +export type BoxRect = { + moduleId: string; + top: number; + left: number; + width: number; + height: number; + centerX: number; + centerY: number; +}; + +export type BoxModel = { + rect: BoxRect; + dependencyBoxRects: BoxRect[]; + dependentBoxRects: BoxRect[]; +}; diff --git a/development/ts-migration-dashboard/app/styles/custom-elements.scss b/development/ts-migration-dashboard/app/styles/custom-elements.scss index 216c1ed6d..e53562024 100644 --- a/development/ts-migration-dashboard/app/styles/custom-elements.scss +++ b/development/ts-migration-dashboard/app/styles/custom-elements.scss @@ -2,6 +2,10 @@ --blue-gray-350: hsl(209deg 13.7% 62.4%); --blue-gray-100: hsl(209.8deg 16.5% 89%); --green: hsl(113deg 100% 38%); + --red: hsl(13deg 98% 61%); + --blue: hsl(246deg 97% 55%); + --light-cyan: hsl(178deg 100% 85%); + --cyan: hsl(178deg 100% 42%); } .page-header { @@ -69,6 +73,10 @@ border: 1px solid rgb(0 0 0 / 50%); } +.partitions { + position: relative; +} + .partition { display: flex; gap: 0.5rem; @@ -94,8 +102,9 @@ .module { width: 1.5rem; height: 1.5rem; - border: 1px solid rgba(0 0 0 / 25%); + border: 1px solid hsla(0deg 0% 0% / 25%); border-radius: 0.25rem; + cursor: pointer; &--inline { display: inline-block; @@ -120,6 +129,55 @@ &--storybook { opacity: 0.3; } + + &--active { + border-color: var(--cyan); + background-color: var(--light-cyan); + border-width: 2px; + } +} + +.module-connections { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + +.module-connection { + &__dependency-arrowhead { + fill: var(--red); + } + + &__dependency { + fill: none; + stroke: var(--red); + color: var(--red); + stroke-width: 2px; + } + + &__dependency-point { + fill: var(--red); + r: 3px; + } + + &__dependent-arrowhead { + fill: var(--blue); + } + + &__dependent { + fill: none; + stroke: var(--blue); + color: var(--blue); + stroke-width: 2px; + } + + &__dependent-point { + fill: var(--blue); + r: 3px; + } } /* Package overrides */ diff --git a/development/ts-migration-dashboard/common/build-module-partitions.ts b/development/ts-migration-dashboard/common/build-module-partitions.ts index 8ceaff18a..aa58568b3 100644 --- a/development/ts-migration-dashboard/common/build-module-partitions.ts +++ b/development/ts-migration-dashboard/common/build-module-partitions.ts @@ -9,24 +9,42 @@ import { } from './constants'; /** - * Represents a module that has been imported somewhere in the codebase. + * Represents a module that has been imported somewhere in the codebase, whether + * it is local to the project or an NPM package. * - * @property id - The name of a file or NPM module. + * @property id - If an NPM package, then the name of the package; otherwise the + * path to a file within the project. * @property dependents - The modules which are imported by this module. * @property level - How many modules it takes to import this module (from the * root of the dependency tree). - * @property isExternal - Whether the module refers to a NPM module. + * @property isExternal - Whether the module refers to a NPM package. * @property hasBeenConverted - Whether the module was one of the files we * wanted to convert to TypeScript and has been converted. */ type Module = { id: string; dependents: Module[]; + dependencies: Module[]; level: number; isExternal: boolean; hasBeenConverted: boolean; }; +/** + * Represents a module that belongs to a certain level within the dependency + * graph, displayed as a box in the UI. + * + * @property id - The id of the module. + * @property hasBeenConverted - Whether or not the module has been converted to + * TypeScript. + */ +export type ModulePartitionChild = { + id: string; + hasBeenConverted: boolean; + dependentIds: string[]; + dependencyIds: string[]; +}; + /** * Represents a set of modules that sit at a certain level within the final * dependency tree. @@ -34,16 +52,10 @@ type Module = { * @property level - How many modules it takes to import this module (from the * root of the dependency tree). * @property children - The modules that share this same level. - * @property children[].name - The name of the item being imported. - * @property children[].hasBeenConverted - Whether or not the module (assuming - * that it is a file in our codebase) has been converted to TypeScript. */ export type ModulePartition = { level: number; - children: { - name: string; - hasBeenConverted: boolean; - }[]; + children: ModulePartitionChild[]; }; /** @@ -196,10 +208,11 @@ function buildModulesWithLevels( // includes `foo.js`, so we say `foo.js` is a circular dependency of `baz.js`, // and we don't need to follow it. - const modulesToFill: Module[] = entryModuleIds.map((moduleId) => { + let modulesToFill: Module[] = entryModuleIds.map((moduleId) => { return { id: moduleId, dependents: [], + dependencies: [], level: 0, isExternal: false, hasBeenConverted: /\.tsx?$/u.test(moduleId), @@ -208,76 +221,101 @@ function buildModulesWithLevels( const modulesById: Record = {}; while (modulesToFill.length > 0) { - const currentModule = modulesToFill.shift() as Module; - const childModulesToFill: Module[] = []; - (dependenciesByModuleId[currentModule.id] ?? []).forEach( - (givenChildModuleId) => { - const npmPackageMatch = givenChildModuleId.match( - /^node_modules\/(?:(@[^/]+)\/)?([^/]+)\/.+$/u, - ); + const currentModule = modulesToFill[0]; - let childModuleId; - if (npmPackageMatch) { - childModuleId = - npmPackageMatch[1] === undefined - ? `${npmPackageMatch[2]}` - : `${npmPackageMatch[1]}/${npmPackageMatch[2]}`; - } else { - childModuleId = givenChildModuleId; - } - - // Skip circular dependencies - if ( - findDirectAndIndirectDependentIdsOf(currentModule).has(childModuleId) - ) { - return; - } - - // Skip files that weren't on the original list of JavaScript files to - // convert, as we don't want them to show up on the status dashboard - if ( - !npmPackageMatch && - !allowedFilePaths.includes(childModuleId.replace(/\.tsx?$/u, '.js')) - ) { - return; - } - - const existingChildModule = modulesById[childModuleId]; - - if (existingChildModule === undefined) { - const childModule: Module = { - id: childModuleId, - dependents: [currentModule], - level: currentModule.level + 1, - isExternal: Boolean(npmPackageMatch), - hasBeenConverted: /\.tsx?$/u.test(childModuleId), - }; - childModulesToFill.push(childModule); - } else { - if (currentModule.level + 1 > existingChildModule.level) { - existingChildModule.level = currentModule.level + 1; - // Update descendant modules' levels - childModulesToFill.push(existingChildModule); - } else { - // There is no point in descending into dependencies of this module - // if the new level of the module would be <= the existing level, - // because all of the dependencies from this point on are guaranteed - // to have a higher level and are therefore already at the right - // level. - } - - if (!existingChildModule.dependents.includes(currentModule)) { - existingChildModule.dependents.push(currentModule); - } - } - }, - ); - if (childModulesToFill.length > 0) { - modulesToFill.push(...childModulesToFill); + if (currentModule.level > 100) { + throw new Error( + "Can't build module partitions, as the dependency graph is being traversed ad infinitum.", + ); } + + const dependencyModulesToFill: Module[] = []; + for (const nonCanonicalDependencyModuleId of dependenciesByModuleId[ + currentModule.id + ]) { + // If the file path of the module is located in `node_modules`, use the + // name of the package as the ID + let dependencyModuleId; + const npmPackageMatch = nonCanonicalDependencyModuleId.match( + /^node_modules\/(?:(@[^/]+)\/)?([^/]+)\/.+$/u, + ); + if (npmPackageMatch) { + dependencyModuleId = + npmPackageMatch[1] === undefined + ? `${npmPackageMatch[2]}` + : `${npmPackageMatch[1]}/${npmPackageMatch[2]}`; + } else { + dependencyModuleId = nonCanonicalDependencyModuleId; + } + + // Skip circular dependencies + if ( + findDirectAndIndirectDependentIdsOf(currentModule).has( + dependencyModuleId, + ) + ) { + continue; + } + + // Skip files that weren't on the original list of JavaScript files to + // convert, as we don't want them to show up on the status dashboard + if ( + !npmPackageMatch && + !allowedFilePaths.includes( + dependencyModuleId.replace(/\.tsx?$/u, '.js'), + ) + ) { + continue; + } + + let existingDependencyModule = modulesById[dependencyModuleId]; + + if (existingDependencyModule === undefined) { + existingDependencyModule = { + id: dependencyModuleId, + dependents: [currentModule], + dependencies: [], + level: currentModule.level + 1, + isExternal: Boolean(npmPackageMatch), + hasBeenConverted: /\.tsx?$/u.test(dependencyModuleId), + }; + dependencyModulesToFill.push(existingDependencyModule); + } else if (currentModule.level + 1 > existingDependencyModule.level) { + existingDependencyModule.level = currentModule.level + 1; + // Update descendant modules' levels + dependencyModulesToFill.push(existingDependencyModule); + } else { + // There is no point in descending into dependencies of this module + // if the new level of the module would be <= the existing level, + // because all of the dependencies from this point on are guaranteed + // to have a higher level and are therefore already at the right + // level. + } + + if ( + !existingDependencyModule.dependents.some( + (m) => m.id === currentModule.id, + ) + ) { + existingDependencyModule.dependents.push(currentModule); + } + + if ( + !currentModule.dependencies.some( + (m) => m.id === existingDependencyModule.id, + ) + ) { + currentModule.dependencies.push(existingDependencyModule); + } + } + if (dependencyModulesToFill.length > 0) { + modulesToFill.push(...dependencyModulesToFill); + } + if (!(currentModule.id in modulesById)) { modulesById[currentModule.id] = currentModule; } + modulesToFill = modulesToFill.slice(1); } return modulesById; @@ -288,20 +326,21 @@ function buildModulesWithLevels( * which import that file directly or through some other file. * * @param module - A module in the graph. - * @returns The ids of the modules which are incoming connections to + * @returns The ids of the modules which are direct and indirect dependents of * the module. */ function findDirectAndIndirectDependentIdsOf(module: Module): Set { - const modulesToProcess = [module]; + let modulesToProcess = [module]; const allDependentIds = new Set(); while (modulesToProcess.length > 0) { - const currentModule = modulesToProcess.shift() as Module; - currentModule.dependents.forEach((dependent) => { + const currentModule = modulesToProcess[0]; + for (const dependent of currentModule.dependents) { if (!allDependentIds.has(dependent.id)) { allDependentIds.add(dependent.id); modulesToProcess.push(dependent); } - }); + } + modulesToProcess = modulesToProcess.slice(1); } return allDependentIds; } @@ -328,8 +367,22 @@ function partitionModulesByLevel( } Object.values(modulesById).forEach((module) => { modulePartitions[module.level].children.push({ - name: module.id, + id: module.id, hasBeenConverted: module.hasBeenConverted, + dependentIds: module.dependents.map((dependent) => dependent.id).sort(), + dependencyIds: module.dependencies + .map((dependency) => dependency.id) + .sort(), + }); + }); + Object.values(modulePartitions).forEach((partition) => { + partition.children.sort((a, b) => { + if (a.id < b.id) { + return -1; + } else if (a.id > b.id) { + return 1; + } + return 0; }); }); return modulePartitions.reverse(); diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 254019f80..713f53ff7 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -386,9 +386,9 @@ "ui/components/app/asset-list-item/asset-list-item.js", "ui/components/app/asset-list-item/index.js", "ui/components/app/asset-list/asset-list.js", - "ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.js", - "ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.stories.js", - "ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.test.js", + "ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.js", + "ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.stories.js", + "ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.test.js", "ui/components/app/asset-list/index.js", "ui/components/app/cancel-button/cancel-button.js", "ui/components/app/cancel-button/index.js", @@ -1342,10 +1342,6 @@ "ui/pages/lock/lock.component.js", "ui/pages/lock/lock.container.js", "ui/pages/lock/lock.test.js", - "ui/pages/mobile-sync/index.js", - "ui/pages/mobile-sync/mobile-sync.component.js", - "ui/pages/mobile-sync/mobile-sync.container.js", - "ui/pages/mobile-sync/mobile-sync.stories.js", "ui/pages/notifications/index.js", "ui/pages/notifications/notification.test.js", "ui/pages/notifications/notifications.js", diff --git a/docs/assets/confirmation.png b/docs/assets/confirmation.png new file mode 100644 index 000000000..d4b58a2f6 Binary files /dev/null and b/docs/assets/confirmation.png differ diff --git a/docs/confirmations.md b/docs/confirmations.md new file mode 100644 index 000000000..2f3aef515 --- /dev/null +++ b/docs/confirmations.md @@ -0,0 +1,253 @@ +# Adding New Confirmations + +## Overview + +Given the security focused nature of self-custody, confirmations and approvals form a pivotal aspect of the MetaMask extension. + +Confirmations can be triggered by dApps and the UI itself, and are used to approve a variety of operations such as: + +- Connecting to dApps +- Giving permissions to dApps +- Sending Eth +- Transfering tokens +- Signing data +- Interacting with Snaps +- Adding Ethereum networks + +It is vital any new confirmations are implemented using best practices and consistent patterns, to avoid adding complexity to the code, and to minimise the maintenance cost of many alternate confirmations. + +As we try to maintain a clean boundary between the UI and background page, the effort to implement a new confirmation can also be split accordingly. + +## Background + +### 1. Create Messenger + +Ensure the controller or logic requiring a confirmation has access to a controller messenger. + +Provide a `messenger` argument in the constructor if using a controller. + +Ensure the allowed actions include at least the `ApprovalController:addRequest` action. + +If the controller extends `BaseControllerV2`, the property `messagingSystem` is available to access the messenger passed to the base controller. Otherwise, ensure the provided messenger is assigned to a private property. + +#### Example + +``` +this.someController = new SomeController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'SomeController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), + ... +}); +``` + +### 2. Create Approval Request + +Send an `addRequest` message to the `ApprovalController` to create an approval request. + +This message returns a `Promise` which will resolve if the confirmation is approved, and reject if the confirmation is denied or cancelled. + +Use an `async` function to send the message so the logic can `await` the confirmation and code execution can continue once approved. This enables the logic ran after approval to be kept in the same flow and therefore the logic to remain readable and encapsulated. + +Ensure suitable error handling is in place to handle the confirmation being cancelled or denied and therefore the `Promise` being rejected. + +The available message arguments are: + +| Name | Description | Example Value | +| -- | -- | -- | +| opts.id | The ID of the approval request.
Assigned to a random value if not provided. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` | +| opts.origin | The origin of the request.
Either the dApp host or "metamask" if internal. | `"metamask.github.io"` | +| opts.type | An arbitrary string identifying the type of request. | `"eth_signTypedData"` | +| opts.requestData | Additional fixed data for the request.
Must be a JSON compatible object.| `{ transactionId: '123' }` | +| opts.requestState | Additional mutable data for the request.
Must be a JSON compatible object.
Can be updated using the `ApprovalController.updateRequestState` action. | `{ status: 'pending' }` | +| shouldShowRequest | A boolean indicating whether the popup should be displayed. | `true` | + +#### Example + +``` +await this.messagingSystem.call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + requestData, + }, + true, +); +``` + +### 3. Update Approval Request + +If you wish to provide additional state to the confirmation while it is visible, send an `updateRequestState` message to the `ApprovalController`. + +This requires you to have provided the `id` when creating the approval request, so it can be passed to the update message. + +The available message arguments are: + +| Name | Description | Example Value | +| -- | -- | -- | +| opts.id | The ID of the approval request to update. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` | +| opts.requestState | The updated mutable data for the request.
Must be a JSON compatible object. | `{ status: 'pending' }` | + +#### Example + +``` +await this.messagingSystem.call( + 'ApprovalController:updateRequestState', + { + id, + requestState: { counter }, + }, +); +``` + +## Frontend + +### 1. Create Template File + +The `ConfirmationPage` component is already configured to display any approval requests generated by the `ApprovalController` and the associated `pendingApprovals` state. + +In order to configure how the resulting confirmation is rendered, an **Approval Template** is required. + +Create a new JavaScript file in `ui/pages/confirmation/templates` with the name matching the `type` used in the background approval request. + +### 2. Update Approval Templates + +Add your imported file to the `APPROVAL_TEMPLATES` constant in: +[ui/pages/confirmation/templates/index.js](../ui/pages/confirmation/templates/index.js) + +### 3. Define Values + +Inside the template file, define a `getValues` function that returns an object with the following properties: + +| Name | Description | Example Value | +| -- | -- | -- | +| content | An array of objects defining the components to be rendered in the confirmation.
Processed by the [MetaMaskTemplateRenderer](../ui/components/app/metamask-template-renderer/metamask-template-renderer.js). | See example below. | +| onSubmit | A callback to execute when the user approves the confirmation. | `actions.resolvePendingApproval(...)` | +| onCancel | A callback to execute when the user rejects the confirmation. | `actions.rejectPendingApproval(...)` | +| submitText | Text shown for the accept button. | `t('approveButtonText')` | +| cancelText | Text shown on the reject button. | `t('cancel')` | +| loadingText | Text shown while waiting for the onSubmit callback to complete. | `t('addingCustomNetwork')` | +| networkDisplay | A boolean indicating whether to show the current network at the top of the confirmation. | `true` | + +#### Example + +``` +function getValues(pendingApproval, t, actions, _history) { + return { + content: [ + { + element: 'Typography', + key: 'title', + children: 'Example', + props: { + variant: TypographyVariant.H3, + align: 'center', + fontWeight: 'bold', + boxProps: { + margin: [0, 0, 4], + }, + }, + }, + ... + ], + cancelText: t('cancel'), + submitText: t('approveButtonText'), + loadingText: t('addingCustomNetwork'), + onSubmit: () => + actions.resolvePendingApproval( + pendingApproval.id, + pendingApproval.requestData, + ), + onCancel: () => + actions.rejectPendingApproval( + pendingApproval.id, + ethErrors.provider.userRejectedRequest().serialize(), + ), + networkDisplay: true, + }; +} +``` + +### 4. Define Alerts + +If any alerts are required in the confirmation, define the `getAlerts` function in the template file. + +This needs to return an array of any required alerts, based on the current pending approval. + +Each alert is an object with the following properties: + +| Name | Description | Example Value | +| -- | -- | -- | +| id | A unique string to identify the alert. | `"MISMATCHED_NETWORK_RPC"` | +| severity | The severity of the alert.
Use the constants from the design system. | `SEVERITIES.DANGER` | +| content | The component to be rendered inside the alert.
Uses the same format as the `content` returned from `getValues`.
The component can have nested components via the `children` property. | See example below. | + +#### Example + +``` +function getAlerts(_pendingApproval) { + return [ + { + id: 'EXAMPLE_ALERT', + severity: SEVERITIES.WARNING, + content: { + element: 'span', + children: { + element: 'MetaMaskTranslation', + props: { + translationKey: 'exampleMessage', + }, + }, + }, + }, + ]; +} +``` + +### 5. Export Functions + +Ensure the `getValues` and `getAlerts` functions are exported from the template file. + +#### Example + +``` +const example = { + getAlerts, + getValues, +}; + +export default example; +``` + +## Example Branch + +See [this branch](https://github.com/MetaMask/metamask-extension/compare/develop...example/confirmation) as an example of the full code needed to add a confirmation. + +The confirmation can be tested using the [E2E Test dApp](https://metamask.github.io/test-dapp/) and selecting `Request Permissions`. + +## Glossary + +### ApprovalController + +The [ApprovalController](https://github.com/MetaMask/core/blob/main/packages/approval-controller/src/ApprovalController.ts) is a controller defined in the core repository which is responsible for creating and tracking approvals and confirmations in both the MetaMask extension and MetaMask mobile. + +The `pendingApprovals` state used by the `ApprovalController` is not currently persisted, meaning any confirmations created by it will not persist after restarting the browser for example. + +### ConfirmationPage + +The [ConfirmationPage](../ui/pages/confirmation/confirmation.js) is a React component that aims to provide a generic confirmation window which can be configured using templates, each implementing a consistent interface. + +This avoids the need for additional React components when creating confirmations, as additional templates with less logic, and less duplication, can be created instead. + +## Screenshots + +### Confirmation Window + +[](assets/confirmation.png) diff --git a/jest.config.js b/jest.config.js index 94daa4971..ba0c5219e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { '/app/scripts/constants/error-utils.js', '/app/scripts/controllers/network/**/*.js', '/app/scripts/controllers/permissions/**/*.js', + '/app/scripts/controllers/sign.ts', '/app/scripts/flask/**/*.js', '/app/scripts/lib/**/*.js', '/app/scripts/lib/createRPCMethodTrackingMiddleware.js', @@ -13,7 +14,7 @@ module.exports = { '/development/fitness-functions/**/*.test.(js|ts|tsx)', ], coverageDirectory: './coverage', - coveragePathIgnorePatterns: ['.stories.js', '.snap'], + coveragePathIgnorePatterns: ['.stories.*', '.snap'], coverageReporters: ['json'], reporters: [ 'default', @@ -39,6 +40,7 @@ module.exports = { '/app/scripts/controllers/app-state.test.js', '/app/scripts/controllers/network/**/*.test.js', '/app/scripts/controllers/permissions/**/*.test.js', + '/app/scripts/controllers/sign.test.ts', '/app/scripts/flask/**/*.test.js', '/app/scripts/lib/**/*.test.js', '/app/scripts/lib/**/*.test.ts', diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 5cf58f0f3..37ab828c8 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -469,8 +469,8 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { @@ -661,7 +661,7 @@ "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/controller-utils": true + "@metamask/address-book-controller>@metamask/controller-utils": true } }, "@metamask/address-book-controller>@metamask/base-controller": { @@ -669,6 +669,21 @@ "immer": true } }, + "@metamask/address-book-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/announcement-controller": { "packages": { "@metamask/announcement-controller>@metamask/base-controller": true @@ -681,11 +696,16 @@ }, "@metamask/approval-controller": { "packages": { + "@metamask/approval-controller>@metamask/base-controller": true, "@metamask/approval-controller>nanoid": true, - "@metamask/base-controller": true, "eth-rpc-errors": true } }, + "@metamask/approval-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -707,12 +727,12 @@ "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/controller-utils": true, + "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/metamask-eth-abis": true, - "@metamask/utils": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, @@ -724,10 +744,22 @@ }, "@metamask/assets-controllers>@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>superstruct": true } }, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>@metamask/controller-utils": { "globals": { "console.error": true, @@ -735,7 +767,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -743,6 +775,18 @@ "ethjs>ethjs-unit": true } }, + "@metamask/assets-controllers>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>abort-controller": { "globals": { "AbortController": true @@ -767,7 +811,8 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/controller-utils>@metamask/utils": true, + "@metamask/controller-utils>@spruceid/siwe-parser": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -775,23 +820,34 @@ "ethjs>ethjs-unit": true } }, - "@metamask/controller-utils>isomorphic-fetch": { + "@metamask/controller-utils>@metamask/utils": { "globals": { - "fetch.bind": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": true + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": { + "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true + "console.error": true, + "console.log": true + }, + "packages": { + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": true + } + }, + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": { + "globals": { + "mode": true + }, + "packages": { + "browserify>buffer": true, + "browserify>insert-module-globals>is-buffer": true } }, "@metamask/controllers>nanoid": { @@ -819,13 +875,25 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true } }, + "@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { "globals": { "URL": true, @@ -835,62 +903,51 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, + "@metamask/safe-event-emitter": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/utils": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -906,33 +963,17 @@ "TextEncoder": true }, "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "@metamask/scure-bip39": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": { "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true, @@ -940,21 +981,6 @@ "eth-sig-util>tweetnacl-util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -965,31 +991,16 @@ "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, "eth-lattice-keyring>@ethereumjs/util": true, "ethereumjs-wallet>randombytes": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>obs-store": { "packages": { - "safe-event-emitter": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "watchify>xtend": true } }, @@ -1022,17 +1033,25 @@ }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": { "packages": { + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -1069,9 +1088,9 @@ "@metamask/eth-token-tracker>eth-block-tracker": true, "@metamask/eth-token-tracker>ethjs": true, "@metamask/eth-token-tracker>human-standard-token-abi": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "ethjs-contract": true, - "ethjs-query": true, - "safe-event-emitter": true + "ethjs-query": true } }, "@metamask/eth-token-tracker>deep-equal": { @@ -1103,8 +1122,8 @@ }, "packages": { "@metamask/eth-token-tracker>eth-block-tracker>pify": true, - "eth-query": true, - "safe-event-emitter": true + "@metamask/eth-token-tracker>safe-event-emitter": true, + "eth-query": true } }, "@metamask/eth-token-tracker>ethjs": { @@ -1163,6 +1182,15 @@ "promise-to-callback": true } }, + "@metamask/eth-token-tracker>safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>util": true, + "webpack>events": true + } + }, "@metamask/etherscan-link": { "globals": { "URL": true @@ -1175,8 +1203,8 @@ "setInterval": true }, "packages": { - "@metamask/controller-utils": true, "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, @@ -1188,6 +1216,21 @@ "immer": true } }, + "@metamask/gas-fee-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1222,12 +1265,24 @@ }, "@metamask/key-tree": { "packages": { + "@metamask/key-tree>@metamask/utils": true, "@metamask/key-tree>@noble/ed25519": true, "@metamask/key-tree>@noble/hashes": true, "@metamask/key-tree>@noble/secp256k1": true, "@metamask/key-tree>@scure/base": true, - "@metamask/scure-bip39": true, - "@metamask/utils": true + "@metamask/scure-bip39": true + } + }, + "@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/key-tree>@noble/ed25519": { @@ -1272,19 +1327,38 @@ "@metamask/logo>gl-vec3": true } }, + "@metamask/message-manager": { + "packages": { + "@metamask/controller-utils": true, + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, "@metamask/obs-store": { - "globals": { - "localStorage": true - }, "packages": { "@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@metamask/obs-store>through2": { @@ -1296,9 +1370,12 @@ } }, "@metamask/permission-controller": { + "globals": { + "console.error": true + }, "packages": { - "@metamask/base-controller": true, - "@metamask/permission-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, + "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1306,19 +1383,9 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, + "@metamask/permission-controller>@metamask/base-controller": { "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "immer": true } }, "@metamask/permission-controller>nanoid": { @@ -1332,8 +1399,8 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/controller-utils>isomorphic-fetch": true, "@metamask/phishing-controller>@metamask/controller-utils": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } @@ -1345,7 +1412,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1353,6 +1420,25 @@ "ethjs>ethjs-unit": true } }, + "@metamask/phishing-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1385,6 +1471,14 @@ "crypto.getRandomValues": true } }, + "@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/scure-bip39": { "globals": { "TextEncoder": true @@ -1408,13 +1502,28 @@ "@ethersproject/bignumber": true, "@ethersproject/providers": true, "@metamask/base-controller": true, - "@metamask/controller-utils": true, - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/smart-transactions-controller>@metamask/controllers>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1448,16 +1557,77 @@ "semver": true } }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": { + "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>case": true, + "browserify": true, + "browserify>buffer": true + } + }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { + "globals": { + "WeakRef": true + }, + "packages": { + "browserify": true + } + }, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": { + "globals": { + "TextDecoder": true, + "crypto": true + }, + "packages": { + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "@metamask/key-tree>@scure/base": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, "@ngraveio/bc-ur>bignumber.js": true, + "@ngraveio/bc-ur>cbor-sync": true, "@ngraveio/bc-ur>crc": true, "@ngraveio/bc-ur>jsbi": true, "addons-linter>sha.js": true, "browserify>assert": true, - "browserify>buffer": true, - "pubnub>cbor-sync": true + "browserify>buffer": true } }, "@ngraveio/bc-ur>assert>object-is": { @@ -1472,6 +1642,14 @@ "define": true } }, + "@ngraveio/bc-ur>cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "browserify>buffer": true + } + }, "@ngraveio/bc-ur>crc": { "packages": { "browserify>buffer": true @@ -1625,24 +1803,6 @@ "define": true } }, - "@spruceid/siwe-parser": { - "globals": { - "console.error": true, - "console.log": true - }, - "packages": { - "@spruceid/siwe-parser>apg-js": true - } - }, - "@spruceid/siwe-parser>apg-js": { - "globals": { - "mode": true - }, - "packages": { - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true - } - }, "@storybook/api>regenerator-runtime": { "globals": { "regeneratorRuntime": "write" @@ -2595,10 +2755,22 @@ "setTimeout": true }, "packages": { - "@metamask/utils": true, + "@metamask/safe-event-emitter": true, + "eth-block-tracker>@metamask/utils": true, "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "eth-query>json-rpc-random-id": true + } + }, + "eth-block-tracker>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "eth-ens-namehash": { @@ -2629,10 +2801,10 @@ "console.error": true }, "packages": { + "@metamask/safe-event-emitter": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "pify": true } }, @@ -2682,61 +2854,12 @@ "console.warn": true }, "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": true, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": true, - "eth-lattice-keyring>@ethereumjs/util>async": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>async": { - "globals": { - "clearTimeout": true, - "console": true, - "define": true, - "queueMicrotask": true, - "setTimeout": true - }, - "packages": { - "browserify>process": true, - "browserify>timers-browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": { - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@scure/base": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true + "browserify>insert-module-globals>is-buffer": true } }, "eth-lattice-keyring>bn.js": { @@ -2937,14 +3060,22 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3064,13 +3195,21 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ganache>secp256k1>elliptic": true } }, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-abi>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3255,14 +3394,22 @@ "ethereumjs-wallet>safe-buffer": true } }, + "ethereumjs-wallet>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-wallet>ethereumjs-util": { "packages": { "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, + "ethereumjs-wallet>ethereum-cryptography": true, "ethereumjs-wallet>ethereumjs-util>ethjs-util": true, "ganache>secp256k1>elliptic": true } @@ -3496,16 +3643,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true + "@metamask/safe-event-emitter": true, + "eth-rpc-errors": true } }, "json-rpc-middleware-stream": { @@ -3514,15 +3653,10 @@ "setTimeout": true }, "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, + "@metamask/safe-event-emitter": true, "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true @@ -3656,32 +3790,6 @@ "console": true } }, - "pubnub": { - "globals": { - "ActiveXObject": true, - "XMLHttpRequest": true, - "addEventListener": true, - "btoa": true, - "clearInterval": true, - "clearTimeout": true, - "console": true, - "define": true, - "localStorage.getItem": true, - "localStorage.setItem": true, - "location": true, - "navigator": true, - "setInterval": true, - "setTimeout": true - } - }, - "pubnub>cbor-sync": { - "globals": { - "define": true - }, - "packages": { - "browserify>buffer": true - } - }, "pump": { "packages": { "browserify>browser-resolve": true, @@ -4098,15 +4206,6 @@ "@babel/runtime": true } }, - "safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>util": true, - "webpack>events": true - } - }, "semver": { "globals": { "console.error": true diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 00325bca9..3c9a08e54 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -469,8 +469,8 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { @@ -661,7 +661,7 @@ "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/controller-utils": true + "@metamask/address-book-controller>@metamask/controller-utils": true } }, "@metamask/address-book-controller>@metamask/base-controller": { @@ -669,6 +669,21 @@ "immer": true } }, + "@metamask/address-book-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/announcement-controller": { "packages": { "@metamask/announcement-controller>@metamask/base-controller": true @@ -681,11 +696,16 @@ }, "@metamask/approval-controller": { "packages": { + "@metamask/approval-controller>@metamask/base-controller": true, "@metamask/approval-controller>nanoid": true, - "@metamask/base-controller": true, "eth-rpc-errors": true } }, + "@metamask/approval-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -707,12 +727,12 @@ "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/controller-utils": true, + "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/metamask-eth-abis": true, - "@metamask/utils": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, @@ -724,10 +744,22 @@ }, "@metamask/assets-controllers>@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>superstruct": true } }, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>@metamask/controller-utils": { "globals": { "console.error": true, @@ -735,7 +767,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -743,6 +775,18 @@ "ethjs>ethjs-unit": true } }, + "@metamask/assets-controllers>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>abort-controller": { "globals": { "AbortController": true @@ -767,7 +811,8 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/controller-utils>@metamask/utils": true, + "@metamask/controller-utils>@spruceid/siwe-parser": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -775,23 +820,34 @@ "ethjs>ethjs-unit": true } }, - "@metamask/controller-utils>isomorphic-fetch": { + "@metamask/controller-utils>@metamask/utils": { "globals": { - "fetch.bind": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": true + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": { + "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true + "console.error": true, + "console.log": true + }, + "packages": { + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": true + } + }, + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": { + "globals": { + "mode": true + }, + "packages": { + "browserify>buffer": true, + "browserify>insert-module-globals>is-buffer": true } }, "@metamask/controllers>nanoid": { @@ -833,9 +889,9 @@ "setTimeout": true }, "packages": { + "@metamask/desktop>@metamask/obs-store": true, "@metamask/desktop>eciesjs": true, "@metamask/desktop>otpauth": true, - "@metamask/obs-store": true, "browserify>buffer": true, "browserify>events": true, "browserify>process": true, @@ -848,6 +904,24 @@ "webextension-polyfill": true } }, + "@metamask/desktop>@metamask/obs-store": { + "globals": { + "localStorage": true + }, + "packages": { + "@metamask/desktop>@metamask/obs-store>through2": true, + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true + } + }, + "@metamask/desktop>@metamask/obs-store>through2": { + "packages": { + "browserify>process": true, + "browserify>util": true, + "readable-stream": true, + "watchify>xtend": true + } + }, "@metamask/desktop>eciesjs": { "packages": { "@metamask/desktop>eciesjs>futoin-hkdf": true, @@ -873,13 +947,25 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true } }, + "@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { "globals": { "URL": true, @@ -889,62 +975,51 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, + "@metamask/safe-event-emitter": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/utils": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -960,33 +1035,17 @@ "TextEncoder": true }, "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "@metamask/scure-bip39": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": { "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true, @@ -994,21 +1053,6 @@ "eth-sig-util>tweetnacl-util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -1019,31 +1063,16 @@ "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, "eth-lattice-keyring>@ethereumjs/util": true, "ethereumjs-wallet>randombytes": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>obs-store": { "packages": { - "safe-event-emitter": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "watchify>xtend": true } }, @@ -1076,17 +1105,25 @@ }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": { "packages": { + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -1123,9 +1160,9 @@ "@metamask/eth-token-tracker>eth-block-tracker": true, "@metamask/eth-token-tracker>ethjs": true, "@metamask/eth-token-tracker>human-standard-token-abi": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "ethjs-contract": true, - "ethjs-query": true, - "safe-event-emitter": true + "ethjs-query": true } }, "@metamask/eth-token-tracker>deep-equal": { @@ -1157,8 +1194,8 @@ }, "packages": { "@metamask/eth-token-tracker>eth-block-tracker>pify": true, - "eth-query": true, - "safe-event-emitter": true + "@metamask/eth-token-tracker>safe-event-emitter": true, + "eth-query": true } }, "@metamask/eth-token-tracker>ethjs": { @@ -1217,6 +1254,15 @@ "promise-to-callback": true } }, + "@metamask/eth-token-tracker>safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>util": true, + "webpack>events": true + } + }, "@metamask/etherscan-link": { "globals": { "URL": true @@ -1229,8 +1275,8 @@ "setInterval": true }, "packages": { - "@metamask/controller-utils": true, "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, @@ -1242,6 +1288,21 @@ "immer": true } }, + "@metamask/gas-fee-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1276,12 +1337,24 @@ }, "@metamask/key-tree": { "packages": { + "@metamask/key-tree>@metamask/utils": true, "@metamask/key-tree>@noble/ed25519": true, "@metamask/key-tree>@noble/hashes": true, "@metamask/key-tree>@noble/secp256k1": true, "@metamask/key-tree>@scure/base": true, - "@metamask/scure-bip39": true, - "@metamask/utils": true + "@metamask/scure-bip39": true + } + }, + "@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/key-tree>@noble/ed25519": { @@ -1326,10 +1399,32 @@ "@metamask/logo>gl-vec3": true } }, - "@metamask/notification-controller": { + "@metamask/message-manager": { "packages": { "@metamask/controller-utils": true, + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, + "@metamask/notification-controller": { + "packages": { "@metamask/notification-controller>@metamask/base-controller": true, + "@metamask/notification-controller>@metamask/controller-utils": true, "@metamask/notification-controller>nanoid": true } }, @@ -1338,19 +1433,31 @@ "immer": true } }, + "@metamask/notification-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, "@metamask/obs-store": { - "globals": { - "localStorage": true - }, "packages": { "@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@metamask/obs-store>through2": { @@ -1362,9 +1469,12 @@ } }, "@metamask/permission-controller": { + "globals": { + "console.error": true + }, "packages": { - "@metamask/base-controller": true, - "@metamask/permission-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, + "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1372,19 +1482,9 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, + "@metamask/permission-controller>@metamask/base-controller": { "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "immer": true } }, "@metamask/permission-controller>nanoid": { @@ -1398,8 +1498,8 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/controller-utils>isomorphic-fetch": true, "@metamask/phishing-controller>@metamask/controller-utils": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } @@ -1411,7 +1511,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1419,6 +1519,25 @@ "ethjs>ethjs-unit": true } }, + "@metamask/phishing-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1426,6 +1545,7 @@ }, "@metamask/post-message-stream": { "globals": { + "MessageEvent.prototype": true, "WorkerGlobalScope": true, "addEventListener": true, "browser": true, @@ -1436,8 +1556,20 @@ "removeEventListener": true }, "packages": { - "@metamask/post-message-stream>readable-stream": true, - "@metamask/utils": true + "@metamask/post-message-stream>@metamask/utils": true, + "@metamask/post-message-stream>readable-stream": true + } + }, + "@metamask/post-message-stream>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/post-message-stream>readable-stream": { @@ -1498,8 +1630,8 @@ "packages": { "@metamask/key-tree": true, "@metamask/key-tree>@noble/hashes": true, + "@metamask/permission-controller": true, "@metamask/rpc-methods>@metamask/browser-passworder": true, - "@metamask/rpc-methods>@metamask/permission-controller": true, "@metamask/rpc-methods>nanoid": true, "@metamask/snaps-ui": true, "@metamask/snaps-utils": true, @@ -1522,41 +1654,19 @@ "browserify>buffer": true } }, - "@metamask/rpc-methods>@metamask/permission-controller": { - "packages": { - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": true, - "@metamask/rpc-methods>nanoid": true, - "deep-freeze-strict": true, - "eth-rpc-errors": true, - "immer": true, - "json-rpc-engine": true - } - }, - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/rpc-methods>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/scure-bip39": { "globals": { "TextEncoder": true @@ -1580,13 +1690,28 @@ "@ethersproject/bignumber": true, "@ethersproject/providers": true, "@metamask/base-controller": true, - "@metamask/controller-utils": true, - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/smart-transactions-controller>@metamask/controllers>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1609,11 +1734,11 @@ "setTimeout": true }, "packages": { + "@metamask/permission-controller": true, "@metamask/post-message-stream": true, "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/permission-controller": true, "@metamask/snaps-controllers>@metamask/subject-metadata-controller": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, @@ -1622,6 +1747,7 @@ "@metamask/snaps-controllers>readable-web-to-node-stream": true, "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -1634,31 +1760,6 @@ "immer": true } }, - "@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/snaps-controllers>@metamask/permission-controller": { - "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": true, - "@metamask/snaps-controllers>nanoid": true, - "deep-freeze-strict": true, - "eth-rpc-errors": true, - "immer": true, - "json-rpc-engine": true - } - }, "@metamask/snaps-controllers>@metamask/subject-metadata-controller": { "packages": { "@metamask/snaps-controllers>@metamask/base-controller": true @@ -1838,6 +1939,13 @@ "semver": true } }, + "@metamask/snaps-utils>@metamask/snaps-registry": { + "packages": { + "@metamask/key-tree>@noble/secp256k1": true, + "@metamask/utils": true, + "@metamask/utils>superstruct": true + } + }, "@metamask/snaps-utils>cron-parser": { "packages": { "browserify>browser-resolve": true, @@ -1877,16 +1985,77 @@ "semver": true } }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": { + "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>case": true, + "browserify": true, + "browserify>buffer": true + } + }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { + "globals": { + "WeakRef": true + }, + "packages": { + "browserify": true + } + }, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": { + "globals": { + "TextDecoder": true, + "crypto": true + }, + "packages": { + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "@metamask/key-tree>@scure/base": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, "@ngraveio/bc-ur>bignumber.js": true, + "@ngraveio/bc-ur>cbor-sync": true, "@ngraveio/bc-ur>crc": true, "@ngraveio/bc-ur>jsbi": true, "addons-linter>sha.js": true, "browserify>assert": true, - "browserify>buffer": true, - "pubnub>cbor-sync": true + "browserify>buffer": true } }, "@ngraveio/bc-ur>assert>object-is": { @@ -1901,6 +2070,14 @@ "define": true } }, + "@ngraveio/bc-ur>cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "browserify>buffer": true + } + }, "@ngraveio/bc-ur>crc": { "packages": { "browserify>buffer": true @@ -2054,24 +2231,6 @@ "define": true } }, - "@spruceid/siwe-parser": { - "globals": { - "console.error": true, - "console.log": true - }, - "packages": { - "@spruceid/siwe-parser>apg-js": true - } - }, - "@spruceid/siwe-parser>apg-js": { - "globals": { - "mode": true - }, - "packages": { - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true - } - }, "@storybook/api>regenerator-runtime": { "globals": { "regeneratorRuntime": "write" @@ -3024,10 +3183,22 @@ "setTimeout": true }, "packages": { - "@metamask/utils": true, + "@metamask/safe-event-emitter": true, + "eth-block-tracker>@metamask/utils": true, "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "eth-query>json-rpc-random-id": true + } + }, + "eth-block-tracker>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "eth-ens-namehash": { @@ -3058,10 +3229,10 @@ "console.error": true }, "packages": { + "@metamask/safe-event-emitter": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "pify": true } }, @@ -3111,61 +3282,12 @@ "console.warn": true }, "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": true, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": true, - "eth-lattice-keyring>@ethereumjs/util>async": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>async": { - "globals": { - "clearTimeout": true, - "console": true, - "define": true, - "queueMicrotask": true, - "setTimeout": true - }, - "packages": { - "browserify>process": true, - "browserify>timers-browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": { - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@scure/base": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true + "browserify>insert-module-globals>is-buffer": true } }, "eth-lattice-keyring>bn.js": { @@ -3366,14 +3488,22 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3493,13 +3623,21 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ganache>secp256k1>elliptic": true } }, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-abi>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3684,14 +3822,22 @@ "ethereumjs-wallet>safe-buffer": true } }, + "ethereumjs-wallet>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-wallet>ethereumjs-util": { "packages": { "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, + "ethereumjs-wallet>ethereum-cryptography": true, "ethereumjs-wallet>ethereumjs-util>ethjs-util": true, "ganache>secp256k1>elliptic": true } @@ -3925,16 +4071,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true + "@metamask/safe-event-emitter": true, + "eth-rpc-errors": true } }, "json-rpc-middleware-stream": { @@ -3943,15 +4081,10 @@ "setTimeout": true }, "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, + "@metamask/safe-event-emitter": true, "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true @@ -4103,32 +4236,6 @@ "console": true } }, - "pubnub": { - "globals": { - "ActiveXObject": true, - "XMLHttpRequest": true, - "addEventListener": true, - "btoa": true, - "clearInterval": true, - "clearTimeout": true, - "console": true, - "define": true, - "localStorage.getItem": true, - "localStorage.setItem": true, - "location": true, - "navigator": true, - "setInterval": true, - "setTimeout": true - } - }, - "pubnub>cbor-sync": { - "globals": { - "define": true - }, - "packages": { - "browserify>buffer": true - } - }, "pump": { "packages": { "browserify>browser-resolve": true, @@ -4659,15 +4766,6 @@ "@babel/runtime": true } }, - "safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>util": true, - "webpack>events": true - } - }, "semver": { "globals": { "console.error": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 00325bca9..3c9a08e54 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -469,8 +469,8 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { @@ -661,7 +661,7 @@ "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/controller-utils": true + "@metamask/address-book-controller>@metamask/controller-utils": true } }, "@metamask/address-book-controller>@metamask/base-controller": { @@ -669,6 +669,21 @@ "immer": true } }, + "@metamask/address-book-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/announcement-controller": { "packages": { "@metamask/announcement-controller>@metamask/base-controller": true @@ -681,11 +696,16 @@ }, "@metamask/approval-controller": { "packages": { + "@metamask/approval-controller>@metamask/base-controller": true, "@metamask/approval-controller>nanoid": true, - "@metamask/base-controller": true, "eth-rpc-errors": true } }, + "@metamask/approval-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -707,12 +727,12 @@ "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/controller-utils": true, + "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/metamask-eth-abis": true, - "@metamask/utils": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, @@ -724,10 +744,22 @@ }, "@metamask/assets-controllers>@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>superstruct": true } }, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>@metamask/controller-utils": { "globals": { "console.error": true, @@ -735,7 +767,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -743,6 +775,18 @@ "ethjs>ethjs-unit": true } }, + "@metamask/assets-controllers>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>abort-controller": { "globals": { "AbortController": true @@ -767,7 +811,8 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/controller-utils>@metamask/utils": true, + "@metamask/controller-utils>@spruceid/siwe-parser": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -775,23 +820,34 @@ "ethjs>ethjs-unit": true } }, - "@metamask/controller-utils>isomorphic-fetch": { + "@metamask/controller-utils>@metamask/utils": { "globals": { - "fetch.bind": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": true + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": { + "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true + "console.error": true, + "console.log": true + }, + "packages": { + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": true + } + }, + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": { + "globals": { + "mode": true + }, + "packages": { + "browserify>buffer": true, + "browserify>insert-module-globals>is-buffer": true } }, "@metamask/controllers>nanoid": { @@ -833,9 +889,9 @@ "setTimeout": true }, "packages": { + "@metamask/desktop>@metamask/obs-store": true, "@metamask/desktop>eciesjs": true, "@metamask/desktop>otpauth": true, - "@metamask/obs-store": true, "browserify>buffer": true, "browserify>events": true, "browserify>process": true, @@ -848,6 +904,24 @@ "webextension-polyfill": true } }, + "@metamask/desktop>@metamask/obs-store": { + "globals": { + "localStorage": true + }, + "packages": { + "@metamask/desktop>@metamask/obs-store>through2": true, + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true + } + }, + "@metamask/desktop>@metamask/obs-store>through2": { + "packages": { + "browserify>process": true, + "browserify>util": true, + "readable-stream": true, + "watchify>xtend": true + } + }, "@metamask/desktop>eciesjs": { "packages": { "@metamask/desktop>eciesjs>futoin-hkdf": true, @@ -873,13 +947,25 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true } }, + "@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { "globals": { "URL": true, @@ -889,62 +975,51 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, + "@metamask/safe-event-emitter": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/utils": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -960,33 +1035,17 @@ "TextEncoder": true }, "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "@metamask/scure-bip39": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": { "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true, @@ -994,21 +1053,6 @@ "eth-sig-util>tweetnacl-util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -1019,31 +1063,16 @@ "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, "eth-lattice-keyring>@ethereumjs/util": true, "ethereumjs-wallet>randombytes": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>obs-store": { "packages": { - "safe-event-emitter": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "watchify>xtend": true } }, @@ -1076,17 +1105,25 @@ }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": { "packages": { + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -1123,9 +1160,9 @@ "@metamask/eth-token-tracker>eth-block-tracker": true, "@metamask/eth-token-tracker>ethjs": true, "@metamask/eth-token-tracker>human-standard-token-abi": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "ethjs-contract": true, - "ethjs-query": true, - "safe-event-emitter": true + "ethjs-query": true } }, "@metamask/eth-token-tracker>deep-equal": { @@ -1157,8 +1194,8 @@ }, "packages": { "@metamask/eth-token-tracker>eth-block-tracker>pify": true, - "eth-query": true, - "safe-event-emitter": true + "@metamask/eth-token-tracker>safe-event-emitter": true, + "eth-query": true } }, "@metamask/eth-token-tracker>ethjs": { @@ -1217,6 +1254,15 @@ "promise-to-callback": true } }, + "@metamask/eth-token-tracker>safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>util": true, + "webpack>events": true + } + }, "@metamask/etherscan-link": { "globals": { "URL": true @@ -1229,8 +1275,8 @@ "setInterval": true }, "packages": { - "@metamask/controller-utils": true, "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, @@ -1242,6 +1288,21 @@ "immer": true } }, + "@metamask/gas-fee-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1276,12 +1337,24 @@ }, "@metamask/key-tree": { "packages": { + "@metamask/key-tree>@metamask/utils": true, "@metamask/key-tree>@noble/ed25519": true, "@metamask/key-tree>@noble/hashes": true, "@metamask/key-tree>@noble/secp256k1": true, "@metamask/key-tree>@scure/base": true, - "@metamask/scure-bip39": true, - "@metamask/utils": true + "@metamask/scure-bip39": true + } + }, + "@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/key-tree>@noble/ed25519": { @@ -1326,10 +1399,32 @@ "@metamask/logo>gl-vec3": true } }, - "@metamask/notification-controller": { + "@metamask/message-manager": { "packages": { "@metamask/controller-utils": true, + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, + "@metamask/notification-controller": { + "packages": { "@metamask/notification-controller>@metamask/base-controller": true, + "@metamask/notification-controller>@metamask/controller-utils": true, "@metamask/notification-controller>nanoid": true } }, @@ -1338,19 +1433,31 @@ "immer": true } }, + "@metamask/notification-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, "@metamask/obs-store": { - "globals": { - "localStorage": true - }, "packages": { "@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@metamask/obs-store>through2": { @@ -1362,9 +1469,12 @@ } }, "@metamask/permission-controller": { + "globals": { + "console.error": true + }, "packages": { - "@metamask/base-controller": true, - "@metamask/permission-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, + "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1372,19 +1482,9 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, + "@metamask/permission-controller>@metamask/base-controller": { "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "immer": true } }, "@metamask/permission-controller>nanoid": { @@ -1398,8 +1498,8 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/controller-utils>isomorphic-fetch": true, "@metamask/phishing-controller>@metamask/controller-utils": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } @@ -1411,7 +1511,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1419,6 +1519,25 @@ "ethjs>ethjs-unit": true } }, + "@metamask/phishing-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1426,6 +1545,7 @@ }, "@metamask/post-message-stream": { "globals": { + "MessageEvent.prototype": true, "WorkerGlobalScope": true, "addEventListener": true, "browser": true, @@ -1436,8 +1556,20 @@ "removeEventListener": true }, "packages": { - "@metamask/post-message-stream>readable-stream": true, - "@metamask/utils": true + "@metamask/post-message-stream>@metamask/utils": true, + "@metamask/post-message-stream>readable-stream": true + } + }, + "@metamask/post-message-stream>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/post-message-stream>readable-stream": { @@ -1498,8 +1630,8 @@ "packages": { "@metamask/key-tree": true, "@metamask/key-tree>@noble/hashes": true, + "@metamask/permission-controller": true, "@metamask/rpc-methods>@metamask/browser-passworder": true, - "@metamask/rpc-methods>@metamask/permission-controller": true, "@metamask/rpc-methods>nanoid": true, "@metamask/snaps-ui": true, "@metamask/snaps-utils": true, @@ -1522,41 +1654,19 @@ "browserify>buffer": true } }, - "@metamask/rpc-methods>@metamask/permission-controller": { - "packages": { - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": true, - "@metamask/rpc-methods>nanoid": true, - "deep-freeze-strict": true, - "eth-rpc-errors": true, - "immer": true, - "json-rpc-engine": true - } - }, - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/rpc-methods>nanoid": { "globals": { "crypto.getRandomValues": true } }, + "@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/scure-bip39": { "globals": { "TextEncoder": true @@ -1580,13 +1690,28 @@ "@ethersproject/bignumber": true, "@ethersproject/providers": true, "@metamask/base-controller": true, - "@metamask/controller-utils": true, - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/smart-transactions-controller>@metamask/controllers>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1609,11 +1734,11 @@ "setTimeout": true }, "packages": { + "@metamask/permission-controller": true, "@metamask/post-message-stream": true, "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/permission-controller": true, "@metamask/snaps-controllers>@metamask/subject-metadata-controller": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, @@ -1622,6 +1747,7 @@ "@metamask/snaps-controllers>readable-web-to-node-stream": true, "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, + "@metamask/snaps-utils>@metamask/snaps-registry": true, "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -1634,31 +1760,6 @@ "immer": true } }, - "@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/snaps-controllers>@metamask/permission-controller": { - "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": true, - "@metamask/snaps-controllers>nanoid": true, - "deep-freeze-strict": true, - "eth-rpc-errors": true, - "immer": true, - "json-rpc-engine": true - } - }, "@metamask/snaps-controllers>@metamask/subject-metadata-controller": { "packages": { "@metamask/snaps-controllers>@metamask/base-controller": true @@ -1838,6 +1939,13 @@ "semver": true } }, + "@metamask/snaps-utils>@metamask/snaps-registry": { + "packages": { + "@metamask/key-tree>@noble/secp256k1": true, + "@metamask/utils": true, + "@metamask/utils>superstruct": true + } + }, "@metamask/snaps-utils>cron-parser": { "packages": { "browserify>browser-resolve": true, @@ -1877,16 +1985,77 @@ "semver": true } }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": { + "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>case": true, + "browserify": true, + "browserify>buffer": true + } + }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { + "globals": { + "WeakRef": true + }, + "packages": { + "browserify": true + } + }, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": { + "globals": { + "TextDecoder": true, + "crypto": true + }, + "packages": { + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "@metamask/key-tree>@scure/base": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, "@ngraveio/bc-ur>bignumber.js": true, + "@ngraveio/bc-ur>cbor-sync": true, "@ngraveio/bc-ur>crc": true, "@ngraveio/bc-ur>jsbi": true, "addons-linter>sha.js": true, "browserify>assert": true, - "browserify>buffer": true, - "pubnub>cbor-sync": true + "browserify>buffer": true } }, "@ngraveio/bc-ur>assert>object-is": { @@ -1901,6 +2070,14 @@ "define": true } }, + "@ngraveio/bc-ur>cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "browserify>buffer": true + } + }, "@ngraveio/bc-ur>crc": { "packages": { "browserify>buffer": true @@ -2054,24 +2231,6 @@ "define": true } }, - "@spruceid/siwe-parser": { - "globals": { - "console.error": true, - "console.log": true - }, - "packages": { - "@spruceid/siwe-parser>apg-js": true - } - }, - "@spruceid/siwe-parser>apg-js": { - "globals": { - "mode": true - }, - "packages": { - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true - } - }, "@storybook/api>regenerator-runtime": { "globals": { "regeneratorRuntime": "write" @@ -3024,10 +3183,22 @@ "setTimeout": true }, "packages": { - "@metamask/utils": true, + "@metamask/safe-event-emitter": true, + "eth-block-tracker>@metamask/utils": true, "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "eth-query>json-rpc-random-id": true + } + }, + "eth-block-tracker>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "eth-ens-namehash": { @@ -3058,10 +3229,10 @@ "console.error": true }, "packages": { + "@metamask/safe-event-emitter": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "pify": true } }, @@ -3111,61 +3282,12 @@ "console.warn": true }, "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": true, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": true, - "eth-lattice-keyring>@ethereumjs/util>async": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>async": { - "globals": { - "clearTimeout": true, - "console": true, - "define": true, - "queueMicrotask": true, - "setTimeout": true - }, - "packages": { - "browserify>process": true, - "browserify>timers-browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": { - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@scure/base": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true + "browserify>insert-module-globals>is-buffer": true } }, "eth-lattice-keyring>bn.js": { @@ -3366,14 +3488,22 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3493,13 +3623,21 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ganache>secp256k1>elliptic": true } }, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-abi>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3684,14 +3822,22 @@ "ethereumjs-wallet>safe-buffer": true } }, + "ethereumjs-wallet>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-wallet>ethereumjs-util": { "packages": { "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, + "ethereumjs-wallet>ethereum-cryptography": true, "ethereumjs-wallet>ethereumjs-util>ethjs-util": true, "ganache>secp256k1>elliptic": true } @@ -3925,16 +4071,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true + "@metamask/safe-event-emitter": true, + "eth-rpc-errors": true } }, "json-rpc-middleware-stream": { @@ -3943,15 +4081,10 @@ "setTimeout": true }, "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, + "@metamask/safe-event-emitter": true, "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true @@ -4103,32 +4236,6 @@ "console": true } }, - "pubnub": { - "globals": { - "ActiveXObject": true, - "XMLHttpRequest": true, - "addEventListener": true, - "btoa": true, - "clearInterval": true, - "clearTimeout": true, - "console": true, - "define": true, - "localStorage.getItem": true, - "localStorage.setItem": true, - "location": true, - "navigator": true, - "setInterval": true, - "setTimeout": true - } - }, - "pubnub>cbor-sync": { - "globals": { - "define": true - }, - "packages": { - "browserify>buffer": true - } - }, "pump": { "packages": { "browserify>browser-resolve": true, @@ -4659,15 +4766,6 @@ "@babel/runtime": true } }, - "safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>util": true, - "webpack>events": true - } - }, "semver": { "globals": { "console.error": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 5cf58f0f3..37ab828c8 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -469,8 +469,8 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { @@ -661,7 +661,7 @@ "@metamask/address-book-controller": { "packages": { "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/controller-utils": true + "@metamask/address-book-controller>@metamask/controller-utils": true } }, "@metamask/address-book-controller>@metamask/base-controller": { @@ -669,6 +669,21 @@ "immer": true } }, + "@metamask/address-book-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/announcement-controller": { "packages": { "@metamask/announcement-controller>@metamask/base-controller": true @@ -681,11 +696,16 @@ }, "@metamask/approval-controller": { "packages": { + "@metamask/approval-controller>@metamask/base-controller": true, "@metamask/approval-controller>nanoid": true, - "@metamask/base-controller": true, "eth-rpc-errors": true } }, + "@metamask/approval-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -707,12 +727,12 @@ "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/controller-utils": true, + "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/metamask-eth-abis": true, - "@metamask/utils": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, @@ -724,10 +744,22 @@ }, "@metamask/assets-controllers>@metamask/abi-utils": { "packages": { - "@metamask/utils": true, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": true, "@metamask/utils>superstruct": true } }, + "@metamask/assets-controllers>@metamask/abi-utils>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>@metamask/controller-utils": { "globals": { "console.error": true, @@ -735,7 +767,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -743,6 +775,18 @@ "ethjs>ethjs-unit": true } }, + "@metamask/assets-controllers>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/assets-controllers>abort-controller": { "globals": { "AbortController": true @@ -767,7 +811,8 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/controller-utils>@metamask/utils": true, + "@metamask/controller-utils>@spruceid/siwe-parser": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -775,23 +820,34 @@ "ethjs>ethjs-unit": true } }, - "@metamask/controller-utils>isomorphic-fetch": { + "@metamask/controller-utils>@metamask/utils": { "globals": { - "fetch.bind": true + "TextDecoder": true, + "TextEncoder": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": true + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": { + "@metamask/controller-utils>@spruceid/siwe-parser": { "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true + "console.error": true, + "console.log": true + }, + "packages": { + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": true + } + }, + "@metamask/controller-utils>@spruceid/siwe-parser>apg-js": { + "globals": { + "mode": true + }, + "packages": { + "browserify>buffer": true, + "browserify>insert-module-globals>is-buffer": true } }, "@metamask/controllers>nanoid": { @@ -819,13 +875,25 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, "node-fetch": true } }, + "@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { "globals": { "URL": true, @@ -835,62 +903,51 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, + "@metamask/safe-event-emitter": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/utils": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -906,33 +963,17 @@ "TextEncoder": true }, "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "@metamask/scure-bip39": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": { "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "bn.js": true, "browserify>buffer": true, "eth-lattice-keyring>@ethereumjs/util": true, @@ -940,21 +981,6 @@ "eth-sig-util>tweetnacl-util": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -965,31 +991,16 @@ "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, "eth-lattice-keyring>@ethereumjs/util": true, "ethereumjs-wallet>randombytes": true } }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, "@metamask/eth-keyring-controller>obs-store": { "packages": { - "safe-event-emitter": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "watchify>xtend": true } }, @@ -1022,17 +1033,25 @@ }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": { "packages": { + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -1069,9 +1088,9 @@ "@metamask/eth-token-tracker>eth-block-tracker": true, "@metamask/eth-token-tracker>ethjs": true, "@metamask/eth-token-tracker>human-standard-token-abi": true, + "@metamask/eth-token-tracker>safe-event-emitter": true, "ethjs-contract": true, - "ethjs-query": true, - "safe-event-emitter": true + "ethjs-query": true } }, "@metamask/eth-token-tracker>deep-equal": { @@ -1103,8 +1122,8 @@ }, "packages": { "@metamask/eth-token-tracker>eth-block-tracker>pify": true, - "eth-query": true, - "safe-event-emitter": true + "@metamask/eth-token-tracker>safe-event-emitter": true, + "eth-query": true } }, "@metamask/eth-token-tracker>ethjs": { @@ -1163,6 +1182,15 @@ "promise-to-callback": true } }, + "@metamask/eth-token-tracker>safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>util": true, + "webpack>events": true + } + }, "@metamask/etherscan-link": { "globals": { "URL": true @@ -1175,8 +1203,8 @@ "setInterval": true }, "packages": { - "@metamask/controller-utils": true, "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, @@ -1188,6 +1216,21 @@ "immer": true } }, + "@metamask/gas-fee-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1222,12 +1265,24 @@ }, "@metamask/key-tree": { "packages": { + "@metamask/key-tree>@metamask/utils": true, "@metamask/key-tree>@noble/ed25519": true, "@metamask/key-tree>@noble/hashes": true, "@metamask/key-tree>@noble/secp256k1": true, "@metamask/key-tree>@scure/base": true, - "@metamask/scure-bip39": true, - "@metamask/utils": true + "@metamask/scure-bip39": true + } + }, + "@metamask/key-tree>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "@metamask/key-tree>@noble/ed25519": { @@ -1272,19 +1327,38 @@ "@metamask/logo>gl-vec3": true } }, + "@metamask/message-manager": { + "packages": { + "@metamask/controller-utils": true, + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true } }, "@metamask/obs-store": { - "globals": { - "localStorage": true - }, "packages": { "@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "@metamask/safe-event-emitter": true, + "browserify>stream-browserify": true } }, "@metamask/obs-store>through2": { @@ -1296,9 +1370,12 @@ } }, "@metamask/permission-controller": { + "globals": { + "console.error": true + }, "packages": { - "@metamask/base-controller": true, - "@metamask/permission-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, + "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1306,19 +1383,9 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, + "@metamask/permission-controller>@metamask/base-controller": { "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "immer": true } }, "@metamask/permission-controller>nanoid": { @@ -1332,8 +1399,8 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/controller-utils>isomorphic-fetch": true, "@metamask/phishing-controller>@metamask/controller-utils": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } @@ -1345,7 +1412,7 @@ "setTimeout": true }, "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1353,6 +1420,25 @@ "ethjs>ethjs-unit": true } }, + "@metamask/phishing-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1385,6 +1471,14 @@ "crypto.getRandomValues": true } }, + "@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/scure-bip39": { "globals": { "TextEncoder": true @@ -1408,13 +1502,28 @@ "@ethersproject/bignumber": true, "@ethersproject/providers": true, "@metamask/base-controller": true, - "@metamask/controller-utils": true, - "@metamask/controller-utils>isomorphic-fetch": true, + "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/phishing-controller>isomorphic-fetch": true, + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, "@metamask/smart-transactions-controller>@metamask/controllers>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1448,16 +1557,77 @@ "semver": true } }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": { + "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": true, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>case": true, + "browserify": true, + "browserify>buffer": true + } + }, + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz>@chainsafe/persistent-merkle-tree": { + "globals": { + "WeakRef": true + }, + "packages": { + "browserify": true + } + }, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": { + "globals": { + "TextEncoder": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": { + "globals": { + "TextDecoder": true, + "crypto": true + }, + "packages": { + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/hashes": { + "globals": { + "TextEncoder": true, + "crypto": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32": { + "packages": { + "@metamask/key-tree>@noble/hashes": true, + "@metamask/key-tree>@scure/base": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true + } + }, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": { + "globals": { + "crypto": true + }, + "packages": { + "browserify>browser-resolve": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@apocentre/alias-sampling": true, "@ngraveio/bc-ur>bignumber.js": true, + "@ngraveio/bc-ur>cbor-sync": true, "@ngraveio/bc-ur>crc": true, "@ngraveio/bc-ur>jsbi": true, "addons-linter>sha.js": true, "browserify>assert": true, - "browserify>buffer": true, - "pubnub>cbor-sync": true + "browserify>buffer": true } }, "@ngraveio/bc-ur>assert>object-is": { @@ -1472,6 +1642,14 @@ "define": true } }, + "@ngraveio/bc-ur>cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "browserify>buffer": true + } + }, "@ngraveio/bc-ur>crc": { "packages": { "browserify>buffer": true @@ -1625,24 +1803,6 @@ "define": true } }, - "@spruceid/siwe-parser": { - "globals": { - "console.error": true, - "console.log": true - }, - "packages": { - "@spruceid/siwe-parser>apg-js": true - } - }, - "@spruceid/siwe-parser>apg-js": { - "globals": { - "mode": true - }, - "packages": { - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true - } - }, "@storybook/api>regenerator-runtime": { "globals": { "regeneratorRuntime": "write" @@ -2595,10 +2755,22 @@ "setTimeout": true }, "packages": { - "@metamask/utils": true, + "@metamask/safe-event-emitter": true, + "eth-block-tracker>@metamask/utils": true, "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "eth-query>json-rpc-random-id": true + } + }, + "eth-block-tracker>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>superstruct": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true } }, "eth-ens-namehash": { @@ -2629,10 +2801,10 @@ "console.error": true }, "packages": { + "@metamask/safe-event-emitter": true, "eth-json-rpc-filters>async-mutex": true, "eth-query": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, "pify": true } }, @@ -2682,61 +2854,12 @@ "console.warn": true }, "packages": { + "@metamask/utils>@ethereumjs/tx>@chainsafe/ssz": true, + "@metamask/utils>@ethereumjs/tx>@ethereumjs/rlp": true, + "@metamask/utils>@ethereumjs/tx>ethereum-cryptography": true, "browserify>buffer": true, "browserify>events": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": true, - "eth-lattice-keyring>@ethereumjs/util>async": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>async": { - "globals": { - "clearTimeout": true, - "console": true, - "define": true, - "queueMicrotask": true, - "setTimeout": true - }, - "packages": { - "browserify>process": true, - "browserify>timers-browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": { - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@scure/base": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true + "browserify>insert-module-globals>is-buffer": true } }, "eth-lattice-keyring>bn.js": { @@ -2937,14 +3060,22 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": true, "eth-sig-util>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ethereumjs-wallet>safe-buffer": true, "ganache>secp256k1>elliptic": true } }, + "eth-sig-util>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "eth-sig-util>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3064,13 +3195,21 @@ "bn.js": true, "browserify>assert": true, "browserify>buffer": true, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": true, "ethereumjs-abi>ethereumjs-util>ethjs-util": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, "ganache>secp256k1>elliptic": true } }, + "ethereumjs-abi>ethereumjs-util>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-abi>ethereumjs-util>ethjs-util": { "packages": { "browserify>buffer": true, @@ -3255,14 +3394,22 @@ "ethereumjs-wallet>safe-buffer": true } }, + "ethereumjs-wallet>ethereum-cryptography": { + "packages": { + "browserify>buffer": true, + "ethereumjs-util>ethereum-cryptography>keccak": true, + "ethereumjs-util>ethereum-cryptography>secp256k1": true, + "ethereumjs-wallet>randombytes": true + } + }, "ethereumjs-wallet>ethereumjs-util": { "packages": { "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, "ethereumjs-util>rlp": true, + "ethereumjs-wallet>ethereum-cryptography": true, "ethereumjs-wallet>ethereumjs-util>ethjs-util": true, "ganache>secp256k1>elliptic": true } @@ -3496,16 +3643,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true + "@metamask/safe-event-emitter": true, + "eth-rpc-errors": true } }, "json-rpc-middleware-stream": { @@ -3514,15 +3653,10 @@ "setTimeout": true }, "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, + "@metamask/safe-event-emitter": true, "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true @@ -3656,32 +3790,6 @@ "console": true } }, - "pubnub": { - "globals": { - "ActiveXObject": true, - "XMLHttpRequest": true, - "addEventListener": true, - "btoa": true, - "clearInterval": true, - "clearTimeout": true, - "console": true, - "define": true, - "localStorage.getItem": true, - "localStorage.setItem": true, - "location": true, - "navigator": true, - "setInterval": true, - "setTimeout": true - } - }, - "pubnub>cbor-sync": { - "globals": { - "define": true - }, - "packages": { - "browserify>buffer": true - } - }, "pump": { "packages": { "browserify>browser-resolve": true, @@ -4098,15 +4206,6 @@ "@babel/runtime": true } }, - "safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>util": true, - "webpack>events": true - } - }, "semver": { "globals": { "console.error": true diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json deleted file mode 100644 index 5cf58f0f3..000000000 --- a/lavamoat/browserify/mmi/policy.json +++ /dev/null @@ -1,4206 +0,0 @@ -{ - "resources": { - "@babel/runtime": { - "globals": { - "regeneratorRuntime": "write" - } - }, - "@download/blockies": { - "globals": { - "document.createElement": true - } - }, - "@ensdomains/content-hash": { - "globals": { - "console.warn": true - }, - "packages": { - "@ensdomains/content-hash>cids": true, - "@ensdomains/content-hash>js-base64": true, - "@ensdomains/content-hash>multicodec": true, - "@ensdomains/content-hash>multihashes": true, - "browserify>buffer": true - } - }, - "@ensdomains/content-hash>cids": { - "packages": { - "@ensdomains/content-hash>cids>multibase": true, - "@ensdomains/content-hash>cids>multicodec": true, - "@ensdomains/content-hash>cids>multihashes": true, - "@ensdomains/content-hash>cids>uint8arrays": true - } - }, - "@ensdomains/content-hash>cids>multibase": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@ensdomains/content-hash>cids>multibase>@multiformats/base-x": true - } - }, - "@ensdomains/content-hash>cids>multicodec": { - "packages": { - "@ensdomains/content-hash>cids>multicodec>varint": true, - "@ensdomains/content-hash>cids>uint8arrays": true - } - }, - "@ensdomains/content-hash>cids>multihashes": { - "packages": { - "@ensdomains/content-hash>cids>multibase": true, - "@ensdomains/content-hash>cids>uint8arrays": true, - "@ensdomains/content-hash>multihashes>varint": true - } - }, - "@ensdomains/content-hash>cids>uint8arrays": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@ensdomains/content-hash>cids>multibase": true - } - }, - "@ensdomains/content-hash>js-base64": { - "globals": { - "Base64": "write", - "TextDecoder": true, - "TextEncoder": true, - "atob": true, - "btoa": true, - "define": true - }, - "packages": { - "browserify>buffer": true - } - }, - "@ensdomains/content-hash>multicodec": { - "packages": { - "@ensdomains/content-hash>multicodec>uint8arrays": true, - "@ensdomains/content-hash>multicodec>varint": true - } - }, - "@ensdomains/content-hash>multicodec>uint8arrays": { - "packages": { - "@ensdomains/content-hash>multicodec>uint8arrays>multibase": true, - "@ensdomains/content-hash>multihashes>web-encoding": true - } - }, - "@ensdomains/content-hash>multicodec>uint8arrays>multibase": { - "packages": { - "@ensdomains/content-hash>cids>multibase>@multiformats/base-x": true, - "@ensdomains/content-hash>multihashes>web-encoding": true - } - }, - "@ensdomains/content-hash>multihashes": { - "packages": { - "@ensdomains/content-hash>multihashes>multibase": true, - "@ensdomains/content-hash>multihashes>varint": true, - "@ensdomains/content-hash>multihashes>web-encoding": true, - "browserify>buffer": true - } - }, - "@ensdomains/content-hash>multihashes>multibase": { - "packages": { - "@ensdomains/content-hash>multihashes>web-encoding": true, - "browserify>buffer": true, - "ethereumjs-wallet>bs58check>bs58>base-x": true - } - }, - "@ensdomains/content-hash>multihashes>web-encoding": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "browserify>util": true - } - }, - "@ethereumjs/common": { - "packages": { - "@ethereumjs/common>crc-32": true, - "browserify>buffer": true, - "browserify>events": true, - "ethereumjs-util": true - } - }, - "@ethereumjs/common>crc-32": { - "globals": { - "DO_NOT_EXPORT_CRC": true, - "define": true - } - }, - "@ethereumjs/tx": { - "packages": { - "@ethereumjs/common": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util": true - } - }, - "@ethersproject/abi": { - "globals": { - "console.log": true - }, - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/constants": true, - "@ethersproject/abi>@ethersproject/hash": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/bignumber": true - } - }, - "@ethersproject/abi>@ethersproject/address": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/bignumber": true, - "@ethersproject/providers>@ethersproject/rlp": true - } - }, - "@ethersproject/abi>@ethersproject/bytes": { - "packages": { - "@ethersproject/abi>@ethersproject/logger": true - } - }, - "@ethersproject/abi>@ethersproject/constants": { - "packages": { - "@ethersproject/bignumber": true - } - }, - "@ethersproject/abi>@ethersproject/hash": { - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/bignumber": true, - "@ethersproject/providers>@ethersproject/base64": true - } - }, - "@ethersproject/abi>@ethersproject/keccak256": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/keccak256>js-sha3": true - } - }, - "@ethersproject/abi>@ethersproject/keccak256>js-sha3": { - "globals": { - "define": true - }, - "packages": { - "browserify>process": true - } - }, - "@ethersproject/abi>@ethersproject/logger": { - "globals": { - "console": true - } - }, - "@ethersproject/abi>@ethersproject/properties": { - "packages": { - "@ethersproject/abi>@ethersproject/logger": true - } - }, - "@ethersproject/abi>@ethersproject/strings": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/constants": true, - "@ethersproject/abi>@ethersproject/logger": true - } - }, - "@ethersproject/bignumber": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/bignumber>bn.js": true - } - }, - "@ethersproject/bignumber>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "@ethersproject/contracts": { - "globals": { - "setTimeout": true - }, - "packages": { - "@ethersproject/abi": true, - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/bignumber": true, - "@ethersproject/hdnode>@ethersproject/abstract-signer": true, - "@ethersproject/hdnode>@ethersproject/transactions": true, - "@ethersproject/providers>@ethersproject/abstract-provider": true - } - }, - "@ethersproject/hdnode": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/bignumber": true, - "@ethersproject/hdnode>@ethersproject/basex": true, - "@ethersproject/hdnode>@ethersproject/pbkdf2": true, - "@ethersproject/hdnode>@ethersproject/sha2": true, - "@ethersproject/hdnode>@ethersproject/signing-key": true, - "@ethersproject/hdnode>@ethersproject/transactions": true, - "@ethersproject/hdnode>@ethersproject/wordlists": true - } - }, - "@ethersproject/hdnode>@ethersproject/abstract-signer": { - "packages": { - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true - } - }, - "@ethersproject/hdnode>@ethersproject/basex": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/properties": true - } - }, - "@ethersproject/hdnode>@ethersproject/pbkdf2": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/hdnode>@ethersproject/sha2": true - } - }, - "@ethersproject/hdnode>@ethersproject/sha2": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "ethereumjs-util>ethereum-cryptography>hash.js": true - } - }, - "@ethersproject/hdnode>@ethersproject/signing-key": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "ganache>secp256k1>elliptic": true - } - }, - "@ethersproject/hdnode>@ethersproject/transactions": { - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/constants": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/bignumber": true, - "@ethersproject/hdnode>@ethersproject/signing-key": true, - "@ethersproject/providers>@ethersproject/rlp": true - } - }, - "@ethersproject/hdnode>@ethersproject/wordlists": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/hash": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true - } - }, - "@ethersproject/providers": { - "globals": { - "WebSocket": true, - "clearInterval": true, - "clearTimeout": true, - "console.log": true, - "console.warn": true, - "setInterval": true, - "setTimeout": true - }, - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/constants": true, - "@ethersproject/abi>@ethersproject/hash": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/bignumber": true, - "@ethersproject/hdnode>@ethersproject/abstract-signer": true, - "@ethersproject/hdnode>@ethersproject/basex": true, - "@ethersproject/hdnode>@ethersproject/sha2": true, - "@ethersproject/hdnode>@ethersproject/transactions": true, - "@ethersproject/providers>@ethersproject/abstract-provider": true, - "@ethersproject/providers>@ethersproject/base64": true, - "@ethersproject/providers>@ethersproject/networks": true, - "@ethersproject/providers>@ethersproject/random": true, - "@ethersproject/providers>@ethersproject/web": true, - "@ethersproject/providers>bech32": true - } - }, - "@ethersproject/providers>@ethersproject/abstract-provider": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/bignumber": true - } - }, - "@ethersproject/providers>@ethersproject/base64": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true - } - }, - "@ethersproject/providers>@ethersproject/networks": { - "packages": { - "@ethersproject/abi>@ethersproject/logger": true - } - }, - "@ethersproject/providers>@ethersproject/random": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true - } - }, - "@ethersproject/providers>@ethersproject/rlp": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true - } - }, - "@ethersproject/providers>@ethersproject/web": { - "globals": { - "clearTimeout": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/providers>@ethersproject/base64": true - } - }, - "@formatjs/intl-relativetimeformat": { - "globals": { - "Intl": true - }, - "packages": { - "@formatjs/intl-relativetimeformat>@formatjs/intl-utils": true - } - }, - "@formatjs/intl-relativetimeformat>@formatjs/intl-utils": { - "globals": { - "Intl.getCanonicalLocales": true - } - }, - "@keystonehq/bc-ur-registry-eth": { - "packages": { - "@keystonehq/bc-ur-registry-eth>@keystonehq/bc-ur-registry": true, - "@keystonehq/bc-ur-registry-eth>hdkey": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/util": true, - "uuid": true - } - }, - "@keystonehq/bc-ur-registry-eth>@keystonehq/bc-ur-registry": { - "globals": { - "define": true - }, - "packages": { - "@ngraveio/bc-ur": true, - "browserify>buffer": true, - "ethereumjs-wallet>bs58check": true, - "wait-on>rxjs>tslib": true - } - }, - "@keystonehq/bc-ur-registry-eth>hdkey": { - "packages": { - "browserify>assert": true, - "browserify>crypto-browserify": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>bs58check": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "@keystonehq/metamask-airgapped-keyring": { - "packages": { - "@ethereumjs/tx": true, - "@keystonehq/bc-ur-registry-eth": true, - "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, - "browserify>buffer": true, - "browserify>events": true, - "ethereumjs-util>rlp": true, - "uuid": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": { - "packages": { - "@ethereumjs/tx": true, - "@keystonehq/bc-ur-registry-eth": true, - "@keystonehq/bc-ur-registry-eth>hdkey": true, - "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/util": true, - "uuid": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": { - "globals": { - "TextEncoder": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { - "packages": { - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2": { - "packages": { - "browserify>process": true, - "browserify>util": true, - "readable-stream": true, - "watchify>xtend": true - } - }, - "@material-ui/core": { - "globals": { - "Image": true, - "_formatMuiErrorMessage": true, - "addEventListener": true, - "clearInterval": true, - "clearTimeout": true, - "console.error": true, - "console.warn": true, - "document": true, - "getComputedStyle": true, - "getSelection": true, - "innerHeight": true, - "innerWidth": true, - "matchMedia": true, - "navigator": true, - "performance.now": true, - "removeEventListener": true, - "requestAnimationFrame": true, - "setInterval": true, - "setTimeout": true - }, - "packages": { - "@babel/runtime": true, - "@material-ui/core>@material-ui/styles": true, - "@material-ui/core>@material-ui/system": true, - "@material-ui/core>@material-ui/utils": true, - "@material-ui/core>clsx": true, - "@material-ui/core>popper.js": true, - "@material-ui/core>react-transition-group": true, - "prop-types": true, - "prop-types>react-is": true, - "react": true, - "react-dom": true, - "react-redux>hoist-non-react-statics": true - } - }, - "@material-ui/core>@material-ui/styles": { - "globals": { - "console.error": true, - "console.warn": true, - "document.createComment": true, - "document.head": true - }, - "packages": { - "@babel/runtime": true, - "@material-ui/core>@material-ui/styles>jss": true, - "@material-ui/core>@material-ui/styles>jss-plugin-camel-case": true, - "@material-ui/core>@material-ui/styles>jss-plugin-default-unit": true, - "@material-ui/core>@material-ui/styles>jss-plugin-global": true, - "@material-ui/core>@material-ui/styles>jss-plugin-nested": true, - "@material-ui/core>@material-ui/styles>jss-plugin-props-sort": true, - "@material-ui/core>@material-ui/styles>jss-plugin-rule-value-function": true, - "@material-ui/core>@material-ui/styles>jss-plugin-vendor-prefixer": true, - "@material-ui/core>@material-ui/utils": true, - "@material-ui/core>clsx": true, - "prop-types": true, - "react": true, - "react-redux>hoist-non-react-statics": true - } - }, - "@material-ui/core>@material-ui/styles>jss": { - "globals": { - "CSS": true, - "document.createElement": true, - "document.querySelector": true - }, - "packages": { - "@babel/runtime": true, - "@material-ui/core>@material-ui/styles>jss>is-in-browser": true, - "react-router-dom>tiny-warning": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-camel-case": { - "packages": { - "@material-ui/core>@material-ui/styles>jss-plugin-camel-case>hyphenate-style-name": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-default-unit": { - "globals": { - "CSS": true - }, - "packages": { - "@material-ui/core>@material-ui/styles>jss": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-global": { - "packages": { - "@babel/runtime": true, - "@material-ui/core>@material-ui/styles>jss": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-nested": { - "packages": { - "@babel/runtime": true, - "react-router-dom>tiny-warning": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-rule-value-function": { - "packages": { - "@material-ui/core>@material-ui/styles>jss": true, - "react-router-dom>tiny-warning": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-vendor-prefixer": { - "packages": { - "@material-ui/core>@material-ui/styles>jss": true, - "@material-ui/core>@material-ui/styles>jss-plugin-vendor-prefixer>css-vendor": true - } - }, - "@material-ui/core>@material-ui/styles>jss-plugin-vendor-prefixer>css-vendor": { - "globals": { - "document.createElement": true, - "document.documentElement": true, - "getComputedStyle": true - }, - "packages": { - "@babel/runtime": true, - "@material-ui/core>@material-ui/styles>jss>is-in-browser": true - } - }, - "@material-ui/core>@material-ui/styles>jss>is-in-browser": { - "globals": { - "document": true - } - }, - "@material-ui/core>@material-ui/system": { - "globals": { - "console.error": true - }, - "packages": { - "@babel/runtime": true, - "@material-ui/core>@material-ui/utils": true, - "prop-types": true - } - }, - "@material-ui/core>@material-ui/utils": { - "packages": { - "@babel/runtime": true, - "prop-types": true, - "prop-types>react-is": true - } - }, - "@material-ui/core>popper.js": { - "globals": { - "MSInputMethodContext": true, - "Node.DOCUMENT_POSITION_FOLLOWING": true, - "cancelAnimationFrame": true, - "console.warn": true, - "define": true, - "devicePixelRatio": true, - "document": true, - "getComputedStyle": true, - "innerHeight": true, - "innerWidth": true, - "navigator": true, - "requestAnimationFrame": true, - "setTimeout": true - } - }, - "@material-ui/core>react-transition-group": { - "globals": { - "Element": true, - "setTimeout": true - }, - "packages": { - "@material-ui/core>react-transition-group>dom-helpers": true, - "prop-types": true, - "react": true, - "react-dom": true - } - }, - "@material-ui/core>react-transition-group>dom-helpers": { - "packages": { - "@babel/runtime": true - } - }, - "@metamask/address-book-controller": { - "packages": { - "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/controller-utils": true - } - }, - "@metamask/address-book-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/announcement-controller": { - "packages": { - "@metamask/announcement-controller>@metamask/base-controller": true - } - }, - "@metamask/announcement-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/approval-controller": { - "packages": { - "@metamask/approval-controller>nanoid": true, - "@metamask/base-controller": true, - "eth-rpc-errors": true - } - }, - "@metamask/approval-controller>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/assets-controllers": { - "globals": { - "Headers": true, - "URL": true, - "clearInterval": true, - "clearTimeout": true, - "console.info": true, - "console.log": true, - "setInterval": true, - "setTimeout": true - }, - "packages": { - "@ethersproject/contracts": true, - "@ethersproject/providers": true, - "@metamask/assets-controllers>@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/controller-utils": true, - "@metamask/assets-controllers>abort-controller": true, - "@metamask/assets-controllers>multiformats": true, - "@metamask/base-controller": true, - "@metamask/contract-metadata": true, - "@metamask/metamask-eth-abis": true, - "@metamask/utils": true, - "browserify>events": true, - "eth-json-rpc-filters>async-mutex": true, - "eth-query": true, - "eth-rpc-errors": true, - "ethereumjs-util": true, - "single-call-balance-checker-abi": true, - "uuid": true - } - }, - "@metamask/assets-controllers>@metamask/abi-utils": { - "packages": { - "@metamask/utils": true, - "@metamask/utils>superstruct": true - } - }, - "@metamask/assets-controllers>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/assets-controllers>abort-controller": { - "globals": { - "AbortController": true - } - }, - "@metamask/assets-controllers>multiformats": { - "globals": { - "TextDecoder": true, - "TextEncoder": true, - "console.warn": true - } - }, - "@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/controller-utils>isomorphic-fetch": { - "globals": { - "fetch.bind": true - }, - "packages": { - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": true - } - }, - "@metamask/controller-utils>isomorphic-fetch>whatwg-fetch": { - "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true - } - }, - "@metamask/controllers>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/controllers>web3": { - "globals": { - "XMLHttpRequest": true - } - }, - "@metamask/controllers>web3-provider-engine>cross-fetch>node-fetch": { - "globals": { - "fetch": true - } - }, - "@metamask/controllers>web3-provider-engine>eth-json-rpc-middleware>node-fetch": { - "globals": { - "fetch": true - } - }, - "@metamask/eth-json-rpc-infura": { - "globals": { - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "@metamask/utils": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "node-fetch": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "@metamask/eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-middleware>pify": true, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/utils": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, - "@metamask/eth-keyring-controller": { - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring": true, - "@metamask/eth-keyring-controller>obs-store": true, - "@metamask/rpc-methods>@metamask/browser-passworder": true, - "browserify>events": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": { - "globals": { - "TextEncoder": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": true, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/scure-bip39": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/util": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util": { - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": true, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": true, - "bn.js": true, - "browserify>buffer": true, - "eth-lattice-keyring>@ethereumjs/util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring": { - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": true, - "browserify>buffer": true, - "browserify>events": true, - "eth-lattice-keyring>@ethereumjs/util": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": true - } - }, - "@metamask/eth-keyring-controller>@metamask/eth-simple-keyring>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "@metamask/eth-keyring-controller>obs-store": { - "packages": { - "safe-event-emitter": true, - "watchify>xtend": true - } - }, - "@metamask/eth-ledger-bridge-keyring": { - "globals": { - "addEventListener": true, - "console.log": true, - "document.createElement": true, - "document.head.appendChild": true, - "fetch": true, - "removeEventListener": true - }, - "packages": { - "@ethereumjs/tx": true, - "@metamask/eth-ledger-bridge-keyring>eth-sig-util": true, - "@metamask/eth-ledger-bridge-keyring>hdkey": true, - "browserify>buffer": true, - "browserify>events": true, - "ethereumjs-util": true - } - }, - "@metamask/eth-ledger-bridge-keyring>eth-sig-util": { - "packages": { - "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": true, - "browserify>buffer": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-ledger-bridge-keyring>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "@metamask/eth-ledger-bridge-keyring>hdkey": { - "packages": { - "@metamask/eth-ledger-bridge-keyring>hdkey>secp256k1": true, - "browserify>assert": true, - "browserify>crypto-browserify": true, - "eth-trezor-keyring>hdkey>coinstring": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "@metamask/eth-ledger-bridge-keyring>hdkey>secp256k1": { - "packages": { - "bn.js": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-trezor-keyring>hdkey>secp256k1>bip66": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-token-tracker": { - "globals": { - "console.warn": true - }, - "packages": { - "@babel/runtime": true, - "@metamask/eth-token-tracker>deep-equal": true, - "@metamask/eth-token-tracker>eth-block-tracker": true, - "@metamask/eth-token-tracker>ethjs": true, - "@metamask/eth-token-tracker>human-standard-token-abi": true, - "ethjs-contract": true, - "ethjs-query": true, - "safe-event-emitter": true - } - }, - "@metamask/eth-token-tracker>deep-equal": { - "packages": { - "@metamask/eth-token-tracker>deep-equal>is-arguments": true, - "@metamask/eth-token-tracker>deep-equal>is-date-object": true, - "@ngraveio/bc-ur>assert>object-is": true, - "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, - "string.prototype.matchall>regexp.prototype.flags": true - } - }, - "@metamask/eth-token-tracker>deep-equal>is-arguments": { - "packages": { - "koa>is-generator-function>has-tostringtag": true, - "string.prototype.matchall>call-bind": true - } - }, - "@metamask/eth-token-tracker>deep-equal>is-date-object": { - "packages": { - "koa>is-generator-function>has-tostringtag": true - } - }, - "@metamask/eth-token-tracker>eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-token-tracker>eth-block-tracker>pify": true, - "eth-query": true, - "safe-event-emitter": true - } - }, - "@metamask/eth-token-tracker>ethjs": { - "globals": { - "clearInterval": true, - "setInterval": true - }, - "packages": { - "@metamask/eth-token-tracker>ethjs>bn.js": true, - "@metamask/eth-token-tracker>ethjs>ethjs-abi": true, - "@metamask/eth-token-tracker>ethjs>ethjs-contract": true, - "@metamask/eth-token-tracker>ethjs>ethjs-query": true, - "browserify>buffer": true, - "ethjs>ethjs-filter": true, - "ethjs>ethjs-provider-http": true, - "ethjs>ethjs-unit": true, - "ethjs>ethjs-util": true, - "ethjs>js-sha3": true, - "ethjs>number-to-bn": true - } - }, - "@metamask/eth-token-tracker>ethjs>ethjs-abi": { - "packages": { - "@metamask/eth-token-tracker>ethjs>bn.js": true, - "browserify>buffer": true, - "ethjs>js-sha3": true, - "ethjs>number-to-bn": true - } - }, - "@metamask/eth-token-tracker>ethjs>ethjs-contract": { - "packages": { - "@metamask/eth-token-tracker>ethjs>ethjs-contract>ethjs-abi": true, - "ethjs-query>babel-runtime": true, - "ethjs>ethjs-filter": true, - "ethjs>ethjs-util": true, - "ethjs>js-sha3": true, - "promise-to-callback": true - } - }, - "@metamask/eth-token-tracker>ethjs>ethjs-contract>ethjs-abi": { - "packages": { - "@metamask/eth-token-tracker>ethjs>bn.js": true, - "browserify>buffer": true, - "ethjs>js-sha3": true, - "ethjs>number-to-bn": true - } - }, - "@metamask/eth-token-tracker>ethjs>ethjs-query": { - "globals": { - "console": true - }, - "packages": { - "ethjs-query>babel-runtime": true, - "ethjs-query>ethjs-format": true, - "ethjs-query>ethjs-rpc": true, - "promise-to-callback": true - } - }, - "@metamask/etherscan-link": { - "globals": { - "URL": true - } - }, - "@metamask/gas-fee-controller": { - "globals": { - "clearInterval": true, - "console.error": true, - "setInterval": true - }, - "packages": { - "@metamask/controller-utils": true, - "@metamask/gas-fee-controller>@metamask/base-controller": true, - "eth-query": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true, - "uuid": true - } - }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/jazzicon": { - "globals": { - "document.createElement": true, - "document.createElementNS": true - }, - "packages": { - "@metamask/jazzicon>color": true, - "@metamask/jazzicon>mersenne-twister": true - } - }, - "@metamask/jazzicon>color": { - "packages": { - "@metamask/jazzicon>color>clone": true, - "@metamask/jazzicon>color>color-convert": true, - "@metamask/jazzicon>color>color-string": true - } - }, - "@metamask/jazzicon>color>clone": { - "packages": { - "browserify>buffer": true - } - }, - "@metamask/jazzicon>color>color-convert": { - "packages": { - "@metamask/jazzicon>color>color-convert>color-name": true - } - }, - "@metamask/jazzicon>color>color-string": { - "packages": { - "jest-canvas-mock>moo-color>color-name": true - } - }, - "@metamask/key-tree": { - "packages": { - "@metamask/key-tree>@noble/ed25519": true, - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@noble/secp256k1": true, - "@metamask/key-tree>@scure/base": true, - "@metamask/scure-bip39": true, - "@metamask/utils": true - } - }, - "@metamask/key-tree>@noble/ed25519": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "@metamask/key-tree>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "@metamask/key-tree>@noble/secp256k1": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "@metamask/key-tree>@scure/base": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - } - }, - "@metamask/logo": { - "globals": { - "addEventListener": true, - "document.body.appendChild": true, - "document.createElementNS": true, - "innerHeight": true, - "innerWidth": true, - "requestAnimationFrame": true - }, - "packages": { - "@metamask/logo>gl-mat4": true, - "@metamask/logo>gl-vec3": true - } - }, - "@metamask/notification-controller>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/obs-store": { - "globals": { - "localStorage": true - }, - "packages": { - "@metamask/obs-store>through2": true, - "browserify>stream-browserify": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "@metamask/obs-store>through2": { - "packages": { - "browserify>process": true, - "browserify>util": true, - "readable-stream": true, - "watchify>xtend": true - } - }, - "@metamask/permission-controller": { - "packages": { - "@metamask/base-controller": true, - "@metamask/permission-controller>@metamask/controller-utils": true, - "@metamask/permission-controller>nanoid": true, - "deep-freeze-strict": true, - "eth-rpc-errors": true, - "immer": true, - "json-rpc-engine": true - } - }, - "@metamask/permission-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/permission-controller>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/phishing-controller": { - "globals": { - "fetch": true - }, - "packages": { - "@metamask/base-controller": true, - "@metamask/controller-utils>isomorphic-fetch": true, - "@metamask/phishing-controller>@metamask/controller-utils": true, - "@metamask/phishing-warning>eth-phishing-detect": true, - "punycode": true - } - }, - "@metamask/phishing-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/controller-utils>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/phishing-warning>eth-phishing-detect": { - "packages": { - "eslint>optionator>fast-levenshtein": true - } - }, - "@metamask/rpc-methods": { - "packages": { - "@metamask/key-tree": true, - "@metamask/key-tree>@noble/hashes": true, - "@metamask/utils": true, - "@metamask/utils>superstruct": true - } - }, - "@metamask/rpc-methods>@metamask/browser-passworder": { - "globals": { - "btoa": true, - "crypto.getRandomValues": true, - "crypto.subtle.decrypt": true, - "crypto.subtle.deriveKey": true, - "crypto.subtle.encrypt": true, - "crypto.subtle.exportKey": true, - "crypto.subtle.importKey": true - }, - "packages": { - "browserify>buffer": true - } - }, - "@metamask/rpc-methods>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/scure-bip39": { - "globals": { - "TextEncoder": true - }, - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@scure/base": true - } - }, - "@metamask/smart-transactions-controller": { - "globals": { - "URLSearchParams": true, - "clearInterval": true, - "console.error": true, - "console.log": true, - "fetch": true, - "setInterval": true - }, - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/bignumber": true, - "@ethersproject/providers": true, - "@metamask/base-controller": true, - "@metamask/controller-utils": true, - "@metamask/controller-utils>isomorphic-fetch": true, - "@metamask/smart-transactions-controller>bignumber.js": true, - "fast-json-patch": true, - "lodash": true - } - }, - "@metamask/smart-transactions-controller>@metamask/controllers>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/smart-transactions-controller>bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "@metamask/snaps-controllers>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "@metamask/subject-metadata-controller": { - "packages": { - "@metamask/base-controller": true - } - }, - "@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "@metamask/utils>superstruct": true, - "browserify>buffer": true, - "nock>debug": true, - "semver": true - } - }, - "@ngraveio/bc-ur": { - "packages": { - "@ngraveio/bc-ur>@apocentre/alias-sampling": true, - "@ngraveio/bc-ur>bignumber.js": true, - "@ngraveio/bc-ur>crc": true, - "@ngraveio/bc-ur>jsbi": true, - "addons-linter>sha.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "pubnub>cbor-sync": true - } - }, - "@ngraveio/bc-ur>assert>object-is": { - "packages": { - "globalthis>define-properties": true, - "string.prototype.matchall>call-bind": true - } - }, - "@ngraveio/bc-ur>bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "@ngraveio/bc-ur>crc": { - "packages": { - "browserify>buffer": true - } - }, - "@ngraveio/bc-ur>jsbi": { - "globals": { - "define": true - } - }, - "@popperjs/core": { - "globals": { - "Element": true, - "HTMLElement": true, - "ShadowRoot": true, - "console.error": true, - "console.warn": true, - "document": true, - "navigator.userAgent": true - } - }, - "@reduxjs/toolkit": { - "globals": { - "AbortController": true, - "__REDUX_DEVTOOLS_EXTENSION_COMPOSE__": true, - "__REDUX_DEVTOOLS_EXTENSION__": true, - "console.error": true, - "console.info": true, - "console.warn": true - }, - "packages": { - "@reduxjs/toolkit>reselect": true, - "immer": true, - "redux": true, - "redux-thunk": true - } - }, - "@segment/loosely-validate-event": { - "packages": { - "@segment/loosely-validate-event>component-type": true, - "@segment/loosely-validate-event>join-component": true, - "browserify>assert": true, - "browserify>buffer": true - } - }, - "@sentry/browser": { - "globals": { - "XMLHttpRequest": true, - "setTimeout": true - }, - "packages": { - "@sentry/browser>@sentry/core": true, - "@sentry/browser>tslib": true, - "@sentry/types": true, - "@sentry/utils": true - } - }, - "@sentry/browser>@sentry/core": { - "globals": { - "clearInterval": true, - "setInterval": true - }, - "packages": { - "@sentry/browser>@sentry/core>@sentry/hub": true, - "@sentry/browser>@sentry/core>@sentry/minimal": true, - "@sentry/browser>@sentry/core>tslib": true, - "@sentry/types": true, - "@sentry/utils": true - } - }, - "@sentry/browser>@sentry/core>@sentry/hub": { - "globals": { - "clearInterval": true, - "setInterval": true - }, - "packages": { - "@sentry/browser>@sentry/core>@sentry/hub>tslib": true, - "@sentry/types": true, - "@sentry/utils": true - } - }, - "@sentry/browser>@sentry/core>@sentry/hub>tslib": { - "globals": { - "define": true - } - }, - "@sentry/browser>@sentry/core>@sentry/minimal": { - "packages": { - "@sentry/browser>@sentry/core>@sentry/hub": true, - "@sentry/browser>@sentry/core>@sentry/minimal>tslib": true - } - }, - "@sentry/browser>@sentry/core>@sentry/minimal>tslib": { - "globals": { - "define": true - } - }, - "@sentry/browser>@sentry/core>tslib": { - "globals": { - "define": true - } - }, - "@sentry/browser>tslib": { - "globals": { - "define": true - } - }, - "@sentry/integrations": { - "globals": { - "clearTimeout": true, - "console.error": true, - "console.log": true, - "setTimeout": true - }, - "packages": { - "@sentry/integrations>tslib": true, - "@sentry/types": true, - "@sentry/utils": true, - "localforage": true - } - }, - "@sentry/integrations>tslib": { - "globals": { - "define": true - } - }, - "@sentry/utils": { - "globals": { - "CustomEvent": true, - "DOMError": true, - "DOMException": true, - "Element": true, - "ErrorEvent": true, - "Event": true, - "Headers": true, - "Request": true, - "Response": true, - "XMLHttpRequest.prototype": true, - "clearTimeout": true, - "console.error": true, - "document": true, - "setTimeout": true - }, - "packages": { - "@sentry/utils>tslib": true, - "browserify>process": true - } - }, - "@sentry/utils>tslib": { - "globals": { - "define": true - } - }, - "@spruceid/siwe-parser": { - "globals": { - "console.error": true, - "console.log": true - }, - "packages": { - "@spruceid/siwe-parser>apg-js": true - } - }, - "@spruceid/siwe-parser>apg-js": { - "globals": { - "mode": true - }, - "packages": { - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true - } - }, - "@storybook/api>regenerator-runtime": { - "globals": { - "regeneratorRuntime": "write" - } - }, - "@storybook/api>telejson>is-regex": { - "packages": { - "koa>is-generator-function>has-tostringtag": true, - "string.prototype.matchall>call-bind": true - } - }, - "@storybook/api>util-deprecate": { - "globals": { - "console.trace": true, - "console.warn": true, - "localStorage": true - } - }, - "@truffle/codec": { - "packages": { - "@truffle/codec>@truffle/abi-utils": true, - "@truffle/codec>@truffle/compile-common": true, - "@truffle/codec>big.js": true, - "@truffle/codec>bn.js": true, - "@truffle/codec>cbor": true, - "@truffle/codec>semver": true, - "@truffle/codec>utf8": true, - "@truffle/codec>web3-utils": true, - "browserify>buffer": true, - "browserify>os-browserify": true, - "browserify>util": true, - "lodash": true, - "nock>debug": true - } - }, - "@truffle/codec>@truffle/abi-utils": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case": true, - "@truffle/codec>@truffle/abi-utils>fast-check": true, - "@truffle/codec>web3-utils": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>camel-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>constant-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>dot-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>header-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>is-lower-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>is-upper-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>lower-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>lower-case-first": true, - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>param-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>pascal-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>path-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>sentence-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>snake-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>swap-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>title-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case-first": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>camel-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>constant-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>snake-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>dot-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>header-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>is-lower-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>lower-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>is-upper-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>lower-case-first": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>lower-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>no-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>lower-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>param-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>pascal-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>pascal-case>camel-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case-first": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>pascal-case>camel-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>path-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>sentence-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case-first": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>snake-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>swap-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>lower-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>title-case": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>no-case": true, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>change-case>upper-case-first": { - "packages": { - "@truffle/codec>@truffle/abi-utils>change-case>upper-case": true - } - }, - "@truffle/codec>@truffle/abi-utils>fast-check": { - "globals": { - "clearTimeout": true, - "console.log": true, - "setTimeout": true - }, - "packages": { - "@truffle/codec>@truffle/abi-utils>fast-check>pure-rand": true, - "browserify>buffer": true - } - }, - "@truffle/codec>@truffle/compile-common": { - "packages": { - "@truffle/codec>@truffle/compile-common>@truffle/error": true, - "@truffle/codec>@truffle/compile-common>colors": true, - "browserify>path-browserify": true - } - }, - "@truffle/codec>@truffle/compile-common>colors": { - "globals": { - "console.log": true - }, - "packages": { - "browserify>os-browserify": true, - "browserify>process": true, - "browserify>util": true - } - }, - "@truffle/codec>big.js": { - "globals": { - "define": true - } - }, - "@truffle/codec>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "@truffle/codec>cbor": { - "globals": { - "TextDecoder": true - }, - "packages": { - "@truffle/codec>cbor>bignumber.js": true, - "@truffle/codec>cbor>nofilter": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "browserify>stream-browserify": true, - "browserify>url": true, - "browserify>util": true - } - }, - "@truffle/codec>cbor>bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "@truffle/codec>cbor>nofilter": { - "packages": { - "browserify>buffer": true, - "browserify>stream-browserify": true, - "browserify>util": true - } - }, - "@truffle/codec>semver": { - "globals": { - "console.error": true - }, - "packages": { - "@truffle/codec>semver>lru-cache": true, - "browserify>process": true - } - }, - "@truffle/codec>semver>lru-cache": { - "packages": { - "semver>lru-cache>yallist": true - } - }, - "@truffle/codec>web3-utils": { - "globals": { - "setTimeout": true - }, - "packages": { - "@truffle/codec>utf8": true, - "@truffle/codec>web3-utils>bn.js": true, - "@truffle/codec>web3-utils>ethereum-bloom-filters": true, - "browserify>buffer": true, - "ethereumjs-util": true, - "ethereumjs-wallet>randombytes": true, - "ethjs>ethjs-unit": true, - "ethjs>number-to-bn": true - } - }, - "@truffle/codec>web3-utils>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "@truffle/codec>web3-utils>ethereum-bloom-filters": { - "packages": { - "@truffle/codec>web3-utils>ethereum-bloom-filters>js-sha3": true - } - }, - "@truffle/codec>web3-utils>ethereum-bloom-filters>js-sha3": { - "globals": { - "define": true - }, - "packages": { - "browserify>process": true - } - }, - "@truffle/decoder": { - "packages": { - "@truffle/codec": true, - "@truffle/codec>@truffle/abi-utils": true, - "@truffle/codec>@truffle/compile-common": true, - "@truffle/codec>web3-utils": true, - "@truffle/decoder>@truffle/encoder": true, - "@truffle/decoder>@truffle/source-map-utils": true, - "@truffle/decoder>bn.js": true, - "nock>debug": true - } - }, - "@truffle/decoder>@truffle/encoder": { - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/bignumber": true, - "@truffle/codec": true, - "@truffle/codec>@truffle/abi-utils": true, - "@truffle/codec>@truffle/compile-common": true, - "@truffle/codec>web3-utils": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs": true, - "@truffle/decoder>@truffle/encoder>big.js": true, - "@truffle/decoder>@truffle/encoder>bignumber.js": true, - "lodash": true, - "nock>debug": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs": { - "globals": { - "console.log": true, - "console.warn": true, - "registries": true - }, - "packages": { - "@babel/runtime": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>@ensdomains/address-encoder": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>@ensdomains/ens": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>@ensdomains/resolver": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>js-sha3": true, - "browserify>buffer": true, - "eth-ens-namehash": true, - "ethereumjs-wallet>bs58check>bs58": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>@ensdomains/address-encoder": { - "globals": { - "console": true - }, - "packages": { - "bn.js": true, - "browserify>buffer": true, - "browserify>crypto-browserify": true, - "ethereumjs-util>create-hash>ripemd160": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash": { - "packages": { - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multicodec": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multihashes": true, - "browserify>buffer": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids": { - "packages": { - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids>class-is": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids>multibase": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids>multicodec": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multihashes": true, - "browserify>buffer": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids>multibase": { - "packages": { - "browserify>buffer": true, - "ethereumjs-wallet>bs58check>bs58>base-x": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>cids>multicodec": { - "packages": { - "@ensdomains/content-hash>multihashes>varint": true, - "browserify>buffer": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multicodec": { - "packages": { - "@ensdomains/content-hash>multihashes>varint": true, - "browserify>buffer": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multihashes": { - "packages": { - "@ensdomains/content-hash>multihashes>varint": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multihashes>multibase": true, - "browserify>buffer": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>content-hash>multihashes>multibase": { - "packages": { - "browserify>buffer": true, - "ethereumjs-wallet>bs58check>bs58>base-x": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers": { - "packages": { - "@ethersproject/abi": true, - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/constants": true, - "@ethersproject/abi>@ethersproject/hash": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/bignumber": true, - "@ethersproject/contracts": true, - "@ethersproject/hdnode": true, - "@ethersproject/hdnode>@ethersproject/abstract-signer": true, - "@ethersproject/hdnode>@ethersproject/basex": true, - "@ethersproject/hdnode>@ethersproject/sha2": true, - "@ethersproject/hdnode>@ethersproject/signing-key": true, - "@ethersproject/hdnode>@ethersproject/transactions": true, - "@ethersproject/hdnode>@ethersproject/wordlists": true, - "@ethersproject/providers": true, - "@ethersproject/providers>@ethersproject/base64": true, - "@ethersproject/providers>@ethersproject/random": true, - "@ethersproject/providers>@ethersproject/rlp": true, - "@ethersproject/providers>@ethersproject/web": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/json-wallets": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/solidity": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/units": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/wallet": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/json-wallets": { - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/hdnode": true, - "@ethersproject/hdnode>@ethersproject/pbkdf2": true, - "@ethersproject/hdnode>@ethersproject/transactions": true, - "@ethersproject/providers>@ethersproject/random": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/json-wallets>aes-js": true, - "ethereumjs-util>ethereum-cryptography>scrypt-js": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/json-wallets>aes-js": { - "globals": { - "define": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/solidity": { - "packages": { - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/strings": true, - "@ethersproject/bignumber": true, - "@ethersproject/hdnode>@ethersproject/sha2": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/units": { - "packages": { - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/bignumber": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/wallet": { - "packages": { - "@ethersproject/abi>@ethersproject/address": true, - "@ethersproject/abi>@ethersproject/bytes": true, - "@ethersproject/abi>@ethersproject/hash": true, - "@ethersproject/abi>@ethersproject/keccak256": true, - "@ethersproject/abi>@ethersproject/logger": true, - "@ethersproject/abi>@ethersproject/properties": true, - "@ethersproject/hdnode": true, - "@ethersproject/hdnode>@ethersproject/abstract-signer": true, - "@ethersproject/hdnode>@ethersproject/signing-key": true, - "@ethersproject/hdnode>@ethersproject/transactions": true, - "@ethersproject/providers>@ethersproject/abstract-provider": true, - "@ethersproject/providers>@ethersproject/random": true, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>ethers>@ethersproject/json-wallets": true - } - }, - "@truffle/decoder>@truffle/encoder>@ensdomains/ensjs>js-sha3": { - "globals": { - "define": true - }, - "packages": { - "browserify>process": true - } - }, - "@truffle/decoder>@truffle/encoder>big.js": { - "globals": { - "define": true - } - }, - "@truffle/decoder>@truffle/encoder>bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "@truffle/decoder>@truffle/source-map-utils": { - "packages": { - "@truffle/codec": true, - "@truffle/codec>web3-utils": true, - "@truffle/decoder>@truffle/source-map-utils>@truffle/code-utils": true, - "@truffle/decoder>@truffle/source-map-utils>json-pointer": true, - "@truffle/decoder>@truffle/source-map-utils>node-interval-tree": true, - "nock>debug": true - } - }, - "@truffle/decoder>@truffle/source-map-utils>@truffle/code-utils": { - "packages": { - "@truffle/codec>cbor": true, - "browserify>buffer": true - } - }, - "@truffle/decoder>@truffle/source-map-utils>json-pointer": { - "packages": { - "@truffle/decoder>@truffle/source-map-utils>json-pointer>foreach": true - } - }, - "@truffle/decoder>@truffle/source-map-utils>node-interval-tree": { - "packages": { - "@storybook/addon-a11y>react-sizeme>shallowequal": true - } - }, - "@truffle/decoder>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "@zxing/browser": { - "globals": { - "HTMLElement": true, - "HTMLImageElement": true, - "HTMLVideoElement": true, - "URL.createObjectURL": true, - "clearTimeout": true, - "console.error": true, - "console.warn": true, - "document": true, - "navigator": true, - "setTimeout": true - }, - "packages": { - "@zxing/library": true - } - }, - "@zxing/library": { - "globals": { - "TextDecoder": true, - "TextEncoder": true, - "btoa": true, - "clearTimeout": true, - "define": true, - "document.createElement": true, - "document.createElementNS": true, - "document.getElementById": true, - "navigator.mediaDevices.enumerateDevices": true, - "navigator.mediaDevices.getUserMedia": true, - "setTimeout": true - } - }, - "addons-linter>sha.js": { - "packages": { - "ethereumjs-wallet>safe-buffer": true, - "pumpify>inherits": true - } - }, - "await-semaphore": { - "packages": { - "browserify>process": true, - "browserify>timers-browserify": true - } - }, - "base32-encode": { - "packages": { - "base32-encode>to-data-view": true - } - }, - "bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "bowser": { - "globals": { - "define": true - } - }, - "browserify>assert": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>assert>util": true, - "react>object-assign": true - } - }, - "browserify>assert>util": { - "globals": { - "console.error": true, - "console.log": true, - "console.trace": true, - "process": true - }, - "packages": { - "browserify>assert>util>inherits": true, - "browserify>process": true - } - }, - "browserify>browser-resolve": { - "packages": { - "ethjs-query>babel-runtime>core-js": true - } - }, - "browserify>buffer": { - "globals": { - "console": true - }, - "packages": { - "base64-js": true, - "browserify>buffer>ieee754": true - } - }, - "browserify>crypto-browserify": { - "packages": { - "browserify>crypto-browserify>browserify-cipher": true, - "browserify>crypto-browserify>browserify-sign": true, - "browserify>crypto-browserify>create-ecdh": true, - "browserify>crypto-browserify>create-hmac": true, - "browserify>crypto-browserify>diffie-hellman": true, - "browserify>crypto-browserify>pbkdf2": true, - "browserify>crypto-browserify>public-encrypt": true, - "browserify>crypto-browserify>randomfill": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-wallet>randombytes": true - } - }, - "browserify>crypto-browserify>browserify-cipher": { - "packages": { - "browserify>crypto-browserify>browserify-cipher>browserify-des": true, - "browserify>crypto-browserify>browserify-cipher>evp_bytestokey": true, - "ethereumjs-util>ethereum-cryptography>browserify-aes": true - } - }, - "browserify>crypto-browserify>browserify-cipher>browserify-des": { - "packages": { - "browserify>buffer": true, - "browserify>crypto-browserify>browserify-cipher>browserify-des>des.js": true, - "ethereumjs-util>create-hash>cipher-base": true, - "pumpify>inherits": true - } - }, - "browserify>crypto-browserify>browserify-cipher>browserify-des>des.js": { - "packages": { - "ganache>secp256k1>elliptic>minimalistic-assert": true, - "pumpify>inherits": true - } - }, - "browserify>crypto-browserify>browserify-cipher>evp_bytestokey": { - "packages": { - "ethereumjs-util>create-hash>md5.js": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "browserify>crypto-browserify>browserify-sign": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "browserify>crypto-browserify>create-hmac": true, - "browserify>crypto-browserify>public-encrypt>browserify-rsa": true, - "browserify>crypto-browserify>public-encrypt>parse-asn1": true, - "browserify>stream-browserify": true, - "ethereumjs-util>create-hash": true, - "ganache>secp256k1>elliptic": true, - "pumpify>inherits": true - } - }, - "browserify>crypto-browserify>create-ecdh": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "browserify>crypto-browserify>create-hmac": { - "packages": { - "addons-linter>sha.js": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>create-hash>cipher-base": true, - "ethereumjs-util>create-hash>ripemd160": true, - "ethereumjs-wallet>safe-buffer": true, - "pumpify>inherits": true - } - }, - "browserify>crypto-browserify>diffie-hellman": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "browserify>crypto-browserify>diffie-hellman>miller-rabin": true, - "ethereumjs-wallet>randombytes": true - } - }, - "browserify>crypto-browserify>diffie-hellman>miller-rabin": { - "packages": { - "bn.js": true, - "ganache>secp256k1>elliptic>brorand": true - } - }, - "browserify>crypto-browserify>pbkdf2": { - "globals": { - "crypto": true, - "process": true, - "queueMicrotask": true, - "setImmediate": true, - "setTimeout": true - }, - "packages": { - "addons-linter>sha.js": true, - "browserify>process": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>create-hash>ripemd160": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "browserify>crypto-browserify>public-encrypt": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "browserify>crypto-browserify>public-encrypt>browserify-rsa": true, - "browserify>crypto-browserify>public-encrypt>parse-asn1": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-wallet>randombytes": true - } - }, - "browserify>crypto-browserify>public-encrypt>browserify-rsa": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "ethereumjs-wallet>randombytes": true - } - }, - "browserify>crypto-browserify>public-encrypt>parse-asn1": { - "packages": { - "browserify>buffer": true, - "browserify>crypto-browserify>browserify-cipher>evp_bytestokey": true, - "browserify>crypto-browserify>pbkdf2": true, - "browserify>crypto-browserify>public-encrypt>parse-asn1>asn1.js": true, - "ethereumjs-util>ethereum-cryptography>browserify-aes": true - } - }, - "browserify>crypto-browserify>public-encrypt>parse-asn1>asn1.js": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "browserify>vm-browserify": true, - "ganache>secp256k1>elliptic>minimalistic-assert": true, - "pumpify>inherits": true - } - }, - "browserify>crypto-browserify>randomfill": { - "globals": { - "crypto": true, - "msCrypto": true - }, - "packages": { - "browserify>process": true, - "ethereumjs-wallet>randombytes": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "browserify>events": { - "globals": { - "console": true - } - }, - "browserify>has": { - "packages": { - "mocha>object.assign>function-bind": true - } - }, - "browserify>os-browserify": { - "globals": { - "location": true, - "navigator": true - } - }, - "browserify>path-browserify": { - "packages": { - "browserify>process": true - } - }, - "browserify>process": { - "globals": { - "clearTimeout": true, - "setTimeout": true - } - }, - "browserify>punycode": { - "globals": { - "define": true - } - }, - "browserify>stream-browserify": { - "packages": { - "browserify>events": true, - "pumpify>inherits": true, - "readable-stream": true - } - }, - "browserify>string_decoder": { - "packages": { - "ethereumjs-wallet>safe-buffer": true - } - }, - "browserify>timers-browserify": { - "globals": { - "clearInterval": true, - "clearTimeout": true, - "setInterval": true, - "setTimeout": true - }, - "packages": { - "browserify>process": true - } - }, - "browserify>url": { - "packages": { - "browserify>punycode": true, - "browserify>querystring-es3": true - } - }, - "browserify>util": { - "globals": { - "console.error": true, - "console.log": true, - "console.trace": true, - "process": true - }, - "packages": { - "browserify>process": true, - "browserify>util>inherits": true - } - }, - "browserify>vm-browserify": { - "globals": { - "document.body.appendChild": true, - "document.body.removeChild": true, - "document.createElement": true - } - }, - "classnames": { - "globals": { - "classNames": "write", - "define": true - } - }, - "copy-to-clipboard": { - "globals": { - "clipboardData": true, - "console.error": true, - "console.warn": true, - "document.body.appendChild": true, - "document.body.removeChild": true, - "document.createElement": true, - "document.createRange": true, - "document.execCommand": true, - "document.getSelection": true, - "navigator.userAgent": true, - "prompt": true - }, - "packages": { - "copy-to-clipboard>toggle-selection": true - } - }, - "copy-to-clipboard>toggle-selection": { - "globals": { - "document.activeElement": true, - "document.getSelection": true - } - }, - "currency-formatter": { - "packages": { - "currency-formatter>accounting": true, - "currency-formatter>locale-currency": true, - "react>object-assign": true - } - }, - "currency-formatter>accounting": { - "globals": { - "define": true - } - }, - "currency-formatter>locale-currency": { - "globals": { - "countryCode": true - } - }, - "debounce-stream": { - "packages": { - "debounce-stream>debounce": true, - "debounce-stream>duplexer": true, - "debounce-stream>through": true - } - }, - "debounce-stream>debounce": { - "globals": { - "clearTimeout": true, - "setTimeout": true - } - }, - "debounce-stream>duplexer": { - "packages": { - "browserify>stream-browserify": true - } - }, - "debounce-stream>through": { - "packages": { - "browserify>process": true, - "browserify>stream-browserify": true - } - }, - "depcheck>@vue/compiler-sfc>postcss>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "dependency-tree>precinct>detective-postcss>postcss>nanoid": { - "globals": { - "crypto.getRandomValues": true - } - }, - "end-of-stream": { - "packages": { - "browserify>process": true, - "pump>once": true - } - }, - "eslint>optionator>fast-levenshtein": { - "globals": { - "Intl": true, - "Levenshtein": "write", - "console.log": true, - "define": true, - "importScripts": true, - "postMessage": true - } - }, - "eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/utils": true, - "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "eth-ens-namehash": { - "globals": { - "name": "write" - }, - "packages": { - "browserify>buffer": true, - "eth-ens-namehash>idna-uts46-hx": true, - "eth-ens-namehash>js-sha3": true - } - }, - "eth-ens-namehash>idna-uts46-hx": { - "globals": { - "define": true - }, - "packages": { - "browserify>punycode": true - } - }, - "eth-ens-namehash>js-sha3": { - "packages": { - "browserify>process": true - } - }, - "eth-json-rpc-filters": { - "globals": { - "console.error": true - }, - "packages": { - "eth-json-rpc-filters>async-mutex": true, - "eth-query": true, - "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "pify": true - } - }, - "eth-json-rpc-filters>async-mutex": { - "globals": { - "setTimeout": true - }, - "packages": { - "wait-on>rxjs>tslib": true - } - }, - "eth-keyring-controller>@metamask/browser-passworder": { - "globals": { - "crypto": true - } - }, - "eth-lattice-keyring": { - "globals": { - "addEventListener": true, - "browser": true, - "clearInterval": true, - "fetch": true, - "open": true, - "setInterval": true - }, - "packages": { - "browserify>buffer": true, - "browserify>crypto-browserify": true, - "browserify>events": true, - "eth-lattice-keyring>@ethereumjs/tx": true, - "eth-lattice-keyring>@ethereumjs/util": true, - "eth-lattice-keyring>bn.js": true, - "eth-lattice-keyring>gridplus-sdk": true, - "eth-lattice-keyring>rlp": true - } - }, - "eth-lattice-keyring>@ethereumjs/tx": { - "packages": { - "@ethereumjs/common": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util": true - } - }, - "eth-lattice-keyring>@ethereumjs/util": { - "globals": { - "console.warn": true - }, - "packages": { - "browserify>buffer": true, - "browserify>events": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": true, - "eth-lattice-keyring>@ethereumjs/util>async": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>@ethereumjs/rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>async": { - "globals": { - "clearTimeout": true, - "console": true, - "define": true, - "queueMicrotask": true, - "setTimeout": true - }, - "packages": { - "browserify>process": true, - "browserify>timers-browserify": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography": { - "globals": { - "TextDecoder": true, - "crypto": true - }, - "packages": { - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/hashes": { - "globals": { - "TextEncoder": true, - "crypto": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": { - "globals": { - "crypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@scure/bip32": { - "packages": { - "@metamask/key-tree>@noble/hashes": true, - "@metamask/key-tree>@scure/base": true, - "eth-lattice-keyring>@ethereumjs/util>ethereum-cryptography>@noble/secp256k1": true - } - }, - "eth-lattice-keyring>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>gridplus-sdk": { - "globals": { - "AbortController": true, - "Request": true, - "__values": true, - "caches": true, - "clearTimeout": true, - "console.error": true, - "console.log": true, - "console.warn": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@ethereumjs/common>crc-32": true, - "@ethersproject/abi": true, - "bn.js": true, - "browserify>buffer": true, - "browserify>process": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": true, - "eth-lattice-keyring>gridplus-sdk>bech32": true, - "eth-lattice-keyring>gridplus-sdk>bignumber.js": true, - "eth-lattice-keyring>gridplus-sdk>bitwise": true, - "eth-lattice-keyring>gridplus-sdk>borc": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, - "eth-lattice-keyring>gridplus-sdk>js-sha3": true, - "eth-lattice-keyring>gridplus-sdk>rlp": true, - "eth-lattice-keyring>gridplus-sdk>secp256k1": true, - "eth-lattice-keyring>gridplus-sdk>uuid": true, - "ethereumjs-util>ethereum-cryptography>hash.js": true, - "ethereumjs-wallet>aes-js": true, - "ethereumjs-wallet>bs58check": true, - "ganache>secp256k1>elliptic": true, - "lodash": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/common": { - "packages": { - "@ethereumjs/common>crc-32": true, - "browserify>buffer": true, - "browserify>events": true, - "ethereumjs-util": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx": { - "packages": { - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": true, - "ethereumjs-util": true - } - }, - "eth-lattice-keyring>gridplus-sdk>@ethereumjs/tx>@ethereumjs/common": { - "packages": { - "@ethereumjs/common>crc-32": true, - "browserify>buffer": true, - "browserify>events": true, - "ethereumjs-util": true - } - }, - "eth-lattice-keyring>gridplus-sdk>bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "eth-lattice-keyring>gridplus-sdk>bitwise": { - "packages": { - "browserify>buffer": true - } - }, - "eth-lattice-keyring>gridplus-sdk>borc": { - "globals": { - "console": true - }, - "packages": { - "browserify>buffer": true, - "browserify>buffer>ieee754": true, - "eth-lattice-keyring>gridplus-sdk>borc>bignumber.js": true, - "eth-lattice-keyring>gridplus-sdk>borc>iso-url": true - } - }, - "eth-lattice-keyring>gridplus-sdk>borc>bignumber.js": { - "globals": { - "crypto": true, - "define": true - } - }, - "eth-lattice-keyring>gridplus-sdk>borc>iso-url": { - "globals": { - "URL": true, - "URLSearchParams": true, - "location": true - } - }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": { - "globals": { - "intToBuffer": true - }, - "packages": { - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser>bn.js": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser>buffer": true, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser>js-sha3": true - } - }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser>buffer": { - "globals": { - "console": true - }, - "packages": { - "base64-js": true, - "browserify>buffer>ieee754": true - } - }, - "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser>js-sha3": { - "globals": { - "define": true - }, - "packages": { - "browserify>process": true - } - }, - "eth-lattice-keyring>gridplus-sdk>js-sha3": { - "globals": { - "define": true - }, - "packages": { - "browserify>process": true - } - }, - "eth-lattice-keyring>gridplus-sdk>rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-lattice-keyring>gridplus-sdk>secp256k1": { - "packages": { - "ganache>secp256k1>elliptic": true - } - }, - "eth-lattice-keyring>gridplus-sdk>uuid": { - "globals": { - "crypto": true - } - }, - "eth-lattice-keyring>rlp": { - "globals": { - "TextEncoder": true - } - }, - "eth-method-registry": { - "packages": { - "ethjs": true - } - }, - "eth-query": { - "packages": { - "eth-query>json-rpc-random-id": true, - "nock>debug": true, - "watchify>xtend": true - } - }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, - "eth-sig-util": { - "packages": { - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util": true, - "eth-sig-util>tweetnacl": true, - "eth-sig-util>tweetnacl-util": true, - "ethereumjs-abi": true - } - }, - "eth-sig-util>ethereumjs-util": { - "packages": { - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "eth-sig-util>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "eth-sig-util>tweetnacl": { - "globals": { - "crypto": true, - "msCrypto": true, - "nacl": "write" - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-sig-util>tweetnacl-util": { - "globals": { - "atob": true, - "btoa": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "eth-trezor-keyring": { - "globals": { - "setTimeout": true - }, - "packages": { - "@ethereumjs/tx": true, - "browserify>buffer": true, - "browserify>events": true, - "eth-trezor-keyring>hdkey": true, - "eth-trezor-keyring>trezor-connect": true, - "ethereumjs-util": true - } - }, - "eth-trezor-keyring>hdkey": { - "packages": { - "browserify>assert": true, - "browserify>crypto-browserify": true, - "eth-trezor-keyring>hdkey>coinstring": true, - "eth-trezor-keyring>hdkey>secp256k1": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "eth-trezor-keyring>hdkey>coinstring": { - "packages": { - "browserify>buffer": true, - "eth-trezor-keyring>hdkey>coinstring>bs58": true, - "ethereumjs-util>create-hash": true - } - }, - "eth-trezor-keyring>hdkey>secp256k1": { - "packages": { - "bn.js": true, - "browserify>insert-module-globals>is-buffer": true, - "eth-trezor-keyring>hdkey>secp256k1>bip66": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "eth-trezor-keyring>hdkey>secp256k1>bip66": { - "packages": { - "ethereumjs-wallet>safe-buffer": true - } - }, - "eth-trezor-keyring>trezor-connect": { - "globals": { - "__TREZOR_CONNECT_SRC": true, - "addEventListener": true, - "btoa": true, - "chrome": true, - "clearInterval": true, - "clearTimeout": true, - "console": true, - "document.body": true, - "document.createElement": true, - "document.createTextNode": true, - "document.getElementById": true, - "document.querySelectorAll": true, - "location": true, - "navigator": true, - "open": true, - "removeEventListener": true, - "setInterval": true, - "setTimeout": true - }, - "packages": { - "@babel/runtime": true, - "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>events": true, - "eth-trezor-keyring>trezor-connect>cross-fetch": true - } - }, - "eth-trezor-keyring>trezor-connect>cross-fetch": { - "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true - } - }, - "ethereumjs-abi": { - "packages": { - "bn.js": true, - "browserify>buffer": true, - "ethereumjs-abi>ethereumjs-util": true - } - }, - "ethereumjs-abi>ethereumjs-util": { - "packages": { - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-abi>ethereumjs-util>ethjs-util": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ganache>secp256k1>elliptic": true - } - }, - "ethereumjs-abi>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "ethereumjs-util": { - "packages": { - "browserify>assert": true, - "browserify>buffer": true, - "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util>bn.js": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true - } - }, - "ethereumjs-util>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "ethereumjs-util>create-hash": { - "packages": { - "addons-linter>sha.js": true, - "ethereumjs-util>create-hash>cipher-base": true, - "ethereumjs-util>create-hash>md5.js": true, - "ethereumjs-util>create-hash>ripemd160": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>create-hash>cipher-base": { - "packages": { - "browserify>stream-browserify": true, - "browserify>string_decoder": true, - "ethereumjs-wallet>safe-buffer": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>create-hash>md5.js": { - "packages": { - "ethereumjs-util>create-hash>md5.js>hash-base": true, - "ethereumjs-wallet>safe-buffer": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>create-hash>md5.js>hash-base": { - "packages": { - "ethereumjs-util>create-hash>md5.js>hash-base>readable-stream": true, - "ethereumjs-wallet>safe-buffer": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>create-hash>md5.js>hash-base>readable-stream": { - "packages": { - "@storybook/api>util-deprecate": true, - "browserify>browser-resolve": true, - "browserify>buffer": true, - "browserify>events": true, - "browserify>process": true, - "browserify>string_decoder": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>create-hash>ripemd160": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>create-hash>md5.js>hash-base": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "ethereumjs-util>ethereum-cryptography>browserify-aes": { - "packages": { - "browserify>buffer": true, - "browserify>crypto-browserify>browserify-cipher>evp_bytestokey": true, - "ethereumjs-util>create-hash>cipher-base": true, - "ethereumjs-util>ethereum-cryptography>browserify-aes>buffer-xor": true, - "ethereumjs-wallet>safe-buffer": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>ethereum-cryptography>browserify-aes>buffer-xor": { - "packages": { - "browserify>buffer": true - } - }, - "ethereumjs-util>ethereum-cryptography>hash.js": { - "packages": { - "ganache>secp256k1>elliptic>minimalistic-assert": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>ethereum-cryptography>keccak": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak>readable-stream": true - } - }, - "ethereumjs-util>ethereum-cryptography>keccak>readable-stream": { - "packages": { - "@storybook/api>util-deprecate": true, - "browserify>browser-resolve": true, - "browserify>buffer": true, - "browserify>events": true, - "browserify>process": true, - "browserify>string_decoder": true, - "pumpify>inherits": true - } - }, - "ethereumjs-util>ethereum-cryptography>scrypt-js": { - "globals": { - "define": true, - "setTimeout": true - }, - "packages": { - "browserify>timers-browserify": true - } - }, - "ethereumjs-util>ethereum-cryptography>secp256k1": { - "packages": { - "ganache>secp256k1>elliptic": true - } - }, - "ethereumjs-util>rlp": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>rlp>bn.js": true - } - }, - "ethereumjs-util>rlp>bn.js": { - "globals": { - "Buffer": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "ethereumjs-wallet": { - "packages": { - "@truffle/codec>utf8": true, - "browserify>crypto-browserify": true, - "ethereumjs-wallet>aes-js": true, - "ethereumjs-wallet>bs58check": true, - "ethereumjs-wallet>ethereumjs-util": true, - "ethereumjs-wallet>randombytes": true, - "ethereumjs-wallet>safe-buffer": true, - "ethereumjs-wallet>scryptsy": true, - "ethereumjs-wallet>uuid": true - } - }, - "ethereumjs-wallet>aes-js": { - "globals": { - "define": true - } - }, - "ethereumjs-wallet>bs58check": { - "packages": { - "ethereumjs-util>create-hash": true, - "ethereumjs-wallet>bs58check>bs58": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "ethereumjs-wallet>bs58check>bs58": { - "packages": { - "ethereumjs-wallet>bs58check>bs58>base-x": true - } - }, - "ethereumjs-wallet>bs58check>bs58>base-x": { - "packages": { - "ethereumjs-wallet>safe-buffer": true - } - }, - "ethereumjs-wallet>ethereumjs-util": { - "packages": { - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>ethereum-cryptography": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>ethereumjs-util>ethjs-util": true, - "ganache>secp256k1>elliptic": true - } - }, - "ethereumjs-wallet>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "ethereumjs-wallet>randombytes": { - "globals": { - "crypto": true, - "msCrypto": true - }, - "packages": { - "browserify>process": true, - "ethereumjs-wallet>safe-buffer": true - } - }, - "ethereumjs-wallet>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "ethereumjs-wallet>scryptsy": { - "packages": { - "browserify>buffer": true, - "browserify>crypto-browserify>pbkdf2": true - } - }, - "ethereumjs-wallet>uuid": { - "globals": { - "crypto": true, - "msCrypto": true - } - }, - "ethers>@ethersproject/random": { - "globals": { - "crypto.getRandomValues": true - } - }, - "ethjs": { - "globals": { - "clearInterval": true, - "setInterval": true - }, - "packages": { - "browserify>buffer": true, - "ethjs-contract": true, - "ethjs-query": true, - "ethjs>bn.js": true, - "ethjs>ethjs-abi": true, - "ethjs>ethjs-filter": true, - "ethjs>ethjs-provider-http": true, - "ethjs>ethjs-unit": true, - "ethjs>ethjs-util": true, - "ethjs>js-sha3": true, - "ethjs>number-to-bn": true - } - }, - "ethjs-contract": { - "packages": { - "ethjs-contract>ethjs-abi": true, - "ethjs-query>babel-runtime": true, - "ethjs>ethjs-filter": true, - "ethjs>ethjs-util": true, - "ethjs>js-sha3": true, - "promise-to-callback": true - } - }, - "ethjs-contract>ethjs-abi": { - "packages": { - "browserify>buffer": true, - "ethjs-contract>ethjs-abi>bn.js": true, - "ethjs>js-sha3": true, - "ethjs>number-to-bn": true - } - }, - "ethjs-query": { - "globals": { - "console": true - }, - "packages": { - "ethjs-query>ethjs-format": true, - "ethjs-query>ethjs-rpc": true, - "promise-to-callback": true - } - }, - "ethjs-query>babel-runtime": { - "packages": { - "@babel/runtime": true, - "@storybook/api>regenerator-runtime": true, - "ethjs-query>babel-runtime>core-js": true - } - }, - "ethjs-query>babel-runtime>core-js": { - "globals": { - "PromiseRejectionEvent": true, - "__e": "write", - "__g": "write", - "document.createTextNode": true, - "postMessage": true, - "setTimeout": true - } - }, - "ethjs-query>ethjs-format": { - "packages": { - "ethjs-query>ethjs-format>ethjs-schema": true, - "ethjs>ethjs-util": true, - "ethjs>ethjs-util>strip-hex-prefix": true, - "ethjs>number-to-bn": true - } - }, - "ethjs-query>ethjs-rpc": { - "packages": { - "promise-to-callback": true - } - }, - "ethjs>ethjs-abi": { - "packages": { - "browserify>buffer": true, - "ethjs>bn.js": true, - "ethjs>js-sha3": true, - "ethjs>number-to-bn": true - } - }, - "ethjs>ethjs-filter": { - "globals": { - "clearInterval": true, - "setInterval": true - } - }, - "ethjs>ethjs-provider-http": { - "packages": { - "ethjs>ethjs-provider-http>xhr2": true - } - }, - "ethjs>ethjs-provider-http>xhr2": { - "globals": { - "XMLHttpRequest": true - } - }, - "ethjs>ethjs-unit": { - "packages": { - "ethjs>ethjs-unit>bn.js": true, - "ethjs>number-to-bn": true - } - }, - "ethjs>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, - "ethjs>ethjs-util>strip-hex-prefix": { - "packages": { - "ethjs>ethjs-util>is-hex-prefixed": true - } - }, - "ethjs>js-sha3": { - "packages": { - "browserify>process": true - } - }, - "ethjs>number-to-bn": { - "packages": { - "ethjs>ethjs-util>strip-hex-prefix": true, - "ethjs>number-to-bn>bn.js": true - } - }, - "extension-port-stream": { - "packages": { - "browserify>buffer": true, - "browserify>stream-browserify": true - } - }, - "fast-json-patch": { - "globals": { - "addEventListener": true, - "clearTimeout": true, - "removeEventListener": true, - "setTimeout": true - } - }, - "fuse.js": { - "globals": { - "console": true, - "define": true - } - }, - "ganache>secp256k1>elliptic": { - "packages": { - "bn.js": true, - "ethereumjs-util>ethereum-cryptography>hash.js": true, - "ganache>secp256k1>elliptic>brorand": true, - "ganache>secp256k1>elliptic>hmac-drbg": true, - "ganache>secp256k1>elliptic>minimalistic-assert": true, - "ganache>secp256k1>elliptic>minimalistic-crypto-utils": true, - "pumpify>inherits": true - } - }, - "ganache>secp256k1>elliptic>brorand": { - "globals": { - "crypto": true, - "msCrypto": true - }, - "packages": { - "browserify>browser-resolve": true - } - }, - "ganache>secp256k1>elliptic>hmac-drbg": { - "packages": { - "ethereumjs-util>ethereum-cryptography>hash.js": true, - "ganache>secp256k1>elliptic>minimalistic-assert": true, - "ganache>secp256k1>elliptic>minimalistic-crypto-utils": true - } - }, - "globalthis>define-properties": { - "packages": { - "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true - } - }, - "globalthis>define-properties>has-property-descriptors": { - "packages": { - "string.prototype.matchall>get-intrinsic": true - } - }, - "json-rpc-engine": { - "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true - } - }, - "json-rpc-engine>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true - } - }, - "json-rpc-middleware-stream": { - "globals": { - "console.warn": true, - "setTimeout": true - }, - "packages": { - "json-rpc-engine>@metamask/safe-event-emitter": true, - "readable-stream": true - } - }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, - "koa>is-generator-function>has-tostringtag": { - "packages": { - "string.prototype.matchall>has-symbols": true - } - }, - "lavamoat>json-stable-stringify": { - "packages": { - "lavamoat>json-stable-stringify>jsonify": true - } - }, - "localforage": { - "globals": { - "Blob": true, - "BlobBuilder": true, - "FileReader": true, - "IDBKeyRange": true, - "MSBlobBuilder": true, - "MozBlobBuilder": true, - "OIndexedDB": true, - "WebKitBlobBuilder": true, - "atob": true, - "btoa": true, - "console.error": true, - "console.info": true, - "console.warn": true, - "define": true, - "fetch": true, - "indexedDB": true, - "localStorage": true, - "mozIndexedDB": true, - "msIndexedDB": true, - "navigator.platform": true, - "navigator.userAgent": true, - "openDatabase": true, - "setTimeout": true, - "webkitIndexedDB": true - } - }, - "lodash": { - "globals": { - "clearTimeout": true, - "define": true, - "setTimeout": true - } - }, - "loglevel": { - "globals": { - "console": true, - "define": true, - "document.cookie": true, - "localStorage": true, - "log": "write", - "navigator": true - } - }, - "luxon": { - "globals": { - "Intl": true - } - }, - "nanoid": { - "globals": { - "crypto": true, - "msCrypto": true, - "navigator": true - } - }, - "nock>debug": { - "globals": { - "console": true, - "document": true, - "localStorage": true, - "navigator": true, - "process": true - }, - "packages": { - "browserify>process": true, - "nock>debug>ms": true - } - }, - "node-fetch": { - "globals": { - "Headers": true, - "Request": true, - "Response": true, - "fetch": true - } - }, - "nonce-tracker": { - "packages": { - "await-semaphore": true, - "browserify>assert": true, - "ethjs-query": true - } - }, - "obj-multiplex": { - "globals": { - "console.warn": true - }, - "packages": { - "end-of-stream": true, - "pump>once": true, - "readable-stream": true - } - }, - "promise-to-callback": { - "packages": { - "promise-to-callback>is-fn": true, - "promise-to-callback>set-immediate-shim": true - } - }, - "promise-to-callback>set-immediate-shim": { - "globals": { - "setTimeout.apply": true - }, - "packages": { - "browserify>timers-browserify": true - } - }, - "prop-types": { - "globals": { - "console": true - }, - "packages": { - "prop-types>react-is": true, - "react>object-assign": true - } - }, - "prop-types>react-is": { - "globals": { - "console": true - } - }, - "pubnub": { - "globals": { - "ActiveXObject": true, - "XMLHttpRequest": true, - "addEventListener": true, - "btoa": true, - "clearInterval": true, - "clearTimeout": true, - "console": true, - "define": true, - "localStorage.getItem": true, - "localStorage.setItem": true, - "location": true, - "navigator": true, - "setInterval": true, - "setTimeout": true - } - }, - "pubnub>cbor-sync": { - "globals": { - "define": true - }, - "packages": { - "browserify>buffer": true - } - }, - "pump": { - "packages": { - "browserify>browser-resolve": true, - "browserify>process": true, - "end-of-stream": true, - "pump>once": true - } - }, - "pump>once": { - "packages": { - "pump>once>wrappy": true - } - }, - "qrcode-generator": { - "globals": { - "define": true - } - }, - "qrcode.react": { - "globals": { - "Path2D": true, - "devicePixelRatio": true - }, - "packages": { - "prop-types": true, - "qrcode.react>qr.js": true, - "react": true - } - }, - "react": { - "globals": { - "console": true - }, - "packages": { - "prop-types": true, - "react>object-assign": true - } - }, - "react-devtools": { - "packages": { - "react-devtools>react-devtools-core": true - } - }, - "react-devtools>react-devtools-core": { - "globals": { - "WebSocket": true, - "setTimeout": true - } - }, - "react-dnd-html5-backend": { - "globals": { - "addEventListener": true, - "clearTimeout": true, - "removeEventListener": true - } - }, - "react-dom": { - "globals": { - "HTMLIFrameElement": true, - "MSApp": true, - "__REACT_DEVTOOLS_GLOBAL_HOOK__": true, - "addEventListener": true, - "clearTimeout": true, - "clipboardData": true, - "console": true, - "dispatchEvent": true, - "document": true, - "event": "write", - "jest": true, - "location.protocol": true, - "navigator.userAgent.indexOf": true, - "performance": true, - "removeEventListener": true, - "self": true, - "setTimeout": true, - "top": true, - "trustedTypes": true - }, - "packages": { - "prop-types": true, - "react": true, - "react-dom>scheduler": true, - "react>object-assign": true - } - }, - "react-dom>scheduler": { - "globals": { - "MessageChannel": true, - "cancelAnimationFrame": true, - "clearTimeout": true, - "console": true, - "navigator": true, - "performance": true, - "requestAnimationFrame": true, - "setTimeout": true - } - }, - "react-idle-timer": { - "globals": { - "clearTimeout": true, - "document": true, - "setTimeout": true - }, - "packages": { - "prop-types": true, - "react": true - } - }, - "react-inspector": { - "globals": { - "Node.CDATA_SECTION_NODE": true, - "Node.COMMENT_NODE": true, - "Node.DOCUMENT_FRAGMENT_NODE": true, - "Node.DOCUMENT_NODE": true, - "Node.DOCUMENT_TYPE_NODE": true, - "Node.ELEMENT_NODE": true, - "Node.PROCESSING_INSTRUCTION_NODE": true, - "Node.TEXT_NODE": true - }, - "packages": { - "ethjs-query>babel-runtime": true, - "prop-types": true, - "react": true, - "react-inspector>is-dom": true - } - }, - "react-inspector>is-dom": { - "globals": { - "Node": true - }, - "packages": { - "@lavamoat/snow>is-cross-origin>is-window": true, - "proxyquire>fill-keys>is-object": true - } - }, - "react-popper": { - "globals": { - "document": true - }, - "packages": { - "@popperjs/core": true, - "react": true, - "react-popper>react-fast-compare": true, - "react-popper>warning": true - } - }, - "react-popper>react-fast-compare": { - "globals": { - "Element": true, - "console.warn": true - } - }, - "react-popper>warning": { - "globals": { - "console": true - } - }, - "react-redux": { - "globals": { - "console": true, - "document": true - }, - "packages": { - "@babel/runtime": true, - "prop-types": true, - "prop-types>react-is": true, - "react": true, - "react-dom": true, - "react-redux>hoist-non-react-statics": true, - "redux": true - } - }, - "react-redux>hoist-non-react-statics": { - "packages": { - "prop-types>react-is": true - } - }, - "react-responsive-carousel": { - "globals": { - "HTMLElement": true, - "addEventListener": true, - "clearTimeout": true, - "console.warn": true, - "document": true, - "getComputedStyle": true, - "removeEventListener": true, - "setTimeout": true - }, - "packages": { - "classnames": true, - "react": true, - "react-dom": true, - "react-responsive-carousel>react-easy-swipe": true - } - }, - "react-responsive-carousel>react-easy-swipe": { - "globals": { - "addEventListener": true, - "define": true, - "document.addEventListener": true, - "document.removeEventListener": true - }, - "packages": { - "prop-types": true, - "react": true - } - }, - "react-router-dom": { - "packages": { - "prop-types": true, - "react": true, - "react-router-dom>history": true, - "react-router-dom>react-router": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true - } - }, - "react-router-dom>history": { - "globals": { - "addEventListener": true, - "confirm": true, - "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true - }, - "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true - } - }, - "react-router-dom>react-router": { - "packages": { - "prop-types": true, - "prop-types>react-is": true, - "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "sinon>nise>path-to-regexp": true - } - }, - "react-router-dom>react-router>history": { - "globals": { - "addEventListener": true, - "confirm": true, - "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true - }, - "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true - } - }, - "react-router-dom>react-router>mini-create-react-context": { - "packages": { - "@babel/runtime": true, - "prop-types": true, - "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true - } - }, - "react-router-dom>tiny-warning": { - "globals": { - "console": true - } - }, - "react-simple-file-input": { - "globals": { - "File": true, - "FileReader": true, - "console.warn": true - }, - "packages": { - "prop-types": true, - "react": true - } - }, - "react-tippy": { - "globals": { - "Element": true, - "MSStream": true, - "MutationObserver": true, - "addEventListener": true, - "clearTimeout": true, - "console.error": true, - "console.warn": true, - "define": true, - "document": true, - "getComputedStyle": true, - "innerHeight": true, - "innerWidth": true, - "navigator.maxTouchPoints": true, - "navigator.msMaxTouchPoints": true, - "navigator.userAgent": true, - "performance": true, - "requestAnimationFrame": true, - "setTimeout": true - }, - "packages": { - "react": true, - "react-dom": true, - "react-tippy>popper.js": true - } - }, - "react-tippy>popper.js": { - "globals": { - "MSInputMethodContext": true, - "Node.DOCUMENT_POSITION_FOLLOWING": true, - "cancelAnimationFrame": true, - "console.warn": true, - "define": true, - "devicePixelRatio": true, - "document": true, - "getComputedStyle": true, - "innerHeight": true, - "innerWidth": true, - "navigator.userAgent": true, - "requestAnimationFrame": true, - "setTimeout": true - } - }, - "react-toggle-button": { - "globals": { - "clearTimeout": true, - "console.warn": true, - "define": true, - "performance": true, - "setTimeout": true - }, - "packages": { - "react": true - } - }, - "react-transition-group": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "prop-types": true, - "react": true, - "react-dom": true, - "react-transition-group>chain-function": true, - "react-transition-group>dom-helpers": true, - "react-transition-group>warning": true - } - }, - "react-transition-group>dom-helpers": { - "globals": { - "document": true, - "setTimeout": true - }, - "packages": { - "@babel/runtime": true - } - }, - "react-transition-group>warning": { - "globals": { - "console": true - } - }, - "readable-stream": { - "packages": { - "@storybook/api>util-deprecate": true, - "browserify>browser-resolve": true, - "browserify>events": true, - "browserify>process": true, - "browserify>timers-browserify": true, - "pumpify>inherits": true, - "readable-stream>core-util-is": true, - "readable-stream>isarray": true, - "readable-stream>process-nextick-args": true, - "readable-stream>safe-buffer": true, - "readable-stream>string_decoder": true - } - }, - "readable-stream>core-util-is": { - "packages": { - "browserify>insert-module-globals>is-buffer": true - } - }, - "readable-stream>process-nextick-args": { - "packages": { - "browserify>process": true - } - }, - "readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "readable-stream>string_decoder": { - "packages": { - "readable-stream>safe-buffer": true - } - }, - "redux": { - "globals": { - "console": true - }, - "packages": { - "@babel/runtime": true - } - }, - "safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>util": true, - "webpack>events": true - } - }, - "semver": { - "globals": { - "console.error": true - }, - "packages": { - "browserify>process": true, - "semver>lru-cache": true - } - }, - "semver>lru-cache": { - "packages": { - "semver>lru-cache>yallist": true - } - }, - "sinon>nise>path-to-regexp": { - "packages": { - "sinon>nise>path-to-regexp>isarray": true - } - }, - "string.prototype.matchall>call-bind": { - "packages": { - "mocha>object.assign>function-bind": true, - "string.prototype.matchall>get-intrinsic": true - } - }, - "string.prototype.matchall>get-intrinsic": { - "globals": { - "AggregateError": true, - "FinalizationRegistry": true, - "WeakRef": true - }, - "packages": { - "browserify>has": true, - "mocha>object.assign>function-bind": true, - "string.prototype.matchall>has-symbols": true - } - }, - "string.prototype.matchall>regexp.prototype.flags": { - "packages": { - "globalthis>define-properties": true, - "string.prototype.matchall>call-bind": true, - "string.prototype.matchall>regexp.prototype.flags>functions-have-names": true - } - }, - "uuid": { - "globals": { - "crypto": true, - "msCrypto": true - } - }, - "vinyl>clone": { - "packages": { - "browserify>buffer": true - } - }, - "wait-on>rxjs>tslib": { - "globals": { - "define": true - } - }, - "web3": { - "globals": { - "XMLHttpRequest": true - } - }, - "web3-stream-provider": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>util": true, - "readable-stream": true, - "web3-stream-provider>uuid": true - } - }, - "web3-stream-provider>uuid": { - "globals": { - "crypto": true, - "msCrypto": true - } - }, - "webextension-polyfill": { - "globals": { - "browser": true, - "chrome": true, - "console.error": true, - "console.warn": true, - "define": true - } - }, - "webpack>events": { - "globals": { - "console": true - } - } - } -} \ No newline at end of file diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 871dcc172..985346c52 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -4405,11 +4405,11 @@ "packages": { "gulp-watch>chokidar>braces>snapdragon>base>cache-base": true, "gulp-watch>chokidar>braces>snapdragon>base>class-utils": true, + "gulp-watch>chokidar>braces>snapdragon>base>component-emitter": true, "gulp-watch>chokidar>braces>snapdragon>base>define-property": true, "gulp-watch>chokidar>braces>snapdragon>base>mixin-deep": true, "gulp-watch>chokidar>braces>snapdragon>base>pascalcase": true, - "gulp>gulp-cli>isobject": true, - "pubnub>superagent>component-emitter": true + "gulp>gulp-cli>isobject": true } }, "gulp-watch>chokidar>braces>snapdragon>base>cache-base": { @@ -4420,9 +4420,9 @@ "gulp-watch>chokidar>braces>snapdragon>base>cache-base>to-object-path": true, "gulp-watch>chokidar>braces>snapdragon>base>cache-base>union-value": true, "gulp-watch>chokidar>braces>snapdragon>base>cache-base>unset-value": true, + "gulp-watch>chokidar>braces>snapdragon>base>component-emitter": true, "gulp>gulp-cli>array-sort>get-value": true, - "gulp>gulp-cli>isobject": true, - "pubnub>superagent>component-emitter": true + "gulp>gulp-cli>isobject": true } }, "gulp-watch>chokidar>braces>snapdragon>base>cache-base>collection-visit": { diff --git a/package.json b/package.json index df2bfa370..9d20895f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "10.28.3", + "version": "10.29.0", "private": true, "repository": { "type": "git", @@ -21,6 +21,7 @@ "build:test": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 PORTFOLIO_URL=http://127.0.0.1:8080 yarn build test", "build:test:flask": "yarn build test --build-type flask", "build:test:mv3": "ENABLE_MV3=true SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 PORTFOLIO_URL=http://127.0.0.1:8080 yarn build test", + "build:test:dev:mv3": "ENABLE_MV3=true SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 PORTFOLIO_URL=http://127.0.0.1:8080 yarn build:dev testDev --apply-lavamoat=false", "test": "yarn lint && yarn test:unit && yarn test:unit:jest", "dapp": "node development/static-server.js node_modules/@metamask/test-dapp/dist --port 8080", "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'", @@ -32,6 +33,7 @@ "test:unit:mocha": "node ./test/run-unit-tests.js --mocha", "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:snaps": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --snaps", + "test:e2e:chrome:mv3": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mv3", "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", "test:e2e:firefox:snaps": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js --snaps", "test:e2e:single": "node test/e2e/run-e2e-test.js", @@ -93,12 +95,9 @@ }, "resolutions": { "analytics-node/axios": "^0.21.2", - "cookiejar": "^2.1.4", "ganache-core/lodash": "^4.17.21", "netmask": "^2.0.1", - "pubnub/superagent-proxy": "^3.0.0", "json-schema": "^0.4.0", - "simple-get": "^4.0.1", "ast-types": "^0.14.2", "web3-provider-engine/eth-json-rpc-filters": "^6.0.0", "typescript@~4.4.0": "patch:typescript@npm:4.4.4#.yarn/patches/typescript-npm-4.4.4-3fedcc07a3.patch", @@ -201,7 +200,10 @@ "async-done@~1.3.2": "patch:async-done@npm%3A1.3.2#./.yarn/patches/async-done-npm-1.3.2-1f0a4a8997.patch", "async-done@^1.2.0": "patch:async-done@npm%3A1.3.2#./.yarn/patches/async-done-npm-1.3.2-1f0a4a8997.patch", "async-done@^1.2.2": "patch:async-done@npm%3A1.3.2#./.yarn/patches/async-done-npm-1.3.2-1f0a4a8997.patch", - "fast-json-patch@^3.1.1": "patch:fast-json-patch@npm%3A3.1.1#./.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch" + "fast-json-patch@^3.1.1": "patch:fast-json-patch@npm%3A3.1.1#./.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch", + "request@^2.83.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", + "request@^2.88.2": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", + "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch" }, "dependencies": { "@babel/runtime": "^7.5.5", @@ -222,40 +224,44 @@ "@material-ui/core": "^4.11.0", "@metamask/address-book-controller": "^1.0.0", "@metamask/announcement-controller": "^1.0.0", - "@metamask/approval-controller": "^1.0.0", + "@metamask/approval-controller": "^2.1.0", "@metamask/assets-controllers": "^4.0.1", "@metamask/base-controller": "^1.0.0", - "@metamask/contract-metadata": "^2.2.0", - "@metamask/controller-utils": "^1.0.0", + "@metamask/contract-metadata": "^2.3.1", + "@metamask/controller-utils": "^3.1.0", "@metamask/design-tokens": "^1.9.0", "@metamask/desktop": "^0.3.0", - "@metamask/eth-json-rpc-infura": "^7.0.0", - "@metamask/eth-json-rpc-middleware": "^10.0.0", + "@metamask/eth-json-rpc-infura": "^8.0.0", + "@metamask/eth-json-rpc-middleware": "^11.0.0", + "@metamask/eth-json-rpc-provider": "^1.0.0", "@metamask/eth-keyring-controller": "^10.0.1", "@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/etherscan-link": "^2.2.0", "@metamask/gas-fee-controller": "^1.0.0", "@metamask/jazzicon": "^2.0.0", - "@metamask/key-tree": "^6.2.1", + "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", + "@metamask/message-manager": "^2.1.0", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/notification-controller": "^1.0.0", - "@metamask/obs-store": "^5.0.0", - "@metamask/permission-controller": "^2.0.0", + "@metamask/obs-store": "^8.0.0", + "@metamask/permission-controller": "^3.1.0", "@metamask/phishing-controller": "^2.0.0", "@metamask/post-message-stream": "^6.0.0", "@metamask/providers": "^10.2.1", "@metamask/rate-limit-controller": "^1.0.0", - "@metamask/rpc-methods": "^0.30.0", + "@metamask/rpc-methods": "^0.32.2", + "@metamask/safe-event-emitter": "^2.0.0", "@metamask/scure-bip39": "^2.0.3", "@metamask/slip44": "^2.1.0", "@metamask/smart-transactions-controller": "^3.1.0", - "@metamask/snaps-controllers": "^0.30.0", - "@metamask/snaps-ui": "^0.30.0", - "@metamask/snaps-utils": "^0.30.0", + "@metamask/snaps-controllers": "^0.32.2", + "@metamask/snaps-ui": "^0.32.2", + "@metamask/snaps-utils": "^0.32.2", "@metamask/subject-metadata-controller": "^1.0.0", - "@metamask/utils": "^3.6.0", + "@metamask/swappable-obj-proxy": "^2.1.0", + "@metamask/utils": "^5.0.0", "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.6.2", @@ -264,7 +270,6 @@ "@sentry/integrations": "^6.0.0", "@sentry/types": "^6.0.1", "@sentry/utils": "^6.0.1", - "@spruceid/siwe-parser": "^1.1.3", "@truffle/codec": "^0.14.12", "@truffle/decoder": "^5.3.5", "@zxing/browser": "^0.0.10", @@ -281,7 +286,7 @@ "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", "end-of-stream": "^1.4.4", - "eth-block-tracker": "^6.0.0", + "eth-block-tracker": "^7.0.0", "eth-ens-namehash": "^2.0.8", "eth-json-rpc-filters": "^6.0.0", "eth-lattice-keyring": "^0.12.3", @@ -307,7 +312,6 @@ "jest-junit": "^14.0.1", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^4.2.1", - "jsonschema": "^1.2.4", "labeled-stream-splicer": "^2.0.2", "localforage": "^1.9.0", "lodash": "^4.17.21", @@ -319,7 +323,6 @@ "pify": "^5.0.0", "promise-to-callback": "^1.0.0", "prop-types": "^15.6.1", - "pubnub": "4.27.3", "pump": "^3.0.0", "punycode": "^2.1.1", "qrcode-generator": "1.4.1", @@ -342,10 +345,8 @@ "redux-thunk": "^2.3.0", "remove-trailing-slash": "^0.1.1", "reselect": "^3.0.1", - "safe-event-emitter": "^1.0.1", "ses": "^0.12.4", "single-call-balance-checker-abi": "^1.0.0", - "swappable-obj-proxy": "^1.1.0", "unicode-confusables": "^0.1.1", "uuid": "^8.3.2", "valid-url": "^1.0.9", @@ -507,7 +508,7 @@ "redux-mock-store": "^1.5.4", "remote-redux-devtools": "^0.5.16", "require-from-string": "^2.0.2", - "resolve-url-loader": "^3.1.2", + "resolve-url-loader": "^3.1.5", "sass": "^1.32.4", "sass-loader": "^10.1.1", "selenium-webdriver": "^4.3.1", diff --git a/shared/constants/hardware-wallets.ts b/shared/constants/hardware-wallets.ts index 71cdeb038..4f549aa2f 100644 --- a/shared/constants/hardware-wallets.ts +++ b/shared/constants/hardware-wallets.ts @@ -1,15 +1,11 @@ /** - * Accounts can be instantiated from simple, HD or the multiple hardware wallet - * keyring types. Both simple and HD are treated as default but we do special - * case accounts managed by a hardware wallet. + * Hardware wallets supported by MetaMask. */ -export enum HardwareKeyringTypes { +export enum HardwareKeyringType { ledger = 'Ledger Hardware', trezor = 'Trezor Hardware', lattice = 'Lattice Hardware', qr = 'QR Hardware Wallet Device', - hdKeyTree = 'HD Key Tree', - imported = 'Simple Key Pair', } export enum HardwareKeyringNames { diff --git a/shared/constants/keyring.ts b/shared/constants/keyring.ts new file mode 100644 index 000000000..f285fa468 --- /dev/null +++ b/shared/constants/keyring.ts @@ -0,0 +1,17 @@ +import { HardwareKeyringType } from './hardware-wallets'; + +/** + * These are the keyrings that are managed entirely by MetaMask. + */ +export enum InternalKeyringType { + hdKeyTree = 'HD Key Tree', + imported = 'Simple Key Pair', +} + +/** + * All keyrings supported by MetaMask. + */ +export const KeyringType = { + ...HardwareKeyringType, + ...InternalKeyringType, +}; diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js deleted file mode 100644 index a9691d5a2..000000000 --- a/shared/constants/metametrics.js +++ /dev/null @@ -1,478 +0,0 @@ -// Type Imports -/** - * @typedef {import('../../shared/constants/app').EnvironmentType} EnvironmentType - */ - -// Type Declarations -/** - * Used to attach context of where the user was at in the application when the - * event was triggered. Also included as full details of the current page in - * page events. - * - * @typedef {object} MetaMetricsPageObject - * @property {string} [path] - the path of the current page (e.g /home) - * @property {string} [title] - the title of the current page (e.g 'home') - * @property {string} [url] - the fully qualified url of the current page - */ - -/** - * For metamask, this is the dapp that triggered an interaction - * - * @typedef {object} MetaMetricsReferrerObject - * @property {string} [url] - the origin of the dapp issuing the - * notification - */ - -/** - * We attach context to every meta metrics event that help to qualify our - * analytics. This type has all optional values because it represents a - * returned object from a method call. Ideally app and userAgent are - * defined on every event. This is confirmed in the getTrackMetaMetricsEvent - * function, but still provides the consumer a way to override these values if - * necessary. - * - * @typedef {object} MetaMetricsContext - * @property {object} app - Application metadata. - * @property {string} app.name - the name of the application tracking the event - * @property {string} app.version - the version of the application - * @property {string} userAgent - the useragent string of the user - * @property {MetaMetricsPageObject} [page] - an object representing details of - * the current page - * @property {MetaMetricsReferrerObject} [referrer] - for metamask, this is the - * dapp that triggered an interaction - */ - -/** - * @typedef {object} MetaMetricsEventPayload - * @property {string} event - event name to track - * @property {string} category - category to associate event to - * @property {number} [actionId] - Action id to deduplicate event requests from - * the UI - * @property {string} [environmentType] - The type of environment this event - * occurred in. Defaults to the background process type - * @property {object} [properties] - object of custom values to track, keys - * in this object must be in snake_case - * @property {object} [sensitiveProperties] - Object of sensitive values to - * track. Keys in this object must be in snake_case. These properties will be - * sent in an additional event that excludes the user's metaMetricsId - * @property {number} [revenue] - amount of currency that event creates in - * revenue for MetaMask - * @property {string} [currency] - ISO 4127 format currency for events with - * revenue, defaults to US dollars - * @property {number} [value] - Abstract business "value" attributable to - * customers who trigger this event - * @property {MetaMetricsPageObject} [page] - the page/route that the event - * occurred on - * @property {MetaMetricsReferrerObject} [referrer] - the origin of the dapp - * that triggered the event - */ - -/** - * @typedef {object} MetaMetricsEventOptions - * @property {boolean} [isOptIn] - happened during opt in/out workflow - * @property {boolean} [flushImmediately] - When true will automatically flush - * the segment queue after tracking the event. Recommended if the result of - * tracking the event must be known before UI transition or update - * @property {boolean} [excludeMetaMetricsId] - whether to exclude the user's - * metametrics id for anonymity - * @property {string} [metaMetricsId] - an override for the metaMetricsId in - * the event one is created as part of an asynchronous workflow, such as - * awaiting the result of the metametrics opt-in function that generates the - * user's metametrics id - * @property {boolean} [matomoEvent] - is this event a holdover from matomo - * that needs further migration? when true, sends the data to a special - * segment source that marks the event data as not conforming to our schema - */ - -/** - * @typedef {object} MetaMetricsEventFragment - * @property {string} successEvent - The event name to fire when the fragment - * is closed in an affirmative action. - * @property {string} [failureEvent] - The event name to fire when the fragment - * is closed with a rejection. - * @property {string} [initialEvent] - An event name to fire immediately upon - * fragment creation. This is useful for building funnels in mixpanel and for - * reduction of code duplication. - * @property {string} category - the event category to use for both the success - * and failure events - * @property {boolean} [persist] - Should this fragment be persisted in - * state and progressed after the extension is locked and unlocked. - * @property {number} [timeout] - Time in seconds the event should be persisted - * for. After the timeout the fragment will be closed as abandoned. if not - * supplied the fragment is stored indefinitely. - * @property {number} [lastUpdated] - Date.now() when the fragment was last - * updated. Used to determine if the timeout has expired and the fragment - * should be closed. - * @property {object} [properties] - Object of custom values to track, keys in - * this object must be in snake_case. - * @property {object} [sensitiveProperties] - Object of sensitive values to - * track. Keys in this object must be in snake_case. These properties will be - * sent in an additional event that excludes the user's metaMetricsId - * @property {number} [revenue] - amount of currency that event creates in - * revenue for MetaMask if fragment is successful. - * @property {string} [currency] - ISO 4127 format currency for events with - * revenue, defaults to US dollars - * @property {number} [value] - Abstract business "value" attributable to - * customers who successfully complete this fragment - * @property {MetaMetricsPageObject} [page] - the page/route that the event - * occurred on - * @property {MetaMetricsReferrerObject} [referrer] - the origin of the dapp - * that initiated the event fragment. - * @property {string} [uniqueIdentifier] - optional argument to override the - * automatic generation of UUID for the event fragment. This is useful when - * tracking events for subsystems that already generate UUIDs so to avoid - * unnecessary lookups and reduce accidental duplication. - */ - -/** - * Represents the shape of data sent to the segment.track method. - * - * @typedef {object} SegmentEventPayload - * @property {string} [userId] - The metametrics id for the user - * @property {string} [anonymousId] - An anonymousId that is used to track - * sensitive data while preserving anonymity. - * @property {string} event - name of the event to track - * @property {object} properties - properties to attach to the event - * @property {MetaMetricsContext} context - the context the event occurred in - */ - -/** - * @typedef {object} MetaMetricsPagePayload - * @property {string} name - The name of the page that was viewed - * @property {object} [params] - The variadic parts of the page url - * example (route: `/asset/:asset`, path: `/asset/ETH`) - * params: { asset: 'ETH' } - * @property {EnvironmentType} environmentType - the environment type that the - * page was viewed in - * @property {MetaMetricsPageObject} [page] - the details of the page - * @property {MetaMetricsReferrerObject} [referrer] - dapp that triggered the page - * view - */ - -/** - * @typedef {object} MetaMetricsPageOptions - * @property {boolean} [isOptInPath] - is the current path one of the pages in - * the onboarding workflow? If true and participateInMetaMetrics is null track - * the page view - */ - -/** - * @typedef {object} Traits - * @property {'address_book_entries'} ADDRESS_BOOK_ENTRIES - When the user - * adds or modifies addresses in address book the address_book_entries trait - * is identified. - * @property {'ledger_connection_type'} LEDGER_CONNECTION_TYPE - when ledger - * live connnection type is changed we identify the ledger_connection_type - * trait - * @property {'networks_added'} NETWORKS_ADDED - when user modifies networks - * we identify the networks_added trait - * @property {'networks_without_ticker'} NETWORKS_WITHOUT_TICKER - when user - * modifies networks we identify the networks_without_ticker trait for - * networks without a ticker. - * @property {'nft_autodetection_enabled'} NFT_AUTODETECTION_ENABLED - when Autodetect NFTs - * feature is toggled we identify the nft_autodetection_enabled trait - * @property {'number_of_accounts'} NUMBER_OF_ACCOUNTS - when identities - * change, we identify the new number_of_accounts trait - * @property {'number_of_nft_collections'} NUMBER_OF_NFT_COLLECTIONS - user - * trait for number of unique NFT addresses - * @property {'number_of_nfts'} NUMBER_OF_NFTS - user trait for number of all NFT addresses - * @property {'number_of_tokens'} NUMBER_OF_TOKENS - when the number of tokens change, we - * identify the new number_of_tokens trait - * @property {'opensea_api_enabled'} OPENSEA_API_ENABLED - when the OpenSea API is enabled - * we identify the opensea_api_enabled trait - * @property {'three_box_enabled'} THREE_BOX_ENABLED - When 3Box feature is - * toggled we identify the 3box_enabled trait. This trait has been deprecated. - * @property {'theme'} THEME - when the user's theme changes we identify the theme trait - * @property {'token_detection_enabled'} TOKEN_DETECTION_ENABLED - when token detection feature is toggled we - * identify the token_detection_enabled trait - * @property {'install_date_ext'} INSTALL_DATE_EXT - when the user installed the extension - * @property {'desktop_enabled'} [DESKTOP_ENABLED] - optional / does the user have desktop enabled? - * @property {'security_providers'} SECURITY_PROVIDERS - when security provider feature is toggled we - * identify the security_providers trait - */ - -/** - * - * @type {Traits} - */ - -export const TRAITS = { - ADDRESS_BOOK_ENTRIES: 'address_book_entries', - INSTALL_DATE_EXT: 'install_date_ext', - LEDGER_CONNECTION_TYPE: 'ledger_connection_type', - NETWORKS_ADDED: 'networks_added', - NETWORKS_WITHOUT_TICKER: 'networks_without_ticker', - NFT_AUTODETECTION_ENABLED: 'nft_autodetection_enabled', - NUMBER_OF_ACCOUNTS: 'number_of_accounts', - NUMBER_OF_NFT_COLLECTIONS: 'number_of_nft_collections', - NUMBER_OF_NFTS: 'number_of_nfts', - NUMBER_OF_TOKENS: 'number_of_tokens', - OPENSEA_API_ENABLED: 'opensea_api_enabled', - THEME: 'theme', - THREE_BOX_ENABLED: 'three_box_enabled', - TOKEN_DETECTION_ENABLED: 'token_detection_enabled', - DESKTOP_ENABLED: 'desktop_enabled', - SECURITY_PROVIDERS: 'security_providers', -}; - -/** - * @typedef {object} MetaMetricsTraits - * @property {number} [address_book_entries] - The number of entries in the - * user's address book. - * @property {'ledgerLive' | 'webhid' | 'u2f'} [ledger_connection_type] - the - * type of ledger connection set by user preference. - * @property {Array} [networks_added] - An array consisting of chainIds - * that indicate the networks a user has added to their MetaMask. - * @property {Array} [networks_without_ticker] - An array consisting of - * chainIds that indicate the networks added by the user that do not have a - * ticker. - * @property {number} [nft_autodetection_enabled] - does the user have the - * use collection/nft detection enabled? - * @property {number} [number_of_accounts] - A number representing the number - * of identities(accounts) added to the user's MetaMask. - * @property {number} [number_of_nft_collections] - A number representing the - * amount of different NFT collections the user possesses an NFT from. - * @property {number} [number_of_nfts] - A number representing the - * amount of all NFTs the user possesses across all networks and accounts. - * @property {number} [number_of_tokens] - The total number of token contracts - * the user has across all networks and accounts. - * @property {boolean} [opensea_api_enabled] - does the user have the OpenSea - * API enabled? - * @property {boolean} [three_box_enabled] - Does the user have 3box sync - * enabled? (deprecated) - * @property {string} [theme] - which theme the user has selected - * @property {boolean} [token_detection_enabled] - does the user have token detection is enabled? - * @property {boolean} [desktop_enabled] - optional / does the user have desktop enabled? - * @property {Array} [security_providers] - whether security provider feature toggle is on or off - */ - -// Mixpanel converts the zero address value to a truly anonymous event, which -// speeds up reporting -export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'; - -/** - * This object is used to identify events that are triggered by the background - * process. - * - * @type {MetaMetricsPageObject} - */ -export const METAMETRICS_BACKGROUND_PAGE_OBJECT = { - path: '/background-process', - title: 'Background Process', - url: '/background-process', -}; - -/** - * @typedef {object} SegmentInterface - * @property {SegmentEventPayload[]} queue - A queue of events to be sent when - * the flushAt limit has been reached, or flushInterval occurs - * @property {() => void} flush - Immediately flush the queue, resetting it to - * an empty array and sending the pending events to Segment - * @property {( - * payload: SegmentEventPayload, - * callback: (err?: Error) => void - * ) => void} track - Track an event with Segment, using the internal batching - * mechanism to optimize network requests - * @property {(payload: object) => void} page - Track a page view with Segment - * @property {() => void} identify - Identify an anonymous user. We do not - * currently use this method. - */ - -export const REJECT_NOTFICIATION_CLOSE = 'Cancel Via Notification Close'; -export const REJECT_NOTFICIATION_CLOSE_SIG = - 'Cancel Sig Request Via Notification Close'; - -/** - * EVENTS - */ - -export const EVENT_NAMES = { - ACCOUNT_ADDED: 'Account Added', - ACCOUNT_ADD_SELECTED: 'Account Add Selected', - ACCOUNT_ADD_FAILED: 'Account Add Failed', - ACCOUNT_PASSWORD_CREATED: 'Account Password Created', - ACCOUNT_RESET: 'Account Reset', - APP_INSTALLED: 'App Installed', - APP_UNLOCKED: 'App Unlocked', - APP_UNLOCKED_FAILED: 'App Unlocked Failed', - APP_WINDOW_EXPANDED: 'App Window Expanded', - BRIDGE_LINK_CLICKED: 'Bridge Link Clicked', - DECRYPTION_APPROVED: 'Decryption Approved', - DECRYPTION_REJECTED: 'Decryption Rejected', - DECRYPTION_REQUESTED: 'Decryption Requested', - ENCRYPTION_PUBLIC_KEY_APPROVED: 'Encryption Approved', - ENCRYPTION_PUBLIC_KEY_REJECTED: 'Encryption Rejected', - ENCRYPTION_PUBLIC_KEY_REQUESTED: 'Encryption Requested', - EXTERNAL_LINK_CLICKED: 'External Link Clicked', - KEY_EXPORT_SELECTED: 'Key Export Selected', - KEY_EXPORT_REQUESTED: 'Key Export Requested', - KEY_EXPORT_FAILED: 'Key Export Failed', - KEY_EXPORT_CANCELED: 'Key Export Canceled', - KEY_EXPORT_REVEALED: 'Key Material Revealed', - KEY_EXPORT_COPIED: 'Key Material Copied', - KEY_TOKEN_DETECTION_SELECTED: 'Key Token Detection Selected', - KEY_GLOBAL_SECURITY_TOGGLE_SELECTED: 'Key Global Security/Privacy Settings', - KEY_BALANCE_TOKEN_PRICE_CHECKER: - 'Key Show Balance and Token Price Checker Settings', - KEY_GAS_FEE_ESTIMATION_BUY_SWAP_TOKENS: - 'Key Show Gas Fee Estimation, Buy Crypto and Swap Tokens', - KEY_AUTO_DETECT_TOKENS: 'Key Autodetect tokens', - KEY_BATCH_ACCOUNT_BALANCE_REQUESTS: 'Key Batch account balance requests', - METRICS_OPT_IN: 'Metrics Opt In', - METRICS_OPT_OUT: 'Metrics Opt Out', - NAV_ACCOUNT_MENU_OPENED: 'Account Menu Opened', - NAV_ACCOUNT_DETAILS_OPENED: 'Account Details Opened', - NAV_CONNECTED_SITES_OPENED: 'Connected Sites Opened', - NAV_MAIN_MENU_OPENED: 'Main Menu Opened', - NAV_NETWORK_MENU_OPENED: 'Network Menu Opened', - NAV_SETTINGS_OPENED: 'Settings Opened', - NAV_ACCOUNT_SWITCHED: 'Account Switched', - NAV_NETWORK_SWITCHED: 'Network Switched', - NAV_BUY_BUTTON_CLICKED: 'Buy Button Clicked', - NAV_SEND_BUTTON_CLICKED: 'Send Button Clicked', - NAV_SWAP_BUTTON_CLICKED: 'Swap Button Clicked', - SRP_TO_CONFIRM_BACKUP: 'SRP Backup Confirm Displayed', - WALLET_SETUP_STARTED: 'Wallet Setup Selected', - WALLET_SETUP_CANCELED: 'Wallet Setup Canceled', - WALLET_SETUP_FAILED: 'Wallet Setup Failed', - WALLET_CREATED: 'Wallet Created', - NFT_ADDED: 'NFT Added', - ONRAMP_PROVIDER_SELECTED: 'On-ramp Provider Selected', - PERMISSIONS_APPROVED: 'Permissions Approved', - PERMISSIONS_REJECTED: 'Permissions Rejected', - PERMISSIONS_REQUESTED: 'Permissions Requested', - PHISHING_PAGE_DISPLAYED: 'Phishing Page Displayed', - PORTFOLIO_LINK_CLICKED: 'Portfolio Link Clicked', - PUBLIC_ADDRESS_COPIED: 'Public Address Copied', - PROVIDER_METHOD_CALLED: 'Provider Method Called', - SIGNATURE_APPROVED: 'Signature Approved', - SIGNATURE_FAILED: 'Signature Failed', - SIGNATURE_REJECTED: 'Signature Rejected', - SIGNATURE_REQUESTED: 'Signature Requested', - TOKEN_IMPORT_BUTTON_CLICKED: 'Import Token Button Clicked', - TOKEN_SCREEN_OPENED: 'Token Screen Opened', - SUPPORT_LINK_CLICKED: 'Support Link Clicked', - TOKEN_ADDED: 'Token Added', - TOKEN_DETECTED: 'Token Detected', - TOKEN_HIDDEN: 'Token Hidden', - TOKEN_IMPORT_CANCELED: 'Token Import Canceled', - TOKEN_IMPORT_CLICKED: 'Token Import Clicked', - ONBOARDING_WELCOME: 'App Installed', - ONBOARDING_WALLET_CREATION_STARTED: 'Wallet Setup Selected', - ONBOARDING_WALLET_IMPORT_STARTED: 'Wallet Import Started', - ONBOARDING_WALLET_CREATION_ATTEMPTED: 'Wallet Password Created', - ONBOARDING_WALLET_SECURITY_STARTED: 'SRP Backup Selected', - ONBOARDING_WALLET_SECURITY_SKIP_INITIATED: 'SRP Skip Backup Selected', - ONBOARDING_WALLET_SECURITY_SKIP_CONFIRMED: 'SRP Backup Skipped', - ONBOARDING_WALLET_SECURITY_SKIP_CANCELED: 'SRP Skip Backup Canceled', - ONBOARDING_WALLET_SECURITY_PHRASE_REVEALED: 'SRP Revealed', - ONBOARDING_WALLET_SECURITY_PHRASE_WRITTEN_DOWN: 'SRP Backup Confirm Display', - ONBOARDING_WALLET_SECURITY_PHRASE_CONFIRMED: 'SRP Backup Confirmed', - ONBOARDING_WALLET_CREATION_COMPLETE: 'Wallet Created', - ONBOARDING_WALLET_SETUP_COMPLETE: 'Application Opened', - ONBOARDING_WALLET_ADVANCED_SETTINGS: 'Settings Updated', - ONBOARDING_WALLET_IMPORT_ATTEMPTED: 'Wallet Import Attempted', - ONBOARDING_WALLET_VIDEO_PLAY: 'SRP Intro Video Played', - ONBOARDING_TWITTER_CLICK: 'External Link Clicked', -}; - -export const EVENT = { - ACCOUNT_TYPES: { - DEFAULT: 'metamask', - IMPORTED: 'imported', - HARDWARE: 'hardware', - }, - ACCOUNT_IMPORT_TYPES: { - JSON: 'json', - PRIVATE_KEY: 'private_key', - SRP: 'srp', - }, - CATEGORIES: { - ACCOUNTS: 'Accounts', - APP: 'App', - AUTH: 'Auth', - BACKGROUND: 'Background', - ERROR: 'Error', - FOOTER: 'Footer', - HOME: 'Home', - INPAGE_PROVIDER: 'inpage_provider', - KEYS: 'Keys', - MESSAGES: 'Messages', - NAVIGATION: 'Navigation', - NETWORK: 'Network', - ONBOARDING: 'Onboarding', - PHISHING: 'Phishing', - RETENTION: 'Retention', - SETTINGS: 'Settings', - SNAPS: 'Snaps', - SWAPS: 'Swaps', - TRANSACTIONS: 'Transactions', - WALLET: 'Wallet', - DESKTOP: 'Desktop', - }, - EXTERNAL_LINK_TYPES: { - TRANSACTION_BLOCK_EXPLORER: 'Transaction Block Explorer', - BLOCK_EXPLORER: 'Block Explorer', - ACCOUNT_TRACKER: 'Account Tracker', - TOKEN_TRACKER: 'Token Tracker', - }, - KEY_TYPES: { - PKEY: 'private_key', - SRP: 'srp', - }, - ONRAMP_PROVIDER_TYPES: { - COINBASE: 'coinbase', - MOONPAY: 'moonpay', - WYRE: 'wyre', - TRANSAK: 'transak', - SELF_DEPOSIT: 'direct_deposit', - }, - SOURCE: { - NETWORK: { - CUSTOM_NETWORK_FORM: 'custom_network_form', - POPULAR_NETWORK_LIST: 'popular_network_list', - DAPP: 'dapp', - }, - SWAPS: { - MAIN_VIEW: 'Main View', - TOKEN_VIEW: 'Token View', - }, - TOKEN: { - CUSTOM: 'custom', - DAPP: 'dapp', - DETECTED: 'detected', - LIST: 'list', - }, - TRANSACTION: { - DAPP: 'dapp', - USER: 'user', - }, - }, - LOCATION: { - TOKEN_DETAILS: 'token_details', - TOKEN_DETECTION: 'token_detection', - TOKEN_MENU: 'token_menu', - }, -}; - -// Values below (e.g. 'location') can be used in the "properties" -// tracking object as keys, e.g. { location: 'Home' } -export const CONTEXT_PROPS = { - PAGE_TITLE: 'location', -}; - -/** - * These types correspond to the keys in the METAMETRIC_KEY_OPTIONS object - */ -export const METAMETRIC_KEY = { - UI_CUSTOMIZATIONS: `ui_customizations`, -}; - -/** - * This object maps a method name to a METAMETRIC_KEY - */ -export const METAMETRIC_KEY_OPTIONS = { - [METAMETRIC_KEY.UI_CUSTOMIZATIONS]: { - SIWE: 'sign_in_with_ethereum', - }, -}; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts new file mode 100644 index 000000000..03626ff61 --- /dev/null +++ b/shared/constants/metametrics.ts @@ -0,0 +1,642 @@ +import type { EnvironmentType } from './app'; +import { LedgerTransportTypes } from './hardware-wallets'; + +/** + * Used to attach context of where the user was at in the application when the + * event was triggered. Also included as full details of the current page in + * page events. + */ +export type MetaMetricsPageObject = { + /** + * The path of the current page (e.g. "/home"). + */ + path?: string; + /** + * The title of the current page (e.g. "home"). + */ + title?: string; + /** + * The fully qualified URL of the current page. + */ + url?: string; +}; + +/** + * The dapp that triggered an interaction (MetaMask only). + */ +export type MetaMetricsReferrerObject = { + /** + * The origin of the dapp issuing the notification. + */ + url?: string; +}; + +/** + * We attach context to every meta metrics event that help to qualify our + * analytics. This type has all optional values because it represents a + * returned object from a method call. Ideally app and userAgent are + * defined on every event. This is confirmed in the getTrackMetaMetricsEvent + * function, but still provides the consumer a way to override these values if + * necessary. + */ +type MetaMetricsContext = { + /** + * Application metadata. + */ + app: { + /** + * The name of the application tracking the event. + */ + name: string; + /** + * The version of the application. + */ + version: string; + }; + /** + * The user agent of the application. + */ + userAgent: string; + /** + * An object representing details of the current page. + */ + page?: MetaMetricsPageObject; + /** + * The dapp that triggered an interaction (MetaMask only). + */ + referrer?: MetaMetricsReferrerObject; +}; + +export type MetaMetricsEventPayload = { + /** + * The event name to track. + */ + event: string; + /** + * The category to associate the event to. + */ + category: string; + /** + * The action ID to deduplicate event requests from the UI. + */ + actionId?: number; + /** + * The type of environment this event occurred in. Defaults to the background + * process type. + */ + environmentType?: string; + /** + * Custom values to track. Keys in this object must be `snake_case`. + */ + properties?: object; + /** + * Sensitive values to track. These properties will be sent in an additional + * event that excludes the user's `metaMetricsId`. Keys in this object must be + * in `snake_case`. + */ + sensitiveProperties?: object; + /** + * Amount of currency that the event creates in revenue for MetaMask. + */ + revenue?: number; + /** + * ISO-4127-formatted currency for events with revenue. Defaults to US + * dollars. + */ + currency?: string; + /** + * Abstract business "value" attributable to customers who trigger this event. + */ + value?: number; + /** + * The page/route that the event occurred on. + */ + page?: MetaMetricsPageObject; + /** + * The origin of the dapp that triggered this event. + */ + referrer?: MetaMetricsReferrerObject; +}; + +export type MetaMetricsEventOptions = { + /** + * Whether or not the event happened during the opt-in workflow. + */ + isOptIn?: boolean; + /** + * Whether the segment queue should be flushed after tracking the event. + * Recommended if the result of tracking the event must be known before UI + * transition or update. + */ + flushImmediately?: boolean; + /** + * Whether to exclude the user's `metaMetricsId` for anonymity. + */ + excludeMetaMetricsId?: boolean; + /** + * An override for the `metaMetricsId` in the event (no pun intended) one is + * created as a part of an asynchronous workflow, such as awaiting the result + * of the MetaMetrics opt-in function that generates the user's + * `metaMetricsId`. + */ + metaMetricsId?: string; + /** + * Is this event a holdover from Matomo that needs further migration? When + * true, sends the data to a special Segment source that marks the event data + * as not conforming to our schema. + */ + matomoEvent?: boolean; +}; + +export type MetaMetricsEventFragment = { + /** + * The event name to fire when the fragment is closed in an affirmative action. + */ + successEvent: string; + /** + * The event name to fire when the fragment is closed with a rejection. + */ + failureEvent?: string; + /** + * An event name to fire immediately upon fragment creation. This is useful + * for building funnels in mixpanel and for reduction of code duplication. + */ + initialEvent?: string; + /** + * The event category to use for both the success and failure events. + */ + category: string; + /** + * Should this fragment be persisted in state and progressed after the + * extension is locked and unlocked. + */ + persist?: boolean; + /** + * Time in seconds the event should be persisted for. After the timeout the + * fragment will be closed as abandoned. If not supplied the fragment is + * stored indefinitely. + */ + timeout?: number; + /** + * `Date.now()` when the fragment was last updated. Used to determine if the + * timeout has expired and the fragment should be closed. + */ + lastUpdated?: number; + /** + * Custom values to track. Keys in this object must be `snake_case`. + */ + properties?: object; + /** + * Sensitive values to track. These properties will be sent in an additional + * event that excludes the user's `metaMetricsId`. Keys in this object must be + * in `snake_case`. + */ + sensitiveProperties?: object; + /** + * Amount of currency that the event creates in revenue for MetaMask. + */ + revenue?: number; + /** + * ISO-4127-formatted currency for events with revenue. Defaults to US + * dollars. + */ + currency?: string; + /** + * Abstract business "value" attributable to customers who trigger this event. + */ + value?: number; + /** + * The page/route that the event occurred on. + */ + page?: MetaMetricsPageObject; + /** + * The origin of the dapp that triggered this event. + */ + referrer?: MetaMetricsReferrerObject; + /** + * Overrides the automatic generation of UUID for the event fragment. This is + * useful when tracking events for subsystems that already generate UUIDs so + * to avoid unnecessary lookups and reduce accidental duplication. + */ + uniqueIdentifier?: string; +}; + +/** + * Data sent to the `segment.track` method. + */ +export type SegmentEventPayload = { + /** + * The MetaMetrics id for the user. + */ + userId?: string; + /** + * An anonymous ID that is used to track sensitive data while preserving + * anonymity. + */ + anonymousId?: string; + /** + * The name of the event to track. + */ + event: string; + /** + * Properties to attach to the event. + */ + properties: object; + /** + * The context the event occurred in. + */ + context: MetaMetricsContext; +}; + +/** + * Data sent to MetaMetrics for page views. + */ +export type MetaMetricsPagePayload = { + /** + * The name of the page that was viewed. + */ + name: string; + /** + * The variadic parts of the page URL. + * + * Example: If the route is `/asset/:asset` and the path is `/asset/ETH`, + * the `params` property would be `{ asset: 'ETH' }`. + */ + params?: object; + /** + * The environment type that the page was viewed in. + */ + environmentType: EnvironmentType; + /** + * The details of the page. + */ + page?: MetaMetricsPageObject; + /** + * The dapp that triggered the page view. + */ + referrer?: MetaMetricsReferrerObject; +}; + +export type MetaMetricsPageOptions = { + /** + * Is the current path one of the pages in the onboarding workflow? (If this + * is true and participateInMetaMetrics is null, then the page view will be + * tracked.) + */ + isOptInPath?: boolean; +}; + +/** + * Data sent to MetaMetrics for user traits. + */ +export type MetaMetricsUserTraits = { + /** + * The number of entries in the user's address book. + */ + address_book_entries?: number; + /** + * The type of ledger connection set by user preference. + */ + ledger_connection_type?: LedgerTransportTypes; + /** + * An array consisting of chain IDs that represent the networks added by the + * user. + */ + networks_added?: string[]; + /** + * An array consisting of chain IDs that represent the networks added by the + * user that do not have a ticker. + */ + networks_without_ticker?: string[]; + /** + * Does the user have the Autodetect NFTs feature enabled? + */ + nft_autodetection_enabled?: number; + /** + * A number representing the number of identities (accounts) added to the + * user's wallet. + */ + number_of_accounts?: number; + /** + * A number representing the amount of NFT collections from which the user + * possesses NFTs. + */ + number_of_nft_collections?: number; + /** + * A number representing the amount of all NFTs the user possesses across all + * networks and accounts. + */ + number_of_nfts?: number; + /** + * The total number of token contracts the user has across all networks and + * accounts. + */ + number_of_tokens?: number; + /** + * Does the user have the OpenSea API enabled? + */ + opensea_api_enabled?: boolean; + /** + * Does the user have 3Box sync enabled? + * + * @deprecated + */ + three_box_enabled?: boolean; + /** + * Which theme the user has selected. + */ + theme?: string; + /** + * Does the user have token detection enabled? + */ + token_detection_enabled?: boolean; + /** + * Does the user have desktop enabled? + */ + desktop_enabled?: boolean; + /** + * Whether the security provider feature has been enabled. + */ + security_providers?: string[]; +}; + +export enum MetaMetricsUserTrait { + /** + * Identified when the user adds or modifies addresses in the address book. + */ + AddressBookEntries = 'address_book_entries', + /** + * Identified when the user installed the extension. + */ + InstallDateExt = 'install_date_ext', + /** + * Identified when the Ledger Live connection type is changed. + */ + LedgerConnectionType = 'ledger_connection_type', + /** + * Identified when the user modifies networks. + */ + NetworksAdded = 'networks_added', + /** + * Identified when the user modifies networks that lack a ticker. + */ + NetworksWithoutTicker = 'networks_without_ticker', + /** + * Identified when the "Autodetect NFTs" feature is toggled. + */ + NftAutodetectionEnabled = 'nft_autodetection_enabled', + /** + * Identified when identities change. + */ + NumberOfAccounts = 'number_of_accounts', + /** + * The number of unique NFT addresses. + */ + NumberOfNftCollections = 'number_of_nft_collections', + /** + * Identified when the number of NFTs owned by the user changes. + */ + NumberOfNfts = 'number_of_nfts', + /** + * Identified when the number of tokens change. + */ + NumberOfTokens = 'number_of_tokens', + /** + * Identified when the OpenSea API is enabled. + */ + OpenSeaApiEnabled = 'opensea_api_enabled', + /** + * Identified when the user's theme changes. + */ + Theme = 'theme', + /** + * Identified when the 3Box feature is toggled. + * + * @deprecated + */ + ThreeBoxEnabled = 'three_box_enabled', + /** + * Identified when the token detection feature is toggled. + */ + TokenDetectionEnabled = 'token_detection_enabled', + /** + * Identified when the user enables desktop. + */ + DesktopEnabled = 'desktop_enabled', + /** + * Identified when the security provider feature is enabled. + */ + SecurityProviders = 'security_providers', +} + +/** + * Mixpanel converts the zero address value to a truly anonymous event, which + * speeds up reporting + */ +export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'; + +/** + * Used to identify events that are triggered by the background process. + */ +export const METAMETRICS_BACKGROUND_PAGE_OBJECT: MetaMetricsPageObject = { + path: '/background-process', + title: 'Background Process', + url: '/background-process', +}; + +export const REJECT_NOTIFICATION_CLOSE = 'Cancel Via Notification Close'; + +export const REJECT_NOTIFICATION_CLOSE_SIG = + 'Cancel Sig Request Via Notification Close'; + +export enum MetaMetricsEventName { + AccountAdded = 'Account Added', + AccountAddSelected = 'Account Add Selected', + AccountAddFailed = 'Account Add Failed', + AccountPasswordCreated = 'Account Password Created', + AccountReset = 'Account Reset', + AppInstalled = 'App Installed', + AppUnlocked = 'App Unlocked', + AppUnlockedFailed = 'App Unlocked Failed', + AppWindowExpanded = 'App Window Expanded', + BridgeLinkClicked = 'Bridge Link Clicked', + DecryptionApproved = 'Decryption Approved', + DecryptionRejected = 'Decryption Rejected', + DecryptionRequested = 'Decryption Requested', + EncryptionPublicKeyApproved = 'Encryption Approved', + EncryptionPublicKeyRejected = 'Encryption Rejected', + EncryptionPublicKeyRequested = 'Encryption Requested', + ExternalLinkClicked = 'External Link Clicked', + KeyExportSelected = 'Key Export Selected', + KeyExportRequested = 'Key Export Requested', + KeyExportFailed = 'Key Export Failed', + KeyExportCanceled = 'Key Export Canceled', + KeyExportRevealed = 'Key Material Revealed', + KeyExportCopied = 'Key Material Copied', + KeyTokenDetectionSelected = 'Key Token Detection Selected', + KeyGlobalSecurityToggleSelected = 'Key Global Security/Privacy Settings', + KeyBalanceTokenPriceChecker = 'Key Show Balance and Token Price Checker Settings', + KeyGasFeeEstimationBuySwapTokens = 'Key Show Gas Fee Estimation, Buy Crypto and Swap Tokens', + KeyAutoDetectTokens = 'Key Autodetect tokens', + KeyBatchAccountBalanceRequests = 'Key Batch account balance requests', + MetricsOptIn = 'Metrics Opt In', + MetricsOptOut = 'Metrics Opt Out', + NavAccountMenuOpened = 'Account Menu Opened', + NavAccountDetailsOpened = 'Account Details Opened', + NavConnectedSitesOpened = 'Connected Sites Opened', + NavMainMenuOpened = 'Main Menu Opened', + NavNetworkMenuOpened = 'Network Menu Opened', + NavSettingsOpened = 'Settings Opened', + NavAccountSwitched = 'Account Switched', + NavNetworkSwitched = 'Network Switched', + NavBuyButtonClicked = 'Buy Button Clicked', + NavSendButtonClicked = 'Send Button Clicked', + NavSwapButtonClicked = 'Swap Button Clicked', + SrpToConfirmBackup = 'SRP Backup Confirm Displayed', + WalletSetupStarted = 'Wallet Setup Selected', + WalletSetupCanceled = 'Wallet Setup Canceled', + WalletSetupFailed = 'Wallet Setup Failed', + WalletCreated = 'Wallet Created', + NftAdded = 'NFT Added', + OnrampProviderSelected = 'On-ramp Provider Selected', + PermissionsApproved = 'Permissions Approved', + PermissionsRejected = 'Permissions Rejected', + PermissionsRequested = 'Permissions Requested', + PhishingPageDisplayed = 'Phishing Page Displayed', + PortfolioLinkClicked = 'Portfolio Link Clicked', + PublicAddressCopied = 'Public Address Copied', + ProviderMethodCalled = 'Provider Method Called', + SignatureApproved = 'Signature Approved', + SignatureFailed = 'Signature Failed', + SignatureRejected = 'Signature Rejected', + SignatureRequested = 'Signature Requested', + TokenImportButtonClicked = 'Import Token Button Clicked', + TokenScreenOpened = 'Token Screen Opened', + SupportLinkClicked = 'Support Link Clicked', + TokenAdded = 'Token Added', + TokenDetected = 'Token Detected', + TokenHidden = 'Token Hidden', + TokenImportCanceled = 'Token Import Canceled', + TokenImportClicked = 'Token Import Clicked', + OnboardingWelcome = 'App Installed', + OnboardingWalletCreationStarted = 'Wallet Setup Selected', + OnboardingWalletImportStarted = 'Wallet Import Started', + OnboardingWalletCreationAttempted = 'Wallet Password Created', + OnboardingWalletSecurityStarted = 'SRP Backup Selected', + OnboardingWalletSecuritySkipInitiated = 'SRP Skip Backup Selected', + OnboardingWalletSecuritySkipConfirmed = 'SRP Backup Skipped', + OnboardingWalletSecuritySkipCanceled = 'SRP Skip Backup Canceled', + OnboardingWalletSecurityPhraseRevealed = 'SRP Revealed', + OnboardingWalletSecurityPhraseWrittenDown = 'SRP Backup Confirm Display', + OnboardingWalletSecurityPhraseConfirmed = 'SRP Backup Confirmed', + OnboardingWalletCreationComplete = 'Wallet Created', + OnboardingWalletSetupComplete = 'Application Opened', + OnboardingWalletAdvancedSettings = 'Settings Updated', + OnboardingWalletImportAttempted = 'Wallet Import Attempted', + OnboardingWalletVideoPlay = 'SRP Intro Video Played', + OnboardingTwitterClick = 'External Link Clicked', + ServiceWorkerRestarted = 'Service Worker Restarted', +} + +export enum MetaMetricsEventAccountType { + Default = 'metamask', + Imported = 'imported', + Hardware = 'hardware', +} + +export enum MetaMetricsEventAccountImportType { + Json = 'json', + PrivateKey = 'private_key', + Srp = 'srp', +} + +export enum MetaMetricsEventCategory { + Accounts = 'Accounts', + App = 'App', + Auth = 'Auth', + Background = 'Background', + // The TypeScript ESLint rule is incorrectly marking this line. + /* eslint-disable-next-line @typescript-eslint/no-shadow */ + Error = 'Error', + Footer = 'Footer', + Home = 'Home', + InpageProvider = 'inpage_provider', + Keys = 'Keys', + Messages = 'Messages', + Navigation = 'Navigation', + Network = 'Network', + Onboarding = 'Onboarding', + Phishing = 'Phishing', + Retention = 'Retention', + Settings = 'Settings', + Snaps = 'Snaps', + Swaps = 'Swaps', + Transactions = 'Transactions', + Wallet = 'Wallet', + Desktop = 'Desktop', + ServiceWorkers = 'service_workers', +} + +export enum MetaMetricsEventLinkType { + TransactionBlockExplorer = 'Transaction Block Explorer', + BlockExplorer = 'Block Explorer', + AccountTracker = 'Account Tracker', + TokenTracker = 'Token Tracker', +} + +export enum MetaMetricsEventKeyType { + Pkey = 'private_key', + Srp = 'srp', +} + +// NOTE: This doesn't seem to be used at all +export enum MetaMetricsEventOnrampProviderType { + Coinbase = 'coinbase', + Moonpay = 'moonpay', + Wyre = 'wyre', + Transak = 'transak', + SelfDeposit = 'direct_deposit', +} + +export enum MetaMetricsNetworkEventSource { + CustomNetworkForm = 'custom_network_form', + PopularNetworkList = 'popular_network_list', + Dapp = 'dapp', +} + +export enum MetaMetricsSwapsEventSource { + MainView = 'Main View', + TokenView = 'Token View', +} + +export enum MetaMetricsTokenEventSource { + Custom = 'custom', + Dapp = 'dapp', + Detected = 'detected', + List = 'list', +} + +export enum MetaMetricsTransactionEventSource { + Dapp = 'dapp', + User = 'user', +} + +export enum MetaMetricsEventLocation { + TokenDetails = 'token_details', + TokenDetection = 'token_detection', + TokenMenu = 'token_menu', +} + +export enum MetaMetricsEventUiCustomization { + FlaggedAsMalicious = 'flagged_as_malicious', + FlaggedAsSafetyUnknown = 'flagged_as_safety_unknown', + Siwe = 'sign_in_with_ethereum', +} + +/** + * Values that can used in the "properties" tracking object as keys, e.g. `{ + * location: 'Home' }`. + */ +export enum MetaMetricsContextProp { + PageTitle = 'location', +} diff --git a/shared/constants/network.ts b/shared/constants/network.ts index b061c2647..1f20538c3 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -1,4 +1,4 @@ -import { capitalize } from 'lodash'; +import { capitalize, pick } from 'lodash'; /** * A type representing any valid value for 'type' for setProviderType and other * methods that add or manipulate networks in MetaMask state. @@ -301,6 +301,13 @@ export const BUILT_IN_NETWORKS = { }, } as const; +export const BUILT_IN_INFURA_NETWORKS = pick( + BUILT_IN_NETWORKS, + INFURA_PROVIDER_TYPES, +); + +export type BuiltInInfuraNetwork = keyof typeof BUILT_IN_INFURA_NETWORKS; + export const NETWORK_TO_NAME_MAP = { [NETWORK_TYPES.MAINNET]: MAINNET_DISPLAY_NAME, [NETWORK_TYPES.GOERLI]: GOERLI_DISPLAY_NAME, @@ -682,3 +689,30 @@ export const FEATURED_RPCS: RPCDefinition[] = [ export const SHOULD_SHOW_LINEA_TESTNET_NETWORK = new Date().getTime() > Date.UTC(2023, 2, 28, 8); + +/** + * Represents the availability state of the currently selected network. + */ +export enum NetworkStatus { + /** + * The network may or may not be able to receive requests, but either no + * attempt has been made to determine this, or an attempt was made but was + * unsuccessful. + */ + Unknown = 'unknown', + /** + * The network is able to receive and respond to requests. + */ + Available = 'available', + /** + * The network is unable to receive and respond to requests for unknown + * reasons. + */ + Unavailable = 'unavailable', + /** + * The network is not only unavailable, but is also inaccessible for the user + * specifically based on their location. This state only applies to Infura + * networks. + */ + Blocked = 'blocked', +} diff --git a/shared/constants/snaps.ts b/shared/constants/snaps.ts index 7d6d0ea53..6a272cbb8 100644 --- a/shared/constants/snaps.ts +++ b/shared/constants/snaps.ts @@ -52,13 +52,103 @@ type SnapsDerivationPath = { export const SNAPS_DERIVATION_PATHS: SnapsDerivationPath[] = [ { path: ['m', `44'`, `0'`], + curve: 'ed25519', + name: 'Test BIP-32 Path (ed25519)', + }, + { + path: ['m', `44'`, `1'`], curve: 'secp256k1', name: 'Test BIP-32 Path (secp256k1)', }, { path: ['m', `44'`, `0'`], + curve: 'secp256k1', + name: 'Bitcoin Legacy', + }, + { + path: ['m', `49'`, `0'`], + curve: 'secp256k1', + name: 'Bitcoin Nested SegWit', + }, + { + path: ['m', `49'`, `1'`], + curve: 'secp256k1', + name: 'Bitcoin Testnet Nested SegWit', + }, + { + path: ['m', `84'`, `0'`], + curve: 'secp256k1', + name: 'Bitcoin Native SegWit', + }, + { + path: ['m', `84'`, `1'`], + curve: 'secp256k1', + name: 'Bitcoin Testnet Native SegWit', + }, + { + path: ['m', `44'`, `501'`], + curve: 'secp256k1', + name: 'Solana', + }, + { + path: ['m', `44'`, `2'`], + curve: 'secp256k1', + name: 'Litecoin', + }, + { + path: ['m', `44'`, `3'`], + curve: 'secp256k1', + name: 'Dogecoin', + }, + { + path: ['m', `44'`, `60'`], + curve: 'secp256k1', + name: 'Ethereum', + }, + { + path: ['m', `44'`, `118'`], + curve: 'secp256k1', + name: 'Atom', + }, + { + path: ['m', `44'`, `145'`], + curve: 'secp256k1', + name: 'Bitcoin Cash', + }, + { + path: ['m', `44'`, `714'`], + curve: 'secp256k1', + name: 'Binance (BNB)', + }, + { + path: ['m', `44'`, `931'`], + curve: 'secp256k1', + name: 'THORChain (RUNE)', + }, + { + path: ['m', `44'`, `330'`], + curve: 'secp256k1', + name: 'Terra (LUNA)', + }, + { + path: ['m', `44'`, `459'`], + curve: 'secp256k1', + name: 'Kava', + }, + { + path: ['m', `44'`, `529'`], + curve: 'secp256k1', + name: 'Secret Network', + }, + { + path: ['m', `44'`, `397'`], curve: 'ed25519', - name: 'Test BIP-32 Path (ed25519)', + name: 'NEAR Protocol', + }, + { + path: ['m', `44'`, `1'`, `0'`], + curve: 'ed25519', + name: 'NEAR Protocol Testnet', }, ]; ///: END:ONLY_INCLUDE_IN diff --git a/shared/constants/test-flags.js b/shared/constants/test-flags.js new file mode 100644 index 000000000..a5018d6be --- /dev/null +++ b/shared/constants/test-flags.js @@ -0,0 +1 @@ +export const ACTION_QUEUE_METRICS_E2E_TEST = 'action_queue_metrics_e2e_test'; diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js index d5f5feb3f..631fd04b0 100644 --- a/shared/lib/error-utils.js +++ b/shared/lib/error-utils.js @@ -121,12 +121,17 @@ export async function getErrorHtml( } ///: BEGIN:ONLY_INCLUDE_IN(flask) +export const MMD_DOWNLOAD_LINK = + 'https://github.com/MetaMask/metamask-desktop/releases'; + function disableDesktop(backgroundConnection) { backgroundConnection.disableDesktopError(); } export function downloadDesktopApp() { - global.platform.openTab({ url: 'https://metamask.io/' }); + global.platform.openTab({ + url: MMD_DOWNLOAD_LINK, + }); } export function downloadExtension() { @@ -139,7 +144,7 @@ export function restartExtension() { export function openOrDownloadMMD() { openCustomProtocol('metamask-desktop://pair').catch(() => { - window.open('https://metamask.io/download.html', '_blank').focus(); + window.open(MMD_DOWNLOAD_LINK, '_blank').focus(); }); } diff --git a/shared/lib/error-utils.test.js b/shared/lib/error-utils.test.js index de846aa8b..f761acd10 100644 --- a/shared/lib/error-utils.test.js +++ b/shared/lib/error-utils.test.js @@ -1,13 +1,48 @@ +import browser from 'webextension-polyfill'; import { fetchLocale } from '../../ui/helpers/utils/i18n-helper'; import { SUPPORT_LINK } from './ui-utils'; -import { getErrorHtml } from './error-utils'; +import { + downloadDesktopApp, + openOrDownloadMMD, + downloadExtension, + getErrorHtml, + restartExtension, + registerDesktopErrorActions, + MMD_DOWNLOAD_LINK, +} from './error-utils'; +import { openCustomProtocol } from './deep-linking'; jest.mock('../../ui/helpers/utils/i18n-helper', () => ({ fetchLocale: jest.fn(), loadRelativeTimeFormatLocaleData: jest.fn(), })); +jest.mock('./deep-linking', () => ({ + openCustomProtocol: jest.fn(), +})); + +jest.mock('webextension-polyfill', () => { + return { + runtime: { + reload: jest.fn(), + }, + }; +}); describe('Error utils Tests', function () { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + + global.platform = { + openTab: jest.fn(), + }; + }); + + afterAll(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + delete global.platform; + }); it('should get error html', async function () { const mockStore = { localeMessages: { @@ -50,4 +85,93 @@ describe('Error utils Tests', function () { expect(errorHtml).toContain(stillGettingMessageMessage); expect(errorHtml).toContain(sendBugReportMessage); }); + describe('desktop', () => { + it('downloadDesktopApp opens a new tab on metamask-desktop releases url', () => { + downloadDesktopApp(); + + expect(global.platform.openTab).toHaveBeenCalledTimes(1); + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: MMD_DOWNLOAD_LINK, + }); + }); + + it('downloadExtension opens a new tab on metamask extension url', () => { + downloadExtension(); + + expect(global.platform.openTab).toHaveBeenCalledTimes(1); + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: 'https://metamask.io/', + }); + }); + + it('restartExtension calls runtime reload method', () => { + restartExtension(); + + expect(browser.runtime.reload).toHaveBeenCalledTimes(1); + }); + + describe('openOrDownloadMMD', () => { + it('launches installed desktop app by calling openCustomProtocol successfully', () => { + openCustomProtocol.mockResolvedValue(); + openOrDownloadMMD(); + + expect(openCustomProtocol).toHaveBeenCalledTimes(1); + expect(openCustomProtocol).toHaveBeenCalledWith( + 'metamask-desktop://pair', + ); + }); + + it('opens metamask-desktop release url when fails to find and start a local metamask-desktop app', async () => { + openCustomProtocol.mockRejectedValue(); + const focusMock = jest.fn(); + jest.spyOn(window, 'open').mockReturnValue({ + focus: focusMock, + }); + + openOrDownloadMMD(); + + // this ensures that we are awaiting for pending promises to resolve + // as the openOrDownloadMMD calls a promise, but returns before it is resolved + await new Promise(process.nextTick); + + expect(openCustomProtocol).toHaveBeenCalledTimes(1); + expect(openCustomProtocol).toHaveBeenCalledWith( + 'metamask-desktop://pair', + ); + + expect(window.open).toHaveBeenCalledTimes(1); + expect(window.open).toHaveBeenCalledWith(MMD_DOWNLOAD_LINK, '_blank'); + expect(focusMock).toHaveBeenCalledTimes(1); + }); + }); + + it('registerDesktopErrorActions add click event listeners for each desktop error elements', async () => { + const addEventListenerMock = jest.fn(); + jest.spyOn(document, 'getElementById').mockReturnValue({ + addEventListener: addEventListenerMock, + }); + + registerDesktopErrorActions(); + + expect(document.getElementById).toHaveBeenCalledTimes(4); + expect(document.getElementById).toHaveBeenNthCalledWith( + 1, + 'desktop-error-button-disable-mmd', + ); + expect(document.getElementById).toHaveBeenNthCalledWith( + 2, + 'desktop-error-button-restart-mm', + ); + expect(document.getElementById).toHaveBeenNthCalledWith( + 3, + 'desktop-error-button-download-mmd', + ); + expect(document.getElementById).toHaveBeenNthCalledWith( + 4, + 'desktop-error-button-open-or-download-mmd', + ); + + expect(addEventListenerMock).toHaveBeenCalledTimes(4); + }); + }); }); diff --git a/shared/modules/Numeric.test.ts b/shared/modules/Numeric.test.ts index 2786bec91..33e8f4ee2 100644 --- a/shared/modules/Numeric.test.ts +++ b/shared/modules/Numeric.test.ts @@ -10,98 +10,98 @@ const ONE_WEI = new Numeric(1, 10, EtherDenomination.WEI); describe('Numeric', () => { describe('Basic Numeric Construction', () => { describe('From hexadeciaml strings', () => { - it('Should create a new Numeric from a hexadecimal string', () => { + it('should create a new Numeric from a hexadecimal string', () => { const numeric = new Numeric('0xa', 16); - expect(numeric.value).toEqual(new BigNumber(10, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(10, 10)); }); - it('Should create a new Numeric from a hexadecimal string with a decimal', () => { + it('should create a new Numeric from a hexadecimal string with a decimal', () => { const numeric = new Numeric('0xa.7', 16); - expect(numeric.value).toEqual(new BigNumber(10.4375, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(10.4375, 10)); }); - it('Should create a new Numeric from a hexadecimal string with negation', () => { + it('should create a new Numeric from a hexadecimal string with negation', () => { const numeric = new Numeric('-0xa', 16); - expect(numeric.value).toEqual(new BigNumber(-10, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(-10, 10)); }); - it('Should create a new Numeric from a hexadecimal string with negation and decimal', () => { + it('should create a new Numeric from a hexadecimal string with negation and decimal', () => { const numeric = new Numeric('-0xa.7', 16); - expect(numeric.value).toEqual(new BigNumber(-10.4375, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(-10.4375, 10)); }); }); describe('From decimal strings', () => { - it('Should create a new Numeric from a decimal string', () => { + it('should create a new Numeric from a decimal string', () => { const numeric = new Numeric('10', 10); - expect(numeric.value).toEqual(new BigNumber(10, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(10, 10)); }); - it('Should create a new Numeric from a decimal string with a decimal', () => { + it('should create a new Numeric from a decimal string with a decimal', () => { const numeric = new Numeric('10.4375', 10); - expect(numeric.value).toEqual(new BigNumber(10.4375, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(10.4375, 10)); }); - it('Should create a new Numeric from a decimal string with negation', () => { + it('should create a new Numeric from a decimal string with negation', () => { const numeric = new Numeric('-10', 10); - expect(numeric.value).toEqual(new BigNumber(-10, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(-10, 10)); }); - it('Should create a new Numeric from a decimal string with negation and decimal', () => { + it('should create a new Numeric from a decimal string with negation and decimal', () => { const numeric = new Numeric('-10.4375', 10); - expect(numeric.value).toEqual(new BigNumber(-10.4375, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(-10.4375, 10)); }); }); describe('From decimal numbers', () => { - it('Should create a new Numeric from a hexadecimal number', () => { + it('should create a new Numeric from a hexadecimal number', () => { const numeric = new Numeric(10, 10); - expect(numeric.value).toEqual(new BigNumber(10, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(10, 10)); }); - it('Should create a new Numeric from a hexadecimal string with a decimal', () => { + it('should create a new Numeric from a hexadecimal string with a decimal', () => { const numeric = new Numeric(10.4375, 10); - expect(numeric.value).toEqual(new BigNumber(10.4375, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(10.4375, 10)); }); - it('Should create a new Numeric from a hexadecimal string with negation', () => { + it('should create a new Numeric from a hexadecimal string with negation', () => { const numeric = new Numeric(-10, 10); - expect(numeric.value).toEqual(new BigNumber(-10, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(-10, 10)); }); - it('Should create a new Numeric from a hexadecimal string with negation and decimal', () => { + it('should create a new Numeric from a hexadecimal string with negation and decimal', () => { const numeric = new Numeric(-10.4375, 16); - expect(numeric.value).toEqual(new BigNumber(-10.4375, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(-10.4375, 10)); }); }); describe('From BigNumbers or BN', () => { - it('Should create a new Numeric from a BigNumber', () => { + it('should create a new Numeric from a BigNumber', () => { const numeric = new Numeric(new BigNumber(100, 10)); - expect(numeric.value).toEqual(new BigNumber(100, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(100, 10)); }); - it('Should create a new Numeric from a BN', () => { + it('should create a new Numeric from a BN', () => { const numeric = new Numeric(new BN(100, 10), 10); - expect(numeric.value).toEqual(new BigNumber(100, 10)); + expect(numeric.value).toStrictEqual(new BigNumber(100, 10)); }); }); }); describe('Error checking', () => { - it('Should throw an error for a non numeric string', () => { + it('should throw an error for a non numeric string', () => { expect(() => new Numeric('Hello there', 10)).toThrow( 'String provided to stringToBigNumber is not a hexadecimal or decimal string: Hello there, 10', ); }); - it('Should throw an error for a numeric string without a base', () => { + it('should throw an error for a numeric string without a base', () => { expect(() => new Numeric('10')).toThrow( 'You must specify the base of the provided number if the value is not already a BigNumber', ); }); - it('Should throw an error for a non numeric type', () => { + it('should throw an error for a non numeric type', () => { expect(() => new Numeric(true as unknown as number, 10)).toThrow( 'Value: true is not a string, number, BigNumber or BN. Type is: boolean.', ); @@ -109,454 +109,474 @@ describe('Numeric', () => { }); describe('Erroneous behaviors that we are temporarily continuing', () => { - it('Handles values that are undefined, setting the value to 0', () => { - expect(new Numeric(undefined as unknown as number).toString()).toEqual( - '0', - ); + it('handles values that are undefined, setting the value to 0', () => { + expect( + new Numeric(undefined as unknown as number).toString(), + ).toStrictEqual('0'); }); - it('Handles values that are NaN, setting the value to 0', () => { - expect(new Numeric(NaN).toString()).toEqual('0'); + it('handles values that are NaN, setting the value to 0', () => { + expect(new Numeric(NaN).toString()).toStrictEqual('0'); }); }); describe('Ether denomination conversion', () => { it('should convert 1 ETH to 1000000000 GWEI', () => { - expect(ONE_ETH.toDenomination(EtherDenomination.GWEI).toString()).toEqual( - '1000000000', - ); + expect( + ONE_ETH.toDenomination(EtherDenomination.GWEI).toString(), + ).toStrictEqual('1000000000'); }); it('should convert 1 ETH to 1000000000000000000 WEI', () => { - expect(ONE_ETH.toDenomination(EtherDenomination.WEI).toString()).toEqual( - '1000000000000000000', - ); + expect( + ONE_ETH.toDenomination(EtherDenomination.WEI).toString(), + ).toStrictEqual('1000000000000000000'); }); it('should convert 1 GWEI to 0.000000001 ETH', () => { - expect(ONE_GWEI.toDenomination(EtherDenomination.ETH).toString()).toEqual( - '0.000000001', - ); + expect( + ONE_GWEI.toDenomination(EtherDenomination.ETH).toString(), + ).toStrictEqual('0.000000001'); }); it('should convert 1 GWEI to 1000000000 WEI', () => { - expect(ONE_GWEI.toDenomination(EtherDenomination.WEI).toString()).toEqual( - '1000000000', - ); + expect( + ONE_GWEI.toDenomination(EtherDenomination.WEI).toString(), + ).toStrictEqual('1000000000'); }); it('should convert 1 WEI to 0 ETH due to rounding', () => { - expect(ONE_WEI.toDenomination(EtherDenomination.ETH).toString()).toEqual( - '0', - ); + expect( + ONE_WEI.toDenomination(EtherDenomination.ETH).toString(), + ).toStrictEqual('0'); }); it('should convert 1 WEI to 0.000000001 GWEI', () => { - expect(ONE_WEI.toDenomination(EtherDenomination.GWEI).toString()).toEqual( - '0.000000001', - ); + expect( + ONE_WEI.toDenomination(EtherDenomination.GWEI).toString(), + ).toStrictEqual('0.000000001'); }); }); describe('Math operations', () => { describe('Multiplication', () => { - it('Should compute correct results for simple multiplication', () => { - expect(new Numeric(5, 10).times(5, 10).toNumber()).toEqual(25); + it('should compute correct results for simple multiplication', () => { + expect(new Numeric(5, 10).times(5, 10).toNumber()).toStrictEqual(25); expect( new Numeric(5, 10).times(new Numeric(10, 10)).toNumber(), - ).toEqual(50); + ).toStrictEqual(50); expect( new Numeric(25, 10).times(new Numeric(10, 10)).toNumber(), - ).toEqual(250); + ).toStrictEqual(250); }); - it('Should compute correct results for multiplication of big numbers', () => { + it('should compute correct results for multiplication of big numbers', () => { expect( new Numeric('175671432', 10).times('686216', 10).toString(), - ).toEqual('120548547381312'); + ).toStrictEqual('120548547381312'); expect( new Numeric('1756714320', 10) .times(new Numeric('686216', 10)) .toString(), - ).toEqual('1205485473813120'); + ).toStrictEqual('1205485473813120'); expect( new Numeric('41756714320', 10) .times(new Numeric('6862160', 10)) .toString(), - ).toEqual('286541254738131200'); + ).toStrictEqual('286541254738131200'); }); - it('Should compute correct results for multiplication of negative big numbers', () => { + it('should compute correct results for multiplication of negative big numbers', () => { expect( new Numeric('175671432', 10).times('-686216', 10).toString(), - ).toEqual('-120548547381312'); + ).toStrictEqual('-120548547381312'); expect( new Numeric('1756714320', 10) .times(new Numeric('-686216', 10)) .toString(), - ).toEqual('-1205485473813120'); + ).toStrictEqual('-1205485473813120'); expect( new Numeric('-41756714320', 10) .times(new Numeric('-6862160', 10)) .toString(), - ).toEqual('286541254738131200'); + ).toStrictEqual('286541254738131200'); }); }); describe('Division', () => { - it('Should compute correct results for simple division', () => { - expect(new Numeric(25, 10).divide(5, 10).toNumber()).toEqual(5); + it('should compute correct results for simple division', () => { + expect(new Numeric(25, 10).divide(5, 10).toNumber()).toStrictEqual(5); expect( new Numeric(50, 10).divide(new Numeric(10, 10)).toNumber(), - ).toEqual(5); + ).toStrictEqual(5); expect( new Numeric(250, 10).divide(new Numeric(10, 10)).toNumber(), - ).toEqual(25); + ).toStrictEqual(25); }); - it('Should compute correct results for division of big numbers', () => { + it('should compute correct results for division of big numbers', () => { expect( new Numeric('175671432', 10).divide('686216', 10).toString(), - ).toEqual('256.00019818832554181191'); + ).toStrictEqual('256.00019818832554181191'); expect( new Numeric('1756714320', 10) .divide(new Numeric('686216', 10)) .toString(), - ).toEqual('2560.00198188325541811908'); + ).toStrictEqual('2560.00198188325541811908'); expect( new Numeric('41756714320', 10) .divide(new Numeric('6862160', 10)) .toString(), - ).toEqual('6085.06859647691106007438'); + ).toStrictEqual('6085.06859647691106007438'); }); - it('Should compute correct results for division of negative big numbers', () => { + it('should compute correct results for division of negative big numbers', () => { expect( new Numeric('175671432', 10).divide('-686216', 10).toString(), - ).toEqual('-256.00019818832554181191'); + ).toStrictEqual('-256.00019818832554181191'); expect( new Numeric('1756714320', 10) .divide(new Numeric('-686216', 10)) .toString(), - ).toEqual('-2560.00198188325541811908'); + ).toStrictEqual('-2560.00198188325541811908'); expect( new Numeric('-41756714320', 10) .divide(new Numeric('-6862160', 10)) .toString(), - ).toEqual('6085.06859647691106007438'); + ).toStrictEqual('6085.06859647691106007438'); }); }); describe('Addition', () => { - it('Should compute correct results for simple addition', () => { - expect(new Numeric(25, 10).add(5, 10).toNumber()).toEqual(30); + it('should compute correct results for simple addition', () => { + expect(new Numeric(25, 10).add(5, 10).toNumber()).toStrictEqual(30); - expect(new Numeric(50, 10).add(new Numeric(10, 10)).toNumber()).toEqual( - 60, - ); + expect( + new Numeric(50, 10).add(new Numeric(10, 10)).toNumber(), + ).toStrictEqual(60); expect( new Numeric(250, 10).add(new Numeric(100, 10)).toNumber(), - ).toEqual(350); + ).toStrictEqual(350); }); - it('Should compute correct results for addition of big numbers', () => { + it('should compute correct results for addition of big numbers', () => { expect( new Numeric('175671432', 10).add('686216', 10).toString(), - ).toEqual('176357648'); + ).toStrictEqual('176357648'); expect( new Numeric('1756714320', 10) .add(new Numeric('686216', 10)) .toString(), - ).toEqual('1757400536'); + ).toStrictEqual('1757400536'); expect( new Numeric('41756714320', 10) .add(new Numeric('6862160', 10)) .toString(), - ).toEqual('41763576480'); + ).toStrictEqual('41763576480'); }); - it('Should compute correct results for addition of negative big numbers', () => { + it('should compute correct results for addition of negative big numbers', () => { expect( new Numeric('175671432', 10).add('-686216', 10).toString(), - ).toEqual('174985216'); + ).toStrictEqual('174985216'); expect( new Numeric('1756714320', 10) .add(new Numeric('-686216', 10)) .toString(), - ).toEqual('1756028104'); + ).toStrictEqual('1756028104'); expect( new Numeric('-41756714320', 10) .add(new Numeric('-6862160', 10)) .toString(), - ).toEqual('-41763576480'); + ).toStrictEqual('-41763576480'); }); }); describe('Subtraction', () => { - it('Should compute correct results for simple subtraction', () => { - expect(new Numeric(25, 10).minus(5, 10).toNumber()).toEqual(20); + it('should compute correct results for simple subtraction', () => { + expect(new Numeric(25, 10).minus(5, 10).toNumber()).toStrictEqual(20); expect( new Numeric(50, 10).minus(new Numeric(10, 10)).toNumber(), - ).toEqual(40); + ).toStrictEqual(40); expect( new Numeric(250, 10).minus(new Numeric(100, 10)).toNumber(), - ).toEqual(150); + ).toStrictEqual(150); }); - it('Should compute correct results for subtraction of big numbers', () => { + it('should compute correct results for subtraction of big numbers', () => { expect( new Numeric('175671432', 10).minus('686216', 10).toString(), - ).toEqual('174985216'); + ).toStrictEqual('174985216'); expect( new Numeric('1756714320', 10) .minus(new Numeric('686216', 10)) .toString(), - ).toEqual('1756028104'); + ).toStrictEqual('1756028104'); expect( new Numeric('41756714320', 10) .minus(new Numeric('6862160', 10)) .toString(), - ).toEqual('41749852160'); + ).toStrictEqual('41749852160'); }); - it('Should compute correct results for subtraction of negative big numbers', () => { + it('should compute correct results for subtraction of negative big numbers', () => { expect( new Numeric('175671432', 10).minus('-686216', 10).toString(), - ).toEqual('176357648'); + ).toStrictEqual('176357648'); expect( new Numeric('1756714320', 10) .minus(new Numeric('-686216', 10)) .toString(), - ).toEqual('1757400536'); + ).toStrictEqual('1757400536'); expect( new Numeric('-41756714320', 10) .minus(new Numeric('-6862160', 10)) .toString(), - ).toEqual('-41749852160'); + ).toStrictEqual('-41749852160'); }); }); describe('applyConversionRate', () => { - it('Should multiply the value by the conversionRate supplied', () => { + it('should multiply the value by the conversionRate supplied', () => { expect( new Numeric(10, 10).applyConversionRate(468.5).toString(), - ).toEqual('4685'); + ).toStrictEqual('4685'); }); - it('Should multiply the value by the conversionRate supplied when conversionRate is a BigNumber', () => { + it('should multiply the value by the conversionRate supplied when conversionRate is a BigNumber', () => { expect( new Numeric(10, 10) .applyConversionRate(new BigNumber(468.5, 10)) .toString(), - ).toEqual('4685'); + ).toStrictEqual('4685'); }); - it('Should multiply the value by the inverse of conversionRate supplied when second parameter is true', () => { + it('should multiply the value by the inverse of conversionRate supplied when second parameter is true', () => { expect( new Numeric(10, 10).applyConversionRate(468.5, true).toString(), - ).toEqual('0.0213447171824973319'); + ).toStrictEqual('0.0213447171824973319'); }); - it('Should multiply the value by the inverse of the BigNumber conversionRate supplied when second parameter is true', () => { + it('should multiply the value by the inverse of the BigNumber conversionRate supplied when second parameter is true', () => { expect( new Numeric(10, 10) .applyConversionRate(new BigNumber(468.5, 10), true) .toString(), - ).toEqual('0.0213447171824973319'); + ).toStrictEqual('0.0213447171824973319'); }); }); }); describe('Base conversion', () => { it('should convert a hexadecimal string to a decimal string', () => { - expect(new Numeric('0x5208', 16).toBase(10).toString()).toEqual('21000'); + expect(new Numeric('0x5208', 16).toBase(10).toString()).toStrictEqual( + '21000', + ); }); it('should convert a decimal string to a hexadecimal string', () => { - expect(new Numeric('21000', 10).toBase(16).toString()).toEqual('5208'); + expect(new Numeric('21000', 10).toBase(16).toString()).toStrictEqual( + '5208', + ); }); it('should convert a decimal string to a 0x prefixed hexadecimal string', () => { - expect(new Numeric('21000', 10).toPrefixedHexString()).toEqual('0x5208'); + expect(new Numeric('21000', 10).toPrefixedHexString()).toStrictEqual( + '0x5208', + ); }); it('should convert a decimal number to a hexadecimal string', () => { - expect(new Numeric(21000, 10).toBase(16).toString()).toEqual('5208'); + expect(new Numeric(21000, 10).toBase(16).toString()).toStrictEqual( + '5208', + ); }); it('should convert a decimal number to a 0x prefixed hexadecimal string', () => { - expect(new Numeric(21000, 10).toPrefixedHexString()).toEqual('0x5208'); + expect(new Numeric(21000, 10).toPrefixedHexString()).toStrictEqual( + '0x5208', + ); }); }); describe('Comparisons', () => { - it('Should correctly identify that 0xa is greater than 0x9', () => { - expect(new Numeric('0xa', 16).greaterThan('0x9', 16)).toEqual(true); + it('should correctly identify that 0xa is greater than 0x9', () => { + expect(new Numeric('0xa', 16).greaterThan('0x9', 16)).toStrictEqual(true); }); - it('Should correctly identify that 0x9 is less than 0xa', () => { - expect(new Numeric('0x9', 16).lessThan('0xa', 16)).toEqual(true); + it('should correctly identify that 0x9 is less than 0xa', () => { + expect(new Numeric('0x9', 16).lessThan('0xa', 16)).toStrictEqual(true); }); - it('Should correctly identify that 0xa is greater than or equal to 0xa', () => { - expect(new Numeric('0xa', 16).greaterThanOrEqualTo('0xa', 16)).toEqual( + it('should correctly identify that 0xa is greater than or equal to 0xa', () => { + expect( + new Numeric('0xa', 16).greaterThanOrEqualTo('0xa', 16), + ).toStrictEqual(true); + }); + it('should correctly identify that 0xa is less than or equal to 0xa', () => { + expect(new Numeric('0xa', 16).lessThanOrEqualTo('0xa', 16)).toStrictEqual( true, ); }); - it('Should correctly identify that 0xa is less than or equal to 0xa', () => { - expect(new Numeric('0xa', 16).lessThanOrEqualTo('0xa', 16)).toEqual(true); - }); - it('Should correctly identify that 0xa is greater than 9', () => { - expect(new Numeric('0xa', 16).greaterThan(9, 10)).toEqual(true); + it('should correctly identify that 0xa is greater than 9', () => { + expect(new Numeric('0xa', 16).greaterThan(9, 10)).toStrictEqual(true); }); - it('Should correctly identify that 0x9 is less than 10', () => { - expect(new Numeric('0x9', 16).lessThan(10, 10)).toEqual(true); + it('should correctly identify that 0x9 is less than 10', () => { + expect(new Numeric('0x9', 16).lessThan(10, 10)).toStrictEqual(true); }); - it('Should correctly identify that 10 is greater than or equal to 0xa', () => { - expect(new Numeric(10, 10).greaterThanOrEqualTo('0xa', 16)).toEqual(true); + it('should correctly identify that 10 is greater than or equal to 0xa', () => { + expect(new Numeric(10, 10).greaterThanOrEqualTo('0xa', 16)).toStrictEqual( + true, + ); }); - it('Should correctly identify that 10 is less than or equal to 0xa', () => { - expect(new Numeric(10, 10).lessThanOrEqualTo('0xa', 16)).toEqual(true); + it('should correctly identify that 10 is less than or equal to 0xa', () => { + expect(new Numeric(10, 10).lessThanOrEqualTo('0xa', 16)).toStrictEqual( + true, + ); }); }); describe('Positive and Negative determination', () => { - it('Should correctly identify a negative number with isNegative', () => { - expect(new Numeric(-10, 10).isNegative()).toEqual(true); - expect(new Numeric('-10', 10).isNegative()).toEqual(true); - expect(new Numeric('-0xa', 16).isNegative()).toEqual(true); + it('should correctly identify a negative number with isNegative', () => { + expect(new Numeric(-10, 10).isNegative()).toStrictEqual(true); + expect(new Numeric('-10', 10).isNegative()).toStrictEqual(true); + expect(new Numeric('-0xa', 16).isNegative()).toStrictEqual(true); }); - it('Should return false for isNegative when number is positive', () => { - expect(new Numeric(10, 10).isNegative()).toEqual(false); - expect(new Numeric('10', 10).isNegative()).toEqual(false); - expect(new Numeric('0xa', 16).isNegative()).toEqual(false); + it('should return false for isNegative when number is positive', () => { + expect(new Numeric(10, 10).isNegative()).toStrictEqual(false); + expect(new Numeric('10', 10).isNegative()).toStrictEqual(false); + expect(new Numeric('0xa', 16).isNegative()).toStrictEqual(false); }); - it('Should correctly identify a positive number with isPositive', () => { - expect(new Numeric(10, 10).isPositive()).toEqual(true); - expect(new Numeric('10', 10).isPositive()).toEqual(true); - expect(new Numeric('0xa', 16).isPositive()).toEqual(true); + it('should correctly identify a positive number with isPositive', () => { + expect(new Numeric(10, 10).isPositive()).toStrictEqual(true); + expect(new Numeric('10', 10).isPositive()).toStrictEqual(true); + expect(new Numeric('0xa', 16).isPositive()).toStrictEqual(true); }); - it('Should return false for isPositive when number is negative', () => { - expect(new Numeric(-10, 10).isPositive()).toEqual(false); - expect(new Numeric('-10', 10).isPositive()).toEqual(false); - expect(new Numeric('-0xa', 16).isPositive()).toEqual(false); + it('should return false for isPositive when number is negative', () => { + expect(new Numeric(-10, 10).isPositive()).toStrictEqual(false); + expect(new Numeric('-10', 10).isPositive()).toStrictEqual(false); + expect(new Numeric('-0xa', 16).isPositive()).toStrictEqual(false); }); }); describe('Terminating functions, return values', () => { describe('toString', () => { - it('Should return a string representation of provided hex', () => { - expect(new Numeric('0xa', 16).toString()).toEqual('a'); + it('should return a string representation of provided hex', () => { + expect(new Numeric('0xa', 16).toString()).toStrictEqual('a'); }); - it('Should return a string representation of provided decimal string', () => { - expect(new Numeric('10', 10).toString()).toEqual('10'); + it('should return a string representation of provided decimal string', () => { + expect(new Numeric('10', 10).toString()).toStrictEqual('10'); }); - it('Should return a string representation of provided number', () => { - expect(new Numeric(10, 10).toString()).toEqual('10'); + it('should return a string representation of provided number', () => { + expect(new Numeric(10, 10).toString()).toStrictEqual('10'); }); - it('Should return a string representation of provided float', () => { - expect(new Numeric(10.5, 10).toString()).toEqual('10.5'); + it('should return a string representation of provided float', () => { + expect(new Numeric(10.5, 10).toString()).toStrictEqual('10.5'); }); - it('Should return a string representation of provided BigNumber', () => { - expect(new Numeric(new BigNumber(10, 10)).toString()).toEqual('10'); + it('should return a string representation of provided BigNumber', () => { + expect(new Numeric(new BigNumber(10, 10)).toString()).toStrictEqual( + '10', + ); }); - it('Should return a string representation of provided BN', () => { - expect(new Numeric(new BN(10, 10)).toString()).toEqual('10'); + it('should return a string representation of provided BN', () => { + expect(new Numeric(new BN(10, 10)).toString()).toStrictEqual('10'); }); }); describe('toNumber', () => { - it('Should return a number representing provided hex', () => { - expect(new Numeric('0xa', 16).toNumber()).toEqual(10); + it('should return a number representing provided hex', () => { + expect(new Numeric('0xa', 16).toNumber()).toStrictEqual(10); }); - it('Should return a number representation of provided decimal string', () => { - expect(new Numeric('10', 10).toNumber()).toEqual(10); + it('should return a number representation of provided decimal string', () => { + expect(new Numeric('10', 10).toNumber()).toStrictEqual(10); }); - it('Should return a number representation of provided number', () => { - expect(new Numeric(10, 10).toNumber()).toEqual(10); + it('should return a number representation of provided number', () => { + expect(new Numeric(10, 10).toNumber()).toStrictEqual(10); }); - it('Should return a number representation of provided float', () => { - expect(new Numeric(10.5, 10).toNumber()).toEqual(10.5); + it('should return a number representation of provided float', () => { + expect(new Numeric(10.5, 10).toNumber()).toStrictEqual(10.5); }); - it('Should return a number representation of provided BigNumber', () => { - expect(new Numeric(new BigNumber(10, 10)).toNumber()).toEqual(10); + it('should return a number representation of provided BigNumber', () => { + expect(new Numeric(new BigNumber(10, 10)).toNumber()).toStrictEqual(10); }); - it('Should return a number representation of provided BN', () => { - expect(new Numeric(new BN(10, 10)).toNumber()).toEqual(10); + it('should return a number representation of provided BN', () => { + expect(new Numeric(new BN(10, 10)).toNumber()).toStrictEqual(10); }); }); describe('toFixed', () => { - it('Should return a string representing provided hex to 2 decimal places', () => { - expect(new Numeric('0xa.7', 16).toFixed(2)).toEqual('10.44'); + it('should return a string representing provided hex to 2 decimal places', () => { + expect(new Numeric('0xa.7', 16).toFixed(2)).toStrictEqual('10.44'); }); - it('Should return a string representation of provided decimal string to 2 decimal places', () => { - expect(new Numeric('10.4375', 10).toFixed(2)).toEqual('10.44'); + it('should return a string representation of provided decimal string to 2 decimal places', () => { + expect(new Numeric('10.4375', 10).toFixed(2)).toStrictEqual('10.44'); }); - it('Should return a string representation of provided float to 2 decimal places', () => { - expect(new Numeric(10.4375, 10).toFixed(2)).toEqual('10.44'); + it('should return a string representation of provided float to 2 decimal places', () => { + expect(new Numeric(10.4375, 10).toFixed(2)).toStrictEqual('10.44'); }); - it('Should return a number representation of provided BigNumber to 2 decimal places', () => { - expect(new Numeric(new BigNumber(10.4375, 10)).toFixed(2)).toEqual( - '10.44', - ); + it('should return a number representation of provided BigNumber to 2 decimal places', () => { + expect( + new Numeric(new BigNumber(10.4375, 10)).toFixed(2), + ).toStrictEqual('10.44'); }); }); describe('round', () => { it('should return number rounded', () => { - expect(new Numeric(10.4375, 10).round()).toEqual( + expect(new Numeric(10.4375, 10).round()).toStrictEqual( new Numeric(10.4375, 10), ); - expect(new Numeric(10.4375, 10).round(0)).toEqual(new Numeric(10, 10)); - expect(new Numeric(10.4375, 10).round(1)).toEqual( + expect(new Numeric(10.4375, 10).round(0)).toStrictEqual( + new Numeric(10, 10), + ); + expect(new Numeric(10.4375, 10).round(1)).toStrictEqual( new Numeric(10.4, 10), ); - expect(new Numeric(10.4375, 10).round(2)).toEqual( + expect(new Numeric(10.4375, 10).round(2)).toStrictEqual( new Numeric(10.44, 10), ); - expect(new Numeric(10.4375, 10).round(3)).toEqual( + expect(new Numeric(10.4375, 10).round(3)).toStrictEqual( new Numeric(10.437, 10), ); - expect(new Numeric(10.4375, 10).round(4)).toEqual( + expect(new Numeric(10.4375, 10).round(4)).toStrictEqual( new Numeric(10.4375, 10), ); - expect(new Numeric(10.4375, 10).round(5)).toEqual( + expect(new Numeric(10.4375, 10).round(5)).toStrictEqual( new Numeric(10.4375, 10), ); }); diff --git a/shared/modules/error.test.ts b/shared/modules/error.test.ts index 5f1d3eee6..247ef302d 100644 --- a/shared/modules/error.test.ts +++ b/shared/modules/error.test.ts @@ -3,11 +3,11 @@ import { isErrorWithMessage, logErrorWithMessage } from './error'; jest.mock('loglevel'); -afterEach(() => { - jest.resetAllMocks(); -}); - describe('error module', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + describe('isErrorWithMessage', () => { it('returns true when passed an instance of an Error', () => { expect(isErrorWithMessage(new Error('test'))).toBe(true); @@ -21,12 +21,12 @@ describe('error module', () => { describe('logErrorWithMessage', () => { it('calls loglevel.error with the error.message when passed an instance of Error', () => { logErrorWithMessage(new Error('test')); - expect(log.error).toBeCalledWith('test'); + expect(log.error).toHaveBeenCalledWith('test'); }); it('calls loglevel.error with the parameter passed in when parameter is not an instance of Error', () => { logErrorWithMessage({ test: 'test' }); - expect(log.error).toBeCalledWith({ test: 'test' }); + expect(log.error).toHaveBeenCalledWith({ test: 'test' }); }); }); }); diff --git a/shared/modules/fetch-with-timeout.test.js b/shared/modules/fetch-with-timeout.test.ts similarity index 100% rename from shared/modules/fetch-with-timeout.test.js rename to shared/modules/fetch-with-timeout.test.ts diff --git a/shared/modules/fetch-with-timeout.js b/shared/modules/fetch-with-timeout.ts similarity index 51% rename from shared/modules/fetch-with-timeout.js rename to shared/modules/fetch-with-timeout.ts index 791470d77..f5c6d5384 100644 --- a/shared/modules/fetch-with-timeout.js +++ b/shared/modules/fetch-with-timeout.ts @@ -1,12 +1,25 @@ import { memoize } from 'lodash'; import { SECOND } from '../constants/time'; +/** + * Returns a function that can be used to make an HTTP request but timing out + * automatically after a desired amount of time. + * + * @param timeout - The number of milliseconds to wait until the request times + * out. + * @returns A function that, when called, returns a promise that either resolves + * to the HTTP response object or is rejected if a network error is encountered + * or the request times out. + */ const getFetchWithTimeout = memoize((timeout = SECOND * 30) => { if (!Number.isInteger(timeout) || timeout < 1) { throw new Error('Must specify positive integer timeout.'); } - return async function _fetch(url, opts) { + return async function fetchWithTimeout( + url: RequestInfo, + opts?: RequestInit, + ): Promise { const abortController = new window.AbortController(); const { signal } = abortController; const f = window.fetch(url, { @@ -17,12 +30,9 @@ const getFetchWithTimeout = memoize((timeout = SECOND * 30) => { const timer = setTimeout(() => abortController.abort(), timeout); try { - const res = await f; + return await f; + } finally { clearTimeout(timer); - return res; - } catch (e) { - clearTimeout(timer); - throw e; } }; }); diff --git a/shared/modules/network.utils.test.ts b/shared/modules/network.utils.test.ts new file mode 100644 index 000000000..ee4ef3f83 --- /dev/null +++ b/shared/modules/network.utils.test.ts @@ -0,0 +1,86 @@ +import { MAX_SAFE_CHAIN_ID } from '../constants/network'; +import { + isSafeChainId, + isPrefixedFormattedHexString, + isTokenDetectionEnabledForNetwork, +} from './network.utils'; + +describe('network utils', () => { + describe('isSafeChainId', () => { + it('returns true given an integer greater than 0 and less than or equal to the max safe chain ID', () => { + expect(isSafeChainId(3)).toBe(true); + }); + + it('returns true given the max safe chain ID', () => { + expect(isSafeChainId(MAX_SAFE_CHAIN_ID)).toBe(true); + }); + + it('returns false given something other than an integer', () => { + expect(isSafeChainId('not-an-integer')).toBe(false); + }); + + it('returns false given a negative integer', () => { + expect(isSafeChainId(-1)).toBe(false); + }); + + it('returns false given an integer greater than the max safe chain ID', () => { + expect(isSafeChainId(MAX_SAFE_CHAIN_ID + 1)).toBe(false); + }); + }); + + describe('isPrefixedFormattedHexString', () => { + it('returns true given a string that matches a hex number formatted as a "0x"-prefixed, non-zero, non-zero-padded string', () => { + expect(isPrefixedFormattedHexString('0x1')).toBe(true); + expect(isPrefixedFormattedHexString('0xa')).toBe(true); + expect(isPrefixedFormattedHexString('0xabc123')).toBe(true); + }); + + it('returns true given a "0x"-prefixed hex string that contains uppercase characters', () => { + expect(isPrefixedFormattedHexString('0XABC123')).toBe(true); + }); + + it('returns false given a "0x"-prefixed hex string that evaluates to zero', () => { + expect(isPrefixedFormattedHexString('0x0')).toBe(false); + }); + + it('returns false given a "0x"-prefixed hex string that does not evaluate to zero but is zero-padded', () => { + expect(isPrefixedFormattedHexString('0x01')).toBe(false); + }); + + it('returns false given a hex number that is simply a string but not "0x"-prefixed', () => { + expect(isPrefixedFormattedHexString('abc123')).toBe(false); + }); + + it('returns false if given something other than a string', () => { + expect(isPrefixedFormattedHexString({ something: 'else' })).toBe(false); + }); + }); + + describe('isTokenDetectionEnabledForNetwork', () => { + it('returns true given the chain ID for Mainnet', () => { + expect(isTokenDetectionEnabledForNetwork('0x1')).toBe(true); + }); + + it('returns true given the chain ID for BSC', () => { + expect(isTokenDetectionEnabledForNetwork('0x38')).toBe(true); + }); + + it('returns true given the chain ID for Polygon', () => { + expect(isTokenDetectionEnabledForNetwork('0x89')).toBe(true); + }); + + it('returns true given the chain ID for Avalanche', () => { + expect(isTokenDetectionEnabledForNetwork('0xa86a')).toBe(true); + }); + + it('returns false given a string that is not the chain ID for Mainnet, BSC, Polygon, or Avalanche', () => { + expect(isTokenDetectionEnabledForNetwork('some other chain ID')).toBe( + false, + ); + }); + + it('returns false given undefined', () => { + expect(isTokenDetectionEnabledForNetwork(undefined)).toBe(false); + }); + }); +}); diff --git a/shared/modules/network.utils.js b/shared/modules/network.utils.ts similarity index 52% rename from shared/modules/network.utils.js rename to shared/modules/network.utils.ts index 05723b3ac..13fca471d 100644 --- a/shared/modules/network.utils.js +++ b/shared/modules/network.utils.ts @@ -5,24 +5,22 @@ import { CHAIN_IDS, MAX_SAFE_CHAIN_ID } from '../constants/network'; * Because some cryptographic libraries we use expect the chain ID to be a * number primitive, it must not exceed a certain size. * - * @param {number} chainId - The chain ID to check for safety. - * @returns {boolean} Whether the given chain ID is safe. + * @param chainId - The chain ID to check for safety. + * @returns Whether the given chain ID is safe. */ -export function isSafeChainId(chainId) { - return ( - Number.isSafeInteger(chainId) && chainId > 0 && chainId <= MAX_SAFE_CHAIN_ID - ); +export function isSafeChainId(chainId: unknown): boolean { + return isSafeInteger(chainId) && chainId > 0 && chainId <= MAX_SAFE_CHAIN_ID; } /** * Checks whether the given value is a 0x-prefixed, non-zero, non-zero-padded, * hexadecimal string. * - * @param {any} value - The value to check. - * @returns {boolean} True if the value is a correctly formatted hex string, + * @param value - The value to check. + * @returns True if the value is a correctly formatted hex string, * false otherwise. */ -export function isPrefixedFormattedHexString(value) { +export function isPrefixedFormattedHexString(value: unknown) { if (typeof value !== 'string') { return false; } @@ -35,7 +33,7 @@ export function isPrefixedFormattedHexString(value) { * @param chainId - ChainID of network * @returns Whether the current network supports token detection */ -export function isTokenDetectionEnabledForNetwork(chainId) { +export function isTokenDetectionEnabledForNetwork(chainId: string | undefined) { switch (chainId) { case CHAIN_IDS.MAINNET: case CHAIN_IDS.BSC: @@ -46,3 +44,14 @@ export function isTokenDetectionEnabledForNetwork(chainId) { return false; } } + +/** + * Like {@link Number.isSafeInteger}, but types the input as a `number` if it is + * indeed a safe integer. + * + * @param value - The value to check. + * @returns True if the value is a safe integer, false otherwise. + */ +function isSafeInteger(value: unknown): value is number { + return Number.isSafeInteger(value); +} diff --git a/shared/modules/siwe.js b/shared/modules/siwe.js index 6edbc7d75..5d719d243 100644 --- a/shared/modules/siwe.js +++ b/shared/modules/siwe.js @@ -1,55 +1,3 @@ -import { ParsedMessage } from '@spruceid/siwe-parser'; -import log from 'loglevel'; -import { stripHexPrefix } from './hexstring-utils'; - -const msgHexToText = (hex) => { - try { - const stripped = stripHexPrefix(hex); - const buff = Buffer.from(stripped, 'hex'); - return buff.length === 32 ? hex : buff.toString('utf8'); - } catch (e) { - log.error(e); - return hex; - } -}; - -/** - * A locally defined object used to provide data to identify a Sign-In With Ethereum (SIWE)(EIP-4361) message and provide the parsed message - * - * @typedef localSIWEObject - * @param {boolean} isSIWEMessage - Does the intercepted message conform to the SIWE specification? - * @param {ParsedMessage} parsedMessage - The data parsed out of the message - */ - -/** - * This function intercepts a sign message, detects if it's a - * Sign-In With Ethereum (SIWE)(EIP-4361) message, and returns an object with - * relevant SIWE data. - * - * {@see {@link https://eips.ethereum.org/EIPS/eip-4361}} - * - * @param {object} msgParams - The params of the message to sign - * @returns {localSIWEObject} - */ -export const detectSIWE = (msgParams) => { - try { - const { data } = msgParams; - const message = msgHexToText(data); - const parsedMessage = new ParsedMessage(message); - - return { - isSIWEMessage: true, - parsedMessage, - }; - } catch (error) { - // ignore error, it's not a valid SIWE message - return { - isSIWEMessage: false, - parsedMessage: null, - }; - } -}; - /** * Takes in a parsed Sign-In with Ethereum Message (EIP-4361) * and generates an array of label-value pairs diff --git a/shared/notifications/index.js b/shared/notifications/index.js index 10c24bbfe..9bf285957 100644 --- a/shared/notifications/index.js +++ b/shared/notifications/index.js @@ -102,6 +102,10 @@ export const UI_NOTIFICATIONS = { width: '100%', }, }, + 20: { + id: 20, + date: null, + }, }; export const getTranslatedUINotifications = (t, locale) => { @@ -279,5 +283,16 @@ export const getTranslatedUINotifications = (t, locale) => { ) : '', }, + 20: { + ...UI_NOTIFICATIONS[20], + title: t('notifications20Title'), + description: [t('notifications20Description')], + actionText: t('notifications20ActionText'), + date: UI_NOTIFICATIONS[20].date + ? new Intl.DateTimeFormat(formattedLocale).format( + new Date(UI_NOTIFICATIONS[20].date), + ) + : '', + }, }; }; diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 8f08aee96..951f7de92 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -16,7 +16,8 @@ "name": null } }, - "warning": null + "warning": null, + "customTokenAmount": "10" }, "history": { "mostRecentOverviewPage": "/mostRecentOverviewPage" @@ -72,7 +73,8 @@ "featureFlags": { "showIncomingTransactions": true }, - "network": "5", + "networkId": "5", + "networkStatus": "available", "provider": { "type": "rpc", "chainId": "0x5", diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 5771feb0c..3ded4081b 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -4,6 +4,9 @@ const { } = require('@metamask/snaps-utils'); const { merge } = require('lodash'); const { CHAIN_IDS } = require('../../shared/constants/network'); +const { + ACTION_QUEUE_METRICS_E2E_TEST, +} = require('../../shared/constants/test-flags'); const { SMART_CONTRACTS } = require('./seeder/smart-contracts'); function defaultFixture() { @@ -190,7 +193,8 @@ function defaultFixture() { traits: {}, }, NetworkController: { - network: '1337', + networkId: '1337', + networkStatus: 'available', provider: { chainId: CHAIN_IDS.LOCALHOST, nickname: 'Localhost 8545', @@ -318,9 +322,11 @@ function onboardingFixture() { [CHAIN_IDS.GOERLI]: true, [CHAIN_IDS.LOCALHOST]: true, }, + [ACTION_QUEUE_METRICS_E2E_TEST]: false, }, NetworkController: { - network: '1337', + networkId: '1337', + networkStatus: 'available', provider: { ticker: 'ETH', type: 'rpc', @@ -497,15 +503,6 @@ class FixtureBuilder { return this; } - withNetworkControllerSupportEIP1559() { - merge(this.fixture.data.NetworkController, { - networkDetails: { - EIPS: { 1559: true }, - }, - }); - return this; - } - withNftController(data) { merge( this.fixture.data.NftController @@ -707,6 +704,43 @@ class FixtureBuilder { return this; } + withTokensControllerERC20() { + merge(this.fixture.data.TokensController, { + tokens: [ + { + address: `__FIXTURE_SUBSTITUTION__CONTRACT${SMART_CONTRACTS.HST}`, + symbol: 'TST', + decimals: 4, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1337/0x581c3c1a2a4ebde2a0df29b5cf4c116e42945947.png', + isERC721: false, + aggregators: [], + }, + ], + ignoredTokens: [], + detectedTokens: [], + allTokens: { + '0x539': { + '0x5cfe73b6021e818b776b421b1c4db2474086a7e1': [ + { + address: `__FIXTURE_SUBSTITUTION__CONTRACT${SMART_CONTRACTS.HST}`, + symbol: 'TST', + decimals: 4, + image: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1337/0x581c3c1a2a4ebde2a0df29b5cf4c116e42945947.png', + isERC721: false, + aggregators: [], + }, + ], + }, + }, + allIgnoredTokens: {}, + allDetectedTokens: {}, + suggestedAssets: [], + }); + return this; + } + withTransactionController(data) { merge( this.fixture.data.TransactionController diff --git a/test/e2e/ganache.js b/test/e2e/ganache.js index 107b879ed..4285ea41d 100644 --- a/test/e2e/ganache.js +++ b/test/e2e/ganache.js @@ -23,6 +23,27 @@ class Ganache { return this._server.provider; } + async getAccounts() { + return await this.getProvider().request({ + method: 'eth_accounts', + params: [], + }); + } + + async getBalance() { + const accounts = await this.getAccounts(); + const balanceHex = await this.getProvider().request({ + method: 'eth_getBalance', + params: [accounts[0], 'latest'], + }); + const balanceInt = parseInt(balanceHex, 16) / 10 ** 18; + + const balanceFormatted = + balanceInt % 1 === 0 ? balanceInt : balanceInt.toFixed(4); + + return balanceFormatted; + } + async quit() { if (!this._server) { throw new Error('Server not running yet'); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 4efd0ba1b..2569dd4a3 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -1,3 +1,4 @@ +const { strict: assert } = require('assert'); const path = require('path'); const { promises: fs } = require('fs'); const BigNumber = require('bignumber.js'); @@ -144,6 +145,8 @@ async function withFixtures(options, testSuite) { driver: driverProxy ?? driver, mockServer, contractRegistry, + ganacheServer, + secondaryGanacheServer, }); } catch (error) { failed = true; @@ -289,6 +292,91 @@ const completeImportSRPOnboardingFlowWordByWord = async ( await driver.clickElement('[data-testid="pin-extension-done"]'); }; +const completeCreateNewWalletOnboardingFlow = async (driver, password) => { + // welcome + await driver.clickElement('[data-testid="onboarding-create-wallet"]'); + + // metrics + await driver.clickElement('[data-testid="metametrics-no-thanks"]'); + + // create password + await driver.fill('[data-testid="create-password-new"]', password); + await driver.fill('[data-testid="create-password-confirm"]', password); + await driver.clickElement('[data-testid="create-password-terms"]'); + await driver.clickElement('[data-testid="create-password-wallet"]'); + + // secure my wallet + await driver.clickElement('[data-testid="secure-wallet-recommended"]'); + + // reveal SRP + await driver.clickElement('[data-testid="recovery-phrase-reveal"]'); + + const revealedSeedPhrase = await driver.findElement( + '[data-testid="recovery-phrase-chips"]', + ); + + const recoveryPhrase = await revealedSeedPhrase.getText(); + + await driver.clickElement('[data-testid="recovery-phrase-next"]'); + + // confirm SRP + const words = recoveryPhrase.split(/\s*(?:[0-9)]+|\n|\.|^$|$)\s*/u); + const finalWords = words.filter((str) => str !== ''); + assert.equal(finalWords.length, 12); + + await driver.fill('[data-testid="recovery-phrase-input-2"]', finalWords[2]); + await driver.fill('[data-testid="recovery-phrase-input-3"]', finalWords[3]); + await driver.fill('[data-testid="recovery-phrase-input-7"]', finalWords[7]); + + await driver.clickElement('[data-testid="confirm-recovery-phrase"]'); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + // complete + await driver.findElement({ text: 'Wallet creation successful', tag: 'h2' }); + await driver.clickElement('[data-testid="onboarding-complete-done"]'); + + // pin extension + await driver.clickElement('[data-testid="pin-extension-next"]'); + await driver.clickElement('[data-testid="pin-extension-done"]'); +}; + +const importWrongSRPOnboardingFlow = async (driver, seedPhrase) => { + // welcome + await driver.clickElement('[data-testid="onboarding-import-wallet"]'); + + // metrics + await driver.clickElement('[data-testid="metametrics-no-thanks"]'); + + // import with recovery phrase + await driver.pasteIntoField( + '[data-testid="import-srp__srp-word-0"]', + seedPhrase, + ); + + const warningText = 'Invalid Secret Recovery Phrase'; + const warnings = await driver.findElements('.actionable-message__message'); + const warning = warnings[1]; + + assert.equal(await warning.getText(), warningText); +}; + +const selectDropdownByNum = async (elements, index) => { + await elements[index].click(); +}; + +const testSRPDropdownIterations = async (options, driver, iterations) => { + for (let i = 0; i < iterations; i++) { + await selectDropdownByNum(options, i); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const formFields = await driver.findElements('.import-srp__srp-word-label'); + const expectedNumFields = 12 + i * 3; + const actualNumFields = formFields.length; + assert.equal(actualNumFields, expectedNumFields); + } +}; + module.exports = { getWindowHandles, convertToHexValue, @@ -300,5 +388,8 @@ module.exports = { importSRPOnboardingFlow, completeImportSRPOnboardingFlow, completeImportSRPOnboardingFlowWordByWord, + completeCreateNewWalletOnboardingFlow, createDownloadFolder, + importWrongSRPOnboardingFlow, + testSRPDropdownIterations, }; diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 6f3754bd8..6cc81b400 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -346,13 +346,10 @@ describe('MetaMask', function () { }); it('finds the transaction in the transactions list', async function () { - await driver.waitForSelector( - { - css: '.transaction-list__completed-transactions .transaction-list-item__primary-currency', - text: '-1 TST', - }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.transaction-list__completed-transactions .transaction-list-item__primary-currency', + text: '-1 TST', + }); await driver.waitForSelector({ css: '.list-item__heading', @@ -380,13 +377,10 @@ describe('MetaMask', function () { await driver.delay(largeDelayMs); await driver.findElements('.transaction-list__pending-transactions'); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-1.5 TST', - }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); await driver.clickElement('.transaction-list-item__primary-currency'); await driver.delay(regularDelayMs); diff --git a/test/e2e/mv3/dapp-interactions.spec.js b/test/e2e/mv3/dapp-interactions.spec.js index 8020047cf..294182cc9 100644 --- a/test/e2e/mv3/dapp-interactions.spec.js +++ b/test/e2e/mv3/dapp-interactions.spec.js @@ -12,6 +12,7 @@ describe('MV3 - Dapp interactions', function () { balance: convertToHexValue(25000000000000000000), }, ], + concurrent: { port: 8546, chainId: 1338 }, }; it('should continue to support dapp interactions after service worker re-start', async function () { await withFixtures( diff --git a/test/e2e/nft/erc1155-interaction.spec.js b/test/e2e/nft/erc1155-interaction.spec.js index 5dcc75203..7a5b95961 100644 --- a/test/e2e/nft/erc1155-interaction.spec.js +++ b/test/e2e/nft/erc1155-interaction.spec.js @@ -129,9 +129,9 @@ describe('ERC1155 NFTs testdapp interaction', function () { it('should enable approval for a third party address to manage all ERC1155 token', async function () { const expectedMessageTitle = - 'Allow access to and transfer of all your NFT?'; + 'Allow access to and transfer all of your NFTs from this collection?'; const expectedDescription = - 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.'; + 'This allows a third party to access and transfer all of your NFTs from this collection without further notice until you revoke its access.'; const expectedWarningMessage = 'Your NFT may be at risk'; await withFixtures( { @@ -219,9 +219,9 @@ describe('ERC1155 NFTs testdapp interaction', function () { it('should revoke approval for a third party address to manage all ERC1155 token', async function () { const expectedMessageTitle = - 'Revoke permission to access and transfer all of your NFT?'; + 'Revoke permission to access and transfer all of your NFTs from this collection?'; const expectedDescription = - 'This revokes the permission for a third party to access and transfer all of your NFT without further notice.'; + 'This revokes the permission for a third party to access and transfer all of your NFTs from this collection without further notice.'; await withFixtures( { dapp: true, diff --git a/test/e2e/nft/erc721-interaction.spec.js b/test/e2e/nft/erc721-interaction.spec.js index 967c33f1e..6e9a9e959 100644 --- a/test/e2e/nft/erc721-interaction.spec.js +++ b/test/e2e/nft/erc721-interaction.spec.js @@ -59,7 +59,6 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); // Verify transaction @@ -130,7 +129,6 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); // Verify transaction @@ -202,7 +200,6 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); // Verify transaction @@ -277,7 +274,6 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); // Verify transaction diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index c5992424c..ec89f5314 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/5.0.3/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/5.1.2/', }; diff --git a/test/e2e/snaps/test-snap-bip-32.spec.js b/test/e2e/snaps/test-snap-bip-32.spec.js index 2b05c8c4a..b477fade4 100644 --- a/test/e2e/snaps/test-snap-bip-32.spec.js +++ b/test/e2e/snaps/test-snap-bip-32.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); @@ -53,79 +52,66 @@ describe('Test Snap bip-32', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // switch to metamask extension - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - - // approve install of snap - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); // wait for permissions popover, click checkboxes and confirm - await driver.delay(1000); + await driver.delay(500); await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); - await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0'); + await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1'); + await driver.clickElement( + '#public-key-access-bip32-m-44h-0h-secp256k1-0', + ); await driver.clickElement({ text: 'Confirm', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // switch back to test-snaps window - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectBip32', + text: 'Reconnect to BIP-32 Snap', + }); + // scroll to and click get public key await driver.delay(1000); - const snapButton2 = await driver.findElement('#bip32GetPublic'); - await driver.scrollToElement(snapButton2); - await driver.delay(1000); + await driver.waitForSelector({ text: 'Get Public Key' }); await driver.clickElement('#bip32GetPublic'); - // check for proper public key response - await driver.delay(1000); - const retrievePublicKeyResult1 = await driver.findElement( - '#bip32PublicKeyResult', - ); - assert.equal( - await retrievePublicKeyResult1.getText(), - '"0x043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"', - ); + // check for proper public key response using waitForSelector + await driver.waitForSelector({ + css: '#bip32PublicKeyResult', + text: '"0x043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb', + }); // scroll to and click get compressed public key - await driver.delay(1000); - const snapButton3 = await driver.findElement( - '#bip32GetCompressedPublic', - ); - await driver.scrollToElement(snapButton3); - await driver.delay(1000); + await driver.waitForSelector({ text: 'Get Compressed Public Key' }); await driver.clickElement('#bip32GetCompressedPublic'); - // check for proper public key response - await driver.delay(1000); - const retrievePublicKeyResult2 = await driver.findElement( - '#bip32PublicKeyResult', - ); - assert.equal( - await retrievePublicKeyResult2.getText(), - '"0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"', - ); + // check for proper public key response using waitForSelector + await driver.waitForSelector({ + css: '#bip32PublicKeyResult', + text: '"0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366', + }); // wait then run SECP256K1 test - await driver.delay(1000); await driver.fill('#bip32Message-secp256k1', 'foo bar'); await driver.clickElement('#sendBip32-secp256k1'); - // hit 'approve' on the custom confirm + // hit 'approve' on the signature confirmation windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle( 'MetaMask Notification', @@ -136,27 +122,22 @@ describe('Test Snap bip-32', function () { tag: 'button', }); + // switch back to the test-snaps window windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - // check result - await driver.delay(1000); - const secp256k1Result = await driver.findElement( - '#bip32MessageResult-secp256k1', - ); - assert.equal( - await secp256k1Result.getText(), - '"0x3045022100b3ade2992ea3e5eb58c7550e9bddad356e9554233c8b099ebc3cb418e9301ae2022064746e15ae024808f0ba5d860e44dc4c97e65c8cba6f5ef9ea2e8c819930d2dc"', - ); + // check results of the secp256k1 signature with waitForSelector + await driver.waitForSelector({ + css: '#bip32MessageResult-secp256k1', + text: '"0x3045022100b3ade2992ea3e5eb58c7550e9bddad356e9554233c8b099ebc3cb418e9301ae2022064746e15ae024808f0ba5d860e44dc4c97e65c8cba6f5ef9ea2e8c819930d2dc', + }); // scroll further into messages section - await driver.delay(1000); - const snapButton4 = await driver.findElement('#bip32Message-ed25519'); + const snapButton4 = await driver.findElement('#sendBip32-ed25519'); await driver.scrollToElement(snapButton4); - await driver.delay(1000); // wait then run ed25519 test - await driver.delay(1000); + await driver.delay(500); await driver.fill('#bip32Message-ed25519', 'foo bar'); await driver.clickElement('#sendBip32-ed25519'); @@ -174,15 +155,11 @@ describe('Test Snap bip-32', function () { windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - // check result - await driver.delay(1000); - const ed25519Result = await driver.findElement( - '#bip32MessageResult-ed25519', - ); - assert.equal( - await ed25519Result.getText(), - '"0xf3215b4d6c59aac7e01b4ceef530d1e2abf4857926b85a81aaae3894505699243768a887b7da4a8c2e0f25196196ba290b6531050db8dc15c252bdd508532a0a"', - ); + // check results of ed25519 signature with waitForSelector + await driver.waitForSelector({ + css: '#bip32MessageResult-ed25519', + text: '"0xf3215b4d6c59aac7e01b4ceef530d1e2abf4857926b85a81aaae3894505699243768a887b7da4a8c2e0f25196196ba290b6531050db8dc15c252bdd508532a0a"', + }); }, ); }); diff --git a/test/e2e/snaps/test-snap-bip-44.spec.js b/test/e2e/snaps/test-snap-bip-44.spec.js index 54cd9b142..cbee5ff15 100644 --- a/test/e2e/snaps/test-snap-bip-44.spec.js +++ b/test/e2e/snaps/test-snap-bip-44.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); @@ -31,13 +30,15 @@ describe('Test Snap bip-44', function () { // navigate to test snaps page and connect await driver.driver.get(TEST_SNAPS_WEBSITE_URL); await driver.delay(1000); + + // find and scroll to the bip44 test and connect const snapButton1 = await driver.findElement('#connectBip44Snap'); await driver.scrollToElement(snapButton1); await driver.delay(1000); await driver.clickElement('#connectBip44Snap'); await driver.delay(1000); - // switch to metamask extension and click connect + // switch to metamask extension and click connect and approve let windowHandles = await driver.waitUntilXWindowHandles( 2, 1000, @@ -51,54 +52,49 @@ describe('Test Snap bip-44', function () { text: 'Connect', tag: 'button', }); - - await driver.delay(2000); - - // switch to metamask extension - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - - // approve install of snap - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); + await driver.waitForSelector({ text: 'Approve & install' }); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); // deal with permissions popover - await driver.delay(1000); + await driver.delay(500); await driver.clickElement('#key-access-bip44-1-0'); - await driver.delay(1000); await driver.clickElement({ text: 'Confirm', tag: 'button', }); + await driver.waitForSelector({ text: 'Ok' }); + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); - // delay for npm installation - await driver.delay(2000); - - // click send inputs on test snap page - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); + // switch back to test-snaps window await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - await driver.delay(1000); + + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectBip44Snap', + text: 'Reconnect to BIP-44 Snap', + }); + + // find and click bip44 test await driver.clickElement('#sendBip44Test'); - // check the results of the public key test - await driver.delay(1000); - const bip44Result = await driver.findElement('#bip44Result'); - assert.equal( - await bip44Result.getText(), - '"0x86debb44fb3a984d93f326131d4c1db0bc39644f1a67b673b3ab45941a1cea6a385981755185ac4594b6521e4d1e08d1"', - ); + // check the results of the public key test using waitForSelector + await driver.waitForSelector({ + css: '#bip44Result', + text: '"0x86debb44fb3a984d93f326131d4c1db0bc39644f1a67b673b3ab45941a1cea6a385981755185ac4594b6521e4d1e08d1"', + }); // enter a message to sign - await driver.fill('#bip44Message', '1234'); - await driver.delay(1000); + await driver.pasteIntoField('#bip44Message', '1234'); + await driver.delay(500); const snapButton3 = await driver.findElement('#signBip44Message'); await driver.scrollToElement(snapButton3); - await driver.delay(1000); + await driver.delay(500); await driver.clickElement('#signBip44Message'); // Switch to approve signature message window and approve @@ -115,14 +111,12 @@ describe('Test Snap bip-44', function () { // switch back to test-snaps page windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - await driver.delay(1000); - // check the results of the message signature - const bip44SignResult = await driver.findElement('#bip44SignResult'); - assert.equal( - await bip44SignResult.getText(), - '"0xa41ab87ca50606eefd47525ad90294bbe44c883f6bc53655f1b8a55aa8e1e35df216f31be62e52c7a1faa519420e20810162e07dedb0fde2a4d997ff7180a78232ecd8ce2d6f4ba42ccacad33c5e9e54a8c4d41506bdffb2bb4c368581d8b086"', - ); + // check the results of the message signature using waitForSelector + await driver.waitForSelector({ + css: '#bip44SignResult', + text: '"0xa41ab87ca50606eefd47525ad90294bbe44c883f6bc53655f1b8a55aa8e1e35df216f31be62e52c7a1faa519420e20810162e07dedb0fde2a4d997ff7180a78232ecd8ce2d6f4ba42ccacad33c5e9e54a8c4d41506bdffb2bb4c368581d8b086"', + }); }, ); }); diff --git a/test/e2e/snaps/test-snap-cronjob.spec.js b/test/e2e/snaps/test-snap-cronjob.spec.js index 5818c1d37..78a15490a 100644 --- a/test/e2e/snaps/test-snap-cronjob.spec.js +++ b/test/e2e/snaps/test-snap-cronjob.spec.js @@ -52,26 +52,29 @@ describe('Test Snap Cronjob', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // click send inputs on test snap page - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectCronjobSnap', + text: 'Reconnect to Cronjob Snap', + }); + // switch to dialog popup, wait for a maximum of 65 seconds windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 65000); await driver.switchToWindowWithTitle( diff --git a/test/e2e/snaps/test-snap-dialog.spec.js b/test/e2e/snaps/test-snap-dialog.spec.js index f910a6b3b..f2b51321b 100644 --- a/test/e2e/snaps/test-snap-dialog.spec.js +++ b/test/e2e/snaps/test-snap-dialog.spec.js @@ -52,29 +52,32 @@ describe('Test Snap Dialog', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // switch to test snaps tab - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectDialogSnap', + text: 'Reconnect to Dialog Snap', + }); + // click on alert dialog await driver.clickElement('#sendAlertButton'); - await driver.delay(1000); + await driver.delay(500); // switch to dialog popup windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); @@ -82,12 +85,12 @@ describe('Test Snap Dialog', function () { 'MetaMask Notification', windowHandles, ); - await driver.delay(1000); + await driver.delay(500); // check dialog contents let result = await driver.findElement('.snap-ui-renderer__panel'); await driver.scrollToElement(result); - await driver.delay(1000); + await driver.delay(500); assert.equal(await result.getText(), 'Alert Dialog\nText here'); // click ok button @@ -102,12 +105,12 @@ describe('Test Snap Dialog', function () { // check result is null result = await driver.findElement('#dialogResult'); - await driver.delay(1000); + await driver.delay(500); assert.equal(await result.getText(), 'null'); // click conf button await driver.clickElement('#sendConfButton'); - await driver.delay(1000); + await driver.delay(500); // switch to dialog popup windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); @@ -115,7 +118,7 @@ describe('Test Snap Dialog', function () { 'MetaMask Notification', windowHandles, ); - await driver.delay(1000); + await driver.delay(500); // click reject await driver.clickElement({ @@ -129,12 +132,12 @@ describe('Test Snap Dialog', function () { // check for false result result = await driver.findElement('#dialogResult'); - await driver.delay(1000); + await driver.delay(500); assert.equal(await result.getText(), 'false'); // click conf button again await driver.clickElement('#sendConfButton'); - await driver.delay(1000); + await driver.delay(500); // switch to dialog popup windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); @@ -142,7 +145,7 @@ describe('Test Snap Dialog', function () { 'MetaMask Notification', windowHandles, ); - await driver.delay(1000); + await driver.delay(500); // click accept await driver.clickElement({ @@ -156,12 +159,12 @@ describe('Test Snap Dialog', function () { // check for true result result = await driver.findElement('#dialogResult'); - await driver.delay(1000); + await driver.delay(500); assert.equal(await result.getText(), 'true'); // click prompt button await driver.clickElement('#sendPromptButton'); - await driver.delay(1000); + await driver.delay(500); // switch to dialog popup windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); @@ -169,7 +172,7 @@ describe('Test Snap Dialog', function () { 'MetaMask Notification', windowHandles, ); - await driver.delay(1000); + await driver.delay(500); // click cancel button await driver.clickElement({ @@ -183,12 +186,12 @@ describe('Test Snap Dialog', function () { // check result is equal to 'null' result = await driver.findElement('#dialogResult'); - await driver.delay(1000); + await driver.delay(500); assert.equal(await result.getText(), 'null'); // click prompt button await driver.clickElement('#sendPromptButton'); - await driver.delay(1000); + await driver.delay(500); // switch to dialog popup windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); @@ -196,10 +199,10 @@ describe('Test Snap Dialog', function () { 'MetaMask Notification', windowHandles, ); - await driver.delay(1000); + await driver.delay(500); // fill '2323' in form field - await driver.fill('.MuiInput-input', '2323'); + await driver.pasteIntoField('.MuiInput-input', '2323'); // click submit button await driver.clickElement({ @@ -213,7 +216,6 @@ describe('Test Snap Dialog', function () { // check result is equal to '2323' result = await driver.findElement('#dialogResult'); - await driver.delay(1000); assert.equal(await result.getText(), '"2323"'); }, ); diff --git a/test/e2e/snaps/test-snap-error.spec.js b/test/e2e/snaps/test-snap-error.spec.js index 2efb31d89..3e116c6e3 100644 --- a/test/e2e/snaps/test-snap-error.spec.js +++ b/test/e2e/snaps/test-snap-error.spec.js @@ -38,7 +38,7 @@ describe('Test Snap Error', function () { await driver.delay(1000); // switch to metamask extension and click connect - let windowHandles = await driver.waitUntilXWindowHandles( + const windowHandles = await driver.waitUntilXWindowHandles( 3, 1000, 10000, @@ -53,33 +53,35 @@ describe('Test Snap Error', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // click send inputs on test snap page - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - await driver.delay(1000); + + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectErrorSnap', + text: 'Reconnect to Error Snap', + }); // find and click on send error await driver.clickElement('#sendError'); // switch back to the extension page await driver.switchToWindow(extensionPage); - await driver.delay(1000); + await driver.delay(500); // look for the actual error and check if it is correct const error = await driver.findElement( diff --git a/test/e2e/snaps/test-snap-installed.spec.js b/test/e2e/snaps/test-snap-installed.spec.js index 08cabb6ec..d14852a52 100644 --- a/test/e2e/snaps/test-snap-installed.spec.js +++ b/test/e2e/snaps/test-snap-installed.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); @@ -33,9 +32,9 @@ describe('Test Snap Installed', function () { await driver.delay(1000); const confirmButton = await driver.findElement('#connectDialogSnap'); await driver.scrollToElement(confirmButton); - await driver.delay(1000); + await driver.delay(500); await driver.clickElement('#connectDialogSnap'); - await driver.delay(1000); + await driver.delay(500); // switch to metamask extension and click connect let windowHandles = await driver.waitUntilXWindowHandles( @@ -52,29 +51,32 @@ describe('Test Snap Installed', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // click send inputs on test snap page - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectDialogSnap', + text: 'Reconnect to Dialog Snap', + }); + const errorButton = await driver.findElement('#connectErrorSnap'); await driver.scrollToElement(errorButton); - await driver.delay(1000); + await driver.delay(500); await driver.clickElement('#connectErrorSnap'); // switch to metamask extension and click connect @@ -88,32 +90,27 @@ describe('Test Snap Installed', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // approve install of snap - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - const result = await driver.findElement('#installedSnapsResult'); - await driver.scrollToElement(result); - await driver.delay(1000); - assert.equal( - await result.getText(), - 'npm:@metamask/test-snap-dialog, npm:@metamask/test-snap-error', - ); + // wait for npm installation success + await driver.waitForSelector({ + css: '#installedSnapsResult', + text: 'npm:@metamask/test-snap-dialog, npm:@metamask/test-snap-error', + }); }, ); }); diff --git a/test/e2e/snaps/test-snap-management.spec.js b/test/e2e/snaps/test-snap-management.spec.js index f66068b53..cb44e7d57 100644 --- a/test/e2e/snaps/test-snap-management.spec.js +++ b/test/e2e/snaps/test-snap-management.spec.js @@ -54,21 +54,19 @@ describe('Test Snap Management', function () { tag: 'button', }); - await driver.delay(1000); + await driver.waitForSelector({ text: 'Approve & install' }); - // approve install of snap - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // switch to the original MM tab const extensionPage = windowHandles[0]; @@ -147,9 +145,12 @@ describe('Test Snap Management', function () { // check the results of the removal await driver.delay(2000); const removeResult = await driver.findElement( - '.snap-list-tab__container--no-snaps', + '.snap-list-tab__container--no-snaps_inner', + ); + assert.equal( + await removeResult.getText(), + "You don't have any snaps installed.", ); - assert.equal(await removeResult.getText(), 'No Snaps installed'); }, ); }); diff --git a/test/e2e/snaps/test-snap-managestate.spec.js b/test/e2e/snaps/test-snap-managestate.spec.js index 28892419d..7e209d01a 100644 --- a/test/e2e/snaps/test-snap-managestate.spec.js +++ b/test/e2e/snaps/test-snap-managestate.spec.js @@ -41,7 +41,7 @@ describe('Test Snap manageState', function () { await driver.delay(1000); // switch to metamask extension and click connect - let windowHandles = await driver.waitUntilXWindowHandles( + const windowHandles = await driver.waitUntilXWindowHandles( 2, 1000, 10000, @@ -54,27 +54,32 @@ describe('Test Snap manageState', function () { text: 'Connect', tag: 'button', }); - await driver.delay(2000); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); + await driver.waitForSelector({ text: 'Approve & install' }); + await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // fill and click send inputs on test snap page - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); + + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectManageState', + text: 'Reconnect to Manage State Snap', + }); + await driver.delay(1000); - await driver.fill('#dataManageState', '23'); + await driver.pasteIntoField('#dataManageState', '23'); const snapButton2 = await driver.findElement( '#retrieveManageStateResult', ); diff --git a/test/e2e/snaps/test-snap-notification.spec.js b/test/e2e/snaps/test-snap-notification.spec.js index 81a4a1307..5113a327a 100644 --- a/test/e2e/snaps/test-snap-notification.spec.js +++ b/test/e2e/snaps/test-snap-notification.spec.js @@ -40,7 +40,7 @@ describe('Test Snap Notification', function () { await driver.delay(1000); // switch to metamask extension and click connect - let windowHandles = await driver.waitUntilXWindowHandles( + const windowHandles = await driver.waitUntilXWindowHandles( 3, 1000, 10000, @@ -54,31 +54,35 @@ describe('Test Snap Notification', function () { text: 'Connect', tag: 'button', }); - await driver.delay(2000); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(3, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); + await driver.waitForSelector({ text: 'Approve & install' }); + await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // click send inputs on test snap page - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - await driver.delay(1000); + + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectNotification', + text: 'Reconnect to Notification Snap', + }); + await driver.clickElement('#sendInAppNotification'); // switch back to the extension page await driver.switchToWindow(extensionPage); - await driver.delay(1500); + await driver.delay(1000); // check to see that there is one notification const notificationResult = await driver.findElement( @@ -88,14 +92,14 @@ describe('Test Snap Notification', function () { // try to click on the account menu icon (via xpath) await driver.clickElement('.account-menu__icon'); - await driver.delay(1000); + await driver.delay(500); // try to click on the notification item (via xpath) await driver.clickElement({ text: 'Notifications', tag: 'div', }); - await driver.delay(1000); + await driver.delay(500); // look for the correct text in notifications (via xpath) const notificationResultMessage = await driver.findElement( diff --git a/test/e2e/snaps/test-snap-rpc.spec.js b/test/e2e/snaps/test-snap-rpc.spec.js index 19715ec36..3505a6c85 100644 --- a/test/e2e/snaps/test-snap-rpc.spec.js +++ b/test/e2e/snaps/test-snap-rpc.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); @@ -28,13 +27,15 @@ describe('Test Snap RPC', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - // navigate to test snaps page and connect + // navigate to test snaps page await driver.driver.get(TEST_SNAPS_WEBSITE_URL); await driver.delay(1000); - const snapButton1 = await driver.findElement('#connectRpcSnap'); + + // find and scroll to the bip32 test and connect + const snapButton1 = await driver.findElement('#connectBip32'); await driver.scrollToElement(snapButton1); await driver.delay(1000); - await driver.clickElement('#connectRpcSnap'); + await driver.clickElement('#connectBip32'); await driver.delay(1000); // switch to metamask extension and click connect @@ -52,21 +53,65 @@ describe('Test Snap RPC', function () { tag: 'button', }); - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); + + await driver.clickElement({ + text: 'Approve & install', + tag: 'button', + }); + + // wait for permissions popover, click checkboxes and confirm + await driver.delay(500); + await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); + await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1'); + await driver.clickElement( + '#public-key-access-bip32-m-44h-0h-secp256k1-0', + ); + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + }); + + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); + + // switch back to test-snaps window + await driver.switchToWindowWithTitle('Test Snaps', windowHandles); + + const snapButton2 = await driver.findElement('#connectRpcSnap'); + await driver.scrollToElement(snapButton2); + await driver.delay(1000); + await driver.clickElement('#connectRpcSnap'); + await driver.delay(1000); - // approve install of snap windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); await driver.switchToWindowWithTitle( 'MetaMask Notification', windowHandles, ); + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.waitForSelector({ text: 'Approve & install' }); + await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - // switch back to test snaps page - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); + await driver.switchToWindowWithTitle('Test Snaps', windowHandles); // wait for npm installation success @@ -76,61 +121,16 @@ describe('Test Snap RPC', function () { }); // click send inputs on test snap page - const snapButton2 = await driver.findElement('#sendRpc'); - await driver.scrollToElement(snapButton2); + const snapButton3 = await driver.findElement('#sendRpc'); + await driver.scrollToElement(snapButton3); await driver.delay(1000); await driver.clickElement('#sendRpc'); - // approve and install part one - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.clickElement({ - text: 'Approve & install', - tag: 'button', - }); - - // wait for window to close - await driver.delay(2000); - - // approve and install part two - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.clickElement({ - text: 'Approve & install', - tag: 'button', - }); - - // wait for permissions popover + // check result with waitForSelector await driver.waitForSelector({ - text: 'Confirm', - tag: 'button', + css: '#rpcResult', + text: '"0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"', }); - - // click checkboxes and confirm - await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); - await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0'); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - - // delay for result creation - await driver.delay(2500); - - // check the results of the custom confirm - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); - await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - const confirmResult = await driver.findElement('#rpcResult'); - assert.equal( - await confirmResult.getText(), - '"0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"', - ); }, ); }); diff --git a/test/e2e/snaps/test-snap-txinsights.spec.js b/test/e2e/snaps/test-snap-txinsights.spec.js index 4003501a6..138e42897 100644 --- a/test/e2e/snaps/test-snap-txinsights.spec.js +++ b/test/e2e/snaps/test-snap-txinsights.spec.js @@ -53,26 +53,21 @@ describe('Test Snap TxInsights', function () { tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Approve & install' }); - // switch to metamask extension - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - - // approve install of snap - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); await driver.clickElement({ text: 'Approve & install', tag: 'button', }); - await driver.delay(1000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // switch to test-snaps page and get accounts - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); await driver.clickElement('#getAccounts'); await driver.delay(1000); @@ -105,7 +100,7 @@ describe('Test Snap TxInsights', function () { 'MetaMask Notification', windowHandles, ); - await driver.delay(2000); + await driver.delay(1000); await driver.clickElement({ text: 'TxInsightsTest', tag: 'button', diff --git a/test/e2e/snaps/test-snap-update.spec.js b/test/e2e/snaps/test-snap-update.spec.js index 1120f20b8..ac80c18ff 100644 --- a/test/e2e/snaps/test-snap-update.spec.js +++ b/test/e2e/snaps/test-snap-update.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); @@ -37,7 +36,7 @@ describe('Test Snap update', function () { await driver.scrollToElement(snapButton); await driver.delay(1000); await driver.clickElement('#connectUpdate'); - await driver.delay(2000); + await driver.delay(1000); // switch to metamask extension and click connect let windowHandles = await driver.waitUntilXWindowHandles( @@ -53,35 +52,41 @@ describe('Test Snap update', function () { text: 'Connect', tag: 'button', }); - await driver.delay(2000); - // approve install of snap - windowHandles = await driver.waitUntilXWindowHandles(2, 1000, 10000); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); + await driver.waitForSelector({ text: 'Approve & install' }); + await driver.clickElement({ text: 'Approve & install', tag: 'button', }); // wait for permissions popover, click checkboxes and confirm - await driver.delay(1000); + await driver.delay(500); await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); - await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0'); + await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1'); + await driver.clickElement( + '#public-key-access-bip32-m-44h-0h-secp256k1-0', + ); await driver.clickElement({ text: 'Confirm', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // navigate to test snap page - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); - await driver.delay(1000); + + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectUpdate', + text: 'Reconnect to Update Snap', + }); // find and scroll to the correct card and click first const snapButton2 = await driver.findElement('#connectUpdateNew'); @@ -90,32 +95,36 @@ describe('Test Snap update', function () { await driver.clickElement('#connectUpdateNew'); await driver.delay(1000); - // switch to metamask extension and click connect + // switch to metamask extension and update await driver.waitUntilXWindowHandles(2, 1000, 10000); - await driver.delay(1000); - - // approve update of snap windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle( 'MetaMask Notification', windowHandles, ); + + await driver.waitForSelector({ text: 'Approve & update' }); + await driver.clickElement({ text: 'Approve & update', tag: 'button', }); - // delay for npm installation - await driver.delay(2000); + await driver.waitForSelector({ text: 'Ok' }); + + await driver.clickElement({ + text: 'Ok', + tag: 'button', + }); // navigate to test snap page - windowHandles = await driver.waitUntilXWindowHandles(1, 1000, 10000); await driver.switchToWindowWithTitle('Test Snaps', windowHandles); // look for the correct version text - const versionResult = await driver.findElement('#updateSnapVersion'); - await driver.delay(1000); - assert.equal(await versionResult.getText(), '"5.0.1"'); + await driver.waitForSelector({ + css: '#updateSnapVersion', + text: '"5.1.2"', + }); }, ); }); diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index dc10beb56..226c8bda9 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -242,7 +242,7 @@ describe('Add account', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( diff --git a/test/e2e/tests/address-book.spec.js b/test/e2e/tests/address-book.spec.js index 9184afb04..af1214521 100644 --- a/test/e2e/tests/address-book.spec.js +++ b/test/e2e/tests/address-book.spec.js @@ -63,13 +63,10 @@ describe('Address Book', function () { return confirmedTxes.length === 1; }, 10000); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-2 ETH', - }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-2 ETH', + }); }, ); }); diff --git a/test/e2e/tests/clear-activity.spec.js b/test/e2e/tests/clear-activity.spec.js index 12903510c..a16254c9c 100644 --- a/test/e2e/tests/clear-activity.spec.js +++ b/test/e2e/tests/clear-activity.spec.js @@ -34,14 +34,14 @@ describe('Clear account activity', function () { // Check send transaction and receive transaction history are all displayed await driver.clickElement('[data-testid="home__activity-tab"]'); - await driver.waitForSelector( - { css: '.list-item__title', text: 'Send' }, - { timeout: 10000 }, - ); - await driver.waitForSelector( - { css: '.list-item__title', text: 'Receive' }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.list-item__title', + text: 'Send', + }); + await driver.waitForSelector({ + css: '.list-item__title', + text: 'Receive', + }); // Clear activity and nonce data await driver.clickElement('.account-menu__icon'); diff --git a/test/e2e/tests/contract-interactions.spec.js b/test/e2e/tests/contract-interactions.spec.js index 98b562a2e..dbfcd5259 100644 --- a/test/e2e/tests/contract-interactions.spec.js +++ b/test/e2e/tests/contract-interactions.spec.js @@ -25,7 +25,7 @@ describe('Deploy contract and call contract methods', function () { smartContract, title: this.test.title, }, - async ({ driver, contractRegistry }) => { + async ({ driver, contractRegistry, ganacheServer }) => { const contractAddress = await contractRegistry.getContractAddress( smartContract, ); @@ -63,15 +63,11 @@ describe('Deploy contract and call contract methods', function () { await driver.clickElement({ text: 'Activity', tag: 'button' }); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, - ); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-4 ETH', - }, - { timeout: 10000 }, ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-4 ETH', + }); // calls and confirms a contract method where ETH is received await driver.switchToWindow(dapp); @@ -87,27 +83,20 @@ describe('Deploy contract and call contract methods', function () { await driver.switchToWindow(extension); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', - { timeout: 10000 }, - ); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-0 ETH', - }, - { timeout: 10000 }, ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-0 ETH', + }); // renders the correct ETH balance await driver.switchToWindow(extension); - const balance = await driver.waitForSelector( - { - css: '[data-testid="eth-overview__primary-currency"]', - text: '21.', - }, - { timeout: 10000 }, - ); - const tokenAmount = await balance.getText(); - assert.ok(/^21.*\s*ETH.*$/u.test(tokenAmount)); + const balance = await ganacheServer.getBalance(); + const balanceElement = await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"]', + text: balance, + }); + assert.equal(`${balance}\nETH`, await balanceElement.getText()); }, ); }); diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index 62700d389..5e1addcc5 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -119,16 +119,16 @@ describe('Create token, approve token and approve token without gas', function ( ); await driver.clickElement({ - text: 'Verify contract details', + text: 'Verify third-party details', css: '.token-allowance-container__verify-link', }); const modalTitle = await driver.waitForSelector({ - text: 'Contract details', + text: 'Third-party details', tag: 'h5', }); - assert.equal(await modalTitle.getText(), 'Contract details'); + assert.equal(await modalTitle.getText(), 'Third-party details'); await driver.clickElement({ text: 'Got it', @@ -162,7 +162,7 @@ describe('Create token, approve token and approve token without gas', function ( await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.findElement({ - text: 'Review your spending cap', + text: 'Review the spending cap for your', tag: 'div', }); diff --git a/test/e2e/tests/ens.spec.js b/test/e2e/tests/ens.spec.js index f5e3536c0..3a0dffee1 100644 --- a/test/e2e/tests/ens.spec.js +++ b/test/e2e/tests/ens.spec.js @@ -25,6 +25,34 @@ describe('ENS', function () { }; }); + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBalance' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: '0x1', + }, + }; + }); + + await mockServer + .forPost(infuraUrl) + .withJsonBodyIncluding({ method: 'eth_getBlockByNumber' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + jsonrpc: '2.0', + id: '1111111111111111', + result: {}, + }, + }; + }); + await mockServer .forPost(infuraUrl) .withJsonBodyIncluding({ method: 'eth_call' }) diff --git a/test/e2e/tests/failing-contract.spec.js b/test/e2e/tests/failing-contract.spec.js index b3f9bb5c0..7f0dd766e 100644 --- a/test/e2e/tests/failing-contract.spec.js +++ b/test/e2e/tests/failing-contract.spec.js @@ -63,7 +63,7 @@ describe('Failing contract interaction ', function () { // dismiss warning and confirm the transaction await driver.clickElement({ text: 'I want to proceed anyway', - tag: 'span', + tag: 'button', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); @@ -71,7 +71,6 @@ describe('Failing contract interaction ', function () { await driver.clickElement({ text: 'Activity', tag: 'button' }); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); // display the transaction status @@ -149,7 +148,7 @@ describe('Failing contract interaction on non-EIP1559 network', function () { // dismiss warning and confirm the transaction await driver.clickElement({ text: 'I want to proceed anyway', - tag: 'span', + tag: 'button', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); @@ -157,7 +156,6 @@ describe('Failing contract interaction on non-EIP1559 network', function () { await driver.clickElement({ text: 'Activity', tag: 'button' }); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); // display the transaction status diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index 6a9318351..892453c17 100644 --- a/test/e2e/tests/from-import-ui.spec.js +++ b/test/e2e/tests/from-import-ui.spec.js @@ -212,7 +212,7 @@ describe('MetaMask Import UI', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey1); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( @@ -239,7 +239,7 @@ describe('MetaMask Import UI', function () { await driver.clickElement({ text: 'Import account', tag: 'div' }); // enter private key await driver.fill('#private-key-box', testPrivateKey2); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should see new account in account menu const importedAccount2Name = await driver.findElement( @@ -267,13 +267,10 @@ describe('MetaMask Import UI', function () { await driver.clickElement({ text: 'Remove', tag: 'button' }); // Wait until selected account switches away from removed account to first account - await driver.waitForSelector( - { - css: '.selected-account__name', - text: 'Account 1', - }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.selected-account__name', + text: 'Account 1', + }); await driver.delay(regularDelayMs); await driver.clickElement('.account-menu__icon'); @@ -330,7 +327,7 @@ describe('MetaMask Import UI', function () { await driver.fill('#json-password-box', 'foobarbazqux'); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( @@ -392,7 +389,7 @@ describe('MetaMask Import UI', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // error should occur await driver.waitForSelector({ diff --git a/test/e2e/tests/metamask-responsive-ui.spec.js b/test/e2e/tests/metamask-responsive-ui.spec.js index 8dc91e0b4..4c83081c2 100644 --- a/test/e2e/tests/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/metamask-responsive-ui.spec.js @@ -84,7 +84,7 @@ describe('MetaMask Responsive UI', function () { title: this.test.title, failOnConsoleError: false, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { await driver.navigate(); // Import Secret Recovery Phrase @@ -104,9 +104,10 @@ describe('MetaMask Responsive UI', function () { await driver.press('#confirm-password', driver.Key.ENTER); // balance renders + const balance = await ganacheServer.getBalance(); await driver.waitForSelector({ css: '[data-testid="eth-overview__primary-currency"]', - text: '1000 ETH', + text: `${balance} ETH`, }); }, ); @@ -162,13 +163,10 @@ describe('MetaMask Responsive UI', function () { return confirmedTxes.length === 1; }, 10000); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-1 ETH', - }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }); }, ); }); diff --git a/test/e2e/tests/navigate-transactions.spec.js b/test/e2e/tests/navigate-transactions.spec.js index 4d4136732..9a0011b34 100644 --- a/test/e2e/tests/navigate-transactions.spec.js +++ b/test/e2e/tests/navigate-transactions.spec.js @@ -136,13 +136,10 @@ describe('Navigate transactions', function () { const windowHandles = await driver.getAllWindowHandles(); const extension = windowHandles[0]; await driver.switchToWindow(extension); - navigationElement = await driver.waitForSelector( - { - css: '.confirm-page-container-navigation', - text: '2 of 5', - }, - { timeout: 10000 }, - ); + navigationElement = await driver.waitForSelector({ + css: '.confirm-page-container-navigation', + text: '2 of 5', + }); navigationText = await navigationElement.getText(); assert.equal( navigationText.includes('2 of 5'), @@ -169,13 +166,10 @@ describe('Navigate transactions', function () { // reject transaction await driver.clickElement({ text: 'Reject', tag: 'button' }); - const navigationElement = await driver.waitForSelector( - { - css: '.confirm-page-container-navigation', - text: '1 of 3', - }, - { timeout: 10000 }, - ); + const navigationElement = await driver.waitForSelector({ + css: '.confirm-page-container-navigation', + text: '1 of 3', + }); const navigationText = await navigationElement.getText(); assert.equal( navigationText.includes('1 of 3'), @@ -202,13 +196,10 @@ describe('Navigate transactions', function () { // confirm transaction await driver.clickElement({ text: 'Confirm', tag: 'button' }); - const navigationElement = await driver.waitForSelector( - { - css: '.confirm-page-container-navigation', - text: '1 of 3', - }, - { timeout: 10000 }, - ); + const navigationElement = await driver.waitForSelector({ + css: '.confirm-page-container-navigation', + text: '1 of 3', + }); const navigationText = await navigationElement.getText(); assert.equal( navigationText.includes('1 of 3'), @@ -228,7 +219,7 @@ describe('Navigate transactions', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); @@ -236,10 +227,11 @@ describe('Navigate transactions', function () { // reject transactions await driver.clickElement({ text: 'Reject 4', tag: 'a' }); await driver.clickElement({ text: 'Reject all', tag: 'button' }); - const balance = await driver.findElement( + const balance = await ganacheServer.getBalance(); + const balanceElement = await driver.findElement( '[data-testid="eth-overview__primary-currency"]', ); - assert.ok(/^25\sETH$/u.test(await balance.getText())); + assert.equal(`${balance}\nETH`, await balanceElement.getText()); }, ); }); diff --git a/test/e2e/tests/onboarding.spec.js b/test/e2e/tests/onboarding.spec.js index 3430c3efc..7778989d3 100644 --- a/test/e2e/tests/onboarding.spec.js +++ b/test/e2e/tests/onboarding.spec.js @@ -1,8 +1,13 @@ const { strict: assert } = require('assert'); +const { By } = require('selenium-webdriver'); const { convertToHexValue, withFixtures, + completeCreateNewWalletOnboardingFlow, + completeImportSRPOnboardingFlow, importSRPOnboardingFlow, + importWrongSRPOnboardingFlow, + testSRPDropdownIterations, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -10,6 +15,10 @@ describe('MetaMask onboarding', function () { const testSeedPhrase = 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress'; const testPassword = 'correct horse battery staple'; + const wrongSeedPhrase = + 'test test test test test test test test test test test test'; + const wrongTestPassword = 'test test test test'; + const ganacheOptions = { accounts: [ { @@ -19,6 +28,212 @@ describe('MetaMask onboarding', function () { }, ], }; + + it('Clicks create a new wallet, accepts a secure password, reveals the Secret Recovery Phrase, confirm SRP', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + await completeCreateNewWalletOnboardingFlow(driver, testPassword); + + const homePage = await driver.findElement('.home__main-view'); + const homePageDisplayed = await homePage.isDisplayed(); + + assert.equal(homePageDisplayed, true); + }, + ); + }); + + it('Clicks import a new wallet, accepts a secure password, reveals the Secret Recovery Phrase, confirm SRP', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + await completeImportSRPOnboardingFlow( + driver, + testSeedPhrase, + testPassword, + ); + + const homePage = await driver.findElement('.home__main-view'); + const homePageDisplayed = await homePage.isDisplayed(); + + assert.equal(homePageDisplayed, true); + }, + ); + }); + + it('User import wrong Secret Recovery Phrase', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + await importWrongSRPOnboardingFlow(driver, wrongSeedPhrase); + + const confirmSeedPhrase = await driver.findElement( + '[data-testid="import-srp-confirm"]', + ); + + assert.equal(await confirmSeedPhrase.isEnabled(), false); + }, + ); + }); + + it('Check if user select different type of secret recovery phrase', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + // welcome + await driver.clickElement('[data-testid="onboarding-import-wallet"]'); + + await driver.clickElement('[data-testid="metametrics-no-thanks"]'); + + const dropdowns = await driver.findElements('select'); + const dropdownElement = dropdowns[1]; + await dropdownElement.click(); + const options = await dropdownElement.findElements( + By.tagName('option'), + ); + + const iterations = options.length; + + await testSRPDropdownIterations(options, driver, iterations); + + const finalFormFields = await driver.findElements( + '.import-srp__srp-word-label', + ); + const expectedFinalNumFields = 24; // The last iteration will have 24 fields + const actualFinalNumFields = finalFormFields.length; + assert.equal(actualFinalNumFields, expectedFinalNumFields); + }, + ); + }); + + it('User enters the wrong password during password creation', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + await driver.clickElement('[data-testid="onboarding-create-wallet"]'); + + // metrics + await driver.clickElement('[data-testid="metametrics-no-thanks"]'); + + // Fill in confirm password field with incorrect password + await driver.fill('[data-testid="create-password-new"]', testPassword); + await driver.fill( + '[data-testid="create-password-confirm"]', + wrongTestPassword, + ); + + // Check that the error message is displayed for the password fields + await driver.isElementPresent( + // eslint-disable-next-line prettier/prettier + { text: 'Passwords don\'t match', tag: 'h6' }, + true, + ); + + // Check that the "Confirm Password" button is disabled + const confirmPasswordButton = await driver.findElement( + '[data-testid="create-password-wallet"]', + ); + assert.equal(await confirmPasswordButton.isEnabled(), false); + }, + ); + }); + + it('Verify that the user has been redirected to the correct page after importing their wallet', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + await importSRPOnboardingFlow(driver, testSeedPhrase, testPassword); + // Verify site + assert.equal( + await driver.isElementPresent({ + text: 'Wallet creation successful', + tag: 'h2', + }), + true, + ); + }, + ); + }); + + it('Verify that the user has been redirected to the correct page after creating a password for their new wallet', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }).build(), + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + await driver.clickElement('[data-testid="onboarding-create-wallet"]'); + + // metrics + await driver.clickElement('[data-testid="metametrics-no-thanks"]'); + + // Fill in confirm password field with correct password + await driver.fill('[data-testid="create-password-new"]', testPassword); + await driver.fill( + '[data-testid="create-password-confirm"]', + testPassword, + ); + await driver.clickElement('[data-testid="create-password-terms"]'); + await driver.clickElement('[data-testid="create-password-wallet"]'); + + // Verify site + assert.equal( + await driver.isElementPresent({ + text: 'Secure your wallet', + tag: 'h2', + }), + true, + ); + }, + ); + }); + const ganacheOptions2 = { accounts: [ { @@ -45,7 +260,7 @@ describe('MetaMask onboarding', function () { title: this.test.title, }, - async ({ driver }) => { + async ({ driver, secondaryGanacheServer }) => { await driver.navigate(); await importSRPOnboardingFlow(driver, testSeedPhrase, testPassword); @@ -85,10 +300,11 @@ describe('MetaMask onboarding', function () { ); assert.equal(await networkDisplay.getText(), networkName); - const balance1 = await driver.findElement( + const balance = await secondaryGanacheServer.getBalance(); + const balanceElement = await driver.findElement( '[data-testid="eth-overview__primary-currency"]', ); - assert.ok(/^10\sETH$/u.test(await balance1.getText())); + assert.equal(`${balance}\nETH`, await balanceElement.getText()); }, ); }); diff --git a/test/e2e/tests/permissions.spec.js b/test/e2e/tests/permissions.spec.js index 3ebcbf117..3f61a7689 100644 --- a/test/e2e/tests/permissions.spec.js +++ b/test/e2e/tests/permissions.spec.js @@ -13,7 +13,6 @@ describe('Permissions', function () { }, ], }; - const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; await withFixtures( { dapp: true, @@ -21,7 +20,9 @@ describe('Permissions', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { + const addresses = await ganacheServer.getAccounts(); + const publicAddress = addresses[0]; await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); diff --git a/test/e2e/tests/personal-sign.spec.js b/test/e2e/tests/personal-sign.spec.js index 0622595d3..86f798c67 100644 --- a/test/e2e/tests/personal-sign.spec.js +++ b/test/e2e/tests/personal-sign.spec.js @@ -13,7 +13,6 @@ describe('Personal sign', function () { }, ], }; - const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; await withFixtures( { dapp: true, @@ -23,7 +22,9 @@ describe('Personal sign', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { + const addresses = await ganacheServer.getAccounts(); + const publicAddress = addresses[0]; await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); @@ -56,13 +57,10 @@ describe('Personal sign', function () { const verifySigUtil = await driver.findElement( '#personalSignVerifySigUtilResult', ); - const verifyECRecover = await driver.waitForSelector( - { - css: '#personalSignVerifyECRecoverResult', - text: publicAddress, - }, - { timeout: 10000 }, - ); + const verifyECRecover = await driver.waitForSelector({ + css: '#personalSignVerifyECRecoverResult', + text: publicAddress, + }); assert.equal(await verifySigUtil.getText(), publicAddress); assert.equal(await verifyECRecover.getText(), publicAddress); }, diff --git a/test/e2e/tests/provider-api.spec.js b/test/e2e/tests/provider-api.spec.js index 449f894bd..50cac9cef 100644 --- a/test/e2e/tests/provider-api.spec.js +++ b/test/e2e/tests/provider-api.spec.js @@ -24,7 +24,9 @@ describe('MetaMask', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { + const addresses = await ganacheServer.getAccounts(); + const publicAddress = addresses[0]; await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); @@ -60,10 +62,7 @@ describe('MetaMask', function () { assert.equal(await switchedNetworkDiv.getText(), '0x1'); assert.equal(await switchedChainIdDiv.getText(), '0x1'); - assert.equal( - await accountsDiv.getText(), - '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - ); + assert.equal(await accountsDiv.getText(), publicAddress); }, ); }); diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index d5005dcf3..b5542c1cb 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -196,13 +196,10 @@ describe('Send ETH from inside MetaMask using advanced gas modal', function () { return confirmedTxes.length === 1; }, 10000); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-1 ETH', - }, - { timeout: 10000 }, - ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }); }, ); }); @@ -275,7 +272,6 @@ describe('Send ETH from dapp using advanced gas controls', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); await driver.waitForSelector({ css: '.transaction-list-item__primary-currency', @@ -302,9 +298,11 @@ describe('Send ETH from dapp using advanced gas controls', function () { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() - .withNetworkControllerSupportEIP1559() .build(), - ganacheOptions, + ganacheOptions: { + ...ganacheOptions, + hardfork: 'london', + }, title: this.test.title, }, async ({ driver }) => { @@ -345,7 +343,6 @@ describe('Send ETH from dapp using advanced gas controls', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); await driver.waitForSelector({ css: '.transaction-list-item__primary-currency', diff --git a/test/e2e/tests/send-hex-address.spec.js b/test/e2e/tests/send-hex-address.spec.js index fcde57874..08d7b2c1d 100644 --- a/test/e2e/tests/send-hex-address.spec.js +++ b/test/e2e/tests/send-hex-address.spec.js @@ -123,43 +123,21 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { await withFixtures( { dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), + fixtures: new FixtureBuilder().withTokensControllerERC20().build(), ganacheOptions, smartContract, title: this.test.title, failOnConsoleError: false, }, - async ({ driver, contractRegistry }) => { - const contractAddress = await contractRegistry.getContractAddress( - smartContract, - ); + async ({ driver, ganacheServer }) => { await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - - await driver.openNewPage( - `http://127.0.0.1:8080/?contract=${contractAddress}`, - ); - let windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - - // Using the line below to make wait time deterministic and avoid using a delay - // See more here https://github.com/MetaMask/metamask-extension/pull/15604/files#r949300551 - await driver.findClickableElement('#deployButton'); - - // Add token - await driver.clickElement('#watchAsset'); - await driver.waitUntilXWindowHandles(3); - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.clickElement({ text: 'Add token', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + const balanceAfterDeployment = await ganacheServer.getBalance(); + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"]', + text: `${balanceAfterDeployment} ETH`, + }); // Send TST await driver.clickElement('[data-testid="home__asset-tab"]'); @@ -175,7 +153,10 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { css: '.ens-input__selected-input__title', text: hexPrefixedAddress, }); - await driver.delay(2000); + await driver.waitForSelector({ + css: '.transaction-detail-item', + text: '0.00008346 ETH', + }); await driver.clickElement({ text: 'Next', tag: 'button' }); // Confirm transaction @@ -187,7 +168,6 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); const sendTransactionListItem = await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', @@ -208,42 +188,21 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { await withFixtures( { dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), + fixtures: new FixtureBuilder().withTokensControllerERC20().build(), ganacheOptions, smartContract, title: this.test.title, failOnConsoleError: false, }, - async ({ driver, contractRegistry }) => { - const contractAddress = await contractRegistry.getContractAddress( - smartContract, - ); + async ({ driver, ganacheServer }) => { await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - - // Create TST - await driver.openNewPage( - `http://127.0.0.1:8080/?contract=${contractAddress}`, - ); - - let windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - - // Add token - await driver.findClickableElement('#deployButton'); - await driver.clickElement('#watchAsset'); - await driver.waitUntilXWindowHandles(3); - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.clickElement({ text: 'Add token', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + const balanceAfterDeployment = await ganacheServer.getBalance(); + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"]', + text: `${balanceAfterDeployment} ETH`, + }); // Send TST await driver.clickElement('[data-testid="home__asset-tab"]'); @@ -259,7 +218,10 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { css: '.ens-input__selected-input__title', text: hexPrefixedAddress, }); - await driver.delay(2000); + await driver.waitForSelector({ + css: '.transaction-detail-item', + text: '0.00008346 ETH', + }); await driver.clickElement({ text: 'Next', tag: 'button' }); // Confirm transaction @@ -271,7 +233,6 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', - { timeout: 10000 }, ); const sendTransactionListItem = await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', diff --git a/test/e2e/tests/send-to-contract.spec.js b/test/e2e/tests/send-to-contract.spec.js index e7dffa5e7..62a6a58e4 100644 --- a/test/e2e/tests/send-to-contract.spec.js +++ b/test/e2e/tests/send-to-contract.spec.js @@ -18,9 +18,7 @@ describe('Send ERC20 token to contract address', function () { await withFixtures( { dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), + fixtures: new FixtureBuilder().withTokensControllerERC20().build(), ganacheOptions, smartContract, title: this.test.title, @@ -34,26 +32,6 @@ describe('Send ERC20 token to contract address', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - // Create TST - await driver.openNewPage( - `http://127.0.0.1:8080/?contract=${contractAddress}`, - ); - let windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - - // Add token - await driver.findClickableElement('#deployButton'); - await driver.clickElement('#watchAsset'); - await driver.waitUntilXWindowHandles(3); - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.clickElement({ text: 'Add token', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); - // Send TST await driver.clickElement('[data-testid="home__asset-tab"]'); await driver.clickElement('.token-cell'); diff --git a/test/e2e/tests/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js index 91a91eaf0..62ac5cd78 100644 --- a/test/e2e/tests/signature-request.spec.js +++ b/test/e2e/tests/signature-request.spec.js @@ -17,7 +17,6 @@ describe('Sign Typed Data V4 Signature Request', function () { }, ], }; - const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; await withFixtures( { dapp: true, @@ -27,7 +26,9 @@ describe('Sign Typed Data V4 Signature Request', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { + const addresses = await ganacheServer.getAccounts(); + const publicAddress = addresses[0]; await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); @@ -59,7 +60,7 @@ describe('Sign Typed Data V4 Signature Request', function () { assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); verifyContractDetailsButton.click(); - await driver.findElement({ text: 'Contract details', tag: 'h5' }); + await driver.findElement({ text: 'Third-party details', tag: 'h5' }); await driver.findElement('[data-testid="recipient"]'); await driver.clickElement({ text: 'Got it', tag: 'button' }); @@ -97,7 +98,6 @@ describe('Sign Typed Data V3 Signature Request', function () { }, ], }; - const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; await withFixtures( { dapp: true, @@ -107,7 +107,9 @@ describe('Sign Typed Data V3 Signature Request', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { + const addresses = await ganacheServer.getAccounts(); + const publicAddress = addresses[0]; await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); @@ -140,7 +142,7 @@ describe('Sign Typed Data V3 Signature Request', function () { assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); verifyContractDetailsButton.click(); - await driver.findElement({ text: 'Contract details', tag: 'h5' }); + await driver.findElement({ text: 'Third-party details', tag: 'h5' }); await driver.findElement('[data-testid="recipient"]'); await driver.clickElement({ text: 'Got it', tag: 'button' }); @@ -178,7 +180,6 @@ describe('Sign Typed Data Signature Request', function () { }, ], }; - const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; await withFixtures( { dapp: true, @@ -188,7 +189,9 @@ describe('Sign Typed Data Signature Request', function () { ganacheOptions, title: this.test.title, }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { + const addresses = await ganacheServer.getAccounts(); + const publicAddress = addresses[0]; await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 09bdccb1b..2b2fcb386 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -428,10 +428,6 @@ class Driver { const artifactDir = `./test-artifacts/${this.browser}/${title}`; const filepathBase = `${artifactDir}/test-failure`; await fs.mkdir(artifactDir, { recursive: true }); - const isPageError = await this.isElementPresent('.error-page__details'); - if (isPageError) { - await this.clickElement('.error-page__details'); - } const screenshot = await this.driver.takeScreenshot(); await fs.writeFile(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64', @@ -482,20 +478,22 @@ class Driver { // 4Byte 'Failed to load resource: the server responded with a status of 502 (Bad Gateway)', ]; + const { errors } = this; const cdpConnection = await this.driver.createCDPConnection('page'); await this.driver.onLogEvent(cdpConnection, (event) => { if (event.type === 'error') { - const eventDescription = event.args.filter( + const eventDescriptions = event.args.filter( (err) => err.description !== undefined, ); - const [{ description }] = eventDescription; + + const [eventDescription] = eventDescriptions; const ignore = ignoredErrorMessages.some((message) => - description.includes(message), + eventDescription?.description.includes(message), ); if (!ignore) { - errors.push(description); - logBrowserError(failOnConsoleError, description); + errors.push(eventDescription?.description); + logBrowserError(failOnConsoleError, eventDescription?.description); } } }); diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index b0dd8eaee..80c477a10 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -1,5 +1,5 @@ import { CHAIN_IDS } from '../../shared/constants/network'; -import { HardwareKeyringTypes } from '../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../shared/constants/keyring'; const createGetSmartTransactionFeesApiResponse = () => { return { @@ -252,13 +252,10 @@ export const createSwapsMockStore = () => { }, }, selectedAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - keyringTypes: [ - HardwareKeyringTypes.imported, - HardwareKeyringTypes.hdKeyTree, - ], + keyringTypes: [KeyringType.imported, KeyringType.hdKeyTree], keyrings: [ { - type: HardwareKeyringTypes.hdKeyTree, + type: KeyringType.hdKeyTree, accounts: [ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb', @@ -266,7 +263,7 @@ export const createSwapsMockStore = () => { ], }, { - type: HardwareKeyringTypes.imported, + type: KeyringType.imported, accounts: ['0xd85a4b6a394794842887b8284293d69163007bbb'], }, ], diff --git a/test/merge-coverage.js b/test/merge-coverage.js index 36e17c079..9cf8cad20 100644 --- a/test/merge-coverage.js +++ b/test/merge-coverage.js @@ -233,7 +233,7 @@ async function start() { // checking test file coverage is redundant. '!**/*.test.js', '!**/__mocks__/**/*.js', - '!**/*.stories.js', + '!**/*.stories.*', ]), coverageMap, ); diff --git a/types/eth-json-rpc-filters/index.d.ts b/types/eth-json-rpc-filters/index.d.ts new file mode 100644 index 000000000..f7515bccd --- /dev/null +++ b/types/eth-json-rpc-filters/index.d.ts @@ -0,0 +1 @@ +declare module 'eth-json-rpc-filters'; diff --git a/types/eth-json-rpc-filters/subscriptionManager.d.ts b/types/eth-json-rpc-filters/subscriptionManager.d.ts new file mode 100644 index 000000000..a79ff8ee4 --- /dev/null +++ b/types/eth-json-rpc-filters/subscriptionManager.d.ts @@ -0,0 +1 @@ +declare module 'eth-json-rpc-filters/subscriptionManager'; diff --git a/types/eth-keyring-controller.d.ts b/types/eth-keyring-controller.d.ts new file mode 100644 index 000000000..86d8ffc6b --- /dev/null +++ b/types/eth-keyring-controller.d.ts @@ -0,0 +1,9 @@ +declare module '@metamask/eth-keyring-controller' { + export class KeyringController { + signMessage: (...any) => any; + + signPersonalMessage: (...any) => any; + + signTypedMessage: (...any) => any; + } +} diff --git a/ui/components/app/account-menu/__snapshots__/account-menu.test.js.snap b/ui/components/app/account-menu/__snapshots__/account-menu.test.js.snap new file mode 100644 index 000000000..f7604d462 --- /dev/null +++ b/ui/components/app/account-menu/__snapshots__/account-menu.test.js.snap @@ -0,0 +1,312 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Account Menu Render Content should not render keyring label if keyring tyoe is Custody - JSONRPC 1`] = ` +
+
0xDea...beeF
@@ -580,7 +580,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xbBb...BBbB
@@ -656,7 +656,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xB0B...Ea57
@@ -732,7 +732,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xB0B...0000
@@ -999,7 +999,7 @@ exports[`Signature Request Component render should match snapshot when we want t
- Verify contract details + Verify third-party details
@@ -1144,7 +1144,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xCD2...D826
@@ -1220,7 +1220,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xDea...beeF
@@ -1355,7 +1355,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xbBb...BBbB
@@ -1431,7 +1431,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xB0B...Ea57
@@ -1507,7 +1507,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xB0B...0000
diff --git a/ui/components/app/signature-request/signature-request.component.js b/ui/components/app/signature-request/signature-request.component.js index b1b40b283..92fb56a06 100644 --- a/ui/components/app/signature-request/signature-request.component.js +++ b/ui/components/app/signature-request/signature-request.component.js @@ -3,7 +3,7 @@ import { memoize } from 'lodash'; import PropTypes from 'prop-types'; import LedgerInstructionField from '../ledger-instruction-field'; import { sanitizeMessage, getURLHostName } from '../../../helpers/utils/util'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import SiteOrigin from '../../ui/site-origin'; import Button from '../../ui/button'; import Typography from '../../ui/typography/typography'; @@ -196,7 +196,7 @@ export default class SignatureRequest extends PureComponent { const onSign = (event) => { sign(event); trackEvent({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, event: 'Confirm', properties: { action: 'Sign Request', @@ -210,7 +210,7 @@ export default class SignatureRequest extends PureComponent { const onCancel = (event) => { cancel(event); trackEvent({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, event: 'Cancel', properties: { action: 'Sign Request', diff --git a/ui/components/app/tab-bar/tab-bar.js b/ui/components/app/tab-bar/tab-bar.js index c76fba371..bcb97b80b 100644 --- a/ui/components/app/tab-bar/tab-bar.js +++ b/ui/components/app/tab-bar/tab-bar.js @@ -2,7 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Icon, ICON_NAMES, ICON_SIZES } from '../../component-library'; +import { + Icon, + ICON_NAMES, + ICON_SIZES, +} from '../../component-library/icon/deprecated'; const TabBar = (props) => { const { tabs = [], onSelect, isActive } = props; diff --git a/ui/components/app/tab-bar/tab-bar.stories.js b/ui/components/app/tab-bar/tab-bar.stories.js index fb304c6ee..d7e08ed7c 100644 --- a/ui/components/app/tab-bar/tab-bar.stories.js +++ b/ui/components/app/tab-bar/tab-bar.stories.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Icon, ICON_NAMES } from '../../component-library'; +import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import TabBar from '.'; export default { diff --git a/ui/components/app/token-cell/token-cell.js b/ui/components/app/token-cell/token-cell.js index 1a18f9c83..60d8f304a 100644 --- a/ui/components/app/token-cell/token-cell.js +++ b/ui/components/app/token-cell/token-cell.js @@ -3,9 +3,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import { useSelector } from 'react-redux'; import AssetListItem from '../asset-list-item'; -import { getSelectedAddress } from '../../../selectors'; +import { getSelectedAddress, getTokenList } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; +import { MultichainTokenListItem } from '../../multichain'; +import { ButtonLink, Text } from '../../component-library'; +import { TextColor } from '../../../helpers/constants/design-system'; export default function TokenCell({ address, @@ -19,39 +22,58 @@ export default function TokenCell({ }) { const userAddress = useSelector(getSelectedAddress); const t = useI18nContext(); - + const tokenList = useSelector(getTokenList); + const tokenData = Object.values(tokenList).find( + (token) => token.symbol === symbol, + ); + const title = tokenData?.name || symbol; + const tokenImage = tokenData?.iconUrl || image; const formattedFiat = useTokenFiatAmount(address, string, symbol); const warning = balanceError ? ( - + {t('troubleTokenBalances')} - event.stopPropagation()} - style={{ color: 'var(--color-warning-default)' }} + textProps={{ + color: TextColor.warningDefault, + }} > {t('here')} - - + + ) : null; return ( - + <> + {process.env.MULTICHAIN ? ( + onClick(address)} + tokenSymbol={symbol} + tokenImage={tokenImage} + primary={`${string || 0}`} + secondary={formattedFiat} + title={title} + /> + ) : ( + onClick(address)} + tokenAddress={address} + tokenSymbol={symbol} + tokenDecimals={decimals} + tokenImage={image} + warning={warning} + primary={`${string || 0}`} + secondary={formattedFiat} + isERC721={isERC721} + /> + )} + ); } diff --git a/ui/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js b/ui/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js index 23a79ee7c..3e253e621 100644 --- a/ui/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js +++ b/ui/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js @@ -12,7 +12,11 @@ import { TRANSACTION_CANCEL_ATTEMPTED_EVENT, TRANSACTION_CANCEL_SUCCESS_EVENT, } from '../transaction-activity-log.constants'; -import { Icon, ICON_NAMES, ICON_SIZES } from '../../../component-library'; +import { + Icon, + ICON_NAMES, + ICON_SIZES, +} from '../../../component-library/icon/deprecated'; import { Color } from '../../../../helpers/constants/design-system'; export const ACTIVITY_ICONS = { diff --git a/ui/components/app/transaction-activity-log/transaction-activity-log.component.js b/ui/components/app/transaction-activity-log/transaction-activity-log.component.js index c041f351b..ede7e175e 100644 --- a/ui/components/app/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/components/app/transaction-activity-log/transaction-activity-log.component.js @@ -4,7 +4,7 @@ import classnames from 'classnames'; import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { formatDate, getURLHostName } from '../../../helpers/utils/util'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { getValueFromWeiHex } from '../../../../shared/modules/conversion.utils'; import TransactionActivityLogIcon from './transaction-activity-log-icon'; import { CONFIRMED_STATUS } from './transaction-activity-log.constants'; @@ -34,7 +34,7 @@ export default class TransactionActivityLog extends PureComponent { const etherscanUrl = getBlockExplorerLink(activity, rpcPrefs); this.context.trackEvent({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, event: 'Clicked Block Explorer Link', properties: { link_type: 'Transaction Block Explorer', diff --git a/ui/components/app/transaction-alerts/transaction-alerts.stories.js b/ui/components/app/transaction-alerts/transaction-alerts.stories.js index 1edddfd2d..7cb5886fe 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.stories.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.stories.js @@ -18,7 +18,7 @@ const customTransaction = ({ userFeeLevel: estimateUsed ? 'low' : 'medium', blockNumber: `${10902987 + i}`, id: 4678200543090545 + i, - metamaskNetworkId: testData?.metamask?.network, + metamaskNetworkId: testData?.metamask?.networkId, chainId: testData?.metamask?.provider?.chainId, status: 'confirmed', time: 1600654021000, diff --git a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js index 9b576b5b3..21220448a 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js +++ b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js @@ -58,7 +58,7 @@ const Address = ({
setShowNicknamePopovers(true)} > {recipientToRender} diff --git a/ui/components/app/transaction-decoding/components/decoding/address/index.scss b/ui/components/app/transaction-decoding/components/decoding/address/index.scss index ca821dae8..71bf88fad 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/index.scss +++ b/ui/components/app/transaction-decoding/components/decoding/address/index.scss @@ -1,12 +1,13 @@ -.tx-insight-content { - .tx-insight-component-address { - display: flex; - align-items: center; - cursor: pointer; - overflow: visible; +.tx-insight-component-address { + display: flex; + align-items: center; + overflow: visible; - &__sender-icon { - padding-right: 5px; - } + &__sender-icon { + padding-right: 5px; + } + + &__name { + cursor: pointer; } } diff --git a/ui/components/app/transaction-decoding/components/ui/copy-raw-data/copy-raw-data.component.js b/ui/components/app/transaction-decoding/components/ui/copy-raw-data/copy-raw-data.component.js index a84003d75..8e1f62d93 100644 --- a/ui/components/app/transaction-decoding/components/ui/copy-raw-data/copy-raw-data.component.js +++ b/ui/components/app/transaction-decoding/components/ui/copy-raw-data/copy-raw-data.component.js @@ -4,7 +4,10 @@ import Tooltip from '../../../../../ui/tooltip/tooltip'; import { I18nContext } from '../../../../../../contexts/i18n'; import { useCopyToClipboard } from '../../../../../../hooks/useCopyToClipboard'; -import { Icon, ICON_NAMES } from '../../../../../component-library'; +import { + Icon, + ICON_NAMES, +} from '../../../../../component-library/icon/deprecated'; import { IconColor } from '../../../../../../helpers/constants/design-system'; const CopyRawData = ({ data }) => { diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 7e96ace0e..493b461b8 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -12,7 +12,7 @@ import Tooltip from '../../ui/tooltip'; import CancelButton from '../cancel-button'; import Popover from '../../ui/popover'; import { SECOND } from '../../../../shared/constants/time'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { TransactionType } from '../../../../shared/constants/transaction'; import { getURLHostName } from '../../../helpers/utils/util'; import TransactionDecoding from '../transaction-decoding'; @@ -76,7 +76,7 @@ export default class TransactionListItemDetails extends PureComponent { history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); } else { this.context.trackEvent({ - category: EVENT.CATEGORIES.TRANSACTIONS, + category: MetaMetricsEventCategory.Transactions, event: 'Clicked Block Explorer Link', properties: { link_type: 'Transaction Block Explorer', @@ -109,7 +109,7 @@ export default class TransactionListItemDetails extends PureComponent { const { hash } = transaction; this.context.trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, + category: MetaMetricsEventCategory.Navigation, event: 'Copied Transaction ID', properties: { action: 'Activity Log', @@ -250,7 +250,7 @@ export default class TransactionListItemDetails extends PureComponent { senderAddress={senderAddress} onRecipientClick={() => { this.context.trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, + category: MetaMetricsEventCategory.Navigation, event: 'Copied "To" Address', properties: { action: 'Activity Log', @@ -260,7 +260,7 @@ export default class TransactionListItemDetails extends PureComponent { }} onSenderClick={() => { this.context.trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, + category: MetaMetricsEventCategory.Navigation, event: 'Copied "From" Address', properties: { action: 'Activity Log', diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index 3166bab54..dc99a7eac 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -13,7 +13,7 @@ import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'; import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp'; import TransactionStatusLabel from '../transaction-status-label/transaction-status-label'; import TransactionIcon from '../transaction-icon'; -import { EVENT } from '../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { TransactionGroupCategory, TransactionStatus, @@ -65,7 +65,7 @@ function TransactionListItemInner({ event.stopPropagation(); trackEvent({ event: 'Clicked "Speed Up"', - category: EVENT.CATEGORIES.NAVIGATION, + category: MetaMetricsEventCategory.Navigation, properties: { action: 'Activity Log', legacy_event: true, @@ -86,7 +86,7 @@ function TransactionListItemInner({ event.stopPropagation(); trackEvent({ event: 'Clicked "Cancel"', - category: EVENT.CATEGORIES.NAVIGATION, + category: MetaMetricsEventCategory.Navigation, properties: { action: 'Activity Log', legacy_event: true, diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 12e69b59b..0d3b37d81 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -29,19 +29,16 @@ import IconButton from '../../ui/icon-button'; import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - EVENT, - EVENT_NAMES, - CONTEXT_PROPS, + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsSwapsEventSource, } from '../../../../shared/constants/metametrics'; import Spinner from '../../ui/spinner'; import { startNewDraftTransaction } from '../../../ducks/send'; import { AssetType } from '../../../../shared/constants/transaction'; -import { - ButtonIcon, - BUTTON_ICON_SIZES, - Icon, - ICON_NAMES, -} from '../../component-library'; +import { ButtonIcon, BUTTON_ICON_SIZES } from '../../component-library'; +import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { IconColor } from '../../../helpers/constants/design-system'; import useRamps from '../../../hooks/experiences/useRamps'; import WalletOverview from './wallet-overview'; @@ -110,15 +107,15 @@ const EthOverview = ({ className }) => { }); trackEvent( { - category: EVENT.CATEGORIES.HOME, - event: EVENT_NAMES.PORTFOLIO_LINK_CLICKED, + category: MetaMetricsEventCategory.Home, + event: MetaMetricsEventName.PortfolioLinkClicked, properties: { url: portfolioUrl, }, }, { contextPropsIntoEventProperties: [ - CONTEXT_PROPS.PAGE_TITLE, + MetaMetricsContextProp.PageTitle, ], }, ); @@ -154,8 +151,8 @@ const EthOverview = ({ className }) => { onClick={() => { openBuyCryptoInPdapp(); trackEvent({ - event: EVENT_NAMES.NAV_BUY_BUTTON_CLICKED, - category: EVENT.CATEGORIES.NAVIGATION, + event: MetaMetricsEventName.NavBuyButtonClicked, + category: MetaMetricsEventCategory.Navigation, properties: { location: 'Home', text: 'Buy', @@ -175,8 +172,8 @@ const EthOverview = ({ className }) => { label={t('send')} onClick={() => { trackEvent({ - event: EVENT_NAMES.NAV_SEND_BUTTON_CLICKED, - category: EVENT.CATEGORIES.NAVIGATION, + event: MetaMetricsEventName.NavSendButtonClicked, + category: MetaMetricsEventCategory.Navigation, properties: { token_symbol: 'ETH', location: 'Home', @@ -202,11 +199,11 @@ const EthOverview = ({ className }) => { onClick={() => { if (isSwapsChain) { trackEvent({ - event: EVENT_NAMES.NAV_SWAP_BUTTON_CLICKED, - category: EVENT.CATEGORIES.SWAPS, + event: MetaMetricsEventName.NavSwapButtonClicked, + category: MetaMetricsEventCategory.Swaps, properties: { token_symbol: 'ETH', - location: EVENT.SOURCE.SWAPS.MAIN_VIEW, + location: MetaMetricsSwapsEventSource.MainView, text: 'Swap', }, }); @@ -248,8 +245,8 @@ const EthOverview = ({ className }) => { url: `${bridgeUrl}?metamaskEntry=ext`, }); trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, - event: EVENT_NAMES.BRIDGE_LINK_CLICKED, + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.BridgeLinkClicked, properties: { location: 'Home', text: 'Bridge', diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index af49e420b..3d4f459c1 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -4,7 +4,7 @@ import thunk from 'redux-thunk'; import { fireEvent, waitFor } from '@testing-library/react'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { renderWithProvider } from '../../../../test/jest/rendering'; -import { HardwareKeyringTypes } from '../../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../../shared/constants/keyring'; import EthOverview from './eth-overview'; // Mock BUYABLE_CHAINS_MAP @@ -56,11 +56,11 @@ describe('EthOverview', () => { selectedAddress: '0x1', keyrings: [ { - type: HardwareKeyringTypes.imported, + type: KeyringType.imported, accounts: ['0x1', '0x2'], }, { - type: HardwareKeyringTypes.ledger, + type: KeyringType.ledger, accounts: [], }, ], diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index 4c7fc481d..c83a17f3b 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -27,14 +27,15 @@ import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; import { showModal } from '../../../store/actions'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - EVENT, - EVENT_NAMES, - CONTEXT_PROPS, + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsSwapsEventSource, } from '../../../../shared/constants/metametrics'; import { AssetType } from '../../../../shared/constants/transaction'; import useRamps from '../../../hooks/experiences/useRamps'; -import { Icon, ICON_NAMES } from '../../component-library'; +import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { IconColor } from '../../../helpers/constants/design-system'; import WalletOverview from './wallet-overview'; @@ -100,8 +101,8 @@ const TokenOverview = ({ className, token }) => { onClick={() => { openBuyCryptoInPdapp(); trackEvent({ - event: EVENT_NAMES.NAV_BUY_BUTTON_CLICKED, - category: EVENT.CATEGORIES.NAVIGATION, + event: MetaMetricsEventName.NavBuyButtonClicked, + category: MetaMetricsEventCategory.Navigation, properties: { location: 'Token Overview', text: 'Buy', @@ -114,11 +115,11 @@ const TokenOverview = ({ className, token }) => { className="token-overview__button" onClick={async () => { trackEvent({ - event: EVENT_NAMES.NAV_SEND_BUTTON_CLICKED, - category: EVENT.CATEGORIES.NAVIGATION, + event: MetaMetricsEventName.NavSendButtonClicked, + category: MetaMetricsEventCategory.Navigation, properties: { token_symbol: token.symbol, - location: EVENT.SOURCE.SWAPS.TOKEN_VIEW, + location: MetaMetricsSwapsEventSource.TokenView, text: 'Send', }, }); @@ -158,11 +159,11 @@ const TokenOverview = ({ className, token }) => { onClick={() => { if (isSwapsChain) { trackEvent({ - event: EVENT_NAMES.NAV_SWAP_BUTTON_CLICKED, - category: EVENT.CATEGORIES.SWAPS, + event: MetaMetricsEventName.NavSwapButtonClicked, + category: MetaMetricsEventCategory.Swaps, properties: { token_symbol: token.symbol, - location: EVENT.SOURCE.SWAPS.TOKEN_VIEW, + location: MetaMetricsSwapsEventSource.TokenView, text: 'Swap', }, }); @@ -214,14 +215,16 @@ const TokenOverview = ({ className, token }) => { }); trackEvent( { - category: EVENT.CATEGORIES.HOME, - event: EVENT_NAMES.PORTFOLIO_LINK_CLICKED, + category: MetaMetricsEventCategory.Home, + event: MetaMetricsEventName.PortfolioLinkClicked, properties: { url: portfolioUrl, }, }, { - contextPropsIntoEventProperties: [CONTEXT_PROPS.PAGE_TITLE], + contextPropsIntoEventProperties: [ + MetaMetricsContextProp.PageTitle, + ], }, ); }} diff --git a/ui/components/app/wallet-overview/token-overview.test.js b/ui/components/app/wallet-overview/token-overview.test.js index c46e31f3b..9b2769181 100644 --- a/ui/components/app/wallet-overview/token-overview.test.js +++ b/ui/components/app/wallet-overview/token-overview.test.js @@ -4,7 +4,7 @@ import thunk from 'redux-thunk'; import { fireEvent, waitFor } from '@testing-library/react'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { renderWithProvider } from '../../../../test/jest/rendering'; -import { HardwareKeyringTypes } from '../../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../../shared/constants/keyring'; import TokenOverview from './token-overview'; // Mock BUYABLE_CHAINS_MAP @@ -42,11 +42,11 @@ describe('TokenOverview', () => { selectedAddress: '0x1', keyrings: [ { - type: HardwareKeyringTypes.hdKeyTree, + type: KeyringType.hdKeyTree, accounts: ['0x1', '0x2'], }, { - type: HardwareKeyringTypes.ledger, + type: KeyringType.ledger, accounts: [], }, ], 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 98c636e50..fd0bf14a7 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -73,6 +73,12 @@ function getActionFunctionById(id, history) { updateViewedNotifications({ 19: true }); history.push(`${EXPERIMENTAL_ROUTE}#autodetect-nfts`); }, + 20: () => { + updateViewedNotifications({ 20: true }); + global.platform.openTab({ + url: ZENDESK_URLS.LEDGER_FIREFOX_U2F_GUIDE, + }); + }, }; return actionFunctions[id]; diff --git a/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap b/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap index 4fb7ecbbe..6a46eb546 100644 --- a/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap +++ b/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap @@ -3,7 +3,7 @@ exports[`AvatarAccount should render correctly 1`] = `