mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Network tab refactor (#12502)
This commit is contained in:
parent
e3e6da1a75
commit
524725b24b
@ -665,12 +665,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "በርቷል"
|
"message": "በርቷል"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "ኤክስፕሎረር URL አግድ (አማራጭ)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "ምልክት (አማራጭ)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "መነሻ"
|
"message": "መነሻ"
|
||||||
},
|
},
|
||||||
|
@ -661,12 +661,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "تشغيل"
|
"message": "تشغيل"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "العنوان الإلكتروني لمستكشف البلوكات (اختياري)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "الرمز (اختياري)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "الأصل"
|
"message": "الأصل"
|
||||||
},
|
},
|
||||||
|
@ -664,12 +664,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Включено"
|
"message": "Включено"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Блокиране на Explorer URL (по избор)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Символ (по избор)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Произход"
|
"message": "Произход"
|
||||||
},
|
},
|
||||||
|
@ -668,12 +668,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "চালু"
|
"message": "চালু"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "এক্সপ্লোরার URL ব্লক করুন (ঐচ্ছিক)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "প্রতীক (ঐচ্ছিক)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "উৎস"
|
"message": "উৎস"
|
||||||
},
|
},
|
||||||
|
@ -652,12 +652,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Activat"
|
"message": "Activat"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Bloqueja l'URL d'Explorer (opcional)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Símbol (opcional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origen"
|
"message": "Origen"
|
||||||
},
|
},
|
||||||
|
@ -652,12 +652,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Til"
|
"message": "Til"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blok-stifinder-URL (valgfrit)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbol (valgfrit)"
|
|
||||||
},
|
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"message": "Parametre"
|
"message": "Parametre"
|
||||||
},
|
},
|
||||||
|
@ -644,9 +644,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "An"
|
"message": "An"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Block-Explorer-URL (optional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Ursprung"
|
"message": "Ursprung"
|
||||||
},
|
},
|
||||||
|
@ -665,12 +665,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Ενεργό"
|
"message": "Ενεργό"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Διεύθυνση URL Εξερευνητή Μπλοκ (προαιρετικό)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Σύμβολο (προαιρετικό)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Προέλευση"
|
"message": "Προέλευση"
|
||||||
},
|
},
|
||||||
|
@ -1767,11 +1767,8 @@
|
|||||||
"optional": {
|
"optional": {
|
||||||
"message": "Optional"
|
"message": "Optional"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
"optionalWithParanthesis": {
|
||||||
"message": "Block Explorer URL (optional)"
|
"message": "(Optional)"
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Currency Symbol (optional)"
|
|
||||||
},
|
},
|
||||||
"or": {
|
"or": {
|
||||||
"message": "or"
|
"message": "or"
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Conéctese solo con sitios de confianza."
|
"message": "Conéctese solo con sitios de confianza."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Dirección URL del explorador de bloques (opcional)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Símbolo de moneda (opcional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origen"
|
"message": "Origen"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Conéctese solo con sitios de confianza."
|
"message": "Conéctese solo con sitios de confianza."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Dirección URL del explorador de bloques (opcional)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Símbolo de moneda (opcional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origen"
|
"message": "Origen"
|
||||||
},
|
},
|
||||||
|
@ -658,12 +658,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Sees"
|
"message": "Sees"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokeeri Exploreri URL (valikuline)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Sümbol (valikuline)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Päritolu"
|
"message": "Päritolu"
|
||||||
},
|
},
|
||||||
|
@ -668,12 +668,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "روشن"
|
"message": "روشن"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "بلاک کردن مرورگر URL (انتخابی)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "سمبول (انتخابی)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "مبدأ"
|
"message": "مبدأ"
|
||||||
},
|
},
|
||||||
|
@ -665,12 +665,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Käytössä"
|
"message": "Käytössä"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Estä Explorerin URL-osoite (valinnainen)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symboli (valinnainen)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Alkuperä"
|
"message": "Alkuperä"
|
||||||
},
|
},
|
||||||
|
@ -602,12 +602,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Naka-on"
|
"message": "Naka-on"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Block Explorer URL (opsyonal)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbolo (opsyonal)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Pinanggalingan"
|
"message": "Pinanggalingan"
|
||||||
},
|
},
|
||||||
|
@ -650,12 +650,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Activé"
|
"message": "Activé"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL de l'explorateur de blocs (facultatif)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbole (facultatif)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origine"
|
"message": "Origine"
|
||||||
},
|
},
|
||||||
|
@ -665,12 +665,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "פועל"
|
"message": "פועל"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "חסום כתובת URL של אקספלורר (אופציונלי)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "סמל (אופציונלי)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "מקור"
|
"message": "מקור"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "केवल उन साइटों से कनेक्ट करें, जिन पर आप भरोसा करते हैं।"
|
"message": "केवल उन साइटों से कनेक्ट करें, जिन पर आप भरोसा करते हैं।"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "ब्लॉक एक्सप्लोरर URL (वैकल्पिक)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "मुद्रा प्रतीक (वैकल्पिक)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "उत्पत्ति"
|
"message": "उत्पत्ति"
|
||||||
},
|
},
|
||||||
|
@ -661,12 +661,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Uključi"
|
"message": "Uključi"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokiraj Explorerov URL (neobavezno)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbol (neobavezno)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Podrijetlo"
|
"message": "Podrijetlo"
|
||||||
},
|
},
|
||||||
|
@ -661,12 +661,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Be"
|
"message": "Be"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Explorer URL letiltása (nem kötelező)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Szimbólum (opcionális)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Eredet"
|
"message": "Eredet"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Hanya hubungkan ke situs yang Anda percayai."
|
"message": "Hanya hubungkan ke situs yang Anda percayai."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL Block Explorer (opsional)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbol Mata Uang (opsional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Asal"
|
"message": "Asal"
|
||||||
},
|
},
|
||||||
|
@ -1080,12 +1080,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Connettiti solo con siti di cui ti fidi."
|
"message": "Connettiti solo con siti di cui ti fidi."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL del Block Explorer (opzionale)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbolo (opzionale)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origine"
|
"message": "Origine"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "信頼するサイトにのみ接続します。"
|
"message": "信頼するサイトにのみ接続します。"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "ブロック エクスプローラーの URL (オプション)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "通貨記号 (オプション)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "起点"
|
"message": "起点"
|
||||||
},
|
},
|
||||||
|
@ -668,12 +668,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "ಆನ್"
|
"message": "ಆನ್"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "ಅನ್ವೇಷಕ URL ಅನ್ನು ನಿರ್ಬಂಧಿಸಿ (ಐಚ್ಛಿಕ)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "ಚಿಹ್ನೆ (ಐಚ್ಛಿಕ)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "ಮೂಲ"
|
"message": "ಮೂಲ"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "신뢰하는 사이트만 연결하세요."
|
"message": "신뢰하는 사이트만 연결하세요."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "블록 탐색기 URL(선택 사항)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "통화 기호(선택 사항)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "원본"
|
"message": "원본"
|
||||||
},
|
},
|
||||||
|
@ -668,12 +668,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Įjungta"
|
"message": "Įjungta"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokuoti naršyklės URL (pasirinktinai)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbolis (nebūtinas)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Kilmė"
|
"message": "Kilmė"
|
||||||
},
|
},
|
||||||
|
@ -664,12 +664,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Iesl."
|
"message": "Iesl."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Bloķēt Explorer URL (pēc izvēles)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbols (neobligāti)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Avots"
|
"message": "Avots"
|
||||||
},
|
},
|
||||||
|
@ -645,12 +645,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Hidupkan"
|
"message": "Hidupkan"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Sekat URL Explorer (pilihan)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbol (pilihan)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Asal"
|
"message": "Asal"
|
||||||
},
|
},
|
||||||
|
@ -655,12 +655,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "På"
|
"message": "På"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokker Explorer URL (valgfritt)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbol (valgfritt)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Opprinnelse"
|
"message": "Opprinnelse"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo."
|
"message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL ng Block Explorer (opsyonal)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbolo ng Currency (opsyonal)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Pinagmulan"
|
"message": "Pinagmulan"
|
||||||
},
|
},
|
||||||
|
@ -662,12 +662,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Włączone"
|
"message": "Włączone"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Adres URL przeglądarki łańcucha bloków (opcjonalnie)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbol (opcjonalnie)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Pochodzenie"
|
"message": "Pochodzenie"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Conecte-se somente com sites em quem você confia."
|
"message": "Conecte-se somente com sites em quem você confia."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL do Block Explorer (opcional)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Símbolo de moeda (opcional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origem"
|
"message": "Origem"
|
||||||
},
|
},
|
||||||
|
@ -655,12 +655,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Activat"
|
"message": "Activat"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL explorator bloc (opțional)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbol (opțional)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Origine"
|
"message": "Origine"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Подключайтесь только к сайтам, которым доверяете."
|
"message": "Подключайтесь только к сайтам, которым доверяете."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL-адрес проводника блока (необязательно)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Символ валюты (необязательно)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Источник"
|
"message": "Источник"
|
||||||
},
|
},
|
||||||
|
@ -637,12 +637,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Zapnuté"
|
"message": "Zapnuté"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokovať URL Explorera (voliteľné)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbol (voliteľné)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Pôvod"
|
"message": "Pôvod"
|
||||||
},
|
},
|
||||||
|
@ -656,12 +656,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Vklopljeno"
|
"message": "Vklopljeno"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokiraj URL Explorerja (poljubno)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbol (nezahtevano)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Izvor"
|
"message": "Izvor"
|
||||||
},
|
},
|
||||||
|
@ -659,12 +659,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Укључено"
|
"message": "Укључено"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Blokirajte URL Explorer-a (opciono)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbol (opciono)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Извор"
|
"message": "Извор"
|
||||||
},
|
},
|
||||||
|
@ -652,12 +652,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "På"
|
"message": "På"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Block Explorer URL (valfritt)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbol (frivillig)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Ursprung"
|
"message": "Ursprung"
|
||||||
},
|
},
|
||||||
|
@ -646,12 +646,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Imewashwa"
|
"message": "Imewashwa"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL ya Block Explorer URL (hiari)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Ishara (hiari)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Asili"
|
"message": "Asili"
|
||||||
},
|
},
|
||||||
|
@ -1071,12 +1071,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo."
|
"message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL ng Block Explorer (opsyonal)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Simbolo ng Currency (opsyonal)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Pinagmulan"
|
"message": "Pinagmulan"
|
||||||
},
|
},
|
||||||
|
@ -668,12 +668,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "Увімкнути"
|
"message": "Увімкнути"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "Блокувати Explorer URL (не обов'язково)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Символ (не обов'язково)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Походження"
|
"message": "Походження"
|
||||||
},
|
},
|
||||||
|
@ -1321,12 +1321,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "Chỉ kết nối với các trang web mà bạn tin tưởng."
|
"message": "Chỉ kết nối với các trang web mà bạn tin tưởng."
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "URL trình khám phá khối (không bắt buộc)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Ký hiệu tiền tệ (không bắt buộc)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "Nguồn gốc"
|
"message": "Nguồn gốc"
|
||||||
},
|
},
|
||||||
|
@ -1074,12 +1074,6 @@
|
|||||||
"onlyConnectTrust": {
|
"onlyConnectTrust": {
|
||||||
"message": "只连接您信任的网站。"
|
"message": "只连接您信任的网站。"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "区块浏览器 URL(选填)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "符号(选填)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "来源"
|
"message": "来源"
|
||||||
},
|
},
|
||||||
|
@ -668,12 +668,6 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "開啟"
|
"message": "開啟"
|
||||||
},
|
},
|
||||||
"optionalBlockExplorerUrl": {
|
|
||||||
"message": "區塊鏈瀏覽器 URL(非必要)"
|
|
||||||
},
|
|
||||||
"optionalCurrencySymbol": {
|
|
||||||
"message": "Symbol (可選)"
|
|
||||||
},
|
|
||||||
"origin": {
|
"origin": {
|
||||||
"message": "來源"
|
"message": "來源"
|
||||||
},
|
},
|
||||||
|
@ -32,13 +32,7 @@ describe('Stores custom RPC history', function () {
|
|||||||
|
|
||||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||||
|
|
||||||
await driver.findVisibleElement('.settings-page__content');
|
await driver.findElement('.networks-tab__sub-header-text');
|
||||||
|
|
||||||
await driver.findElement('.settings-page__sub-header-text');
|
|
||||||
|
|
||||||
await driver.clickElement(
|
|
||||||
'.add-network-form__header-add-network-button',
|
|
||||||
);
|
|
||||||
|
|
||||||
const customRpcInputs = await driver.findElements('input[type="text"]');
|
const customRpcInputs = await driver.findElements('input[type="text"]');
|
||||||
const networkNameInput = customRpcInputs[0];
|
const networkNameInput = customRpcInputs[0];
|
||||||
@ -54,7 +48,10 @@ describe('Stores custom RPC history', function () {
|
|||||||
await chainIdInput.clear();
|
await chainIdInput.clear();
|
||||||
await chainIdInput.sendKeys(chainId.toString());
|
await chainIdInput.sendKeys(chainId.toString());
|
||||||
|
|
||||||
await driver.clickElement('.add-network-form__footer-submit-button');
|
await driver.clickElement(
|
||||||
|
'.networks-tab__add-network-form-footer .btn-primary',
|
||||||
|
);
|
||||||
|
|
||||||
await driver.findElement({ text: networkName, tag: 'span' });
|
await driver.findElement({ text: networkName, tag: 'span' });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -79,13 +76,7 @@ describe('Stores custom RPC history', function () {
|
|||||||
|
|
||||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||||
|
|
||||||
await driver.findVisibleElement('.settings-page__content');
|
await driver.findElement('.networks-tab__sub-header-text');
|
||||||
|
|
||||||
await driver.findElement('.settings-page__sub-header-text');
|
|
||||||
|
|
||||||
await driver.clickElement(
|
|
||||||
'.add-network-form__header-add-network-button',
|
|
||||||
);
|
|
||||||
|
|
||||||
const customRpcInputs = await driver.findElements('input[type="text"]');
|
const customRpcInputs = await driver.findElements('input[type="text"]');
|
||||||
const rpcUrlInput = customRpcInputs[1];
|
const rpcUrlInput = customRpcInputs[1];
|
||||||
@ -94,7 +85,7 @@ describe('Stores custom RPC history', function () {
|
|||||||
await rpcUrlInput.sendKeys(duplicateRpcUrl);
|
await rpcUrlInput.sendKeys(duplicateRpcUrl);
|
||||||
await driver.findElement({
|
await driver.findElement({
|
||||||
text: 'This URL is currently used by the localhost network.',
|
text: 'This URL is currently used by the localhost network.',
|
||||||
tag: 'p',
|
tag: 'h6',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -120,15 +111,7 @@ describe('Stores custom RPC history', function () {
|
|||||||
|
|
||||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||||
|
|
||||||
// await driver.findElement('.add-network-form__sub-header-text');
|
await driver.findElement('.networks-tab__sub-header-text');
|
||||||
// wait for the full screen to be visible
|
|
||||||
await driver.findVisibleElement('.settings-page__content');
|
|
||||||
|
|
||||||
await driver.findElement('.settings-page__sub-header-text');
|
|
||||||
|
|
||||||
await driver.clickElement(
|
|
||||||
'.add-network-form__header-add-network-button',
|
|
||||||
);
|
|
||||||
|
|
||||||
const customRpcInputs = await driver.findElements('input[type="text"]');
|
const customRpcInputs = await driver.findElements('input[type="text"]');
|
||||||
const rpcUrlInput = customRpcInputs[1];
|
const rpcUrlInput = customRpcInputs[1];
|
||||||
@ -141,7 +124,7 @@ describe('Stores custom RPC history', function () {
|
|||||||
await chainIdInput.sendKeys(duplicateChainId);
|
await chainIdInput.sendKeys(duplicateChainId);
|
||||||
await driver.findElement({
|
await driver.findElement({
|
||||||
text: 'This Chain ID is currently used by the localhost network.',
|
text: 'This Chain ID is currently used by the localhost network.',
|
||||||
tag: 'p',
|
tag: 'h6',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -211,6 +194,10 @@ describe('Stores custom RPC history', function () {
|
|||||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||||
|
|
||||||
await driver.findVisibleElement('.settings-page__content');
|
await driver.findVisibleElement('.settings-page__content');
|
||||||
|
// // cancel new custom rpc
|
||||||
|
await driver.clickElement(
|
||||||
|
'.networks-tab__add-network-form-footer button.btn-secondary',
|
||||||
|
);
|
||||||
|
|
||||||
const networkListItems = await driver.findClickableElements(
|
const networkListItems = await driver.findClickableElements(
|
||||||
'.networks-tab__networks-list-name',
|
'.networks-tab__networks-list-name',
|
||||||
|
@ -14,7 +14,7 @@ import { COLORS, SIZES } from '../../../helpers/constants/design-system';
|
|||||||
import { getShowTestNetworks } from '../../../selectors';
|
import { getShowTestNetworks } from '../../../selectors';
|
||||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||||
import { NETWORKS_ROUTE } from '../../../helpers/constants/routes';
|
import { ADD_NETWORK_ROUTE } from '../../../helpers/constants/routes';
|
||||||
import { Dropdown, DropdownMenuItem } from './dropdown';
|
import { Dropdown, DropdownMenuItem } from './dropdown';
|
||||||
|
|
||||||
// classes from nodes of the toggle element.
|
// classes from nodes of the toggle element.
|
||||||
@ -129,9 +129,9 @@ class NetworkDropdown extends Component {
|
|||||||
size="large"
|
size="large"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
|
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
|
||||||
global.platform.openExtensionInBrowser(NETWORKS_ROUTE);
|
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
||||||
} else {
|
} else {
|
||||||
this.props.history.push(NETWORKS_ROUTE);
|
this.props.history.push(ADD_NETWORK_ROUTE);
|
||||||
}
|
}
|
||||||
this.props.hideNetworkDropdown();
|
this.props.hideNetworkDropdown();
|
||||||
}}
|
}}
|
||||||
|
@ -117,7 +117,7 @@ FormField.propTypes = {
|
|||||||
titleDetail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
titleDetail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
value: PropTypes.number,
|
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
detailText: PropTypes.string,
|
detailText: PropTypes.string,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
numeric: PropTypes.bool,
|
numeric: PropTypes.bool,
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './networks-tab.container';
|
export { default } from './networks-tab';
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
@import 'network-form/index.scss';
|
|
||||||
|
|
||||||
.networks-tab {
|
.networks-tab {
|
||||||
&__content {
|
&__content {
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 739px;
|
max-width: 739px;
|
||||||
@ -27,13 +24,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__subheader {
|
||||||
|
@include H4;
|
||||||
|
|
||||||
|
padding: 16px 4px;
|
||||||
|
border-bottom: 1px solid $alto;
|
||||||
|
height: 72px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subheader--break {
|
||||||
|
margin-inline-start: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sub-header-text {
|
||||||
|
@include H4;
|
||||||
|
|
||||||
|
color: $ui-4;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&__network-form {
|
&__network-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 343px;
|
|
||||||
max-height: 465px;
|
max-height: 465px;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
.page-container__footer {
|
.page-container__footer {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@ -53,29 +72,39 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 90%;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__add-network-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 465px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__network-form-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
width: 95%;
|
||||||
|
|
||||||
|
&__view-only {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add-network-form-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48% 48%;
|
||||||
|
// row-gap: 10%;
|
||||||
|
column-gap: 5%;
|
||||||
|
margin-top: 24px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__network-form-row {
|
&__network-form-row {
|
||||||
@media screen and (max-width: $break-small) {
|
@media screen and (max-width: $break-small) {
|
||||||
width: 93%;
|
width: 99%;
|
||||||
}
|
|
||||||
|
|
||||||
&--warning {
|
|
||||||
@include H7;
|
|
||||||
|
|
||||||
background-color: #fefae8;
|
|
||||||
border: 1px solid #ffd33d;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 12px;
|
|
||||||
margin: 12px 0;
|
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) {
|
|
||||||
width: 93%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,12 +129,14 @@
|
|||||||
&__networks-list {
|
&__networks-list {
|
||||||
flex: 0.5 0 auto;
|
flex: 0.5 0 auto;
|
||||||
max-width: 343px;
|
max-width: 343px;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) {
|
@media screen and (max-width: $break-small) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,13 +254,12 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #cdcdcd;
|
color: #cdcdcd;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.network-form {
|
&__network-form-footer {
|
||||||
&__footer {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
padding: 0.75rem 0;
|
padding: 0.75rem 0;
|
||||||
|
width: 95%;
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) {
|
@media screen and (max-width: $break-small) {
|
||||||
width: 93%;
|
width: 93%;
|
||||||
@ -247,4 +277,19 @@
|
|||||||
margin-right: 3.75rem;
|
margin-right: 3.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__add-network-form-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
width: 60%;
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { default } from './network-form.component';
|
|
@ -1,76 +0,0 @@
|
|||||||
.add-network-form {
|
|
||||||
&__body {
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subheader {
|
|
||||||
@include H4;
|
|
||||||
|
|
||||||
padding: 16px 4px;
|
|
||||||
border-bottom: 1px solid $alto;
|
|
||||||
height: 72px;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subheader--break {
|
|
||||||
margin-inline-start: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sub-header-text {
|
|
||||||
@include H4;
|
|
||||||
|
|
||||||
color: $ui-4;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
justify-content: space-between;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&--warning {
|
|
||||||
@include H7;
|
|
||||||
|
|
||||||
background-color: $Yellow-000;
|
|
||||||
border: 1px solid $alert-1;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 12px;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__form-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__form-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__network-form-row {
|
|
||||||
padding-bottom: 30px;
|
|
||||||
width: 48%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
padding: 0 0 0.75rem 0;
|
|
||||||
width: 60%;
|
|
||||||
|
|
||||||
&-cancel-button {
|
|
||||||
margin-right: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-submit-button {
|
|
||||||
margin-left: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,761 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import validUrl from 'valid-url';
|
|
||||||
import log from 'loglevel';
|
|
||||||
import TextField from '../../../../components/ui/text-field';
|
|
||||||
import Button from '../../../../components/ui/button';
|
|
||||||
import Tooltip from '../../../../components/ui/tooltip';
|
|
||||||
import {
|
|
||||||
isPrefixedFormattedHexString,
|
|
||||||
isSafeChainId,
|
|
||||||
} from '../../../../../shared/modules/network.utils';
|
|
||||||
import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
|
|
||||||
import { decimalToHex } from '../../../../helpers/utils/conversions.util';
|
|
||||||
|
|
||||||
const FORM_STATE_KEYS = [
|
|
||||||
'rpcUrl',
|
|
||||||
'chainId',
|
|
||||||
'ticker',
|
|
||||||
'networkName',
|
|
||||||
'blockExplorerUrl',
|
|
||||||
];
|
|
||||||
|
|
||||||
export default class NetworkForm extends PureComponent {
|
|
||||||
static contextTypes = {
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
metricsEvent: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
editRpc: PropTypes.func,
|
|
||||||
showConfirmDeleteNetworkModal: PropTypes.func,
|
|
||||||
rpcUrl: PropTypes.string,
|
|
||||||
chainId: PropTypes.string,
|
|
||||||
ticker: PropTypes.string,
|
|
||||||
viewOnly: PropTypes.bool,
|
|
||||||
networkName: PropTypes.string,
|
|
||||||
onClear: PropTypes.func.isRequired,
|
|
||||||
setRpcTarget: PropTypes.func.isRequired,
|
|
||||||
isCurrentRpcTarget: PropTypes.bool,
|
|
||||||
blockExplorerUrl: PropTypes.string,
|
|
||||||
rpcPrefs: PropTypes.object,
|
|
||||||
networksToRender: PropTypes.array.isRequired,
|
|
||||||
onAddNetwork: PropTypes.func,
|
|
||||||
setNewNetworkAdded: PropTypes.func,
|
|
||||||
addNewNetwork: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
rpcUrl: '',
|
|
||||||
chainId: '',
|
|
||||||
ticker: '',
|
|
||||||
networkName: '',
|
|
||||||
blockExplorerUrl: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
rpcUrl: this.props.rpcUrl,
|
|
||||||
chainId: this.getDisplayChainId(this.props.chainId),
|
|
||||||
ticker: this.props.ticker,
|
|
||||||
networkName: this.props.networkName,
|
|
||||||
blockExplorerUrl: this.props.blockExplorerUrl,
|
|
||||||
errors: {},
|
|
||||||
isSubmitting: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { addNewNetwork: prevAddMode } = prevProps;
|
|
||||||
const { addNewNetwork } = this.props;
|
|
||||||
|
|
||||||
if (!prevAddMode && addNewNetwork) {
|
|
||||||
this.setState({
|
|
||||||
rpcUrl: '',
|
|
||||||
chainId: '',
|
|
||||||
ticker: '',
|
|
||||||
networkName: '',
|
|
||||||
blockExplorerUrl: '',
|
|
||||||
errors: {},
|
|
||||||
isSubmitting: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for (const key of FORM_STATE_KEYS) {
|
|
||||||
if (prevProps[key] !== this.props[key]) {
|
|
||||||
this.resetForm();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.setState({
|
|
||||||
rpcUrl: '',
|
|
||||||
chainId: '',
|
|
||||||
ticker: '',
|
|
||||||
networkName: '',
|
|
||||||
blockExplorerUrl: '',
|
|
||||||
errors: {},
|
|
||||||
});
|
|
||||||
// onClear will push the network settings route unless was pass false.
|
|
||||||
// Since we call onClear to cause this component to be unmounted, the
|
|
||||||
// route will already have been updated, and we avoid setting it twice.
|
|
||||||
this.props.onClear(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetForm() {
|
|
||||||
const {
|
|
||||||
rpcUrl,
|
|
||||||
chainId,
|
|
||||||
ticker,
|
|
||||||
networkName,
|
|
||||||
blockExplorerUrl,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
rpcUrl,
|
|
||||||
chainId: this.getDisplayChainId(chainId),
|
|
||||||
ticker,
|
|
||||||
networkName,
|
|
||||||
blockExplorerUrl,
|
|
||||||
errors: {},
|
|
||||||
isSubmitting: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to convert the given chainId to a decimal string, for display
|
|
||||||
* purposes.
|
|
||||||
*
|
|
||||||
* Should be called with the props chainId whenever it is used to set the
|
|
||||||
* component's state.
|
|
||||||
*
|
|
||||||
* @param {unknown} chainId - The chainId to convert.
|
|
||||||
* @returns {string} The props chainId in decimal, or the original value if
|
|
||||||
* it can't be converted.
|
|
||||||
*/
|
|
||||||
getDisplayChainId(chainId) {
|
|
||||||
if (!chainId || typeof chainId !== 'string' || !chainId.startsWith('0x')) {
|
|
||||||
return chainId;
|
|
||||||
}
|
|
||||||
return parseInt(chainId, 16).toString(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefixes a given id with '0x' if the prefix does not exist
|
|
||||||
*
|
|
||||||
* @param {string} chainId - The chainId to prefix
|
|
||||||
* @returns {string} The chainId, prefixed with '0x'
|
|
||||||
*/
|
|
||||||
prefixChainId(chainId) {
|
|
||||||
let prefixedChainId = chainId;
|
|
||||||
if (!chainId.startsWith('0x')) {
|
|
||||||
prefixedChainId = `0x${parseInt(chainId, 10).toString(16)}`;
|
|
||||||
}
|
|
||||||
return prefixedChainId;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit = async () => {
|
|
||||||
this.setState({
|
|
||||||
isSubmitting: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
setRpcTarget,
|
|
||||||
rpcUrl: propsRpcUrl,
|
|
||||||
editRpc,
|
|
||||||
rpcPrefs = {},
|
|
||||||
onAddNetwork,
|
|
||||||
setNewNetworkAdded,
|
|
||||||
addNewNetwork,
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
networkName,
|
|
||||||
rpcUrl,
|
|
||||||
chainId: stateChainId,
|
|
||||||
ticker,
|
|
||||||
blockExplorerUrl,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const formChainId = stateChainId.trim().toLowerCase();
|
|
||||||
const chainId = this.prefixChainId(formChainId);
|
|
||||||
|
|
||||||
if (!(await this.validateChainIdOnSubmit(formChainId, chainId, rpcUrl))) {
|
|
||||||
this.setState({
|
|
||||||
isSubmitting: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// After this point, isSubmitting will be reset in componentDidUpdate
|
|
||||||
if (propsRpcUrl && rpcUrl !== propsRpcUrl) {
|
|
||||||
await editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, {
|
|
||||||
...rpcPrefs,
|
|
||||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await setRpcTarget(rpcUrl, chainId, ticker, networkName, {
|
|
||||||
...rpcPrefs,
|
|
||||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addNewNetwork) {
|
|
||||||
setNewNetworkAdded(networkName);
|
|
||||||
onAddNetwork();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.setState({
|
|
||||||
isSubmitting: false,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancel = () => {
|
|
||||||
const { addNewNetwork, onClear } = this.props;
|
|
||||||
|
|
||||||
if (addNewNetwork) {
|
|
||||||
onClear();
|
|
||||||
} else {
|
|
||||||
this.resetForm();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onDelete = () => {
|
|
||||||
const { showConfirmDeleteNetworkModal, rpcUrl, onClear } = this.props;
|
|
||||||
showConfirmDeleteNetworkModal({
|
|
||||||
target: rpcUrl,
|
|
||||||
onConfirm: () => {
|
|
||||||
this.resetForm();
|
|
||||||
onClear();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
isSubmitting() {
|
|
||||||
return this.state.isSubmitting;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateIsUnchanged() {
|
|
||||||
const {
|
|
||||||
rpcUrl,
|
|
||||||
chainId: propsChainId,
|
|
||||||
ticker,
|
|
||||||
networkName,
|
|
||||||
blockExplorerUrl,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
rpcUrl: stateRpcUrl,
|
|
||||||
chainId: stateChainId,
|
|
||||||
ticker: stateTicker,
|
|
||||||
networkName: stateNetworkName,
|
|
||||||
blockExplorerUrl: stateBlockExplorerUrl,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
// These added conditions are in case the saved chainId is invalid, which
|
|
||||||
// was possible in versions <8.1 of the extension.
|
|
||||||
// Basically, we always want to be able to overwrite an invalid chain ID.
|
|
||||||
const chainIdIsUnchanged =
|
|
||||||
typeof propsChainId === 'string' &&
|
|
||||||
propsChainId.toLowerCase().startsWith('0x') &&
|
|
||||||
stateChainId === this.getDisplayChainId(propsChainId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
stateRpcUrl === rpcUrl &&
|
|
||||||
chainIdIsUnchanged &&
|
|
||||||
stateTicker === ticker &&
|
|
||||||
stateNetworkName === networkName &&
|
|
||||||
stateBlockExplorerUrl === blockExplorerUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFormTextField({
|
|
||||||
className,
|
|
||||||
fieldKey,
|
|
||||||
textFieldId,
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
optionalTextFieldKey,
|
|
||||||
tooltipText,
|
|
||||||
autoFocus = false,
|
|
||||||
}) {
|
|
||||||
const { errors } = this.state;
|
|
||||||
const { viewOnly } = this.props;
|
|
||||||
const errorMessage = errors[fieldKey]?.msg || '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className="networks-tab__network-form-label">
|
|
||||||
<div className="networks-tab__network-form-label-text">
|
|
||||||
{this.context.t(optionalTextFieldKey || fieldKey)}
|
|
||||||
</div>
|
|
||||||
{!viewOnly && tooltipText ? (
|
|
||||||
<Tooltip
|
|
||||||
position="top"
|
|
||||||
title={tooltipText}
|
|
||||||
wrapperClassName="networks-tab__network-form-label-tooltip"
|
|
||||||
>
|
|
||||||
<i className="fa fa-info-circle" />
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<TextField
|
|
||||||
type="text"
|
|
||||||
id={textFieldId}
|
|
||||||
onChange={onChange}
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
value={value}
|
|
||||||
disabled={viewOnly}
|
|
||||||
error={errorMessage}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStateWithValue = (stateKey, validator) => {
|
|
||||||
return (e) => {
|
|
||||||
validator?.(e.target.value, stateKey);
|
|
||||||
this.setState({ [stateKey]: e.target.value });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
setErrorTo = (errorKey, errorVal) => {
|
|
||||||
this.setState({
|
|
||||||
errors: {
|
|
||||||
...this.state.errors,
|
|
||||||
[errorKey]: errorVal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setErrorEmpty = (errorKey) => {
|
|
||||||
this.setState({
|
|
||||||
errors: {
|
|
||||||
...this.state.errors,
|
|
||||||
[errorKey]: {
|
|
||||||
msg: '',
|
|
||||||
key: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
hasError = (errorKey, errorKeyVal) => {
|
|
||||||
return this.state.errors[errorKey]?.key === errorKeyVal;
|
|
||||||
};
|
|
||||||
|
|
||||||
hasErrors = () => {
|
|
||||||
const { errors } = this.state;
|
|
||||||
return Object.keys(errors).some((key) => {
|
|
||||||
const error = errors[key];
|
|
||||||
// Do not factor in duplicate chain id error for submission disabling
|
|
||||||
if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return error.key && error.msg;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
validateChainIdOnChange = (selfRpcUrl, chainIdArg = '') => {
|
|
||||||
const { t } = this.context;
|
|
||||||
const { networksToRender } = this.props;
|
|
||||||
const chainId = chainIdArg.trim();
|
|
||||||
|
|
||||||
let errorKey = '';
|
|
||||||
let errorMessage = '';
|
|
||||||
let radix = 10;
|
|
||||||
let hexChainId = chainId;
|
|
||||||
|
|
||||||
if (!hexChainId.startsWith('0x')) {
|
|
||||||
try {
|
|
||||||
hexChainId = `0x${decimalToHex(hexChainId)}`;
|
|
||||||
} catch (err) {
|
|
||||||
this.setErrorTo('chainId', {
|
|
||||||
key: 'invalidHexNumber',
|
|
||||||
msg: t('invalidHexNumber'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [matchingChainId] = networksToRender.filter(
|
|
||||||
(e) => e.chainId === hexChainId && e.rpcUrl !== selfRpcUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (chainId === '') {
|
|
||||||
this.setErrorEmpty('chainId');
|
|
||||||
return;
|
|
||||||
} else if (matchingChainId) {
|
|
||||||
errorKey = 'chainIdExistsErrorMsg';
|
|
||||||
errorMessage = t('chainIdExistsErrorMsg', [
|
|
||||||
matchingChainId.label ?? matchingChainId.labelKey,
|
|
||||||
]);
|
|
||||||
} else if (chainId.startsWith('0x')) {
|
|
||||||
radix = 16;
|
|
||||||
if (!/^0x[0-9a-f]+$/iu.test(chainId)) {
|
|
||||||
errorKey = 'invalidHexNumber';
|
|
||||||
errorMessage = t('invalidHexNumber');
|
|
||||||
} else if (!isPrefixedFormattedHexString(chainId)) {
|
|
||||||
errorMessage = t('invalidHexNumberLeadingZeros');
|
|
||||||
}
|
|
||||||
} else if (!/^[0-9]+$/u.test(chainId)) {
|
|
||||||
errorKey = 'invalidNumber';
|
|
||||||
errorMessage = t('invalidNumber');
|
|
||||||
} else if (chainId.startsWith('0')) {
|
|
||||||
errorKey = 'invalidNumberLeadingZeros';
|
|
||||||
errorMessage = t('invalidNumberLeadingZeros');
|
|
||||||
} else if (!isSafeChainId(parseInt(chainId, radix))) {
|
|
||||||
errorKey = 'invalidChainIdTooBig';
|
|
||||||
errorMessage = t('invalidChainIdTooBig');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setErrorTo('chainId', {
|
|
||||||
key: errorKey,
|
|
||||||
msg: errorMessage,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the chain ID by checking it against the `eth_chainId` return
|
|
||||||
* value from the given RPC URL.
|
|
||||||
* Assumes that all strings are non-empty and correctly formatted.
|
|
||||||
*
|
|
||||||
* @param {string} formChainId - Non-empty, hex or decimal number string from
|
|
||||||
* the form.
|
|
||||||
* @param {string} parsedChainId - The parsed, hex string chain ID.
|
|
||||||
* @param {string} rpcUrl - The RPC URL from the form.
|
|
||||||
*/
|
|
||||||
validateChainIdOnSubmit = async (formChainId, parsedChainId, rpcUrl) => {
|
|
||||||
const { t } = this.context;
|
|
||||||
let errorKey;
|
|
||||||
let errorMessage;
|
|
||||||
let endpointChainId;
|
|
||||||
let providerError;
|
|
||||||
|
|
||||||
try {
|
|
||||||
endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId');
|
|
||||||
} catch (err) {
|
|
||||||
log.warn('Failed to fetch the chainId from the endpoint.', err);
|
|
||||||
providerError = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerError || typeof endpointChainId !== 'string') {
|
|
||||||
errorKey = 'failedToFetchChainId';
|
|
||||||
errorMessage = t('failedToFetchChainId');
|
|
||||||
} else if (parsedChainId !== endpointChainId) {
|
|
||||||
// Here, we are in an error state. The endpoint should always return a
|
|
||||||
// hexadecimal string. If the user entered a decimal string, we attempt
|
|
||||||
// to convert the endpoint's return value to decimal before rendering it
|
|
||||||
// in an error message in the form.
|
|
||||||
if (!formChainId.startsWith('0x')) {
|
|
||||||
try {
|
|
||||||
endpointChainId = parseInt(endpointChainId, 16).toString(10);
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(
|
|
||||||
'Failed to convert endpoint chain ID to decimal',
|
|
||||||
endpointChainId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorKey = 'endpointReturnedDifferentChainId';
|
|
||||||
errorMessage = t('endpointReturnedDifferentChainId', [
|
|
||||||
endpointChainId.length <= 12
|
|
||||||
? endpointChainId
|
|
||||||
: `${endpointChainId.slice(0, 9)}...`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorKey) {
|
|
||||||
this.setErrorTo('chainId', {
|
|
||||||
key: errorKey,
|
|
||||||
msg: errorMessage,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setErrorEmpty('chainId');
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
isValidWhenAppended = (url) => {
|
|
||||||
const appendedRpc = `http://${url}`;
|
|
||||||
return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u);
|
|
||||||
};
|
|
||||||
|
|
||||||
validateBlockExplorerURL = (url, stateKey) => {
|
|
||||||
const { t } = this.context;
|
|
||||||
if (!validUrl.isWebUri(url) && url !== '') {
|
|
||||||
let errorKey;
|
|
||||||
let errorMessage;
|
|
||||||
|
|
||||||
if (this.isValidWhenAppended(url)) {
|
|
||||||
errorKey = 'urlErrorMsg';
|
|
||||||
errorMessage = t('urlErrorMsg');
|
|
||||||
} else {
|
|
||||||
errorKey = 'invalidBlockExplorerURL';
|
|
||||||
errorMessage = t('invalidBlockExplorerURL');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setErrorTo(stateKey, {
|
|
||||||
key: errorKey,
|
|
||||||
msg: errorMessage,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setErrorEmpty(stateKey);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validateUrlRpcUrl = (url, stateKey) => {
|
|
||||||
const { t } = this.context;
|
|
||||||
const { networksToRender } = this.props;
|
|
||||||
const { chainId: stateChainId } = this.state;
|
|
||||||
const isValidUrl = validUrl.isWebUri(url);
|
|
||||||
const chainIdFetchFailed = this.hasError('chainId', 'failedToFetchChainId');
|
|
||||||
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
|
|
||||||
|
|
||||||
if (!isValidUrl && url !== '') {
|
|
||||||
let errorKey;
|
|
||||||
let errorMessage;
|
|
||||||
if (this.isValidWhenAppended(url)) {
|
|
||||||
errorKey = 'urlErrorMsg';
|
|
||||||
errorMessage = t('urlErrorMsg');
|
|
||||||
} else {
|
|
||||||
errorKey = 'invalidRPC';
|
|
||||||
errorMessage = t('invalidRPC');
|
|
||||||
}
|
|
||||||
this.setErrorTo(stateKey, {
|
|
||||||
key: errorKey,
|
|
||||||
msg: errorMessage,
|
|
||||||
});
|
|
||||||
} else if (matchingRPCUrl) {
|
|
||||||
this.setErrorTo(stateKey, {
|
|
||||||
key: 'urlExistsErrorMsg',
|
|
||||||
msg: t('urlExistsErrorMsg', [
|
|
||||||
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setErrorEmpty(stateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-validate the chain id if it could not be found with previous rpc url
|
|
||||||
if (stateChainId && isValidUrl && chainIdFetchFailed) {
|
|
||||||
const formChainId = stateChainId.trim().toLowerCase();
|
|
||||||
const chainId = this.prefixChainId(formChainId);
|
|
||||||
this.validateChainIdOnSubmit(formChainId, chainId, url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderAddNetworkForm() {
|
|
||||||
const { t } = this.context;
|
|
||||||
const {
|
|
||||||
networkName,
|
|
||||||
rpcUrl,
|
|
||||||
chainId = '',
|
|
||||||
ticker,
|
|
||||||
blockExplorerUrl,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isSubmitDisabled =
|
|
||||||
this.hasErrors() || this.isSubmitting() || !rpcUrl || !chainId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="add-network-form__body">
|
|
||||||
<div className="add-network-form__subheader">
|
|
||||||
<span className="add-network-form__sub-header-text">
|
|
||||||
{t('networks')}
|
|
||||||
</span>
|
|
||||||
<span>{' > '}</span>
|
|
||||||
<div className="add-network-form__subheader--break">
|
|
||||||
{t('addANetwork')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="add-network-form__content">
|
|
||||||
<div className="add-network-form__content--warning">
|
|
||||||
{t('onlyAddTrustedNetworks')}
|
|
||||||
</div>
|
|
||||||
<div className="add-network-form__form-column">
|
|
||||||
<div className="add-network-form__form-row">
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'add-network-form__network-form-row',
|
|
||||||
fieldKey: 'networkName',
|
|
||||||
textFieldId: 'network-name',
|
|
||||||
onChange: this.setStateWithValue('networkName'),
|
|
||||||
value: networkName,
|
|
||||||
autoFocus: true,
|
|
||||||
})}
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'add-network-form__network-form-row',
|
|
||||||
fieldKey: 'rpcUrl',
|
|
||||||
textFieldId: 'rpc-url',
|
|
||||||
onChange: this.setStateWithValue(
|
|
||||||
'rpcUrl',
|
|
||||||
this.validateUrlRpcUrl,
|
|
||||||
),
|
|
||||||
value: rpcUrl,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="add-network-form__form-row">
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'add-network-form__network-form-row',
|
|
||||||
fieldKey: 'chainId',
|
|
||||||
textFieldId: 'chainId',
|
|
||||||
onChange: this.setStateWithValue(
|
|
||||||
'chainId',
|
|
||||||
this.validateChainIdOnChange.bind(this, rpcUrl),
|
|
||||||
),
|
|
||||||
value: chainId,
|
|
||||||
tooltipText: t('networkSettingsChainIdDescription'),
|
|
||||||
})}
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'add-network-form__network-form-row',
|
|
||||||
fieldKey: 'symbol',
|
|
||||||
textFieldId: 'network-ticker',
|
|
||||||
onChange: this.setStateWithValue('ticker'),
|
|
||||||
value: ticker,
|
|
||||||
optionalTextFieldKey: 'optionalCurrencySymbol',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="add-network-form__form-row">
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'add-network-form__network-form-row',
|
|
||||||
fieldKey: 'blockExplorerUrl',
|
|
||||||
textFieldId: 'block-explorer-url',
|
|
||||||
onChange: this.setStateWithValue(
|
|
||||||
'blockExplorerUrl',
|
|
||||||
this.validateBlockExplorerURL,
|
|
||||||
),
|
|
||||||
value: blockExplorerUrl,
|
|
||||||
optionalTextFieldKey: 'optionalBlockExplorerUrl',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="add-network-form__footer">
|
|
||||||
<Button
|
|
||||||
type="secondary"
|
|
||||||
onClick={this.onCancel}
|
|
||||||
className="add-network-form__footer-cancel-button"
|
|
||||||
>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
disabled={isSubmitDisabled}
|
|
||||||
onClick={this.onSubmit}
|
|
||||||
className="add-network-form__footer-submit-button"
|
|
||||||
>
|
|
||||||
{t('save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNetworkForm() {
|
|
||||||
const { t } = this.context;
|
|
||||||
const { viewOnly, isCurrentRpcTarget } = this.props;
|
|
||||||
const {
|
|
||||||
networkName,
|
|
||||||
rpcUrl,
|
|
||||||
chainId = '',
|
|
||||||
ticker,
|
|
||||||
blockExplorerUrl,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const deletable = !isCurrentRpcTarget && !viewOnly;
|
|
||||||
|
|
||||||
const isSubmitDisabled =
|
|
||||||
this.hasErrors() ||
|
|
||||||
this.isSubmitting() ||
|
|
||||||
this.stateIsUnchanged() ||
|
|
||||||
!rpcUrl ||
|
|
||||||
!chainId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="networks-tab__network-form">
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'networks-tab__network-form-row',
|
|
||||||
fieldKey: 'networkName',
|
|
||||||
textFieldId: 'network-name',
|
|
||||||
onChange: this.setStateWithValue('networkName'),
|
|
||||||
value: networkName,
|
|
||||||
})}
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'networks-tab__network-form-row',
|
|
||||||
fieldKey: 'rpcUrl',
|
|
||||||
textFieldId: 'rpc-url',
|
|
||||||
onChange: this.setStateWithValue('rpcUrl', this.validateUrlRpcUrl),
|
|
||||||
value: rpcUrl,
|
|
||||||
})}
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'networks-tab__network-form-row',
|
|
||||||
fieldKey: 'chainId',
|
|
||||||
textFieldId: 'chainId',
|
|
||||||
onChange: this.setStateWithValue(
|
|
||||||
'chainId',
|
|
||||||
this.validateChainIdOnChange.bind(this, rpcUrl),
|
|
||||||
),
|
|
||||||
value: chainId,
|
|
||||||
tooltipText: viewOnly ? null : t('networkSettingsChainIdDescription'),
|
|
||||||
})}
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'networks-tab__network-form-row',
|
|
||||||
fieldKey: 'symbol',
|
|
||||||
textFieldId: 'network-ticker',
|
|
||||||
onChange: this.setStateWithValue('ticker'),
|
|
||||||
value: ticker,
|
|
||||||
optionalTextFieldKey: 'optionalCurrencySymbol',
|
|
||||||
})}
|
|
||||||
{this.renderFormTextField({
|
|
||||||
className: 'networks-tab__network-form-row',
|
|
||||||
fieldKey: 'blockExplorerUrl',
|
|
||||||
textFieldId: 'block-explorer-url',
|
|
||||||
onChange: this.setStateWithValue(
|
|
||||||
'blockExplorerUrl',
|
|
||||||
this.validateBlockExplorerURL,
|
|
||||||
),
|
|
||||||
value: blockExplorerUrl,
|
|
||||||
optionalTextFieldKey: 'optionalBlockExplorerUrl',
|
|
||||||
})}
|
|
||||||
<div className="network-form__footer">
|
|
||||||
{!viewOnly && (
|
|
||||||
<>
|
|
||||||
{deletable && (
|
|
||||||
<Button type="danger" onClick={this.onDelete}>
|
|
||||||
{t('delete')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="secondary"
|
|
||||||
onClick={this.onCancel}
|
|
||||||
disabled={this.stateIsUnchanged()}
|
|
||||||
>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
disabled={isSubmitDisabled}
|
|
||||||
onClick={this.onSubmit}
|
|
||||||
>
|
|
||||||
{t('save')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { addNewNetwork } = this.props;
|
|
||||||
return addNewNetwork
|
|
||||||
? this.renderAddNetworkForm()
|
|
||||||
: this.renderNetworkForm();
|
|
||||||
}
|
|
||||||
}
|
|
1
ui/pages/settings/networks-tab/networks-form/index.js
Normal file
1
ui/pages/settings/networks-tab/networks-form/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './networks-form';
|
575
ui/pages/settings/networks-tab/networks-form/networks-form.js
Normal file
575
ui/pages/settings/networks-tab/networks-form/networks-form.js
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import validUrl from 'valid-url';
|
||||||
|
import log from 'loglevel';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
import {
|
||||||
|
isPrefixedFormattedHexString,
|
||||||
|
isSafeChainId,
|
||||||
|
} from '../../../../../shared/modules/network.utils';
|
||||||
|
import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
|
||||||
|
import ActionableMessage from '../../../../components/ui/actionable-message';
|
||||||
|
import Button from '../../../../components/ui/button';
|
||||||
|
import FormField from '../../../../components/ui/form-field';
|
||||||
|
import { decimalToHex } from '../../../../helpers/utils/conversions.util';
|
||||||
|
import {
|
||||||
|
setSelectedSettingsRpcUrl,
|
||||||
|
updateAndSetCustomRpc,
|
||||||
|
editRpc,
|
||||||
|
showModal,
|
||||||
|
setNewNetworkAdded,
|
||||||
|
} from '../../../../store/actions';
|
||||||
|
import {
|
||||||
|
DEFAULT_ROUTE,
|
||||||
|
NETWORKS_ROUTE,
|
||||||
|
} from '../../../../helpers/constants/routes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to convert the given chainId to a decimal string, for display
|
||||||
|
* purposes.
|
||||||
|
*
|
||||||
|
* Should be called with the props chainId whenever it is used to set the
|
||||||
|
* component's state.
|
||||||
|
*
|
||||||
|
* @param {unknown} chainId - The chainId to convert.
|
||||||
|
* @returns {string} The props chainId in decimal, or the original value if
|
||||||
|
* it can't be converted.
|
||||||
|
*/
|
||||||
|
const getDisplayChainId = (chainId) => {
|
||||||
|
if (!chainId || typeof chainId !== 'string' || !chainId.startsWith('0x')) {
|
||||||
|
return chainId;
|
||||||
|
}
|
||||||
|
return parseInt(chainId, 16).toString(10);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefixes a given id with '0x' if the prefix does not exist
|
||||||
|
*
|
||||||
|
* @param {string} chainId - The chainId to prefix
|
||||||
|
* @returns {string} The chainId, prefixed with '0x'
|
||||||
|
*/
|
||||||
|
const prefixChainId = (chainId) => {
|
||||||
|
let prefixedChainId = chainId;
|
||||||
|
if (!chainId.startsWith('0x')) {
|
||||||
|
prefixedChainId = `0x${parseInt(chainId, 10).toString(16)}`;
|
||||||
|
}
|
||||||
|
return prefixedChainId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidWhenAppended = (url) => {
|
||||||
|
const appendedRpc = `http://${url}`;
|
||||||
|
return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NetworksForm = ({
|
||||||
|
addNewNetwork,
|
||||||
|
isCurrentRpcTarget,
|
||||||
|
networksToRender,
|
||||||
|
selectedNetwork,
|
||||||
|
}) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const history = useHistory();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { label, labelKey, viewOnly, rpcPrefs } = selectedNetwork;
|
||||||
|
const selectedNetworkName = label || (labelKey && t(labelKey));
|
||||||
|
const [networkName, setNetworkName] = useState(selectedNetworkName || '');
|
||||||
|
const [rpcUrl, setRpcUrl] = useState(selectedNetwork?.rpcUrl || '');
|
||||||
|
const [chainId, setChainId] = useState(selectedNetwork?.chainId || '');
|
||||||
|
const [ticker, setTicker] = useState(selectedNetwork?.ticker || '');
|
||||||
|
const [blockExplorerUrl, setBlockExplorerUrl] = useState(
|
||||||
|
selectedNetwork?.blockExplorerUrl || '',
|
||||||
|
);
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const resetForm = useCallback(() => {
|
||||||
|
setNetworkName(selectedNetworkName || '');
|
||||||
|
setRpcUrl(selectedNetwork.rpcUrl);
|
||||||
|
setChainId(getDisplayChainId(selectedNetwork.chainId));
|
||||||
|
setTicker(selectedNetwork?.ticker);
|
||||||
|
setBlockExplorerUrl(selectedNetwork?.blockExplorerUrl);
|
||||||
|
setErrors({});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}, [selectedNetwork, selectedNetworkName]);
|
||||||
|
|
||||||
|
const stateIsUnchanged = () => {
|
||||||
|
// These added conditions are in case the saved chainId is invalid, which
|
||||||
|
// was possible in versions <8.1 of the extension.
|
||||||
|
// Basically, we always want to be able to overwrite an invalid chain ID.
|
||||||
|
const chainIdIsUnchanged =
|
||||||
|
typeof selectedNetwork.chainId === 'string' &&
|
||||||
|
selectedNetwork.chainId.toLowerCase().startsWith('0x') &&
|
||||||
|
chainId === getDisplayChainId(selectedNetwork.chainId);
|
||||||
|
return (
|
||||||
|
rpcUrl === selectedNetwork.rpcUrl &&
|
||||||
|
chainIdIsUnchanged &&
|
||||||
|
ticker === selectedNetwork.ticker &&
|
||||||
|
networkName === selectedNetworkName &&
|
||||||
|
blockExplorerUrl === selectedNetwork.blockExplorerUrl
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAddNewNetwork = useRef();
|
||||||
|
const prevNetworkName = useRef();
|
||||||
|
const prevChainId = useRef();
|
||||||
|
const prevRpcUrl = useRef();
|
||||||
|
const prevTicker = useRef();
|
||||||
|
const prevBlockExplorerUrl = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!prevAddNewNetwork.current && addNewNetwork) {
|
||||||
|
setNetworkName('');
|
||||||
|
setRpcUrl('');
|
||||||
|
setChainId('');
|
||||||
|
setTicker('');
|
||||||
|
setBlockExplorerUrl('');
|
||||||
|
setErrors({});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
} else if (
|
||||||
|
prevNetworkName.current !== selectedNetworkName ||
|
||||||
|
prevRpcUrl.current !== selectedNetwork.rpcUrl ||
|
||||||
|
prevChainId.current !== selectedNetwork.chainId ||
|
||||||
|
prevTicker.current !== selectedNetwork.ticker ||
|
||||||
|
prevBlockExplorerUrl.current !== selectedNetwork.blockExplorerUrl
|
||||||
|
) {
|
||||||
|
resetForm(selectedNetwork);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
selectedNetwork,
|
||||||
|
selectedNetworkName,
|
||||||
|
addNewNetwork,
|
||||||
|
setNetworkName,
|
||||||
|
setRpcUrl,
|
||||||
|
setChainId,
|
||||||
|
setTicker,
|
||||||
|
setBlockExplorerUrl,
|
||||||
|
setErrors,
|
||||||
|
setIsSubmitting,
|
||||||
|
resetForm,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setNetworkName('');
|
||||||
|
setRpcUrl('');
|
||||||
|
setChainId('');
|
||||||
|
setTicker('');
|
||||||
|
setBlockExplorerUrl('');
|
||||||
|
setErrors({});
|
||||||
|
dispatch(setSelectedSettingsRpcUrl(''));
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
setNetworkName,
|
||||||
|
setRpcUrl,
|
||||||
|
setChainId,
|
||||||
|
setTicker,
|
||||||
|
setBlockExplorerUrl,
|
||||||
|
setErrors,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const setErrorTo = (errorKey, errorVal) => {
|
||||||
|
setErrors({ ...errors, [errorKey]: errorVal });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setErrorEmpty = (errorKey) => {
|
||||||
|
setErrors({
|
||||||
|
...errors,
|
||||||
|
[errorKey]: {
|
||||||
|
msg: '',
|
||||||
|
key: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasError = (errorKey, errorKeyVal) => {
|
||||||
|
return errors[errorKey]?.key === errorKeyVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasErrors = () => {
|
||||||
|
return Object.keys(errors).some((key) => {
|
||||||
|
const error = errors[key];
|
||||||
|
// Do not factor in duplicate chain id error for submission disabling
|
||||||
|
if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return error.key && error.msg;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateChainIdOnChange = (chainArg = '') => {
|
||||||
|
const formChainId = chainArg.trim();
|
||||||
|
let errorKey = '';
|
||||||
|
let errorMessage = '';
|
||||||
|
let radix = 10;
|
||||||
|
let hexChainId = formChainId;
|
||||||
|
|
||||||
|
if (!hexChainId.startsWith('0x')) {
|
||||||
|
try {
|
||||||
|
hexChainId = `0x${decimalToHex(hexChainId)}`;
|
||||||
|
} catch (err) {
|
||||||
|
setErrorTo('chainId', {
|
||||||
|
key: 'invalidHexNumber',
|
||||||
|
msg: t('invalidHexNumber'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [matchingChainId] = networksToRender.filter(
|
||||||
|
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (formChainId === '') {
|
||||||
|
setErrorEmpty('chainId');
|
||||||
|
return;
|
||||||
|
} else if (matchingChainId) {
|
||||||
|
errorKey = 'chainIdExistsErrorMsg';
|
||||||
|
errorMessage = t('chainIdExistsErrorMsg', [
|
||||||
|
matchingChainId.label ?? matchingChainId.labelKey,
|
||||||
|
]);
|
||||||
|
} else if (formChainId.startsWith('0x')) {
|
||||||
|
radix = 16;
|
||||||
|
if (!/^0x[0-9a-f]+$/iu.test(formChainId)) {
|
||||||
|
errorKey = 'invalidHexNumber';
|
||||||
|
errorMessage = t('invalidHexNumber');
|
||||||
|
} else if (!isPrefixedFormattedHexString(formChainId)) {
|
||||||
|
errorMessage = t('invalidHexNumberLeadingZeros');
|
||||||
|
}
|
||||||
|
} else if (!/^[0-9]+$/u.test(formChainId)) {
|
||||||
|
errorKey = 'invalidNumber';
|
||||||
|
errorMessage = t('invalidNumber');
|
||||||
|
} else if (formChainId.startsWith('0')) {
|
||||||
|
errorKey = 'invalidNumberLeadingZeros';
|
||||||
|
errorMessage = t('invalidNumberLeadingZeros');
|
||||||
|
} else if (!isSafeChainId(parseInt(formChainId, radix))) {
|
||||||
|
errorKey = 'invalidChainIdTooBig';
|
||||||
|
errorMessage = t('invalidChainIdTooBig');
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorTo('chainId', {
|
||||||
|
key: errorKey,
|
||||||
|
msg: errorMessage,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the chain ID by checking it against the `eth_chainId` return
|
||||||
|
* value from the given RPC URL.
|
||||||
|
* Assumes that all strings are non-empty and correctly formatted.
|
||||||
|
*
|
||||||
|
* @param {string} formChainId - Non-empty, hex or decimal number string from
|
||||||
|
* the form.
|
||||||
|
* @param {string} parsedChainId - The parsed, hex string chain ID.
|
||||||
|
* @param {string} formRpcUrl - The RPC URL from the form.
|
||||||
|
*/
|
||||||
|
const validateChainIdOnSubmit = async (
|
||||||
|
formChainId,
|
||||||
|
parsedChainId,
|
||||||
|
formRpcUrl,
|
||||||
|
) => {
|
||||||
|
let errorKey;
|
||||||
|
let errorMessage;
|
||||||
|
let endpointChainId;
|
||||||
|
let providerError;
|
||||||
|
|
||||||
|
try {
|
||||||
|
endpointChainId = await jsonRpcRequest(formRpcUrl, 'eth_chainId');
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('Failed to fetch the chainId from the endpoint.', err);
|
||||||
|
providerError = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerError || typeof endpointChainId !== 'string') {
|
||||||
|
errorKey = 'failedToFetchChainId';
|
||||||
|
errorMessage = t('failedToFetchChainId');
|
||||||
|
} else if (parsedChainId !== endpointChainId) {
|
||||||
|
// Here, we are in an error state. The endpoint should always return a
|
||||||
|
// hexadecimal string. If the user entered a decimal string, we attempt
|
||||||
|
// to convert the endpoint's return value to decimal before rendering it
|
||||||
|
// in an error message in the form.
|
||||||
|
if (!formChainId.startsWith('0x')) {
|
||||||
|
try {
|
||||||
|
endpointChainId = parseInt(endpointChainId, 16).toString(10);
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(
|
||||||
|
'Failed to convert endpoint chain ID to decimal',
|
||||||
|
endpointChainId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorKey = 'endpointReturnedDifferentChainId';
|
||||||
|
errorMessage = t('endpointReturnedDifferentChainId', [
|
||||||
|
endpointChainId.length <= 12
|
||||||
|
? endpointChainId
|
||||||
|
: `${endpointChainId.slice(0, 9)}...`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorKey) {
|
||||||
|
setErrorTo('chainId', {
|
||||||
|
key: errorKey,
|
||||||
|
msg: errorMessage,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorEmpty('chainId');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateBlockExplorerURL = (url) => {
|
||||||
|
if (!validUrl.isWebUri(url) && url !== '') {
|
||||||
|
let errorKey;
|
||||||
|
let errorMessage;
|
||||||
|
|
||||||
|
if (isValidWhenAppended(url)) {
|
||||||
|
errorKey = 'urlErrorMsg';
|
||||||
|
errorMessage = t('urlErrorMsg');
|
||||||
|
} else {
|
||||||
|
errorKey = 'invalidBlockExplorerURL';
|
||||||
|
errorMessage = t('invalidBlockExplorerURL');
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorTo('blockExplorerUrl', {
|
||||||
|
key: errorKey,
|
||||||
|
msg: errorMessage,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setErrorEmpty('blockExplorerUrl');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateUrlRpcUrl = (url) => {
|
||||||
|
const isValidUrl = validUrl.isWebUri(url);
|
||||||
|
const chainIdFetchFailed = hasError('chainId', 'failedToFetchChainId');
|
||||||
|
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
|
||||||
|
|
||||||
|
if (!isValidUrl && url !== '') {
|
||||||
|
let errorKey;
|
||||||
|
let errorMessage;
|
||||||
|
if (isValidWhenAppended(url)) {
|
||||||
|
errorKey = 'urlErrorMsg';
|
||||||
|
errorMessage = t('urlErrorMsg');
|
||||||
|
} else {
|
||||||
|
errorKey = 'invalidRPC';
|
||||||
|
errorMessage = t('invalidRPC');
|
||||||
|
}
|
||||||
|
setErrorTo('rpcUrl', {
|
||||||
|
key: errorKey,
|
||||||
|
msg: errorMessage,
|
||||||
|
});
|
||||||
|
} else if (matchingRPCUrl) {
|
||||||
|
setErrorTo('rpcUrl', {
|
||||||
|
key: 'urlExistsErrorMsg',
|
||||||
|
msg: t('urlExistsErrorMsg', [
|
||||||
|
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setErrorEmpty('rpcUrl');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-validate the chain id if it could not be found with previous rpc url
|
||||||
|
if (chainId && isValidUrl && chainIdFetchFailed) {
|
||||||
|
const formChainId = chainId.trim().toLowerCase();
|
||||||
|
const prefixedChainId = prefixChainId(formChainId);
|
||||||
|
validateChainIdOnSubmit(formChainId, prefixedChainId, url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const formChainId = chainId.trim().toLowerCase();
|
||||||
|
const prefixedChainId = prefixChainId(formChainId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await validateChainIdOnSubmit(formChainId, prefixedChainId, rpcUrl))
|
||||||
|
) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After this point, isSubmitting will be reset in componentDidUpdate
|
||||||
|
if (selectedNetwork.rpcUrl && rpcUrl !== selectedNetwork.rpcUrl) {
|
||||||
|
await dispatch(
|
||||||
|
editRpc(
|
||||||
|
selectedNetwork.rpcUrl,
|
||||||
|
rpcUrl,
|
||||||
|
prefixedChainId,
|
||||||
|
ticker,
|
||||||
|
networkName,
|
||||||
|
{
|
||||||
|
...rpcPrefs,
|
||||||
|
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await dispatch(
|
||||||
|
updateAndSetCustomRpc(rpcUrl, prefixedChainId, ticker, networkName, {
|
||||||
|
...rpcPrefs,
|
||||||
|
blockExplorerUrl: blockExplorerUrl || rpcPrefs?.blockExplorerUrl,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addNewNetwork) {
|
||||||
|
dispatch(setNewNetworkAdded(networkName));
|
||||||
|
history.push(DEFAULT_ROUTE);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
if (addNewNetwork) {
|
||||||
|
dispatch(setSelectedSettingsRpcUrl(''));
|
||||||
|
history.push(NETWORKS_ROUTE);
|
||||||
|
} else {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
dispatch(
|
||||||
|
showModal({
|
||||||
|
name: 'CONFIRM_DELETE_NETWORK',
|
||||||
|
target: selectedNetwork.rpcUrl,
|
||||||
|
onConfirm: () => {
|
||||||
|
resetForm();
|
||||||
|
dispatch(setSelectedSettingsRpcUrl(''));
|
||||||
|
history.push(NETWORKS_ROUTE);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const deletable = !isCurrentRpcTarget && !viewOnly && !addNewNetwork;
|
||||||
|
const stateUnchanged = stateIsUnchanged();
|
||||||
|
const isSubmitDisabled =
|
||||||
|
hasErrors() || isSubmitting || stateUnchanged || !rpcUrl || !chainId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames({
|
||||||
|
'networks-tab__network-form': !addNewNetwork,
|
||||||
|
'networks-tab__add-network-form': addNewNetwork,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{addNewNetwork ? (
|
||||||
|
<ActionableMessage
|
||||||
|
type="warning"
|
||||||
|
message={t('onlyAddTrustedNetworks')}
|
||||||
|
iconFillColor="#f8c000"
|
||||||
|
useIcon
|
||||||
|
withRightButton
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className={classnames({
|
||||||
|
'networks-tab__network-form-body': !addNewNetwork,
|
||||||
|
'networks-tab__network-form-body__view-only': viewOnly,
|
||||||
|
'networks-tab__add-network-form-body': addNewNetwork,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
autoFocus
|
||||||
|
error={errors.networkName?.msg || ''}
|
||||||
|
onChange={setNetworkName}
|
||||||
|
titleText={t('networkName')}
|
||||||
|
value={networkName}
|
||||||
|
disabled={viewOnly}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
error={errors.rpcUrl?.msg || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
setRpcUrl(value);
|
||||||
|
validateUrlRpcUrl(value);
|
||||||
|
}}
|
||||||
|
titleText={t('rpcUrl')}
|
||||||
|
value={rpcUrl}
|
||||||
|
disabled={viewOnly}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
error={errors.chainId?.msg || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
setChainId(value);
|
||||||
|
validateChainIdOnChange(value);
|
||||||
|
}}
|
||||||
|
titleText={t('chainId')}
|
||||||
|
value={chainId}
|
||||||
|
disabled={viewOnly}
|
||||||
|
tooltipText={viewOnly ? null : t('networkSettingsChainIdDescription')}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
error={errors.ticker?.msg || ''}
|
||||||
|
onChange={setTicker}
|
||||||
|
titleText={t('currencySymbol')}
|
||||||
|
titleUnit={t('optionalWithParanthesis')}
|
||||||
|
value={ticker}
|
||||||
|
disabled={viewOnly}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
error={errors.blockExplorerUrl?.msg || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
setBlockExplorerUrl(value);
|
||||||
|
validateBlockExplorerURL(value);
|
||||||
|
}}
|
||||||
|
titleText={t('blockExplorerUrl')}
|
||||||
|
titleUnit={t('optionalWithParanthesis')}
|
||||||
|
value={blockExplorerUrl}
|
||||||
|
disabled={viewOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classnames({
|
||||||
|
'networks-tab__network-form-footer': !addNewNetwork,
|
||||||
|
'networks-tab__add-network-form-footer': addNewNetwork,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!viewOnly && (
|
||||||
|
<>
|
||||||
|
{deletable && (
|
||||||
|
<Button type="danger" onClick={onDelete}>
|
||||||
|
{t('delete')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={stateUnchanged}
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
{t('save')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworksForm.propTypes = {
|
||||||
|
addNewNetwork: PropTypes.bool,
|
||||||
|
isCurrentRpcTarget: PropTypes.bool,
|
||||||
|
networksToRender: PropTypes.array.isRequired,
|
||||||
|
selectedNetwork: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworksForm.defaultProps = {
|
||||||
|
selectedNetwork: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworksForm;
|
@ -3,11 +3,11 @@ import configureMockStore from 'redux-mock-store';
|
|||||||
import { fireEvent } from '@testing-library/react';
|
import { fireEvent } from '@testing-library/react';
|
||||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||||
import { defaultNetworksData } from '../networks-tab.constants';
|
import { defaultNetworksData } from '../networks-tab.constants';
|
||||||
import NetworkForm from '.';
|
import NetworksForm from '.';
|
||||||
|
|
||||||
const renderComponent = (props) => {
|
const renderComponent = (props) => {
|
||||||
const store = configureMockStore([])({ metamask: {} });
|
const store = configureMockStore([])({ metamask: {} });
|
||||||
return renderWithProvider(<NetworkForm {...props} />, store);
|
return renderWithProvider(<NetworksForm {...props} />, store);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||||
@ -16,41 +16,39 @@ const defaultNetworks = defaultNetworksData.map((network) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const propNewNetwork = {
|
const propNewNetwork = {
|
||||||
onClear: () => undefined,
|
|
||||||
setRpcTarget: () => undefined,
|
|
||||||
networksToRender: defaultNetworks,
|
networksToRender: defaultNetworks,
|
||||||
onAddNetwork: () => undefined,
|
|
||||||
setNewNetworkAdded: () => undefined,
|
|
||||||
addNewNetwork: true,
|
addNewNetwork: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const propNetworkDisplay = {
|
const propNetworkDisplay = {
|
||||||
editRpc: () => undefined,
|
selectedNetwork: {
|
||||||
showConfirmDeleteNetworkModal: () => undefined,
|
rpcUrl: 'http://localhost:8545',
|
||||||
rpcUrl: 'http://localhost:8545',
|
chainId: '1337',
|
||||||
chainId: '1337',
|
ticker: 'ETH',
|
||||||
ticker: 'ETH',
|
label: 'LocalHost',
|
||||||
viewOnly: false,
|
blockExplorerUrl: '',
|
||||||
networkName: 'LocalHost',
|
viewOnly: false,
|
||||||
onClear: () => undefined,
|
rpcPrefs: {},
|
||||||
setRpcTarget: () => undefined,
|
},
|
||||||
isCurrentRpcTarget: false,
|
isCurrentRpcTarget: false,
|
||||||
blockExplorerUrl: '',
|
|
||||||
rpcPrefs: {},
|
|
||||||
networksToRender: defaultNetworks,
|
networksToRender: defaultNetworks,
|
||||||
onAddNetwork: () => undefined,
|
|
||||||
setNewNetworkAdded: () => undefined,
|
|
||||||
addNewNetwork: false,
|
addNewNetwork: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('NetworkForm Component', () => {
|
describe('NetworkForm Component', () => {
|
||||||
it('should render Add new network form correctly', () => {
|
it('should render Add new network form correctly', () => {
|
||||||
const { queryByText } = renderComponent(propNewNetwork);
|
const { queryByText, queryAllByText } = renderComponent(propNewNetwork);
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'A malicious network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
expect(queryByText('Network Name')).toBeInTheDocument();
|
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||||
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||||
expect(queryByText('Currency Symbol (optional)')).toBeInTheDocument();
|
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||||
expect(queryByText('Block Explorer URL (optional)')).toBeInTheDocument();
|
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||||
|
expect(queryAllByText('(Optional)')).toHaveLength(2);
|
||||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||||
expect(queryByText('Save')).toBeInTheDocument();
|
expect(queryByText('Save')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -62,35 +60,50 @@ describe('NetworkForm Component', () => {
|
|||||||
expect(queryByText('Network Name')).toBeInTheDocument();
|
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||||
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||||
expect(queryByText('Currency Symbol (optional)')).toBeInTheDocument();
|
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||||
expect(queryByText('Block Explorer URL (optional)')).toBeInTheDocument();
|
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||||
expect(queryByText('Delete')).toBeInTheDocument();
|
expect(queryByText('Delete')).toBeInTheDocument();
|
||||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||||
expect(queryByText('Save')).toBeInTheDocument();
|
expect(queryByText('Save')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getByDisplayValue(propNetworkDisplay.networkName),
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.label),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(getByDisplayValue(propNetworkDisplay.rpcUrl)).toBeInTheDocument();
|
|
||||||
expect(getByDisplayValue(propNetworkDisplay.chainId)).toBeInTheDocument();
|
|
||||||
expect(getByDisplayValue(propNetworkDisplay.ticker)).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
getByDisplayValue(propNetworkDisplay.blockExplorerUrl),
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.rpcUrl),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
fireEvent.change(getByDisplayValue(propNetworkDisplay.networkName), {
|
expect(
|
||||||
target: { value: 'LocalHost 8545' },
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
|
||||||
});
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.ticker),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.blockExplorerUrl),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.change(
|
||||||
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.label),
|
||||||
|
{
|
||||||
|
target: { value: 'LocalHost 8545' },
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||||
fireEvent.change(getByDisplayValue(propNetworkDisplay.chainId), {
|
fireEvent.change(
|
||||||
target: { value: '1' },
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
|
||||||
});
|
{
|
||||||
|
target: { value: '1' },
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
queryByText('This Chain ID is currently used by the mainnet network.'),
|
queryByText('This Chain ID is currently used by the mainnet network.'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.change(getByDisplayValue(propNetworkDisplay.rpcUrl), {
|
fireEvent.change(
|
||||||
target: { value: 'test' },
|
getByDisplayValue(propNetworkDisplay.selectedNetwork.rpcUrl),
|
||||||
});
|
{
|
||||||
|
target: { value: 'test' },
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './networks-list-item';
|
@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
import { NETWORK_TYPE_RPC } from '../../../../../shared/constants/network';
|
||||||
|
import { SIZES } from '../../../../helpers/constants/design-system';
|
||||||
|
import ColorIndicator from '../../../../components/ui/color-indicator';
|
||||||
|
import LockIcon from '../../../../components/ui/lock-icon';
|
||||||
|
import { NETWORKS_FORM_ROUTE } from '../../../../helpers/constants/routes';
|
||||||
|
import { setSelectedSettingsRpcUrl } from '../../../../store/actions';
|
||||||
|
import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
|
||||||
|
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
|
||||||
|
import { getProvider } from '../../../../selectors';
|
||||||
|
|
||||||
|
const NetworksListItem = ({ network, networkIsSelected, selectedRpcUrl }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const history = useHistory();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const environmentType = getEnvironmentType();
|
||||||
|
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
||||||
|
const provider = useSelector(getProvider);
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
labelKey,
|
||||||
|
rpcUrl,
|
||||||
|
providerType: currentProviderType,
|
||||||
|
} = network;
|
||||||
|
|
||||||
|
const listItemNetworkIsSelected = selectedRpcUrl && selectedRpcUrl === rpcUrl;
|
||||||
|
const listItemUrlIsProviderUrl = rpcUrl === provider.rpcUrl;
|
||||||
|
const listItemTypeIsProviderNonRpcType =
|
||||||
|
provider.type !== NETWORK_TYPE_RPC && currentProviderType === provider.type;
|
||||||
|
const listItemNetworkIsCurrentProvider =
|
||||||
|
!networkIsSelected &&
|
||||||
|
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
|
||||||
|
const displayNetworkListItemAsSelected =
|
||||||
|
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`settings-network-list-item:${rpcUrl}`}
|
||||||
|
className="networks-tab__networks-list-item"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setSelectedSettingsRpcUrl(rpcUrl));
|
||||||
|
if (!isFullScreen) {
|
||||||
|
history.push(NETWORKS_FORM_ROUTE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorIndicator
|
||||||
|
color={labelKey}
|
||||||
|
type={ColorIndicator.TYPES.FILLED}
|
||||||
|
size={SIZES.LG}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classnames('networks-tab__networks-list-name', {
|
||||||
|
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
|
||||||
|
'networks-tab__networks-list-name--disabled':
|
||||||
|
currentProviderType !== NETWORK_TYPE_RPC &&
|
||||||
|
!displayNetworkListItemAsSelected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label || t(labelKey)}
|
||||||
|
{currentProviderType !== NETWORK_TYPE_RPC && (
|
||||||
|
<LockIcon width="14px" height="17px" fill="#cdcdcd" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="networks-tab__networks-list-arrow" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworksListItem.propTypes = {
|
||||||
|
network: PropTypes.object.isRequired,
|
||||||
|
networkIsSelected: PropTypes.bool,
|
||||||
|
selectedRpcUrl: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworksListItem;
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||||
|
import { defaultNetworksData } from '../networks-tab.constants';
|
||||||
|
import NetworksListItem from '.';
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
chainId: '0x4',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: 'https://rinkeby.infura.io/v3/undefined',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'rinkeby',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (props) => {
|
||||||
|
const store = configureMockStore([])(mockState);
|
||||||
|
return renderWithProvider(<NetworksListItem {...props} />, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||||
|
...network,
|
||||||
|
viewOnly: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MainnetProps = {
|
||||||
|
network: defaultNetworks[0],
|
||||||
|
networkIsSelected: false,
|
||||||
|
selectedRpcUrl: 'http://localhost:8545',
|
||||||
|
};
|
||||||
|
const testNetProps = {
|
||||||
|
network: defaultNetworks[1],
|
||||||
|
networkIsSelected: false,
|
||||||
|
selectedRpcUrl: 'http://localhost:8545',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworksListItem Component', () => {
|
||||||
|
it('should render a Mainnet network item correctly', () => {
|
||||||
|
const { queryByText } = renderComponent(MainnetProps);
|
||||||
|
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a test network item correctly', () => {
|
||||||
|
const { queryByText } = renderComponent(testNetProps);
|
||||||
|
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
1
ui/pages/settings/networks-tab/networks-list/index.js
Normal file
1
ui/pages/settings/networks-tab/networks-list/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './networks-list';
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import NetworksListItem from '../networks-list-item';
|
||||||
|
|
||||||
|
const NetworksList = ({
|
||||||
|
networkIsSelected,
|
||||||
|
networksToRender,
|
||||||
|
networkDefaultedToProvider,
|
||||||
|
selectedRpcUrl,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames('networks-tab__networks-list', {
|
||||||
|
'networks-tab__networks-list--selection':
|
||||||
|
networkIsSelected && !networkDefaultedToProvider,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{networksToRender.map((network) => (
|
||||||
|
<NetworksListItem
|
||||||
|
key={`settings-network-list:${network.rpcUrl}`}
|
||||||
|
network={network}
|
||||||
|
networkIsSelected={networkIsSelected}
|
||||||
|
selectedRpcUrl={selectedRpcUrl}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworksList.propTypes = {
|
||||||
|
networkDefaultedToProvider: PropTypes.bool,
|
||||||
|
networkIsSelected: PropTypes.bool,
|
||||||
|
networksToRender: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedRpcUrl: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworksList;
|
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||||
|
import { defaultNetworksData } from '../networks-tab.constants';
|
||||||
|
import NetworksList from '.';
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
chainId: '0x4',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: 'https://rinkeby.infura.io/v3/undefined',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'rinkeby',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (props) => {
|
||||||
|
const store = configureMockStore([])(mockState);
|
||||||
|
return renderWithProvider(<NetworksList {...props} />, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||||
|
...network,
|
||||||
|
viewOnly: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
networkDefaultedToProvider: false,
|
||||||
|
networkIsSelected: false,
|
||||||
|
networksToRender: defaultNetworks,
|
||||||
|
selectedRpcUrl: 'http://localhost:8545',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworksList Component', () => {
|
||||||
|
it('should render a list of networks correctly', () => {
|
||||||
|
const { queryByText } = renderComponent(props);
|
||||||
|
|
||||||
|
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Rinkeby Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Goerli Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Kovan Test Network')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './networks-tab-content';
|
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import NetworksForm from '../networks-form';
|
||||||
|
import NetworksList from '../networks-list';
|
||||||
|
import { getProvider } from '../../../../selectors';
|
||||||
|
|
||||||
|
const NetworksTabContent = ({
|
||||||
|
networkDefaultedToProvider,
|
||||||
|
networkIsSelected,
|
||||||
|
networksToRender,
|
||||||
|
selectedNetwork,
|
||||||
|
shouldRenderNetworkForm,
|
||||||
|
}) => {
|
||||||
|
const provider = useSelector(getProvider);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NetworksList
|
||||||
|
networkDefaultedToProvider={networkDefaultedToProvider}
|
||||||
|
networkIsSelected={networkIsSelected}
|
||||||
|
networksToRender={networksToRender}
|
||||||
|
selectedRpcUrl={selectedNetwork.rpcUrl}
|
||||||
|
/>
|
||||||
|
{shouldRenderNetworkForm ? (
|
||||||
|
<NetworksForm
|
||||||
|
isCurrentRpcTarget={provider.rpcUrl === selectedNetwork.rpcUrl}
|
||||||
|
networksToRender={networksToRender}
|
||||||
|
selectedNetwork={selectedNetwork}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
NetworksTabContent.propTypes = {
|
||||||
|
networkDefaultedToProvider: PropTypes.bool,
|
||||||
|
networkIsSelected: PropTypes.bool,
|
||||||
|
networksToRender: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedNetwork: PropTypes.object,
|
||||||
|
shouldRenderNetworkForm: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworksTabContent;
|
@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { fireEvent } from '@testing-library/react';
|
||||||
|
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||||
|
import { defaultNetworksData } from '../networks-tab.constants';
|
||||||
|
import NetworksTabContent from '.';
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
chainId: '0x539',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: 'http://localhost:8545',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'localhost',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (props) => {
|
||||||
|
const store = configureMockStore([])(mockState);
|
||||||
|
return renderWithProvider(<NetworksTabContent {...props} />, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||||
|
...network,
|
||||||
|
viewOnly: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
networkDefaultedToProvider: false,
|
||||||
|
networkIsSelected: true,
|
||||||
|
networksToRender: defaultNetworks,
|
||||||
|
selectedNetwork: {
|
||||||
|
rpcUrl: 'http://localhost:8545',
|
||||||
|
chainId: '1337',
|
||||||
|
ticker: 'ETH',
|
||||||
|
label: 'LocalHost',
|
||||||
|
blockExplorerUrl: '',
|
||||||
|
viewOnly: false,
|
||||||
|
rpcPrefs: {},
|
||||||
|
},
|
||||||
|
shouldRenderNetworkForm: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworksTabContent Component', () => {
|
||||||
|
it('should render networks tab content correctly', () => {
|
||||||
|
const { queryByText, getByDisplayValue } = renderComponent(props);
|
||||||
|
|
||||||
|
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Rinkeby Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Goerli Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Kovan Test Network')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||||
|
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Save')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(getByDisplayValue(props.selectedNetwork.label)).toBeInTheDocument();
|
||||||
|
expect(getByDisplayValue(props.selectedNetwork.rpcUrl)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByDisplayValue(props.selectedNetwork.chainId),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(getByDisplayValue(props.selectedNetwork.ticker)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByDisplayValue(props.selectedNetwork.blockExplorerUrl),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.change(getByDisplayValue(props.selectedNetwork.label), {
|
||||||
|
target: { value: 'LocalHost 8545' },
|
||||||
|
});
|
||||||
|
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||||
|
fireEvent.change(getByDisplayValue(props.selectedNetwork.chainId), {
|
||||||
|
target: { value: '1' },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText('This Chain ID is currently used by the mainnet network.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.change(getByDisplayValue(props.selectedNetwork.rpcUrl), {
|
||||||
|
target: { value: 'test' },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './networks-tab-subheader';
|
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
import { ADD_NETWORK_ROUTE } from '../../../../helpers/constants/routes';
|
||||||
|
import Button from '../../../../components/ui/button';
|
||||||
|
|
||||||
|
const NetworksFormSubheader = ({ addNewNetwork }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const history = useHistory();
|
||||||
|
return addNewNetwork ? (
|
||||||
|
<div className="networks-tab__subheader">
|
||||||
|
<span className="networks-tab__sub-header-text">{t('networks')}</span>
|
||||||
|
<span>{' > '}</span>
|
||||||
|
<div className="networks-tab__subheader--break">{t('addANetwork')}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="settings-page__sub-header">
|
||||||
|
<span className="settings-page__sub-header-text">{t('networks')}</span>
|
||||||
|
<div className="networks-tab__add-network-header-button-wrapper">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
history.push(ADD_NETWORK_ROUTE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('addANetwork')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworksFormSubheader.propTypes = {
|
||||||
|
addNewNetwork: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworksFormSubheader;
|
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||||
|
import NetworksTabSubheader from '.';
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
chainId: '0x539',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: 'http://localhost:8545',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'localhost',
|
||||||
|
},
|
||||||
|
frequentRpcListDetail: [],
|
||||||
|
},
|
||||||
|
appState: {
|
||||||
|
networksTabSelectedRpcUrl: 'http://localhost:8545',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (props) => {
|
||||||
|
const store = configureMockStore([])(mockState);
|
||||||
|
return renderWithProvider(<NetworksTabSubheader {...props} />, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworksTabSubheader Component', () => {
|
||||||
|
it('should render network subheader correctly', () => {
|
||||||
|
const { queryByText, getByRole } = renderComponent({
|
||||||
|
addNewNetwork: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByText('Networks')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Add a network')).toBeInTheDocument();
|
||||||
|
expect(getByRole('button', { text: 'Add a network' })).toBeDefined();
|
||||||
|
});
|
||||||
|
it('should render add network form subheader correctly', () => {
|
||||||
|
const { queryByText } = renderComponent({
|
||||||
|
addNewNetwork: true,
|
||||||
|
});
|
||||||
|
expect(queryByText('Networks')).toBeInTheDocument();
|
||||||
|
expect(queryByText('>')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Add a network')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,259 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
|
||||||
import Button from '../../../components/ui/button';
|
|
||||||
import LockIcon from '../../../components/ui/lock-icon';
|
|
||||||
import {
|
|
||||||
NETWORKS_ROUTE,
|
|
||||||
NETWORKS_FORM_ROUTE,
|
|
||||||
DEFAULT_ROUTE,
|
|
||||||
ADD_NETWORK_ROUTE,
|
|
||||||
} from '../../../helpers/constants/routes';
|
|
||||||
import ColorIndicator from '../../../components/ui/color-indicator';
|
|
||||||
import { SIZES } from '../../../helpers/constants/design-system';
|
|
||||||
import NetworkForm from './network-form';
|
|
||||||
|
|
||||||
export default class NetworksTab extends PureComponent {
|
|
||||||
static contextTypes = {
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
metricsEvent: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
editRpc: PropTypes.func.isRequired,
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
networkIsSelected: PropTypes.bool,
|
|
||||||
networksToRender: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedNetwork: PropTypes.object,
|
|
||||||
setRpcTarget: PropTypes.func.isRequired,
|
|
||||||
setSelectedSettingsRpcUrl: PropTypes.func.isRequired,
|
|
||||||
showConfirmDeleteNetworkModal: PropTypes.func.isRequired,
|
|
||||||
providerUrl: PropTypes.string,
|
|
||||||
providerType: PropTypes.string,
|
|
||||||
networkDefaultedToProvider: PropTypes.bool,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
shouldRenderNetworkForm: PropTypes.bool.isRequired,
|
|
||||||
isFullScreen: PropTypes.bool.isRequired,
|
|
||||||
setNewNetworkAdded: PropTypes.func.isRequired,
|
|
||||||
addNewNetwork: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.setSelectedSettingsRpcUrl('');
|
|
||||||
}
|
|
||||||
|
|
||||||
isCurrentPath(pathname) {
|
|
||||||
return this.props.location.pathname === pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSubHeader() {
|
|
||||||
const { history } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="settings-page__sub-header">
|
|
||||||
<span className="settings-page__sub-header-text">
|
|
||||||
{this.context.t('networks')}
|
|
||||||
</span>
|
|
||||||
<div className="networks-tab__add-network-header-button-wrapper">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
history.push(ADD_NETWORK_ROUTE);
|
|
||||||
}}
|
|
||||||
className="add-network-form__header-add-network-button"
|
|
||||||
>
|
|
||||||
{this.context.t('addANetwork')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNetworkListItem(network, selectRpcUrl) {
|
|
||||||
const {
|
|
||||||
setSelectedSettingsRpcUrl,
|
|
||||||
networkIsSelected,
|
|
||||||
providerUrl,
|
|
||||||
providerType,
|
|
||||||
history,
|
|
||||||
isFullScreen,
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
labelKey,
|
|
||||||
rpcUrl,
|
|
||||||
providerType: currentProviderType,
|
|
||||||
} = network;
|
|
||||||
|
|
||||||
const listItemNetworkIsSelected = selectRpcUrl && selectRpcUrl === rpcUrl;
|
|
||||||
const listItemUrlIsProviderUrl = rpcUrl === providerUrl;
|
|
||||||
const listItemTypeIsProviderNonRpcType =
|
|
||||||
providerType !== NETWORK_TYPE_RPC && currentProviderType === providerType;
|
|
||||||
const listItemNetworkIsCurrentProvider =
|
|
||||||
!networkIsSelected &&
|
|
||||||
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
|
|
||||||
const displayNetworkListItemAsSelected =
|
|
||||||
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`settings-network-list-item:${rpcUrl}`}
|
|
||||||
className="networks-tab__networks-list-item"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedSettingsRpcUrl(rpcUrl);
|
|
||||||
if (!isFullScreen) {
|
|
||||||
history.push(NETWORKS_FORM_ROUTE);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ColorIndicator
|
|
||||||
color={labelKey}
|
|
||||||
type={ColorIndicator.TYPES.FILLED}
|
|
||||||
size={SIZES.LG}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classnames('networks-tab__networks-list-name', {
|
|
||||||
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
|
|
||||||
'networks-tab__networks-list-name--disabled':
|
|
||||||
currentProviderType !== NETWORK_TYPE_RPC &&
|
|
||||||
!displayNetworkListItemAsSelected,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{label || this.context.t(labelKey)}
|
|
||||||
{currentProviderType !== NETWORK_TYPE_RPC && (
|
|
||||||
<LockIcon width="14px" height="17px" fill="#cdcdcd" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="networks-tab__networks-list-arrow" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNetworksList() {
|
|
||||||
const {
|
|
||||||
networksToRender,
|
|
||||||
selectedNetwork,
|
|
||||||
networkIsSelected,
|
|
||||||
networkDefaultedToProvider,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classnames('networks-tab__networks-list', {
|
|
||||||
'networks-tab__networks-list--selection':
|
|
||||||
networkIsSelected && !networkDefaultedToProvider,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{networksToRender.map((network) =>
|
|
||||||
this.renderNetworkListItem(network, selectedNetwork.rpcUrl),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNetworksTabContent() {
|
|
||||||
const { t } = this.context;
|
|
||||||
const {
|
|
||||||
setRpcTarget,
|
|
||||||
showConfirmDeleteNetworkModal,
|
|
||||||
setSelectedSettingsRpcUrl,
|
|
||||||
selectedNetwork: {
|
|
||||||
labelKey,
|
|
||||||
label,
|
|
||||||
rpcUrl,
|
|
||||||
chainId,
|
|
||||||
ticker,
|
|
||||||
viewOnly,
|
|
||||||
rpcPrefs,
|
|
||||||
blockExplorerUrl,
|
|
||||||
},
|
|
||||||
editRpc,
|
|
||||||
providerUrl,
|
|
||||||
networksToRender,
|
|
||||||
history,
|
|
||||||
isFullScreen,
|
|
||||||
shouldRenderNetworkForm,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.renderNetworksList()}
|
|
||||||
{shouldRenderNetworkForm ? (
|
|
||||||
<NetworkForm
|
|
||||||
setRpcTarget={setRpcTarget}
|
|
||||||
editRpc={editRpc}
|
|
||||||
networkName={label || (labelKey && t(labelKey)) || ''}
|
|
||||||
rpcUrl={rpcUrl}
|
|
||||||
chainId={chainId}
|
|
||||||
networksToRender={networksToRender}
|
|
||||||
ticker={ticker}
|
|
||||||
onClear={(shouldUpdateHistory = true) => {
|
|
||||||
setSelectedSettingsRpcUrl('');
|
|
||||||
if (shouldUpdateHistory) {
|
|
||||||
history.push(NETWORKS_ROUTE);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showConfirmDeleteNetworkModal={showConfirmDeleteNetworkModal}
|
|
||||||
viewOnly={viewOnly}
|
|
||||||
isCurrentRpcTarget={providerUrl === rpcUrl}
|
|
||||||
rpcPrefs={rpcPrefs}
|
|
||||||
blockExplorerUrl={blockExplorerUrl}
|
|
||||||
isFullScreen={isFullScreen}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
history,
|
|
||||||
isFullScreen,
|
|
||||||
shouldRenderNetworkForm,
|
|
||||||
setRpcTarget,
|
|
||||||
networksToRender,
|
|
||||||
setNewNetworkAdded,
|
|
||||||
selectedNetwork: { rpcPrefs },
|
|
||||||
addNewNetwork,
|
|
||||||
} = this.props;
|
|
||||||
return addNewNetwork ? (
|
|
||||||
<NetworkForm
|
|
||||||
setRpcTarget={setRpcTarget}
|
|
||||||
onClear={(shouldUpdateHistory = true) => {
|
|
||||||
if (shouldUpdateHistory) {
|
|
||||||
history.push(NETWORKS_ROUTE);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onAddNetwork={() => {
|
|
||||||
history.push(DEFAULT_ROUTE);
|
|
||||||
}}
|
|
||||||
rpcPrefs={rpcPrefs}
|
|
||||||
networksToRender={networksToRender}
|
|
||||||
setNewNetworkAdded={setNewNetworkAdded}
|
|
||||||
addNewNetwork={addNewNetwork}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="networks-tab__body">
|
|
||||||
{isFullScreen ? this.renderSubHeader() : null}
|
|
||||||
<div className="networks-tab__content">
|
|
||||||
{this.renderNetworksTabContent()}
|
|
||||||
{!isFullScreen && !shouldRenderNetworkForm ? (
|
|
||||||
<div className="networks-tab__networks-list-popup-footer">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.context.t('addNetwork')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
import { compose } from 'redux';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
setSelectedSettingsRpcUrl,
|
|
||||||
updateAndSetCustomRpc,
|
|
||||||
displayWarning,
|
|
||||||
editRpc,
|
|
||||||
showModal,
|
|
||||||
setNewNetworkAdded,
|
|
||||||
} from '../../../store/actions';
|
|
||||||
import {
|
|
||||||
ADD_NETWORK_ROUTE,
|
|
||||||
NETWORKS_FORM_ROUTE,
|
|
||||||
} from '../../../helpers/constants/routes';
|
|
||||||
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
|
|
||||||
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
|
||||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
|
||||||
import NetworksTab from './networks-tab.component';
|
|
||||||
import { defaultNetworksData } from './networks-tab.constants';
|
|
||||||
|
|
||||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
|
||||||
...network,
|
|
||||||
viewOnly: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
const {
|
|
||||||
location: { pathname },
|
|
||||||
} = ownProps;
|
|
||||||
|
|
||||||
const environmentType = getEnvironmentType();
|
|
||||||
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
|
||||||
const shouldRenderNetworkForm =
|
|
||||||
isFullScreen || Boolean(pathname.match(NETWORKS_FORM_ROUTE));
|
|
||||||
const addNewNetwork = Boolean(pathname.match(ADD_NETWORK_ROUTE));
|
|
||||||
|
|
||||||
const { frequentRpcListDetail, provider } = state.metamask;
|
|
||||||
const { networksTabSelectedRpcUrl } = state.appState;
|
|
||||||
const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => {
|
|
||||||
return {
|
|
||||||
label: rpc.nickname,
|
|
||||||
iconColor: '#6A737D',
|
|
||||||
providerType: NETWORK_TYPE_RPC,
|
|
||||||
rpcUrl: rpc.rpcUrl,
|
|
||||||
chainId: rpc.chainId,
|
|
||||||
ticker: rpc.ticker,
|
|
||||||
blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const networksToRender = [
|
|
||||||
...defaultNetworks,
|
|
||||||
...frequentRpcNetworkListDetails,
|
|
||||||
];
|
|
||||||
let selectedNetwork =
|
|
||||||
networksToRender.find(
|
|
||||||
(network) => network.rpcUrl === networksTabSelectedRpcUrl,
|
|
||||||
) || {};
|
|
||||||
const networkIsSelected = Boolean(selectedNetwork.rpcUrl);
|
|
||||||
|
|
||||||
let networkDefaultedToProvider = false;
|
|
||||||
if (!networkIsSelected) {
|
|
||||||
selectedNetwork =
|
|
||||||
networksToRender.find((network) => {
|
|
||||||
return (
|
|
||||||
network.rpcUrl === provider.rpcUrl ||
|
|
||||||
(network.providerType !== NETWORK_TYPE_RPC &&
|
|
||||||
network.providerType === provider.type)
|
|
||||||
);
|
|
||||||
}) || {};
|
|
||||||
networkDefaultedToProvider = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedNetwork,
|
|
||||||
networksToRender,
|
|
||||||
networkIsSelected,
|
|
||||||
providerType: provider.type,
|
|
||||||
providerUrl: provider.rpcUrl,
|
|
||||||
networkDefaultedToProvider,
|
|
||||||
isFullScreen,
|
|
||||||
shouldRenderNetworkForm,
|
|
||||||
addNewNetwork,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
|
||||||
return {
|
|
||||||
setSelectedSettingsRpcUrl: (newRpcUrl) =>
|
|
||||||
dispatch(setSelectedSettingsRpcUrl(newRpcUrl)),
|
|
||||||
setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => {
|
|
||||||
return dispatch(
|
|
||||||
updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
showConfirmDeleteNetworkModal: ({ target, onConfirm }) => {
|
|
||||||
return dispatch(
|
|
||||||
showModal({ name: 'CONFIRM_DELETE_NETWORK', target, onConfirm }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
displayWarning: (warning) => dispatch(displayWarning(warning)),
|
|
||||||
editRpc: (oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs) => {
|
|
||||||
return dispatch(
|
|
||||||
editRpc(oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
setNewNetworkAdded: (newNetwork) => {
|
|
||||||
dispatch(setNewNetworkAdded(newNetwork));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withRouter,
|
|
||||||
connect(mapStateToProps, mapDispatchToProps),
|
|
||||||
)(NetworksTab);
|
|
129
ui/pages/settings/networks-tab/networks-tab.js
Normal file
129
ui/pages/settings/networks-tab/networks-tab.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import {
|
||||||
|
ADD_NETWORK_ROUTE,
|
||||||
|
NETWORKS_FORM_ROUTE,
|
||||||
|
} from '../../../helpers/constants/routes';
|
||||||
|
import { setSelectedSettingsRpcUrl } from '../../../store/actions';
|
||||||
|
import Button from '../../../components/ui/button';
|
||||||
|
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||||
|
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
|
||||||
|
import {
|
||||||
|
getFrequentRpcListDetail,
|
||||||
|
getNetworksTabSelectedRpcUrl,
|
||||||
|
getProvider,
|
||||||
|
} from '../../../selectors';
|
||||||
|
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
||||||
|
import { defaultNetworksData } from './networks-tab.constants';
|
||||||
|
import NetworksTabContent from './networks-tab-content';
|
||||||
|
import NetworksForm from './networks-form';
|
||||||
|
import NetworksFormSubheader from './networks-tab-subheader';
|
||||||
|
|
||||||
|
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||||
|
...network,
|
||||||
|
viewOnly: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const NetworksTab = ({ addNewNetwork }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const environmentType = getEnvironmentType();
|
||||||
|
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
||||||
|
const shouldRenderNetworkForm =
|
||||||
|
isFullScreen || Boolean(pathname.match(NETWORKS_FORM_ROUTE));
|
||||||
|
|
||||||
|
const frequentRpcListDetail = useSelector(getFrequentRpcListDetail);
|
||||||
|
const provider = useSelector(getProvider);
|
||||||
|
const networksTabSelectedRpcUrl = useSelector(getNetworksTabSelectedRpcUrl);
|
||||||
|
|
||||||
|
const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => {
|
||||||
|
return {
|
||||||
|
label: rpc.nickname,
|
||||||
|
iconColor: '#6A737D',
|
||||||
|
providerType: NETWORK_TYPE_RPC,
|
||||||
|
rpcUrl: rpc.rpcUrl,
|
||||||
|
chainId: rpc.chainId,
|
||||||
|
ticker: rpc.ticker,
|
||||||
|
blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const networksToRender = [
|
||||||
|
...defaultNetworks,
|
||||||
|
...frequentRpcNetworkListDetails,
|
||||||
|
];
|
||||||
|
let selectedNetwork =
|
||||||
|
networksToRender.find(
|
||||||
|
(network) => network.rpcUrl === networksTabSelectedRpcUrl,
|
||||||
|
) || {};
|
||||||
|
const networkIsSelected = Boolean(selectedNetwork.rpcUrl);
|
||||||
|
|
||||||
|
let networkDefaultedToProvider = false;
|
||||||
|
if (!networkIsSelected) {
|
||||||
|
selectedNetwork =
|
||||||
|
networksToRender.find((network) => {
|
||||||
|
return (
|
||||||
|
network.rpcUrl === provider.rpcUrl ||
|
||||||
|
(network.providerType !== NETWORK_TYPE_RPC &&
|
||||||
|
network.providerType === provider.type)
|
||||||
|
);
|
||||||
|
}) || {};
|
||||||
|
networkDefaultedToProvider = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
dispatch(setSelectedSettingsRpcUrl(''));
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="networks-tab__body">
|
||||||
|
{isFullScreen ? (
|
||||||
|
<NetworksFormSubheader addNewNetwork={addNewNetwork} />
|
||||||
|
) : null}
|
||||||
|
<div className="networks-tab__content">
|
||||||
|
{addNewNetwork ? (
|
||||||
|
<NetworksForm
|
||||||
|
networksToRender={networksToRender}
|
||||||
|
addNewNetwork={addNewNetwork}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<NetworksTabContent
|
||||||
|
networkDefaultedToProvider={networkDefaultedToProvider}
|
||||||
|
networkIsSelected={networkIsSelected}
|
||||||
|
networksToRender={networksToRender}
|
||||||
|
providerUrl={provider.rpcUrl}
|
||||||
|
selectedNetwork={selectedNetwork}
|
||||||
|
shouldRenderNetworkForm={shouldRenderNetworkForm}
|
||||||
|
/>
|
||||||
|
{!isFullScreen && !shouldRenderNetworkForm ? (
|
||||||
|
<div className="networks-tab__networks-list-popup-footer">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('addNetwork')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworksTab.propTypes = {
|
||||||
|
addNewNetwork: PropTypes.bool,
|
||||||
|
};
|
||||||
|
export default NetworksTab;
|
53
ui/pages/settings/networks-tab/networks-tab.test.js
Normal file
53
ui/pages/settings/networks-tab/networks-tab.test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { renderWithProvider } from '../../../../test/jest/rendering';
|
||||||
|
import NetworksTab from '.';
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
chainId: '0x539',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: 'http://localhost:8545',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'localhost',
|
||||||
|
},
|
||||||
|
frequentRpcListDetail: [],
|
||||||
|
},
|
||||||
|
appState: {
|
||||||
|
networksTabSelectedRpcUrl: 'http://localhost:8545',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (props) => {
|
||||||
|
const store = configureMockStore([])(mockState);
|
||||||
|
return renderWithProvider(<NetworksTab {...props} />, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworksTab Component', () => {
|
||||||
|
it('should render networks tab content correctly', () => {
|
||||||
|
const { queryByText } = renderComponent({
|
||||||
|
addNewNetwork: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Rinkeby Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Goerli Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Kovan Test Network')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Add Network')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should render add network form correctly', () => {
|
||||||
|
const { queryByText } = renderComponent({
|
||||||
|
addNewNetwork: true,
|
||||||
|
});
|
||||||
|
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||||
|
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||||
|
expect(queryByText('Save')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -231,7 +231,11 @@ class SettingsPage extends PureComponent {
|
|||||||
<Route exact path={ABOUT_US_ROUTE} component={InfoTab} />
|
<Route exact path={ABOUT_US_ROUTE} component={InfoTab} />
|
||||||
<Route exact path={ADVANCED_ROUTE} component={AdvancedTab} />
|
<Route exact path={ADVANCED_ROUTE} component={AdvancedTab} />
|
||||||
<Route exact path={ALERTS_ROUTE} component={AlertsTab} />
|
<Route exact path={ALERTS_ROUTE} component={AlertsTab} />
|
||||||
<Route exact path={ADD_NETWORK_ROUTE} component={NetworksTab} />
|
<Route
|
||||||
|
exact
|
||||||
|
path={ADD_NETWORK_ROUTE}
|
||||||
|
render={() => <NetworksTab addNewNetwork />}
|
||||||
|
/>
|
||||||
<Route path={NETWORKS_ROUTE} component={NetworksTab} />
|
<Route path={NETWORKS_ROUTE} component={NetworksTab} />
|
||||||
<Route exact path={SECURITY_ROUTE} component={SecurityTab} />
|
<Route exact path={SECURITY_ROUTE} component={SecurityTab} />
|
||||||
<Route exact path={EXPERIMENTAL_ROUTE} component={ExperimentalTab} />
|
<Route exact path={EXPERIMENTAL_ROUTE} component={ExperimentalTab} />
|
||||||
|
@ -688,3 +688,15 @@ export function doesAddressRequireLedgerHidConnection(state, address) {
|
|||||||
export function getNewNetworkAdded(state) {
|
export function getNewNetworkAdded(state) {
|
||||||
return state.appState.newNetworkAdded;
|
return state.appState.newNetworkAdded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNetworksTabSelectedRpcUrl(state) {
|
||||||
|
return state.appState.networksTabSelectedRpcUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProvider(state) {
|
||||||
|
return state.metamask.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFrequentRpcListDetail(state) {
|
||||||
|
return state.metamask.frequentRpcListDetail;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user