From 3552941323a9ddb8818c27fd4c789db17b3fc7df Mon Sep 17 00:00:00 2001 From: Victorien Gauch <85494462+VGau@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:28:38 +0100 Subject: [PATCH 01/15] feat: add the ConsenSys zkEVM (Linea) as a default network (#17875) * feat: add the consensys zkEVM as a default network * fix: change infuraNetworkStatus in navigate-txs file * fix: remove account tracker for zkEVM + remove zkEVM from infura list * fix: change consensys zkevm name to linea + change rpc url for linea network * fix: rebase conflicts * feat: add new colors for linea goerli network * feat: add new function inside network dropdown to render non infura networks * feat: add feature toggle for linea network * fix: add new unit test --------- Co-authored-by: Dan J Miller --- app/_locales/am/messages.json | 6 ++ app/_locales/ar/messages.json | 6 ++ app/_locales/bg/messages.json | 6 ++ app/_locales/ca/messages.json | 6 ++ app/_locales/da/messages.json | 6 ++ app/_locales/de/messages.json | 6 ++ app/_locales/el/messages.json | 6 ++ app/_locales/en/messages.json | 6 ++ app/_locales/es/messages.json | 6 ++ app/_locales/es_419/messages.json | 6 ++ app/_locales/et/messages.json | 6 ++ app/_locales/fa/messages.json | 6 ++ app/_locales/fi/messages.json | 6 ++ app/_locales/fil/messages.json | 3 + app/_locales/fr/messages.json | 6 ++ app/_locales/he/messages.json | 6 ++ app/_locales/hi/messages.json | 6 ++ app/_locales/hr/messages.json | 6 ++ app/_locales/hu/messages.json | 6 ++ app/_locales/id/messages.json | 6 ++ app/_locales/it/messages.json | 6 ++ app/_locales/ja/messages.json | 6 ++ app/_locales/kn/messages.json | 6 ++ app/_locales/ko/messages.json | 6 ++ app/_locales/lt/messages.json | 6 ++ app/_locales/lv/messages.json | 6 ++ app/_locales/ms/messages.json | 6 ++ app/_locales/no/messages.json | 3 + app/_locales/ph/messages.json | 6 ++ app/_locales/pl/messages.json | 6 ++ app/_locales/pt/messages.json | 6 ++ app/_locales/pt_BR/messages.json | 6 ++ app/_locales/ro/messages.json | 6 ++ app/_locales/ru/messages.json | 6 ++ app/_locales/sk/messages.json | 6 ++ app/_locales/sl/messages.json | 6 ++ app/_locales/sr/messages.json | 6 ++ app/_locales/sv/messages.json | 6 ++ app/_locales/sw/messages.json | 6 ++ app/_locales/tl/messages.json | 6 ++ app/_locales/tr/messages.json | 6 ++ app/_locales/uk/messages.json | 6 ++ app/_locales/vi/messages.json | 6 ++ app/_locales/zh_CN/messages.json | 6 ++ app/_locales/zh_TW/messages.json | 6 ++ .../handlers/switch-ethereum-chain.js | 3 +- development/states/navigate-txs.json | 3 +- shared/constants/network.ts | 27 +++++++ .../app/dropdowns/network-dropdown.js | 80 +++++++++++++++++-- .../app/dropdowns/network-dropdown.test.js | 19 ++++- .../loading-network-screen.component.js | 2 + .../signature-request-original.component.js | 2 + .../signature-request.component.js | 2 + ui/components/ui/typography/typography.js | 2 + ui/css/design-system/colors.scss | 2 + ui/css/itcss/components/network.scss | 4 + ui/css/utilities/colors.scss | 2 + ui/helpers/constants/design-system.ts | 8 ++ ui/helpers/constants/settings.js | 7 ++ ui/helpers/utils/settings-search.test.js | 2 +- ui/helpers/utils/util.js | 1 + ui/helpers/utils/util.test.js | 11 +++ ui/pages/routes/routes.component.js | 2 + .../networks-tab/networks-tab.constants.js | 10 +++ .../settings/networks-tab/networks-tab.js | 40 ++++++---- ui/store/actions.ts | 2 + 66 files changed, 469 insertions(+), 26 deletions(-) diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 8d86237b503c..155a3286ce4f 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -172,6 +172,9 @@ "connectingToGoerli": { "message": "ከ Goerli የሙከራ አውታረ መረብ ጋር መገናኘት" }, + "connectingToLineaTestnet": { + "message": "ከ Linea Goerli የሙከራ አውታረ መረብ ጋር መገናኘት" + }, "connectingToMainnet": { "message": "ከዋናው የ Ethereum አውታረ መረብ ጋር መገናኘት" }, @@ -425,6 +428,9 @@ "likeToImportTokens": { "message": "እነዚህን ተለዋጭ ስሞች ለማከል ይፈልጋሉ?" }, + "lineatestnet": { + "message": "የ Linea Goerli የሙከራ አውታረ መረብ" + }, "links": { "message": "ማስፈንጠሪያዎች" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index b1462ad75fc9..50d252b1b24c 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -182,6 +182,9 @@ "connectingToGoerli": { "message": "الاتصال بشبكة اختبار Goerli" }, + "connectingToLineaTestnet": { + "message": "الاتصال بشبكة اختبار Linea Goerli" + }, "connectingToMainnet": { "message": "جارِ الاتصال بشبكة إيثيريوم الرئيسية" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "هل ترغب في إضافة هذه الرموز؟" }, + "lineatestnet": { + "message": "شبكة اختبار Linea Goerli" + }, "links": { "message": "الروابط" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 5eb731605c63..ea6d78ee68e3 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Свързване с тестова мрежа на Goerli" }, + "connectingToLineaTestnet": { + "message": "Свързване с тестова мрежа на Linea Goerli" + }, "connectingToMainnet": { "message": "Свързване с главната мрежа Ethereum" }, @@ -433,6 +436,9 @@ "likeToImportTokens": { "message": "Искате ли да добавите тези жетони?" }, + "lineatestnet": { + "message": "Тестова мрежа на Linea Goerli" + }, "links": { "message": "Връзки" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index bae26373308c..b206f37dd413 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -175,6 +175,9 @@ "connectingToGoerli": { "message": "Connectant a Xarxa de Prova Goerli" }, + "connectingToLineaTestnet": { + "message": "Connectant a Xarxa de Prova Linea Goerli" + }, "connectingToMainnet": { "message": "Connectant a Xarxa Principal Ethereum" }, @@ -424,6 +427,9 @@ "likeToImportTokens": { "message": "T'agradaria afegir aquestes fitxes?" }, + "lineatestnet": { + "message": "Xarxa de test Linea Goerli" + }, "links": { "message": "Enllaços" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index f0d400c0e594..4640d2957507 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Opretter forbindelse til Goerli Testnetværk" }, + "connectingToLineaTestnet": { + "message": "Opretter forbindelse til Linea Goerli Testnetværk" + }, "connectingToMainnet": { "message": "Forbinder til dit Primære Ethereum Netværk" }, @@ -430,6 +433,9 @@ "likeToImportTokens": { "message": "Ønsker du at tilføje disse tokens?" }, + "lineatestnet": { + "message": "Linea-testnetværk" + }, "loadMore": { "message": "Indlæs Mere" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index be6cb778527c..7890f6927e35 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Verbindungsaufbau zum Goerli-Testnetzwerk" }, + "connectingToLineaTestnet": { + "message": "Verbindungsaufbau zum Linea-Testnetzwerk" + }, "connectingToMainnet": { "message": "Verbinde zum Ethereum Mainnet" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Möchtest du diese Token hinzufügen?" }, + "lineatestnet": { + "message": "Linea-Testnetzwerk" + }, "link": { "message": "Link" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index df76ccafa40b..0f487ff170e1 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Σύνδεση στο Δίκτυο Δοκιμής Goerli" }, + "connectingToLineaTestnet": { + "message": "Σύνδεση στο δίκτυο δοκιμών Linea Goerli" + }, "connectingToMainnet": { "message": "Σύνδεση στο Κύριο Δίκτυο Ethereum" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Θέλετε να προσθέσετε αυτά τα token;" }, + "lineatestnet": { + "message": "Δίκτυο δοκιμών Linea Goerli" + }, "link": { "message": "Σύνδεσμος" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 12a2abfef5ec..49ad840b26b3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -743,6 +743,9 @@ "connectingToGoerli": { "message": "Connecting to Goerli test network" }, + "connectingToLineaTestnet": { + "message": "Connecting to Linea Goerli test network" + }, "connectingToMainnet": { "message": "Connecting to Ethereum Mainnet" }, @@ -1836,6 +1839,9 @@ "likeToImportTokens": { "message": "Would you like to import these tokens?" }, + "lineatestnet": { + "message": "Linea Goerli test network" + }, "link": { "message": "Link" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 22d7489d62fe..aa1104a2dc56 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Estableciendo conexión a la red de prueba Goerli" }, + "connectingToLineaTestnet": { + "message": "Conectando a la red de prueba Linea Goerli" + }, "connectingToMainnet": { "message": "Estableciendo conexión a la red principal de Ethereum" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "¿Le gustaría agregar estos tokens?" }, + "lineatestnet": { + "message": "Red de prueba Linea Goerli" + }, "link": { "message": "Vínculo" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 774f552c7ac6..b8e429718959 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -469,6 +469,9 @@ "connectingToGoerli": { "message": "Estableciendo conexión a la red de prueba Goerli" }, + "connectingToLineaTestnet": { + "message": "Estableciendo conexión a la red de prueba Linea Goerli" + }, "connectingToMainnet": { "message": "Estableciendo conexión a la red principal de Ethereum" }, @@ -1308,6 +1311,9 @@ "likeToImportTokens": { "message": "¿Quiere agregar estos tokens?" }, + "lineatestnet": { + "message": "Red de prueba Linea Goerli" + }, "link": { "message": "Enlace" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index e6879d2d60c5..ba3c7b7ba9e4 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Ühendamine Goerli testvõrguga" }, + "connectingToLineaTestnet": { + "message": "Ühendamine Linea Goerli testvõrguga" + }, "connectingToMainnet": { "message": "Ühenduse loomine peamise Etherumi võrguga" }, @@ -433,6 +436,9 @@ "likeToImportTokens": { "message": "Kas soovite need load lisada?" }, + "lineatestnet": { + "message": "Linea Goerli testvõrk" + }, "links": { "message": "Lingid" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 1b1a1ff53800..084da8b5b5df 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "در حال اتصال به شبکه آزمایشی Goerli " }, + "connectingToLineaTestnet": { + "message": "در حال اتصال به شبکه آزمایشی Linea Goerli" + }, "connectingToMainnet": { "message": "در حال اتصال به شبکه اصلی ایتریم" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "آیا میخواهید این رمزیاب ها را اضافه نمایید؟" }, + "lineatestnet": { + "message": "شبکه آزمایشی Linea Goerli" + }, "links": { "message": "لینک ها" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 2cff7d700f3b..46d6a0fac3c6 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Yhdistetään Goerlin testiverkostoon" }, + "connectingToLineaTestnet": { + "message": "Yhdistetään Linea Goerli testiverkostoon" + }, "connectingToMainnet": { "message": "Yhdistetään Ethereumin pääverkkoon" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "Haluaisitko lisätä nämä poletit?" }, + "lineatestnet": { + "message": "Linea-testiverkko" + }, "links": { "message": "Linkit" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 8168dc729ada..5c4207eab89b 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -157,6 +157,9 @@ "connectingToGoerli": { "message": "Kumokonekta sa Goerli Test Network" }, + "connectingToLineaTestnet": { + "message": "Kumokonekta sa Linea Goerli Test Network" + }, "connectingToMainnet": { "message": "Kumokonekta sa Ethereum Mainnet" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 909e074177c9..545919216138 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Connexion au testnet Goerli" }, + "connectingToLineaTestnet": { + "message": "Connexion au réseau de test Linea Goerli" + }, "connectingToMainnet": { "message": "Connexion au réseau principal Ethereum" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Souhaitez-vous ajouter ces jetons ?" }, + "lineatestnet": { + "message": "Réseau de test Linea Goerli" + }, "link": { "message": "Associer" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 94543a14fda2..22cfe088b2ab 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "מתחבר ל-Goerli Test Network" }, + "connectingToLineaTestnet": { + "message": "מתחבר ל-Linea Goerli Test Network" + }, "connectingToMainnet": { "message": "מתחבר לרשת אתריום הראשית" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "האם ברצונך להוסיף טוקנים אלה?" }, + "lineatestnet": { + "message": "רשת בדיקה Linea Goerli" + }, "links": { "message": "קישורים" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 256e5b16651c..e232416af41f 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Goerli टेस्ट नेटवर्क से कनेक्ट हो रहा है" }, + "connectingToLineaTestnet": { + "message": "Linea Goerli टेस्ट नेटवर्क से कनेक्ट हो रहा है" + }, "connectingToMainnet": { "message": "Ethereum Mainnet से कनेक्ट हो रहा है" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "क्या आप इन टोकन को इंपोर्ट करना चाहते हैं?" }, + "lineatestnet": { + "message": "Linea Goerli टेस्ट नेटवर्क" + }, "link": { "message": "लिंक" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index d959ef76c71d..ac747094d179 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Povezivanje na testnu mrežu Goerli" }, + "connectingToLineaTestnet": { + "message": "Povezivanje na testnu mrežu Linea Goerli" + }, "connectingToMainnet": { "message": "Povezivanje na glavnu mrežu Ethereum" }, @@ -433,6 +436,9 @@ "likeToImportTokens": { "message": "Želite li dodati ove tokene?" }, + "lineatestnet": { + "message": "Testna mreža Linea Goerli" + }, "links": { "message": "Poveznice" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 1c0a5c87f121..040a7822ed4f 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Csatlakozás a Goerli teszthálózathoz" }, + "connectingToLineaTestnet": { + "message": "Csatlakozás a Linea Goerli teszthálózathoz" + }, "connectingToMainnet": { "message": "Csatlakozás a fő Ethereum hálózathoz" }, @@ -433,6 +436,9 @@ "likeToImportTokens": { "message": "Hozzá szeretné adni ezeket az érméket?" }, + "lineatestnet": { + "message": "Linea Goerli teszthálózat" + }, "links": { "message": "Linkek" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 65d3d696977b..077a530283ed 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Menghubungkan ke jaringan uji Goerli" }, + "connectingToLineaTestnet": { + "message": "Menghubungkan ke jaringan uji Linea Goerli" + }, "connectingToMainnet": { "message": "Menghubungkan ke Ethereum Mainnet" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Apakah Anda ingin menambahkan token ini?" }, + "lineatestnet": { + "message": "Jaringan uji Linea Goerli" + }, "link": { "message": "Tautan" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index ed5798e4d460..8bb36f323c0c 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -599,6 +599,9 @@ "connectingToGoerli": { "message": "Connessione alla Rete di Test Goerli" }, + "connectingToLineaTestnet": { + "message": "Connessione alla Rete di test Linea Goerli" + }, "connectingToMainnet": { "message": "Connessione alla Rete Ethereum Principale" }, @@ -1210,6 +1213,9 @@ "likeToImportTokens": { "message": "Vorresti aggiungere questi token?" }, + "lineatestnet": { + "message": "Rete di test Linea Goerli" + }, "links": { "message": "Collegamenti" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 797c534353cc..a78e13c1957a 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Goerliテストネットワークに接続中" }, + "connectingToLineaTestnet": { + "message": "Linea Goerli テストネットワークに接続中" + }, "connectingToMainnet": { "message": "イーサリアムメインネットに接続中" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "これらのトークンを追加しますか?" }, + "lineatestnet": { + "message": "Linea Goerli テストネットワーク" + }, "link": { "message": "リンク" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 2c58b461fcb4..f4b75091e0d5 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Goerli ಪರೀಕ್ಷಾ ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ" }, + "connectingToLineaTestnet": { + "message": "Linea Goerli ಪರೀಕ್ಷಾ ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ" + }, "connectingToMainnet": { "message": "ಮುಖ್ಯ ಎಥೆರಿಯಮ್ ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "ನೀವು ಈ ಟೋಕನ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಬಯಸುತ್ತೀರಾ?" }, + "lineatestnet": { + "message": "Linea Goerli ಪರೀಕ್ಷೆ ನೆಟ್‌ವರ್ಕ್" + }, "links": { "message": "ಲಿಂಕ್‌ಗಳು" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 9988fc17b363..4fd89911004b 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Goerli 테스트 네트워크에 연결 중" }, + "connectingToLineaTestnet": { + "message": "Linea Goerli 테스트 네트워크에 연결 중" + }, "connectingToMainnet": { "message": "이더리움 메인넷에 연결 중" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "이 토큰을 추가할까요?" }, + "lineatestnet": { + "message": "Linea Goerli 테스트 네트워크" + }, "link": { "message": "링크" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 76765856fafa..e5942963d503 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Jungiamasi prie „Goerli“ bandomojo tinklo" }, + "connectingToLineaTestnet": { + "message": "Jungiamasi prie „Linea“ bandomojo tinklo" + }, "connectingToMainnet": { "message": "Jungiamasi prie pagrindinio „Ethereum“ tinklo" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "Ar norėtumėte pridėti šiuos žetonus?" }, + "lineatestnet": { + "message": "„Linea“ bandomasis tinklas" + }, "links": { "message": "Nuorodos" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 4a16847d0289..906b990e743e 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Pieslēdzas Goerli testa tīklam" }, + "connectingToLineaTestnet": { + "message": "Pieslēdzas Linea Goerli testa tīklam" + }, "connectingToMainnet": { "message": "Savienojas ar galveno Ethereum tīklu" }, @@ -433,6 +436,9 @@ "likeToImportTokens": { "message": "Vai vēlaties pievienot šos marķierus?" }, + "lineatestnet": { + "message": "Linea Goerli testa tīkls" + }, "links": { "message": "Saites" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 95865e4c2552..86a29324f4dd 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Menyambung kepada Rangkaian Ujian Goerli" }, + "connectingToLineaTestnet": { + "message": "Menyambung kepada Rangkaian Ujian Linea Goerli" + }, "connectingToMainnet": { "message": "Menyambung kepada Rangkaian Ethereum Utama" }, @@ -426,6 +429,9 @@ "likeToImportTokens": { "message": "Adakah anda ingin menambah token ini?" }, + "lineatestnet": { + "message": "Rangkaian Ujian Linea Goerli" + }, "links": { "message": "Pautan" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 46788f1c26cf..b8dfff373cd8 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -175,6 +175,9 @@ "connectingToGoerli": { "message": "Oppretter forbindelse med Goerli Test Network" }, + "connectingToLineaTestnet": { + "message": "Oppretter forbindelse med Linea Goerli Test Network" + }, "connectingToMainnet": { "message": "Forbinder med hoved-Ethereumnettverk " }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index c972807c7de1..5f89b000aa84 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -332,6 +332,9 @@ "connectingToGoerli": { "message": "Kumokonekta sa Goerli Test Network" }, + "connectingToLineaTestnet": { + "message": "Kumokonekta sa Linea Goerli Test Network" + }, "connectingToMainnet": { "message": "Kumokonekta sa Ethereum Mainnet" }, @@ -840,6 +843,9 @@ "likeToImportTokens": { "message": "Gusto mo bang idagdag ang mga token na ito?" }, + "lineatestnet": { + "message": "Linea Goerli Test Network" + }, "links": { "message": "Mga Link" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index a403f46de45e..424615fa469a 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Łączenie z siecią testową Goerli" }, + "connectingToLineaTestnet": { + "message": "Łączenie z siecią testową Linea Goerli" + }, "connectingToMainnet": { "message": "Łączenie z główną siecią Ethereum" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "Czy chcesz dodać te tokeny?" }, + "lineatestnet": { + "message": "Sieć testowa Linea Goerli" + }, "links": { "message": "Łącza" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index bfe7febde71d..7f1f61efde7d 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Conectando à rede de testes Goerli" }, + "connectingToLineaTestnet": { + "message": "Conectando à rede de teste Linea Goerli" + }, "connectingToMainnet": { "message": "Conectando à mainnet do Ethereum" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Gostaria de adicionar estes tokens?" }, + "lineatestnet": { + "message": "Rede de teste Linea Goerli" + }, "link": { "message": "Link" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index ae06eebdc6ce..e5d15ee4280d 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -469,6 +469,9 @@ "connectingToGoerli": { "message": "Conectando à rede de testes Goerli" }, + "connectingToLineaTestnet": { + "message": "Conectando à rede de testes Linea Goerli" + }, "connectingToMainnet": { "message": "Conectando à mainnet do Ethereum" }, @@ -1308,6 +1311,9 @@ "likeToImportTokens": { "message": "Você gostaria de importar esses tokens?" }, + "lineatestnet": { + "message": "Rede de testes Linea Goerli" + }, "link": { "message": "Link" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 44efbb0cce48..3592f2faa25d 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Se conectează la rețeaua de test Goerli" }, + "connectingToLineaTestnet": { + "message": "Se conectează la rețeaua de test Linea Goerli" + }, "connectingToMainnet": { "message": "Se conectează la rețeaua Ethereum principală" }, @@ -427,6 +430,9 @@ "likeToImportTokens": { "message": "Adăugați aceste indicative?" }, + "lineatestnet": { + "message": "Rețea de test Linea Goerli" + }, "links": { "message": "Link-uri" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 2221e078be76..efc0c1ec706b 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Подключение к тестовой сети Goerli..." }, + "connectingToLineaTestnet": { + "message": "Подключение к тестовой сети Linea..." + }, "connectingToMainnet": { "message": "Подключение к сети Ethereum Mainnet..." }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Вы хотели бы импортировать эти токены?" }, + "lineatestnet": { + "message": "Тестовая сеть Linea Goerli" + }, "link": { "message": "Привязать" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index b43ecd63fb83..29b29567cb82 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -172,6 +172,9 @@ "connectingToGoerli": { "message": "Pripája sa k testovacej sieti Goerli" }, + "connectingToLineaTestnet": { + "message": "Pripája sa k testovacej sieti Linea Goerli" + }, "connectingToMainnet": { "message": "Připojuji se k Ethereum Mainnet" }, @@ -424,6 +427,9 @@ "likeToImportTokens": { "message": "Chcete přidat tyto tokeny?" }, + "lineatestnet": { + "message": "Testovacia sieť Linea Goerli" + }, "links": { "message": "Odkazy" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index fca992011a2b..386e4e8a8366 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Povezovanje na testno omrežje Goerli" }, + "connectingToLineaTestnet": { + "message": "Povezovanje na testno omrežje Linea Goerli" + }, "connectingToMainnet": { "message": "Povezovanje na glavno omrežje" }, @@ -431,6 +434,9 @@ "likeToImportTokens": { "message": "Želite dodati te žetone?" }, + "lineatestnet": { + "message": "Testno omrežje Linea Goerli" + }, "links": { "message": "Povezave" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index 72e8eb829671..5308a5653315 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -175,6 +175,9 @@ "connectingToGoerli": { "message": "Povezuje se sa test mrežom Goerli " }, + "connectingToLineaTestnet": { + "message": "Povezuje se sa test mrežom Linea Goerli" + }, "connectingToMainnet": { "message": "Povezuje se na glavnu Ethereum mrežu" }, @@ -434,6 +437,9 @@ "likeToImportTokens": { "message": "Želite li da dodate ove tokene?" }, + "lineatestnet": { + "message": "Test mreža Linea Goerli" + }, "links": { "message": "Veze" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index b73e7c7f552d..a5b3659165ef 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -172,6 +172,9 @@ "connectingToGoerli": { "message": "Ansluter till Goerli Test Network" }, + "connectingToLineaTestnet": { + "message": "Ansluter till Linea Goerli Test Network" + }, "connectingToMainnet": { "message": "Koppla till Ethereums huvudnätverk" }, @@ -427,6 +430,9 @@ "likeToImportTokens": { "message": "Vill du lägga till dessa tokens?" }, + "lineatestnet": { + "message": "Linea Goerli testnätverk" + }, "links": { "message": "Länkar" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 59e28b5c7837..4884f08c64bc 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -172,6 +172,9 @@ "connectingToGoerli": { "message": "Inaunganisha kwenye Mtandao wa Majaribio wa Goerli" }, + "connectingToLineaTestnet": { + "message": "Inaunganisha kwenye Mtandao wa Majaribio wa Linea Goerli" + }, "connectingToMainnet": { "message": "Inaunganisha kwenye Mtandao Mkuu wa Ethereum" }, @@ -424,6 +427,9 @@ "likeToImportTokens": { "message": "Je, ungependa kuongeza vianzio hivi?" }, + "lineatestnet": { + "message": "Mtandao wa Majaribio wa Linea Goerli" + }, "links": { "message": "Viungo" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index ef92e1c314a0..d49004713fed 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Kumokonekta sa Goerli Test Network" }, + "connectingToLineaTestnet": { + "message": "Kumokonekta sa Linea Goerli test network" + }, "connectingToMainnet": { "message": "Kumokonekta sa Ethereum Mainnet" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Gusto mo bang idagdag ang mga token na ito?" }, + "lineatestnet": { + "message": "Linea Goerli test network" + }, "link": { "message": "Link" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index bc120bb1161c..ffa5f6c6a63f 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Goerli Test Ağına Bağlanıyor" }, + "connectingToLineaTestnet": { + "message": "Linea Goerli test ağına bağlanılıyor" + }, "connectingToMainnet": { "message": "Ethereum Mainnet ağına bağlanıyor" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Bu tokenleri içe aktarmak ister misiniz?" }, + "lineatestnet": { + "message": "Linea Goerli test ağı" + }, "link": { "message": "Bağlantı" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 69fa453ba361..3a1748a5deeb 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -178,6 +178,9 @@ "connectingToGoerli": { "message": "Підключення до тестової мережі Goerli" }, + "connectingToLineaTestnet": { + "message": "Підключення до тестової мережі Linea Goerli" + }, "connectingToMainnet": { "message": "З'єднуємось з Головною мережею Ethereum" }, @@ -437,6 +440,9 @@ "likeToImportTokens": { "message": "Ви б хотіли додати ці токени?" }, + "lineatestnet": { + "message": "Тестова мережа Linea Goerli" + }, "links": { "message": "Посилання" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index bc03a062d7b6..0fbff37abfa8 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "Đang kết nối với mạng thử nghiệm Goerli" }, + "connectingToLineaTestnet": { + "message": "Đang kết nối với mạng thử nghiệm Linea Goerli" + }, "connectingToMainnet": { "message": "Đang kết nối với mạng chính thức của Ethereum" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "Bạn có muốn nhập những token này không?" }, + "lineatestnet": { + "message": "Mạng thử nghiệm Linea Goerli" + }, "link": { "message": "Liên kết" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index e7f16c271671..986e750c1ea1 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -736,6 +736,9 @@ "connectingToGoerli": { "message": "正在连接 Goerli 测试网络" }, + "connectingToLineaTestnet": { + "message": "正在连接Linea测试网络" + }, "connectingToMainnet": { "message": "正在连接到以太坊主网" }, @@ -1813,6 +1816,9 @@ "likeToImportTokens": { "message": "您想导入这些代币吗?" }, + "lineatestnet": { + "message": "Linea测试网络" + }, "link": { "message": "链接" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 16b1843a4e12..6d56de0c32df 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -331,6 +331,9 @@ "connectingToGoerli": { "message": "連線到 Goerli 測試網路" }, + "connectingToLineaTestnet": { + "message": "連線到 Linea Goerli 測試網路" + }, "connectingToMainnet": { "message": "連線到 Ethereum 主網路" }, @@ -854,6 +857,9 @@ "likeToImportTokens": { "message": "確定要加入代幣?" }, + "lineatestnet": { + "message": "Linea Goerli 測試網路" + }, "links": { "message": "連結" }, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js index 7c5edc155ec8..c3a0bbfe8e78 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js @@ -110,7 +110,8 @@ async function switchEthereumChainHandler( }); if ( chainId in CHAIN_ID_TO_TYPE_MAP && - approvedRequestData.type !== NETWORK_TYPES.LOCALHOST + approvedRequestData.type !== NETWORK_TYPES.LOCALHOST && + approvedRequestData.type !== NETWORK_TYPES.LINEA_TESTNET ) { setProviderType(approvedRequestData.type); } else { diff --git a/development/states/navigate-txs.json b/development/states/navigate-txs.json index 68d2535a2d13..6a4605e9afa1 100644 --- a/development/states/navigate-txs.json +++ b/development/states/navigate-txs.json @@ -304,7 +304,8 @@ "infuraNetworkStatus": { "mainnet": "ok", "goerli": "ok", - "sepolia": "ok" + "sepolia": "ok", + "lineatestnet": "ok" } }, "send": { diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 84314ac3dd58..a11db2b5e42c 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -165,6 +165,7 @@ export const NETWORK_TYPES = { MAINNET: 'mainnet', RPC: 'rpc', SEPOLIA: 'sepolia', + LINEA_TESTNET: 'lineatestnet', } as const; /** @@ -190,6 +191,7 @@ export const NETWORK_IDS = { GOERLI: '5', LOCALHOST: '1337', SEPOLIA: '11155111', + LINEA_TESTNET: '59140', } as const; /** @@ -211,6 +213,7 @@ export const CHAIN_IDS = { HARMONY: '0x63564c40', PALM: '0x2a15c308d', SEPOLIA: '0xaa36a7', + LINEA_TESTNET: '0xe704', AURORA: '0x4e454152', } as const; @@ -223,6 +226,7 @@ export const MAX_SAFE_CHAIN_ID = 4503599627370476; export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; export const GOERLI_DISPLAY_NAME = 'Goerli'; export const SEPOLIA_DISPLAY_NAME = 'Sepolia'; +export const LINEA_TESTNET_DISPLAY_NAME = 'Linea Goerli test network'; export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545'; export const BSC_DISPLAY_NAME = 'Binance Smart Chain'; export const POLYGON_DISPLAY_NAME = 'Polygon'; @@ -252,6 +256,7 @@ export const MAINNET_RPC_URL = getRpcUrl({ }); export const GOERLI_RPC_URL = getRpcUrl({ network: NETWORK_TYPES.GOERLI }); export const SEPOLIA_RPC_URL = getRpcUrl({ network: NETWORK_TYPES.SEPOLIA }); +export const LINEA_TESTNET_RPC_URL = 'https://rpc.goerli.linea.build'; export const LOCALHOST_RPC_URL = 'http://localhost:8545'; /** @@ -428,6 +433,7 @@ export const INFURA_PROVIDER_TYPES = [ export const TEST_CHAINS = [ CHAIN_IDS.GOERLI, CHAIN_IDS.SEPOLIA, + CHAIN_IDS.LINEA_TESTNET, CHAIN_IDS.LOCALHOST, ]; @@ -446,6 +452,10 @@ export const TEST_NETWORK_TICKER_MAP: { [NETWORK_TYPES.SEPOLIA]: `${typedCapitalize(NETWORK_TYPES.SEPOLIA)}${ CURRENCY_SYMBOLS.ETH }`, + [NETWORK_TYPES.LINEA_TESTNET]: + `Linea${CURRENCY_SYMBOLS.ETH}` as `${Capitalize< + typeof NETWORK_TYPES.LINEA_TESTNET + >}${typeof CURRENCY_SYMBOLS.ETH}`, }; /** @@ -462,6 +472,12 @@ export const BUILT_IN_NETWORKS = { chainId: CHAIN_IDS.SEPOLIA, ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA], }, + [NETWORK_TYPES.LINEA_TESTNET]: { + networkId: NETWORK_IDS.LINEA_TESTNET, + chainId: CHAIN_IDS.LINEA_TESTNET, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_TESTNET], + blockExplorerUrl: 'https://explorer.goerli.linea.build', + }, [NETWORK_TYPES.MAINNET]: { networkId: NETWORK_IDS.MAINNET, chainId: CHAIN_IDS.MAINNET, @@ -476,15 +492,18 @@ export const NETWORK_TO_NAME_MAP = { [NETWORK_TYPES.MAINNET]: MAINNET_DISPLAY_NAME, [NETWORK_TYPES.GOERLI]: GOERLI_DISPLAY_NAME, [NETWORK_TYPES.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_TESTNET]: LINEA_TESTNET_DISPLAY_NAME, [NETWORK_TYPES.LOCALHOST]: LOCALHOST_DISPLAY_NAME, [NETWORK_IDS.GOERLI]: GOERLI_DISPLAY_NAME, [NETWORK_IDS.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + [NETWORK_IDS.LINEA_TESTNET]: LINEA_TESTNET_DISPLAY_NAME, [NETWORK_IDS.MAINNET]: MAINNET_DISPLAY_NAME, [NETWORK_IDS.LOCALHOST]: LOCALHOST_DISPLAY_NAME, [CHAIN_IDS.GOERLI]: GOERLI_DISPLAY_NAME, [CHAIN_IDS.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.LINEA_TESTNET]: LINEA_TESTNET_DISPLAY_NAME, [CHAIN_IDS.MAINNET]: MAINNET_DISPLAY_NAME, [CHAIN_IDS.LOCALHOST]: LOCALHOST_DISPLAY_NAME, } as const; @@ -493,12 +512,14 @@ export const CHAIN_ID_TO_TYPE_MAP = { [CHAIN_IDS.MAINNET]: NETWORK_TYPES.MAINNET, [CHAIN_IDS.GOERLI]: NETWORK_TYPES.GOERLI, [CHAIN_IDS.SEPOLIA]: NETWORK_TYPES.SEPOLIA, + [CHAIN_IDS.LINEA_TESTNET]: NETWORK_TYPES.LINEA_TESTNET, [CHAIN_IDS.LOCALHOST]: NETWORK_TYPES.LOCALHOST, } as const; export const CHAIN_ID_TO_RPC_URL_MAP = { [CHAIN_IDS.GOERLI]: GOERLI_RPC_URL, [CHAIN_IDS.SEPOLIA]: SEPOLIA_RPC_URL, + [CHAIN_IDS.LINEA_TESTNET]: LINEA_TESTNET_RPC_URL, [CHAIN_IDS.MAINNET]: MAINNET_RPC_URL, [CHAIN_IDS.LOCALHOST]: LOCALHOST_RPC_URL, } as const; @@ -520,6 +541,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { export const NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP = { [NETWORK_IDS.GOERLI]: NETWORK_TYPES.GOERLI, [NETWORK_IDS.SEPOLIA]: NETWORK_TYPES.SEPOLIA, + [NETWORK_IDS.LINEA_TESTNET]: NETWORK_TYPES.LINEA_TESTNET, [NETWORK_IDS.MAINNET]: NETWORK_NAMES.HOMESTEAD, } as const; @@ -527,6 +549,7 @@ export const CHAIN_ID_TO_NETWORK_ID_MAP = { [CHAIN_IDS.MAINNET]: NETWORK_IDS.MAINNET, [CHAIN_IDS.GOERLI]: NETWORK_IDS.GOERLI, [CHAIN_IDS.SEPOLIA]: NETWORK_IDS.SEPOLIA, + [CHAIN_IDS.LINEA_TESTNET]: NETWORK_IDS.LINEA_TESTNET, [CHAIN_IDS.LOCALHOST]: NETWORK_IDS.LOCALHOST, } as const; @@ -591,6 +614,7 @@ export const BUYABLE_CHAINS_MAP: { | typeof CHAIN_IDS.PALM | typeof CHAIN_IDS.HARMONY | typeof CHAIN_IDS.OPTIMISM_TESTNET + | typeof CHAIN_IDS.LINEA_TESTNET >]: BuyableChainSettings; } = { [CHAIN_IDS.MAINNET]: { @@ -982,3 +1006,6 @@ export const FEATURED_RPCS: RPCDefinition[] = [ }, }, ]; + +export const SHOULD_SHOW_LINEA_TESTNET_NETWORK = + new Date().getTime() > Date.UTC(2023, 2, 28); diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index f54c8ddcef0e..0e1884441142 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -7,8 +7,13 @@ import Button from '../../ui/button'; import * as actions from '../../../store/actions'; import { openAlert as displayInvalidCustomNetworkAlert } from '../../../ducks/alerts/invalid-custom-network'; import { + BUILT_IN_NETWORKS, + CHAIN_ID_TO_RPC_URL_MAP, + LINEA_TESTNET_RPC_URL, LOCALHOST_RPC_URL, + NETWORK_TO_NAME_MAP, NETWORK_TYPES, + SHOULD_SHOW_LINEA_TESTNET_NETWORK, } from '../../../../shared/constants/network'; import { isPrefixedFormattedHexString } from '../../../../shared/modules/network.utils'; @@ -57,8 +62,12 @@ function mapDispatchToProps(dispatch) { setProviderType: (type) => { dispatch(actions.setProviderType(type)); }, - setRpcTarget: (target, chainId, ticker, nickname) => { - dispatch(actions.setRpcTarget(target, chainId, ticker, nickname)); + setRpcTarget: (target, chainId, ticker, nickname, { blockExplorerUrl }) => { + dispatch( + actions.setRpcTarget(target, chainId, ticker, nickname, { + blockExplorerUrl, + }), + ); }, hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), displayInvalidCustomNetworkAlert: (networkName) => { @@ -224,6 +233,8 @@ class NetworkDropdown extends Component { return t('goerli'); case NETWORK_TYPES.SEPOLIA: return t('sepolia'); + case NETWORK_TYPES.LINEA_TESTNET: + return t('lineatestnet'); case NETWORK_TYPES.LOCALHOST: return t('localhost'); default: @@ -268,6 +279,55 @@ class NetworkDropdown extends Component { ); } + renderNonInfuraDefaultNetwork(network) { + const { + provider: { type: providerType }, + setRpcTarget, + } = this.props; + + const isCurrentRpcTarget = providerType === NETWORK_TYPES.RPC; + return ( + { + const { chainId, ticker, blockExplorerUrl } = + BUILT_IN_NETWORKS[network]; + const networkName = NETWORK_TO_NAME_MAP[network]; + + const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; + await setRpcTarget(rpcUrl, chainId, ticker, networkName, { + blockExplorerUrl, + }); + }} + style={DROP_DOWN_MENU_ITEM_STYLE} + > + {isCurrentRpcTarget ? ( + + ) : ( +
+ )} + + + {this.context.t(network)} + +
+ ); + } + render() { const { history, @@ -277,9 +337,12 @@ class NetworkDropdown extends Component { showTestnetMessageInDropdown, hideTestNetMessage, } = this.props; + const rpcListDetail = this.props.frequentRpcListDetail; - const rpcListDetailWithoutLocalHost = rpcListDetail.filter( - (rpc) => rpc.rpcUrl !== LOCALHOST_RPC_URL, + const rpcListDetailWithoutLocalHostAndLinea = rpcListDetail.filter( + (rpc) => + rpc.rpcUrl !== LOCALHOST_RPC_URL && + rpc.rpcUrl !== LINEA_TESTNET_RPC_URL, ); const rpcListDetailForLocalHost = rpcListDetail.filter( (rpc) => rpc.rpcUrl === LOCALHOST_RPC_URL, @@ -352,7 +415,7 @@ class NetworkDropdown extends Component { {this.renderNetworkEntry(NETWORK_TYPES.MAINNET)} {this.renderCustomRpcList( - rpcListDetailWithoutLocalHost, + rpcListDetailWithoutLocalHostAndLinea, this.props.provider, )} @@ -360,6 +423,13 @@ class NetworkDropdown extends Component { <> {this.renderNetworkEntry(NETWORK_TYPES.GOERLI)} {this.renderNetworkEntry(NETWORK_TYPES.SEPOLIA)} + {SHOULD_SHOW_LINEA_TESTNET_NETWORK && ( + <> + {this.renderNonInfuraDefaultNetwork( + NETWORK_TYPES.LINEA_TESTNET, + )} + + )} {this.renderCustomRpcList( rpcListDetailForLocalHost, this.props.provider, diff --git a/ui/components/app/dropdowns/network-dropdown.test.js b/ui/components/app/dropdowns/network-dropdown.test.js index 727b8fadcdc8..e02804fa3b19 100644 --- a/ui/components/app/dropdowns/network-dropdown.test.js +++ b/ui/components/app/dropdowns/network-dropdown.test.js @@ -6,6 +6,15 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { LOCALHOST_RPC_URL } from '../../../../shared/constants/network'; import NetworkDropdown from './network-dropdown'; +// Mock linea test network feature toggle +jest.mock('../../../../shared/constants/network', () => { + const constants = jest.requireActual('../../../../shared/constants/network'); + return { + ...constants, + SHOULD_SHOW_LINEA_TESTNET_NETWORK: true, + }; +}); + describe('Network Dropdown', () => { const createMockStore = configureMockStore([thunk]); @@ -97,6 +106,13 @@ describe('Network Dropdown', () => { expect(localhostColorIndicator).toBeInTheDocument(); }); + it('checks background color for seventh ColorIndicator', () => { + const lineaColorIndicator = screen.queryByTestId( + 'color-icon-lineatestnet', + ); + expect(lineaColorIndicator).toBeInTheDocument(); + }); + it('checks that Add Network button is rendered', () => { const addNetworkButton = screen.queryByText('Add network'); expect(addNetworkButton).toBeInTheDocument(); @@ -104,8 +120,7 @@ describe('Network Dropdown', () => { it('shows test networks in the dropdown', () => { const networkItems = screen.queryAllByTestId(/network-item/u); - - expect(networkItems).toHaveLength(6); + expect(networkItems).toHaveLength(7); }); }); diff --git a/ui/components/app/loading-network-screen/loading-network-screen.component.js b/ui/components/app/loading-network-screen/loading-network-screen.component.js index bf241cfcea4f..9a258557da25 100644 --- a/ui/components/app/loading-network-screen/loading-network-screen.component.js +++ b/ui/components/app/loading-network-screen/loading-network-screen.component.js @@ -49,6 +49,8 @@ export default class LoadingNetworkScreen extends PureComponent { return t('connectingToGoerli'); case NETWORK_TYPES.SEPOLIA: return t('connectingToSepolia'); + case NETWORK_TYPES.LINEA_TESTNET: + return t('connectingToLineaTestnet'); default: return t('connectingTo', [providerId]); } diff --git a/ui/components/app/signature-request-original/signature-request-original.component.js b/ui/components/app/signature-request-original/signature-request-original.component.js index 6e5fd5313050..9c3039de422b 100644 --- a/ui/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/components/app/signature-request-original/signature-request-original.component.js @@ -72,6 +72,8 @@ export default class SignatureRequestOriginal extends Component { return t('goerli'); case NETWORK_TYPES.SEPOLIA: return t('sepolia'); + case NETWORK_TYPES.LINEA_TESTNET: + return t('lineatestnet'); case NETWORK_TYPES.LOCALHOST: return t('localhost'); default: diff --git a/ui/components/app/signature-request/signature-request.component.js b/ui/components/app/signature-request/signature-request.component.js index d9cf0f88cf38..86b631a64b66 100644 --- a/ui/components/app/signature-request/signature-request.component.js +++ b/ui/components/app/signature-request/signature-request.component.js @@ -113,6 +113,8 @@ export default class SignatureRequest extends PureComponent { return t('goerli'); case NETWORK_TYPES.SEPOLIA: return t('sepolia'); + case NETWORK_TYPES.LINEA_TESTNET: + return t('lineatestnet'); case NETWORK_TYPES.LOCALHOST: return t('localhost'); default: diff --git a/ui/components/ui/typography/typography.js b/ui/components/ui/typography/typography.js index 19cbabccb78a..5557c325bd12 100644 --- a/ui/components/ui/typography/typography.js +++ b/ui/components/ui/typography/typography.js @@ -32,6 +32,8 @@ export const ValidColors = [ Color.sepolia, Color.goerli, Color.sepoliaInverse, + Color.lineaTestnet, + Color.lineaTestnetInverse, ]; export const ValidTags = [ diff --git a/ui/css/design-system/colors.scss b/ui/css/design-system/colors.scss index 55c3a681188c..0b3e330bf371 100644 --- a/ui/css/design-system/colors.scss +++ b/ui/css/design-system/colors.scss @@ -41,6 +41,8 @@ $color-map: ( 'sepolia': --color-network-sepolia-default, 'goerli-inverse':--color-network-goerli-inverse, 'sepolia-inverse': --color-network-sepolia-inverse, + 'lineatestnet': --color-network-linea-testnet-default, + 'lineatestnet-inverse': --color-network-linea-testnet-inverse, 'localhost': --color-network-localhost-default, 'transparent': --transparent, 'flask-purple': --color-flask-default, diff --git a/ui/css/itcss/components/network.scss b/ui/css/itcss/components/network.scss index c89f93d55f89..f7baf1a07a7c 100644 --- a/ui/css/itcss/components/network.scss +++ b/ui/css/itcss/components/network.scss @@ -25,6 +25,10 @@ background-color: rgba(207, 181, 240, 0.7) !important; } + &.linea-test-network .menu-icon-circle div { + background-color: rgba(0, 0, 0, 0.7) !important; + } + &.localhost-network .menu-icon-circle div { background-color: rgba(3, 135, 137, 0.7) !important; } diff --git a/ui/css/utilities/colors.scss b/ui/css/utilities/colors.scss index 221786dcf672..7d2e8e7c88cd 100644 --- a/ui/css/utilities/colors.scss +++ b/ui/css/utilities/colors.scss @@ -4,6 +4,8 @@ --mainnet: #29b6af; --inherit: inherit; --transparent: transparent; + --color-network-linea-testnet-default: #000; + --color-network-linea-testnet-inverse: #fcfcfc; // DO NOT CHANGE // Required for the QR reader to work properly --qr-code-white-background: #fff; diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index a73831274881..f2490be01312 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -46,6 +46,8 @@ export enum Color { mainnet = 'mainnet', goerli = 'goerli', sepolia = 'sepolia', + lineaTestnet = 'lineatestnet', + lineaTestnetInverse = 'lineatestnet-inverse', transparent = 'transparent', localhost = 'localhost', inherit = 'inherit', @@ -75,6 +77,7 @@ export enum BackgroundColor { mainnet = 'mainnet', goerli = 'goerli', sepolia = 'sepolia', + lineaTestnet = 'lineatestnet', transparent = 'transparent', localhost = 'localhost', } @@ -100,6 +103,7 @@ export enum BorderColor { mainnet = 'mainnet', goerli = 'goerli', sepolia = 'sepolia', + lineaTestnet = 'lineatestnet', transparent = 'transparent', localhost = 'localhost', } @@ -121,6 +125,8 @@ export enum TextColor { inherit = 'inherit', goerli = 'goerli', sepolia = 'sepolia', + lineaTestnet = 'lineatestnet', + lineaTestnetInverse = 'lineatestnet-inverse', goerliInverse = 'goerli-inverse', sepoliaInverse = 'sepolia-inverse', } @@ -143,6 +149,8 @@ export enum IconColor { inherit = 'inherit', goerli = 'goerli', sepolia = 'sepolia', + lineaTestnet = 'lineatestnet', + lineaTestnetInverse = 'lineatestnet-inverse', goerliInverse = 'goerli-inverse', sepoliaInverse = 'sepolia-inverse', } diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index bbbfd581264d..391bfd4c73c7 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -254,6 +254,13 @@ export const SETTINGS_CONSTANTS = [ route: `${NETWORKS_ROUTE}#networks-sepolia`, icon: 'fa fa-plug', }, + { + tabMessage: (t) => t('networks'), + sectionMessage: (t) => t('lineatestnet'), + descriptionMessage: (t) => t('lineatestnet'), + route: `${NETWORKS_ROUTE}#networks-lineatestnet`, + icon: 'fa fa-plug', + }, { tabMessage: (t) => t('networks'), sectionMessage: (t) => t('localhost'), diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 74c2c08b4eef..f78c498b5254 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -177,7 +177,7 @@ describe('Settings Search Utils', () => { }); it('should get good network section number', () => { - expect(getNumberOfSettingsInSection(t, t('networks'))).toStrictEqual(4); + expect(getNumberOfSettingsInSection(t, t('networks'))).toStrictEqual(5); }); it('should get good experimental section number', () => { diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index f6804c1f1647..95c841bd5446 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -56,6 +56,7 @@ export function isDefaultMetaMaskChain(chainId) { chainId === CHAIN_IDS.MAINNET || chainId === CHAIN_IDS.GOERLI || chainId === CHAIN_IDS.SEPOLIA || + chainId === CHAIN_IDS.LINEA_TESTNET || chainId === CHAIN_IDS.LOCALHOST ) { return true; diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index e4d152658d80..36fa14b433e3 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -1,5 +1,6 @@ import Bowser from 'bowser'; import { BN } from 'ethereumjs-util'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { addHexPrefixToObjectValues } from '../../../shared/lib/swaps-utils'; import { toPrecisionWithoutTrailingZeros } from '../../../shared/lib/transactions-controller-utils'; import * as util from './util'; @@ -589,4 +590,14 @@ describe('util', () => { ).toStrictEqual('The Quick Brown \\u202EFox Jumps Over The Lazy Dog'); }); }); + + describe('isDefaultMetaMaskChain()', () => { + it('should return true if the provided chainId is a default MetaMask chain', () => { + expect(util.isDefaultMetaMaskChain(CHAIN_IDS.GOERLI)).toBeTruthy(); + }); + + it('should return false if the provided chainId is a not default MetaMask chain', () => { + expect(util.isDefaultMetaMaskChain(CHAIN_IDS.CELO)).toBeFalsy(); + }); + }); }); diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index cca89123156d..2d8ccfbe5f90 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -472,6 +472,8 @@ export default class Routes extends Component { return t('connectingToGoerli'); case NETWORK_TYPES.SEPOLIA: return t('connectingToSepolia'); + case NETWORK_TYPES.LINEA_TESTNET: + return t('connectingToLineaTestnet'); default: return t('connectingTo', [providerId]); } diff --git a/ui/pages/settings/networks-tab/networks-tab.constants.js b/ui/pages/settings/networks-tab/networks-tab.constants.js index 088d8a991677..ef34723b99b7 100644 --- a/ui/pages/settings/networks-tab/networks-tab.constants.js +++ b/ui/pages/settings/networks-tab/networks-tab.constants.js @@ -4,6 +4,7 @@ import { CURRENCY_SYMBOLS, CHAIN_IDS, NETWORK_TYPES, + LINEA_TESTNET_RPC_URL, } from '../../../../shared/constants/network'; const defaultNetworksData = [ @@ -43,6 +44,15 @@ const defaultNetworksData = [ ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA], blockExplorerUrl: 'https://sepolia.etherscan.io', }, + { + labelKey: NETWORK_TYPES.LINEA_TESTNET, + iconColor: '#234FD5', + providerType: NETWORK_TYPES.LINEA_TESTNET, + rpcUrl: LINEA_TESTNET_RPC_URL, + chainId: CHAIN_IDS.LINEA_TESTNET, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_TESTNET], + blockExplorerUrl: 'https://explorer.goerli.linea.build', + }, ]; export { defaultNetworksData }; diff --git a/ui/pages/settings/networks-tab/networks-tab.js b/ui/pages/settings/networks-tab/networks-tab.js index 287b187df5d3..785cbcf3214b 100644 --- a/ui/pages/settings/networks-tab/networks-tab.js +++ b/ui/pages/settings/networks-tab/networks-tab.js @@ -20,7 +20,9 @@ import { getProvider, } from '../../../selectors'; import { + CHAIN_IDS, NETWORK_TYPES, + SHOULD_SHOW_LINEA_TESTNET_NETWORK, TEST_CHAINS, } from '../../../../shared/constants/network'; import { defaultNetworksData } from './networks-tab.constants'; @@ -51,23 +53,29 @@ const NetworksTab = ({ addNewNetwork }) => { const provider = useSelector(getProvider); const networksTabSelectedRpcUrl = useSelector(getNetworksTabSelectedRpcUrl); - const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => { - return { - label: rpc.nickname, - iconColor: 'var(--color-icon-alternative)', - providerType: NETWORK_TYPES.RPC, - rpcUrl: rpc.rpcUrl, - chainId: rpc.chainId, - ticker: rpc.ticker, - blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '', - isATestNetwork: TEST_CHAINS.includes(rpc.chainId), - }; - }); + const frequentRpcNetworkListDetails = frequentRpcListDetail + .map((rpc) => { + return { + label: rpc.nickname, + iconColor: 'var(--color-icon-alternative)', + providerType: NETWORK_TYPES.RPC, + rpcUrl: rpc.rpcUrl, + chainId: rpc.chainId, + ticker: rpc.ticker, + blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '', + isATestNetwork: TEST_CHAINS.includes(rpc.chainId), + }; + }) + .filter((network) => network.chainId !== CHAIN_IDS.LINEA_TESTNET); + + let networksToRender = [...defaultNetworks, ...frequentRpcNetworkListDetails]; + + if (!SHOULD_SHOW_LINEA_TESTNET_NETWORK) { + networksToRender = networksToRender.filter( + (network) => network.chainId !== CHAIN_IDS.LINEA_TESTNET, + ); + } - const networksToRender = [ - ...defaultNetworks, - ...frequentRpcNetworkListDetails, - ]; let selectedNetwork = networksToRender.find( (network) => network.rpcUrl === networksTabSelectedRpcUrl, diff --git a/ui/store/actions.ts b/ui/store/actions.ts index de453b6b1932..50fa81bf35f2 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -2512,6 +2512,7 @@ export function setRpcTarget( chainId: string, ticker?: EtherDenomination, nickname?: string, + rpcPrefs?: object, ): ThunkAction { return async (dispatch) => { log.debug( @@ -2524,6 +2525,7 @@ export function setRpcTarget( chainId, ticker ?? EtherDenomination.ETH, nickname || newRpcUrl, + rpcPrefs, ]); } catch (error) { logErrorWithMessage(error); From 98ed05c7c0200eee4a068ab25164ba0231b72bc3 Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Tue, 21 Mar 2023 17:21:00 +0000 Subject: [PATCH 02/15] Version v10.27.0 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aca89dc0deeb..82be5abced68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.27.0] +### Uncategorized +- feat: add the ConsenSys zkEVM (Linea) as a default network ([#17875](https://github.com/MetaMask/metamask-extension/pull/17875)) + ## [10.26.2] ### Changed - Sign in with Ethereum: re-enable warning UI for mismatched domains / disable domain binding ([#18200](https://github.com/MetaMask/metamask-extension/pull/18200)) @@ -3536,7 +3540,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.26.2...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.27.0...HEAD +[10.27.0]: https://github.com/MetaMask/metamask-extension/compare/v10.26.2...v10.27.0 [10.26.2]: https://github.com/MetaMask/metamask-extension/compare/v10.26.1...v10.26.2 [10.26.1]: https://github.com/MetaMask/metamask-extension/compare/v10.26.0...v10.26.1 [10.26.0]: https://github.com/MetaMask/metamask-extension/compare/v10.25.0...v10.26.0 diff --git a/package.json b/package.json index a298877942ba..fea8dd0211ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "10.26.2", + "version": "10.27.0", "private": true, "repository": { "type": "git", From 1dc09c027c4073f7b118511e570dcfbb013e0460 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 23 Mar 2023 09:07:28 -0700 Subject: [PATCH 03/15] Fixes to the Linea Goerli implementation (#18290) * Ensure that NonInfuraDefaultNetworks are only selected in the dropdown if they are the currently selected network * Ensure Linea Goerli network appears in network settings tab if added manually --- .../app/dropdowns/network-dropdown.js | 19 ++++------ .../settings/networks-tab/networks-tab.js | 38 +++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index 0e1884441142..c3606a31465a 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -280,22 +280,19 @@ class NetworkDropdown extends Component { } renderNonInfuraDefaultNetwork(network) { - const { - provider: { type: providerType }, - setRpcTarget, - } = this.props; + const { provider, setRpcTarget } = this.props; - const isCurrentRpcTarget = providerType === NETWORK_TYPES.RPC; + const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[network]; + const networkName = NETWORK_TO_NAME_MAP[network]; + const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; + + const isCurrentRpcTarget = + provider.type === NETWORK_TYPES.RPC && rpcUrl === provider.rpcUrl; return ( { - const { chainId, ticker, blockExplorerUrl } = - BUILT_IN_NETWORKS[network]; - const networkName = NETWORK_TO_NAME_MAP[network]; - - const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; await setRpcTarget(rpcUrl, chainId, ticker, networkName, { blockExplorerUrl, }); @@ -317,7 +314,7 @@ class NetworkDropdown extends Component { data-testid={`${network}-network-item`} style={{ color: - providerType === network + provider.type === network ? 'var(--color-text-default)' : 'var(--color-text-alternative)', }} diff --git a/ui/pages/settings/networks-tab/networks-tab.js b/ui/pages/settings/networks-tab/networks-tab.js index 785cbcf3214b..5aa9cc27eae3 100644 --- a/ui/pages/settings/networks-tab/networks-tab.js +++ b/ui/pages/settings/networks-tab/networks-tab.js @@ -30,11 +30,13 @@ 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, - isATestNetwork: TEST_CHAINS.includes(network.chainId), -})); +const defaultNetworks = defaultNetworksData + .map((network) => ({ + ...network, + viewOnly: true, + isATestNetwork: TEST_CHAINS.includes(network.chainId), + })) + .filter((network) => network.chainId !== CHAIN_IDS.LINEA_TESTNET); const NetworksTab = ({ addNewNetwork }) => { const t = useI18nContext(); @@ -53,20 +55,18 @@ const NetworksTab = ({ addNewNetwork }) => { const provider = useSelector(getProvider); const networksTabSelectedRpcUrl = useSelector(getNetworksTabSelectedRpcUrl); - const frequentRpcNetworkListDetails = frequentRpcListDetail - .map((rpc) => { - return { - label: rpc.nickname, - iconColor: 'var(--color-icon-alternative)', - providerType: NETWORK_TYPES.RPC, - rpcUrl: rpc.rpcUrl, - chainId: rpc.chainId, - ticker: rpc.ticker, - blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '', - isATestNetwork: TEST_CHAINS.includes(rpc.chainId), - }; - }) - .filter((network) => network.chainId !== CHAIN_IDS.LINEA_TESTNET); + const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => { + return { + label: rpc.nickname, + iconColor: 'var(--color-icon-alternative)', + providerType: NETWORK_TYPES.RPC, + rpcUrl: rpc.rpcUrl, + chainId: rpc.chainId, + ticker: rpc.ticker, + blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '', + isATestNetwork: TEST_CHAINS.includes(rpc.chainId), + }; + }); let networksToRender = [...defaultNetworks, ...frequentRpcNetworkListDetails]; From 50d6e0abc0364cb27ffc2c621e0db50a8e5e4b3c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 23 Mar 2023 13:49:17 -0230 Subject: [PATCH 04/15] Update changelog for v10.27.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82be5abced68..4a7eab9804be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [10.27.0] -### Uncategorized +### Added - feat: add the ConsenSys zkEVM (Linea) as a default network ([#17875](https://github.com/MetaMask/metamask-extension/pull/17875)) ## [10.26.2] From 129535095ac85ece970d6bdca4568241a1198afe Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 23 Mar 2023 15:00:19 -0230 Subject: [PATCH 05/15] Fix error in code written to handle merge conflicts in 3552941323 --- ui/components/app/dropdowns/network-dropdown.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index c3606a31465a..72205a42b19a 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -62,7 +62,13 @@ function mapDispatchToProps(dispatch) { setProviderType: (type) => { dispatch(actions.setProviderType(type)); }, - setRpcTarget: (target, chainId, ticker, nickname, { blockExplorerUrl }) => { + setRpcTarget: ( + target, + chainId, + ticker, + nickname, + { blockExplorerUrl } = {}, + ) => { dispatch( actions.setRpcTarget(target, chainId, ticker, nickname, { blockExplorerUrl, From 3e2361a3fcf0db0312213952eda067c54832e520 Mon Sep 17 00:00:00 2001 From: Victorien Gauch <85494462+VGau@users.noreply.github.com> Date: Thu, 23 Mar 2023 20:56:07 +0100 Subject: [PATCH 06/15] fix: update zkevm feature toggle date (#18307) --- shared/constants/network.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/constants/network.ts b/shared/constants/network.ts index a11db2b5e42c..8f9bf7e46bf4 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -1008,4 +1008,4 @@ export const FEATURED_RPCS: RPCDefinition[] = [ ]; export const SHOULD_SHOW_LINEA_TESTNET_NETWORK = - new Date().getTime() > Date.UTC(2023, 2, 28); + new Date().getTime() > Date.UTC(2023, 2, 28, 8); From 12bd9b0b94acd6a4be8516915c62b5b099fa90bf Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Wed, 29 Mar 2023 12:19:50 +0100 Subject: [PATCH 07/15] Fix missing transaction notifications (#18323) --- app/scripts/metamask-controller.js | 17 ++++++++++++----- app/scripts/platforms/extension.js | 19 ++++++++++--------- development/build/index.js | 1 + 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b4c2ef4ba55e..2d210f2a15b7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -826,10 +826,12 @@ export default class MetamaskController extends EventEmitter { const originMetadata = subjectMetadataState.subjectMetadata[origin]; - this.platform._showNotification( - originMetadata?.name ?? origin, - message, - ); + this.platform + ._showNotification(originMetadata?.name ?? origin, message) + .catch((error) => { + log.error('Failed to create notification', error); + }); + return null; }, showInAppNotification: (origin, message) => { @@ -969,7 +971,12 @@ export default class MetamaskController extends EventEmitter { ); rpcPrefs = matchingNetworkConfig?.rpcPrefs ?? {}; } - this.platform.showTransactionNotification(txMeta, rpcPrefs); + + try { + await this.platform.showTransactionNotification(txMeta, rpcPrefs); + } catch (error) { + log.error('Failed to create transaction notification', error); + } const { txReceipt } = txMeta; diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 3a298e699c41..098c69efdbd9 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -113,19 +113,19 @@ export default class ExtensionPlatform { } } - showTransactionNotification(txMeta, rpcPrefs) { + async showTransactionNotification(txMeta, rpcPrefs) { const { status, txReceipt: { status: receiptStatus } = {} } = txMeta; if (status === TransactionStatus.confirmed) { // There was an on-chain failure receiptStatus === '0x0' - ? this._showFailedTransaction( + ? await this._showFailedTransaction( txMeta, 'Transaction encountered an error.', ) - : this._showConfirmedTransaction(txMeta, rpcPrefs); + : await this._showConfirmedTransaction(txMeta, rpcPrefs); } else if (status === TransactionStatus.failed) { - this._showFailedTransaction(txMeta); + await this._showFailedTransaction(txMeta); } } @@ -157,7 +157,7 @@ export default class ExtensionPlatform { await browser.tabs.remove(tabId); } - _showConfirmedTransaction(txMeta, rpcPrefs) { + async _showConfirmedTransaction(txMeta, rpcPrefs) { this._subscribeToNotificationClicked(); const url = getBlockExplorerLink(txMeta, rpcPrefs); @@ -170,10 +170,10 @@ export default class ExtensionPlatform { const message = `Transaction ${nonce} confirmed! ${ url.length ? `View on ${view}` : '' }`; - this._showNotification(title, message, url); + await this._showNotification(title, message, url); } - _showFailedTransaction(txMeta, errorMessage) { + async _showFailedTransaction(txMeta, errorMessage) { const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Failed transaction'; let message = `Transaction ${nonce} failed! ${ @@ -184,12 +184,13 @@ export default class ExtensionPlatform { message = `Transaction failed! ${errorMessage || txMeta.err.message}`; } ///: END:ONLY_INCLUDE_IN - this._showNotification(title, message); + await this._showNotification(title, message); } async _showNotification(title, message, url) { const iconUrl = await browser.runtime.getURL('../../images/icon-64.png'); - browser.notifications.create(url, { + + await browser.notifications.create(url, { type: 'basic', title, iconUrl, diff --git a/development/build/index.js b/development/build/index.js index 456128c1fa49..9c9f75f16ce8 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -102,6 +102,7 @@ async function defineAndRunBuildTasks() { 'navigator', 'harden', 'console', + 'Image', // Used by browser to generate notifications // globals chrome driver needs to function (test env) /cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu, 'performance', From 8fbd1e30e88abcbcc1c0634b44b2a28c0877ca99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Wed, 29 Mar 2023 14:49:45 +0100 Subject: [PATCH 08/15] [MMI] compliance feature page (#18320) * MMI-2657 adds the compliance settings screen * MMI-2657 adds stories file * wip compliance feature page * adds stories and fixes imports * adds storybook and tests * lint fix --- app/_locales/en/messages.json | 36 ++++++ .../compliance-settings.test.js.snap | 98 ++++++++++++++++ .../compliance-settings.js | 98 ++++++++++++++++ .../compliance-settings.stories.js | 35 ++++++ .../compliance-settings.test.js | 67 +++++++++++ .../compliance-settings/index.js | 1 + .../compliance-settings/index.scss | 27 +++++ .../compliance-feature-page.js | 95 ++++++++++++++++ .../compliance-feature-page.stories.js | 35 ++++++ .../compliance-feature-page.test.js | 107 ++++++++++++++++++ .../compliance-feature-page/index.js | 1 + .../compliance-feature-page/index.scss | 29 +++++ 12 files changed, 629 insertions(+) create mode 100644 ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap create mode 100644 ui/components/institutional/compliance-settings/compliance-settings.js create mode 100644 ui/components/institutional/compliance-settings/compliance-settings.stories.js create mode 100644 ui/components/institutional/compliance-settings/compliance-settings.test.js create mode 100644 ui/components/institutional/compliance-settings/index.js create mode 100644 ui/components/institutional/compliance-settings/index.scss create mode 100644 ui/pages/institutional/compliance-feature-page/compliance-feature-page.js create mode 100644 ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js create mode 100644 ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js create mode 100644 ui/pages/institutional/compliance-feature-page/index.js create mode 100644 ui/pages/institutional/compliance-feature-page/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index cf2ebbab5aee..b194112a5527 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -150,6 +150,9 @@ "accountSelectionRequired": { "message": "You need to select an account!" }, + "activated": { + "message": "Active" + }, "active": { "message": "Active" }, @@ -639,9 +642,39 @@ "close": { "message": "Close" }, + "codefiCompliance": { + "message": "Codefi Compliance" + }, "coingecko": { "message": "CoinGecko" }, + "complianceBlurb0": { + "message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties." + }, + "complianceBlurb1": { + "message": "Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting." + }, + "complianceBlurbStep1": { + "message": "Sign up to Codefi Compliance below" + }, + "complianceBlurbStep2": { + "message": "Create an organisation" + }, + "complianceBlurbStep3": { + "message": "Create a project" + }, + "complianceBlurbStep4": { + "message": "Set your compliance settings" + }, + "complianceBlurbStep5": { + "message": "Click the \"Enable Compliance in MMI\" button" + }, + "complianceBlurpStep0": { + "message": "Steps to enable AML/CFT Compliance:" + }, + "complianceSettingsExplanation": { + "message": "Change your settings or view reports by opening up Codefi Compliance or disconnect below." + }, "confirm": { "message": "Confirm" }, @@ -2676,6 +2709,9 @@ "onlyConnectTrust": { "message": "Only connect with sites you trust." }, + "openCodefiCompliance": { + "message": "Open Codefi Compliance" + }, "openFullScreenForLedgerWebHid": { "message": "Open MetaMask in full screen to connect your ledger via WebHID.", "description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid." diff --git a/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap b/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap new file mode 100644 index 000000000000..bddb1ef055ce --- /dev/null +++ b/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Compliance Settings shows disconnect when Compliance is activated 1`] = ` +
+
+
+ Change your settings or view reports by opening up Codefi Compliance or disconnect below. +
+
+ + +
+
+
+`; + +exports[`Compliance Settings shows start btn when Compliance its not activated 1`] = ` +
+
+
+

+ DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties. +

+

+ Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting. +

+

+ Steps to enable AML/CFT Compliance: +

+
    +
  1. + Sign up to Codefi Compliance below +
  2. +
  3. + Create an organisation +
  4. +
  5. + Create a project +
  6. +
  7. + Set your compliance settings +
  8. +
  9. + Click the "Enable Compliance in MMI" button +
  10. +
+
+
+
+ +
+
+
+
+`; diff --git a/ui/components/institutional/compliance-settings/compliance-settings.js b/ui/components/institutional/compliance-settings/compliance-settings.js new file mode 100644 index 000000000000..aca08e1980a8 --- /dev/null +++ b/ui/components/institutional/compliance-settings/compliance-settings.js @@ -0,0 +1,98 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + JustifyContent, + DISPLAY, + TextColor, + FLEX_DIRECTION, +} from '../../../helpers/constants/design-system'; +import { I18nContext } from '../../../contexts/i18n'; +import { mmiActionsFactory } from '../../../store/institutional/institution-background'; +import { Text } from '../../component-library'; +import Box from '../../ui/box'; +import Button from '../../ui/button'; + +const ComplianceSettings = () => { + const t = useContext(I18nContext); + const dispatch = useDispatch(); + const mmiActions = mmiActionsFactory(); + + const complianceActivated = useSelector((state) => + Boolean(state.metamask.institutionalFeatures?.complianceProjectId), + ); + + const disconnectFromCompliance = async () => { + await dispatch(mmiActions.deleteComplianceAuthData()); + }; + + const renderDisconnect = () => { + return ( + + ); + }; + + const renderLinkButton = () => { + return ( + + ); + }; + + return complianceActivated ? ( + + + {t('complianceSettingsExplanation')} + +
+ {renderDisconnect()} + {renderLinkButton()} +
+
+ ) : ( + + + {t('complianceBlurb0')} + {t('complianceBlurb1')} + {t('complianceBlurpStep0')} +
    +
  1. {t('complianceBlurbStep1')}
  2. +
  3. {t('complianceBlurbStep2')}
  4. +
  5. {t('complianceBlurbStep3')}
  6. +
  7. {t('complianceBlurbStep4')}
  8. +
  9. {t('complianceBlurbStep5')}
  10. +
+
+ +
+ {renderLinkButton()} +
+
+
+ ); +}; + +export default ComplianceSettings; diff --git a/ui/components/institutional/compliance-settings/compliance-settings.stories.js b/ui/components/institutional/compliance-settings/compliance-settings.stories.js new file mode 100644 index 000000000000..c3f73a8a3707 --- /dev/null +++ b/ui/components/institutional/compliance-settings/compliance-settings.stories.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { action } from '@storybook/addon-actions'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import ComplianceSettings from '.'; + +const customData = { + ...testData, + metamask: { + ...testData.metamask, + institutionalFeatures: { + complianceProjectId: '', + complianceClientId: '', + reportsInProgress: {}, + }, + }, +}; + +const store = configureStore(customData); + +export default { + title: 'Components/Institutional/ComplianceSettings', + decorators: [(story) => {story()}], + component: ComplianceSettings, + args: { + onClick: () => { + action('onClick'); + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'ComplianceSettings'; diff --git a/ui/components/institutional/compliance-settings/compliance-settings.test.js b/ui/components/institutional/compliance-settings/compliance-settings.test.js new file mode 100644 index 000000000000..b6d3e95b14ce --- /dev/null +++ b/ui/components/institutional/compliance-settings/compliance-settings.test.js @@ -0,0 +1,67 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import ComplianceSettings from '.'; + +const mockedDeleteComplianceAuthData = jest + .fn() + .mockReturnValue({ type: 'TYPE' }); +jest.mock('../../../store/institutional/institution-background', () => ({ + mmiActionsFactory: () => ({ + deleteComplianceAuthData: mockedDeleteComplianceAuthData, + }), +})); + +const mockStore = { + metamask: { + provider: { + type: 'test', + }, + institutionalFeatures: { + complianceProjectId: '', + complianceClientId: '', + reportsInProgress: {}, + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, +}; + +describe('Compliance Settings', () => { + it('shows start btn when Compliance its not activated', () => { + const store = configureMockStore()(mockStore); + + const { container, getByTestId } = renderWithProvider( + , + store, + ); + + expect(getByTestId('start-compliance')).toBeVisible(); + expect(container).toMatchSnapshot(); + }); + + it('shows disconnect when Compliance is activated', () => { + const customMockStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + institutionalFeatures: { + complianceProjectId: '123', + complianceClientId: '123', + reportsInProgress: {}, + }, + }, + }; + + const store = configureMockStore()(customMockStore); + + const { container, getByTestId } = renderWithProvider( + , + store, + ); + + expect(getByTestId('disconnect-compliance')).toBeVisible(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/components/institutional/compliance-settings/index.js b/ui/components/institutional/compliance-settings/index.js new file mode 100644 index 000000000000..5105b8bea903 --- /dev/null +++ b/ui/components/institutional/compliance-settings/index.js @@ -0,0 +1 @@ +export { default } from './compliance-settings'; diff --git a/ui/components/institutional/compliance-settings/index.scss b/ui/components/institutional/compliance-settings/index.scss new file mode 100644 index 000000000000..df59c0c0648c --- /dev/null +++ b/ui/components/institutional/compliance-settings/index.scss @@ -0,0 +1,27 @@ +.institutional-feature { + &__footer { + border-top: 1px solid var(--color-text-muted); + padding: 16px 30px; + flex: 0 0 auto; + + button { + min-width: 0; + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + } + } + + &__content { + @include H6; + + line-height: 22px; + + ol { + list-style: decimal; + list-style-position: inside; + } + } +} diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js new file mode 100644 index 000000000000..0b050ad4c101 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js @@ -0,0 +1,95 @@ +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { I18nContext } from '../../../contexts/i18n'; +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import { + JustifyContent, + DISPLAY, + AlignItems, + TextColor, + TEXT_ALIGN, + BackgroundColor, + Color, + FLEX_DIRECTION, +} from '../../../helpers/constants/design-system'; +import { + ButtonIcon, + ICON_NAMES, + ICON_SIZES, + Text, +} from '../../../components/component-library'; +import Box from '../../../components/ui/box'; +import ComplianceSettings from '../../../components/institutional/compliance-settings'; + +const ComplianceFeaturePage = () => { + const t = useContext(I18nContext); + const history = useHistory(); + + const complianceActivated = useSelector((state) => + Boolean(state.metamask.institutionalFeatures?.complianceProjectId), + ); + + return ( + + <> + + history.push(DEFAULT_ROUTE)} + display={[DISPLAY.FLEX]} + /> + + + Codefi Compliance + {t('codefiCompliance')} + {complianceActivated && ( + + {t('activated')} + + )} + + + + + + + ); +}; + +export default ComplianceFeaturePage; diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js new file mode 100644 index 000000000000..a70991369fab --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { action } from '@storybook/addon-actions'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import ComplianceFeaturePage from '.'; + +const customData = { + ...testData, + metamask: { + ...testData.metamask, + institutionalFeatures: { + complianceProjectId: '', + complianceClientId: '', + reportsInProgress: {}, + }, + }, +}; + +const store = configureStore(customData); + +export default { + title: 'Pages/Institutional/ComplianceFeaturePage', + decorators: [(story) => {story()}], + component: ComplianceFeaturePage, + args: { + onClick: () => { + action('onClick'); + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'ComplianceFeaturePage'; diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js new file mode 100644 index 000000000000..9b75b9107550 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js @@ -0,0 +1,107 @@ +import React from 'react'; +import sinon from 'sinon'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import ComplianceFeaturePage from '.'; + +const mockedDeleteComplianceAuthData = jest + .fn() + .mockReturnValue({ type: 'TYPE' }); +jest.mock('../../../store/institutional/institution-background', () => ({ + mmiActionsFactory: () => ({ + deleteComplianceAuthData: mockedDeleteComplianceAuthData, + }), +})); + +describe('Compliance Feature, connect', function () { + const mockStore = { + metamask: { + provider: { + type: 'test', + }, + institutionalFeatures: { + complianceProjectId: '', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + + it('shows compliance feature button as activated', () => { + const customMockStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + institutionalFeatures: { + complianceProjectId: '123', + complianceClientId: '123', + reportsInProgress: {}, + }, + }, + }; + + const store = configureMockStore()(customMockStore); + + const { getByText, getByTestId } = renderWithProvider( + , + store, + ); + + expect(getByTestId('activated-label')).toBeVisible(); + expect(getByText('Active')).toBeInTheDocument(); + }); + + it('shows ComplianceSettings when feature is not activated', () => { + const store = configureMockStore()(mockStore); + + const { getByTestId } = renderWithProvider( + , + store, + ); + + expect(getByTestId('institutional-content')).toBeVisible(); + }); + + it('opens new tab on Open Codefi Compliance click', async () => { + global.platform = { openTab: sinon.spy() }; + const store = configureMockStore()(mockStore); + + const { queryByTestId } = renderWithProvider( + , + store, + ); + + const startBtn = queryByTestId('start-compliance'); + fireEvent.click(startBtn); + + await waitFor(() => { + expect(global.platform.openTab.calledOnce).toStrictEqual(true); + }); + }); + + it('calls deleteComplianceAuthData on disconnect click', async () => { + const customMockStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + institutionalFeatures: { + complianceProjectId: '123', + complianceClientId: '123', + reportsInProgress: {}, + }, + }, + }; + + const store = configureMockStore()(customMockStore); + const { getByTestId } = renderWithProvider( + , + store, + ); + + const disconnectBtn = getByTestId('disconnect-compliance'); + fireEvent.click(disconnectBtn); + expect(mockedDeleteComplianceAuthData).toHaveBeenCalled(); + }); +}); diff --git a/ui/pages/institutional/compliance-feature-page/index.js b/ui/pages/institutional/compliance-feature-page/index.js new file mode 100644 index 000000000000..e105bee40735 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/index.js @@ -0,0 +1 @@ +export { default } from './compliance-feature-page'; diff --git a/ui/pages/institutional/compliance-feature-page/index.scss b/ui/pages/institutional/compliance-feature-page/index.scss new file mode 100644 index 000000000000..61166d9886e7 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/index.scss @@ -0,0 +1,29 @@ +.institutional-entity { + box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08); +} + +.feature-connect { + &__list { + &__list-item { + &__img { + height: 32px; + width: 32px; + margin: 0 10px 0 0; + } + } + } + + &__label { + &__text { + font-size: 0.6rem; + + &--activated { + color: var(--color-text-default); + background: var(--color-primary-default); + padding: 3px 10px; + border-radius: 10px; + margin-top: 0; + } + } + } +} From ff20873c654bf7094f4d2dee5b4b71e828df9d4f Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Wed, 29 Mar 2023 16:01:05 +0100 Subject: [PATCH 09/15] Add documentation for creating new confirmations (#18317) --- README.md | 1 + docs/assets/confirmation.png | Bin 0 -> 72222 bytes docs/confirmations.md | 253 +++++++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+) create mode 100644 docs/assets/confirmation.png create mode 100644 docs/confirmations.md diff --git a/README.md b/README.md index 48dbe9f3f65b..bf2671159717 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack - [How to use the TREZOR emulator](./docs/trezor-emulator.md) - [Developing on MetaMask](./development/README.md) - [How to generate a visualization of this repository's development](./development/gource-viz.sh) +- [How to add new confirmations](./docs/confirmations.md) ## Dapp Developer Resources diff --git a/docs/assets/confirmation.png b/docs/assets/confirmation.png new file mode 100644 index 0000000000000000000000000000000000000000..d4b58a2f68db387bd5328fe708d88f5bb4bee4f2 GIT binary patch literal 72222 zcmb@tWmH^C6E2LqyK4yU4g(C9;1Jy13GTszCIkr@+}%C6OK^90cNm;+IIo>^zkhe# zS!=WR)b6gTuBW=YrmOg-r0@|Hi3kY-0s>W9NYiZ6SN8m zJbCx=-Js+QbJ?9+_zsXjV+w^mT!$+PVFU>MUfgfUkQ6eWHT1z$^dY#G-;De*SmQS3_{(E9 zUHDfSYoxeDrz9qh1@fU9+gMV6 z)hr&9dKIO;vQIwfP9{;7RF8v#h0WF-t$pPreQn;}kqPnE>?)3-L5qkTCEPtR zSMdFfQvzJ3m5cfwqKUnU4wXJkW*iqogD4-jlmoX6g*Qw%9x(s29}y*V9%9qb$WArr ztKt|2ObulSBTO0tohdD_6!}8TS5&E`%e;n9L}3Hjb^xR17t#yhBhwY*jjKUS?11Wi z`{-s3?!zx@M|Cld9-Od|a2V-N7$suIO3~Ql<&}ggslDHcsxf6Zb1zcFN&4|aU}2FFl;Ugl#oT2dazowHLw{yx3NYD}diT>EM7)`?-Mj(Yk1BY$v;IoVm>Z4wtAy>CZ4e&x6 z*=iQrK+K9gk9J5Km;cQztwi-gKPWYhdJ~A-DD}#Y|C?sor5oCTybvwkkyMv_eye;O z9UBywzCtmi+`~MA^h|rwKC&KKkX1=781v~8p;~0⋁geX(6P!=&!(FLKY)gYJ8C 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%0D87{d6s9>EZp*afemO7Q zM04sleVSbU=iV$<9mX5cvcNaH((&~Ldhqekv?A93gTV%7&WEY`lciRLPUv~tI-7|a zQo(Z_)V5DCfvhPm0`>MkJVy@EQKQZq)?PcHd(FSNGEVGG7pa8{xLWSfH2HiJOWiAp zI{N+1enL@%bym{r^9hsy$e!>RlO&H9&4r@BE;MRH{(P z?~^|o=p*cHh{Jm!8b*oTcaWFJ#4}8ei$V*Pn^ZzQ3{~JwmZI-YF4PXXPBV{=m7W_UT>X9uRG zeDCScJ6{xJ7SXxgxrUvB`a(z7d+8EWutQ z`i>uqoYTy>PY-TaI_sP)25IaT6Jr%TR{sIa;EZFhrOU0op64VT|2AmT7IzogAP$;MmV zG7{I%jse(OWUfcyj{4knng=z&2+SWj5lMv6gYxB4$`vmf>l)bOytb7Q_o0u4&O|KP z+G+SUYe=O!mM~D{xZr#8uZ6$iz|Xs0Zc7uLPhu=K+Tf%4zyTakiJ!1=>6PaPP4LGF z48MFG5>HZqMCpqn?Y7UkiztBs$AQX$K&mE(NVj*d=b1J=w`*mI=BaG6sL6v&g? z@9*&|nuvj$RqHBP7+uj(nw&>0IF({n4x`SO=ahrGufh!D+neZclj5lTbAD3gLeBJx z0p7akpm2Xg62tUz^7o9C8BD5baB1Iyi_kDA_*|+u&+f9YalB@z0h#BRX{p&KEhEkc zu|65ft77+_na0(8AyVqMoz4~8kG<8HeiQE)$!-}P$IzkJ0nYsw(4j@r6Y!ouqeB#r zT80GDc9>!Ebq=$^amVyWbz}%R3wOV*@S6N}PLtc`){Bs%gR%CP`axb;F8viWqNnOm zh~b~q@H4-6iDb4R8%sPMeoJ(3S)%Dm;A6Hh^rMh`In`JFO1?tEbcgZKcHVONt84Za z(9fEmH3hkXc@EuXHDq4lI|yxXT=03^^CTOC(}C&niZwo!(?VeefBEU>Yk&n=QU_-= z1<>1=sG;ulxgheQA7^}H9uA!XrWVcPIf8!b^1{Dn~xmm>ccoME^cu zQ_C%jH@K)P6$*L+r5 zP&4?p^dn#+=m5_Mg?^OJ*QM<-jQ%k&T@L+)Iz3h1sh$U-fV!7eaOGb|VL&gSM=!p- zaAHw#zeo(YM61ESApw;Ls-^lBX1KQ_Nodg{KKc@i-CL5H{pnP=HAVIR!v0}wilQD8 ziZGu5YQ|P%Pwh{1QA-Mc2$o{+)VE&@s&~7P^z1_d7Sn6uRzAZYzME9}w>cDhR~k|; zd&g;YKpWaUE;rs)2*Ss>klVi7kmU}zSIb43K59BYu0e@U!@*>O*xJl(8pGm!f){Mt0(ayCaym!oOZg}LFL&!ge;lsp* z288dH*ftwFCI_2LxUWs6Hj*D&Bdib(&po@bTY*d$B{!V)n#Y5r?R6^BGW^N~SDdb1 zOD~OT@PdWTb@%V??3=4-Bd)tO+*RB2f@r=pUl-H1u?3mxXoB9v#mW z7E3aTL@D@Dy-W_177J?Ntzq|QMMjo!Ft+ES0NL^YnZ6o%xCQOFk5w@r5EIX|%z9%}?VYqZcUx`goJJ`EAn~HCA^DN#=q@)yJHUgAFc0hE{?d80{LDWTgV`OT z?Ss2UA`(yl^{AF-WQ2&Y>kAF1?I{mwi~kNIzs_dGdZ%~v2grQ#iM73UQ{a;7o%>94 zw*cA)9L*%3<9WaNiuP0O!?{OT#s)o{+p{Yu<`=VXb(wGpJ|VxvDnYMgFPBQ(@oDre za*(!ana z)@w>8%(Cv0*XD0uZahXS{QDs4UZCL*RHmV)@2V=iT#!4CUKRA3y_ock4RGH7^6JTO zn1-7_n^S_Veb({5rz+ddP-}}4KR(aOT0?}^AvLh4Az`t^TQexEmua0kurZ<(x z>6};TXh`#y?NQuP9L7nRbq-$))*!|AL#f+{nufCNNPyxTPn9h}<)o?`t%|DFGOwQ> zk0BYf5+3oXGrd)l0cSNFtP4nCp7TX=DoI9Ek+Jby79r1rGsss9FDEHcN(cFN_Qsd= zl=fr>9wIxeDseg_B@ZIb5uXd13G~`;B}z2sA#@!4ORe{eO_`2&Sl(L>O)x)Nx^oA<5E~})QQHlY=pk9nWgL5mD!r&nSfKE zMS2Xis2|dvS9;<(Vj{?hu#cxG+(~xtdN>l4yvT76bM3xb0PpCfG6JfyYdsR|(N4-grSdz&@T^CK)ce8hG0P1~9=JK!pXQ-q&XBcO@ot zyh8$azv}K(1Qh=W(_qe|(?DcF%?t=3h#xmgg38mDiY6cNGX~1Oiq9_yS0Ki(H)gOC zM0x#k=wPC>gDjR)Ta10Jq~L*$jLwCcyz{2Xj6D$tV~MVk#0Kkf6^yPFFsV^|n-gBo z5m^#gbo-zdq@d*1JTm1M@ACxjEEahAvy3T|ILv?6d(!ymUFOJKtTb|}92At&kmzAg=fZffj z0gq}XNUSg}8UC6QskgO=(C_sRV_GaEdYX$sL#c)}l^ z?_y{ukj26T!<1q2p6{Y&OT#oA=bb+6AN+FOvl1Co(=q?y1;iYcm7c$G)oH38*ZRK0 zg7s^ID&$1mrx?u^gIwo^T3#!1Ru^SZ-G!^~by>2j*9QS~?mgan6gnrFeH1mOWK5Yz zB$a)k)z$EkWN@*tnT%YOgedbu2+LdTj3BEplY&hP4z6=5% zq9v{URhMM=w0C(Sw2$A#gXeQdgfMKao|56Io^!E4Tp)c7#`=onZ}4Gz)XPw37?j5^ z123P_nFbBqxF>issHLR8#TkQsyYneyv@r^||%!sya zk(Z~Mn-3I?R%FBozc826axFhKgo;G__x(xJbXoKKs2Ak~MS6!l{)@~WVl1L;O_wj; z3~B!!xhv3e?mzDp*J3>m@8PoRP!hX4{5CH-qEg`yJ5 z#osfB5~e0p`1$p@aO2k_SjNU3BM0nLysd&l54eMW0ueZCXh*Pmp1NUf5ZO!f$a)GV z-De|z#vHGxeNB;MxHQ7pu)6(hfV)f4^$ZAOv%jPeqe))~M6RJwUcsx<-F?W4h+il) z$g}cViGq5{*FvcZvOqP;sk9{f_?{ zoac{Rv+QW@Ly|+q@8{$mtC=25{c=rTyuB$JG#6@-p%r@^s?Z?~%)zN?r+QdP&N+8& zxAcQ*k_C~GP0dB@2J^6)NYYUfeDa^`yStc$3~$d|lI}&qRehCho@3O6Y;`{qj%r(f z4?WckKAH)&NwLGOKM%2=|1B+4%XELj8`bF!jy?+=u*N-X4BgB+NhVG7NjK_pxd$(C zpAaomp#x?DAH`E{ewigfEeh|W>3tek>mxe`0mEr8c-Dm(Q-p-3JDr;-9$iuD&|UWI z#yndf4`z3?(zjabS@dn$$vMn(l=Lp{hOp3uJqmyH_+YWXKkm&OKn}fVbqR6pwGtNF zQe_IU)R7j{v{0WFi^IA2&<}~a?v@LKg{@8BPpc#A~ND*upj2{2U zPUjeY7yA^LQ1s%XS}E-@HJ`R_bsTf+Juo%D^J&#FOzGOX?+1P*dKaNu_faX;-Ffx| zpTFLI)(@f74nHmVs|QXTXPW#fFPtQ065%bYMYEIoPF`qbQ?X zJ9MsxB-(<`+U?9B-s~h=MOyiOG}5emNwVzxo#mY0uEGoY+E8sUTjiN=wD2fq+ zm@~0?%Jza);XYk3UVMp-Cq@A&)%A@n_Cxuvyb* zSg?1vLoOn@kGg%~@Rvy#jo0Ow%gSFe8LLh-va7k!aLsW@$=}WI3j|{MEp+}N?nW*; zp7%}uK1(LHfE3iA^aoB~YG+5}L4`$MO42hLGAiB&>vK=}*DhCM5v`LFpBtC25(@s7 z=0;~u7cEpRz1^LE^gizQ5_?j{d&kGhM$2nsfP)Ekic*tAJM|0$+mzGCa1-#!(3N-; z!4BTyKsdSaaHjvg{f2AzcUzObwu35f4s$!;t1cY1ZeYF3H|lDO)h_6&G|q0ami(z+DP zGKVuB=GEqI@KtJ-l6A~+rP0KzSUHb_wY4?)y<*xfyB|kBU(cqnxke@sgmBs{OPQHQ zMsmJB`P<0HjCJ0m(Sh_lGt7$E$bb*@jfGVg94s;p_U`Tb+KH>5>W3BG9wXD$SE%6d9crrwL(#m6_O@@j(SC6k-Kibn^6xH{S0$H(OU6)ayzVj_aois?z7M4ZO6KciHrF{;&wqUW9A(Cc z6}yzFH(iO0Cl2h-;FGs-n$G?@*CeEDMA%Q;J|gS+ntm?0vrW#2|LVbr8u7BIOAFQW zbxGfKKhnge@_|C9$7=xjOvt|Rv9HLa|6uobO3^d-&#E)yvTA$=NN`ZFGrvdG7uDJ* zkR<{cah0a7F*NFYXB>bfM1X#ev2Nt^T=$b~b3iz%Qn28$f-DynGm-_>97u|w5Y8J! zm3iXR`6=HABZU<*qGevmFD|E?RwUXh>xFIhAd z+_JXHKC&TyznfjSTk|Pbd!_?@u@lLXDSN8KT_lPHP&mYrS=AzFUU5FN1HHHJ-wSxS zK%t-UB(&p%agNeOf{X|y@^DOid1=(OYT{}2Ty~MyLw%3&xr)i_l{2QGZ%!-un)hosio_ZR0>tb z+iLh^$F{bHeN{5sV=1^ufaf{g=?o0E+gDgX`tmZLJ3aauS(^Wiv2XIei)C;a<-S3z zN~xZX@KXSYJ2BZ>P8c;i*cH zQ4_DTp$;7sQQt3TgROaCzTpgiw0@pa6yAp$UWBQIdt?H-d(*3rLJzRuXr#bXrk!j# zzT4Zsv)F4tC$d-ZDcBH}YjD2WmGQ>MF|*DAVH5nr&e8CCay-$8B_a}Vw|ZW;FUtlR z$io<~#17B>LBK7r&MhwD!=x7lYl6|$sHh70M2ki8^|?j8Oj(jyGBQcs1p~578XT%i zpa+D3^GADcDcz1{l_JdxtQH1@hbPh0rd~RdJZ&k)1(dWwsCeE##^WcCt{3>*<$8ie z15;y4t6oG|^I^fUI(Xw7@hXL?R{E1Et+l1G3-8Oxc^DX#aH)*2mqJhw*z~`tDHwNF zI}c$pDAkbeFfGlek!7_T*k>qm&bG+kd&)G+C2{DaTe|OjM)vAi35fXg0gB=5{SusF zWB8*T*T zR=W4EJ)gc|x;C*)ub!3=x||O;hg{Jbh+Ki5q?l)11QSn86j4)hxE7wC4@oTuqhsNI z7n-i(xnE2&>^Eex)@Gr~ zv&9fRKO%zT3HbJ9T7XLpA5XN;FP7=T0zS0+hJH8 zlHoY@`4z~s)^U)mg!+4euuQK%8Z$4pBDF{0yhA0oN~b#s-BZfEh8(Xl5c z+oa|L+K%Bp-bblCWx7b}lHeMT@vSNZVL`1>XT{+v=#zZX0w)E51lghtTFA!iT&|_qBO`g;N?tj1)J8z{oK@R{y z9ana@lMTOvL?MyM7$FhNoneWEd7Zd#LfA>w4s!&z>hG#Ruw;8Z9_d%t+n&WLr7W){ zv_9#qRB=M!)Wm=4x^jsER58aF7i&^@#*1c3_K$Z_UEJo-1l;eU51msR4UGo1e;72z zS4i=fmya&VJjBU12@R<%(~vwQVnvn~^q4FibR`82-lXzJo($>4RPU5XI0hH1%syFh z;*#Y5)^Ya0wG|jufhAk}5Q>df1Ro9M7+ffE8ly_7NK>p{38yxk!gH%P8BP-qPNn}x zEeaWg++zR87@qHh|Exo!Foux;eniQ`dH3G|fR8?-F%iQ2M-eo%7U}*@1}EmJGO0t9 zk+7it#$^Bj%)%n5gvl1?@8mE7NX&6eaeCOlsR2YVg#okR>{3hML4y3Rc3=nn;g7H% zmKFE)KUVy!zKsJ4%z|)(L6-O*#d}mAfG{pSN;0BJ1)HEX^!P}wW|Lh|35dg1akm5M>Vb!`(DM^I6Eg|ro=$peMM`GU7qSM zVah~wh>tUe8;C}PRkvyrnkT8FRxkL>jT9`>v@au!q7@5EzpeeXlB$Fcj zNRmH-r3DKXxWWnO0@jB#ZBDWj3+mGwuG6~TU2Mbt?}RJM0{xz6pcr)O<)&*@P^5Lh z?mCswKFtD_V+mF*v`^LvIRND&MYR9vQUUMtSw6A&CKi5T+F%^!1qoKIOS%?}i$HtF zB!Dt{6zm`h7Wk^k)G$=?;0`Z_w{lIVO*Yhew#kW!(b2%Y(X<@hMyI?a=Do2Djqv&H z!K7GY6BF@j=v!JEnh9JDUCc>O`jH=bzZEaMH~5{mGr=Kbu&$gn^1j0fa-*ozSFl6= z-Ok=nMa-G)zuNtIY!{E`#+w?}@RV}x7pmiTeO`jsoYwD8$JJ?f?RUUGAGPp%Xfe$XP&!A6LCFR)}z7I(WuQgpIC=$T^dV3H`Z*Q+>uKh|= zyl~3iQribqVvIwrR&!JzHjwzk%Om;z`3AK*A&m@9!D5~D1a%ZZVy0NVZu17M=LH<9 zZ3qY#> zrV2kkIX|2Z$uq-?yjOWj_+}W^32kp~@(I)CStGRf+2rocZ1T#QfMcUP4O>@Q7Yb zx=62Jx;~(788qe|Q^TW_(P^+cx=P*iy3Jf}be`xC4fNTaKtvb0ra&bb&j42@Xusv;t-(Qo4KLH`_Y5O7x zB`xO`eTUs2#zS2=Jn~!~;%Y#L)%$H)xp=gEvz@D&;ZUprA3Y5|QZUX%VB!@|C zr>IRgvpU}9;X(fTZBOKfGtE( z8bJa(0Gz%*mxWdX`5S&_QEB4Z`4ijEcr=movB+raQAoeW%WN~Ot<@k?y)6A!s*CXp z!Z~mwkiTZX$;L)|VZG>hx(JzBt7-as`1EK2c@6x-;W%KNzKD25=0&rfiGsJFI^$$I zpvm=QGfv_s-gpyM!BN3f7w))y3xDK$x$6_|xf__p*)lJ|*5_H6cCrVcnN-u;T*?41 zPS<`BdBI{7OiFI>$P5|reb?uq%0%SY0fy@#x8Z2~PYEe~ z1Q7I#Hp2suS1{xWUIR@b-ZsV#$%OYv0S*Yd43MbA6K8mvBL*FYO{}{CfD%I?L}n21Us>ImNgJ``B4C0Hys_|O89a$Ra&8Oicm8l0NtwY`KG zqt;9AYk3LD84;MATklpFb3?R-NM)g&KG~tloG5>tExq*k4b-(pvWye4-bVQ1Z^6Yb zE(7b5(e`{WHMW~1>3L!@8`FH+6np6e%JO*xP3!?6oYvZ&_3wwnE_a3|60bx+SP$l8 zZ+cK{z3nNE6|J?p2r!awdJwp*cR)5Zh;I3>aC~L=?Yl10ho19&eMYyQsCMm`n!vx^ zfKR!w{$_SCoWxmcexfZVqT_7MZx9?royp$2=)BVWsVIiV>Zsn9%Wyq2^Du}xuQ7_> z98uK3S#E3gwHFd{?0IL8yif{rjF96i?l|w-lgO8rdlOY~^^y!<3h-#Qtbn&a0-a7_ z!gjm=tX<6>ldBs%F+^UU!qYtlYZO~p&~*U72rp+Q#aH(dLU|7*FN?lrE~jbBD38a0 ziqrSE-cC&5U8CALh?xZ+8Xfh2*bDKpSR0XDbI} zoKirEH6KOj{wIM+;on#noe2AS5iI+*qa<>J_uIsq9qg@O;32jYIJ=`B@x>NJ zK8ff_x9vURbxqp+D#~Cge+^;Tt$m5kSVbt#ia0oV;iZ2l9*WNR=}WT4$`!6MLuOhJ z(_u$93>m{y@WE8Rho|<>*9Df9{U!VME6>AU6FG&BIU!^R;Az$>csThky{Jor&-3!# zib{Ky!C6D=tE&vmHFMqJUD;L=&&wLu|HsXD1vJ%l+e%SDlp-Ps5}JZY6Qy@SLFt0@ zrXambCnN!-DAfYen{*Hm=^a5pr345ylpsh;fY3sKkZ^bW{&UX#?(==Q_mR!s*=wym z*IaXsImUd-gg!ZId+d5nsstw9CJ_(^_j?2cqW?A>o(iBKFah4!5nM;XOlcg7hax5q z=vUxI!spPkZAw0i*XO(ugU7=@Eq_vSN&uEff{XkMpFuA zyHsNbpC({>ROG~6@2tLRl3Xj~wUI{C2Ce_DkO3;4#LZ8UWwCF96CkJgi7LOvu2XG} zOh}vy)XR{cmS-?lRWGoG<)6B_ux590DZYMI8bU{owI~#TwGH4STSgcS-~>6WX?P@G z(lJWAp=Be}s(d9Zdx^T~kz?FQ)RW-wX30aA>Y{jIlga{35Kd{(92nk;ABfQGn=@P0 zZt-vqtmu`#P@PCZeh}2%e~q9)LGS?kb=DZrNo1|OIuOhqfn7zQ&Yll%&d#lpacmyu zeL}&tW`TauJ#E*Ug6$v*Wb*y70?v+6A##kh-=$R(p;|yoeQ9`m_^fcQksYE75O(yt z)A))Vi!yQ?eu97Ac2qgs&R0fYn`=P2nF}xurOeRnVcs%hLO{+k*~VB~+aVzeACG{~ zj6f(l4{<_;gLP*6RQ7`7q9y6&;35lFKomHARp)0=5%2pP} zFrl5;zSuC)Jm}OGfb}e)lpn6!H}|2X`&KU%!FQvaJcJ1H5MJ;WURC3%qb(>5>*c8u z7zFJq7!1sB1)|b48fF(~Wexz<#FKJ8Yl6^(#IgPbw5x22pCHvq< z6JRw3M&u}TJu=ymF8m?$Lt%<|u`^6h*SxN2ZOgpgJ~`TZfyq|-mbfyMCH9k zD=}Bncca?SBNgSnfwtk}!p;Lq-9Uy~F9#fqQ5`I|%pk>iWV`{W$$HeEq-SnmLH>OH<2 z6jYq615lKG9)q=BuF!9uPKo~e-zd-S3xQ^U*CZX|xEGk6R32g}ktp@^W+IC&d-8kx z!KAKjQyYN`x5fdBjNbzSI!vJ6);}kHsCezmJGB*S*fJ-Sd~tkeNlo@Z|^+c<_Xyt(Nr9g^+#Wx zgLu8lJvLT>iiT31{%>j~MIa49Sbz*x1f6{Yu*7cDmc>W=m z({+lhOOD!5V%m9Dku@Jzz!RPCeE&Nudvo!z$#sTp$7P7k;MXTV-@m)5N5&p_vdB5# zkXVVNChh(7&caK>>`+7<&La-Qr*&Mupq zl17<$-^x>qMTU`1Vvoi!k$^1!Hij6u-plE={Uh3#0t?G^>Gb5*6DB^BYH@1ui!a%J z=|e8IzmxC0mDt^c^39%YYGaWTxJD6AS=~4YBnwo^I&vnS_ZN9L(t<>JL~r)`zg%m{aFhSeHoX5wZ~(PkJTY1^?eCk`K#uw;x;F- znypn-pL0#W-hb4AD25#WjJ84K6&Bx%z)N}F-(w41r+;S}TD1x`$e5@&5i?+^4AUrF zOMgY$1R@ZQu}-y?yhIsT9Gl*{Uek-8p~C^vf6pp%hMukXhYUq=hR>iUVjH8c7>72j zx!p4V8e#EC-hl1GK+Np}+z7PzN`_Qyu_PxNrgeAUjkuRsXk$$~%=-KwlhL`i1v@g7 z)#6S)8!e0fDF`Y~;6je*RATBz?62+djy_VYvZ;wmS6Y+fe{CMtVJ=HKsI%xLgjHp{ znJ2byjGTcJwC*3AxVXoH@s}JbzbOE&v>bIY>{r( z#@#s`S?GzZZm8S)ONeMjZix@A2Ro``fB?&NDpxsK6q8-pE&SpB-`vR^Fs7NXA9;Yo zznXGJG4+(#kWlY2qXCFg3K|J-jaIo&)Z6}R}08dc@!{$nT_vbH$`1~d% zwslYJBFw)4E}cPNdN^?Xud@&LBhNs-y>V2M{32B?+Uo!yauliD}|F6rR08`PtTzpyJf8GWF+Vj9q=wVIGkb?jFWv0i#;`7HC zo&T4b{vnp^y70mxoBz7}`X;c+n0tgG{{?6lkUuSg!1|G_T>GCH-cJI;lGlq#UH?60 z@_$Fk;pIBoPML4g4fqc>8~Q8*TXi z^<`!vQcqM86YD+awAkLiFEQ+ic~WZFvnFF9630%F(*FQP#K{0q)i7?-wz!^I*b1l# zT#Wz*eBni&c!T2>sv~Ej3bo)6Fd`glm^NAnXzfL8TrQDeY#5~zJiE|y4m`}LLVE5X zVo^&gN?NDEm<5cgga_xq?uqYjm2({TPDzaAlGM7Z zrc0FoXq$^`$`di_6LRE}m{3r7HTLztXrX#&3M~Rq#52_ zJ`I{4F5IgbE42szN)q~Dntq=)yjB4C4i!~lW7vlPcsL=*dh>10IffvKL8Y5W&~y<) zIO^0)BlZeJ`)GIh#z{%krKrT9WI$)7uM29-DKqU5NHRiL&ufApnI@mD9syWf;dBkE zxN&0|Va-{1?>-p6l`mveEOdFU*?;H7ookAIuX_`7N}4_WHg`pq;W;Y0U%37O4(NNU~Be4hO}3aEzDXi=whk*s*{4@q*xX%&mD0!9B2=Kc$?Qf1p?e{;26Vfcv<1At_Hd!KEj!Td zyga;?Lsu1{1V~o&(KFZwH_F~cO#;q5utF*0S}_%Q-}*Awg@NQ2SpklJ_>arC3h#4; z_difY0eB0Vz{%(vRC1T9Ibe1sw>l=|wC(CXfKEz&aImEy1d;q2gH6ukd_mk}`iNGI-+h15ytopUx7W=&cT zQiQBy&a;WR!|k}LEE}@7Ep`D(rP`_C-36)TV;sf>Kmn4(y+97?(_bB~(cI?~SKMS5 z37)Hy8nhdOhK4FuEKX)BzRvb+B!!UO<*!$-ty=;{wv@m^mi3>X)M#9+Qk)6gAV)dn z_Y}_3U*3MG0wXChZusD}KO3}X_PN=_Eu!k&WGc*CJ|1jLtB;o%=c8xmWjz}zSs(~u z)5_<`!2m>qZ1!mBIhVOzc0~u!#C5e+=Dok6ZFW@s3i@t!DuJA&`YNYtuYJ9HfRiu* zC1J;Cp!lshplbvu8C!ZqbiCfEkHN2BRHmG0CFg4hykxAMls`iauJ=F<)h9ivF-j77 z%vWH3-aCg&DMUwUKY8)aB^DMJTzXxt>-eo(W?y&$b4t_-wUT#(2WyoC^|K4&Ib}xV z>uKtZOANBhb|D!bJQ|{PGo%r}`87Yw<2UBYryK{}ehXx*MQJg_n7>bp98_#9Pcs<6Wm`TcI zov0Lc@FGTj3QOU~Z~hans2uCi|8W_cgPuJroa6c4Zg#C*!0&fgFvr2JLGuwz#@uD< zUWTx-4=g%?U74I^EWQ(?QRAr~pKKUm9-P7@S`*c!=d|Vb#4kQx*2z+QVa{P=d8&#zH1p6-By*6Pvx4K>%2_Pqq+MPR5{%(G?bv=nE_JcVFeXx^* z@;=57$!`LcPqyttd=_|E>->J&eT{uSCwS#$nODyIO6fl4ec@QMt|cq}_w4k?Gb&wb z72J%+5>79M1rI{nX|j|uO3vND*+ti^jZ zSxJqOFx5fD6_O=OA0i94bvu8IH1nbO&fa9(qCXIb0SGE0S}SuObNUJQYf5V&i_^0) zJE~*%&bX{5o4ToWR;9+scxbflD`Yw=*F zMDx_sz|dOqV?4al2oa=pBn_)Dk=jB z0dNC22ySj(o27n^(GY*AuR?S$d{(TeqNA2%@@M_@ov&-tTgl3s%^o}DXon4?j zV@!SR^L4{I?0&Ivd2y*1mMw|ND->pwyc~^ff6OT42#u2q$_wPid3*HQbE8rYGiSK} z>??&4NLpJjim~?~%8ebryp*@-b~zUFid|}=$JeoKgoKn%K&od#TfjOpIBg)r zz362=h?%XzXJh7*&di!wxUB}H;9YpyW3>5 zF}KBxdClrDU@|54K~w(2t7_gl8S>^2G8p+^pNajIe5+>FBWqmmJ3Qpnx_PIU^9x4C znfIBa8p>n#uG_@t>R;3?O}>sHv=X#a0bKGUi#;7Bh-dv34YQwGr-E`v;%qo1!$4l7 zRO3jatcUlxm~?<{Cr^T<=)bT2T0VH5B5av3pQij^M{j+#$Ce1_7vm@JL&#H7P3i(K zLl^z#_pm!bQM&io-C^^E+OYhO6*(iurR{qQjEt3zxhxPm=d8+2Nvw|*TznO#H~oq| z;%b5@`}Uh?D=u%9ByrQBrF)D%+yiNapzRRlQ%d5|r0uU3fLWb3c|4*C%5zfi-xkhA zL9h^xgI~1Ph0P2P&a)YMJ>Dlq!$(^6RStrLm;D$Q?lF7yZ5Q36X}uAFpSA784}1`{ zD2?W>#YSU0k}0Z4VUH}3Oi$nO-lGLhT|j-{XI%2cn7CvbPO;fSn2);U)dvXuTDc)7 zZ;UnYAb1PVk;k(zOV+2WXD`=ceWS2`G&8`vvOc=4#K!zkstvPZ=~k}jfvj&BohCSE z$*gbw6oZPo7Vearnbt&usz;}_9SR|J+it@1F_Hm|Zjs4DplAF&;FPu>x4imI;6c~* zvZUv3QF##Tr|$@JZT^nFluSadNth^}KD1aJ2*N9|fwe1YrQ9Z(_x)9aoinbQjajVn zWpTTI{sSDbP3T!@v=)sfI0&{Pl5B#l1aCZ|NCY${o%kfB%r}XFc{+dYfjbB!nnV#5 zaZ*on<(EY63B}PYhUNh9kgFha7eG_N&zHVcnH+Z~O5&f&25xDZATyQ(8i%r#+_Jzx z%Oca61FnjF>y|)E7so7Tnhq`leEOUXL|292p6y>bPq(^vNr%|%`K5RDUCaHFvYqMC zNmX0ny}cjSa-XF$@So~u+8<;Lf?a;TF9ex;bFYPbsY`J|zJY%|YLi&s6q;ySV7(b( zPv3F$RayVb8>hVLvhGzDe32*3Rt?ZS!E*gj1YHXda4mL1qFaQ_*A)V{#zw@ppwD|` z+B@FFwlJBy9B!pc6S;)BP-h))V=1iklM1QmoiU1`g2(jzABirImEkC_4#K=`0MLBQ zMdOSO4S@QhX(Mjs@-pl=#EL_xL#Hv^T`T&E%e;nu_R}+~qpQEIjAqe_D+d64Z>~Za z&0u{}tp-Bah~5u=d`4IWJU;O0GvvU)GN2wi{4*x8AdK_}0T)rwY5Asg`oqpmnf+uJ zb4hp;HC~=<&2%V#8u7-KZk zqhA4ap#EQXiCKaEweSj|>pa&oyP~-v?#V~|vDVazackiEz#OPvaZ{shZ88m`s(h;d zG0U~)(3UU;{6F8pefmSGuIbFXd<{#*po6oClGoN~?(W@POcs*sYAs^2*ZUUwdgn&& zGeMBWfH!(fs9$gEilczsRKH6Hq5CuGS`=Tdg8x<-`u@xy z_PjNB{VcBMYi!?bvqvjRPhrmY1wt9S1+27ff3N1SSvv5GBX0mZOBnFNBOwlV8m)_X zF7{GYv~^en0ntd$G2g_?3A)ntpHfN!dciyD1+b(bUP@v5h{h}VMb45+A=`e(=aJ=+ z+@f53Lb<$@wR1=}$K7zJYwIOBH2Oj;HrH-H?D^eybyNLzWa9y-jCzK9i@Rb-%A6;5 zJv`v}u?)BDg1@A?E1fejYT;*bwsiR(a+1*^pen;uBVkx=v9#Dy?nb#RtqBB<2D=ar zxF-Bx76)%|ddZUzt7+Ma)`E&vB+5#O$~tS+47|FQviP^LDh%j&SJ*~ikyj0sznwip zHFPiJl?HzLLu9Ja_9ZLh2p2mIusNcVM53-;7e%lx&^(#hfyc1QTX+DrDdTedwv5G#?IdziqoZ>7=n~CJlf}c% zaqW`|pwg<9yTG%>J zW_Pu1OmIDVT6OaB?6=93umfEEvoSYMuO^bndPr9N@2N{0O?tR=#Vn9XRQ#L>bj%XakJo4+4ns&U`>hn_d|;Wk+5$x5u&K zw!|K{7(-fj+i=rAU)yV`h|ake_M%t28C48>TzRQ|90I85O#JToVYnGY52)yz?<%oV zc(i}juzf_-vWmUB`6hCbzACa#$+5%TPyJpf53^kGuP4cHV93iibm=K7BG14V7co~J z?;xo-)}GO()xD+NkKxXKu>GAF`}92RYnh-EBJpmE>zcd|@#L(i9nh>~7Os68hiUc^ zN4^p|+*dD114JW4=RLv8ld6+X5-$QWm8^{j-8|YgZ}$;v5M^EWp8~vYHlQi+V;s#P zhJRRJyp1=kHWwSn#HOrOJgJ2{Aj zjPPjE;#>!;gB1&}aSxdvxM!X{5d=zmIlaO5`E1>gM~qc5w?vjcxJ>sMokZ1)LyQ?@ z2xE_`9eu?-+?Qy(zoj;{{i#axQCTg!L*pFs|V*T8^0P z{q7rx4)WttZ6{HkOMjF=uz0v|&DJ5EksdqXjW3T18=gFC*KInCZ%nJi=#>7)SEXpF zV&ES-++vicklAD8{tdgowwWogy0i3(PEEnWB@iQ3?0ik1@SGoY>MPnE?>O-AzQ~Ne zG&ULDk?M7WrkRG4&bgdtnwc_%4)8cBKO=uRI|AB*wU9j6)9uJaZuOuKgSihFj}b2L z$~1<=*e#kj+?ImR>{ccT?5PEj30rqxA8!(0^6L334=^lVFB;&M_hsU?vB&t1Xd*8- z$C7q5v!7B+4oyO2{F}DD*S$YFfp7QU2Z2={)|6j>iev?)b*a3kL|!vk_7nA2$gFR+ z-oA!>b#z7=a(n^&Rer^oslqKh`jhHCdb->9JG_c>BUyJp+tl>_dP!jNnL>-Hnebs3 z?$KJycw;lxn}?FlJC89Bk@pVH-+Ol>J%@^D;8YJrOXA}s7B1aPoqQ6w{x!#*!9s`J z;6U^kpukfhv6r{6K9G03+|`yxt$a_#OQ2i?yeEvC)zPD`zMeylS@^%HbD*bg%BSGL5D087A~N6^%d0_q?!2 z%9cXB;(?w4=a<#~J+@LE=DA@l`xf+f`Tger!kbV0ey;wPu2(GQoEP_A zxw!@$sfDwoWjU7Hqe;mS(c zPL~Sc)k6>bY?m~~isAIiDJeZKIYeSkSnpBUY-j?+E}L<yy#TBNlP-}8*_LT`+(`ouXKik zWIEVCw3j-qUP?h*(L*(-x_Xh1Z(ujMOnJxBYCYPo7=Gis5VEWQVW>aR2&?QEX&b9= z+T|Vn!tsZw3);6Z9-7pOkkzEQ;@(t|9;L5l ze`M3Lk!K{#{IdHUzh*^`{>IhnAKzy_2!zbZC<>pC51tFI)3!cmlH+E8YNKUX^vg0k z>p9iGWi01!<6CYz&G}QfOUh+8EUn1jJVQ}zkKJ5-HIF+tbfN^>rGqnp(Qt7RzU?HWqQwaMF)=Y-^+bC zJH=+JVh`E+ZA&Ti@Q!&0xzFUO{+y;y{R8X6XEDm8KSqonUYfsQ(yjy)pMw^&z4>8( zs6w%Jg~q;g+p738S7<_ttZ9s2Li~+IL~Zg{a3;oP^_=~I+5k08mxtp?>l6?52KC0g z;ihi*WNJsrxX7h%A@RQ-j}c$^%T0A}S`w?+rRB@|z5vmfvA zbnkhFM0hjU)TH~e5EM|J3RCO#Gl+#egpFED;Lid2DvoBUR8VLVoj#l5Mu>%C3yno! z_iaK*#&SCGCGM%gT`rIl!ZJ=iYhE#Rbq&&bMOrR>&h#yfN1r7%IKw+G*bBE0ZB~R? zB3Nkj<&IanO^A$}eTv8uglSlx0eA=({Dphot@x;{`ZnEe zCndNuSF2V$^3TTBrAb3jDmApFAaqQXF?%fr<^X<(cDpo+f=GoV2( z?;puv-a~kqE>eG|;u=qsv9r2P*U33h#c%u$XgRsJogHBT-)62FRk%BERD8Cj_4b|a zSM+e7p-j1whwoyw4Qa|?KsQ75EYM{w+95DEL`_!(sNeyF%hy^TMedo`I9|lJWz1EY1 zT#TItsAA;~6q^jLpXIXdw zV2fL90o`5rC=P~(b!8?Eoqm@0GW~W4+t8zwr;@Zx|0{X;VF0*tr;>ir_;_jxXraDh zeC93JZzsV3nIZDwWnqT`7IN9=XJnG&-_q3y5HxJkBwVKh2t*eGn(8JzT%VeeX;TFa z0#h|Ve9n;zTdz=OM#6%QE>7BNrrmMSdMq~*GFqgYvA+F||4FSg zGtkTVd2PB-0A08rxi;I}-a6W7Kzn^VaqFuCOg{U)xUKRn^Rny9KeJ{etoisg7w{?{ zA1_YVfX+H5_Ui#Ka^cIf=YzgGB~b|q=y_Bb)}3d)Wi~7%q~7E44ukGb@IRoSdM=x;y}D1PfniNS4w$Rww&Xzr0?9A}B+Acx;6 zBwk%!M(Pet|8sOw!+O1Zol$p0f7!s;mEjKJfr8RpS-jn7r3Yt7yHjFVC{huT06p@5 z^9$eS)p2RwwNMg(;)Ueo^l9%W09)fnb<%q=U&^8|kkkumDc330y0!hZyZfs-u#v7v zTLi$CERzlPN1xhNSrfRWTn^s;+TTk=A(Bsi$xr3>@UeFs+Nt;>tMmJu_dbf&`gCig zEBfE!QVzY}v-E5wBa*%Z(E5J6!u_Nj;IQO-br2KMRSs@mgt|O6SS@fA7%VVH2%s;% zu4;a<{Ogf~ghc!MyE>t+^v*!>7BB6rGgI$j#5kr6Bc?4bE{5*TwFc8mIs4^kC4LW# zrghFK0RRXqdCHRqJokW|Dewj8O>7#eWjx+-76AL6kA5A$d#i2U8ZTRPMI+jBkf^5+c8FNLkxf*=*g(0j~SzTloF3z zn$~fxILxFaz7Hsk8WpB-4-kjTmH@`)j%D9)if#4oVhywB8gf(4*rtO-)g`JmM5ZH0 z0_PWGJ=?gT=%(P)kX&1Cr5Fw=*ENTojHO-#z?m&Q1o+P{uFb*76|gTpA9g>k{LIk2 zc%{s$>5{`x=5)Ct*S8#7;;vdh5Y)>oIGw04a)`Tz;~)~Su4Vni zH#4=Kg^LSQKpR0$Ma5R3CQr?vg9z~z@!Ziut;bwjp3DOmn7`c#&s#{ugoP0(@zF}1 zQSk$3zd=t63Ump0Y_Z1-P+r-HY>UELez9r~mFx2;Cf7~MOiIp&35M`z~dW8LkKqK+@tuTFprlFe8vFgBY(?b1vJ!xw`#t8)+sS0ft>O zDKzq;?JSBM{6L|LpLv0?id@A{RlN;bllGVqhQkLNCJyWZP7dkV6xEQi9-f>i)c3lO zL5?b4CZEIYXKajOFBH9YlS(UnmITXc9T}AteMob7=_fk~RPZFf4R_~VdQO|G%4(YC zGf7jwJ`EHqBLE>3+!u zoHWu;Qu{9`FGpWb{A4cM41yLkIZ#!WGYEOGsQf6|eJTiNc$Qo-L#NPa+lm3iDwXw{ z3L%9{r06-v+igFDO+MFFridANISr+6YH4ZR(f3bVl@HE^k%Fu3^80T$cqL>k$b%0) zYX-M7L;+p->XxfINC5-q2!2OE4}2uQzE&_{R~17b8bSnvBsCZ!RL}AAfY##nYp;9Y zv^)1z^BMh?o-+a@0;e^vgAmBxAm8zRO>kLUG{{|}>H?>0a%!LG+*Vxa3;d9^ZUPBLe1dT;j@`)>$sz&&( z>hWGx$CLDF{i_gPW)S2Lb4a}rO&u1ae{+69X8xKci-4Zt17gbyIp74;-4u7gI`&V% z&5S$yEsAumzHAs#?$-QIv26rB?p9*7Du%vxml0XieImY6DnlXE=DE#)3POlBH|7@e zB6J3zzfE{sU)jK?gSDi%29(!Q9X)y#5F+~cZBxxr1#D~iS8^qGxQc_f(4(sfkO!!W zUu-`NR=u`m+FED^2yU)jYOP}Zl_Gdy__V>tqY6ZUKaJ@GbinPDJ?(K=MSRQmEXuMXq`#+ldOg$P@9(H_S3#<9@RU3Q= zAo2D)y>%}erZ23aJZ(%j!=?AA(NtVR{&$b|PKF%ol<{>m$l94%&1R)kh@WQS)$;c~ zUg*6IqEolahXd7o_n^bUfWge*5vo|fpatA2O-^J&mb|5OnvF9eHg4~!_`LjDC*@cn z>MIX%wWnk4dJf3BMklE4H^l_S{7$u3tIPCP=aB~apV)~|aIF_gIkB}7|CEzNCL3y6 z$hPtWBf|bPXzx(uxDL3MSInxX!eqYM?eM46ey0>vZ8lO%tc|31CqO;onmGyRil0nV zmCrn|C!iDBztalz1u|cKpr)GPA7zKhQ;!x^;CW3MseL1N(36b}<=*MP?B04iA-{A_ z`LxSbzl0L`JoP>1+$)lG8&bVhj>F|XKwVziw-Mj3M!unud~G>fzGmnrpgdp3J1d<= z8G^1|H))y*zd4b^=>9n6YWuUiGDH)qDfxMfwco`T=V$ei=t{eQB?-7xi?`p_sP1^A zn3(LO(3+>k)H#;Jiys_Rnl015U7novn$bF+5_tV=t&8?e6rG9vb4}Xk29%?b{QT_! zZ>1G3-3FfD95ufP4XM=BF9)BsSzjElR2m?tfbkSw+QumQ(D0~-m-AC;3|~D}{kb#ngu1^_YBe{=^tv|k82H!N{FEx-Rz=g~0T+`NMo@6hr(sMuoG8D}jy zo?$;`R#&x?{Q)I#n^m=7hPW9Ctf9VEp-Nvkt~{Pw&T*jadk&WxvL|o_>H(}ENUM%; zMXeptZSPP%&}mJzt@_wDkoMd2;%ZKeKvz%|z1WFKS*6Z|7pJdppM89{q9x%qbDN2C?L@+* zqIHhoP=8!A_5-cp-3a?zFu~f3yDwGWP)0(%hutapshdr6Gh8<8%v62-MJnfw@5;x? zR)YB-*I=D(6J>W}{o_{}8#!c=qgIcz)>zk8qT07~BL=|<0nj;*!KU7$IYO-Mg_a|c zgIL~6KI3HyA~z1Qj7@VHkH(zPq&Mv@p4aK&vJK+be4b?f>MU&HkQ)&mj{p6f;ITj# z|240PQfVSSnW#1Ei*4J4dpBD7?KEg^NCRtErJ8utrouVO<-0*RRD16(n`wSVA(&Op zGlZ*l)_)R?JdiSNXz7zS&CLlxmj-mK5POf2T!weni1rY?#`u?fJ~wxtU4JQ77}eH~cB{++ zK@Z;@pX*)`=vq;-N4?2(Gg7Fm@9BQtzB9Oo%;>hWw0fhA`@=FLH1F8My0+CVWD<%$ zzrK&qE7kW!FnT@CvD9&6L(E*XNY4-v3_05@k?G!XQ9fs6LdG<=&)2xUVd#-y&ie%A zm8k&#lPk-quCCvv7!x?i7rt35vBSjN@A##!+Zv6_aLOBk?&eZ9=qF{Xdx+?qJ(-c zDVgAq83o4)aDN6WvpryC-Q_5-%1Ym7lCUa`))@zX79OtI#YT)+x%1z)q)$35y_Z3^ z&$1SHws~5-=mS8h^!PfxqCS37H(2xx5fC5HQ`V%0yf)QFQHfv4cg#6N#{JxAqnxvH zi7$WL)_)hSh{*QBpZ5!lsRq{0i+HG9c_6iVMcU`a7jwW)yZ!n4gDUHzU_p|QOZEd9 z%XgzeCRK@?x4~6$GKBAg&VxYvRWM*JR;PZQrvY!>s!&t>>)6x3WkT1kiZvhDS>}X)Exoe1ePTug%HTx%3ag zT(F_^0Kejzjd{PBv8*zDuJG__$Elt(a#c9|67}qTGc|FyrABHE`r*g9gv3QKcfRuy4udt`Q~&>pi>a!=qZ)|K#J2I?qBJyW;mj2TS2D{k zohx0LMCfuZO)d=*?%#C&m>OJQ&h5LU_x!4`NbdzkU8WfPTAPosqrUSzc=%$Xgk<4# zv27|6lGujcoa02KOY7930RC2ci02mi7O9hSIvwxYp5QRLF-hd;j!&14G7T+351kCp z`d#hQsnZn?2unEVW4Q@V*@D|f>^ok)oLe4^trZwyY}3XqWaTwJmW`7C-a5`={>f%( zE;OFeWsY%1x2-gU@%-P87!2m{{Fv9XLW&#_Hf)&(u*O>}2Unv|MJ;=ueOM zE#oh#l%kZrieDDL(QtFbL_0{DDlEC-xmQw=Wk-F|l`Fl;GqPd_a>Tf*`Wd^N`7_o; zWa}L08&BoUKC_Xa*J?(0+XJZlGPj=()uk^rRG44MLy$&K>GL@tu zPAW<(L9e9a-{dr<4&L~fx>(hubrs9a?U(NG)gxxfcH<1!DhD@Z*-;y&bmJ3b$9LqU zDQ0urh%uw+LS}De2&Fw(=NczW+WuC{p?N|ndy5L^;26wf#HV$Qq#-aQ1p0NyIV}dC zdqzq4RF8d33`3X1_w>TPyOOHrk^tXr=L>!f8vgu-#|s)6WA?H3M4vv;hY53V#pT4a z^F$VKky|B~9H~Z5c?+#VaC0h3rN}j3Y(#T+AB0~YJl>jQcjL5A1NRyBzS5Z|kZ{q1 zUxu%-)lhslw3Y)&pOc@^U9l=x&8$C6slfDEYR1ps0KNCwnznW0o?xD_4~R>>k>BE8 zGic&+3c;^ea_lr;#VXjTJ3TOi?k>DK9*Vit(z66_n1dF#OIIuPqLm37zIFN?62pKb z8-_a*hwrKE{%Gmd&fA7It}44Ii13>0lh^!x&;Dg}n+t9*NM|wigZ{CWMQMoQpz>r>1uPm~+A-F{i@IS~edlJMaX0#dbqtc&COt(S*D9`vo?M$|m#U z_;Am9>p)_nVH|IQ3g@ejL5IW8arpYRos$DP5eaSG0TYCX1gMq31N0gW%>ou$;;$il zTDA%pdh~reExlwn%kQo29?Q z?X1vWrQO+l?BT)F4j&&`dekj{DeX>g9xuinKk{@6#SKWO_SL6;1$xV(MCZJ_b%nAU zCpmNNbI>ssWtGa4MixEz>hDjUMZ*q;pD11a1U+vD9MVo($)oq-xWGh}?1ZxHg@8HS zWP=gmK?SWCVWVd9b!k6e?)O1AryO6;y^lggG_xL))<`U&&Dj&WF?Wy<*s3H-?3J0= zv;0`yZ+Y^Uvtsm%&i|~5b_Rz$Rn^+f~*8~?s4wQK2yqDrjZNn1OWaj*Q z<@djIN8^?^8|G)*v1Y`8e$j>rENaGUoE7W$tF*ZIkjWcaDp(A&30Z%(I)}};C(uUF zXlp57Urf(9he!G*)-Ey3;I>Q}Jjy*BD{X63xP#pRoZ)2jZJs5()k|&Zyc6dCUYXfuNN3 zrD6Dan@aWS{QB2^G437e-VzmHSFL#@V(F5u^Qfkb%^>CbsTUJYHMxUp86H(Zapir) zc7Sf_rzEq3v1u?&IDoNEsj*@60u`?nvhlvfE1St+!i>aSQWxf1E&9dECg zE!2&-PWmPuYcOBR4nCNcG{u>%Rv&QH4PTb-J?b5AdT_-iXljFHN$9GwPu(`F@gel5 zw#_L1?OFqhxV2Ne_9?cOI1$))YJN2_C{cKNrv$U!tW&GoG&6E>c#i2B#m)}8dvnlCpL);__8JvF60UF4=TvHp-ghuro@Q{>8lm36x88 z!!`CTb*1j%_b8g(St|rppElFd4kHQ$GikypC@J^Pu$MczQBZ``Z`s82Z<~tqsGg1o z=A9eZ0{*u#-;JEqR}I@Lw&S9pL_AOp%NA4mtw?);>2=1XCCA43jFEF2o>Jk{Y76Z{ zIydKl&GcYN^5vQEjt~lpVXomW3&|*g8d)_dgfTgs+S6sOqoI`*{9y#~ENKdcaTu ztQRBxJ_1~zE~)|MoCWS{Vg`PLe1TR+0hkriA84@jp9@T>L%^K;wzyU`{<(vR=k_Kw z^_t7%4ZYWY9|7L~|JHyIwhPsLAZt8v-^GnE2D5Z6kS?83OLWtp1)4^ z;5i>HTz^jInInNuB6682*NL~jf$w$;tyReMvP+^89#khc{Cf6k&>&fIbb{7L-t2B5rl~u9wNXw*KFHe7Ka5}0G;7AP8=5^mJ)S=yhsDQFxW7g zUfGOB@1spSZbAGStY>gdHmy^*;uqQK>gq04ZFt=js}OVRmA9`LwKBZgr=Muae>@Ye zc9wcgO~IyyPUk@`XKqK}VDk8v_{oW75bWfQtL(S<_0~Pd0tMJHB4qBJW~egcX&YXD zdD9VNk8LNM7U5)04CZ>dp{K~&PRhHYeXjdX3`!zdzFU)+IX+s?=Jj42P4a5F@aGhp z7cTPK3+m|Kl1`Zcu1cBtrEGoQt7ApJdN&`Sg)_#d+uO-w{bZ~B`@7%lj%}s3d-YK? zLXc6}`_}R`?J(7El$3tovZ~gp1%!t$4o?yn!2Sl9iI9V7Cxlk{XB%6tId_EaXt4_J z=ayrY0_BH`!`Rc#bGY#p;54|A1^D5tn7g*54X4xel^j^RBr0_8;>>{^%`96B=}WK9 zLdF(+Lrb_KRj+a&jydN{t~DJKy8w?W5|ECyqV4?@=RqDR%}MUy^Sy!csrxLCzm?G^Z7RubQDO9CGs%>@>( zo^FSR^jo4b=f8{@`m@K}KHl(!kE>t3V&VPm;7bl^zjD8e`_#uYkRVr zvi?&LmIK@oj-HCl7~h=ynTbKb6+d{kAAQ{a!Jqi#UI6L1xBUT|bwS0E*J*%$39<5LEw(P5 ztbX?7fb=QMXRnmB>lHymhmU*2p=qSeY_Q>ls>5Pf2*lzz-b<${s7DyN=W26OjqbqO z1REbCa!5(@EnDExSzq{^JI9_|94pGNW)MTXV(DJ(_dCvuBSWPqor{)>q}wM= zNqu}ijy3mmJ6ffH)x@f>owDusIZQDew)N$i|5nE-srEFSra|U~zf8MRRvqFV4cA-J z-6A&MOXUVJ22Z0Xr1x>>t)-~v}t9sxB^$YU!Kr(qosgtR5l zGQB;xdhS4>2m|v|ZxcOO7yBSL*5K+|;s8 zY-jbBbIx`$OZllD$Kw}s_|G3EPCG+f`m$6Ey;_zIEDaC!3!tI2@|X=@#H#XXBr`65 z;#o8_iHVqw3ly0|!j{t=OX$y-adVVf)jh$vmztOwhzU5z4UilPdsgIiYoLJRF zx&M3!7f!>i<5tn;36b|n4qQ$)(Wyj+6i-iv zUEMw#bF25RW(OjgX089S|VCSA>(~1VsmXnrf&&J=q$`V^CJ_pr(v(6 z#!EdfaUq`-TO&7jvd3@7J#*+Svz>d21i@x^1TPq?-$gFLP6;T+^3xyri2xlxc>ZY9 zM=&eYwFO$s#!WaBMA~!YY)#@@@kDifCE~vG4R?OkFjd=d_CQBo;8cyE?q$h^%P##d z_TDqBspX3orV5CFh=S4;5v7Xs9t)x(V566a^xiumpeQN=BGRk$BE5%Rluqa^B!Ki1 zIte5s+|4=Xe-0ns@9%T(!v|4J_UtulR{gD+S>47nGN|bUmwMEhI6RkU%7%~DOISHx ze5S@{GQScvf~^W}AS7a{*hyjdBqo_rn*xH759jdLi2B6|Lzzet4maZ7q7v5%%f+ef z3XjnJ6h?d)vtx@~Xz>ug0nrs0H$l2js49ZZ-K08-7&h@({jE0ktwg z`~H1~f5;#2HO8#x1{4CXQl0So$RIpib@0RUXbQM3qc2U4;)k!-!MRPWaRYp?R%2R?b+QsIkB zA66j+wSNY@V;;Iprm$TwSzOsD^^UnGA0pCrK6F7L4fCK^f^(>N@~lAlea1J;-!3N~ z_F7+VO>pVGGXFs+BJsV6PWVcEj0R3F+TdI7;e>9^l?xZ}zJ!F*6*fW~K_#AdwrRz@ zS7J0yCJKhFTG-#$dp|g|>v3*o3AHPwM+oBdmd-C8k#)<_)Y7enk#2_|OVFz;)-K2o zmu)!i``;)PFa6&7&b0SvVN~UujSQx079oo2fsdP_+C(X+zuy9KhVcy`XnjJ4BtdpJ zJE0E52AYOLT#8$}&o3tJlpCAGm_V4U|Ll22y;R3N`J6J$&Y}Tau(qK<#fj?=Lnjsd zLuRwbTT*w;OP6>g5zKM;`wC!^BpBsp*33N&bW+Nq|-gkQ#=F<~3`>x)nr%>?fH zPyz>y9Ru(RNP|*EMUqyk0P_xxlz5mvv1?@S%k?dSp$D<5PS?Inp!GN{ov@BauUr^C z+Vt7m^--%^AN73H`#avZ%19!d1ewpp^@>Q`P=T)e5y;EB^Shgu$q=^avFsuIL{)2$ z2cwU|VLXN#ZjIG>lbKHXTOCifit?Q4@!t<9VJ0bgKgiKDq95F|5|su*MM8=*kr{Ct zRj7)YAj$Ot&Mlb7nw{IPGDYFvD9PDWABJ2`D`aRQnuDBBzc*4C7FOK+FKl_Exg!a{ zEG@gR4M*>cXFt@~Erka7?P@oyDELlehkstWKWtJ;Wgo_NBc5#m1`(gQyfg(MjcCzm z@D?(lyk>TY&zWh)0g6Xd5E-DJ$VOZpLCLX=ls9mLlq?l{Jl*0=H|%k%mB(QEYOJiEZ=1$MkG7$kn~%Zd zGK}W>HJtK`6owNun_B5)#WpFT4MhQS6I>YyU4a{=qZ5 zsFl0d(7674W~ETykd(Uq-|@q3baA?Kvo#yf9m3eQ|14CSFZY`FJDm9(6MGGJ(|l)% zuI)Kbv!_c0cx|)>uCo`>hqEEK>MeH+* zt>P&2k*K>HOpu{NL9M}ap_;vbFQGmMTAA2I(_hEaIh)TqtPRu(7#aG}EA{N@kZHy28dghi*=%>Oh(yNM|t!4hA8B;3mc%2&HVlND< zyQhSPM~%<~TdCdyf7{Hu8gvB$ffgM$tkt%qE#2oHM2~uIphy(72tc$|p3h}m$2uZ< zu|a`4Ev6~`;Pv_P8m^m)UhP?kSsdYpu?|n3LvGM`8NLmFxCvXe8C_vqyx$WP>={PR zKs4R>QO^g6bPU;VP?%3qFbB z$y@*xXLjWtehPuYjJU3J@X`BBI?G@1L7=I)SQsd+I4=yyoq_os?%+o^@JUYg#jmc~ zw?nS@9#2BXR>*S!E!0_FzcUg+o%iGNavz)% zrC{2+>iN3eEZSGD2dI;yD`I#}>-LnSn^W=En^)dFmBq!FAWXc$wZuKup|F=>dvzu$ zE<<6kHm%vk*_`h^7fcFFY+VGm&L?{M!*#l@CgYwG#HlgkCxzFX7S#)~!LY#nddoJgP z=W13S{Y6>S4;_vCs?7%J5f0G{Nj~e0ytBV+MK}o=t}~Uw*5uX{)5n?8FS@tGkz@5? z+oAU~6c0=R7%xjY%ls3G+r9;Z+HmA%K-hLtcQtA<=D1E73?PQuM7?sV|Uj=EV=Y zv$~s@*#<+d6+OIZ9~A~9p(~HQYQ(271n*HpCDq~kFF%y$jEmY=O_s~nPx17dz+fwP zC6=j__Kiz@WOu#{4s3V96RNLi{pRThDs$Pd42$isR`$d%4ELNO%mnL5Wk*7?{Cpd6 z_cR;4-k7no1RNm+et!M)9NDF*M1|xv*Z!od^+x8f=>A}kVwJJSPUEQPj>;a}C&|sW z&@DxM`(iJG3q}gMBSjuoe(dMp0pQ5pJ0ncow^b>U{BX;=V-8S#LrsJ0J%p0z5WE6( zZxx)yk_5dfR57Mhu_GWQrm<@ju+9vrKfB>1;R9f0Y|?7_9!Kk|pOS1O(s>jN>PFdJ zx}Pf*`2H-?BJ|!?*jX#JD&Qa5!;|zYM6a*zzh>M4sho%Y5->^v5VINa5uaaM!#!Va zWx512Tz!Xes5O#1I&AUd>4O~IEo^(K^d;A6b~!@?Vx=(KnW9&$IKlD|>6N8|Xs8kT zk}C4Y`cSEMK?12}!~_64%>eil*uf4ciR&c*UPvlfT(3QbkERP%K>ozlWdVyP{*9M` zFI@&n0^j!sF_V(Y(xzPlY%CRkB+2f+bvcHo)&PVIPK5k?Pw|=2lCwhru;OoSi+?}H zkBr0t)Jr){GlnEUSLj&}(qPJzSqj}FvzSndL)UQ~gHji-af4U}`r{t|4}tm$yu5cd z`|R!4uoZ8O6J;$jZ zIrXWh!NF;u^iP<08XTMk2dBZoX>f2F9GoWHr^o?-o18ktQ-^r!5KkTAsY5(FYIa4#vA*uPK9;`H#Gu)rI4`*3ph;r5?#%b*@q!*W)1$?GO+5 zJL7L6Zr!^}LP~KS^!JZ|GKvarP}ckd-hcm#ltP}AOq`00jGEh@kDdI#5vZl9&!=z{Wq#N$s$<4jIaLP|wS zRz}XO$@t%}+#r{8|I$FsJq^6oy)l*l1Ys1Gq5sW=l!Bdtx{qGxmGXbX`jhmC{ztXH z|2q=W3DV%)6NH^lkva1}ZUAFB0^%U|`uU|3!;*=&ouHcRlo=-$;FKAs%=ky!PSxbp zW}F((Q*ZL0$3Atfr*Q_rfzzPtGy?r6!afbhPji#w{O&YSJCP}$GUJpP{{Rf9c*coz z`ji=`%m6s>|1#|7@8XXtvtqJ7ktck=8~|*fZ(goTzm!7Lqn`Qd;lF5R`-=eAX4g)V zM6hG8gI5%+81dh8yawD2DGAnI10)=mLemy9OlqoEGfM4N{>#R=UsJxI1ncvyh+5ZW zd%^5Y{pqPthnX#Mj0Idqz2YZi?bp?>f)t2RdJ(vT-e?z7O3TcsIzq+p*1kbj`GcM= z-R7ecAZ>dsNSb|>lX7u<5!1i!lw`%^3v^B<(_kViPUP|%3YqZ2P+Q(B&2>T)$|y^OL2w_hkKJ5{))Nl5 zb)4EtWf{}oam%axD;J=4lLlk1@AVqV9$)??cv|p$7YTa6`{#xAxIld_0ZsH!+=(%ugKY zaGlpVV_D~TyFu>UnVGXZpg|Vdo1HHI6H#uJl;;uVs0aa=2k-}Mlr0!W=V`#nwi#I2 z6FZo7ait>I*`)YfpD7^Lrg$0;NVbTm&jA;KM|4BT^ z_zbFfQY%w2=mHz?jpwUW=P$L2?KN$}{>OPx0>xRLedgbie_p@1M&>$1=})mF&2BVa z%)s;T%lAtHTAp$~eRrvz0KHX9Oxgw~yRL|tcfys9)`smzYGa3{tg^&M(^>y!_a`z+ znnp@pDJPLTN|$O#>SQ1(X=G)q??2+6Uv#70?Ti$dpMoB_;f4fWWpmGWMSJK4b=s4) zaY9qO<06@EgE@EK2nG%G`!`q(-njKL^UHB7=w&)g1D?}s=GR@6thQ0VAq?ynJBV{C zsP!Xw1Dzn$0z8VXhjS@WXV{Ke^CY-&fnugdm#;U8_ZoUZxN#nNF&b^suXfs_L@ zzn6z-9HiJXO|Ae-C8FY1=g85+lgswV*bi8f<{`mJyI(RO5#5pp^wLXx@(!NeOW`jv zoag0t(#%_BKu5p;jeo#K*HU?+<1^kngS*(uF6k z40mC9E5^;)aG=G!S4C%?==;byp)WWgwxA>_&z%s3x!RrH4lyO`hY94nKtI!TuPy}w zlIS;A`DcJj7ytfftBs3@YvKd@?Uhfux@bU0vKgoL;Vl}unt9Agk^RfP7H*NnCT(8$ z&DPnlj&K$Bq<+H8FTf)NydrykS9cX5cWz~;`Ldi|B~$c;E|Pp{VWc#mf-GiOI*8H? ziNabMgBftwYQOULbF{4zphCk+b&S&VYu!3;H_jg`!+9N2V8~T{`)ac!vzaTrGN7O- z8%Nw~VM92rNc~W9q?$+7H4vVQP2)U>Ta}{us%#a4S4e)-yS;^>Q&goAn-!sO6|~rXJ|%+VYWK0xJgdUZ z{~)?`^QT@hT_4Ov81h4c!ls{=#!%#%;L3;vvheMhrH+@G?S(GTVKuM$q89o~9Djs$ zuSl&^>DGD{S9;Ql?T$ZJ;ba`w%YoQtJ&om$=z+?@1C~3$8uio@h2_?Y-ih16YSu>Gr@otr+I0N^LkaVCze5qVy=5bQTl3lr zbx*e{G7862m1(WaD=KG0mTgK4{oIa?ND!pbmMIXs*1(!CvPsVQieqW7rlG6ebpKU*sn zd&lc@Wg;D$)RQna2TbKSwm>fZ&9BeIUG+VWZwozf2ZO>Il9iO+1l%5Qzkq{8AKVk@ zH;uwO3}uJQWX7pr`sq0%ECaUqr$1!BU7!aJ9Y(KBUn4l7np)15<8AOLeBCygT zYSOj)1NOwOgvpuf)r;$XMAywvX9Vf=D%gvTh<7%}v(kayi|$dDQ-eWVUQf>`Vr!K| z4(E(Y2?Xqv2EF|LZRqo=OBzQ@9yNz&9S(rI5QRLjs{$78D}r57LfFw_4Lx*m&^hmA zuR;y%w+z(`lPuvo4k+K6G3ZajnkaeEX;CYCGOJfKR7P~yquo|H$A|BZDy>(U%wrJt zA?Flf^0f+dV$aP&X?iEJFhN|uRfI4L%TnlI5m-^<{Zt4ZNiB2qHu!MV3yYSj6Jcpl zJa|=5wQUV0_GyE+>dW@Ut|g_jrD*5lUar?2`FRocqirIHc%K#tw%!NozZ7(htd#)9$sKSlv2wpi!mb5h$We)1&2_*UJQoB{6+xx?i zKF6uWUPPSI8wX4xUEdXCq@-X zRi1I2xj0?;coyz{^yH0z>refZOQ;Ni4}_Ef$dJ~mx5eU_9@kMsf;S~yA1;nTardMA z-mVz2%f<$|oRz$wvC338^INnnahi$*Er4&rZ#)5dFA%(0^$KfOSYz+FnTGzf4$w{j;~EJQuq3`P%WX-ol@$2uQQoa+P+*7&^CgEKqmvSeA4-(>(v49 zIhyqul6+%*a%d4?x>i{_|2lH3=6?dzAmYVQp!9o?A=0|{pp+xOO4hLhoYO(4` z%y_Qf8k_af6LuV~)1=$ogBqQ{9sf`KD{jhP^9PO41h$l(F&8p;5Hk~2a`T?6(+7)r zTB;?sf(ES$plNe@$#bP>8&6qmE$!3MkD^5H3bPj$OK@UzT$GMoeudf6*QSzDqv|yF zB}rK~Ma0ILls#5STW&sfXu=vQ{nO}YxsJnH;vucbgxYoa%(Hz>pDhxc;w*Ir{I@y< z?q9&@l4qjhQu6CKp4xc3iJb+SZu$zmQ!~{6R$f9yUYMU3W|8CsTjn2lVm*{?ye=r+ zwd#GrR%pckXt^lt0hEj(Y8sPDwwwKY%4gRL5!@>t0@libu29kY=Q!A6*Yi-xB%8;7 z>5jq&TQ5S0)S<{@+=k^w9kwL+i?Pg(KpQ1d9B_Ul0Z-39cyB>q%i$d^dAjamTD)&i|*@A{@>Cgzjl2`E6K6*Zs`}OK7*K&lq;_wmAPiXp-*Qy$m zEWel1)m4p-iU)LVlDp-j8CW}yG5%Dk`6^6Y-j1tGpVmrVVV(L7wYas>VLq`;HH<#e zRd($C{jBmp5&J>K44~Ns3|h06pm)u}ZCOykq59Sjn+|dY#p@bMVtrcSG)i9>IXXpj z-Uetd^4S=Uh|uDcr|a=9YT_wKXO}-}kKQ5xSuD31?cCyX-+cFS_IJqKuf=30I;ENI zPJzrwT*0K)ibaz5J$ki%7RlQB#CkMFZhrWO3?zQ94ze|#Me|TVEqQXp4pTZ@r&xm1 zd|!M27d zu-#nybLY^5Z#Kanj)J(n8*Aul+?z@o_?MhHI$uCi&!#y($hi#!qknFERR^hOsZXM` zW$_Ubq!abAZ_6ICc;uBLEg*tFN%9e_dj!j6?_Mt;-p0K=7`N~UFYQ&tHppC4;c!b| zF4=UvH#S_93VCHm8$054SgdmB)~u1LcVmaPFDude^X{sTw&zPPSh6JvVS8uB>n(nJ zzAFPwtY&$nOgP7Rt`>K%No!wT#MXQg0*BBDt;{eZrZLT=vSVP)mU+GwBAVh-!MUogkDrRS*VZ23z)UtS{x3YUbDA`RVznc%PL z3JO@QEfGbfzE$kHHnl?kx;!RTX>Z?n4NDYdrtgFK?(=b}_>C9Ehurrcd2ItN?*~6H z5(jg?9i<(E46D@X?6uNo;TlpV-EBmc2Y9in?|}(^%9;yT;Tp^`>Q|h_IjjShrB#&9 zjTlw7k6XBHWwmJh13_@xlLAS@cFsa6-%%snu$a6ZT>*hylieG4c~l48RX#Crmso}HI~pL0)iek zFqi?Cq0!X;#2VCC75 z>g2f_9yGUvK_6=VCDOoUwpxlf&lqj(nvc6xGqTXSY`$5(r@K>G6`Vknw)lMbTQ4oN zpyTb9!Jq{u+r4cc@pwL#AH?A0mObgG(#yh$m~Wi2Y>jYR7!4$+@8&T66AV|1INu> zAwN?)fBO0>bp5ifi9)j96QlmDXBvZ*Rz%rTXuWwZ5qKaSX3>0y*KNU4y1{EfM_47` zYBsi7>xs&p#LB6nyz7v2RON?cuAbG%O(3bv*sI4Q9@9{1LIb>(WX$*q%ItK9*SzjJTkvimdW`?6r|txxL%M&~W=7Sgut$h5K$5_v-I?0)lXp z@mOc2m6+ZAN3g8x>nS@0bqoB}yWfKMtdcIU|GKX-T7^v_daVRnSOSeUbycD6(%e*d z(CV*`NBG+wt@Kddod-~y{kXLQbVM8M8&w9j-Q?RlBr9`g9J9*uCb~x`^a}*+aTPGv z36)gXBEAWN3Ux~#>Y*cLNha*B`+Qk>`|YQFk4zI!Os(Mm+ha+svZKpTid+0rG%PD8M^5MQ|^FuNw zZ$>1i&-yI<@uie^E%^rmhuG`vDQ&(=_i|$x1yet(`uiOrD>EKP{C*z)I3{a$-5P2x zgvq>Kaj%3V|9*soU6N0DG-hKY$|!muXEyd$&B~aamG)aExr9FYAqi`Y>MdXn}S4OYRS$ zFq7Y*Vh|{HOC_tQFn(q0#b7dFN2Go?cd3wiVvi>=Rj6trXf#ylr-{^RkyR5guZE+h zAZG%dObd>ejZdyh2vXny8`5{CYV#jCc+Z&9fXC#0PfJ)@P1k=5zU-E0YA8gnxa$b* zHml>-Y1RrgPQl9s>HzrL^5bkwcU@%`HQ{e#8vcz2u0p(6*( z+IajP{|#}6J07gNm1ziZQm2UJyxNXQ!`sI7G^R9##?8nZgFq-Fv-Gt^GGC|q&JT6R zKLFCQ?}n~lm3;ea@^wq$U{2R~lQIolP5XR9l6}{tsHm}*uW0FrWEbcba^ukE2yxEgiTGdg<(Nl%GG!ptKKaMxNbsGh!8&p0W3ECsdV#5QG7?W&@W!%@>M}7V1y7hs_Evlx_nd3xAH zm?6H@roT8s#SiP&jDH0C1Ep;iYG5&P^lNtX$7fB<`I;bP^ZX?8b(y7fGpTW~(>A^>M|0vjA zMFloq?FVs~2(*zQ2GXNk2+72Ol!hdr31w5VMKVRrNcPU4hT;rT^i?!VYpZ>-3V>cW zYJ&7r-G2H$2_+$g#e@br`m1BjB>b(;4{p!B)abg;ptMq$dsn?}f{vNPy`ZHd>i{XY z8@7RPsJp63R;e-~x1%7pSv_ry`sJk7onLz-%F?_1UbDjI(LG)<#!(VG>`M{TA%qS5 z&Wsn(;@7yNB($M!jD78uCL32ckX*CwNg zub`YRLI89tH6yvyG_$i!+s8@|UmBBvTe8az{RmyoZxY$(eSlSSXd`Z`!ubN%dg%I| zx21jP+FOfV*AP#`}hq}a18g&6L>nRwkVQZt;V8Ew`TEfXff zIY|%yt7vsr80c0~Wvr)v)6$>*C}6aoA<1uhtG&Kq=0|n2TREkwKF1zCBbbwvGi**1eRTF=ox)z3CDa01i$1fbEm#T|fBI7C0mag%wUGt^+)R(b>JPTdcORoxZ zgoX@d7f}Jvl~SD7!V-tmG^$$wD$U`dXeZm_{;R>GvRz?Ny`rWVTzclFIU7QHCE$3 zfa+G0h=Gvlrl9ejh3fMHoiQt(B&0!{Z5dFnJni_&Ml7@WcqeOThH3hWu5q?}1wq_;y)6@$(Nn0FYEmQL>7@ zNl)YT^<6C?FGMQ{JG!?TW!~1mdPG;)}?b z{wl`&x&g?L(Ybp=V=1atH2ed;sZq zF^<~haOg|9J8^epuV>|Qe?yG$H36c|OgZ()WW9M3v+k(f?;PR{G2uP3!9t(@)L$>$ zyGo3b=kyQ`VAG@~Y{6aMchv&36$_Woh3zu_)E z|LEUk!xUGboTZ9gOpe@0B1w$oVw3y3v<&(L^i_zC2`kyHWh8T5KKSXC^><;a^*K|66gPs8gLybC||$beX@T21bAykv;6f`*Z&8`~Ox=LENWAopS0gq4~c?rUHaAdt%CO zS(Ba=b`WV&++zg5%ixeMj&fI!K}HWPMi|`3Y^$fpMFhtLJNlg?Hy7SJh2U$8#p_f*w{qz z??5DEIl#t}9Ei?|!=j+K00jZ>Xili=3&1wL#1~yWF*pT40U6cbcKL6z(t*t`Ss3G6 zCkCehD4?Lfagq%}0Ga5P7bgb40N4Q<_NV_$?Rb)3k4@u5P{#kJd&;Q4IQaj~D30v| z$h>!K_3QF(quPfHLWX>_+CQT+@+8|o53`x=!JwvusiV$@Bm1-Yzpj%}Un~d3-qCL# z(T@uMJm1yneyQ>sYh>5sM07=zw?t=GL3u^^LpOm=jAn)K3i36c^8ECn9*Mk`atbx2 zbOUTQ3P)166t;KAdV%W>W-;=PQ!l@+W#%nl%@WDna_xnD)(mtpR?n0tBy;)8d&GHN z$P&O}7H|QcXJf#>n3Pq`{6`{id`!{pED_Ni>DAC1A77!&Rjq1UDh3+MEQbqq@2+SG zh)AfZYHJSuDYtuHVniF`_cD#!AGCFLNkj=p+XuEe3RmhDxu@DKa-TM;Frr#@q!Yft zl6Iwmz?(O6m{{~uXhr*5OPqGJyU)<|r+n=fkq>frZ2If?^&gT1{9(R{UL1XK2?+UN z1}L^Vo%*s&s@FAI5_9cQ5_8XXR3e$p@|tRKoz(@05D7*QcRb~j!GrP#R7=K8QSG$J@Mv9HZJ)N;(BZDGTXr^GGz$z_ z?j0!rpO(+}33GA9hzvM{>0RH6&fh_~w#Z(ZuWj-8j>)%YsM2GNvA<$Sj|yPU5+t^@7w|mOA)9!E>l|(9fM?bO zMOB34J*mX(WmwhO(XR47*>!JT9KAp=_i_Q!SMGpszJ5YZq`1dS3CV1VaC~E)=EM_W zS!X^O$dN3iEQ!t)U>8bKBO4EMXK4#Gavo~Gi<+cV8Hwn6`Ze=93w07WV)PBou^G7e z>;k-y_XbEqN_%>0rx#AlW}Rr;+BR8HeeZ0>q@xG2xoGiOSGtVv$^;|ODb>82GxBV)=G!7qH*%Owg+W9<>Lsy?ZS;5$emArVkJ zzJ7mVt-{bd`DXcfP|6VPQZ@%etrB8mq<^*S!+`PBL3dv z6r^;hd1mhmj)Pu&{L?h5kpH?p5WF_@4UBF^Ih>{ zqa1w_n7q{M*yvt-lyW@*Tq+Bi+U|6Pdh!#-BkOx9Ws`IPE`9OGgp91e)^n}u@-vL{ zgsUQs-Y$*~U{GES0Hb%w#{K+yI!W@SRI1i9tWY=EycZkm6WO}_7M^$?@BMmqnT)50 zT2UdWdvo13TRU9Iv3EUH%8;Lq*V9jkZX+7|5deCa+82O%P+BTfgZB zfH1uGuXtdAIFQIAD?>+Q{qSm3?xBHa{7Js#a3P@ktmVgb;+kn8uPg$;mhr7@arBj& z&WB^pMS?04!KF7qh`gq9_|D7dvJ}L_=NwJ8ETk0M)RrEhs>w-nd!K}9w8Mg?QWP3K zEQ%e6)EBcrlE7ovKvZp4A>D_8os{o>c}ef+QFEt|c^l7M%A}2r`gSKYr#^c6Kr;)l zZ2q7dlqK_rtay@qk4D+m85)#D$1}HB`E)Nkiu2U;PEz|TTDs7-=IMh-4dB)1KL&#& z6Tl{1DR8#i{WS`esOuSvIdrqz<0V|cnmnPz)k}2MjDJQ&VcxI%n3lf5hxW1!q6;n(8?)@qiWFDzcglI6kac7 zj2TM;mpTKRa{+d5kvSvu9znNI1q9tqWwEr+LqqxbaO@kVjlC>UH zTHNrFlUwT48Yn|mj2F)yq0KfA<22=ZdjXI-=fcOG1BnXKiBOe^twxwL;c-n=F8K$k zU=p$|ma3z=@jJT+N?jSnGKGfU;8=wRBmo10=NGqr0Q<6n)O4;XfMc%?^V{)mAj`sD z-PEoS=TfTm02uS5af0rs{jo>bvC@C9H)K`CYe|??MSFR4B!PE?jgR*vo>vN}N&*7| z$tg-30kw0XFEGXi@>m7HtQwHY@v8u)%xtOLXF>SmmY9!U$d+)44X{N1BIieUY}Uwj z@g~e&iG@dVhSK{JZaxS8^2axD{JH74K(6G;auK6wA|+GomM^`Y+Mt0sT!ByjWoIbK zoa7sT{c0YP+Lfz@4SzmHQnp>L0kq?n;OqNVX|PLXAFl}A?ERGlo~;dAV>_mj`Qz+? zgcd1oHK%KSf+e%_*bLWXZipR=eEXRI`b?4CL$lSnrk%J>z(M?Sjz%9yM9H9pTjxV%j2Lfb6?Ge=L1|k!rIi| zW15q|_e*1sH%6D-ZRBY0Ue4Al3Z<5vYZj@L>RBqV0Bh)KqnZjwZX#^Uedv6p~k{^~7a zdQ8c0Z>Y7~RU$N{c649zkWiR7$4E*+WcvHXI0`Y`?|Wir+n%{BHhn0soZ;?x5iqJE z_V?lFdbs7#g+t7xCg_|ptwDU6zi~ECs4g|0VQ~`k)X9^TtZ4`V^K{sga^+SO%<)!tc=r1IRqu1jX*!>VUb;}nV4#0&Y zC6LH&dJ7A9;rN_3D^{}*e}F51nB|pJj~@L*ZicZ>$YGkDfuDQW7(Cl@5uR(ahKSZI6odZ_!7^RUay2Y57$+3vkxg}&-7)z9iFJRm+tQ)H_d z{ArXZ5O4*-9r&r@zy^18*A65i@QpT$B7MW+Hx2x{)xG#pd%pz_94EiY@5BaxtXMt* z)^ipxS{nZvEsV*vJk(KcIkamF()JPKMIXiU6pG~Oru@;P1}ZSqn`Vw@UMb8JjE?R` zPBbIs)UEXO?Pbc19rs=wC6FW{gBl4NJyAcVgg^fB?86+!Ayavw8$rm434@!pW=T@H zJlP4Za0aghG4g-`;qyUTDFBP{as5&#(KMg~y)*f7$df^cY0t$%TU5itViSZSV^U=gjnxjLFJS=+SZxFxGfSSWcuP2 ziCG4(xwqzz-)VO!T`bw)@H2s-41hlrT{Ti@9yu)$u7t?N>JLk_oJalmchBq)`L!d9 z_v5wUFEuOiP)@e zQT(e?XPI#`yj?dPG&k7Ks=crqT+s_Y(ipU!%C=YDnW`vh>y6~W`!l8NFoj^9ITY^J zNR-^umHkD%lFpW; z2?h5iBG)rS{B~9<>|SYxTIwPb^7z{|XtldM2DRI%cPcR((9{}4Vm;I@=S_j~z#U?= zlG!9&K#hy&?ChEsR|1|z*Y4FzEh0s3h(u3C74EFOKguf1D9rh_9-L$%$EnlC#{K%& zO2a(R=Z2>6hMIf!O?*65B;P$}F7ah~xYSmZX=1h@I+T0*@4C55dM_{ zHC(o;;JZk)OVW`GT4{U2gO-LVnr10R{AmE+1D-YNt(3Qh?oPVFaVsq~1 z*2TXC9X9)J)-39vO<^uhq3$&!JnVDbS=ve5%DIE3pvihKWEfJnSHZT;vAliRZQ&tP z@>88_ZR_QAOC16R;zcNNPnhK+dKR<@q0dWgK1$oX9lT1!FeiI~46A?Kx5p;Cd-#fU z^=x|SsoNTMB^XF`1erjwneUDUX=}^4BF{`@r$R}Rh;!Nwrs3K1FcAyRXDS7?u^@3|2!4lyEybV3C7FacOciT#^}x5EPmKekfAs1HUp77wvgNnkM; zQ?2IdaNXn-AX6=BB%HSxK4MLcfxsVJ#d&63rs=c@zn73XTF&8PkGRFtZo;B|3Gm4i&i z-DWSpsrZMuyd}%CVTOG}ax6G8a*bUF<&7UoUWO#-wM7vJW542V1q($?L-n2gRZ4fK z=2_!Rr-k)W)`Oj>7{=7l4XNH_#a7>~s?u5e7pTcrq1;25YoY=6k9fbzlUj;Dj($C! z{))JQexru5Fz@p4xCMkTuN;MXy~WRN{6>0eei0aDw$2?ZMJv}33-$x^EGl5qxIzcV zLAfM}Q#jVQO6$lQ@ckjy#u~*tcU=R+4?myT3|M&6{@SLj0_7^k5|QEJG*7;`E*vD@ zW%r&Bw8+P~F|Vp+|J4NdC5m++`g53rT||Fk`Qqr8bqbNBSJOn7xc0j>i|$@NxCpQ9 zTC3-EgzgW{U#_T5GS_{5CUWx&|4bjNCZGJqR+wMpUGRL_D}-4SiQQs`_hCx{k6a9K zoYmQmW&_x9C3FwUV(YKiS@vpbJm?pUTo2)4oe~m0JYBv>M8c|ok-891+0(pp=fm+el$)WTQ?CnY+-N#-73@CM z8~2^7p?O>(qIb8YEUH2(?E8}igRBh^Y1?GsQF|}8Zl8Q(*xIs;oKBe;VUR1@s}!|d z96cl4aL)Q$r&RY~8P%aLFOk9ti#f=$`@6_qVVQ!n8h zz6+JBRLoz~Z$w_=2weLc{r5w>T18w?#o3C6fm%UFSi6Q1R(Wdo`*X_=M(L52T3M==u%$x zl)d2TOXOJ{>5couMXvfZ1K(G5fsQ$dK0B*V6D)OS=3p#YKBHkv)vwqX&ctCyu5F^2 z?Rk9kYkqL{DDcQM@s$Axtq2 zZ{(WfPG?oBe+jY@xu_T~0{4}L<2?=z(01J{!3p+kxXQT_i_Dl0;gk5wOVRNxHI^QQ&1wG_>05t&?U5@-g;^>V*y5^FaFxPuk3gQ94&|0^b9v2pn~c5v}Rho zD>3>I34;v_y?1uNMp@yor{bf0`NhG6HqHg_h?j_w?W}i5)ny zj^1WD_uIW1!tSc9>Jsj`UX9nHD;F$0bUt6IvRJ5p>M7xV0hc%u9L5*R((Y>C?de}3 zbiE8@L45txgJIXS&`{b|!14BBMombzHx-t3IqX(GkIN)feYZ~?hYOmF)0fipM~xqT z!7kAGEP7RIH|TO7V@AnGxILc(A!HCrwsXVpM7ykxA&z$a=X28Z1CR6M3+9GUe<|K5rPl0Blb(QovxL z@NE#shGId(^9itb%7s_F4mzWVKefB7_xq3HS2d`T*KApz8Ho{M?D~ma6Jff+M$$Qj z7QxunIRUY{j#Z7xU+04L5yIbc^ZyyRFCpNAxvRAxoc-rvDc0h@rk#{WQfeNS0 zJZg)vJSLKcR4*k2hE7xAT6)UfHbLWti5}87K5w1uX^lcmm?R-6|1ZtSZ*e*^Y(|tWG?&9EO;2ACUaL&L8?k= zzlj~HQ~BE8&!Kx`U?V>vg%$Nd9cqgc(UxfWYOQOh7t3%|=-}iCcGZ0~HMeQVQs`w2 zld4S`3FB`XS$VB9N^+@|;8hflNts`qwl@sWa{AndH*U<2Mdh=`n^RIV`}OEr0g zDAq*Rnuf`yCsjOPN{OH`Yx8|L(;f=H_0ng2tgMm#Y*{00y{!XFoW@=naeMbev*i!5 z@=N!iS#$k|&^F(mEjh&FYC^g0cKGt=Oy0{WLGi(R-f_X0Z;@~{EED?14`q=%@(T^m z|4&zE9th?3|8XWR$=1SD5~XmX$i7rWld@|eBZH9LB+OVEYp4j7T^O=N4avR?qC$3J zjA3k9$ChO@7=CBmd%NG?{d1n@IiES_JZF18ulM;J>7dPwmY|_C9&jBtcl{b#%%GJP z(tl`9)AHK)*Kp+>Lf^vv>=F{{O{d>Cs9G%PBE#R+YM51Vc6Lu#$NISlPFMekit-Ot z6-}LSo0ST2!O6x&uReC(wNKRV@)5txbyPYnSTHBX4Lj~;m|X>xZGCds{CH}# zA^mB1a&ovrsZIhk^%npgGd5qhOWzoJT=dX7m`U+Ac}8jCtGpY&-I~*QQYBa%tBGH!Yt&}*X~Qw$6`~`W?wL+ zXp3#P%)v_C4(EIaZk_^TAL>$bcycqM%9uL*A+qiD`+b4N-TP)&!%NQ6oj{jD;a5 zH3GPG>yz0Po zCA>cUwx*45&&l_gpTVau_RF4KDq6pMLaG0N05`5OF4SJ|t3DEs z!@$-eb2oI-P)|Qh;Ecj2$Y^(|Ozu#)nU+?+*VdZ~gd!JL+rZK4R}Ru2V47Z=s*PTB ztS7?D;w9^}t5)(p8n<3`^rCG~<@<)u-h_sP@Fri6{?qBBmlyx=+Ms(&jrI7lRC{W! z7R1J@smd>-2H_TjBA6L=QQG`^arhIz=B`iy{I$ZXS+A{jR*@+vho_aDJ+8rxV|=@x zv1dDjYhEMs>rzAndEQG9(qc$bd0uK92p4x&58a8D)?X>5l#muwkhKtLtse7k{t?$( zp9A4%LIkmE@~N`81_SQ!8kq}@LO~WxC9tfP25%($!IISWp7rc;^D@_j9(o8+4 zbXSV#&Xqf_yTp}x18mebw^;8j4Ip`M9Ij?J%4ygHdU2Rq%$pu+1`6nl z#paZH3TI7T)*S!QJh|RBgH`@nWD>97mPR^BmwKtBWM{BZ6mT@RkOMq{O1oaU`M0b% zy{aH8Vq^XqdJCXK@Wxk<^nZeE5^z5|$Z^s7n1tW&r zC7v)y|JZLY0O%;B@NZ4+{qc7@cVN{3;U`fU~#sN^^poU9cD0z;EhkaS>AigECD$7 zlf;T=m2a7)yyD~P7}9NV{uNmOI_%Wyh&kj&6Vnd5>p~~U3c){P{aje;+FF= zP0&Sl^Xbwo>wyHBMS}u^V(uRW!`lrospB&bm$)B(oXN99nhEAx=afXP+-&gEvnTd_ zsPKQ4M}B)$Y)yy~)q9M)^HcbKbFI-21xTT_sMev$$iRX5lY@|a`ocL^li^y;baSy^ zzba|UJG72Tn9nq?uMeA+;5(H&s~M?pTzAN6e4b*O{wECXiD9ATvj)>Ul-_}=UPHxs42SX`w|KEVoObRB9WShSr^T$Y8l!_{=x8@?G&@#X}9H?}iuJ zJ=A*4kfiONPXx|C>@SA#&{64k?Y`bDZRjOdN*M3VKhJ%Sp*Kv_i?qQ;14p%P0yVCu zryyR$T4|NCJ!^mcV(Yf!llFo~UM=JsJ$KzRPWNl1IbOtP{i5H$#f=|-gLr>La^Q(f z^P}dOpUWQZ;XiAG!@Z90`MZ6sxhtg)A6_I48M>c0Z5SYZ9(iL0a)Uq!lMgRA`F0N8p11?$AE- z*7%i+y3u{UsJ%$lHXX&3;&_b*~=I4FSC3 zvsN8K-K+RW<%g&v_0rr7bn`)KPIGi$CP(MFf;dU_D(B!z_CsOnciRgJ0jN*f&$W@z zrNr%IWWGQ-irJhT`(^0G1-J!P7^29TBx<1x7saH@O!gS>_jxCC|4|eDWFv3ndSf#gaCQf1ryH6_MzPBjx#8qz zUy00AtyedyBCa?Y)pCFgLI^kxo2`!oGuR-bd zcA~1g0p&?J(Vk=j_NkRHq(ydtW{5=B1>Fic{&BT1G&$9xVI{HMFA#iuy>!y=|Kcwq zMinYEg^u8nt{pg$(+)>$&y7`)A05dQ=2Z+-ir8`1yLS*9tR_1vIFx7HN(*yF$1s%A z!ugO->1^Z6EB89jc=>U)PcbCz(RzR5CJLJuU|!W`ml|gTt||!LnUes->1ti+((Kte zAtl9Bs0L^-_aA#Pg4<&)A#SR*U(cvhQbO}FR(WW<3^Z~e$FlqVF2#MN9#+-(?O(%0ER21x|sfwOj(7(B@?usp@Wnh$Ikqil70E%Lv?cl7H4SEfXJ+UZROKYl`%UtuhM z7;=|rCu&5=697?s#E8m_Vw~b_Wbnjdy7l+E!52&N?sggs8V;<=wxzJGm3|{GWdXnC z_*`=h1L;`>DgkH-#q!~HeX{@_f^*;~YsTvhhZeGP8wx_Pnkg^Jkk+)?f4gnS-fr?83 z;Pu8X`2ImS1o{vffM%*?vS02Y|KL(NacB)fWKFx&ZXfC zrwEVo3J)_H)LZq}0LF9eN!kp9i4JX0PMmbW5r{9FuGe5&ruI`>wfGB~)v^|T(UDL-GpsvmIC>IlHj z)rD_Dh+sIu1oZJN4lP2jrzzTbr;S@r-buc%*9Z7J8;+lt|$R1PEy+%m6G}OZa8yU!R#9lC@d{(#7^2>mSilsC-^6bm;SAoC$fMtCa4{a&-C~L`SMc_{0~l{fv^G ziguYIyyxh2bZL|t^4{cFUZS3{SoMnlKMGH9AoP!KpkhqLQ*$kw*k@wi9N-1-e3&% z@K}4^5&-qA``Ni~$Gc#bIhJc0D1USmjN0x7)MDjpqJlQcW=~%VGKr$J8M|JpU`9(y zP8+ptu*YaH;~Ok@(0)|cml;U4gN?Th<7z*X?#5qbYc^XnzBu zEf#w^*cAs~Pv{fD&zHqTvh~HbMCD3>j=q*n6Jm@7{EU9?4OZg}Wd->p9`hc>)~(+oYOEx2y%^D11)=XZ%i>zYwhi%8a{g1f7jdf*9GK9z~_Esh@fBgtQS~AS3qsB zdqD;WG~#IOw`Da&pC0HjQ-gC(7D6e9AfUrObKDJMc7R2Dj8^iQ)t>$tfJdEF>B~Mb z6)zFtD9SWrfpw1pC(G?<0%3>5(ssg^Wgz{c4au*bS#lLsQpA zfHn`}5)C5x2s`b{=x(vt`=0W<=vh&%=u`N}cKhu>2AZE#d;PXU&jVGWJK;=sr^h-& zOo!rVg%@A_+JX2}CGRVHPlyhZ<6g^&$(H56rGG@z3;Y@C-)SYhiXA|XCj1qW^>YiJ z2PwWt<);@SE+v*Ypf;&`COp^>emC#JJ|JWhr+ zElLo4FgUbRfOB`u6gWueRj z=~~WSqN`J`$(!mqN5AvkHE5DadMs^+R22^u7?R+A&}nN?j}y1W0xuz)omabcZ? z&Y1O)-+_~J{>e5PA|;`&_O1V2rNB_#@2r5l9XAQ6NvkQD$NxXm4{9N+&m-`-|C;t; zopzrg5&mO(NXYSWAx~nqvb1WNwD8}!)gX16TNlnR~Dl%+als(SA<$% zS}LE<%lytp;8RHB!PG|EaVusYROPuWl>Qv*f8Li!phTjl31l_CP2w^4bvM|s=F98vVU<*>IN3iO$z+05G4 zuMn&>^^QgW-=+YERHiYv@1Cjud5kwuEYTVnw1V?*lXVElpL{&Mkr7L;{T7cq^z3)p zq;Im!>fC70Y!|%{rLXWk2C_P$cOVmLbUTSRVzb3bkkTvI(lAssigned to a random value if not provided. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` | +| opts.origin | The origin of the request.
Either the dApp host or "metamask" if internal. | `"metamask.github.io"` | +| opts.type | An arbitrary string identifying the type of request. | `"eth_signTypedData"` | +| opts.requestData | Additional fixed data for the request.
Must be a JSON compatible object.| `{ transactionId: '123' }` | +| opts.requestState | Additional mutable data for the request.
Must be a JSON compatible object.
Can be updated using the `ApprovalController.updateRequestState` action. | `{ status: 'pending' }` | +| shouldShowRequest | A boolean indicating whether the popup should be displayed. | `true` | + +#### Example + +``` +await this.messagingSystem.call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + requestData, + }, + true, +); +``` + +### 3. Update Approval Request + +If you wish to provide additional state to the confirmation while it is visible, send an `updateRequestState` message to the `ApprovalController`. + +This requires you to have provided the `id` when creating the approval request, so it can be passed to the update message. + +The available message arguments are: + +| Name | Description | Example Value | +| -- | -- | -- | +| opts.id | The ID of the approval request to update. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` | +| opts.requestState | The updated mutable data for the request.
Must be a JSON compatible object. | `{ status: 'pending' }` | + +#### Example + +``` +await this.messagingSystem.call( + 'ApprovalController:updateRequestState', + { + id, + requestState: { counter }, + }, +); +``` + +## Frontend + +### 1. Create Template File + +The `ConfirmationPage` component is already configured to display any approval requests generated by the `ApprovalController` and the associated `pendingApprovals` state. + +In order to configure how the resulting confirmation is rendered, an **Approval Template** is required. + +Create a new JavaScript file in `ui/pages/confirmation/templates` with the name matching the `type` used in the background approval request. + +### 2. Update Approval Templates + +Add your imported file to the `APPROVAL_TEMPLATES` constant in: +[ui/pages/confirmation/templates/index.js](../ui/pages/confirmation/templates/index.js) + +### 3. Define Values + +Inside the template file, define a `getValues` function that returns an object with the following properties: + +| Name | Description | Example Value | +| -- | -- | -- | +| content | An array of objects defining the components to be rendered in the confirmation.
Processed by the [MetaMaskTemplateRenderer](../ui/components/app/metamask-template-renderer/metamask-template-renderer.js). | See example below. | +| onSubmit | A callback to execute when the user approves the confirmation. | `actions.resolvePendingApproval(...)` | +| onCancel | A callback to execute when the user rejects the confirmation. | `actions.rejectPendingApproval(...)` | +| submitText | Text shown for the accept button. | `t('approveButtonText')` | +| cancelText | Text shown on the reject button. | `t('cancel')` | +| loadingText | Text shown while waiting for the onSubmit callback to complete. | `t('addingCustomNetwork')` | +| networkDisplay | A boolean indicating whether to show the current network at the top of the confirmation. | `true` | + +#### Example + +``` +function getValues(pendingApproval, t, actions, _history) { + return { + content: [ + { + element: 'Typography', + key: 'title', + children: 'Example', + props: { + variant: TypographyVariant.H3, + align: 'center', + fontWeight: 'bold', + boxProps: { + margin: [0, 0, 4], + }, + }, + }, + ... + ], + cancelText: t('cancel'), + submitText: t('approveButtonText'), + loadingText: t('addingCustomNetwork'), + onSubmit: () => + actions.resolvePendingApproval( + pendingApproval.id, + pendingApproval.requestData, + ), + onCancel: () => + actions.rejectPendingApproval( + pendingApproval.id, + ethErrors.provider.userRejectedRequest().serialize(), + ), + networkDisplay: true, + }; +} +``` + +### 4. Define Alerts + +If any alerts are required in the confirmation, define the `getAlerts` function in the template file. + +This needs to return an array of any required alerts, based on the current pending approval. + +Each alert is an object with the following properties: + +| Name | Description | Example Value | +| -- | -- | -- | +| id | A unique string to identify the alert. | `"MISMATCHED_NETWORK_RPC"` | +| severity | The severity of the alert.
Use the constants from the design system. | `SEVERITIES.DANGER` | +| content | The component to be rendered inside the alert.
Uses the same format as the `content` returned from `getValues`.
The component can have nested components via the `children` property. | See example below. | + +#### Example + +``` +function getAlerts(_pendingApproval) { + return [ + { + id: 'EXAMPLE_ALERT', + severity: SEVERITIES.WARNING, + content: { + element: 'span', + children: { + element: 'MetaMaskTranslation', + props: { + translationKey: 'exampleMessage', + }, + }, + }, + }, + ]; +} +``` + +### 5. Export Functions + +Ensure the `getValues` and `getAlerts` functions are exported from the template file. + +#### Example + +``` +const example = { + getAlerts, + getValues, +}; + +export default example; +``` + +## Example Branch + +See [this branch](https://github.com/MetaMask/metamask-extension/compare/develop...example/confirmation) as an example of the full code needed to add a confirmation. + +The confirmation can be tested using the [E2E Test dApp](https://metamask.github.io/test-dapp/) and selecting `Request Permissions`. + +## Glossary + +### ApprovalController + +The [ApprovalController](https://github.com/MetaMask/core/blob/main/packages/approval-controller/src/ApprovalController.ts) is a controller defined in the core repository which is responsible for creating and tracking approvals and confirmations in both the MetaMask extension and MetaMask mobile. + +The `pendingApprovals` state used by the `ApprovalController` is not currently persisted, meaning any confirmations created by it will not persist after restarting the browser for example. + +### ConfirmationPage + +The [ConfirmationPage](../ui/pages/confirmation/confirmation.js) is a React component that aims to provide a generic confirmation window which can be configured using templates, each implementing a consistent interface. + +This avoids the need for additional React components when creating confirmations, as additional templates with less logic, and less duplication, can be created instead. + +## Screenshots + +### Confirmation Window + +[](assets/confirmation.png) From 0fde32c1f5adca908918046c8095b718b35278ad Mon Sep 17 00:00:00 2001 From: PeterYinusa Date: Wed, 29 Mar 2023 16:32:59 +0100 Subject: [PATCH 10/15] Conflict cleanup 10.27.0 master-sync --- shared/constants/network.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 433ee1dba37c..e2d6cf40eebe 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -290,12 +290,6 @@ export const BUILT_IN_NETWORKS = { ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_TESTNET], blockExplorerUrl: 'https://explorer.goerli.linea.build', }, - [NETWORK_TYPES.LINEA_TESTNET]: { - networkId: NETWORK_IDS.LINEA_TESTNET, - chainId: CHAIN_IDS.LINEA_TESTNET, - ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_TESTNET], - blockExplorerUrl: 'https://explorer.goerli.linea.build', - }, [NETWORK_TYPES.MAINNET]: { networkId: NETWORK_IDS.MAINNET, chainId: CHAIN_IDS.MAINNET, From efc34b94203934d7f4ba87048fcc6d5c1f8f65a6 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 29 Mar 2023 11:14:38 -0500 Subject: [PATCH 11/15] UX: Multichain: Address Copy Button (#18153) --- .../address-copy-button.js | 64 +++++++++++++++++++ .../address-copy-button.stories.js | 36 +++++++++++ .../address-copy-button.test.js | 31 +++++++++ .../multichain/address-copy-button/index.js | 1 + .../multichain/address-copy-button/index.scss | 7 ++ ui/components/multichain/index.js | 1 + .../multichain/multichain-components.scss | 1 + ui/components/ui/qr-code/qr-code.js | 44 +++++++------ 8 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 ui/components/multichain/address-copy-button/address-copy-button.js create mode 100644 ui/components/multichain/address-copy-button/address-copy-button.stories.js create mode 100644 ui/components/multichain/address-copy-button/address-copy-button.test.js create mode 100644 ui/components/multichain/address-copy-button/index.js create mode 100644 ui/components/multichain/address-copy-button/index.scss diff --git a/ui/components/multichain/address-copy-button/address-copy-button.js b/ui/components/multichain/address-copy-button/address-copy-button.js new file mode 100644 index 000000000000..f88fabdd8f40 --- /dev/null +++ b/ui/components/multichain/address-copy-button/address-copy-button.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { ICON_NAMES, ButtonBase } from '../../component-library'; +import { + BackgroundColor, + TextVariant, + TextColor, + Size, + BorderRadius, + AlignItems, +} from '../../../helpers/constants/design-system'; +import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; +import { shortenAddress } from '../../../helpers/utils/util'; +import Tooltip from '../../ui/tooltip/tooltip'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const AddressCopyButton = ({ + address, + shorten = false, + wrap = false, +}) => { + const displayAddress = shorten ? shortenAddress(address) : address; + const [copied, handleCopy] = useCopyToClipboard(); + const t = useI18nContext(); + + return ( + + handleCopy(address)} + paddingRight={4} + paddingLeft={4} + size={Size.SM} + variant={TextVariant.bodyXs} + color={TextColor.primaryDefault} + endIconName={copied ? ICON_NAMES.COPY_SUCCESS : ICON_NAMES.COPY} + className={classnames('multichain-address-copy-button', { + 'multichain-address-copy-button__address--wrap': wrap, + })} + borderRadius={BorderRadius.pill} + alignItems={AlignItems.center} + data-testid="address-copy-button-text" + > + {displayAddress} + + + ); +}; + +AddressCopyButton.propTypes = { + /** + * Address to be copied + */ + address: PropTypes.string.isRequired, + /** + * Represents if the address should be shortened + */ + shorten: PropTypes.bool, + /** + * Represents if the element should wrap to multiple lines + */ + wrap: PropTypes.bool, +}; diff --git a/ui/components/multichain/address-copy-button/address-copy-button.stories.js b/ui/components/multichain/address-copy-button/address-copy-button.stories.js new file mode 100644 index 000000000000..869bfec255d7 --- /dev/null +++ b/ui/components/multichain/address-copy-button/address-copy-button.stories.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { AddressCopyButton } from '.'; + +export default { + title: 'Components/Multichain/AddressCopyButton', + component: AddressCopyButton, + argTypes: { + address: { + control: 'text', + }, + shorten: { + control: 'boolean', + }, + wrap: { + control: 'boolean', + }, + }, + args: { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.storyName = 'Default'; + +export const ShortenedStory = (args) => ; +ShortenedStory.storyName = 'Shortened'; +ShortenedStory.args = { shorten: true }; + +export const WrappedStory = (args) => ( +
+ +
+); +WrappedStory.storyName = 'Wrapped'; +WrappedStory.args = { wrap: true }; diff --git a/ui/components/multichain/address-copy-button/address-copy-button.test.js b/ui/components/multichain/address-copy-button/address-copy-button.test.js new file mode 100644 index 000000000000..a885fe45dff9 --- /dev/null +++ b/ui/components/multichain/address-copy-button/address-copy-button.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { AddressCopyButton } from '.'; + +const SAMPLE_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + +describe('AccountListItem', () => { + it('renders the full address by default', () => { + render(); + expect( + document.querySelector('[data-testid="address-copy-button-text"]') + .textContent, + ).toStrictEqual(SAMPLE_ADDRESS); + }); + + it('renders a shortened address when it should', () => { + render(); + expect( + document.querySelector('[data-testid="address-copy-button-text"]') + .textContent, + ).toStrictEqual('0x0dc...e7bc'); + }); + + it('changes icon when clicked', () => { + render(); + fireEvent.click(document.querySelector('button')); + expect(document.querySelector('.mm-icon').style.maskImage).toContain( + 'copy-success.svg', + ); + }); +}); diff --git a/ui/components/multichain/address-copy-button/index.js b/ui/components/multichain/address-copy-button/index.js new file mode 100644 index 000000000000..8b93ba9b9378 --- /dev/null +++ b/ui/components/multichain/address-copy-button/index.js @@ -0,0 +1 @@ +export { AddressCopyButton } from './address-copy-button'; diff --git a/ui/components/multichain/address-copy-button/index.scss b/ui/components/multichain/address-copy-button/index.scss new file mode 100644 index 000000000000..12226fec171a --- /dev/null +++ b/ui/components/multichain/address-copy-button/index.scss @@ -0,0 +1,7 @@ +.multichain-address-copy-button { + &__address--wrap { + word-break: break-word; + min-height: 32px; + height: auto; + } +} diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 29562dc8373a..edf6785b94cc 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -5,3 +5,4 @@ export { DetectedTokensBanner } from './detected-token-banner'; export { GlobalMenu } from './global-menu'; export { MultichainImportTokenLink } from './multichain-import-token-link'; export { MultichainTokenListItem } from './multichain-token-list-item'; +export { AddressCopyButton } from './address-copy-button'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index e8ee40ffa49e..8fe555f0ba21 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -4,6 +4,7 @@ * This will help improve specificity and reduce the chance of * unintended overrides. **/ +@import 'address-copy-button/index'; @import 'account-list-item/index'; @import 'account-list-menu/index'; @import 'multichain-token-list-item/multichain-token-list-item'; diff --git a/ui/components/ui/qr-code/qr-code.js b/ui/components/ui/qr-code/qr-code.js index 758620092839..995424c5f553 100644 --- a/ui/components/ui/qr-code/qr-code.js +++ b/ui/components/ui/qr-code/qr-code.js @@ -8,6 +8,8 @@ import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils import Tooltip from '../tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { Icon, ICON_NAMES, ICON_SIZES } from '../../component-library'; +import { AddressCopyButton } from '../../multichain/address-copy-button'; +import Box from '../box/box'; export default connect(mapStateToProps)(QrCodeView); @@ -56,25 +58,31 @@ function QrCodeView(props) { __html: qrImage.createTableTag(4), }} /> - -
{ - handleCopy(toChecksumHexAddress(data)); - }} + {process.env.MULTICHAIN ? ( + + + + ) : ( + -
{toChecksumHexAddress(data)}
- -
-
+
{ + handleCopy(toChecksumHexAddress(data)); + }} + > +
{toChecksumHexAddress(data)}
+ +
+ + )} ); } From 60528413813ea913a7a4805cbb54ed272b57d31c Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 29 Mar 2023 12:44:04 -0500 Subject: [PATCH 12/15] Fix breaking jest test for MMI (#18365) * Fix breaking jest test for MMI * Bump coverage threshold --------- Co-authored-by: Elliot Winkler Co-authored-by: PeterYinusa --- coverage-targets.js | 6 +++--- .../__snapshots__/compliance-settings.test.js.snap | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coverage-targets.js b/coverage-targets.js index e221e9466c31..6157a706d7be 100644 --- a/coverage-targets.js +++ b/coverage-targets.js @@ -6,10 +6,10 @@ // subset of files to check against these targets. module.exports = { global: { - lines: 65, + lines: 65.5, branches: 53.5, - statements: 64, - functions: 57.4, + statements: 64.75, + functions: 58, }, transforms: { branches: 100, diff --git a/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap b/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap index bddb1ef055ce..946a5a7f59f6 100644 --- a/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap +++ b/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap @@ -44,17 +44,17 @@ exports[`Compliance Settings shows start btn when Compliance its not activated 1 class="box institutional-feature__content box--flex-direction-row" >

DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties.

Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting.

Steps to enable AML/CFT Compliance:

From 058c571fab0d03d5abd7fd1ff980803231d79425 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Wed, 29 Mar 2023 15:25:01 -0300 Subject: [PATCH 13/15] Fix Sign-in With Ethereum (SIWE) metametric, add tests, and clean RPC method middleware event logic (#18008) * rpc middleware: update comment * rpc middleware: use errorCodes const * rpc middleware: only create event props once * rpc middleware: rn properties -> eventProperties * rpc middleware: use TransactionStatus const * rpc middleware: use const for ui_customizations * rpc middleware: no need to push null eventProp - also removes === null check which makes this logic a bit more robust * rpc middleware: rn METRIC..OPTIONS -> METRIC..OPT * clean: add consistency * rpc middleware: refactor let msgParams * lint: rm unused METAMETRIC_KEY * fix test: do not pass ui_customizations: null * rpc middleware test: consolidate tests * rpc middleware: fix siwe event .push returns length of mutated array * rpc middleware test: add siwe test * rpc middleware test: rm redudant set --- .../lib/createRPCMethodTrackingMiddleware.js | 123 +++--------- .../createRPCMethodTrackingMiddleware.test.js | 188 +++++++++++------- shared/constants/metametrics.js | 8 +- 3 files changed, 144 insertions(+), 175 deletions(-) diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index 8a5186d42148..cec5b3308515 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,12 +1,12 @@ import { errorCodes } from 'eth-rpc-errors'; import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; +import { TransactionStatus } from '../../../shared/constants/transaction'; import { SECOND } from '../../../shared/constants/time'; import { detectSIWE } from '../../../shared/modules/siwe'; import { EVENT, EVENT_NAMES, - METAMETRIC_KEY_OPTIONS, - METAMETRIC_KEY, + METAMETRIC_KEY_OPT, } from '../../../shared/constants/metametrics'; /** @@ -41,7 +41,7 @@ const RATE_LIMIT_MAP = { /** * For events with user interaction (approve / reject | cancel) this map will - * return an object with APPROVED, REJECTED and REQUESTED keys that map to the + * return an object with APPROVED, REJECTED, REQUESTED, and FAILED keys that map to the * appropriate event names. */ const EVENT_NAME_MAP = { @@ -142,6 +142,8 @@ export default function createRPCMethodTrackingMiddleware({ // keys for the various events in the flow. const eventType = EVENT_NAME_MAP[method]; + const eventProperties = {}; + // Boolean variable that reduces code duplication and increases legibility const shouldTrackEvent = // Don't track if the request came from our own UI or background @@ -162,27 +164,21 @@ export default function createRPCMethodTrackingMiddleware({ ? eventType.REQUESTED : EVENT_NAMES.PROVIDER_METHOD_CALLED; - const properties = {}; - - let msgParams; - if (event === EVENT_NAMES.SIGNATURE_REQUESTED) { - properties.signature_type = method; + eventProperties.signature_type = method; const data = req?.params?.[0]; const from = req?.params?.[1]; const paramsExamplePassword = req?.params?.[2]; - msgParams = { - ...paramsExamplePassword, - from, - data, - origin, - }; - const msgData = { - msgParams, - status: 'unapproved', + msgParams: { + ...paramsExamplePassword, + from, + data, + origin, + }, + status: TransactionStatus.unapproved, type: req.method, }; @@ -193,25 +189,21 @@ export default function createRPCMethodTrackingMiddleware({ ); if (securityProviderResponse?.flagAsDangerous === 1) { - properties.ui_customizations = ['flagged_as_malicious']; + eventProperties.ui_customizations = [ + METAMETRIC_KEY_OPT.ui_customizations.flaggedAsMalicious, + ]; } else if (securityProviderResponse?.flagAsDangerous === 2) { - properties.ui_customizations = ['flagged_as_safety_unknown']; - } else { - properties.ui_customizations = null; + eventProperties.ui_customizations = [ + METAMETRIC_KEY_OPT.ui_customizations.flaggedAsSafetyUnknown, + ]; } if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const { isSIWEMessage } = detectSIWE({ data }); if (isSIWEMessage) { - properties.ui_customizations === null - ? (properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ]) - : properties.ui_customizations.push( - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ); + eventProperties.ui_customizations = ( + eventProperties.ui_customizations || [] + ).concat(METAMETRIC_KEY_OPT.ui_customizations.SIWE); } } } catch (e) { @@ -220,7 +212,7 @@ export default function createRPCMethodTrackingMiddleware({ ); } } else { - properties.method = method; + eventProperties.method = method; } trackEvent({ @@ -229,7 +221,7 @@ export default function createRPCMethodTrackingMiddleware({ referrer: { url: origin, }, - properties, + properties: eventProperties, }); rateLimitTimeouts[method] = setTimeout(() => { @@ -242,8 +234,6 @@ export default function createRPCMethodTrackingMiddleware({ return callback(); } - const properties = {}; - // The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings const isDisabledEthSignAdvancedSetting = method === MESSAGE_TYPE.ETH_SIGN && @@ -254,79 +244,20 @@ export default function createRPCMethodTrackingMiddleware({ let event; if (isDisabledRPCMethod) { event = eventType.FAILED; - properties.error = res.error; - } else if (res.error?.code === 4001) { + eventProperties.error = res.error; + } else if (res.error?.code === errorCodes.provider.userRejectedRequest) { event = eventType.REJECTED; } else { event = eventType.APPROVED; } - let msgParams; - - if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) { - properties.signature_type = method; - - const data = req?.params?.[0]; - const from = req?.params?.[1]; - const paramsExamplePassword = req?.params?.[2]; - - msgParams = { - ...paramsExamplePassword, - from, - data, - origin, - }; - - const msgData = { - msgParams, - status: 'unapproved', - type: req.method, - }; - - try { - const securityProviderResponse = await securityProviderRequest( - msgData, - req.method, - ); - - if (securityProviderResponse?.flagAsDangerous === 1) { - properties.ui_customizations = ['flagged_as_malicious']; - } else if (securityProviderResponse?.flagAsDangerous === 2) { - properties.ui_customizations = ['flagged_as_safety_unknown']; - } else { - properties.ui_customizations = null; - } - - if (method === MESSAGE_TYPE.PERSONAL_SIGN) { - const { isSIWEMessage } = detectSIWE({ data }); - if (isSIWEMessage) { - properties.ui_customizations === null - ? (properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ]) - : properties.ui_customizations.push( - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] - .SIWE, - ); - } - } - } catch (e) { - console.warn( - `createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`, - ); - } - } else { - properties.method = method; - } - trackEvent({ event, category: EVENT.CATEGORIES.INPAGE_PROVIDER, referrer: { url: origin, }, - properties, + properties: eventProperties, }); return callback(); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index af910279e859..5be404f43fdd 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -1,7 +1,11 @@ import { errorCodes } from 'eth-rpc-errors'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { EVENT_NAMES } from '../../../shared/constants/metametrics'; +import { + EVENT_NAMES, + METAMETRIC_KEY_OPT, +} from '../../../shared/constants/metametrics'; import { SECOND } from '../../../shared/constants/time'; +import { detectSIWE } from '../../../shared/modules/siwe'; import createRPCMethodTrackingMiddleware from './createRPCMethodTrackingMiddleware'; const trackEvent = jest.fn(); @@ -52,6 +56,12 @@ function getNext(timeout = 500) { const waitForSeconds = async (seconds) => await new Promise((resolve) => setTimeout(resolve, SECOND * seconds)); +jest.mock('../../../shared/modules/siwe', () => ({ + detectSIWE: jest.fn().mockImplementation(() => { + return { isSIWEMessage: false }; + }), +})); + describe('createRPCMethodTrackingMiddleware', () => { afterEach(() => { jest.resetAllMocks(); @@ -153,7 +163,7 @@ describe('createRPCMethodTrackingMiddleware', () => { }; const res = { - error: { code: 4001 }, + error: { code: errorCodes.provider.userRejectedRequest }, }; const { next, executeMiddlewareStack } = getNext(); await handler(req, res, next); @@ -230,6 +240,36 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent.mock.calls[1][0].properties.method).toBe('eth_chainId'); }); + it('should track Sign-in With Ethereum (SIWE) message if detected', async () => { + const req = { + method: MESSAGE_TYPE.PERSONAL_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next, executeMiddlewareStack } = getNext(); + + detectSIWE.mockImplementation(() => { + return { isSIWEMessage: true }; + }); + + await handler(req, res, next); + await executeMiddlewareStack(); + + expect(trackEvent).toHaveBeenCalledTimes(2); + + expect(trackEvent.mock.calls[1][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_APPROVED, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + ui_customizations: [METAMETRIC_KEY_OPT.ui_customizations.SIWE], + }, + referrer: { url: 'some.dapp' }, + }); + }); + describe(`when '${MESSAGE_TYPE.ETH_SIGN}' is disabled in advanced settings`, () => { it(`should track ${EVENT_NAMES.SIGNATURE_FAILED} and include error property`, async () => { const mockError = { code: errorCodes.rpc.methodNotFound }; @@ -258,93 +298,89 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); }); - }); - describe('participateInMetaMetrics is set to true with a request flagged as safe', () => { - beforeEach(() => { - metricsState.participateInMetaMetrics = true; - }); + describe('when request is flagged as safe by security provider', () => { + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next } = getNext(); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safe`, async () => { - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; + await handler(req, res, next); - const res = { - error: null, - }; - const { next } = getNext(); - await handler(req, res, next); - expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - ui_customizations: null, - }, - referrer: { url: 'some.dapp' }, + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_REQUESTED, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + }, + referrer: { url: 'some.dapp' }, + }); }); }); - }); - describe('participateInMetaMetrics is set to true with a request flagged as malicious', () => { - beforeEach(() => { - metricsState.participateInMetaMetrics = true; - flagAsDangerous = 1; - }); + describe('when request is flagged as malicious by security provider', () => { + beforeEach(() => { + flagAsDangerous = 1; + }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => { - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next } = getNext(); - const res = { - error: null, - }; - const { next } = getNext(); - await handler(req, res, next); - expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - ui_customizations: ['flagged_as_malicious'], - }, - referrer: { url: 'some.dapp' }, + await handler(req, res, next); + + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_REQUESTED, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: ['flagged_as_malicious'], + }, + referrer: { url: 'some.dapp' }, + }); }); }); - }); - describe('participateInMetaMetrics is set to true with a request flagged as safety unknown', () => { - beforeEach(() => { - metricsState.participateInMetaMetrics = true; - flagAsDangerous = 2; - }); + describe('when request flagged as safety unknown by security provider', () => { + beforeEach(() => { + flagAsDangerous = 2; + }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => { - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next } = getNext(); - const res = { - error: null, - }; - const { next } = getNext(); - await handler(req, res, next); - expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', - event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - ui_customizations: ['flagged_as_safety_unknown'], - }, - referrer: { url: 'some.dapp' }, + await handler(req, res, next); + + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_REQUESTED, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: ['flagged_as_safety_unknown'], + }, + referrer: { url: 'some.dapp' }, + }); }); }); }); diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index a9691d5a29a2..de3244c42af2 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -462,17 +462,19 @@ export const CONTEXT_PROPS = { }; /** - * These types correspond to the keys in the METAMETRIC_KEY_OPTIONS object + * These types correspond to the keys in the METAMETRIC_KEY_OPT object */ export const METAMETRIC_KEY = { UI_CUSTOMIZATIONS: `ui_customizations`, }; /** - * This object maps a method name to a METAMETRIC_KEY + * This object maps a METAMETRIC_KEY to an object of possible options */ -export const METAMETRIC_KEY_OPTIONS = { +export const METAMETRIC_KEY_OPT = { [METAMETRIC_KEY.UI_CUSTOMIZATIONS]: { + flaggedAsMalicious: 'flagged_as_malicious', + flaggedAsSafetyUnknown: 'flagged_as_safety_unknown', SIWE: 'sign_in_with_ethereum', }, }; From 27f3af80bdc71c141e92aa48787326ae59b71f68 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Wed, 29 Mar 2023 20:11:08 +0100 Subject: [PATCH 14/15] Revert "Fix(18190): add tabs to permission when initializing app (#18218)" (#18336) This reverts commit 1eb102fd964952d5d2b9a53070b0c868cbfe59bd. --- app/manifest/v2/_base.json | 14 ++++++++++++++ app/manifest/v2/chrome.json | 16 +--------------- app/manifest/v2/firefox.json | 17 +---------------- app/manifest/v3/_base.json | 10 ++++++++++ app/manifest/v3/chrome.json | 16 +--------------- app/manifest/v3/firefox.json | 17 +---------------- 6 files changed, 28 insertions(+), 62 deletions(-) diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json index 39b289197617..f962de618d51 100644 --- a/app/manifest/v2/_base.json +++ b/app/manifest/v2/_base.json @@ -60,5 +60,19 @@ }, "manifest_version": 2, "name": "__MSG_appName__", + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ], "short_name": "__MSG_appName__" } diff --git a/app/manifest/v2/chrome.json b/app/manifest/v2/chrome.json index 9c0e95ec5d6f..a152130d89b2 100644 --- a/app/manifest/v2/chrome.json +++ b/app/manifest/v2/chrome.json @@ -4,19 +4,5 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "80", - "permissions": [ - "storage", - "unlimitedStorage", - "clipboardWrite", - "http://localhost:8545/", - "https://*.infura.io/", - "https://*.codefi.network/", - "https://chainid.network/chains.json", - "https://lattice.gridplus.io/*", - "activeTab", - "webRequest", - "*://*.eth/", - "notifications" - ] + "minimum_chrome_version": "80" } diff --git a/app/manifest/v2/firefox.json b/app/manifest/v2/firefox.json index 5adf0471356d..d50b26a27ff8 100644 --- a/app/manifest/v2/firefox.json +++ b/app/manifest/v2/firefox.json @@ -4,20 +4,5 @@ "id": "webextension@metamask.io", "strict_min_version": "78.0" } - }, - "permissions": [ - "storage", - "unlimitedStorage", - "clipboardWrite", - "http://localhost:8545/", - "https://*.infura.io/", - "https://*.codefi.network/", - "https://chainid.network/chains.json", - "https://lattice.gridplus.io/*", - "activeTab", - "tabs", - "webRequest", - "*://*.eth/", - "notifications" - ] + } } diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json index 3beeb73790d4..1b9456fd8d93 100644 --- a/app/manifest/v3/_base.json +++ b/app/manifest/v3/_base.json @@ -65,5 +65,15 @@ }, "manifest_version": 3, "name": "__MSG_appName__", + "permissions": [ + "activeTab", + "alarms", + "clipboardWrite", + "notifications", + "scripting", + "storage", + "unlimitedStorage", + "webRequest" + ], "short_name": "__MSG_appName__" } diff --git a/app/manifest/v3/chrome.json b/app/manifest/v3/chrome.json index dbb0ee22cca8..486692539eb4 100644 --- a/app/manifest/v3/chrome.json +++ b/app/manifest/v3/chrome.json @@ -6,19 +6,5 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "80", - "permissions": [ - "storage", - "unlimitedStorage", - "clipboardWrite", - "http://localhost:8545/", - "https://*.infura.io/", - "https://*.codefi.network/", - "https://chainid.network/chains.json", - "https://lattice.gridplus.io/*", - "activeTab", - "webRequest", - "*://*.eth/", - "notifications" - ] + "minimum_chrome_version": "80" } diff --git a/app/manifest/v3/firefox.json b/app/manifest/v3/firefox.json index 67ecf7b09891..5f0e5672fdbe 100644 --- a/app/manifest/v3/firefox.json +++ b/app/manifest/v3/firefox.json @@ -22,20 +22,5 @@ "default_title": "MetaMask", "default_popup": "popup.html" }, - "manifest_version": 2, - "permissions": [ - "storage", - "unlimitedStorage", - "clipboardWrite", - "http://localhost:8545/", - "https://*.infura.io/", - "https://*.codefi.network/", - "https://chainid.network/chains.json", - "https://lattice.gridplus.io/*", - "tabs", - "activeTab", - "webRequest", - "*://*.eth/", - "notifications" - ] + "manifest_version": 2 } From c2b2f2685e71a4b1289216324c39535532caa436 Mon Sep 17 00:00:00 2001 From: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Date: Wed, 29 Mar 2023 15:17:57 -0400 Subject: [PATCH 15/15] [FLASK] Redesign key management modal (#18263) * added the rest of the sev1 warnings to getSnapInstallWarnings * added and updated translations * Updated getSnapInstallWarnings to include warnings for all weight-1 permissions * Updated snap install warning and css according to designs * fix snaps tests * fixed alignment/spacing * updated e2e tests to click through the newly displayed public key access warning * lint fix * fixed update snap test * refactored getSnapInstallWarnings, moved logic to PERMISSION_DESCRIPTIONS * fix logic to account for objects & arrays * update function description * add missing properties to ethereum provider description * moved id and message to baseDescription to fix error * add optional chaining to fix undefined error * more optional chaining * more optional chaining --- app/_locales/en/messages.json | 10 +- test/e2e/snaps/test-snap-bip-32.spec.js | 5 +- test/e2e/snaps/test-snap-rpc.spec.js | 5 +- test/e2e/snaps/test-snap-update.spec.js | 5 +- .../connected-accounts-permissions.js | 6 +- .../app/flask/snap-install-warning/index.scss | 4 + .../snap-install-warning.js | 33 ++-- ui/helpers/utils/permission.js | 147 ++++++++++++++---- ui/helpers/utils/util.js | 2 +- ui/pages/permissions-connect/flask/util.js | 96 ++++-------- 10 files changed, 197 insertions(+), 116 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b194112a5527..17a25f726c74 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1365,6 +1365,10 @@ "ethGasPriceFetchWarning": { "message": "Backup gas price is provided as the main gas estimation service is unavailable right now." }, + "ethereumProviderAccess": { + "message": "Grant Ethereum provider access to $1", + "description": "The parameter is the name of the requesting origin" + }, "ethereumPublicAddress": { "message": "Ethereum public address" }, @@ -3572,7 +3576,11 @@ "message": "Proceed with caution" }, "snapInstallWarningKeyAccess": { - "message": "Grant $2 key access to $1", + "message": "Grant $2 account control to $1", + "description": "The first parameter is the name of the snap and the second one is the protocol" + }, + "snapInstallWarningPublicKeyAccess": { + "message": "Grant $2 public key access to $1", "description": "The first parameter is the name of the snap and the second one is the protocol" }, "snapResultError": { diff --git a/test/e2e/snaps/test-snap-bip-32.spec.js b/test/e2e/snaps/test-snap-bip-32.spec.js index 2a5174498b1d..2babba866499 100644 --- a/test/e2e/snaps/test-snap-bip-32.spec.js +++ b/test/e2e/snaps/test-snap-bip-32.spec.js @@ -63,7 +63,10 @@ describe('Test Snap bip-32', function () { // wait for permissions popover, click checkboxes and confirm await driver.delay(1000); await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); - await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0'); + await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1'); + await driver.clickElement( + '#public-key-access-bip32-m-44h-0h-secp256k1-0', + ); await driver.clickElement({ text: 'Confirm', tag: 'button', diff --git a/test/e2e/snaps/test-snap-rpc.spec.js b/test/e2e/snaps/test-snap-rpc.spec.js index 0f512c6fedc1..3c596ba08a98 100644 --- a/test/e2e/snaps/test-snap-rpc.spec.js +++ b/test/e2e/snaps/test-snap-rpc.spec.js @@ -64,7 +64,10 @@ describe('Test Snap RPC', function () { // wait for permissions popover, click checkboxes and confirm await driver.delay(1000); await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); - await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0'); + await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1'); + await driver.clickElement( + '#public-key-access-bip32-m-44h-0h-secp256k1-0', + ); await driver.clickElement({ text: 'Confirm', tag: 'button', diff --git a/test/e2e/snaps/test-snap-update.spec.js b/test/e2e/snaps/test-snap-update.spec.js index 523356bac7a5..eaf0b847c8f5 100644 --- a/test/e2e/snaps/test-snap-update.spec.js +++ b/test/e2e/snaps/test-snap-update.spec.js @@ -64,7 +64,10 @@ describe('Test Snap update', function () { // wait for permissions popover, click checkboxes and confirm await driver.delay(1000); await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0'); - await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0'); + await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1'); + await driver.clickElement( + '#public-key-access-bip32-m-44h-0h-secp256k1-0', + ); await driver.clickElement({ text: 'Confirm', tag: 'button', diff --git a/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js b/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js index 641dc6e70ff8..10f032424c86 100644 --- a/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js +++ b/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js @@ -20,7 +20,11 @@ const ConnectedAccountsPermissions = ({ permissions }) => { const permissionLabels = flatten( permissions.map(({ key, value }) => - getPermissionDescription(t, key, value), + getPermissionDescription({ + t, + permissionName: key, + permissionValue: value, + }), ), ); diff --git a/ui/components/app/flask/snap-install-warning/index.scss b/ui/components/app/flask/snap-install-warning/index.scss index bc7d078bdb89..ddbf9c8d4a5b 100644 --- a/ui/components/app/flask/snap-install-warning/index.scss +++ b/ui/components/app/flask/snap-install-warning/index.scss @@ -1,4 +1,8 @@ .snap-install-warning { + .popover-header { + padding-bottom: 0; + } + .checkbox-label { @include H7; diff --git a/ui/components/app/flask/snap-install-warning/snap-install-warning.js b/ui/components/app/flask/snap-install-warning/snap-install-warning.js index 70cedf0d2245..15c593c74a9c 100644 --- a/ui/components/app/flask/snap-install-warning/snap-install-warning.js +++ b/ui/components/app/flask/snap-install-warning/snap-install-warning.js @@ -6,12 +6,17 @@ import { useI18nContext } from '../../../../hooks/useI18nContext'; import CheckBox from '../../../ui/check-box/check-box.component'; import { + BackgroundColor, + IconColor, TextVariant, TEXT_ALIGN, + Size, + JustifyContent, } from '../../../../helpers/constants/design-system'; import Popover from '../../../ui/popover'; import Button from '../../../ui/button'; -import { Text } from '../../../component-library'; +import { AvatarIcon, ICON_NAMES, Text } from '../../../component-library'; +import Box from '../../../ui/box/box'; /** * a very simple reducer using produce from Immer to keep checkboxes state manipulation @@ -45,13 +50,6 @@ export default function SnapInstallWarning({ onCancel, onSubmit, warnings }) { const SnapInstallWarningFooter = () => { return (
-