From 088d4c34f112eb0f638ce99dae5c0d0958569038 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 6 Oct 2020 10:57:02 -0700 Subject: [PATCH] Merge pull request from GHSA-c2xw-px2x-pr65 * Remove network config store * Remove inline networks variable in network controller * Re-key network controller 'rpcTarget' to 'rpcUrl' * Require chainId in lookupNetwork, implement eth_chainId * Require chain ID in network form * Add alert, migrations, and tests * Add chainId validation to addToFrequentRpcList * Update public config state selector to match new network controller state * Use network enums in networks-tab.constants * Ensure chainId in provider config is current * Update tests --- app/_locales/am/messages.json | 6 - app/_locales/ar/messages.json | 6 - app/_locales/bg/messages.json | 6 - app/_locales/bn/messages.json | 6 - app/_locales/ca/messages.json | 6 - app/_locales/cs/messages.json | 3 - app/_locales/da/messages.json | 6 - app/_locales/de/messages.json | 3 - app/_locales/el/messages.json | 6 - app/_locales/en/messages.json | 33 ++++- app/_locales/es/messages.json | 6 - app/_locales/es_419/messages.json | 6 - app/_locales/et/messages.json | 6 - app/_locales/fa/messages.json | 6 - app/_locales/fi/messages.json | 6 - app/_locales/fil/messages.json | 6 - app/_locales/fr/messages.json | 6 - app/_locales/he/messages.json | 6 - app/_locales/hi/messages.json | 6 - app/_locales/hn/messages.json | 3 - app/_locales/hr/messages.json | 6 - app/_locales/ht/messages.json | 3 - app/_locales/hu/messages.json | 6 - app/_locales/id/messages.json | 6 - app/_locales/it/messages.json | 6 - app/_locales/ja/messages.json | 3 - app/_locales/kn/messages.json | 6 - app/_locales/ko/messages.json | 6 - app/_locales/lt/messages.json | 6 - app/_locales/lv/messages.json | 6 - app/_locales/ms/messages.json | 6 - app/_locales/nl/messages.json | 3 - app/_locales/no/messages.json | 6 - app/_locales/ph/messages.json | 3 - app/_locales/pl/messages.json | 6 - app/_locales/pt/messages.json | 3 - app/_locales/pt_BR/messages.json | 6 - app/_locales/ro/messages.json | 6 - app/_locales/ru/messages.json | 6 - app/_locales/sk/messages.json | 6 - app/_locales/sl/messages.json | 6 - app/_locales/sr/messages.json | 6 - app/_locales/sv/messages.json | 6 - app/_locales/sw/messages.json | 6 - app/_locales/ta/messages.json | 3 - app/_locales/th/messages.json | 3 - app/_locales/tr/messages.json | 3 - app/_locales/uk/messages.json | 6 - app/_locales/vi/messages.json | 3 - app/_locales/zh_CN/messages.json | 6 - app/_locales/zh_TW/messages.json | 6 - app/scripts/background.js | 3 +- app/scripts/controllers/alert.js | 10 +- .../network/createJsonRpcClient.js | 13 +- app/scripts/controllers/network/network.js | 124 ++++++++--------- app/scripts/controllers/preferences.js | 36 +++-- app/scripts/lib/select-chain-id.js | 20 --- app/scripts/lib/setupSentry.js | 5 - app/scripts/lib/typed-message-manager.js | 9 +- app/scripts/lib/util.js | 16 +++ app/scripts/metamask-controller.js | 33 +++-- app/scripts/migrations/048.js | 38 ++++++ app/scripts/migrations/index.js | 1 + test/e2e/fixtures/imported-account/state.json | 6 +- test/e2e/fixtures/localization/state.json | 6 +- test/e2e/fixtures/personal-sign/state.json | 8 +- test/e2e/metamask-ui.spec.js | 27 ++-- test/unit/actions/config_test.js | 6 +- .../controllers/metamask-controller-test.js | 8 +- .../network/network-controller-test.js | 21 ++- .../preferences-controller-test.js | 37 +++-- test/unit/app/util-test.js | 79 ++++++++++- test/unit/localhostState.js | 2 +- test/unit/migrations/048-test.js | 126 ++++++++++++++++++ test/unit/ui/app/reducers/metamask.spec.js | 2 +- ui/app/components/app/alerts/alerts.js | 15 ++- ui/app/components/app/alerts/alerts.scss | 1 + .../invalid-custom-network-alert/index.js | 1 + .../invalid-custom-network-alert.js | 97 ++++++++++++++ .../invalid-custom-network-alert.scss | 57 ++++++++ .../unconnected-account-alert.js | 2 +- .../app/dropdowns/network-dropdown.js | 107 ++++++--------- .../dropdowns/tests/network-dropdown.test.js | 11 +- .../loading-network-screen.container.js | 4 +- ui/app/components/app/network.js | 4 +- ui/app/components/ui/popover/index.scss | 4 +- ui/app/ducks/alerts/enums.js | 6 + ui/app/ducks/alerts/index.js | 3 + ui/app/ducks/alerts/invalid-custom-network.js | 51 +++++++ ui/app/ducks/alerts/unconnected-account.js | 8 +- ui/app/ducks/index.js | 3 +- ui/app/ducks/metamask/metamask.js | 6 +- ui/app/pages/routes/routes.component.js | 2 +- ui/app/pages/settings/networks-tab/index.scss | 11 ++ .../network-form/network-form.component.js | 66 +++++++-- .../networks-tab/networks-tab.constants.js | 48 ++++--- .../networks-tab/networks-tab.container.js | 4 +- ui/app/selectors/selectors.js | 6 +- .../tests/send-selectors-test-data.js | 2 +- ui/index.js | 6 +- 100 files changed, 875 insertions(+), 583 deletions(-) delete mode 100644 app/scripts/lib/select-chain-id.js create mode 100644 app/scripts/migrations/048.js create mode 100644 test/unit/migrations/048-test.js create mode 100644 ui/app/components/app/alerts/invalid-custom-network-alert/index.js create mode 100644 ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.js create mode 100644 ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.scss create mode 100644 ui/app/ducks/alerts/enums.js create mode 100644 ui/app/ducks/alerts/invalid-custom-network.js diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 36e5e34bb..6e3ce50ea 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "ልክ ያልሆነ Block Explorer ዩአርኤል" }, - "invalidInput": { - "message": "ልክ ያልሆነ ግቤት።" - }, "invalidRPC": { "message": "ልክ ያልሆነ RPC ዩአርኤል" }, @@ -743,9 +740,6 @@ "optionalBlockExplorerUrl": { "message": "ኤክስፕሎረር URL አግድ (አማራጭ)" }, - "optionalChainId": { - "message": "የሰንሰለት መለያ ቁጥር (አማራጭ)" - }, "optionalSymbol": { "message": "ምልክት (አማራጭ)" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index a7ac5ad9b..62265d304 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -562,9 +562,6 @@ "invalidBlockExplorerURL": { "message": "غير صحيح Block Explorer رابط" }, - "invalidInput": { - "message": "مدخل غير صحيح." - }, "invalidRPC": { "message": "رابط آر بي سي غير صحيح" }, @@ -739,9 +736,6 @@ "optionalBlockExplorerUrl": { "message": "العنوان الإلكتروني لمستكشف البلوكات (اختياري)" }, - "optionalChainId": { - "message": "هوية ChainID (اختياري)" - }, "optionalSymbol": { "message": "الرمز (اختياري)" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index cbb4481d1..74726c04c 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -562,9 +562,6 @@ "invalidBlockExplorerURL": { "message": "Невалиден Block Explorer URL адрес" }, - "invalidInput": { - "message": "Невалидно въвеждане." - }, "invalidRPC": { "message": "Невалиден RPC URL адрес" }, @@ -742,9 +739,6 @@ "optionalBlockExplorerUrl": { "message": "Блокиране на Explorer URL (по избор)" }, - "optionalChainId": { - "message": "ChainID (по избор)" - }, "optionalSymbol": { "message": "Символ (по избор)" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index e7879e732..3d707a5cd 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "অবৈধ Block Explorer URL" }, - "invalidInput": { - "message": "অবৈধ ইনপুট" - }, "invalidRPC": { "message": "অবৈধ RPC URL" }, @@ -746,9 +743,6 @@ "optionalBlockExplorerUrl": { "message": "এক্সপ্লোরার URL ব্লক করুন (ঐচ্ছিক)" }, - "optionalChainId": { - "message": "ChainID (ঐচ্ছিক)" - }, "optionalSymbol": { "message": "প্রতীক (ঐচ্ছিক)" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 699e1b553..db93525a0 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -553,9 +553,6 @@ "invalidBlockExplorerURL": { "message": "URL de Block Explorer" }, - "invalidInput": { - "message": "Entrada no vàlida." - }, "invalidRPC": { "message": "URL de RPC" }, @@ -730,9 +727,6 @@ "optionalBlockExplorerUrl": { "message": "Bloqueja l'URL d'Explorer (opcional)" }, - "optionalChainId": { - "message": "Cadena ID (opcional)" - }, "optionalSymbol": { "message": "Símbol (opcional)" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 65a89eba9..3239f00ab 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -222,9 +222,6 @@ "invalidBlockExplorerURL": { "message": "Neplatné Block Explorer URI" }, - "invalidInput": { - "message": "Neplatný vstup." - }, "invalidRPC": { "message": "Neplatné RPC URI" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index a67b80fdc..d302792b3 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -559,9 +559,6 @@ "invalidBlockExplorerURL": { "message": "Ugyldig Block Explorer-webadresse" }, - "invalidInput": { - "message": "Ugyldigt input." - }, "invalidRPC": { "message": "Ugyldig RPC-webadresse" }, @@ -730,9 +727,6 @@ "optionalBlockExplorerUrl": { "message": "Blok-stifinder-URL (valgfrit)" }, - "optionalChainId": { - "message": "KædeID (valgfrit)" - }, "optionalSymbol": { "message": "Symbol (valgfrit)" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 01011213a..1fae46b7b 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -554,9 +554,6 @@ "invalidBlockExplorerURL": { "message": "Ungültige Block Explorer URI" }, - "invalidInput": { - "message": "Ungültige Eingabe." - }, "invalidRPC": { "message": "Ungültige RPC URI" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 29760cc4c..814fc3aa2 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -563,9 +563,6 @@ "invalidBlockExplorerURL": { "message": "Μη έγκυρο Block Explorer URL" }, - "invalidInput": { - "message": "Μη έγκυρη είσοδος." - }, "invalidRPC": { "message": "Μη έγκυρο RPC URL" }, @@ -743,9 +740,6 @@ "optionalBlockExplorerUrl": { "message": "Διεύθυνση URL Εξερευνητή Μπλοκ (προαιρετικό)" }, - "optionalChainId": { - "message": "Ταυτότητα Αλυσίδας (προαιρετικό)" - }, "optionalSymbol": { "message": "Σύμβολο (προαιρετικό)" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3ab85c02e..75be31074 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -823,12 +823,35 @@ "invalidBlockExplorerURL": { "message": "Invalid Block Explorer URL" }, - "invalidInput": { - "message": "Invalid input." + "invalidCustomNetworkAlertContent1": { + "message": "The custom network '$1' is missing its chain ID.", + "description": "$1 is the name/identifier of the network." + }, + "invalidCustomNetworkAlertContent2": { + "message": "To protect you from malicious or faulty network providers, chain IDs are now required for all custom networks." + }, + "invalidCustomNetworkAlertContent3": { + "message": "Go to Settings > Network and enter the chain ID. You can find the chain IDs of most popular networks on $1.", + "description": "$1 is a link to https://chainid.network" + }, + "invalidCustomNetworkAlertTitle": { + "message": "Invalid Custom Network" + }, + "invalidHexNumber": { + "message": "Invalid hexadecimal number." + }, + "invalidHexNumberLeadingZeros": { + "message": "Invalid hexadecimal number. Remove any leading zeros." }, "invalidIpfsGateway": { "message": "Invalid IPFS Gateway: The value must be a valid URL" }, + "invalidNumber": { + "message": "Invalid number. Enter a decimal or hexadecimal number." + }, + "invalidNumberLeadingZeros": { + "message": "Invalid number. Remove any leading zeros." + }, "invalidRPC": { "message": "Invalid RPC URL" }, @@ -951,6 +974,9 @@ "networkName": { "message": "Network Name" }, + "networkSettingsChainIdDescription": { + "message": "The chain ID is used for signing transactions. Enter a decimal or hexadecimal number starting with '0x'." + }, "networkSettingsDescription": { "message": "Add and edit custom RPC networks" }, @@ -1062,9 +1088,6 @@ "optionalBlockExplorerUrl": { "message": "Block Explorer URL (optional)" }, - "optionalChainId": { - "message": "ChainID (optional)" - }, "optionalSymbol": { "message": "Symbol (optional)" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 5f3fde4d4..2fa65546c 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -463,9 +463,6 @@ "invalidBlockExplorerURL": { "message": "Invalida URL del Block Explorer" }, - "invalidInput": { - "message": "Entrada inválida" - }, "invalidRPC": { "message": "Invalida URL del RPC" }, @@ -586,9 +583,6 @@ "ofTextNofM": { "message": "de" }, - "optionalChainId": { - "message": "ChainID (opcional)" - }, "optionalSymbol": { "message": "Símbolo (opcional)" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 481a6b271..26385e8bb 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -557,9 +557,6 @@ "invalidBlockExplorerURL": { "message": "Block Explorer de URL no válido" }, - "invalidInput": { - "message": "Ingreso no válido." - }, "invalidRPC": { "message": "RPC de URL no válido" }, @@ -731,9 +728,6 @@ "optionalBlockExplorerUrl": { "message": "Bloquear la URL de Explorer (opcional)" }, - "optionalChainId": { - "message": "ID de cadena (opcional)" - }, "optionalSymbol": { "message": "Símbolo (opcional)" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 5f447dd45..d1ca0bb40 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -562,9 +562,6 @@ "invalidBlockExplorerURL": { "message": "Vale Block Explorer URL" }, - "invalidInput": { - "message": "Vigane sisend." - }, "invalidRPC": { "message": "Vale RPC URL" }, @@ -736,9 +733,6 @@ "optionalBlockExplorerUrl": { "message": "Blokeeri Exploreri URL (valikuline)" }, - "optionalChainId": { - "message": "ChainID (valikuline)" - }, "optionalSymbol": { "message": "Sümbol (valikuline)" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index a148868bb..531c20a69 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "Block Explorer URL نا معتبر" }, - "invalidInput": { - "message": "ورودی نامعتبر." - }, "invalidRPC": { "message": "RPC URL نا معتبر" }, @@ -746,9 +743,6 @@ "optionalBlockExplorerUrl": { "message": "بلاک کردن مرورگر URL (انتخابی)" }, - "optionalChainId": { - "message": "آی دی زنجیره (انتخابی)" - }, "optionalSymbol": { "message": "سمبول (انتخابی)" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 135cfa199..88856e1ce 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "Virheellinen Block Explorer URL-osoite" }, - "invalidInput": { - "message": "Virheellinen syötetty arvo." - }, "invalidRPC": { "message": "Virheellinen RPC:n URL-osoite" }, @@ -743,9 +740,6 @@ "optionalBlockExplorerUrl": { "message": "Estä Explorerin URL-osoite (valinnainen)" }, - "optionalChainId": { - "message": "Ketjun tunnus (valinnainen)" - }, "optionalSymbol": { "message": "Symboli (valinnainen)" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 677edf819..69231c629 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -523,9 +523,6 @@ "invalidBlockExplorerURL": { "message": "Hindi valid ang Block Explorer URL" }, - "invalidInput": { - "message": "Hindi valid ang input." - }, "invalidRPC": { "message": "Hindi valid ang RPC URL" }, @@ -677,9 +674,6 @@ "optionalBlockExplorerUrl": { "message": "Block Explorer URL (opsyonal)" }, - "optionalChainId": { - "message": "ChainID (opsyonal)" - }, "optionalSymbol": { "message": "Simbolo (opsyonal)" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 83d26b06e..af0b261c7 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -551,9 +551,6 @@ "invalidBlockExplorerURL": { "message": "URL Block Explorer invalide" }, - "invalidInput": { - "message": "Saisie non valide." - }, "invalidIpfsGateway": { "message": "IPFS Gateway Invalide: la valeur doit être une URL valide" }, @@ -728,9 +725,6 @@ "optionalBlockExplorerUrl": { "message": "Bloquer l'URL de l'explorateur (facultatif)" }, - "optionalChainId": { - "message": "ChainID (facultatif)" - }, "optionalSymbol": { "message": "Symbole (facultatif)" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 9b7f58355..2053b8e02 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "כתובת URL לא חוקית של Block Explorer" }, - "invalidInput": { - "message": "קלט לא תקין." - }, "invalidRPC": { "message": "כתובת URL לא חוקית של RPC" }, @@ -743,9 +740,6 @@ "optionalBlockExplorerUrl": { "message": "חסום כתובת URL של אקספלורר (אופציונלי)" }, - "optionalChainId": { - "message": "ChainID (אופציונלי)" - }, "optionalSymbol": { "message": "סמל (אופציונלי)" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 2d4d940f0..d06bd221a 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "अमान्य Block Explorer URL" }, - "invalidInput": { - "message": "अमान्य इनपुट।" - }, "invalidRPC": { "message": "अमान्य RPC URL" }, @@ -743,9 +740,6 @@ "optionalBlockExplorerUrl": { "message": "एक्सप्लोरर यूआरएल ब्लॉक (वैकल्पिक)" }, - "optionalChainId": { - "message": "चैनआईडी (वैकल्पिक)" - }, "optionalSymbol": { "message": "सिम्बल (वैकल्पिक)" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index ec70b07f4..b0e199100 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -202,9 +202,6 @@ "invalidBlockExplorerURL": { "message": "अमान्य Block Explorer कै URI" }, - "invalidInput": { - "message": "अमान्य इनपुट।" - }, "invalidRPC": { "message": "अमान्य RPC कै URI" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 38ee517e8..0a1e3ad11 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -562,9 +562,6 @@ "invalidBlockExplorerURL": { "message": "Nevaljani URL Block Explorer-a" }, - "invalidInput": { - "message": "Nevaljani upis." - }, "invalidRPC": { "message": "Nevaljani URL RPC-a" }, @@ -739,9 +736,6 @@ "optionalBlockExplorerUrl": { "message": "Blokiraj Explorerov URL (neobavezno)" }, - "optionalChainId": { - "message": "Identifikacijska oznaka bloka (neobavezno)" - }, "optionalSymbol": { "message": "Simbol (neobavezno)" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 23f791526..eefec6bc7 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -325,9 +325,6 @@ "invalidBlockExplorerURL": { "message": "Block Explorer URI pa valab" }, - "invalidInput": { - "message": "Sa ou rantre a pa valab" - }, "invalidRPC": { "message": "RPC URI pa valab" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index b2bbe957d..3fb33ca1a 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -562,9 +562,6 @@ "invalidBlockExplorerURL": { "message": "Helytelen Block Explorer URL" }, - "invalidInput": { - "message": "Érvénytelen bevitel." - }, "invalidRPC": { "message": "Helytelen RPC URL" }, @@ -739,9 +736,6 @@ "optionalBlockExplorerUrl": { "message": "Explorer URL letiltása (nem kötelező)" }, - "optionalChainId": { - "message": "ChainID (nem kötelező)" - }, "optionalSymbol": { "message": "Szimbólum (opcionális)" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index f32ebe6a2..01093748b 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -553,9 +553,6 @@ "invalidBlockExplorerURL": { "message": "URL Block Explorer Tidak Sah" }, - "invalidInput": { - "message": "Input tidak sah." - }, "invalidRPC": { "message": "URL RPC Tidak Sah" }, @@ -727,9 +724,6 @@ "optionalBlockExplorerUrl": { "message": "Blokir URL Penjelajah (opsional)" }, - "optionalChainId": { - "message": "ID Rantai (opsional)" - }, "optionalSymbol": { "message": "Simbol (opsional)" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 605f6bb3b..e680d8dc6 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -823,9 +823,6 @@ "invalidBlockExplorerURL": { "message": "URI Block Explorer invalido" }, - "invalidInput": { - "message": "Input non valido." - }, "invalidIpfsGateway": { "message": "Portale IPFS non valido: il valore deve essere un URL valido" }, @@ -1062,9 +1059,6 @@ "optionalBlockExplorerUrl": { "message": "URL del Block Explorer (opzionale)" }, - "optionalChainId": { - "message": "ChainID (opzionale)" - }, "optionalSymbol": { "message": "Simbolo (opzionale)" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 21dcae726..23e4bd9a6 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -265,9 +265,6 @@ "invalidAddress": { "message": "アドレスが無効です。" }, - "invalidInput": { - "message": "インプットが無効です。" - }, "jsonFile": { "message": "JSONファイル", "description": "format for importing an account" diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 8893ec9d2..49528ad64 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "ಅಮಾನ್ಯವಾದ Block Explorer URL" }, - "invalidInput": { - "message": "ಅಮಾನ್ಯವಾದ ಇನ್‌ಪುಟ್." - }, "invalidRPC": { "message": "ಅಮಾನ್ಯವಾದ RPC URL" }, @@ -746,9 +743,6 @@ "optionalBlockExplorerUrl": { "message": "ಅನ್ವೇಷಕ URL ಅನ್ನು ನಿರ್ಬಂಧಿಸಿ (ಐಚ್ಛಿಕ)" }, - "optionalChainId": { - "message": "ಚೈನ್‌ಐಡಿ (ಐಚ್ಛಿಕ)" - }, "optionalSymbol": { "message": "ಚಿಹ್ನೆ (ಐಚ್ಛಿಕ)" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index e03783fd9..d5951a9bc 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -560,9 +560,6 @@ "invalidBlockExplorerURL": { "message": "올바르지 않은 Block Explorer URI" }, - "invalidInput": { - "message": "올바르지 않은 입력값" - }, "invalidRPC": { "message": "올바르지 않은 RPC URI" }, @@ -740,9 +737,6 @@ "optionalBlockExplorerUrl": { "message": "익스플로러 URL 차단 (선택 사항)" }, - "optionalChainId": { - "message": "ChainID (선택)" - }, "optionalSymbol": { "message": "Symbol (선택)" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index f1698c20a..74a5e63b5 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "Netinkamas Block Explorer URL" }, - "invalidInput": { - "message": "Netinkama įvestis." - }, "invalidRPC": { "message": "Netinkamas RPC URL" }, @@ -746,9 +743,6 @@ "optionalBlockExplorerUrl": { "message": "Blokuoti naršyklės URL (pasirinktinai)" }, - "optionalChainId": { - "message": "Grandinės ID (nebūtinas)" - }, "optionalSymbol": { "message": "Simbolis (nebūtinas)" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index b791d5d08..00203e505 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -562,9 +562,6 @@ "invalidBlockExplorerURL": { "message": "Nederīgs Block Explorer URL" }, - "invalidInput": { - "message": "Nederīga ievadītā vērtība." - }, "invalidRPC": { "message": "Nederīgs RPC URL" }, @@ -742,9 +739,6 @@ "optionalBlockExplorerUrl": { "message": "Bloķēt Explorer URL (pēc izvēles)" }, - "optionalChainId": { - "message": "ChainID (neobligāti)" - }, "optionalSymbol": { "message": "Simbols (neobligāti)" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 5fb71082e..90cae7f84 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -549,9 +549,6 @@ "invalidBlockExplorerURL": { "message": "URL Block Explorer tidak sah" }, - "invalidInput": { - "message": "Input tidak sah." - }, "invalidRPC": { "message": "URL RPC tidak sah" }, @@ -720,9 +717,6 @@ "optionalBlockExplorerUrl": { "message": "Sekat URL Explorer (pilihan)" }, - "optionalChainId": { - "message": "ChainID (pilihan)" - }, "optionalSymbol": { "message": "Simbol (pilihan)" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index e03c548ac..fa9e168ff 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -196,9 +196,6 @@ "invalidBlockExplorerURL": { "message": "Ongeldige Block Explorer URI" }, - "invalidInput": { - "message": "Ongeldige invoer." - }, "invalidRPC": { "message": "Ongeldige RPC-URI" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 1ccd227db..525400738 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -553,9 +553,6 @@ "invalidBlockExplorerURL": { "message": "Ugyldig Block Explorer URL" }, - "invalidInput": { - "message": "Ugyldig inndata " - }, "invalidRPC": { "message": "Ugyldig RPC URL" }, @@ -733,9 +730,6 @@ "optionalBlockExplorerUrl": { "message": "Blokker Explorer URL (valgfritt)" }, - "optionalChainId": { - "message": "Blokkjede (vagfritt)" - }, "optionalSymbol": { "message": "Symbol (valgfritt)" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index ed5d3ba9a..b2635da57 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -154,9 +154,6 @@ "invalidAddress": { "message": "Invalid ang address" }, - "invalidInput": { - "message": "Invalid ang input." - }, "loading": { "message": "Naglo-load..." }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 90be03fcf..a818347a4 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "Nieprawidłowe Block Explorer URI" }, - "invalidInput": { - "message": "Nieprawidłowe dane." - }, "invalidRPC": { "message": "Nieprawidłowe RPC URI" }, @@ -740,9 +737,6 @@ "optionalBlockExplorerUrl": { "message": "Adres URL przeglądarki łańcucha bloków (opcjonalnie)" }, - "optionalChainId": { - "message": "Identyfikator łańcucha (opcjonalnie)" - }, "optionalSymbol": { "message": "Symbol (opcjonalnie)" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 3562b819c..067fd8d34 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -202,9 +202,6 @@ "invalidBlockExplorerURL": { "message": "Block Explorer URI Inválido" }, - "invalidInput": { - "message": "Campo inválido." - }, "invalidRPC": { "message": "RPC URI Inválido" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index da5881931..e1ca071f0 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -560,9 +560,6 @@ "invalidBlockExplorerURL": { "message": "URL de Block Explorer inválida" }, - "invalidInput": { - "message": "Entrada inválida." - }, "invalidRPC": { "message": "URL de RPC inválida" }, @@ -734,9 +731,6 @@ "optionalBlockExplorerUrl": { "message": "URL exploradora de blocos (opcional)" }, - "optionalChainId": { - "message": "ChainID (opcional)" - }, "optionalSymbol": { "message": "Símbolo (opcional)" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 828ff2515..a6bb34162 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -556,9 +556,6 @@ "invalidBlockExplorerURL": { "message": "URL Block Explorer nevalid" }, - "invalidInput": { - "message": "Intrare nevalidă." - }, "invalidRPC": { "message": "URL RPC nevalid" }, @@ -733,9 +730,6 @@ "optionalBlockExplorerUrl": { "message": "URL explorator bloc (opțional)" }, - "optionalChainId": { - "message": "ChainID (opțional)" - }, "optionalSymbol": { "message": "Simbol (opțional)" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 0cf300bb6..8c4ef56f7 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -595,9 +595,6 @@ "invalidBlockExplorerURL": { "message": "Неверный Block Explorer URI" }, - "invalidInput": { - "message": "Неверный ввод." - }, "invalidRPC": { "message": "Неверный RPC URI" }, @@ -775,9 +772,6 @@ "optionalBlockExplorerUrl": { "message": "URL блок-эксплорера (необязательно)" }, - "optionalChainId": { - "message": "ID сети (необязательно)" - }, "optionalSymbol": { "message": "Символ (необязательно)" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 73ec7768c..d84d5f585 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -553,9 +553,6 @@ "invalidBlockExplorerURL": { "message": "Neplatné Block Explorer URI" }, - "invalidInput": { - "message": "Neplatný vstup." - }, "invalidRPC": { "message": "Neplatné RPC URI" }, @@ -715,9 +712,6 @@ "optionalBlockExplorerUrl": { "message": "Blokovať URL Explorera (voliteľné)" }, - "optionalChainId": { - "message": "ChainID (voliteľné)" - }, "optionalSymbol": { "message": "Symbol (voliteľné)" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index e65fd609c..eecd18a99 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -557,9 +557,6 @@ "invalidBlockExplorerURL": { "message": "Neveljaven Block Explorer URL" }, - "invalidInput": { - "message": "Neveljaven vnos." - }, "invalidRPC": { "message": "Neveljaven RPC URL" }, @@ -731,9 +728,6 @@ "optionalBlockExplorerUrl": { "message": "Blokiraj URL Explorerja (poljubno)" }, - "optionalChainId": { - "message": "ChainID (nezahtevano)" - }, "optionalSymbol": { "message": "Simbol (nezahtevano)" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index cb232decd..61fa3c4c0 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -563,9 +563,6 @@ "invalidBlockExplorerURL": { "message": "Nevažeći Block Explorer URL" }, - "invalidInput": { - "message": "Nevažeći unos." - }, "invalidRPC": { "message": "Nevažeći RPC URL" }, @@ -737,9 +734,6 @@ "optionalBlockExplorerUrl": { "message": "Blokirajte URL Explorer-a (opciono)" }, - "optionalChainId": { - "message": "ChainID (opciono)" - }, "optionalSymbol": { "message": "Simbol (opciono)" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 02a851aa0..74d4c45b8 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -556,9 +556,6 @@ "invalidBlockExplorerURL": { "message": "Ogiltig Block Explorer URL" }, - "invalidInput": { - "message": "Ogiltigt input." - }, "invalidRPC": { "message": "Ogiltig RPC-URL" }, @@ -730,9 +727,6 @@ "optionalBlockExplorerUrl": { "message": "Block Explorer URL (valfritt)" }, - "optionalChainId": { - "message": "ChainID (frivilligt)" - }, "optionalSymbol": { "message": "Symbol (frivillig)" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 471d9cabf..bdecd20e2 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -553,9 +553,6 @@ "invalidBlockExplorerURL": { "message": "Block Explorer URL batili" }, - "invalidInput": { - "message": "Maandii si sahihi." - }, "invalidRPC": { "message": "RPC URL batili" }, @@ -724,9 +721,6 @@ "optionalBlockExplorerUrl": { "message": "URL ya Block Explorer URL (hiari)" }, - "optionalChainId": { - "message": "Utambulisho wa Mnyororo (hiari)" - }, "optionalSymbol": { "message": "Ishara (hiari)" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 6b15b6f9a..507893d9a 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -256,9 +256,6 @@ "invalidBlockExplorerURL": { "message": "தவறான Block Explorer URI" }, - "invalidInput": { - "message": "தவறான உள்ளீடு.." - }, "invalidRPC": { "message": "தவறான RPC URI" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index cf8246c2e..28a662868 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -280,9 +280,6 @@ "invalidBlockExplorerURL": { "message": "Block Explorer URI ไม่ถูกต้อง" }, - "invalidInput": { - "message": "อินพุทไม่ถูกต้อง" - }, "invalidRPC": { "message": "RPC URI ไม่ถูกต้อง" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 557eb217d..7448ad53f 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -226,9 +226,6 @@ "invalidBlockExplorerURL": { "message": "Geçersiz Block Explorer URI" }, - "invalidInput": { - "message": "Geçersiz giriş." - }, "invalidRPC": { "message": "Geçersiz RPC URI" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 18fe4fa41..a522ba4aa 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "Недійсний Block Explorer URL" }, - "invalidInput": { - "message": "Неприпустимий ввід." - }, "invalidRPC": { "message": "Недійсний RPC URL" }, @@ -746,9 +743,6 @@ "optionalBlockExplorerUrl": { "message": "Блокувати Explorer URL (не обов'язково)" }, - "optionalChainId": { - "message": "ChainID (необов’язково)" - }, "optionalSymbol": { "message": "Символ (не обов'язково)" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index eaaa468b9..1e8f4b5d1 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -166,9 +166,6 @@ "invalidAddress": { "message": "Địa chỉ không hợp lệ" }, - "invalidInput": { - "message": "Thông tin nhập vào không hợp lệ" - }, "jsonFile": { "message": "Tập tin JSON", "description": "format for importing an account" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 08ecc8cbf..5b0bab744 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -557,9 +557,6 @@ "invalidBlockExplorerURL": { "message": "无效 Block Explorer URI" }, - "invalidInput": { - "message": "无效输入." - }, "invalidRPC": { "message": "无效 RPC URI" }, @@ -728,9 +725,6 @@ "optionalBlockExplorerUrl": { "message": "屏蔽管理器 URL(选填)" }, - "optionalChainId": { - "message": "ChainID(选填)" - }, "optionalSymbol": { "message": "符号(选填)" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 2d30e0c31..383fbb991 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -566,9 +566,6 @@ "invalidBlockExplorerURL": { "message": "無效的區塊鏈瀏覽器 URL" }, - "invalidInput": { - "message": "輸入錯誤。" - }, "invalidRPC": { "message": "無效的 RPC URI" }, @@ -737,9 +734,6 @@ "optionalBlockExplorerUrl": { "message": "區塊鏈瀏覽器 URL(非必要)" }, - "optionalChainId": { - "message": "ChainID (可選)" - }, "optionalSymbol": { "message": "Symbol (可選)" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index 2834d0c6a..3f3a076cd 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -97,7 +97,6 @@ initialize().catch(log.error) * @property {boolean} isInitialized - Whether the first vault has been created. * @property {boolean} isUnlocked - Whether the vault is currently decrypted and accounts are available for selection. * @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed. - * @property {string} rpcTarget - DEPRECATED - The URL of the current RPC provider. * @property {Object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys. * @property {Object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions. * @property {Array} frequentRpcList - A list of frequently used RPCs, including custom user-provided ones. @@ -110,7 +109,7 @@ initialize().catch(log.error) * @property {boolean} welcomeScreen - True if welcome screen should be shown. * @property {string} currentLocale - A locale string matching the user's preferred display language. * @property {Object} provider - The current selected network provider. - * @property {string} provider.rpcTarget - The address for the RPC API, if using an RPC API. + * @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 {Object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values. diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js index 820e1a80b..dd8dc66fe 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -2,8 +2,11 @@ import ObservableStore from 'obs-store' /** * @typedef {Object} AlertControllerInitState - * @property {Object} alertEnabledness - A map of any alerts that were suppressed keyed by alert ID, where the value - * is the timestamp of when the user suppressed the alert. + * @property {Object} alertEnabledness - A map of alerts IDs to booleans, where + * `true` indicates that the alert is enabled and shown, and `false` the opposite. + * @property {Object} unconnectedAccountAlertShownOrigins - A map of origin + * strings to booleans indicating whether the "switch to connected" alert has + * been shown (`true`) or otherwise (`false`). */ /** @@ -13,6 +16,8 @@ import ObservableStore from 'obs-store' export const ALERT_TYPES = { unconnectedAccount: 'unconnectedAccount', + // enumerated here but has no background state + invalidCustomNetwork: 'invalidCustomNetwork', } const defaultState = { @@ -44,6 +49,7 @@ export default class AlertController { ...initState, unconnectedAccountAlertShownOrigins: {}, } + this.store = new ObservableStore(state) this.selectedAddress = preferencesStore.getState().selectedAddress diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 375907041..c161b0a8c 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -7,12 +7,13 @@ import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware' import BlockTracker from 'eth-block-tracker' -export default function createJsonRpcClient ({ rpcUrl }) { +export default function createJsonRpcClient ({ rpcUrl, chainId }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }) const blockProvider = providerFromMiddleware(fetchMiddleware) const blockTracker = new BlockTracker({ provider: blockProvider }) const networkMiddleware = mergeMiddleware([ + createChainIdMiddleware(chainId), createBlockRefRewriteMiddleware({ blockTracker }), createBlockCacheMiddleware({ blockTracker }), createInflightMiddleware(), @@ -21,3 +22,13 @@ export default function createJsonRpcClient ({ rpcUrl }) { ]) return { networkMiddleware, blockTracker } } + +function createChainIdMiddleware (chainId) { + return (req, res, next, end) => { + if (req.method === 'eth_chainId') { + res.result = chainId + return end() + } + return next() + } +} diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 1b4caeb6d..7e50095f3 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -2,11 +2,11 @@ import assert from 'assert' import EventEmitter from 'events' import ObservableStore from 'obs-store' import ComposedStore from 'obs-store/lib/composed' -import EthQuery from 'eth-query' import JsonRpcEngine from 'json-rpc-engine' import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine' import log from 'loglevel' import { createSwappableProxy, createEventEmitterProxy } from 'swappable-obj-proxy' +import EthQuery from 'eth-query' import createMetamaskMiddleware from './createMetamaskMiddleware' import createInfuraClient from './createInfuraClient' import createJsonRpcClient from './createJsonRpcClient' @@ -17,16 +17,18 @@ import { MAINNET, LOCALHOST, INFURA_PROVIDER_TYPES, + NETWORK_TYPE_TO_ID_MAP, } from './enums' -const networks = { networkList: {} } - const env = process.env.METAMASK_ENV const { METAMASK_DEBUG } = process.env let defaultProviderConfigType +let defaultProviderChainId if (process.env.IN_TEST === 'true') { defaultProviderConfigType = LOCALHOST + // Decimal 5777, an arbitrary chain ID we use for testing + defaultProviderChainId = '0x1691' } else if (METAMASK_DEBUG || env === 'test') { defaultProviderConfigType = RINKEBY } else { @@ -35,31 +37,36 @@ if (process.env.IN_TEST === 'true') { const defaultProviderConfig = { type: defaultProviderConfigType, -} - -const defaultNetworkConfig = { ticker: 'ETH', } +if (defaultProviderChainId) { + defaultProviderConfig.chainId = defaultProviderChainId +} export default class NetworkController extends EventEmitter { constructor (opts = {}) { super() - // parse options - const providerConfig = opts.provider || defaultProviderConfig // create stores - this.providerStore = new ObservableStore(providerConfig) + this.providerStore = new ObservableStore( + opts.provider || { ...defaultProviderConfig }, + ) this.networkStore = new ObservableStore('loading') - this.networkConfig = new ObservableStore(defaultNetworkConfig) - this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig }) - this.on('networkDidChange', this.lookupNetwork) + this.store = new ComposedStore({ + provider: this.providerStore, + network: this.networkStore, + }) + // provider and block tracker this._provider = null this._blockTracker = null + // provider and block tracker proxies - because the network changes this._providerProxy = null this._blockTrackerProxy = null + + this.on('networkDidChange', this.lookupNetwork) } /** @@ -79,8 +86,8 @@ export default class NetworkController extends EventEmitter { initializeProvider (providerParams) { this._baseProviderParams = providerParams - const { type, rpcTarget, chainId, ticker, nickname } = this.providerStore.getState() - this._configureProvider({ type, rpcTarget, chainId, ticker, nickname }) + const { type, rpcUrl, chainId } = this.getProviderConfig() + this._configureProvider({ type, rpcUrl, chainId }) this.lookupNetwork() } @@ -102,21 +109,8 @@ export default class NetworkController extends EventEmitter { return this.networkStore.getState() } - getNetworkConfig () { - return this.networkConfig.getState() - } - - setNetworkState (network, type) { - if (network === 'loading') { - this.networkStore.putState(network) - return - } - - // type must be defined - if (!type) { - return - } - this.networkStore.putState(networks.networkList[type]?.chainId || network) + setNetworkState (network) { + this.networkStore.putState(network) } isNetworkLoading () { @@ -129,48 +123,61 @@ export default class NetworkController extends EventEmitter { log.warn('NetworkController - lookupNetwork aborted due to missing provider') return } - const { type } = this.providerStore.getState() + + const { type, chainId: configChainId } = this.getProviderConfig() + const chainId = NETWORK_TYPE_TO_ID_MAP[type]?.chainId || configChainId + + if (!chainId) { + log.warn('NetworkController - lookupNetwork aborted due to missing chainId') + this.setNetworkState('loading') + return + } + + // Ping the RPC endpoint so we can confirm that it works const ethQuery = new EthQuery(this._provider) const initialNetwork = this.getNetworkState() - ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + ethQuery.sendAsync({ method: 'net_version' }, (err, _networkVersion) => { const currentNetwork = this.getNetworkState() if (initialNetwork === currentNetwork) { if (err) { this.setNetworkState('loading') return } - log.info(`web3.getNetwork returned ${network}`) - this.setNetworkState(network, type) + + // Now we set the network state to the chainId computed earlier + this.setNetworkState(chainId) } }) } - setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs) { - const providerConfig = { + setRpcTarget (rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs) { + this.setProviderConfig({ type: 'rpc', - rpcTarget, + rpcUrl, chainId, ticker, nickname, rpcPrefs, - } - this.providerConfig = providerConfig + }) } - async setProviderType (type, rpcTarget = '', ticker = 'ETH', nickname = '') { + async setProviderType (type, rpcUrl = '', ticker = 'ETH', nickname = '') { assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`) assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) - const providerConfig = { type, rpcTarget, ticker, nickname } - this.providerConfig = providerConfig + const { chainId } = NETWORK_TYPE_TO_ID_MAP[type] + this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname }) } resetConnection () { - this.providerConfig = this.getProviderConfig() + this.setProviderConfig(this.getProviderConfig()) } - set providerConfig (providerConfig) { - this.providerStore.updateState(providerConfig) - this._switchNetwork(providerConfig) + /** + * Sets the provider config and switches the network. + */ + setProviderConfig (config) { + this.providerStore.updateState(config) + this._switchNetwork(config) } getProviderConfig () { @@ -187,8 +194,7 @@ export default class NetworkController extends EventEmitter { this.emit('networkDidChange', opts.type) } - _configureProvider (opts) { - const { type, rpcTarget, chainId, ticker, nickname } = opts + _configureProvider ({ type, rpcUrl, chainId }) { // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type) if (isInfura) { @@ -198,7 +204,7 @@ export default class NetworkController extends EventEmitter { this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc') { - this._configureStandardProvider({ rpcUrl: rpcTarget, chainId, ticker, nickname }) + this._configureStandardProvider(rpcUrl, chainId) } else { throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`) } @@ -211,11 +217,6 @@ export default class NetworkController extends EventEmitter { projectId, }) this._setNetworkClient(networkClient) - // setup networkConfig - const settings = { - ticker: 'ETH', - } - this.networkConfig.putState(settings) } _configureLocalhostProvider () { @@ -224,22 +225,9 @@ export default class NetworkController extends EventEmitter { this._setNetworkClient(networkClient) } - _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { + _configureStandardProvider (rpcUrl, chainId) { log.info('NetworkController - configureStandardProvider', rpcUrl) - const networkClient = createJsonRpcClient({ rpcUrl }) - // hack to add a 'rpc' network with chainId - networks.networkList.rpc = { - chainId, - rpcUrl, - ticker: ticker || 'ETH', - nickname, - } - // setup networkConfig - let settings = { - network: chainId, - } - settings = Object.assign(settings, networks.networkList.rpc) - this.networkConfig.putState(settings) + const networkClient = createJsonRpcClient({ rpcUrl, chainId }) this._setNetworkClient(networkClient) } diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index c0d2e8d07..2d2cab5eb 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,6 +1,7 @@ import ObservableStore from 'obs-store' import { normalize as normalizeAddress } from 'eth-sig-util' import { isValidAddress, sha3, bufferToHex } from 'ethereumjs-util' +import { isPrefixedFormattedHexString } from '../lib/util' import { addInternalMethodPrefix } from './permissions' export default class PreferencesController { @@ -478,10 +479,8 @@ export default class PreferencesController { * @param {string} chainId - Optional chainId of the selected network. * @param {string} ticker - Optional ticker symbol of the selected network. * @param {string} nickname - Optional nickname of the selected network. - * @returns {Promise} - Promise resolving to updated frequentRpcList. * */ - updateRpc (newRpcDetails) { const rpcList = this.getFrequentRpcListDetail() const index = rpcList.findIndex((element) => { @@ -494,39 +493,38 @@ export default class PreferencesController { this.store.updateState({ frequentRpcListDetail: rpcList }) } else { const { rpcUrl, chainId, ticker, nickname, rpcPrefs = {} } = newRpcDetails - return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs) + this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs) } - return Promise.resolve(rpcList) } /** * Adds custom RPC url to state. * - * @param {string} url - The RPC url to add to frequentRpcList. - * @param {string} chainId - Optional chainId of the selected network. - * @param {string} ticker - Optional ticker symbol of the selected network. - * @param {string} nickname - Optional nickname of the selected network. - * @returns {Promise} - Promise resolving to updated frequentRpcList. + * @param {string} rpcUrl - The RPC url to add to frequentRpcList. + * @param {string} chainId - The chainId of the selected network. + * @param {string} [ticker] - Ticker symbol of the selected network. + * @param {string} [nickname] - Nickname of the selected network. * */ - addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { + addToFrequentRpcList (rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { + if (rpcUrl === 'http://localhost:8545') { + return + } + const rpcList = this.getFrequentRpcListDetail() const index = rpcList.findIndex((element) => { - return element.rpcUrl === url + return element.rpcUrl === rpcUrl }) if (index !== -1) { rpcList.splice(index, 1) } - if (url !== 'http://localhost:8545') { - let checkedChainId - // eslint-disable-next-line radix - if (Boolean(chainId) && !Number.isNaN(parseInt(chainId))) { - checkedChainId = chainId - } - rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs }) + + if (!isPrefixedFormattedHexString(chainId)) { + throw new Error(`Invalid chainId: "${chainId}"`) } + + rpcList.push({ rpcUrl, chainId, ticker, nickname, rpcPrefs }) this.store.updateState({ frequentRpcListDetail: rpcList }) - return Promise.resolve(rpcList) } /** diff --git a/app/scripts/lib/select-chain-id.js b/app/scripts/lib/select-chain-id.js deleted file mode 100644 index ed501b837..000000000 --- a/app/scripts/lib/select-chain-id.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - MAINNET_CHAIN_ID, - ROPSTEN_CHAIN_ID, - RINKEBY_CHAIN_ID, - KOVAN_CHAIN_ID, - GOERLI_CHAIN_ID, -} from '../controllers/network/enums' - -const standardNetworkId = { - '1': MAINNET_CHAIN_ID, - '3': ROPSTEN_CHAIN_ID, - '4': RINKEBY_CHAIN_ID, - '42': KOVAN_CHAIN_ID, - '5': GOERLI_CHAIN_ID, -} - -export default function selectChainId (metamaskState) { - const { network, provider: { chainId } } = metamaskState - return standardNetworkId[network] || `0x${parseInt(chainId, 10).toString(16)}` -} diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index b5e4f2bec..2aba2e2be 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -44,11 +44,6 @@ export const SENTRY_STATE = { type: true, }, seedPhraseBackedUp: true, - settings: { - chainId: true, - ticker: true, - nickname: true, - }, showRestorePrompt: true, threeBoxDisabled: true, threeBoxLastUpdated: true, diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index 64ce6427c..c0378b87c 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -166,9 +166,12 @@ export default class TypedMessageManager extends EventEmitter { assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`) assert.equal(validation.errors.length, 0, 'Signing data must conform to EIP-712 schema. See https://git.io/fNtcx.') const { chainId } = data.domain - // eslint-disable-next-line radix - const activeChainId = parseInt(this.networkController.getNetworkState()) - chainId && assert.equal(chainId, activeChainId, `Provided chainId "${chainId}" must match the active chainId "${activeChainId}"`) + if (chainId) { + // eslint-disable-next-line radix + const activeChainId = parseInt(this.networkController.getNetworkState()) + assert.ok(!Number.isNaN(activeChainId), `Cannot sign messages for chainId "${chainId}", because MetaMask is switching networks.`) + assert.equal(chainId, activeChainId, `Provided chainId "${chainId}" must match the active chainId "${activeChainId}"`) + } break } default: diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 08998d6cc..20ab998de 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -149,6 +149,21 @@ function checkForError () { return new Error(lastError.message) } +/** + * Checks whether the given value is a 0x-prefixed, non-zero, non-zero-padded, + * hexadecimal string. + * + * @param {any} value - The value to check. + * @returns {boolean} True if the value is a correctly formatted hex string, + * false otherwise. + */ +function isPrefixedFormattedHexString (value) { + if (typeof value !== 'string') { + return false + } + return (/^0x[1-9a-f]+[0-9a-f]*$/ui).test(value) +} + export { getPlatform, getEnvironmentType, @@ -157,4 +172,5 @@ export { bnToHex, BnMultiplyByFraction, checkForError, + isPrefixedFormattedHexString, } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 07d3c8b18..84533551d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -54,7 +54,6 @@ import { PermissionsController } from './controllers/permissions' import getRestrictedMethods from './controllers/permissions/restrictedMethods' import nodeify from './lib/nodeify' import accountImporter from './account-import-strategies' -import selectChainId from './lib/select-chain-id' import seedPhraseVerifier from './lib/seed-phrase-verifier' import backgroundMetaMetricsEvent from './lib/background-metametrics' @@ -376,14 +375,16 @@ export default class MetamaskController extends EventEmitter { } function updatePublicConfigStore (memState) { - publicConfigStore.putState(selectPublicState(memState)) + if (memState.network !== 'loading') { + publicConfigStore.putState(selectPublicState(memState)) + } } - function selectPublicState ({ isUnlocked, network, provider }) { + function selectPublicState ({ isUnlocked, network }) { return { isUnlocked, - networkVersion: network, - chainId: selectChainId({ network, provider }), + chainId: network, + networkVersion: Number.parseInt(network, 16).toString(), } } return publicConfigStore @@ -1849,7 +1850,7 @@ export default class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function returning currency info. */ setCurrentCurrency (currencyCode, cb) { - const { ticker } = this.networkController.getNetworkConfig() + const { ticker } = this.networkController.getProviderConfig() try { const currencyState = { nativeCurrency: ticker, @@ -1866,7 +1867,6 @@ export default class MetamaskController extends EventEmitter { } } - // network /** * A method for selecting a custom URL for an ethereum RPC provider and updating it * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. @@ -1875,7 +1875,6 @@ export default class MetamaskController extends EventEmitter { * @param {string} nickname - Optional nickname of the selected network. * @returns {Promise} - The RPC Target URL confirmed. */ - async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname, rpcPrefs) { await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname, rpcPrefs }) this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname, rpcPrefs) @@ -1884,31 +1883,31 @@ export default class MetamaskController extends EventEmitter { /** * A method for selecting a custom URL for an ethereum RPC provider. - * @param {string} rpcTarget - A URL for a valid Ethereum RPC API. + * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. * @param {string} chainId - The chainId of the selected network. * @param {string} ticker - The ticker symbol of the selected network. * @param {string} nickname - Optional nickname of the selected network. * @returns {Promise} - The RPC Target URL confirmed. */ - async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { + async setCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail() - const rpcSettings = frequentRpcListDetail.find((rpc) => rpcTarget === rpc.rpcUrl) + const rpcSettings = frequentRpcListDetail.find((rpc) => rpcUrl === rpc.rpcUrl) if (rpcSettings) { this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname, rpcPrefs) } else { - this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname, rpcPrefs) - await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname, rpcPrefs) + this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname, rpcPrefs) + await this.preferencesController.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs) } - return rpcTarget + return rpcUrl } /** * A method for deleting a selected custom URL. - * @param {string} rpcTarget - A RPC URL to delete. + * @param {string} rpcUrl - A RPC URL to delete. */ - async delCustomRpc (rpcTarget) { - await this.preferencesController.removeFromFrequentRpcList(rpcTarget) + async delCustomRpc (rpcUrl) { + await this.preferencesController.removeFromFrequentRpcList(rpcUrl) } async initializeThreeBox () { diff --git a/app/scripts/migrations/048.js b/app/scripts/migrations/048.js new file mode 100644 index 000000000..8a07abf0b --- /dev/null +++ b/app/scripts/migrations/048.js @@ -0,0 +1,38 @@ +import { cloneDeep } from 'lodash' + +const version = 48 + +/** + * 1. Delete NetworkController.settings + * 2a. Delete NetworkController.provider if set to type 'rpc'. + * It will be re-set to Mainnet on background initialization. + * 2b. Re-key provider.rpcTarget to provider.rpcUrl + */ +export default { + version, + async migrate (originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state = {}) { + // 1. Delete NetworkController.settings + delete state.NetworkController?.settings + + // 2. Delete NetworkController.provider or rename rpcTarget key + if (state.NetworkController?.provider?.type === 'rpc') { + delete state.NetworkController.provider + } else if (state.NetworkController?.provider) { + if ('rpcTarget' in state.NetworkController.provider) { + const rpcUrl = state.NetworkController.provider.rpcTarget + state.NetworkController.provider.rpcUrl = rpcUrl + } + delete state.NetworkController?.provider?.rpcTarget + } + + return state +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index d90c5dc86..a3dc86e29 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -58,6 +58,7 @@ const migrations = [ require('./045').default, require('./046').default, require('./047').default, + require('./048').default, ] export default migrations diff --git a/test/e2e/fixtures/imported-account/state.json b/test/e2e/fixtures/imported-account/state.json index 3eb3b0070..6f3d8d1cb 100644 --- a/test/e2e/fixtures/imported-account/state.json +++ b/test/e2e/fixtures/imported-account/state.json @@ -30,12 +30,10 @@ "network": "5777", "provider": { "nickname": "", - "rpcTarget": "", + "rpcUrl": "", + "chainId": "0x1691", "ticker": "ETH", "type": "localhost" - }, - "settings": { - "ticker": "ETH" } }, "OnboardingController": { diff --git a/test/e2e/fixtures/localization/state.json b/test/e2e/fixtures/localization/state.json index 74325c8c4..52978e003 100644 --- a/test/e2e/fixtures/localization/state.json +++ b/test/e2e/fixtures/localization/state.json @@ -30,12 +30,10 @@ "network": "5777", "provider": { "nickname": "", - "rpcTarget": "", + "rpcUrl": "", + "chainId": "0x1691", "ticker": "ETH", "type": "localhost" - }, - "settings": { - "ticker": "ETH" } }, "OnboardingController": { diff --git a/test/e2e/fixtures/personal-sign/state.json b/test/e2e/fixtures/personal-sign/state.json index 7864db738..c6ad1f27c 100644 --- a/test/e2e/fixtures/personal-sign/state.json +++ b/test/e2e/fixtures/personal-sign/state.json @@ -33,14 +33,12 @@ "NetworkController": { "provider": { "nickname": "", - "rpcTarget": "", + "rpcUrl": "", + "chainId": "0x1691", "ticker": "ETH", "type": "localhost" }, - "network": "5777", - "settings": { - "ticker": "ETH" - } + "network": "5777" }, "OnboardingController": { "onboardingTabs": {}, diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 49d1292ea..37e1ac556 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1252,15 +1252,15 @@ describe('MetaMask', function () { }) describe('Stores custom RPC history', function () { - const customRpcUrls = [ - 'http://127.0.0.1:8545/1', - 'http://127.0.0.1:8545/2', - 'http://127.0.0.1:8545/3', - 'http://127.0.0.1:8545/4', + const customRpcInfo = [ + { rpcUrl: 'http://127.0.0.1:8545/1', chainId: '0x1' }, + { rpcUrl: 'http://127.0.0.1:8545/2', chainId: '0x2' }, + { rpcUrl: 'http://127.0.0.1:8545/3', chainId: '0x3' }, + { rpcUrl: 'http://127.0.0.1:8545/4', chainId: '0x4' }, ] - customRpcUrls.forEach((customRpcUrl) => { - it(`creates custom RPC: ${customRpcUrl}`, async function () { + customRpcInfo.forEach(({ rpcUrl, chainId }) => { + it(`creates custom RPC: '${rpcUrl}' with chainId '${chainId}'`, async function () { await driver.clickElement(By.css('.network-name')) await driver.delay(regularDelayMs) @@ -1270,9 +1270,14 @@ describe('MetaMask', function () { await driver.findElement(By.css('.settings-page__sub-header-text')) const customRpcInputs = await driver.findElements(By.css('input[type="text"]')) - const customRpcInput = customRpcInputs[1] - await customRpcInput.clear() - await customRpcInput.sendKeys(customRpcUrl) + const rpcUrlInput = customRpcInputs[1] + const chainIdInput = customRpcInputs[2] + + await rpcUrlInput.clear() + await rpcUrlInput.sendKeys(rpcUrl) + + await chainIdInput.clear() + await chainIdInput.sendKeys(chainId) await driver.clickElement(By.css('.network-form__footer .btn-secondary')) await driver.delay(largeDelayMs * 2) @@ -1294,7 +1299,7 @@ describe('MetaMask', function () { // only recent 3 are found and in correct order (most recent at the top) const customRpcs = await driver.findElements(By.xpath(`//span[contains(text(), 'http://127.0.0.1:8545/')]`)) - assert.equal(customRpcs.length, customRpcUrls.length) + assert.equal(customRpcs.length, customRpcInfo.length) }) it('deletes a custom RPC', async function () { diff --git a/test/unit/actions/config_test.js b/test/unit/actions/config_test.js index 318b7cd4d..ed60ac299 100644 --- a/test/unit/actions/config_test.js +++ b/test/unit/actions/config_test.js @@ -6,7 +6,7 @@ import * as actionConstants from '../../../ui/app/store/actionConstants' describe('config view actions', function () { const initialState = { metamask: { - rpcTarget: 'foo', + rpcUrl: 'foo', frequentRpcList: [], }, appState: { @@ -18,7 +18,7 @@ describe('config view actions', function () { freeze(initialState) describe('SET_RPC_TARGET', function () { - it('sets the state.metamask.rpcTarget property of the state to the action.value', function () { + it('sets the state.metamask.rpcUrl property of the state to the action.value', function () { const action = { type: actionConstants.SET_RPC_TARGET, value: 'foo', @@ -26,7 +26,7 @@ describe('config view actions', function () { const result = reducers(initialState, action) assert.equal(result.metamask.provider.type, 'rpc') - assert.equal(result.metamask.provider.rpcTarget, 'foo') + assert.equal(result.metamask.provider.rpcUrl, 'foo') }) }) }) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 35c695894..c9f67af77 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -524,19 +524,19 @@ describe('MetaMaskController', function () { }) describe('#setCustomRpc', function () { - let rpcTarget + let rpcUrl beforeEach(function () { - rpcTarget = metamaskController.setCustomRpc(CUSTOM_RPC_URL) + rpcUrl = metamaskController.setCustomRpc(CUSTOM_RPC_URL) }) it('returns custom RPC that when called', async function () { - assert.equal(await rpcTarget, CUSTOM_RPC_URL) + assert.equal(await rpcUrl, CUSTOM_RPC_URL) }) it('changes the network controller rpc', function () { const networkControllerState = metamaskController.networkController.store.getState() - assert.equal(networkControllerState.provider.rpcTarget, CUSTOM_RPC_URL) + assert.equal(networkControllerState.provider.rpcUrl, CUSTOM_RPC_URL) }) }) diff --git a/test/unit/app/controllers/network/network-controller-test.js b/test/unit/app/controllers/network/network-controller-test.js index eb480752e..7971939b0 100644 --- a/test/unit/app/controllers/network/network-controller-test.js +++ b/test/unit/app/controllers/network/network-controller-test.js @@ -1,4 +1,5 @@ -import assert from 'assert' +import { strict as assert } from 'assert' +import sinon from 'sinon' import NetworkController from '../../../../../app/scripts/controllers/network' import { getNetworkDisplayName } from '../../../../../app/scripts/controllers/network/util' @@ -34,9 +35,9 @@ describe('NetworkController', function () { describe('#setNetworkState', function () { it('should update the network', function () { - networkController.setNetworkState(1, 'rpc') + networkController.setNetworkState('1') const networkState = networkController.getNetworkState() - assert.equal(networkState, 1, 'network is 1') + assert.equal(networkState, '1', 'network is 1') }) }) @@ -47,11 +48,21 @@ describe('NetworkController', function () { const { type } = networkController.getProviderConfig() assert.equal(type, 'mainnet', 'provider type is updated') }) + it('should set the network to loading', function () { networkController.initializeProvider(networkControllerProviderConfig) + + const spy = sinon.spy(networkController, 'setNetworkState') networkController.setProviderType('mainnet') - const loading = networkController.isNetworkLoading() - assert.ok(loading, 'network is loading') + + assert.equal( + spy.callCount, 1, + 'should have called setNetworkState 2 times', + ) + assert.ok( + spy.calledOnceWithExactly('loading'), + 'should have called with "loading" first', + ) }) }) }) diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 3484dd210..7db592170 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -498,28 +498,37 @@ describe('preferences controller', function () { describe('#updateRpc', function () { it('should update the rpcDetails properly', function () { - preferencesController.store.updateState({ frequentRpcListDetail: [{}, { rpcUrl: 'test' }, {}] }) - preferencesController.updateRpc({ rpcUrl: 'test', chainId: '1' }) - preferencesController.updateRpc({ rpcUrl: 'test/1', chainId: '1' }) - preferencesController.updateRpc({ rpcUrl: 'test/2', chainId: '1' }) - preferencesController.updateRpc({ rpcUrl: 'test/3', chainId: '1' }) + preferencesController.store.updateState({ frequentRpcListDetail: [{}, { rpcUrl: 'test', chainId: '0x1' }, {}] }) + preferencesController.updateRpc({ rpcUrl: 'test', chainId: '0x1' }) + preferencesController.updateRpc({ rpcUrl: 'test/1', chainId: '0x1' }) + preferencesController.updateRpc({ rpcUrl: 'test/2', chainId: '0x1' }) + preferencesController.updateRpc({ rpcUrl: 'test/3', chainId: '0x1' }) const list = preferencesController.getFrequentRpcListDetail() - assert.deepEqual(list[1], { rpcUrl: 'test', chainId: '1' }) + assert.deepEqual(list[1], { rpcUrl: 'test', chainId: '0x1' }) }) }) - describe('on updateFrequentRpcList', function () { + describe('adding and removing from frequentRpcListDetail', function () { it('should add custom RPC url to state', function () { - preferencesController.addToFrequentRpcList('rpc_url', '1') - preferencesController.addToFrequentRpcList('http://localhost:8545', '1') - assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '1', ticker: 'ETH', nickname: '', rpcPrefs: {} }]) - preferencesController.addToFrequentRpcList('rpc_url', '1') - assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '1', ticker: 'ETH', nickname: '', rpcPrefs: {} }]) + preferencesController.addToFrequentRpcList('rpc_url', '0x1') + preferencesController.addToFrequentRpcList('http://localhost:8545', '0x1') + assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '0x1', ticker: 'ETH', nickname: '', rpcPrefs: {} }]) + preferencesController.addToFrequentRpcList('rpc_url', '0x1') + assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '0x1', ticker: 'ETH', nickname: '', rpcPrefs: {} }]) + }) + + it('should throw if chainId is invalid', function () { + assert.throws( + () => { + preferencesController.addToFrequentRpcList('rpc_url', '1') + }, + 'should throw on invalid chainId', + ) }) it('should remove custom RPC url from state', function () { - preferencesController.addToFrequentRpcList('rpc_url', '1') - assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '1', ticker: 'ETH', nickname: '', rpcPrefs: {} }]) + preferencesController.addToFrequentRpcList('rpc_url', '0x1') + assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '0x1', ticker: 'ETH', nickname: '', rpcPrefs: {} }]) preferencesController.removeFromFrequentRpcList('other_rpc_url') preferencesController.removeFromFrequentRpcList('http://localhost:8545') preferencesController.removeFromFrequentRpcList('rpc_url') diff --git a/test/unit/app/util-test.js b/test/unit/app/util-test.js index a15b8ce72..5d25d9b08 100644 --- a/test/unit/app/util-test.js +++ b/test/unit/app/util-test.js @@ -1,5 +1,9 @@ -import assert from 'assert' -import { getEnvironmentType, sufficientBalance } from '../../../app/scripts/lib/util' +import { strict as assert } from 'assert' +import { + getEnvironmentType, + sufficientBalance, + isPrefixedFormattedHexString, +} from '../../../app/scripts/lib/util' import { ENVIRONMENT_TYPE_POPUP, @@ -88,4 +92,75 @@ describe('app utils', function () { assert.ok(!result, 'insufficient balance found.') }) }) + + describe('isPrefixedFormattedHexString', function () { + it('should return true for valid hex strings', function () { + assert.equal( + isPrefixedFormattedHexString('0x1'), true, + 'should return true', + ) + + assert.equal( + isPrefixedFormattedHexString('0xa'), true, + 'should return true', + ) + + assert.equal( + isPrefixedFormattedHexString('0xabcd1123fae909aad87452'), true, + 'should return true', + ) + }) + + it('should return false for invalid hex strings', function () { + assert.equal( + isPrefixedFormattedHexString('0x'), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString('0x0'), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString('0x01'), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString(' 0x1'), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString('0x1 '), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString('0x1afz'), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString('z'), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString(2), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString(['0x1']), false, + 'should return false', + ) + + assert.equal( + isPrefixedFormattedHexString(), false, + 'should return false', + ) + }) + }) }) diff --git a/test/unit/localhostState.js b/test/unit/localhostState.js index e9fe95dd9..1ec5e1eb2 100644 --- a/test/unit/localhostState.js +++ b/test/unit/localhostState.js @@ -13,7 +13,7 @@ const initialState = { NetworkController: { provider: { type: 'rpc', - rpcTarget: 'http://localhost:8545', + rpcUrl: 'http://localhost:8545', }, }, } diff --git a/test/unit/migrations/048-test.js b/test/unit/migrations/048-test.js new file mode 100644 index 000000000..fc93ea65b --- /dev/null +++ b/test/unit/migrations/048-test.js @@ -0,0 +1,126 @@ +import { strict as assert } from 'assert' +import migration48 from '../../../app/scripts/migrations/048' + +describe('migration #48', function () { + it('should update the version metadata', async function () { + const oldStorage = { + 'meta': { + 'version': 47, + }, + 'data': {}, + } + + const newStorage = await migration48.migrate(oldStorage) + assert.deepEqual(newStorage.meta, { + 'version': 48, + }) + }) + + it('should delete NetworkController.settings', async function () { + const oldStorage = { + meta: {}, + data: { + NetworkController: { + settings: { + fizz: 'buzz', + }, + provider: { + type: 'notRpc', + }, + }, + foo: 'bar', + }, + } + + const newStorage = await migration48.migrate(oldStorage) + assert.deepEqual(newStorage.data, { + NetworkController: { + provider: { + type: 'notRpc', + }, + }, + foo: 'bar', + }) + }) + + it('should delete NetworkController.provider if the type is "rpc"', async function () { + const oldStorage = { + meta: {}, + data: { + NetworkController: { + provider: { + type: 'rpc', + fizz: 'buzz', + }, + foo: 'bar', + }, + foo: 'bar', + }, + } + + const newStorage = await migration48.migrate(oldStorage) + assert.deepEqual(newStorage.data, { + NetworkController: { + foo: 'bar', + }, + foo: 'bar', + }) + }) + + it('should re-key NetworkController.provider.rpcTarget to rpcUrl if the type is not "rpc"', async function () { + const oldStorage = { + meta: {}, + data: { + NetworkController: { + provider: { + type: 'someType', + rpcTarget: 'foo.xyz', + fizz: 'buzz', + }, + foo: 'bar', + }, + foo: 'bar', + }, + } + + const newStorage = await migration48.migrate(oldStorage) + assert.deepEqual(newStorage.data, { + NetworkController: { + foo: 'bar', + provider: { + type: 'someType', + rpcUrl: 'foo.xyz', + fizz: 'buzz', + }, + }, + foo: 'bar', + }) + }) + + it('should do nothing if affected state does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + NetworkController: { + provider: { + type: 'notRpc', + }, + }, + foo: 'bar', + }, + } + + const newStorage = await migration48.migrate(oldStorage) + assert.deepEqual(oldStorage.data, newStorage.data) + }) + + it('should do nothing if state is empty', async function () { + const oldStorage = { + meta: {}, + data: {}, + } + + const newStorage = await migration48.migrate(oldStorage) + assert.deepEqual(oldStorage.data, newStorage.data) + }) +}) diff --git a/test/unit/ui/app/reducers/metamask.spec.js b/test/unit/ui/app/reducers/metamask.spec.js index 1a872b14e..f662d8173 100644 --- a/test/unit/ui/app/reducers/metamask.spec.js +++ b/test/unit/ui/app/reducers/metamask.spec.js @@ -27,7 +27,7 @@ describe('MetaMask Reducers', function () { value: 'https://custom.rpc', }) - assert.equal(state.provider.rpcTarget, 'https://custom.rpc') + assert.equal(state.provider.rpcUrl, 'https://custom.rpc') }) it('sets provider type', function () { diff --git a/ui/app/components/app/alerts/alerts.js b/ui/app/components/app/alerts/alerts.js index 72bb4910a..9aa63418f 100644 --- a/ui/app/components/app/alerts/alerts.js +++ b/ui/app/components/app/alerts/alerts.js @@ -1,12 +1,21 @@ import React from 'react' import { useSelector } from 'react-redux' +import PropTypes from 'prop-types' import { alertIsOpen as unconnectedAccountAlertIsOpen } from '../../../ducks/alerts/unconnected-account' +import { alertIsOpen as invalidCustomNetworkAlertIsOpen } from '../../../ducks/alerts/invalid-custom-network' +import InvalidCustomNetworkAlert from './invalid-custom-network-alert' import UnconnectedAccountAlert from './unconnected-account-alert' -const Alerts = () => { +const Alerts = ({ history }) => { + const _invalidCustomNetworkAlertIsOpen = useSelector(invalidCustomNetworkAlertIsOpen) const _unconnectedAccountAlertIsOpen = useSelector(unconnectedAccountAlertIsOpen) + if (_invalidCustomNetworkAlertIsOpen) { + return ( + + ) + } if (_unconnectedAccountAlertIsOpen) { return ( @@ -16,4 +25,8 @@ const Alerts = () => { return null } +Alerts.propTypes = { + history: PropTypes.object.isRequired, +} + export default Alerts diff --git a/ui/app/components/app/alerts/alerts.scss b/ui/app/components/app/alerts/alerts.scss index 280171f5e..aeba2c3b3 100644 --- a/ui/app/components/app/alerts/alerts.scss +++ b/ui/app/components/app/alerts/alerts.scss @@ -1 +1,2 @@ +@import './invalid-custom-network-alert/invalid-custom-network-alert'; @import './unconnected-account-alert/unconnected-account-alert'; diff --git a/ui/app/components/app/alerts/invalid-custom-network-alert/index.js b/ui/app/components/app/alerts/invalid-custom-network-alert/index.js new file mode 100644 index 000000000..9227a2172 --- /dev/null +++ b/ui/app/components/app/alerts/invalid-custom-network-alert/index.js @@ -0,0 +1 @@ +export { default } from './invalid-custom-network-alert' diff --git a/ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.js b/ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.js new file mode 100644 index 000000000..5c23d58b3 --- /dev/null +++ b/ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.js @@ -0,0 +1,97 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import PropTypes from 'prop-types' + +import { ALERT_STATE } from '../../../../ducks/alerts' +import { + dismissAlert, + getAlertState, + getNetworkName, +} from '../../../../ducks/alerts/invalid-custom-network' +import Popover from '../../../ui/popover' +import Button from '../../../ui/button' +import { useI18nContext } from '../../../../hooks/useI18nContext' +import { NETWORKS_ROUTE } from '../../../../helpers/constants/routes' + +const { + ERROR, + LOADING, +} = ALERT_STATE + +const InvalidCustomNetworkAlert = ({ history }) => { + const t = useI18nContext() + const dispatch = useDispatch() + const alertState = useSelector(getAlertState) + const networkName = useSelector(getNetworkName) + + const onClose = () => dispatch(dismissAlert()) + + const footer = ( + <> + { + alertState === ERROR + ? ( +
+ { t('failureMessage') } +
+ ) + : null + } +
+ + +
+ + ) + + return ( + +

{t('invalidCustomNetworkAlertContent1', [networkName])}

+

{t('invalidCustomNetworkAlertContent2')}

+

+ { + t('invalidCustomNetworkAlertContent3', [( + global.platform.openTab({ url: 'https://chainid.network' }) + } + > + chainId.network + + )]) + } +

+
+ ) +} + +InvalidCustomNetworkAlert.propTypes = { + history: PropTypes.object.isRequired, +} + +export default InvalidCustomNetworkAlert diff --git a/ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.scss b/ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.scss new file mode 100644 index 000000000..de7010883 --- /dev/null +++ b/ui/app/components/app/alerts/invalid-custom-network-alert/invalid-custom-network-alert.scss @@ -0,0 +1,57 @@ +.invalid-custom-network-alert { + &__content { + border-radius: 0; + padding: 0 24px 16px 24px; + + > p { + @include Paragraph; + + font-size: 14px; + padding-bottom: 12px; + } + + > p:last-of-type { + padding-bottom: 0; + } + } + + &__content-link { + color: $primary-blue; + cursor: pointer; + } + + &__footer { + flex-direction: column; + + > :only-child { + margin: 0; + width: 100%; + } + } + + &__footer-row { + display: flex; + flex-direction: row; + justify-content: space-between; + + & &-button { + height: 40px; + width: 50%; + border-radius: 100px; + margin-right: 24px; + + &:last-of-type { + margin-right: 0; + } + } + } + + &__error { + margin-bottom: 16px; + padding: 16px; + font-size: 14px; + border: 1px solid $accent-red; + background: #f8eae8; + border-radius: 3px; + } +} diff --git a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js index 779a5a92b..3ed29ba04 100644 --- a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js +++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js @@ -1,8 +1,8 @@ import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { ALERT_STATE } from '../../../../ducks/alerts' import { - ALERT_STATE, connectAccount, dismissAlert, dismissAndDisableAlert, diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index 16b645858..a59173aa9 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -4,7 +4,11 @@ import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { compose } from 'redux' import * as actions from '../../../store/actions' +import { + openAlert as displayInvalidCustomNetworkAlert, +} from '../../../ducks/alerts/invalid-custom-network' import { NETWORKS_ROUTE } from '../../../helpers/constants/routes' +import { isPrefixedFormattedHexString } from '../../../../../app/scripts/lib/util' import { Dropdown, DropdownMenuItem } from './components/dropdown' import NetworkDropdownIcon from './components/network-dropdown-icon' @@ -22,7 +26,6 @@ function mapStateToProps (state) { provider: state.metamask.provider, frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], networkDropdownOpen: state.appState.networkDropdownOpen, - network: state.metamask.network, } } @@ -38,7 +41,12 @@ function mapDispatchToProps (dispatch) { dispatch(actions.delRpcTarget(target)) }, hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), - setNetworksTabAddMode: (isInAddMode) => dispatch(actions.setNetworksTabAddMode(isInAddMode)), + setNetworksTabAddMode: (isInAddMode) => { + dispatch(actions.setNetworksTabAddMode(isInAddMode)) + }, + displayInvalidCustomNetworkAlert: (networkName) => { + dispatch(displayInvalidCustomNetworkAlert(networkName)) + }, } } @@ -51,12 +59,11 @@ class NetworkDropdown extends Component { static propTypes = { provider: PropTypes.shape({ nickname: PropTypes.string, - rpcTarget: PropTypes.string, + rpcUrl: PropTypes.string, type: PropTypes.string, ticker: PropTypes.string, }).isRequired, setProviderType: PropTypes.func.isRequired, - network: PropTypes.string.isRequired, setRpcTarget: PropTypes.func.isRequired, hideNetworkDropdown: PropTypes.func.isRequired, setNetworksTabAddMode: PropTypes.func.isRequired, @@ -64,6 +71,7 @@ class NetworkDropdown extends Component { networkDropdownOpen: PropTypes.bool.isRequired, history: PropTypes.object.isRequired, delRpcTarget: PropTypes.func.isRequired, + displayInvalidCustomNetworkAlert: PropTypes.func.isRequired, } handleClick (newProviderType) { @@ -84,64 +92,30 @@ class NetworkDropdown extends Component { setProviderType(newProviderType) } - renderCustomOption (provider) { - const { rpcTarget, type, ticker, nickname } = provider - const { network } = this.props - - if (type !== 'rpc') { - return null - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return ( - this.props.setRpcTarget(rpcTarget, network, ticker, nickname)} - closeMenu={() => this.props.hideNetworkDropdown()} - style={{ - fontSize: '16px', - lineHeight: '20px', - padding: '12px 0', - }} - > - - - - {nickname || rpcTarget} - - - ) - } - } - - renderCommonRpc (rpcListDetail, provider) { + renderCustomRpcList (rpcListDetail, provider) { const reversedRpcListDetail = rpcListDetail.slice().reverse() return reversedRpcListDetail.map((entry) => { - const rpc = entry.rpcUrl - const ticker = entry.ticker || 'ETH' - const nickname = entry.nickname || '' - const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget + const { rpcUrl, chainId, ticker = 'ETH', nickname = '' } = entry + const currentRpcTarget = ( + provider.type === 'rpc' && rpcUrl === provider.rpcUrl + ) - if ((rpc === 'http://localhost:8545') || currentRpcTarget) { + if (rpcUrl === 'http://localhost:8545') { return null } - const { chainId } = entry + return ( this.props.hideNetworkDropdown()} - onClick={() => this.props.setRpcTarget(rpc, chainId, ticker, nickname)} + onClick={() => { + if (isPrefixedFormattedHexString(chainId)) { + this.props.setRpcTarget(rpcUrl, chainId, ticker, nickname) + } else { + this.props.displayInvalidCustomNetworkAlert(nickname || rpcUrl) + } + }} style={{ fontSize: '16px', lineHeight: '20px', @@ -162,15 +136,21 @@ class NetworkDropdown extends Component { : '#9b9b9b', }} > - {nickname || rpc} + {nickname || rpcUrl} - { - e.stopPropagation() - this.props.delRpcTarget(rpc) - }} - /> + { + currentRpcTarget + ? null + : ( + { + e.stopPropagation() + this.props.delRpcTarget(rpcUrl) + }} + /> + ) + } ) }) @@ -202,7 +182,7 @@ class NetworkDropdown extends Component { } render () { - const { provider: { type: providerType, rpcTarget: activeNetwork }, setNetworksTabAddMode } = this.props + const { provider: { type: providerType, rpcUrl: activeNetwork }, setNetworksTabAddMode } = this.props const rpcListDetail = this.props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { @@ -382,8 +362,7 @@ class NetworkDropdown extends Component { {this.context.t('localhost')} - {this.renderCustomOption(this.props.provider)} - {this.renderCommonRpc(rpcListDetail, this.props.provider)} + {this.renderCustomRpcList(rpcListDetail, this.props.provider)} this.props.hideNetworkDropdown()} onClick={() => { diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js index 6d581d09b..6eca49568 100644 --- a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js +++ b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js @@ -50,7 +50,8 @@ describe('Network Dropdown', function () { 'type': 'test', }, frequentRpcListDetail: [ - { rpcUrl: 'http://localhost:7545' }, + { chainId: '0x1a', rpcUrl: 'http://localhost:7545' }, + { rpcUrl: 'http://localhost:7546' }, ], }, appState: { @@ -65,8 +66,8 @@ describe('Network Dropdown', function () { ) }) - it('renders 7 DropDownMenuItems ', function () { - assert.equal(wrapper.find(DropdownMenuItem).length, 8) + it('renders 9 DropDownMenuItems ', function () { + assert.equal(wrapper.find(DropdownMenuItem).length, 9) }) it('checks background color for first NetworkDropdownIcon', function () { @@ -93,8 +94,8 @@ describe('Network Dropdown', function () { assert.equal(wrapper.find(NetworkDropdownIcon).at(5).prop('innerBorder'), '1px solid #9b9b9b') }) - it('checks dropdown for frequestRPCList from state ', function () { - assert.equal(wrapper.find(DropdownMenuItem).at(6).text(), '✓http://localhost:7545') + it('checks dropdown for frequestRPCList from state', function () { + assert.equal(wrapper.find(DropdownMenuItem).at(6).text(), '✓http://localhost:7546') }) it('checks background color for seventh NetworkDropdownIcon', function () { diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.container.js b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js index 9c348dab7..7662ae64f 100644 --- a/ui/app/components/app/loading-network-screen/loading-network-screen.container.js +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js @@ -12,10 +12,10 @@ const mapStateToProps = (state) => { provider, network, } = state.metamask - const { rpcTarget, chainId, ticker, nickname, type } = provider + const { rpcUrl, chainId, ticker, nickname, type } = provider const setProviderArgs = type === 'rpc' - ? [rpcTarget, chainId, ticker, nickname] + ? [rpcUrl, chainId, ticker, nickname] : [provider.type] return { diff --git a/ui/app/components/app/network.js b/ui/app/components/app/network.js index a156f676d..97f7c44bb 100644 --- a/ui/app/components/app/network.js +++ b/ui/app/components/app/network.js @@ -47,7 +47,7 @@ export default class Network extends Component { provider: PropTypes.shape({ type: PropTypes.string, nickname: PropTypes.string, - rpcTarget: PropTypes.string, + rpcUrl: PropTypes.string, }).isRequired, disabled: PropTypes.bool, onClick: PropTypes.func.isRequired, @@ -69,7 +69,7 @@ export default class Network extends Component { if (provider) { providerName = provider.type providerNick = provider.nickname || '' - providerUrl = provider.rpcTarget + providerUrl = provider.rpcUrl } switch (providerName) { diff --git a/ui/app/components/ui/popover/index.scss b/ui/app/components/ui/popover/index.scss index a1ef85b06..d5072ccc3 100644 --- a/ui/app/components/ui/popover/index.scss +++ b/ui/app/components/ui/popover/index.scss @@ -22,7 +22,7 @@ &-header { display: flex; - padding: 24px; + padding: 24px 24px 16px; flex-direction: column; background: white; position: relative; @@ -37,7 +37,6 @@ font-weight: bold; line-height: 25px; - padding-bottom: 8px; h2 { white-space: nowrap; @@ -53,6 +52,7 @@ &__subtitle { @include H6; + padding-top: 8px; line-height: 20px; } diff --git a/ui/app/ducks/alerts/enums.js b/ui/app/ducks/alerts/enums.js new file mode 100644 index 000000000..9a2267e52 --- /dev/null +++ b/ui/app/ducks/alerts/enums.js @@ -0,0 +1,6 @@ +export const ALERT_STATE = { + CLOSED: 'CLOSED', + ERROR: 'ERROR', + LOADING: 'LOADING', + OPEN: 'OPEN', +} diff --git a/ui/app/ducks/alerts/index.js b/ui/app/ducks/alerts/index.js index a28d23ecb..d7387326b 100644 --- a/ui/app/ducks/alerts/index.js +++ b/ui/app/ducks/alerts/index.js @@ -1 +1,4 @@ export { default as unconnectedAccount } from './unconnected-account' +export { default as invalidCustomNetwork } from './invalid-custom-network' + +export { ALERT_STATE } from './enums' diff --git a/ui/app/ducks/alerts/invalid-custom-network.js b/ui/app/ducks/alerts/invalid-custom-network.js new file mode 100644 index 000000000..62e06f14c --- /dev/null +++ b/ui/app/ducks/alerts/invalid-custom-network.js @@ -0,0 +1,51 @@ +import { createSlice } from '@reduxjs/toolkit' + +import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_STATE } from './enums' + +// Constants + +const name = ALERT_TYPES.invalidCustomNetwork + +const initialState = { + state: ALERT_STATE.CLOSED, + networkName: '', +} + +// Slice (reducer plus auto-generated actions and action creators) + +const slice = createSlice({ + name, + initialState, + reducers: { + openAlert: (state, action) => { + state.state = ALERT_STATE.OPEN + state.networkName = action.payload + }, + dismissAlert: (state) => { + state.state = ALERT_STATE.CLOSED + state.networkName = '' + }, + }, +}) + +const { actions, reducer } = slice + +export default reducer + +// Selectors + +export const getAlertState = (state) => state[name].state + +export const getNetworkName = (state) => state[name].networkName + +export const alertIsOpen = (state) => state[name].state !== ALERT_STATE.CLOSED + +// Actions / action-creators + +const { + openAlert, + dismissAlert, +} = actions + +export { openAlert, dismissAlert } diff --git a/ui/app/ducks/alerts/unconnected-account.js b/ui/app/ducks/alerts/unconnected-account.js index 397533463..9b150dc71 100644 --- a/ui/app/ducks/alerts/unconnected-account.js +++ b/ui/app/ducks/alerts/unconnected-account.js @@ -12,16 +12,10 @@ import { getOriginOfCurrentTab, getSelectedAddress, } from '../../selectors' +import { ALERT_STATE } from './enums' // Constants -export const ALERT_STATE = { - CLOSED: 'CLOSED', - ERROR: 'ERROR', - LOADING: 'LOADING', - OPEN: 'OPEN', -} - const name = ALERT_TYPES.unconnectedAccount const initialState = { diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js index 91ba222cb..5b130f26f 100644 --- a/ui/app/ducks/index.js +++ b/ui/app/ducks/index.js @@ -6,10 +6,11 @@ import sendReducer from './send/send.duck' import appStateReducer from './app/app' import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck' import gasReducer from './gas/gas.duck' -import { unconnectedAccount } from './alerts' +import { invalidCustomNetwork, unconnectedAccount } from './alerts' import historyReducer from './history/history' export default combineReducers({ + [ALERT_TYPES.invalidCustomNetwork]: invalidCustomNetwork, [ALERT_TYPES.unconnectedAccount]: unconnectedAccount, activeTab: (s) => (s === undefined ? null : s), metamask: metamaskReducer, diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index c2058fc06..f9c259f82 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -6,7 +6,7 @@ export default function reduceMetamask (state = {}, action) { isInitialized: false, isUnlocked: false, isAccountMenuOpen: false, - rpcTarget: 'https://rawtestrpc.metamask.io/', + rpcUrl: 'https://rawtestrpc.metamask.io/', identities: {}, unapprovedTxs: {}, frequentRpcList: [], @@ -65,7 +65,7 @@ export default function reduceMetamask (state = {}, action) { ...metamaskState, provider: { type: 'rpc', - rpcTarget: action.value, + rpcUrl: action.value, }, } @@ -375,6 +375,8 @@ export const getCurrentLocale = (state) => state.metamask.currentLocale export const getAlertEnabledness = (state) => state.metamask.alertEnabledness +export const getInvalidCustomNetworkAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.invalidCustomNetwork] + export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.unconnectedAccount] export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins diff --git a/ui/app/pages/routes/routes.component.js b/ui/app/pages/routes/routes.component.js index 88b8321e9..ff100f0bd 100644 --- a/ui/app/pages/routes/routes.component.js +++ b/ui/app/pages/routes/routes.component.js @@ -256,7 +256,7 @@ export default class Routes extends Component { { isUnlocked ? ( - + ) : null } diff --git a/ui/app/pages/settings/networks-tab/index.scss b/ui/app/pages/settings/networks-tab/index.scss index f6a69af39..455adb971 100644 --- a/ui/app/pages/settings/networks-tab/index.scss +++ b/ui/app/pages/settings/networks-tab/index.scss @@ -96,6 +96,12 @@ } &__network-form-label { + display: flex; + align-items: center; + } + + &__network-form-label-text { + font-family: Roboto; font-style: normal; font-weight: normal; font-size: 14px; @@ -103,6 +109,11 @@ color: #000; } + &__network-form-label-tooltip { + margin-left: 5px; + font-size: 12px; + } + &__networks-list { flex: 0.5 0 auto; max-width: 343px; diff --git a/ui/app/pages/settings/networks-tab/network-form/network-form.component.js b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js index 024af3a31..ea1014afa 100644 --- a/ui/app/pages/settings/networks-tab/network-form/network-form.component.js +++ b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js @@ -1,8 +1,11 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import validUrl from 'valid-url' +import BigNumber from 'bignumber.js' import TextField from '../../../../components/ui/text-field' import Button from '../../../../components/ui/button' +import Tooltip from '../../../../components/ui/tooltip' +import { isPrefixedFormattedHexString } from '../../../../../../app/scripts/lib/util' export default class NetworkForm extends PureComponent { static contextTypes = { @@ -97,10 +100,17 @@ export default class NetworkForm extends PureComponent { const { networkName, rpcUrl, - chainId, + chainId: stateChainId, ticker, blockExplorerUrl, } = this.state + + // Ensure chainId is a 0x-prefixed, lowercase hex string + let chainId = stateChainId.trim().toLowerCase() + if (!chainId.startsWith('0x')) { + chainId = `0x${(new BigNumber(chainId, 10)).toString(16)}` + } + if (propsRpcUrl && rpcUrl !== propsRpcUrl) { editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, { blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl, @@ -168,13 +178,30 @@ export default class NetworkForm extends PureComponent { ) } - renderFormTextField (fieldKey, textFieldId, onChange, value, optionalTextFieldKey) { + renderFormTextField (fieldKey, textFieldId, onChange, value, optionalTextFieldKey, tooltipText) { const { errors } = this.state const { viewOnly } = this.props return (
-
{this.context.t(optionalTextFieldKey || fieldKey)}
+
+
+ {this.context.t(optionalTextFieldKey || fieldKey)} +
+ { + !viewOnly && tooltipText + ? ( + + + + ) + : null + } +
{ - // eslint-disable-next-line radix - this.setErrorTo('chainId', Boolean(chainId) && Number.isNaN(parseInt(chainId)) - ? `${this.context.t('invalidInput')} chainId` - : '') + validateChainId = (chainIdArg = '') => { + const chainId = chainIdArg.trim() + let errorMessage = '' + + if (chainId.startsWith('0x')) { + if (!(/^0x[0-9a-f]+$/ui).test(chainId)) { + errorMessage = this.context.t('invalidHexNumber') + } else if (!isPrefixedFormattedHexString(chainId)) { + errorMessage = this.context.t('invalidHexNumberLeadingZeros') + } + } else if (!(/^[0-9]+$/u).test(chainId)) { + errorMessage = this.context.t('invalidNumber') + } else if (chainId.startsWith('0')) { + errorMessage = this.context.t('invalidNumberLeadingZeros') + } + + this.setErrorTo('chainId', errorMessage) } isValidWhenAppended = (url) => { @@ -262,7 +301,13 @@ export default class NetworkForm extends PureComponent { errors, } = this.state - const isSubmitDisabled = viewOnly || this.stateIsUnchanged() || Object.values(errors).some((x) => x) || !rpcUrl + const isSubmitDisabled = ( + viewOnly || + this.stateIsUnchanged() || + !rpcUrl || + !chainId || + Object.values(errors).some((x) => x) + ) const deletable = !networksTabIsInAddMode && !isCurrentRpcTarget && !viewOnly return ( @@ -285,7 +330,8 @@ export default class NetworkForm extends PureComponent { 'chainId', this.setStateWithValue('chainId', this.validateChainId), chainId, - 'optionalChainId', + null, + t('networkSettingsChainIdDescription'), )} {this.renderFormTextField( 'symbol', diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js index 01e0666ed..a90608bf3 100644 --- a/ui/app/pages/settings/networks-tab/networks-tab.constants.js +++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js @@ -1,54 +1,68 @@ +import { + GOERLI, + GOERLI_CHAIN_ID, + KOVAN, + KOVAN_CHAIN_ID, + LOCALHOST, + MAINNET, + MAINNET_CHAIN_ID, + RINKEBY, + RINKEBY_CHAIN_ID, + ROPSTEN, + ROPSTEN_CHAIN_ID, +} from '../../../../../app/scripts/controllers/network/enums' + const defaultNetworksData = [ { - labelKey: 'mainnet', + labelKey: MAINNET, iconColor: '#29B6AF', - providerType: 'mainnet', + providerType: MAINNET, rpcUrl: `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, - chainId: '1', + chainId: MAINNET_CHAIN_ID, ticker: 'ETH', blockExplorerUrl: 'https://etherscan.io', }, { - labelKey: 'ropsten', + labelKey: ROPSTEN, iconColor: '#FF4A8D', - providerType: 'ropsten', + providerType: ROPSTEN, rpcUrl: `https://ropsten.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, - chainId: '3', + chainId: ROPSTEN_CHAIN_ID, ticker: 'ETH', blockExplorerUrl: 'https://ropsten.etherscan.io', }, { - labelKey: 'rinkeby', + labelKey: RINKEBY, iconColor: '#F6C343', - providerType: 'rinkeby', + providerType: RINKEBY, rpcUrl: `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, - chainId: '4', + chainId: RINKEBY_CHAIN_ID, ticker: 'ETH', blockExplorerUrl: 'https://rinkeby.etherscan.io', }, { - labelKey: 'goerli', + labelKey: GOERLI, iconColor: '#3099f2', - providerType: 'goerli', + providerType: GOERLI, rpcUrl: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, - chainId: '5', + chainId: GOERLI_CHAIN_ID, ticker: 'ETH', blockExplorerUrl: 'https://goerli.etherscan.io', }, { - labelKey: 'kovan', + labelKey: KOVAN, iconColor: '#9064FF', - providerType: 'kovan', + providerType: KOVAN, rpcUrl: `https://kovan.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, - chainId: '42', + chainId: KOVAN_CHAIN_ID, ticker: 'ETH', blockExplorerUrl: 'https://etherscan.io', }, { - labelKey: 'localhost', + labelKey: LOCALHOST, iconColor: 'white', border: '1px solid #6A737D', - providerType: 'localhost', + providerType: LOCALHOST, rpcUrl: 'http://localhost:8545/', blockExplorerUrl: 'https://etherscan.io', }, diff --git a/ui/app/pages/settings/networks-tab/networks-tab.container.js b/ui/app/pages/settings/networks-tab/networks-tab.container.js index 5b35f3ba5..4851bf129 100644 --- a/ui/app/pages/settings/networks-tab/networks-tab.container.js +++ b/ui/app/pages/settings/networks-tab/networks-tab.container.js @@ -43,7 +43,7 @@ const mapStateToProps = (state) => { let networkDefaultedToProvider = false if (!networkIsSelected && !networksTabIsInAddMode) { selectedNetwork = networksToRender.find((network) => { - return network.rpcUrl === provider.rpcTarget || (network.providerType !== 'rpc' && network.providerType === provider.type) + return network.rpcUrl === provider.rpcUrl || (network.providerType !== 'rpc' && network.providerType === provider.type) }) || {} networkDefaultedToProvider = true } @@ -54,7 +54,7 @@ const mapStateToProps = (state) => { networkIsSelected, networksTabIsInAddMode, providerType: provider.type, - providerUrl: provider.rpcTarget, + providerUrl: provider.rpcUrl, networkDefaultedToProvider, } } diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 3789e3854..fe32cd1d5 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -9,9 +9,9 @@ import { import { getPermissionsRequestCount } from './permissions' export function getNetworkIdentifier (state) { - const { metamask: { provider: { type, nickname, rpcTarget } } } = state + const { metamask: { provider: { type, nickname, rpcUrl } } } = state - return nickname || rpcTarget || type + return nickname || rpcUrl || type } export function getCurrentKeyring (state) { @@ -299,7 +299,7 @@ export const getBackgroundMetaMetricState = (state) => { export function getRpcPrefsForCurrentProvider (state) { const { frequentRpcListDetail, provider } = state.metamask - const selectRpcInfo = frequentRpcListDetail.find((rpcInfo) => rpcInfo.rpcUrl === provider.rpcTarget) + const selectRpcInfo = frequentRpcListDetail.find((rpcInfo) => rpcInfo.rpcUrl === provider.rpcUrl) const { rpcPrefs = {} } = selectRpcInfo || {} return rpcPrefs } diff --git a/ui/app/selectors/tests/send-selectors-test-data.js b/ui/app/selectors/tests/send-selectors-test-data.js index a5bd43892..4a7debcda 100644 --- a/ui/app/selectors/tests/send-selectors-test-data.js +++ b/ui/app/selectors/tests/send-selectors-test-data.js @@ -3,7 +3,7 @@ const state = { 'isInitialized': true, 'isUnlocked': true, 'featureFlags': { 'sendHexData': true }, - 'rpcTarget': 'https://rawtestrpc.metamask.io/', + 'rpcUrl': 'https://rawtestrpc.metamask.io/', 'identities': { '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', diff --git a/ui/index.js b/ui/index.js index 9e777ed94..7b02f2122 100644 --- a/ui/index.js +++ b/ui/index.js @@ -14,7 +14,7 @@ import txHelper from './lib/tx-helper' import { fetchLocale, loadRelativeTimeFormatLocaleData } from './app/helpers/utils/i18n-helper' import switchDirection from './app/helpers/utils/switch-direction' import { getPermittedAccountsForCurrentTab, getSelectedAddress } from './app/selectors' -import { ALERT_STATE } from './app/ducks/alerts/unconnected-account' +import { ALERT_STATE } from './app/ducks/alerts' import { getUnconnectedAccountAlertEnabledness, getUnconnectedAccountAlertShown, @@ -88,7 +88,9 @@ async function startApp (metamaskState, backgroundConnection, opts) { permittedAccountsForCurrentTab.length > 0 && !permittedAccountsForCurrentTab.includes(selectedAddress) ) { - draftInitialState[ALERT_TYPES.unconnectedAccount] = { state: ALERT_STATE.OPEN } + draftInitialState[ALERT_TYPES.unconnectedAccount] = { + state: ALERT_STATE.OPEN, + } actions.setUnconnectedAccountAlertShown(origin) } }