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