1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Merge remote-tracking branch 'upstream/develop' into minimal

This commit is contained in:
Matthias Kretschmann 2023-04-01 16:05:44 +01:00
commit 83bb0fda23
Signed by: m
GPG Key ID: 606EEEF3C479A91F
371 changed files with 13325 additions and 4484 deletions

View File

@ -81,6 +81,7 @@ module.exports = {
files: [
'app/**/*.js',
'shared/**/*.js',
'shared/**/*.ts',
'ui/**/*.js',
'**/*.test.js',
'test/lib/**/*.js',
@ -272,6 +273,7 @@ module.exports = {
'app/scripts/platforms/*.test.js',
'development/**/*.test.js',
'shared/**/*.test.js',
'shared/**/*.test.ts',
'test/helpers/*.js',
'test/jest/*.js',
'ui/**/*.test.js',

View File

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [10.27.0]
### Added
- 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

View File

@ -172,6 +172,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

View File

@ -663,12 +663,6 @@
"settings": {
"message": "ቅንብሮች"
},
"showAdvancedGasInline": {
"message": "የላቁ የነዳጅ ቁጥጥሮች"
},
"showAdvancedGasInlineDescription": {
"message": "በላክ እና አረጋግጥ ማያዎች ላይ የነዳጅ ዋጋን ለማሳየትና ቁጥጥሮችን ለመገደብ ይህን ይምረጡ።"
},
"showFiatConversionInTestnets": {
"message": "ልወጣን በ Testnets ላይ አሳይ"
},

View File

@ -675,12 +675,6 @@
"settings": {
"message": "الإعدادات"
},
"showAdvancedGasInline": {
"message": "أدوات التحكم المتقدمة للغاز"
},
"showAdvancedGasInlineDescription": {
"message": "حدد هذا لإظهار سعر عملة جاس والحد من الضوابط مباشرة على شاشات الإرسال والتأكيد."
},
"showFiatConversionInTestnets": {
"message": "عرض التحويل على Testnets"
},

View File

@ -674,12 +674,6 @@
"settings": {
"message": "Настройки"
},
"showAdvancedGasInline": {
"message": "Разширено управление на газа"
},
"showAdvancedGasInlineDescription": {
"message": "Изберете това, за да покажете цените на газа и ограничите контрола директно на екраните за изпращане и потвърждение."
},
"showFiatConversionInTestnets": {
"message": "Показване на преобразуването на Testnets"
},

View File

@ -672,12 +672,6 @@
"settings": {
"message": "সেটিংস"
},
"showAdvancedGasInline": {
"message": "উন্নত গ্যাস নিয়ন্ত্রণসমূহ"
},
"showAdvancedGasInlineDescription": {
"message": "গ্যাসের মূল্য দেখাতে এটি নির্বাচন করুন এবং পাঠানোর এবং নিশ্চিতকরণের স্ক্রিনগুলিতে নিয়ন্ত্রণগুলি সরাসরি সীমিত করুন।"
},
"showFiatConversionInTestnets": {
"message": "Testnets এ রূপান্তর দেখান"
},

View File

@ -656,12 +656,6 @@
"settings": {
"message": "Configuració"
},
"showAdvancedGasInline": {
"message": "Controls de gas avançats"
},
"showAdvancedGasInlineDescription": {
"message": "Selecciona això per a mostrar el preu del gas i els controls de límit directament a les pantalles d'enviament i confirmació."
},
"showFiatConversionInTestnets": {
"message": "Mostra Conversió a Testnets"
},

View File

@ -656,12 +656,6 @@
"settings": {
"message": "Indstillinger "
},
"showAdvancedGasInline": {
"message": "Avanceret Gas-styring"
},
"showAdvancedGasInlineDescription": {
"message": "Vælg dette for at vise brændstofprisen og begræns styringen direkte på afsendelses- og bekræftelseskærmene."
},
"showFiatConversionInTestnets": {
"message": "Vis konvertering på testnet"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Zeigen"
},
"showAdvancedGasInline": {
"message": "Erweiterte Gaskontrollen"
},
"showAdvancedGasInlineDescription": {
"message": "Wählen Sie dies aus, um den Gaspreis und die Limitkontrollen direkt auf den Senden- und Bestätigen-Bildschirmen anzuzeigen."
},
"showFiatConversionInTestnets": {
"message": "Umwandlung auf Testnets anzeigen"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Dies basiert auf Informationen von "
},
"thisServiceIsExperimental": {
"message": "Dieser Dienst ist experimentell"
},
"time": {
"message": "Zeit"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Εμφάνιση"
},
"showAdvancedGasInline": {
"message": "Προωθημένος έλεγχος gas"
},
"showAdvancedGasInlineDescription": {
"message": "Επιλέξτε αυτό για να εμφανίσετε τις τιμές αερίου και να περιορίσετε τα στοιχεία ελέγχου απευθείας στις οθόνες αποστολής και επιβεβαίωσης."
},
"showFiatConversionInTestnets": {
"message": "Εμφάνιση Μετατροπής σε Δοκιμαστικά Δίκτυα"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Αυτό βασίζεται σε πληροφορίες από"
},
"thisServiceIsExperimental": {
"message": "Η υπηρεσία αυτή είναι πειραματική"
},
"time": {
"message": "Ώρα"
},

View File

@ -103,6 +103,9 @@
"SIWEWarningTitle": {
"message": "Are you sure?"
},
"ShowMore": {
"message": "Show more"
},
"about": {
"message": "About"
},
@ -150,6 +153,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
"activated": {
"message": "Active"
},
"active": {
"message": "Active"
},
@ -345,6 +351,9 @@
"amount": {
"message": "Amount"
},
"apiUrl": {
"message": "API URL"
},
"appDescription": {
"message": "An Ethereum Wallet in your Browser",
"description": "The description of the application"
@ -639,9 +648,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"
},
@ -875,6 +914,12 @@
"curveMediumGasEstimate": {
"message": "Market gas estimate graph"
},
"custodian": {
"message": "Custodian"
},
"custodianAccount": {
"message": "Custodian account"
},
"custom": {
"message": "Advanced"
},
@ -1332,6 +1377,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"
},
@ -1967,6 +2016,9 @@
"lock": {
"message": "Lock"
},
"lockMetaMask": {
"message": "Lock MetaMask"
},
"lockTimeTooGreat": {
"message": "Lock time is too great"
},
@ -2078,6 +2130,9 @@
"missingToken": {
"message": "Don't see your token?"
},
"mmiAddToken": {
"message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional"
},
"mobileSyncWarning": {
"message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile."
},
@ -2129,6 +2184,9 @@
"networkIsBusy": {
"message": "Network is busy. Gas prices are high and estimates are less accurate."
},
"networkMenuHeading": {
"message": "Select a network"
},
"networkName": {
"message": "Network name"
},
@ -2269,6 +2327,9 @@
"nfts": {
"message": "NFTs"
},
"nftsPreviouslyOwned": {
"message": "Previously Owned"
},
"nickname": {
"message": "Nickname"
},
@ -2673,6 +2734,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."
@ -2949,6 +3013,9 @@
"portfolio": {
"message": "Portfolio"
},
"portfolioView": {
"message": "Portfolio view"
},
"preferredLedgerConnectionType": {
"message": "Preferred Ledger connection type",
"description": "A header for a dropdown in Settings > Advanced. Appears above the ledgerConnectionPreferenceDescription message"
@ -3423,12 +3490,6 @@
"show": {
"message": "Show"
},
"showAdvancedGasInline": {
"message": "Advanced gas controls"
},
"showAdvancedGasInlineDescription": {
"message": "Select this to show gas price and limit controls directly on the send and confirm screens."
},
"showFiatConversionInTestnets": {
"message": "Show conversion on test networks"
},
@ -3536,7 +3597,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": {
@ -3691,6 +3756,9 @@
"statusNotConnected": {
"message": "Not connected"
},
"statusNotConnectedAccount": {
"message": "No accounts connected"
},
"step1LatticeWallet": {
"message": "Connect your Lattice1"
},
@ -4192,6 +4260,9 @@
"termsOfService": {
"message": "Terms of service"
},
"termsOfUse": {
"message": "terms of use"
},
"testNetworks": {
"message": "Test networks"
},
@ -4211,7 +4282,8 @@
"message": "This is based on information from "
},
"thisServiceIsExperimental": {
"message": "This service is experimental"
"message": "This service is experimental. By enabling this feature, you agree to OpenSea's $1.",
"description": "$1 is link to open sea terms of use"
},
"time": {
"message": "Time"
@ -4285,6 +4357,12 @@
"tooltipApproveButton": {
"message": "I understand"
},
"tooltipSatusConnected": {
"message": "connected"
},
"tooltipSatusNotConnected": {
"message": "not connected"
},
"total": {
"message": "Total"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Mostrar"
},
"showAdvancedGasInline": {
"message": "Controles avanzados de gas"
},
"showAdvancedGasInlineDescription": {
"message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación."
},
"showFiatConversionInTestnets": {
"message": "Mostrar conversión en redes de prueba"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Esto se basa en información de "
},
"thisServiceIsExperimental": {
"message": "Este servicio es experimental"
},
"time": {
"message": "Tiempo"
},

View File

@ -2070,12 +2070,6 @@
"show": {
"message": "Mostrar"
},
"showAdvancedGasInline": {
"message": "Controles avanzados de gas"
},
"showAdvancedGasInlineDescription": {
"message": "Seleccione esta opción para mostrar el precio del gas y limitar los controles directamente en las pantallas de envío y confirmación."
},
"showFiatConversionInTestnets": {
"message": "Mostrar conversión en redes de prueba"
},

View File

@ -668,12 +668,6 @@
"settings": {
"message": "Seaded"
},
"showAdvancedGasInline": {
"message": "Täiustatud gaasijuhikud"
},
"showAdvancedGasInlineDescription": {
"message": "Valige see, et kuvada gaasi hinda ja piirangut otse saatmise ning kinnitamise kuval."
},
"showFiatConversionInTestnets": {
"message": "Kuva teisendus Testnetsis"
},

View File

@ -678,12 +678,6 @@
"settings": {
"message": "تنظیمات"
},
"showAdvancedGasInline": {
"message": "کنترول های پیشرفته گاز"
},
"showAdvancedGasInlineDescription": {
"message": "این را انتخاب نمایید تا قیمت گاز را نشان داده و کنترول ها را بصورت مستقیم در صفحات ارسال و تأیید محدود نماید."
},
"showFiatConversionInTestnets": {
"message": "نمایش تغییرات Testnets"
},

View File

@ -675,12 +675,6 @@
"settings": {
"message": "Asetukset"
},
"showAdvancedGasInline": {
"message": "Bensan lisävalvonta"
},
"showAdvancedGasInlineDescription": {
"message": "Valitse tämä näyttääksesi gas-hinta ja rajoittaaksesi säätimiä suoraan lähetä- ja vahvista-ruuduissa."
},
"showFiatConversionInTestnets": {
"message": "Näytä vaihtokurssi koeverkoissa"
},

View File

@ -602,12 +602,6 @@
"settings": {
"message": "Mga Setting"
},
"showAdvancedGasInline": {
"message": "Mga advanced na kontrol sa gas"
},
"showAdvancedGasInlineDescription": {
"message": "Piliin ito para ipakita ang presyo ng gas at limitahan ang mga kontrol nang direkta sa screen ng pagpapadala at pagkumpirma."
},
"showFiatConversionInTestnets": {
"message": "Ipakita ang Conversion sa mga Testnet"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Afficher"
},
"showAdvancedGasInline": {
"message": "Contrôles de carburant avancés"
},
"showAdvancedGasInlineDescription": {
"message": "Sélectionnez cette option pour afficher le prix du carburant et les contrôles des limites directement sur les écrans denvoi et de confirmation."
},
"showFiatConversionInTestnets": {
"message": "Afficher la conversion sur les testnets"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Ces informations proviennent de "
},
"thisServiceIsExperimental": {
"message": "Ce service est expérimental"
},
"time": {
"message": "Temps"
},

View File

@ -675,12 +675,6 @@
"settings": {
"message": "הגדרות"
},
"showAdvancedGasInline": {
"message": "אמצעי שליטה מתקדמים בדלק"
},
"showAdvancedGasInlineDescription": {
"message": "בחר/י באפשרות זו כדי להציג אמצעי שליטה במחיר הדלק וההגבלה (limit) ישירות במסכי השליחה והאישור."
},
"showFiatConversionInTestnets": {
"message": "הצג המרה -Testnets"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "दिखाएं"
},
"showAdvancedGasInline": {
"message": "उन्नत गैस नियंत्रण"
},
"showAdvancedGasInlineDescription": {
"message": "गैस मूल्य और सीमा नियंत्रण को सीधे भेजने और पुष्टि करने की स्क्रीन पर दिखाने के लिए इसका चयन करें।"
},
"showFiatConversionInTestnets": {
"message": "टेस्ट नेटवर्क पर रूपांतरण दिखाएं"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "से प्राप्त जानकारी पर आधारित है"
},
"thisServiceIsExperimental": {
"message": "यह सेवा प्रयोगात्मक है"
},
"time": {
"message": "समय"
},

View File

@ -671,12 +671,6 @@
"settings": {
"message": "Postavke"
},
"showAdvancedGasInline": {
"message": "Napredno upravljanje gorivom"
},
"showAdvancedGasInlineDescription": {
"message": "Odaberite ovu stavku za prikaz cijene goriva i izravno ograničite kontrole prilikom slanja i potvrđivanja zaslona."
},
"showFiatConversionInTestnets": {
"message": "Prikaži konverziju na usluzi Testnets"
},

View File

@ -671,12 +671,6 @@
"settings": {
"message": "Beállítások"
},
"showAdvancedGasInline": {
"message": "Speciális gázszabályzók"
},
"showAdvancedGasInlineDescription": {
"message": "Jelöld meg ezt a gázárak és korlátozásellenőrzés mutatásához közvetlenül a küldési és megerősítési képernyőkön."
},
"showFiatConversionInTestnets": {
"message": "Konverzió mutatása Testnetsen"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Tampil"
},
"showAdvancedGasInline": {
"message": "Kontrol gas lanjutan"
},
"showAdvancedGasInlineDescription": {
"message": "Pilih ini untuk menampilkan biaya gas dan kontrol batas secara langsung di layar kirim dan konfirmasi."
},
"showFiatConversionInTestnets": {
"message": "Tampilkan konversi di jaringan uji"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Hal ini berdasarkan informasi dari "
},
"thisServiceIsExperimental": {
"message": "Layanan ini bersifat eksperimental"
},
"time": {
"message": "Waktu"
},

View File

@ -1470,12 +1470,6 @@
"settings": {
"message": "Impostazioni"
},
"showAdvancedGasInline": {
"message": "Controlli gas avanzati"
},
"showAdvancedGasInlineDescription": {
"message": "Seleziona per visualizzare i controlli su prezzo e limite del gas nelle schermate di invio e conferma."
},
"showFiatConversionInTestnets": {
"message": "Mostra conversione nelle reti di test"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "表示"
},
"showAdvancedGasInline": {
"message": "高度なガスコントロール"
},
"showAdvancedGasInlineDescription": {
"message": "これを選択すると、ガス代と限度額のコントロールが送金画面と確認画面に直接表示されます。"
},
"showFiatConversionInTestnets": {
"message": "テストネット上に変換を表示"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "これは次の情報源からの情報に基づくものです: "
},
"thisServiceIsExperimental": {
"message": "このサービスは実験段階です"
},
"time": {
"message": "時間"
},

View File

@ -678,12 +678,6 @@
"settings": {
"message": "ಸೆಟ್ಟಿಂಗ್‌ಗಳು"
},
"showAdvancedGasInline": {
"message": "ಸುಧಾರಿತ ಗ್ಯಾಸ್ ನಿಯಂತ್ರಣಗಳು"
},
"showAdvancedGasInlineDescription": {
"message": "ಕಳುಹಿಸುವ ಮತ್ತು ಖಚಿತಪಡಿಸುವ ಪರದೆಯ ಮೇಲೆ ನೇರವಾಗಿ ಗ್ಯಾಸ್ ಬೆಲೆ ಮತ್ತು ಮಿತಿಯ ನಿಯಂತ್ರಣಗಳನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ."
},
"showFiatConversionInTestnets": {
"message": "Testnets ನಲ್ಲಿ ಪರಿವರ್ತನೆಯನ್ನು ತೋರಿಸಿ"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "보기"
},
"showAdvancedGasInline": {
"message": "고급 가스 제어 기능"
},
"showAdvancedGasInlineDescription": {
"message": "이 항목을 선택하면 보내기 및 확인 화면에서 바로 가스 가격과 한도 조절을 확인할 수 있습니다."
},
"showFiatConversionInTestnets": {
"message": "테스트넷에 전환 표시"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "다음에 기반한 정보입니다: "
},
"thisServiceIsExperimental": {
"message": "이 서비스는 시험 서비스입니다"
},
"time": {
"message": "시간"
},

View File

@ -678,12 +678,6 @@
"settings": {
"message": "Nustatymai"
},
"showAdvancedGasInline": {
"message": "Išplėstiniai dujų valdikliai"
},
"showAdvancedGasInlineDescription": {
"message": "Pasirinkite tai, kad būtų rodoma dujų kaina, ir ribokite valdymo elementus tiesiogiai siuntimo ir patvirtinimo ekranuose."
},
"showFiatConversionInTestnets": {
"message": "Rodyti keitimą „Testnet“"
},

View File

@ -674,12 +674,6 @@
"settings": {
"message": "Iestatījumi"
},
"showAdvancedGasInline": {
"message": "Papildu Gas vadīklas"
},
"showAdvancedGasInlineDescription": {
"message": "Atlasiet šo, lai parādītu Gas cenu un ierobežotu kontroles iespējas tieši sūtīšanas un apstiprināšanas ekrānos."
},
"showFiatConversionInTestnets": {
"message": "Rādīt konversiju testa tīklos"
},

View File

@ -658,12 +658,6 @@
"settings": {
"message": "Tetapan"
},
"showAdvancedGasInline": {
"message": "Kawalan gas lanjutan"
},
"showAdvancedGasInlineDescription": {
"message": "Pilih ini untuk menunjukkan harga gas dan kawalan had terus di skrin hantar dan sahkan."
},
"showFiatConversionInTestnets": {
"message": "Tunjukkan Penukaran di Testnets"
},

View File

@ -659,12 +659,6 @@
"settings": {
"message": "Innstillinger"
},
"showAdvancedGasInline": {
"message": "Avanserte datakraftskontroller"
},
"showAdvancedGasInlineDescription": {
"message": "Velg dette for å vise bensinpris og begrensningskontroller direkte på send- og bekreftskjermbildene."
},
"showFiatConversionInTestnets": {
"message": "Vis konvertering på Testnets "
},

View File

@ -1320,12 +1320,6 @@
"settings": {
"message": "Mga Setting"
},
"showAdvancedGasInline": {
"message": "Mga advanced na kontrol sa gas"
},
"showAdvancedGasInlineDescription": {
"message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma."
},
"showFiatConversionInTestnets": {
"message": "Ipakita ang Conversion sa Testnets"
},

View File

@ -672,12 +672,6 @@
"settings": {
"message": "Ustawienia"
},
"showAdvancedGasInline": {
"message": "Zaawansowana kontrola gazu"
},
"showAdvancedGasInlineDescription": {
"message": "Wybierz tę opcję, aby móc zmienić cenę i limit gazu bezpośrednio na ekranach wysyłania i potwierdzania."
},
"showFiatConversionInTestnets": {
"message": "Pokaż przeliczanie w sieciach testowych"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Exibir"
},
"showAdvancedGasInline": {
"message": "Controles avançados de gás"
},
"showAdvancedGasInlineDescription": {
"message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação."
},
"showFiatConversionInTestnets": {
"message": "Mostrar conversão nas redes de teste"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Isso se baseia em informações de "
},
"thisServiceIsExperimental": {
"message": "Esse serviço é experimental"
},
"time": {
"message": "Hora"
},

View File

@ -2070,12 +2070,6 @@
"show": {
"message": "Mostrar"
},
"showAdvancedGasInline": {
"message": "Controles avançados de gás"
},
"showAdvancedGasInlineDescription": {
"message": "Selecione isso para mostrar o preço do gás e limitar os controles diretamente nas telas de envio e de confirmação."
},
"showFiatConversionInTestnets": {
"message": "Mostrar conversão nas redes de teste"
},

View File

@ -665,12 +665,6 @@
"settings": {
"message": "Setări"
},
"showAdvancedGasInline": {
"message": "Controale avansate pentru gas"
},
"showAdvancedGasInlineDescription": {
"message": "Selectați aceasta pentru a arăta prețul gasului și comenzile de limitare direct pe ecranele de trimitere și confirmare."
},
"showFiatConversionInTestnets": {
"message": "Afișează conversiile pe rețelele de test (testnets)"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Показать"
},
"showAdvancedGasInline": {
"message": "Расширенное управление газом"
},
"showAdvancedGasInlineDescription": {
"message": "Выберите это, чтобы отображать цену газа и управление лимитами непосредственно на экранах отправки и подтверждения."
},
"showFiatConversionInTestnets": {
"message": "Показывать конвертацию в тестовых сетях"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Это основано на информации от "
},
"thisServiceIsExperimental": {
"message": "Этот сервис является экспериментальным"
},
"time": {
"message": "Время"
},

View File

@ -650,12 +650,6 @@
"settings": {
"message": "Nastavení"
},
"showAdvancedGasInline": {
"message": "Pokročilé ovládacie prvky GAS"
},
"showAdvancedGasInlineDescription": {
"message": "Vyberte túto možnosť vtedy, keď chcete priamo na obrazovke odosielania a potvrdenia zobraziť ceny za GAS a ovládacie prvky limitov."
},
"showFiatConversionInTestnets": {
"message": "Zobraziť konverziu na Testnets"
},

View File

@ -666,12 +666,6 @@
"settings": {
"message": "Nastavitve"
},
"showAdvancedGasInline": {
"message": "Napredno krmiljenje plina"
},
"showAdvancedGasInlineDescription": {
"message": "Izberite to možnost, če želite prikazati ceno plina in omejiti nadzor neposredno na zaslonih za pošiljanje in potrditev."
},
"showFiatConversionInTestnets": {
"message": "Pokažite pretvorbo na testnih omrežjih"
},

View File

@ -669,12 +669,6 @@
"settings": {
"message": "Podešavanja"
},
"showAdvancedGasInline": {
"message": "Napredne kontrole gasa"
},
"showAdvancedGasInlineDescription": {
"message": "Izaberite ovo kako biste prikazali cenu gasa i kontrole limita direktno na ekranima za slanje i potvrđivanje."
},
"showFiatConversionInTestnets": {
"message": "Prikažite konverzije na Testnet"
},

View File

@ -662,12 +662,6 @@
"settings": {
"message": "Inställningar"
},
"showAdvancedGasInline": {
"message": "Avancerade gaskontroller"
},
"showAdvancedGasInlineDescription": {
"message": "Välj detta för att visa gas-pris och gränskontroller direkt på skicka och bekräfta-skärmarna."
},
"showFiatConversionInTestnets": {
"message": "Visa omvandling på testnätverk"
},

View File

@ -656,12 +656,6 @@
"settings": {
"message": "Mipangilio"
},
"showAdvancedGasInline": {
"message": "Udhibiti wa juu wa gesi"
},
"showAdvancedGasInlineDescription": {
"message": "Chagua hii ili uonyeshe bei ya gesi na punguza vidhibiti moja kwa moja kwenye skrini za tuma na thibitisha."
},
"showFiatConversionInTestnets": {
"message": "Onyesha Ubadilishaji kwenye Testnets"
},

View File

@ -344,9 +344,6 @@
"settings": {
"message": "การตั้งค่า"
},
"showAdvancedGasInline": {
"message": "การควบคุม Gas ขั้นสูง"
},
"showPrivateKeys": {
"message": "แสดงคีย์ส่วนตัว"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Ipakita"
},
"showAdvancedGasInline": {
"message": "Mga advanced na kontrol sa gas"
},
"showAdvancedGasInlineDescription": {
"message": "Piliin ito para direktang maipakita ang presyo ng gas at mga kontrol sa limitasyon sa mga screen ng pagpapadala at pagkumpirma."
},
"showFiatConversionInTestnets": {
"message": "Ipakita ang Conversion sa Testnets"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Ito ay batay sa impormasyon mula sa "
},
"thisServiceIsExperimental": {
"message": "Eksperimental ang serbisyong ito"
},
"time": {
"message": "Oras"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Göster"
},
"showAdvancedGasInline": {
"message": "Gelişmiş gaz kontrolleri"
},
"showAdvancedGasInlineDescription": {
"message": "Gaz fiyatı ve limit kontrollerini doğrudan gönder ve onayla ekranlarında göstermek için bunu seçin."
},
"showFiatConversionInTestnets": {
"message": "Test ağlarında dönüşümü göster"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Bu, şu kaynaktan alınan bilgilere dayanır: "
},
"thisServiceIsExperimental": {
"message": "Bu hizmet deneyseldir"
},
"time": {
"message": "Zaman"
},

View File

@ -678,12 +678,6 @@
"settings": {
"message": "Налаштування"
},
"showAdvancedGasInline": {
"message": "Розширене керування газом"
},
"showAdvancedGasInlineDescription": {
"message": "Виберіть цей параметр, щоб відображати регулятори ціни й ліміту газу на екранах надсилання й підтвердження."
},
"showFiatConversionInTestnets": {
"message": "Показати бесіду у Testnet"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "Hiển thị"
},
"showAdvancedGasInline": {
"message": "Quyền kiểm soát gas nâng cao"
},
"showAdvancedGasInlineDescription": {
"message": "Chọn tùy chọn này để hiển thị các quyền kiểm soát giá gas và giới hạn ngay trên màn hình gửi và xác nhận."
},
"showFiatConversionInTestnets": {
"message": "Hiển thị tỷ lệ quy đổi trên các mạng thử nghiệm"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "Điều này dựa trên thông tin từ "
},
"thisServiceIsExperimental": {
"message": "Đây là dịch vụ thử nghiệm"
},
"time": {
"message": "Thời gian"
},

View File

@ -3250,12 +3250,6 @@
"show": {
"message": "显示"
},
"showAdvancedGasInline": {
"message": "高级燃料控制"
},
"showAdvancedGasInlineDescription": {
"message": "选择此项可直接在发送和确认界面显示燃料价格和上限控制。"
},
"showFiatConversionInTestnets": {
"message": "在测试网络上显示转换"
},
@ -4006,9 +4000,6 @@
"thisIsBasedOn": {
"message": "所根据的信息是来自"
},
"thisServiceIsExperimental": {
"message": "此服务是实验性的"
},
"time": {
"message": "时间"
},

View File

@ -1245,12 +1245,6 @@
"settings": {
"message": "設定"
},
"showAdvancedGasInline": {
"message": "顯示進階 gas 控制選項"
},
"showAdvancedGasInlineDescription": {
"message": "選擇此項會在傳送或確認畫面顯示可微調 gas 價格以及 gas 上限的功能"
},
"showFiatConversionInTestnets": {
"message": "在測試網上顯示匯率"
},

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 314 KiB

View File

@ -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__"
}

View File

@ -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"
}

View File

@ -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"
]
}
}

View File

@ -65,5 +65,15 @@
},
"manifest_version": 3,
"name": "__MSG_appName__",
"permissions": [
"activeTab",
"alarms",
"clipboardWrite",
"notifications",
"scripting",
"storage",
"unlimitedStorage",
"webRequest"
],
"short_name": "__MSG_appName__"
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -223,7 +223,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
* @property {object} provider - The current selected network provider.
* @property {string} provider.rpcUrl - The address for the RPC API, if using an RPC API.
* @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks.
* @property {string} network - A stringified number of the current network ID.
* @property {string} networkId - The stringified number of the current network ID.
* @property {string} networkStatus - Either "unknown", "available", "unavailable", or "blocked", depending on the status of the currently selected network.
* @property {object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values.
* @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string.
* @property {TransactionMeta[]} currentNetworkTxList - An array of transactions associated with the currently selected network.
@ -555,6 +556,10 @@ export function setupController(initState, initLangCode, overrides) {
if (message.name === WORKER_KEEP_ALIVE_MESSAGE) {
// To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI.
remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE });
controller.appStateController.setServiceWorkerLastActiveTime(
Date.now(),
);
}
});
}

View File

@ -51,6 +51,7 @@ export default class AppStateController extends EventEmitter {
'0x5': true,
'0x539': true,
},
serviceWorkerLastActiveTime: 0,
});
this.timer = null;
@ -362,4 +363,10 @@ export default class AppStateController extends EventEmitter {
getCurrentPopupId() {
return this.store.getState().currentPopupId;
}
setServiceWorkerLastActiveTime(serviceWorkerLastActiveTime) {
this.store.updateState({
serviceWorkerLastActiveTime,
});
}
}

View File

@ -13,12 +13,12 @@ import { convertHexToDecimal } from '@metamask/controller-utils';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';
import NetworkController, { NETWORK_EVENTS } from './network';
import NetworkController, { NetworkControllerEventTypes } from './network';
import PreferencesController from './preferences';
describe('DetectTokensController', function () {
const sandbox = sinon.createSandbox();
let assetsContractController,
let sandbox,
assetsContractController,
keyringMemStore,
network,
preferences,
@ -32,78 +32,94 @@ describe('DetectTokensController', function () {
getAccounts: noop,
};
const infuraProjectId = 'infura-project-id';
beforeEach(async function () {
keyringMemStore = new ObservableStore({ isUnlocked: false });
network = new NetworkController({ infuraProjectId: 'foo' });
network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
const tokenListMessenger = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
tokenListController = new TokenListController({
chainId: '1',
preventPollingOnNetworkRestart: false,
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessenger,
});
await tokenListController.start();
preferences = new PreferencesController({
network,
provider,
tokenListController,
});
preferences.setAddresses([
'0x7e57e2',
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
]);
preferences.setUseTokenDetection(true);
tokensController = new TokensController({
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: (cb) =>
network.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
sandbox = sinon.createSandbox();
// Disable all requests, even those to localhost
nock.disableNetConnect();
nock('https://mainnet.infura.io')
.post(`/v3/${infuraProjectId}`)
.reply(200, (_uri, requestBody) => {
if (requestBody.method === 'eth_getBlockByNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: {
number: '0x42',
},
};
return cb(modifiedNetworkState);
}),
});
}
assetsContractController = new AssetsContractController({
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: (cb) =>
network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const networkState = network.store.getState();
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: convertHexToDecimal(networkState.provider.chainId),
if (requestBody.method === 'eth_blockNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '0x42',
};
}
throw new Error(`(Infura) Mock not defined for ${requestBody.method}`);
})
.persist();
nock('https://sepolia.infura.io')
.post(`/v3/${infuraProjectId}`)
.reply(200, (_uri, requestBody) => {
if (requestBody.method === 'eth_getBlockByNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: {
number: '0x42',
},
};
return cb(modifiedNetworkState);
}),
});
}
sandbox
.stub(network, '_getLatestBlock')
.callsFake(() => Promise.resolve({}));
sandbox
.stub(tokensController, '_instantiateNewEthersProvider')
.returns(null);
sandbox
.stub(tokensController, '_detectIsERC721')
.returns(Promise.resolve(false));
if (requestBody.method === 'eth_blockNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '0x42',
};
}
throw new Error(`(Infura) Mock not defined for ${requestBody.method}`);
})
.persist();
nock('http://localhost:8545')
.post('/')
.reply(200, (_uri, requestBody) => {
if (requestBody.method === 'eth_getBlockByNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: {
number: '0x42',
},
};
}
if (requestBody.method === 'eth_blockNumber') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '0x42',
};
}
if (requestBody.method === 'net_version') {
return {
id: requestBody.id,
jsonrpc: '2.0',
result: '1337',
};
}
throw new Error(
`(localhost) Mock not defined for ${requestBody.method}`,
);
})
.persist();
nock('https://token-api.metaswap.codefi.network')
.get(`/tokens/1`)
.reply(200, [
@ -174,9 +190,82 @@ describe('DetectTokensController', function () {
.get(`/tokens/3`)
.reply(200, { error: 'ChainId 3 is not supported' })
.persist();
keyringMemStore = new ObservableStore({ isUnlocked: false });
const networkControllerMessenger = new ControllerMessenger();
network = new NetworkController({
messenger: networkControllerMessenger,
infuraProjectId,
});
await network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
const tokenListMessenger = new ControllerMessenger().getRestricted({
name: 'TokenListController',
});
tokenListController = new TokenListController({
chainId: '1',
preventPollingOnNetworkRestart: false,
onNetworkStateChange: sinon.spy(),
onPreferencesStateChange: sinon.spy(),
messenger: tokenListMessenger,
});
await tokenListController.start();
preferences = new PreferencesController({
network,
provider,
tokenListController,
onInfuraIsBlocked: sinon.stub(),
onInfuraIsUnblocked: sinon.stub(),
});
preferences.setAddresses([
'0x7e57e2',
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
]);
preferences.setUseTokenDetection(true);
tokensController = new TokensController({
config: { provider },
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: (cb) =>
network.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
},
};
return cb(modifiedNetworkState);
}),
});
assetsContractController = new AssetsContractController({
onPreferencesStateChange: preferences.store.subscribe.bind(
preferences.store,
),
onNetworkStateChange: (cb) =>
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
() => {
const networkState = network.store.getState();
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: convertHexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
},
),
});
});
after(function () {
afterEach(function () {
nock.enableNetConnect('localhost');
sandbox.restore();
});

View File

@ -721,6 +721,9 @@ export default class MetaMetricsController {
///: BEGIN:ONLY_INCLUDE_IN(flask)
[TRAITS.DESKTOP_ENABLED]: metamaskState.desktopEnabled || false,
///: END:ONLY_INCLUDE_IN
[TRAITS.SECURITY_PROVIDERS]: metamaskState.transactionSecurityCheckEnabled
? ['opensea']
: [],
};
if (!previousUserTraits) {

View File

@ -15,7 +15,6 @@ import {
} from '../../../shared/constants/network';
import * as Utils from '../lib/util';
import MetaMetricsController from './metametrics';
import { NETWORK_EVENTS } from './network';
const segment = createSegmentMock(2, 10000);
@ -72,17 +71,17 @@ function getMockNetworkController() {
},
network: 'loading',
};
const on = sinon.stub().withArgs(NETWORK_EVENTS.NETWORK_DID_CHANGE);
const onNetworkDidChange = sinon.stub();
const updateState = (newState) => {
state = { ...state, ...newState };
on.getCall(0).args[1]();
onNetworkDidChange.getCall(0).args[0]();
};
return {
store: {
getState: () => state,
updateState,
},
on,
onNetworkDidChange,
};
}
@ -136,10 +135,8 @@ function getMetaMetricsController({
segment: segmentInstance || segment,
getCurrentChainId: () =>
networkController.store.getState().provider.chainId,
onNetworkDidChange: networkController.on.bind(
networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
),
onNetworkDidChange:
networkController.onNetworkDidChange.bind(networkController),
preferencesStore,
version: '0.0.1',
environment: 'test',
@ -952,6 +949,7 @@ describe('MetaMetricsController', function () {
theme: 'default',
useTokenDetection: true,
desktopEnabled: false,
security_providers: [],
});
assert.deepEqual(traits, {
@ -970,6 +968,7 @@ describe('MetaMetricsController', function () {
[TRAITS.THEME]: 'default',
[TRAITS.TOKEN_DETECTION_ENABLED]: true,
[TRAITS.DESKTOP_ENABLED]: false,
[TRAITS.SECURITY_PROVIDERS]: [],
});
});

View File

@ -1 +1 @@
export { default, NETWORK_EVENTS } from './network-controller';
export { default, NetworkControllerEventTypes } from './network-controller';

View File

@ -7,7 +7,12 @@ import {
createEventEmitterProxy,
} from 'swappable-obj-proxy';
import EthQuery from 'eth-query';
// ControllerMessenger is referred to in the JSDocs
// eslint-disable-next-line no-unused-vars
import { ControllerMessenger } from '@metamask/base-controller';
import { v4 as random } from 'uuid';
import { hasProperty, isPlainObject } from '@metamask/utils';
import { errorCodes } from 'eth-rpc-errors';
import {
INFURA_PROVIDER_TYPES,
BUILT_IN_NETWORKS,
@ -15,8 +20,8 @@ import {
TEST_NETWORK_TICKER_MAP,
CHAIN_IDS,
NETWORK_TYPES,
NetworkStatus,
} from '../../../../shared/constants/network';
import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout';
import {
isPrefixedFormattedHexString,
isSafeChainId,
@ -33,91 +38,133 @@ import { createNetworkClient } from './create-network-client';
* @property {string} [nickname] - Personalized network name.
*/
const env = process.env.METAMASK_ENV;
const fetchWithTimeout = getFetchWithTimeout();
function buildDefaultProviderConfigState() {
if (process.env.IN_TEST) {
return {
type: NETWORK_TYPES.RPC,
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
nickname: 'Localhost 8545',
ticker: 'ETH',
};
} else if (
process.env.METAMASK_DEBUG ||
process.env.METAMASK_ENV === 'test'
) {
return {
type: NETWORK_TYPES.GOERLI,
chainId: CHAIN_IDS.GOERLI,
ticker: TEST_NETWORK_TICKER_MAP.GOERLI,
};
}
let defaultProviderConfigOpts;
if (process.env.IN_TEST) {
defaultProviderConfigOpts = {
type: NETWORK_TYPES.RPC,
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
nickname: 'Localhost 8545',
};
} else if (process.env.METAMASK_DEBUG || env === 'test') {
defaultProviderConfigOpts = {
type: NETWORK_TYPES.GOERLI,
chainId: CHAIN_IDS.GOERLI,
ticker: TEST_NETWORK_TICKER_MAP.GOERLI,
};
} else {
defaultProviderConfigOpts = {
return {
type: NETWORK_TYPES.MAINNET,
chainId: CHAIN_IDS.MAINNET,
ticker: 'ETH',
};
}
const defaultProviderConfig = {
ticker: 'ETH',
...defaultProviderConfigOpts,
};
function buildDefaultNetworkIdState() {
return null;
}
const defaultNetworkDetailsState = {
EIPS: { 1559: undefined },
};
function buildDefaultNetworkStatusState() {
return NetworkStatus.Unknown;
}
export const NETWORK_EVENTS = {
// Fired after the actively selected network is changed
NETWORK_DID_CHANGE: 'networkDidChange',
// Fired when the actively selected network *will* change
NETWORK_WILL_CHANGE: 'networkWillChange',
// Fired when Infura returns an error indicating no support
INFURA_IS_BLOCKED: 'infuraIsBlocked',
// Fired when not using an Infura network or when Infura returns no error, indicating support
INFURA_IS_UNBLOCKED: 'infuraIsUnblocked',
function buildDefaultNetworkDetailsState() {
return {
EIPS: {
1559: undefined,
},
};
}
function buildDefaultNetworkConfigurationsState() {
return {};
}
/**
* The name of the controller.
*/
const name = 'NetworkController';
/**
* The set of event types that this controller can publish via its messenger.
*/
export const NetworkControllerEventTypes = {
/**
* Fired after the current network is changed.
*/
NetworkDidChange: `${name}:networkDidChange`,
/**
* Fired when there is a request to change the current network, but no state
* changes have occurred yet.
*/
NetworkWillChange: `${name}:networkWillChange`,
/**
* Fired after the network is changed to an Infura network, but when Infura
* returns an error denying support for the user's location.
*/
InfuraIsBlocked: `${name}:infuraIsBlocked`,
/**
* Fired after the network is changed to an Infura network and Infura does not
* return an error denying support for the user's location, or after the
* network is changed to a custom network.
*/
InfuraIsUnblocked: `${name}:infuraIsUnblocked`,
};
export default class NetworkController extends EventEmitter {
static defaultProviderConfig = defaultProviderConfig;
/**
* Construct a NetworkController.
*
* @param {object} [options] - NetworkController options.
* @param {object} options - Options for this controller.
* @param {ControllerMessenger} options.messenger - The controller messenger.
* @param {object} [options.state] - Initial controller state.
* @param {string} [options.infuraProjectId] - The Infura project ID.
* @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController
*/
constructor({ state = {}, infuraProjectId, trackMetaMetricsEvent } = {}) {
constructor({
messenger,
state = {},
infuraProjectId,
trackMetaMetricsEvent,
} = {}) {
super();
this.messenger = messenger;
// create stores
this.providerStore = new ObservableStore(
state.provider || { ...defaultProviderConfig },
state.provider || buildDefaultProviderConfigState(),
);
this.previousProviderStore = new ObservableStore(
this.providerStore.getState(),
);
this.networkStore = new ObservableStore('loading');
// We need to keep track of a few details about the current network
// Ideally we'd merge this.networkStore with this new store, but doing so
// will require a decent sized refactor of how we're accessing network
// state. Currently this is only used for detecting EIP 1559 support but
// can be extended to track other network details.
this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState());
this.networkStatusStore = new ObservableStore(
buildDefaultNetworkStatusState(),
);
// We need to keep track of a few details about the current network.
// Ideally we'd merge this.networkStatusStore with this new store, but doing
// so will require a decent sized refactor of how we're accessing network
// state. Currently this is only used for detecting EIP-1559 support but can
// be extended to track other network details.
this.networkDetails = new ObservableStore(
state.networkDetails || {
...defaultNetworkDetailsState,
},
state.networkDetails || buildDefaultNetworkDetailsState(),
);
this.networkConfigurationsStore = new ObservableStore(
state.networkConfigurations || {},
state.networkConfigurations || buildDefaultNetworkConfigurationsState(),
);
this.store = new ComposedStore({
provider: this.providerStore,
previousProviderStore: this.previousProviderStore,
network: this.networkStore,
networkId: this.networkIdStore,
networkStatus: this.networkStatusStore,
networkDetails: this.networkDetails,
networkConfigurations: this.networkConfigurationsStore,
});
@ -134,11 +181,8 @@ export default class NetworkController extends EventEmitter {
throw new Error('Invalid Infura project ID');
}
this._infuraProjectId = infuraProjectId;
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
this.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
this.lookupNetwork();
});
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
}
/**
@ -164,10 +208,12 @@ export default class NetworkController extends EventEmitter {
}
/**
* Method to check if the block header contains fields that indicate EIP 1559
* support (baseFeePerGas).
* Determines whether the network supports EIP-1559 by checking whether the
* latest block has a `baseFeePerGas` property, then updates state
* appropriately.
*
* @returns {Promise<boolean>} true if current network supports EIP 1559
* @returns {Promise<boolean>} A promise that resolves to true if the network
* supports EIP-1559 and false otherwise.
*/
async getEIP1559Compatibility() {
const { EIPS } = this.networkDetails.getState();
@ -176,15 +222,28 @@ export default class NetworkController extends EventEmitter {
if (EIPS[1559] !== undefined) {
return EIPS[1559];
}
const latestBlock = await this._getLatestBlock();
const supportsEIP1559 =
latestBlock && latestBlock.baseFeePerGas !== undefined;
this._setNetworkEIPSupport(1559, supportsEIP1559);
const supportsEIP1559 = await this._determineEIP1559Compatibility();
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
},
});
return supportsEIP1559;
}
/**
* Captures information about the currently selected network namely,
* the network ID and whether the network supports EIP-1559 and then uses
* the results of these requests to determine the status of the network.
*/
async lookupNetwork() {
// Prevent firing when provider is not defined.
const { chainId, type } = this.providerStore.getState();
let networkChanged = false;
let networkId;
let supportsEIP1559;
let networkStatus;
if (!this._provider) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing provider',
@ -192,46 +251,102 @@ export default class NetworkController extends EventEmitter {
return;
}
const { chainId } = this.providerStore.getState();
if (!chainId) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId',
);
this._setNetworkState('loading');
this._clearNetworkDetails();
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
return;
}
// Ping the RPC endpoint so we can confirm that it works
const initialNetwork = this.networkStore.getState();
const { type } = this.providerStore.getState();
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
if (isInfura) {
this._checkInfuraAvailability(type);
} else {
this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED);
const listener = () => {
networkChanged = true;
this.messenger.unsubscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
};
this.messenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
try {
const results = await Promise.all([
this._getNetworkId(),
this._determineEIP1559Compatibility(),
]);
networkId = results[0];
supportsEIP1559 = results[1];
networkStatus = NetworkStatus.Available;
} catch (error) {
if (hasProperty(error, 'code')) {
let responseBody;
try {
responseBody = JSON.parse(error.message);
} catch {
// error.message must not be JSON
}
if (
isPlainObject(responseBody) &&
responseBody.error === INFURA_BLOCKED_KEY
) {
networkStatus = NetworkStatus.Blocked;
} else if (error.code === errorCodes.rpc.internal) {
networkStatus = NetworkStatus.Unknown;
} else {
networkStatus = NetworkStatus.Unavailable;
}
} else {
log.warn(
'NetworkController - could not determine network status',
error,
);
networkStatus = NetworkStatus.Unknown;
}
}
let networkVersion;
let networkVersionError;
try {
networkVersion = await this._getNetworkId();
} catch (error) {
networkVersionError = error;
}
if (initialNetwork !== this.networkStore.getState()) {
if (networkChanged) {
// If the network has changed, then `lookupNetwork` either has been or is
// in the process of being called, so we don't need to go further.
return;
}
this.messenger.unsubscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
if (networkVersionError) {
this._setNetworkState('loading');
// keep network details in sync with network state
this._clearNetworkDetails();
this.networkStatusStore.putState(networkStatus);
if (networkStatus === NetworkStatus.Available) {
this.networkIdStore.putState(networkId);
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
},
});
} else {
this._setNetworkState(networkVersion);
// look up EIP-1559 support
await this.getEIP1559Compatibility();
this._resetNetworkId();
this._resetNetworkDetails();
}
if (isInfura) {
if (networkStatus === NetworkStatus.Available) {
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
} else if (networkStatus === NetworkStatus.Blocked) {
this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked);
}
} else {
// Always publish infuraIsUnblocked regardless of network status to
// prevent consumers from being stuck in a blocked state if they were
// previously connected to an Infura network that was blocked
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
}
}
@ -294,13 +409,38 @@ export default class NetworkController extends EventEmitter {
// Private
//
/**
* Method to return the latest block for the current network
*
* @returns {object} Block header
*/
_getLatestBlock() {
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
return new Promise((resolve, reject) => {
ethQuery.sendAsync(
{ method: 'eth_getBlockByNumber', params: ['latest', false] },
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
},
);
});
}
/**
* Get the network ID for the current selected network
*
* @returns {string} The network ID for the current network.
*/
async _getNetworkId() {
const ethQuery = new EthQuery(this._provider);
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
return await new Promise((resolve, reject) => {
ethQuery.sendAsync({ method: 'net_version' }, (error, result) => {
if (error) {
@ -313,49 +453,24 @@ export default class NetworkController extends EventEmitter {
}
/**
* Method to return the latest block for the current network
*
* @returns {object} Block header
* Clears the stored network ID.
*/
_getLatestBlock() {
return new Promise((resolve, reject) => {
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
ethQuery.sendAsync(
{ method: 'eth_getBlockByNumber', params: ['latest', false] },
(err, block) => {
if (err) {
return reject(err);
}
return resolve(block);
},
);
});
}
_setNetworkState(network) {
this.networkStore.putState(network);
_resetNetworkId() {
this.networkIdStore.putState(buildDefaultNetworkIdState());
}
/**
* Set EIP support indication in the networkDetails store
*
* @param {number} EIPNumber - The number of the EIP to mark support for
* @param {boolean} isSupported - True if the EIP is supported
* Resets network status to the default ("unknown").
*/
_setNetworkEIPSupport(EIPNumber, isSupported) {
this.networkDetails.putState({
EIPS: {
[EIPNumber]: isSupported,
},
});
_resetNetworkStatus() {
this.networkStatusStore.putState(buildDefaultNetworkStatusState());
}
/**
* Reset EIP support to default (no support)
* Clears details previously stored for the network.
*/
_clearNetworkDetails() {
this.networkDetails.putState({ ...defaultNetworkDetailsState });
_resetNetworkDetails() {
this.networkDetails.putState(buildDefaultNetworkDetailsState());
}
/**
@ -369,56 +484,27 @@ export default class NetworkController extends EventEmitter {
this._switchNetwork(config);
}
async _checkInfuraAvailability(network) {
const rpcUrl = `https://${network}.infura.io/v3/${this._infuraProjectId}`;
let networkChanged = false;
this.once(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
networkChanged = true;
});
try {
const response = await fetchWithTimeout(rpcUrl, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_blockNumber',
params: [],
id: 1,
}),
});
if (networkChanged) {
return;
}
if (response.ok) {
this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED);
} else {
const responseMessage = await response.json();
if (networkChanged) {
return;
}
if (responseMessage.error === INFURA_BLOCKED_KEY) {
this.emit(NETWORK_EVENTS.INFURA_IS_BLOCKED);
}
}
} catch (err) {
log.warn(`MetaMask - Infura availability check failed`, err);
}
/**
* Retrieves the latest block from the currently selected network; if the
* block has a `baseFeePerGas` property, then we know that the network
* supports EIP-1559; otherwise it doesn't.
*
* @returns {Promise<boolean>} A promise that resolves to true if the network
* supports EIP-1559 and false otherwise.
*/
async _determineEIP1559Compatibility() {
const latestBlock = await this._getLatestBlock();
return latestBlock && latestBlock.baseFeePerGas !== undefined;
}
_switchNetwork(opts) {
// Indicate to subscribers that network is about to change
this.emit(NETWORK_EVENTS.NETWORK_WILL_CHANGE);
// Set loading state
this._setNetworkState('loading');
// Reset network details
this._clearNetworkDetails();
// Configure the provider appropriately
this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
this._configureProvider(opts);
// Notify subscribers that network has changed
this.emit(NETWORK_EVENTS.NETWORK_DID_CHANGE, opts.type);
this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange);
this.lookupNetwork();
}
_configureProvider({ type, rpcUrl, chainId }) {

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ import { normalize as normalizeAddress } from 'eth-sig-util';
import { IPFS_DEFAULT_GATEWAY_URL } from '../../../shared/constants/network';
import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets';
import { ThemeType } from '../../../shared/constants/preferences';
import { NETWORK_EVENTS } from './network';
export default class PreferencesController {
/**
@ -70,7 +69,8 @@ export default class PreferencesController {
...opts.initState,
};
this.network = opts.network;
this._onInfuraIsBlocked = opts.onInfuraIsBlocked;
this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked;
this.store = new ObservableStore(initState);
this.store.setMaxListeners(13);
this.openPopup = opts.openPopup;
@ -511,10 +511,11 @@ export default class PreferencesController {
//
_subscribeToInfuraAvailability() {
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
this._onInfuraIsBlocked(() => {
this._setInfuraBlocked(true);
});
this.network.on(NETWORK_EVENTS.INFURA_IS_UNBLOCKED, () => {
this._onInfuraIsUnblocked(() => {
this._setInfuraBlocked(false);
});
}

View File

@ -19,8 +19,10 @@ describe('preferences controller', function () {
const networkControllerProviderConfig = {
getAccounts: () => undefined,
};
const networkControllerMessenger = new ControllerMessenger();
network = new NetworkController({
infuraProjectId: 'foo',
messenger: networkControllerMessenger,
state: {
provider: {
type: 'mainnet',
@ -50,6 +52,8 @@ describe('preferences controller', function () {
network,
provider,
tokenListController,
onInfuraIsBlocked: sinon.spy(),
onInfuraIsUnblocked: sinon.spy(),
});
});

View File

@ -17,7 +17,7 @@ import {
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
} from '../../../shared/constants/swaps';
import { GasEstimateTypes } from '../../../shared/constants/gas';
import { CHAIN_IDS } from '../../../shared/constants/network';
import { CHAIN_IDS, NetworkStatus } from '../../../shared/constants/network';
import {
FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME,
FALLBACK_SMART_TRANSACTIONS_DEADLINE,
@ -41,7 +41,6 @@ import fetchEstimatedL1Fee from '../../../ui/helpers/utils/optimism/fetchEstimat
import { Numeric } from '../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../shared/constants/common';
import { NETWORK_EVENTS } from './network';
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
const MAX_GAS_LIMIT = 2500000;
@ -114,6 +113,7 @@ export default class SwapsController {
fetchTradesInfo = defaultFetchTradesInfo,
getCurrentChainId,
getEIP1559GasFeeEstimates,
onNetworkDidChange,
}) {
this.store = new ObservableStore({
swapsState: { ...initialState.swapsState },
@ -136,10 +136,14 @@ export default class SwapsController {
this.indexOfNewestCallInFlight = 0;
this.ethersProvider = new Web3Provider(provider);
this._currentNetwork = networkController.store.getState().network;
networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, (network) => {
if (network !== 'loading' && network !== this._currentNetwork) {
this._currentNetwork = network;
this._currentNetworkId = networkController.store.getState().networkId;
onNetworkDidChange(() => {
const { networkId, networkStatus } = networkController.store.getState();
if (
networkStatus === NetworkStatus.Available &&
networkId !== this._currentNetworkId
) {
this._currentNetworkId = networkId;
this.ethersProvider = new Web3Provider(provider);
}
});

View File

@ -4,7 +4,11 @@ import sinon from 'sinon';
import { BigNumber } from '@ethersproject/bignumber';
import { mapValues } from 'lodash';
import BigNumberjs from 'bignumber.js';
import { CHAIN_IDS, NETWORK_IDS } from '../../../shared/constants/network';
import {
CHAIN_IDS,
NETWORK_IDS,
NetworkStatus,
} from '../../../shared/constants/network';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../test/stub/provider';
import { SECOND } from '../../../shared/constants/time';
@ -14,7 +18,6 @@ import {
FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER,
} from '../../../shared/constants/smartTransactions';
import SwapsController, { utils } from './swaps';
import { NETWORK_EVENTS } from './network';
const MOCK_FETCH_PARAMS = {
slippage: 3,
@ -98,16 +101,11 @@ const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({
function getMockNetworkController() {
return {
store: {
getState: () => {
return {
network: NETWORK_IDS.GOERLI,
};
},
getState: sinon.stub().returns({
networkId: NETWORK_IDS.GOERLI,
networkStatus: NetworkStatus.Available,
}),
},
on: sinon
.stub()
.withArgs(NETWORK_EVENTS.NETWORK_DID_CHANGE)
.callsArgAsync(1),
};
}
@ -162,6 +160,7 @@ describe('SwapsController', function () {
return new SwapsController({
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
networkController: getMockNetworkController(),
onNetworkDidChange: sinon.stub(),
provider,
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
@ -209,9 +208,11 @@ describe('SwapsController', function () {
it('should replace ethers instance when network changes', function () {
const networkController = getMockNetworkController();
const onNetworkDidChange = sinon.stub();
const swapsController = new SwapsController({
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
networkController,
onNetworkDidChange,
provider,
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
@ -219,9 +220,13 @@ describe('SwapsController', function () {
getCurrentChainId: getCurrentChainIdStub,
});
const currentEthersInstance = swapsController.ethersProvider;
const onNetworkDidChange = networkController.on.getCall(0).args[1];
const changeNetwork = onNetworkDidChange.getCall(0).args[0];
onNetworkDidChange(NETWORK_IDS.MAINNET);
networkController.store.getState.returns({
networkId: NETWORK_IDS.MAINNET,
networkStatus: NetworkStatus.Available,
});
changeNetwork(NETWORK_IDS.MAINNET);
const newEthersInstance = swapsController.ethersProvider;
assert.notStrictEqual(
@ -233,9 +238,11 @@ describe('SwapsController', function () {
it('should not replace ethers instance when network changes to loading', function () {
const networkController = getMockNetworkController();
const onNetworkDidChange = sinon.stub();
const swapsController = new SwapsController({
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
networkController,
onNetworkDidChange,
provider,
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
@ -243,9 +250,13 @@ describe('SwapsController', function () {
getCurrentChainId: getCurrentChainIdStub,
});
const currentEthersInstance = swapsController.ethersProvider;
const onNetworkDidChange = networkController.on.getCall(0).args[1];
const changeNetwork = onNetworkDidChange.getCall(0).args[0];
onNetworkDidChange('loading');
networkController.store.getState.returns({
networkId: null,
networkStatus: NetworkStatus.Unknown,
});
changeNetwork('loading');
const newEthersInstance = swapsController.ethersProvider;
assert.strictEqual(
@ -257,9 +268,11 @@ describe('SwapsController', function () {
it('should not replace ethers instance when network changes to the same network', function () {
const networkController = getMockNetworkController();
const onNetworkDidChange = sinon.stub();
const swapsController = new SwapsController({
getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT,
networkController,
onNetworkDidChange,
provider,
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
@ -267,9 +280,13 @@ describe('SwapsController', function () {
getCurrentChainId: getCurrentChainIdStub,
});
const currentEthersInstance = swapsController.ethersProvider;
const onNetworkDidChange = networkController.on.getCall(0).args[1];
const changeNetwork = onNetworkDidChange.getCall(0).args[0];
onNetworkDidChange(NETWORK_IDS.GOERLI);
networkController.store.getState.returns({
networkId: NETWORK_IDS.GOERLI,
networkStatus: NetworkStatus.Available,
});
changeNetwork(NETWORK_IDS.GOERLI);
const newEthersInstance = swapsController.ethersProvider;
assert.strictEqual(

View File

@ -44,6 +44,7 @@ import {
HARDFORKS,
CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP,
NETWORK_TYPES,
NetworkStatus,
} from '../../../../shared/constants/network';
import {
determineTransactionAssetType,
@ -115,7 +116,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
*
* @param {object} opts
* @param {object} opts.initState - initial transaction list default is an empty array
* @param {Function} opts.getNetworkState - Get the current network state.
* @param {Function} opts.getNetworkId - Get the current network ID.
* @param {Function} opts.getNetworkStatus - Get the current network status.
* @param {Function} opts.onNetworkStateChange - Subscribe to network state change events.
* @param {object} opts.blockTracker - An instance of eth-blocktracker
* @param {object} opts.provider - A network provider.
@ -129,7 +131,8 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
export default class TransactionController extends EventEmitter {
constructor(opts) {
super();
this.getNetworkState = opts.getNetworkState;
this.getNetworkId = opts.getNetworkId;
this.getNetworkStatus = opts.getNetworkStatus;
this._getCurrentChainId = opts.getCurrentChainId;
this.getProviderConfig = opts.getProviderConfig;
this._getCurrentNetworkEIP1559Compatibility =
@ -167,7 +170,8 @@ export default class TransactionController extends EventEmitter {
this.txStateManager = new TransactionStateManager({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetworkState: this.getNetworkState,
getNetworkId: this.getNetworkId,
getNetworkStatus: this.getNetworkStatus,
getCurrentChainId: opts.getCurrentChainId,
});
@ -226,10 +230,13 @@ export default class TransactionController extends EventEmitter {
* @returns {number} The numerical chainId.
*/
getChainId() {
const networkState = this.getNetworkState();
const networkStatus = this.getNetworkStatus();
const chainId = this._getCurrentChainId();
const integerChainId = parseInt(chainId, 16);
if (networkState === 'loading' || Number.isNaN(integerChainId)) {
if (
networkStatus !== NetworkStatus.Available ||
Number.isNaN(integerChainId)
) {
return 0;
}
return integerChainId;
@ -272,12 +279,13 @@ export default class TransactionController extends EventEmitter {
});
}
// For 'rpc' we need to use the same basic configuration as mainnet,
// since we only support EVM compatible chains, and then override the
// For 'rpc' we need to use the same basic configuration as mainnet, since
// we only support EVM compatible chains, and then override the
// name, chainId and networkId properties. This is done using the
// `forCustomChain` static method on the Common class.
const chainId = parseInt(this._getCurrentChainId(), 16);
const networkId = this.getNetworkState();
const networkStatus = this.getNetworkStatus();
const networkId = this.getNetworkId();
const customChainParams = {
name,
@ -291,7 +299,8 @@ export default class TransactionController extends EventEmitter {
// on a custom network that requires valid network id. I have not ran
// into this limitation on any network I have attempted, even when
// hardcoding networkId to 'loading'.
networkId: networkId === 'loading' ? 0 : parseInt(networkId, 10),
networkId:
networkStatus === NetworkStatus.Available ? parseInt(networkId, 10) : 0,
};
return Common.forCustomChain(
@ -2147,6 +2156,7 @@ export default class TransactionController extends EventEmitter {
originalApprovalAmount,
finalApprovalAmount,
contractMethodName,
securityProviderResponse,
} = txMeta;
const source = referrer === ORIGIN_METAMASK ? 'user' : 'dapp';
@ -2298,6 +2308,16 @@ export default class TransactionController extends EventEmitter {
}
}
let uiCustomizations;
if (securityProviderResponse?.flagAsDangerous === 1) {
uiCustomizations = ['flagged_as_malicious'];
} else if (securityProviderResponse?.flagAsDangerous === 2) {
uiCustomizations = ['flagged_as_safety_unknown'];
} else {
uiCustomizations = null;
}
let properties = {
chain_id: chainId,
referrer,
@ -2312,6 +2332,7 @@ export default class TransactionController extends EventEmitter {
token_standard: tokenStandard,
transaction_type: transactionType,
transaction_speed_up: type === TransactionType.retry,
ui_customizations: uiCustomizations,
};
if (transactionContractMethod === contractMethodNames.APPROVE) {

View File

@ -27,12 +27,14 @@ import {
} from '../../../../shared/constants/gas';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import { NetworkStatus } from '../../../../shared/constants/network';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
import TransactionController from '.';
const noop = () => true;
const currentNetworkId = '5';
const currentChainId = '0x5';
const currentNetworkStatus = NetworkStatus.Available;
const providerConfig = {
type: 'goerli',
};
@ -46,7 +48,8 @@ describe('Transaction Controller', function () {
providerResultStub,
fromAccount,
fragmentExists,
networkStore;
networkStatusStore,
getCurrentChainId;
beforeEach(function () {
fragmentExists = false;
@ -59,22 +62,27 @@ describe('Transaction Controller', function () {
provider = createTestProviderTools({
scaffold: providerResultStub,
networkId: currentNetworkId,
chainId: currentNetworkId,
chainId: parseInt(currentChainId, 16),
}).provider;
networkStore = new ObservableStore(currentNetworkId);
networkStatusStore = new ObservableStore(currentNetworkStatus);
fromAccount = getTestAccounts()[0];
const blockTrackerStub = new EventEmitter();
blockTrackerStub.getCurrentBlock = noop;
blockTrackerStub.getLatestBlock = noop;
getCurrentChainId = sinon.stub().callsFake(() => currentChainId);
txController = new TransactionController({
provider,
getGasPrice() {
return '0xee6b2800';
},
getNetworkState: () => networkStore.getState(),
onNetworkStateChange: (listener) => networkStore.subscribe(listener),
getNetworkId: () => currentNetworkId,
getNetworkStatus: () => networkStatusStore.getState(),
onNetworkStateChange: (listener) =>
networkStatusStore.subscribe(listener),
getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(false),
getCurrentAccountEIP1559Compatibility: () => false,
txHistoryLimit: 10,
@ -85,7 +93,7 @@ describe('Transaction Controller', function () {
}),
getProviderConfig: () => providerConfig,
getPermittedAccounts: () => undefined,
getCurrentChainId: () => currentChainId,
getCurrentChainId,
getParticipateInMetrics: () => false,
trackMetaMetricsEvent: () => undefined,
createEventFragment: () => undefined,
@ -467,8 +475,8 @@ describe('Transaction Controller', function () {
);
});
it('should fail if netId is loading', async function () {
networkStore.putState('loading');
it('should fail if the network status is not "available"', async function () {
networkStatusStore.putState(NetworkStatus.Unknown);
await assert.rejects(
() =>
txController.addUnapprovedTransaction(undefined, {
@ -1079,8 +1087,19 @@ describe('Transaction Controller', function () {
});
describe('#getChainId', function () {
it('returns 0 when the chainId is NaN', function () {
networkStore.putState('loading');
it('returns the chain ID of the network when it is available', function () {
networkStatusStore.putState(NetworkStatus.Available);
assert.equal(txController.getChainId(), 5);
});
it('returns 0 when the network is not available', function () {
networkStatusStore.putState('asdflsfadf');
assert.equal(txController.getChainId(), 0);
});
it('returns 0 when the chain ID cannot be parsed as a hex string', function () {
networkStatusStore.putState(NetworkStatus.Available);
getCurrentChainId.returns('$fdsjfldf');
assert.equal(txController.getChainId(), 0);
});
});
@ -1740,6 +1759,9 @@ describe('Transaction Controller', function () {
gas: '0x7b0d',
gasPrice: '0x77359400',
},
securityProviderResponse: {
flagAsDangerous: 0,
},
};
});
@ -1766,6 +1788,7 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
@ -1852,6 +1875,7 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
@ -1921,6 +1945,9 @@ describe('Transaction Controller', function () {
gas: '0x7b0d',
gasPrice: '0x77359400',
},
securityProviderResponse: {
flagAsDangerous: 0,
},
};
});
@ -1947,6 +1974,7 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
@ -2035,6 +2063,7 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
@ -2099,6 +2128,9 @@ describe('Transaction Controller', function () {
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 0,
},
};
const expectedPayload = {
@ -2122,6 +2154,7 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
gas_price: '2',
@ -2167,6 +2200,9 @@ describe('Transaction Controller', function () {
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 0,
},
};
const expectedPayload = {
actionId,
@ -2190,6 +2226,155 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is malicious', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 1,
},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: EVENT.CATEGORIES.TRANSACTIONS,
properties: {
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: ['flagged_as_malicious'],
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is unknown', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 2,
},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: EVENT.CATEGORIES.TRANSACTIONS,
properties: {
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: ['flagged_as_safety_unknown'],
},
sensitiveProperties: {
baz: 3.0,
@ -2245,6 +2430,9 @@ describe('Transaction Controller', function () {
maxFeePerGas: '0x77359400',
maxPriorityFeePerGas: '0x77359400',
},
securityProviderResponse: {
flagAsDangerous: 0,
},
};
const expectedPayload = {
actionId,
@ -2268,6 +2456,7 @@ describe('Transaction Controller', function () {
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
baz: 3.0,

View File

@ -7,6 +7,7 @@ import { TransactionStatus } from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import { NetworkStatus } from '../../../../shared/constants/network';
import {
generateHistoryEntry,
replayHistory,
@ -54,13 +55,15 @@ export const ERROR_SUBMITTING =
* transactions list keyed by id
* @param {number} [opts.txHistoryLimit] - limit for how many finished
* transactions can hang around in state
* @param {Function} opts.getNetworkState - Get the current network state.
* @param {Function} opts.getNetworkId - Get the current network Id.
* @param {Function} opts.getNetworkStatus - Get the current network status.
*/
export default class TransactionStateManager extends EventEmitter {
constructor({
initState,
txHistoryLimit,
getNetworkState,
getNetworkId,
getNetworkStatus,
getCurrentChainId,
}) {
super();
@ -70,7 +73,8 @@ export default class TransactionStateManager extends EventEmitter {
...initState,
});
this.txHistoryLimit = txHistoryLimit;
this.getNetworkState = getNetworkState;
this.getNetworkId = getNetworkId;
this.getNetworkStatus = getNetworkStatus;
this.getCurrentChainId = getCurrentChainId;
}
@ -86,9 +90,10 @@ export default class TransactionStateManager extends EventEmitter {
* @returns {TransactionMeta} the default txMeta object
*/
generateTxMeta(opts = {}) {
const netId = this.getNetworkState();
const networkId = this.getNetworkId();
const networkStatus = this.getNetworkStatus();
const chainId = this.getCurrentChainId();
if (netId === 'loading') {
if (networkStatus !== NetworkStatus.Available) {
throw new Error('MetaMask is having trouble connecting to the network');
}
@ -128,7 +133,7 @@ export default class TransactionStateManager extends EventEmitter {
id: createId(),
time: new Date().getTime(),
status: TransactionStatus.unapproved,
metamaskNetworkId: netId,
metamaskNetworkId: networkId,
originalGasEstimate: opts.txParams?.gas,
userEditedGasLimit: false,
chainId,
@ -149,12 +154,12 @@ export default class TransactionStateManager extends EventEmitter {
*/
getUnapprovedTxList() {
const chainId = this.getCurrentChainId();
const network = this.getNetworkState();
const networkId = this.getNetworkId();
return pickBy(
this.store.getState().transactions,
(transaction) =>
transaction.status === TransactionStatus.unapproved &&
transactionMatchesNetwork(transaction, chainId, network),
transactionMatchesNetwork(transaction, chainId, networkId),
);
}
@ -413,7 +418,7 @@ export default class TransactionStateManager extends EventEmitter {
limit,
} = {}) {
const chainId = this.getCurrentChainId();
const network = this.getNetworkState();
const networkId = this.getNetworkId();
// searchCriteria is an object that might have values that aren't predicate
// methods. When providing any other value type (string, number, etc), we
// consider this shorthand for "check the value at key for strict equality
@ -442,7 +447,7 @@ export default class TransactionStateManager extends EventEmitter {
// when filterToCurrentNetwork is true.
if (
filterToCurrentNetwork &&
transactionMatchesNetwork(transaction, chainId, network) === false
transactionMatchesNetwork(transaction, chainId, networkId) === false
) {
return false;
}
@ -596,8 +601,7 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
* Removes all transactions for the given address on the current network,
* preferring chainId for comparison over networkId.
* Removes all transactions for the given address on the current network.
*
* @param {string} address - hex string of the from address on the txParams
* to remove
@ -605,8 +609,8 @@ export default class TransactionStateManager extends EventEmitter {
wipeTransactions(address) {
// network only tx
const { transactions } = this.store.getState();
const network = this.getNetworkState();
const chainId = this.getCurrentChainId();
const networkId = this.getNetworkId();
// Update state
this.store.updateState({
@ -614,7 +618,7 @@ export default class TransactionStateManager extends EventEmitter {
transactions,
(transaction) =>
transaction.txParams.from === address &&
transactionMatchesNetwork(transaction, chainId, network),
transactionMatchesNetwork(transaction, chainId, networkId),
),
});
}

View File

@ -4,7 +4,11 @@ import {
TransactionStatus,
TransactionType,
} from '../../../../shared/constants/transaction';
import { CHAIN_IDS, NETWORK_IDS } from '../../../../shared/constants/network';
import {
CHAIN_IDS,
NETWORK_IDS,
NetworkStatus,
} from '../../../../shared/constants/network';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import TxStateManager, { ERROR_SUBMITTING } from './tx-state-manager';
@ -45,6 +49,7 @@ function generateTransactions(
describe('TransactionStateManager', function () {
let txStateManager;
const currentNetworkId = NETWORK_IDS.GOERLI;
const currentNetworkStatus = NetworkStatus.Available;
const currentChainId = CHAIN_IDS.MAINNET;
const otherNetworkId = '2';
@ -54,7 +59,8 @@ describe('TransactionStateManager', function () {
transactions: {},
},
txHistoryLimit: 10,
getNetworkState: () => currentNetworkId,
getNetworkId: () => currentNetworkId,
getNetworkStatus: () => currentNetworkStatus,
getCurrentChainId: () => currentChainId,
});
});
@ -181,7 +187,8 @@ describe('TransactionStateManager', function () {
[confirmedTx.id]: confirmedTx,
},
},
getNetworkState: () => currentNetworkId,
getNetworkId: () => currentNetworkId,
getNetworkStatus: () => currentNetworkStatus,
getCurrentChainId: () => currentChainId,
});
@ -246,7 +253,8 @@ describe('TransactionStateManager', function () {
[confirmedTx3.id]: confirmedTx3,
},
},
getNetworkState: () => currentNetworkId,
getNetworkId: () => currentNetworkId,
getNetworkStatus: () => currentNetworkStatus,
getCurrentChainId: () => currentChainId,
});
@ -355,7 +363,8 @@ describe('TransactionStateManager', function () {
[failedTx3Dupe.id]: failedTx3Dupe,
},
},
getNetworkState: () => currentNetworkId,
getNetworkId: () => currentNetworkId,
getNetworkStatus: () => currentNetworkStatus,
getCurrentChainId: () => currentChainId,
});

View File

@ -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 = {
@ -107,14 +107,16 @@ const rateLimitTimeouts = {};
* MetaMetricsController
* @param {number} [opts.rateLimitSeconds] - number of seconds to wait before
* allowing another set of events to be tracked.
* @param opts.securityProviderRequest
* @returns {Function}
*/
export default function createRPCMethodTrackingMiddleware({
trackEvent,
getMetricsState,
rateLimitSeconds = 60 * 5,
securityProviderRequest,
}) {
return function rpcMethodTrackingMiddleware(
return async function rpcMethodTrackingMiddleware(
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {Function} */ next,
@ -140,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
@ -160,22 +164,55 @@ export default function createRPCMethodTrackingMiddleware({
? eventType.REQUESTED
: EVENT_NAMES.PROVIDER_METHOD_CALLED;
const properties = {};
if (event === EVENT_NAMES.SIGNATURE_REQUESTED) {
properties.signature_type = method;
} else {
properties.method = method;
}
eventProperties.signature_type = method;
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
const data = req?.params?.[0];
const { isSIWEMessage } = detectSIWE({ data });
if (isSIWEMessage) {
properties.ui_customizations = [
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS].SIWE,
];
const from = req?.params?.[1];
const paramsExamplePassword = req?.params?.[2];
const msgData = {
msgParams: {
...paramsExamplePassword,
from,
data,
origin,
},
status: TransactionStatus.unapproved,
type: req.method,
};
try {
const securityProviderResponse = await securityProviderRequest(
msgData,
req.method,
);
if (securityProviderResponse?.flagAsDangerous === 1) {
eventProperties.ui_customizations = [
METAMETRIC_KEY_OPT.ui_customizations.flaggedAsMalicious,
];
} else if (securityProviderResponse?.flagAsDangerous === 2) {
eventProperties.ui_customizations = [
METAMETRIC_KEY_OPT.ui_customizations.flaggedAsSafetyUnknown,
];
}
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
const { isSIWEMessage } = detectSIWE({ data });
if (isSIWEMessage) {
eventProperties.ui_customizations = (
eventProperties.ui_customizations || []
).concat(METAMETRIC_KEY_OPT.ui_customizations.SIWE);
}
}
} catch (e) {
console.warn(
`createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`,
);
}
} else {
eventProperties.method = method;
}
trackEvent({
@ -184,7 +221,7 @@ export default function createRPCMethodTrackingMiddleware({
referrer: {
url: origin,
},
properties,
properties: eventProperties,
});
rateLimitTimeouts[method] = setTimeout(() => {
@ -192,13 +229,11 @@ export default function createRPCMethodTrackingMiddleware({
}, SECOND * rateLimitSeconds);
}
next((callback) => {
next(async (callback) => {
if (shouldTrackEvent === false || typeof eventType === 'undefined') {
return callback();
}
const properties = {};
// The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings
const isDisabledEthSignAdvancedSetting =
method === MESSAGE_TYPE.ETH_SIGN &&
@ -209,36 +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;
}
if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) {
properties.signature_type = method;
} else {
properties.method = method;
}
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
const data = req?.params?.[0];
const { isSIWEMessage } = detectSIWE({ data });
if (isSIWEMessage) {
properties.ui_customizations = [
METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS].SIWE,
];
}
}
trackEvent({
event,
category: EVENT.CATEGORIES.INPAGE_PROVIDER,
referrer: {
url: origin,
},
properties,
properties: eventProperties,
});
return callback();

View File

@ -1,17 +1,30 @@
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();
const metricsState = { participateInMetaMetrics: null };
const getMetricsState = () => metricsState;
let flagAsDangerous = 0;
const securityProviderRequest = () => {
return {
flagAsDangerous,
};
};
const handler = createRPCMethodTrackingMiddleware({
trackEvent,
getMetricsState,
rateLimitSeconds: 1,
securityProviderRequest,
});
function getNext(timeout = 500) {
@ -43,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();
@ -92,7 +111,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
metricsState.participateInMetaMetrics = true;
});
it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, () => {
it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => {
const req = {
method: MESSAGE_TYPE.ETH_SIGN,
origin: 'some.dapp',
@ -102,12 +121,14 @@ describe('createRPCMethodTrackingMiddleware', () => {
error: null,
};
const { next } = getNext();
handler(req, res, next);
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 },
properties: {
signature_type: MESSAGE_TYPE.ETH_SIGN,
},
referrer: { url: 'some.dapp' },
});
});
@ -122,13 +143,15 @@ describe('createRPCMethodTrackingMiddleware', () => {
error: null,
};
const { next, executeMiddlewareStack } = getNext();
handler(req, res, next);
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.ETH_SIGN_TYPED_DATA_V4 },
properties: {
signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4,
},
referrer: { url: 'some.dapp' },
});
});
@ -140,16 +163,18 @@ describe('createRPCMethodTrackingMiddleware', () => {
};
const res = {
error: { code: 4001 },
error: { code: errorCodes.provider.userRejectedRequest },
};
const { next, executeMiddlewareStack } = getNext();
handler(req, res, next);
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_REJECTED,
properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN },
properties: {
signature_type: MESSAGE_TYPE.PERSONAL_SIGN,
},
referrer: { url: 'some.dapp' },
});
});
@ -162,7 +187,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
const res = {};
const { next, executeMiddlewareStack } = getNext();
handler(req, res, next);
await handler(req, res, next);
await executeMiddlewareStack();
expect(trackEvent).toHaveBeenCalledTimes(2);
expect(trackEvent.mock.calls[1][0]).toMatchObject({
@ -215,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 };
@ -227,7 +282,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
};
const { next, executeMiddlewareStack } = getNext();
handler(req, res, next);
await handler(req, res, next);
await executeMiddlewareStack();
expect(trackEvent).toHaveBeenCalledTimes(2);
@ -243,5 +298,90 @@ describe('createRPCMethodTrackingMiddleware', () => {
});
});
});
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();
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,
},
referrer: { url: 'some.dapp' },
});
});
});
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',
};
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' },
});
});
});
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',
};
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' },
});
});
});
});
});

View File

@ -39,29 +39,27 @@ export async function securityProviderCheck(
rpc_method_name: methodName,
chain_id: chainId,
data: {
from_address: requestData.txParams.from,
to_address: requestData.txParams.to,
gas: requestData.txParams.gas,
gasPrice: requestData.txParams.gasPrice,
value: requestData.txParams.value,
data: requestData.txParams.data,
from_address: requestData?.txParams?.from,
to_address: requestData?.txParams?.to,
gas: requestData?.txParams?.gas,
gasPrice: requestData?.txParams?.gasPrice,
value: requestData?.txParams?.value,
data: requestData?.txParams?.data,
},
currentLocale,
};
}
const response = await fetchWithTimeout(
'https://eos9d7dmfj.execute-api.us-east-1.amazonaws.com/metamask/validate',
'https://proxy.metafi.codefi.network/opensea/security/v1/validate',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-API-KEY': 'NKYIN6cXkFaNnVIfzNx7s1z0p3b0B4SB6k29qA7n',
},
body: JSON.stringify(dataToValidate),
},
);
return await response.json();
}

View File

@ -52,7 +52,8 @@ export const SENTRY_STATE = {
isUnlocked: true,
metaMetricsId: true,
nativeCurrency: true,
network: true,
networkId: true,
networkStatus: true,
nextNonce: true,
participateInMetaMetrics: true,
preferences: true,

View File

@ -56,6 +56,7 @@ import SmartTransactionsController from '@metamask/smart-transactions-controller
///: BEGIN:ONLY_INCLUDE_IN(flask)
import {
CronjobController,
JsonSnapsRegistry,
SnapController,
IframeExecutionService,
} from '@metamask/snaps-controllers';
@ -73,7 +74,11 @@ import {
GAS_DEV_API_BASE_URL,
SWAPS_CLIENT_ID,
} from '../../shared/constants/swaps';
import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network';
import {
CHAIN_IDS,
NETWORK_TYPES,
NetworkStatus,
} from '../../shared/constants/network';
import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets';
import { KeyringType } from '../../shared/constants/keyring';
import {
@ -116,6 +121,7 @@ import { isMain, isFlask } from '../../shared/constants/environment';
// eslint-disable-next-line import/order
import { DesktopController } from '@metamask/desktop/dist/controllers/desktop';
///: END:ONLY_INCLUDE_IN
import { ACTION_QUEUE_METRICS_E2E_TEST } from '../../shared/constants/test-flags';
import {
onMessageReceived,
checkForMultipleVersionsRunning,
@ -135,7 +141,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware';
import createOnboardingMiddleware from './lib/createOnboardingMiddleware';
import { setupMultiplex } from './lib/stream-utils';
import EnsController from './controllers/ens';
import NetworkController, { NETWORK_EVENTS } from './controllers/network';
import NetworkController, {
NetworkControllerEventTypes,
} from './controllers/network';
import PreferencesController from './controllers/preferences';
import AppStateController from './controllers/app-state';
import CachedBalancesController from './controllers/cached-balances';
@ -249,7 +257,12 @@ export default class MetamaskController extends EventEmitter {
showApprovalRequest: opts.showUserConfirmation,
});
const networkControllerMessenger = this.controllerMessenger.getRestricted({
name: 'NetworkController',
allowedEvents: Object.values(NetworkControllerEventTypes),
});
this.networkController = new NetworkController({
messenger: networkControllerMessenger,
state: initState.NetworkController,
infuraProjectId: opts.infuraProjectId,
trackMetaMetricsEvent: (...args) =>
@ -292,7 +305,14 @@ export default class MetamaskController extends EventEmitter {
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
openPopup: opts.openPopup,
network: this.networkController,
onInfuraIsBlocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsBlocked,
),
onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsUnblocked,
),
tokenListController: this.tokenListController,
provider: this.provider,
});
@ -319,9 +339,8 @@ export default class MetamaskController extends EventEmitter {
{
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
onNetworkStateChange: (cb) =>
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const networkState = this.networkController.store.getState();
onNetworkStateChange: (cb) => {
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
@ -330,7 +349,8 @@ export default class MetamaskController extends EventEmitter {
},
};
return cb(modifiedNetworkState);
}),
});
},
},
{
provider: this.provider,
@ -427,9 +447,9 @@ export default class MetamaskController extends EventEmitter {
this.metaMetricsController = new MetaMetricsController({
segment,
preferencesStore: this.preferencesController.store,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
),
getNetworkIdentifier: () => {
const { type, rpcUrl } =
@ -464,9 +484,11 @@ export default class MetamaskController extends EventEmitter {
clientId: SWAPS_CLIENT_ID,
getProvider: () =>
this.networkController.getProviderAndBlockTracker().provider,
onNetworkStateChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
// NOTE: This option is inaccurately named; it should be called
// onNetworkDidChange
onNetworkStateChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
),
getCurrentNetworkEIP1559Compatibility:
this.networkController.getEIP1559Compatibility.bind(
@ -578,9 +600,9 @@ export default class MetamaskController extends EventEmitter {
provider: this.provider,
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
),
});
@ -590,9 +612,9 @@ export default class MetamaskController extends EventEmitter {
this.incomingTransactionsController = new IncomingTransactionsController({
blockTracker: this.blockTracker,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
@ -674,6 +696,7 @@ export default class MetamaskController extends EventEmitter {
this.keyringController.memStore.subscribe((state) =>
this._onKeyringControllerUpdate(state),
);
this.keyringController.on('unlock', () => this._onUnlock());
this.keyringController.on('lock', () => this._onLock());
@ -688,6 +711,8 @@ export default class MetamaskController extends EventEmitter {
`${this.approvalController.name}:hasRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
`SnapController:getPermitted`,
`SnapController:install`,
],
}),
state: initState.PermissionController,
@ -747,9 +772,7 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask)
const snapExecutionServiceArgs = {
iframeUrl: new URL(
'https://metamask.github.io/iframe-execution-environment/0.13.0',
),
iframeUrl: new URL('https://execution.metamask.io/0.15.1/index.html'),
messenger: this.controllerMessenger.getRestricted({
name: 'ExecutionService',
}),
@ -787,6 +810,8 @@ export default class MetamaskController extends EventEmitter {
'ExecutionService:terminateSnap',
'ExecutionService:terminateAllSnaps',
'ExecutionService:handleRpcRequest',
'SnapsRegistry:get',
'SnapsRegistry:getMetadata',
],
});
@ -826,10 +851,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) => {
@ -843,7 +870,6 @@ export default class MetamaskController extends EventEmitter {
},
},
});
// --- Snaps Cronjob Controller configuration
const cronjobControllerMessenger = this.controllerMessenger.getRestricted({
name: 'CronjobController',
allowedEvents: [
@ -862,6 +888,24 @@ export default class MetamaskController extends EventEmitter {
messenger: cronjobControllerMessenger,
});
const snapsRegistryMessenger = this.controllerMessenger.getRestricted({
name: 'SnapsRegistry',
allowedEvents: [],
allowedActions: [],
});
this.snapsRegistry = new JsonSnapsRegistry({
state: initState.SnapsRegistry,
messenger: snapsRegistryMessenger,
refetchOnAllowlistMiss: isMain,
failOnUnavailableRegistry: isMain,
url: {
registry: 'https://acl.execution.metamask.io/latest/registry.json',
signature: 'https://acl.execution.metamask.io/latest/signature.json',
},
publicKey:
'0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6',
});
this.desktopController = new DesktopController({
initState: initState.DesktopController,
});
@ -909,9 +953,11 @@ export default class MetamaskController extends EventEmitter {
),
getCurrentAccountEIP1559Compatibility:
this.getCurrentAccountEIP1559Compatibility.bind(this),
getNetworkState: () => this.networkController.store.getState().network,
getNetworkId: () => this.networkController.store.getState().networkId,
getNetworkStatus: () =>
this.networkController.store.getState().networkStatus,
onNetworkStateChange: (listener) =>
this.networkController.networkStore.subscribe(listener),
this.networkController.networkIdStore.subscribe(listener),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
preferencesStore: this.preferencesController.store,
@ -969,7 +1015,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;
@ -1034,15 +1085,18 @@ export default class MetamaskController extends EventEmitter {
}
});
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => {
const { ticker } = this.networkController.store.getState().provider;
try {
await this.currencyRateController.setNativeCurrency(ticker);
} catch (error) {
// TODO: Handle failure to get conversion rate more gracefully
console.error(error);
}
});
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
async () => {
const { ticker } = this.networkController.store.getState().provider;
try {
await this.currencyRateController.setNativeCurrency(ticker);
} catch (error) {
// TODO: Handle failure to get conversion rate more gracefully
console.error(error);
}
},
);
this.networkController.lookupNetwork();
this.decryptMessageManager = new DecryptMessageManager({
@ -1076,6 +1130,10 @@ export default class MetamaskController extends EventEmitter {
this.txController.txGasUtil,
),
networkController: this.networkController,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
),
provider: this.provider,
getProviderConfig: () => this.networkController.store.getState().provider,
getTokenRatesState: () => this.tokenRatesController.state,
@ -1097,7 +1155,8 @@ export default class MetamaskController extends EventEmitter {
return cb(modifiedNetworkState);
});
},
getNetwork: () => this.networkController.store.getState().network,
getNetwork: () =>
this.networkController.store.getState().networkId ?? 'loading',
getNonceLock: this.txController.nonceTracker.getNonceLock.bind(
this.txController.nonceTracker,
),
@ -1115,17 +1174,42 @@ export default class MetamaskController extends EventEmitter {
);
// ensure accountTracker updates balances after network change
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
this.accountTracker._updateAccounts();
});
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
() => {
this.accountTracker._updateAccounts();
},
);
// clear unapproved transactions and messages when the network will change
this.networkController.on(NETWORK_EVENTS.NETWORK_WILL_CHANGE, () => {
this.txController.txStateManager.clearUnapprovedTxs();
this.encryptionPublicKeyManager.clearUnapproved();
this.decryptMessageManager.clearUnapproved();
this.signController.clearUnapproved();
});
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkWillChange,
() => {
this.txController.txStateManager.clearUnapprovedTxs();
this.encryptionPublicKeyManager.clearUnapproved();
this.decryptMessageManager.clearUnapproved();
this.signController.clearUnapproved();
},
);
if (isManifestV3 && globalThis.isFirstTimeProfileLoaded === false) {
const { serviceWorkerLastActiveTime } =
this.appStateController.store.getState();
const metametricsPayload = {
category: EVENT.SOURCE.SERVICE_WORKERS,
event: EVENT_NAMES.SERVICE_WORKER_RESTARTED,
properties: {
service_worker_restarted_time:
Date.now() - serviceWorkerLastActiveTime,
},
};
try {
this.metaMetricsController.trackEvent(metametricsPayload);
} catch (e) {
log.warn('Failed to track service worker restart metric:', e);
}
}
this.metamaskMiddleware = createMetamaskMiddleware({
static: {
@ -1226,6 +1310,7 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
DesktopController: this.desktopController.store,
///: END:ONLY_INCLUDE_IN
@ -1259,6 +1344,7 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
DesktopController: this.desktopController.store,
///: END:ONLY_INCLUDE_IN
@ -1611,16 +1697,16 @@ export default class MetamaskController extends EventEmitter {
function updatePublicConfigStore(memState) {
const { chainId } = networkController.store.getState().provider;
if (memState.network !== 'loading') {
if (memState.networkStatus === NetworkStatus.Available) {
publicConfigStore.putState(selectPublicState(chainId, memState));
}
}
function selectPublicState(chainId, { isUnlocked, network }) {
function selectPublicState(chainId, { isUnlocked, networkId }) {
return {
isUnlocked,
chainId,
networkVersion: network,
networkVersion: networkId ?? 'loading',
};
}
@ -1631,12 +1717,7 @@ export default class MetamaskController extends EventEmitter {
* Gets relevant state for the provider of an external origin.
*
* @param {string} origin - The origin to get the provider state for.
* @returns {Promise<{
* isUnlocked: boolean,
* networkVersion: string,
* chainId: string,
* accounts: string[],
* }>} An object with relevant state properties.
* @returns {Promise<{ isUnlocked: boolean, networkVersion: string, chainId: string, accounts: string[] }>} An object with relevant state properties.
*/
async getProviderState(origin) {
return {
@ -1654,10 +1735,10 @@ export default class MetamaskController extends EventEmitter {
* @returns {object} An object with relevant network state properties.
*/
getProviderNetworkState(memState) {
const { network } = memState || this.getState();
const { networkId } = memState || this.getState();
return {
chainId: this.networkController.store.getState().provider.chainId,
networkVersion: network,
networkVersion: networkId ?? 'loading',
};
}
@ -2567,7 +2648,7 @@ export default class MetamaskController extends EventEmitter {
try {
// Automatic login via config password
const password = process.env.CONF?.PASSWORD;
if (password) {
if (password && !process.env.IN_TEST) {
await this.submitPassword(password);
}
// Automatic login via storage encryption key
@ -2900,6 +2981,13 @@ export default class MetamaskController extends EventEmitter {
* @returns {} keyState
*/
async addNewAccount(accountCount) {
const isActionMetricsQueueE2ETest =
this.appStateController.store.getState()[ACTION_QUEUE_METRICS_E2E_TEST];
if (process.env.IN_TEST && isActionMetricsQueueE2ETest) {
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
const [primaryKeyring] = this.keyringController.getKeyringsByType(
KeyringType.hdKeyTree,
);
@ -3521,7 +3609,11 @@ export default class MetamaskController extends EventEmitter {
phishingStream.on(
'data',
createMetaRPCHandler(
{ safelistPhishingDomain: this.safelistPhishingDomain.bind(this) },
{
safelistPhishingDomain: this.safelistPhishingDomain.bind(this),
backToSafetyPhishingWarning:
this.backToSafetyPhishingWarning.bind(this),
},
phishingStream,
),
);
@ -3729,6 +3821,7 @@ export default class MetamaskController extends EventEmitter {
getMetricsState: this.metaMetricsController.store.getState.bind(
this.metaMetricsController.store,
),
securityProviderRequest: this.securityProviderRequest.bind(this),
}),
);
@ -3832,15 +3925,11 @@ export default class MetamaskController extends EventEmitter {
'SnapController:getPermitted',
origin,
),
requestPermissions: async (requestedPermissions) => {
const [approvedPermissions] =
await this.permissionController.requestPermissions(
{ origin },
requestedPermissions,
);
return Object.values(approvedPermissions);
},
requestPermissions: async (requestedPermissions) =>
await this.permissionController.requestPermissions(
{ origin },
requestedPermissions,
),
getPermissions: this.permissionController.getPermissions.bind(
this.permissionController,
origin,
@ -4285,6 +4374,11 @@ export default class MetamaskController extends EventEmitter {
return this.phishingController.bypass(hostname);
}
async backToSafetyPhishingWarning() {
const extensionURL = this.platform.getExtensionURL();
await this.platform.switchToAnotherURL(undefined, extensionURL);
}
/**
* Locks MetaMask
*/
@ -4383,11 +4477,11 @@ export default class MetamaskController extends EventEmitter {
const { currentLocale, transactionSecurityCheckEnabled } =
this.preferencesController.store.getState();
const chainId = Number(
hexToDecimal(this.networkController.store.getState().provider.chainId),
);
if (transactionSecurityCheckEnabled) {
const chainId = Number(
hexToDecimal(this.networkController.store.getState().provider.chainId),
);
try {
const securityProviderResponse = await securityProviderCheck(
requestData,

View File

@ -779,13 +779,13 @@ describe('MetaMaskController', function () {
metamaskController.preferencesController,
'getSelectedAddress',
);
const getNetworkstub = sinon.stub(
const getNetworkIdStub = sinon.stub(
metamaskController.txController.txStateManager,
'getNetworkState',
'getNetworkId',
);
selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc');
getNetworkstub.returns(42);
getNetworkIdStub.returns(42);
metamaskController.txController.txStateManager._addTransactionsToState([
createTxMeta({

View File

@ -0,0 +1,103 @@
import { migrate } from './083';
describe('migration #83', () => {
it('updates the version metadata', async () => {
const originalVersionedData = buildOriginalVersionedData({
meta: {
version: 9999999,
},
});
const newVersionedData = await migrate(originalVersionedData);
expect(newVersionedData.meta).toStrictEqual({
version: 83,
});
});
it('does not change the state if the network controller state does not exist', async () => {
const originalVersionedData = buildOriginalVersionedData({
data: {
test: '123',
},
});
const newVersionedData = await migrate(originalVersionedData);
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
});
const nonObjects = [undefined, null, 'test', 1, ['test']];
for (const invalidState of nonObjects) {
it(`does not change the state if the network controller state is ${invalidState}`, async () => {
const originalVersionedData = buildOriginalVersionedData({
data: {
NetworkController: invalidState,
},
});
const newVersionedData = await migrate(originalVersionedData);
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
});
}
it('does not change the state if the network controller state does not include "network"', async () => {
const originalVersionedData = buildOriginalVersionedData({
data: {
NetworkController: {
test: '123',
},
},
});
const newVersionedData = await migrate(originalVersionedData);
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
});
it('replaces "network" in the network controller state with "networkId": null, "networkStatus": "unknown" if it is "loading"', async () => {
const originalVersionedData = buildOriginalVersionedData({
data: {
NetworkController: {
network: 'loading',
},
},
});
const newVersionedData = await migrate(originalVersionedData);
expect(newVersionedData.data).toStrictEqual({
NetworkController: {
networkId: null,
networkStatus: 'unknown',
},
});
});
it('replaces "network" in the network controller state with "networkId": network, "networkStatus": "available" if it is not "loading"', async () => {
const originalVersionedData = buildOriginalVersionedData({
data: {
NetworkController: {
network: '12345',
},
},
});
const newVersionedData = await migrate(originalVersionedData);
expect(newVersionedData.data).toStrictEqual({
NetworkController: {
networkId: '12345',
networkStatus: 'available',
},
});
});
});
function buildOriginalVersionedData({ meta = {}, data = {} } = {}) {
return {
meta: { version: 999999, ...meta },
data: { ...data },
};
}

View File

@ -0,0 +1,47 @@
import { cloneDeep } from 'lodash';
import { hasProperty, isObject } from '@metamask/utils';
export const version = 83;
/**
* The `network` property in state was replaced with `networkId` and `networkStatus`.
*
* @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist.
* @param originalVersionedData.meta - State metadata.
* @param originalVersionedData.meta.version - The current state version.
* @param originalVersionedData.data - The persisted MetaMask state, keyed by controller.
* @returns Updated versioned MetaMask extension state.
*/
export async function migrate(originalVersionedData: {
meta: { version: number };
data: Record<string, unknown>;
}) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
versionedData.data = transformState(versionedData.data);
return versionedData;
}
function transformState(state: Record<string, unknown>) {
if (
!hasProperty(state, 'NetworkController') ||
!isObject(state.NetworkController) ||
!hasProperty(state.NetworkController, 'network')
) {
return state;
}
const NetworkController = { ...state.NetworkController };
if (NetworkController.network === 'loading') {
NetworkController.networkId = null;
NetworkController.networkStatus = 'unknown';
} else {
NetworkController.networkId = NetworkController.network;
NetworkController.networkStatus = 'available';
}
delete NetworkController.network;
return { ...state, NetworkController };
}

View File

@ -86,6 +86,7 @@ import m079 from './079';
import m080 from './080';
import * as m081 from './081';
import * as m082 from './082';
import * as m083 from './083';
const migrations = [
m002,
@ -169,6 +170,7 @@ const migrations = [
m080,
m081,
m082,
m083,
];
export default migrations;

View File

@ -77,11 +77,7 @@ export default class ExtensionPlatform {
return version;
}
openExtensionInBrowser(
route = null,
queryString = null,
keepWindowOpen = false,
) {
getExtensionURL(route = null, queryString = null) {
let extensionURL = browser.runtime.getURL('home.html');
if (route) {
@ -92,7 +88,22 @@ export default class ExtensionPlatform {
extensionURL += `?${queryString}`;
}
return extensionURL;
}
openExtensionInBrowser(
route = null,
queryString = null,
keepWindowOpen = false,
) {
const extensionURL = this.getExtensionURL(
route,
queryString,
keepWindowOpen,
);
this.openTab({ url: extensionURL });
if (
getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND &&
!keepWindowOpen
@ -113,19 +124,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);
}
}
@ -153,11 +164,15 @@ export default class ExtensionPlatform {
return tab;
}
async switchToAnotherURL(tabId, url) {
await browser.tabs.update(tabId, { url });
}
async closeTab(tabId) {
await browser.tabs.remove(tabId);
}
_showConfirmedTransaction(txMeta, rpcPrefs) {
async _showConfirmedTransaction(txMeta, rpcPrefs) {
this._subscribeToNotificationClicked();
const url = getBlockExplorerLink(txMeta, rpcPrefs);
@ -170,21 +185,27 @@ 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';
const message = `Transaction ${nonce} failed! ${
let message = `Transaction ${nonce} failed! ${
errorMessage || txMeta.err.message
}`;
this._showNotification(title, message);
///: BEGIN:ONLY_INCLUDE_IN(mmi)
if (isNaN(nonce)) {
message = `Transaction failed! ${errorMessage || txMeta.err.message}`;
}
///: END:ONLY_INCLUDE_IN
await this._showNotification(title, message);
}
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,

View File

@ -1,10 +1,14 @@
import browser from 'webextension-polyfill';
import ExtensionPlatform from './extension';
const TEST_URL =
'chrome-extension://jjlgkphpeekojaidfeknpknnimdbleaf/home.html';
jest.mock('webextension-polyfill', () => {
return {
runtime: {
getManifest: jest.fn(),
getURL: jest.fn(),
},
};
});
@ -91,4 +95,30 @@ describe('extension platform', () => {
);
});
});
describe('getExtensionURL', () => {
let extensionPlatform;
beforeEach(() => {
browser.runtime.getURL.mockReturnValue(TEST_URL);
extensionPlatform = new ExtensionPlatform();
});
it('should return URL itself if no route or queryString is provided', () => {
expect(extensionPlatform.getExtensionURL()).toStrictEqual(TEST_URL);
});
it('should return URL with route when provided', () => {
const TEST_ROUTE = 'test-route';
expect(extensionPlatform.getExtensionURL(TEST_ROUTE)).toStrictEqual(
`${TEST_URL}#${TEST_ROUTE}`,
);
});
it('should return URL with queryString when provided', () => {
const QUERY_STRING = 'name=ferret';
expect(
extensionPlatform.getExtensionURL(null, QUERY_STRING),
).toStrictEqual(`${TEST_URL}?${QUERY_STRING}`);
});
});
});

View File

@ -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,

View File

@ -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',

View File

@ -58,7 +58,12 @@ function getBrowserVersionMap(platforms, version) {
if (!String(buildVersion).match(/^\d+$/u)) {
throw new Error(`Invalid prerelease build version: '${buildVersion}'`);
} else if (
![BuildType.beta, BuildType.flask, BuildType.desktop].includes(buildType)
![
BuildType.beta,
BuildType.flask,
BuildType.desktop,
BuildType.mmi,
].includes(buildType)
) {
throw new Error(`Invalid prerelease build type: ${buildType}`);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

253
docs/confirmations.md Normal file
View File

@ -0,0 +1,253 @@
# Adding New Confirmations
## Overview
Given the security focused nature of self-custody, confirmations and approvals form a pivotal aspect of the MetaMask extension.
Confirmations can be triggered by dApps and the UI itself, and are used to approve a variety of operations such as:
- Connecting to dApps
- Giving permissions to dApps
- Sending Eth
- Transfering tokens
- Signing data
- Interacting with Snaps
- Adding Ethereum networks
It is vital any new confirmations are implemented using best practices and consistent patterns, to avoid adding complexity to the code, and to minimise the maintenance cost of many alternate confirmations.
As we try to maintain a clean boundary between the UI and background page, the effort to implement a new confirmation can also be split accordingly.
## Background
### 1. Create Messenger
Ensure the controller or logic requiring a confirmation has access to a controller messenger.
Provide a `messenger` argument in the constructor if using a controller.
Ensure the allowed actions include at least the `ApprovalController:addRequest` action.
If the controller extends `BaseControllerV2`, the property `messagingSystem` is available to access the messenger passed to the base controller. Otherwise, ensure the provided messenger is assigned to a private property.
#### Example
```
this.someController = new SomeController({
messenger: this.controllerMessenger.getRestricted({
name: 'SomeController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
}),
...
});
```
### 2. Create Approval Request
Send an `addRequest` message to the `ApprovalController` to create an approval request.
This message returns a `Promise` which will resolve if the confirmation is approved, and reject if the confirmation is denied or cancelled.
Use an `async` function to send the message so the logic can `await` the confirmation and code execution can continue once approved. This enables the logic ran after approval to be kept in the same flow and therefore the logic to remain readable and encapsulated.
Ensure suitable error handling is in place to handle the confirmation being cancelled or denied and therefore the `Promise` being rejected.
The available message arguments are:
| Name | Description | Example Value |
| -- | -- | -- |
| opts.id | The ID of the approval request.<br>Assigned to a random value if not provided. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` |
| opts.origin | The origin of the request.<br>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.<br>Must be a JSON compatible object.| `{ transactionId: '123' }` |
| opts.requestState | Additional mutable data for the request.<br>Must be a JSON compatible object.<br>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.<br>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.<br>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.<br>Use the constants from the design system. | `SEVERITIES.DANGER` |
| content | The component to be rendered inside the alert.<br>Uses the same format as the `content` returned from `getValues`.<br>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
[<img src="assets/confirmation.png" width="300">](assets/confirmation.png)

View File

@ -1295,6 +1295,8 @@
"setTimeout": true
},
"packages": {
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1302,6 +1304,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1331,8 +1345,11 @@
}
},
"@metamask/permission-controller": {
"globals": {
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/controller-utils": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
@ -1341,6 +1358,11 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1348,7 +1370,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1356,6 +1379,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1533,7 +1568,15 @@
"packages": {
"@metamask/key-tree>@noble/hashes": true,
"@metamask/key-tree>@scure/base": true,
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true
}
},
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": {
"globals": {
"crypto": true
},
"packages": {
"browserify>browser-resolve": true
}
},
"@ngraveio/bc-ur": {
@ -2579,11 +2622,6 @@
"document.createElement": true
}
},
"btoa": {
"packages": {
"browserify>buffer": true
}
},
"classnames": {
"globals": {
"classNames": "write",

View File

@ -1349,6 +1349,8 @@
"setTimeout": true
},
"packages": {
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1356,6 +1358,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1397,8 +1411,11 @@
}
},
"@metamask/permission-controller": {
"globals": {
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/controller-utils": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
@ -1407,6 +1424,11 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1414,7 +1436,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1422,6 +1445,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1461,6 +1496,7 @@
},
"@metamask/post-message-stream": {
"globals": {
"MessageEvent.prototype": true,
"WorkerGlobalScope": true,
"addEventListener": true,
"browser": true,
@ -1545,8 +1581,8 @@
"packages": {
"@metamask/key-tree": true,
"@metamask/key-tree>@noble/hashes": true,
"@metamask/permission-controller": true,
"@metamask/rpc-methods>@metamask/browser-passworder": true,
"@metamask/rpc-methods>@metamask/permission-controller": true,
"@metamask/rpc-methods>nanoid": true,
"@metamask/snaps-ui": true,
"@metamask/snaps-utils": true,
@ -1569,36 +1605,6 @@
"browserify>buffer": true
}
},
"@metamask/rpc-methods>@metamask/permission-controller": {
"packages": {
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": true,
"@metamask/rpc-methods>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
"immer": true,
"json-rpc-engine": true
}
},
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/rpc-methods>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1656,11 +1662,11 @@
"setTimeout": true
},
"packages": {
"@metamask/permission-controller": true,
"@metamask/post-message-stream": true,
"@metamask/providers>@metamask/object-multiplex": true,
"@metamask/rpc-methods": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/permission-controller": true,
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
@ -1669,6 +1675,7 @@
"@metamask/snaps-controllers>readable-web-to-node-stream": true,
"@metamask/snaps-controllers>tar-stream": true,
"@metamask/snaps-utils": true,
"@metamask/snaps-utils>@metamask/snaps-registry": true,
"@metamask/utils": true,
"eth-rpc-errors": true,
"json-rpc-engine": true,
@ -1681,31 +1688,6 @@
"immer": true
}
},
"@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/snaps-controllers>@metamask/permission-controller": {
"packages": {
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": true,
"@metamask/snaps-controllers>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
"immer": true,
"json-rpc-engine": true
}
},
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": {
"packages": {
"@metamask/snaps-controllers>@metamask/base-controller": true
@ -1885,6 +1867,13 @@
"semver": true
}
},
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/key-tree>@noble/secp256k1": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true
}
},
"@metamask/snaps-utils>cron-parser": {
"packages": {
"browserify>browser-resolve": true,
@ -1974,7 +1963,15 @@
"packages": {
"@metamask/key-tree>@noble/hashes": true,
"@metamask/key-tree>@scure/base": true,
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true
}
},
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": {
"globals": {
"crypto": true
},
"packages": {
"browserify>browser-resolve": true
}
},
"@ngraveio/bc-ur": {
@ -3020,11 +3017,6 @@
"document.createElement": true
}
},
"btoa": {
"packages": {
"browserify>buffer": true
}
},
"classnames": {
"globals": {
"classNames": "write",

View File

@ -1349,6 +1349,8 @@
"setTimeout": true
},
"packages": {
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1356,6 +1358,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1397,8 +1411,11 @@
}
},
"@metamask/permission-controller": {
"globals": {
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/controller-utils": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
@ -1407,6 +1424,11 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1414,7 +1436,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1422,6 +1445,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1461,6 +1496,7 @@
},
"@metamask/post-message-stream": {
"globals": {
"MessageEvent.prototype": true,
"WorkerGlobalScope": true,
"addEventListener": true,
"browser": true,
@ -1545,8 +1581,8 @@
"packages": {
"@metamask/key-tree": true,
"@metamask/key-tree>@noble/hashes": true,
"@metamask/permission-controller": true,
"@metamask/rpc-methods>@metamask/browser-passworder": true,
"@metamask/rpc-methods>@metamask/permission-controller": true,
"@metamask/rpc-methods>nanoid": true,
"@metamask/snaps-ui": true,
"@metamask/snaps-utils": true,
@ -1569,36 +1605,6 @@
"browserify>buffer": true
}
},
"@metamask/rpc-methods>@metamask/permission-controller": {
"packages": {
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": true,
"@metamask/rpc-methods>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
"immer": true,
"json-rpc-engine": true
}
},
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/rpc-methods>@metamask/permission-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/rpc-methods>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1656,11 +1662,11 @@
"setTimeout": true
},
"packages": {
"@metamask/permission-controller": true,
"@metamask/post-message-stream": true,
"@metamask/providers>@metamask/object-multiplex": true,
"@metamask/rpc-methods": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/permission-controller": true,
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
@ -1669,6 +1675,7 @@
"@metamask/snaps-controllers>readable-web-to-node-stream": true,
"@metamask/snaps-controllers>tar-stream": true,
"@metamask/snaps-utils": true,
"@metamask/snaps-utils>@metamask/snaps-registry": true,
"@metamask/utils": true,
"eth-rpc-errors": true,
"json-rpc-engine": true,
@ -1681,31 +1688,6 @@
"immer": true
}
},
"@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/snaps-controllers>@metamask/permission-controller": {
"packages": {
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/base-controller>@metamask/controller-utils": true,
"@metamask/snaps-controllers>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
"immer": true,
"json-rpc-engine": true
}
},
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": {
"packages": {
"@metamask/snaps-controllers>@metamask/base-controller": true
@ -1885,6 +1867,13 @@
"semver": true
}
},
"@metamask/snaps-utils>@metamask/snaps-registry": {
"packages": {
"@metamask/key-tree>@noble/secp256k1": true,
"@metamask/utils": true,
"@metamask/utils>superstruct": true
}
},
"@metamask/snaps-utils>cron-parser": {
"packages": {
"browserify>browser-resolve": true,
@ -1974,7 +1963,15 @@
"packages": {
"@metamask/key-tree>@noble/hashes": true,
"@metamask/key-tree>@scure/base": true,
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true
}
},
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": {
"globals": {
"crypto": true
},
"packages": {
"browserify>browser-resolve": true
}
},
"@ngraveio/bc-ur": {
@ -3020,11 +3017,6 @@
"document.createElement": true
}
},
"btoa": {
"packages": {
"browserify>buffer": true
}
},
"classnames": {
"globals": {
"classNames": "write",

View File

@ -1295,6 +1295,8 @@
"setTimeout": true
},
"packages": {
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1302,6 +1304,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/message-manager>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1331,8 +1345,11 @@
}
},
"@metamask/permission-controller": {
"globals": {
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>@metamask/controller-utils": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
@ -1341,6 +1358,11 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1348,7 +1370,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": true,
"@spruceid/siwe-parser": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1356,6 +1379,18 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/permission-controller>@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1533,7 +1568,15 @@
"packages": {
"@metamask/key-tree>@noble/hashes": true,
"@metamask/key-tree>@scure/base": true,
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@noble/secp256k1": true
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": true
}
},
"@metamask/utils>@ethereumjs/tx>ethereum-cryptography>@scure/bip32>@noble/secp256k1": {
"globals": {
"crypto": true
},
"packages": {
"browserify>browser-resolve": true
}
},
"@ngraveio/bc-ur": {
@ -2579,11 +2622,6 @@
"document.createElement": true
}
},
"btoa": {
"packages": {
"browserify>buffer": true
}
},
"classnames": {
"globals": {
"classNames": "write",

View File

@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "10.26.2",
"version": "10.27.0",
"private": true,
"repository": {
"type": "git",
@ -21,6 +21,7 @@
"build:test": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 PORTFOLIO_URL=http://127.0.0.1:8080 yarn build test",
"build:test:flask": "yarn build test --build-type flask",
"build:test:mv3": "ENABLE_MV3=true SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 PORTFOLIO_URL=http://127.0.0.1:8080 yarn build test",
"build:test:dev:mv3": "ENABLE_MV3=true SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 PORTFOLIO_URL=http://127.0.0.1:8080 yarn build:dev testDev --apply-lavamoat=false",
"test": "yarn lint && yarn test:unit && yarn test:unit:jest",
"dapp": "node development/static-server.js node_modules/@metamask/test-dapp/dist --port 8080",
"dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'",
@ -32,6 +33,7 @@
"test:unit:mocha": "node ./test/run-unit-tests.js --mocha",
"test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js",
"test:e2e:chrome:snaps": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --snaps",
"test:e2e:chrome:mv3": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mv3",
"test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js",
"test:e2e:firefox:snaps": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js --snaps",
"test:e2e:single": "node test/e2e/run-e2e-test.js",
@ -247,18 +249,18 @@
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/notification-controller": "^1.0.0",
"@metamask/obs-store": "^5.0.0",
"@metamask/permission-controller": "^2.0.0",
"@metamask/permission-controller": "^3.1.0",
"@metamask/phishing-controller": "^2.0.0",
"@metamask/post-message-stream": "^6.0.0",
"@metamask/providers": "^10.2.1",
"@metamask/rate-limit-controller": "^1.0.0",
"@metamask/rpc-methods": "^0.31.0",
"@metamask/rpc-methods": "^0.32.2",
"@metamask/scure-bip39": "^2.0.3",
"@metamask/slip44": "^2.1.0",
"@metamask/smart-transactions-controller": "^3.1.0",
"@metamask/snaps-controllers": "^0.31.0",
"@metamask/snaps-ui": "^0.31.0",
"@metamask/snaps-utils": "^0.31.0",
"@metamask/snaps-controllers": "^0.32.2",
"@metamask/snaps-ui": "^0.32.2",
"@metamask/snaps-utils": "^0.32.2",
"@metamask/subject-metadata-controller": "^1.0.0",
"@metamask/utils": "^5.0.0",
"@ngraveio/bc-ur": "^1.1.6",
@ -375,7 +377,7 @@
"@metamask/eslint-config-nodejs": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.1",
"@metamask/forwarder": "^1.1.0",
"@metamask/phishing-warning": "^2.0.1",
"@metamask/phishing-warning": "^2.1.0",
"@metamask/test-dapp": "^5.6.0",
"@sentry/cli": "^1.58.0",
"@storybook/addon-a11y": "^6.5.13",

View File

@ -0,0 +1,10 @@
import { CHAIN_IDS } from './network';
export const ALLOWED_BRIDGE_CHAIN_IDS = [
CHAIN_IDS.MAINNET,
CHAIN_IDS.BSC,
CHAIN_IDS.POLYGON,
CHAIN_IDS.AVALANCHE,
CHAIN_IDS.OPTIMISM,
CHAIN_IDS.ARBITRUM,
];

View File

@ -187,6 +187,8 @@
* identify the token_detection_enabled trait
* @property {'install_date_ext'} INSTALL_DATE_EXT - when the user installed the extension
* @property {'desktop_enabled'} [DESKTOP_ENABLED] - optional / does the user have desktop enabled?
* @property {'security_providers'} SECURITY_PROVIDERS - when security provider feature is toggled we
* identify the security_providers trait
*/
/**
@ -210,6 +212,7 @@ export const TRAITS = {
THREE_BOX_ENABLED: 'three_box_enabled',
TOKEN_DETECTION_ENABLED: 'token_detection_enabled',
DESKTOP_ENABLED: 'desktop_enabled',
SECURITY_PROVIDERS: 'security_providers',
};
/**
@ -240,6 +243,7 @@ export const TRAITS = {
* @property {string} [theme] - which theme the user has selected
* @property {boolean} [token_detection_enabled] - does the user have token detection is enabled?
* @property {boolean} [desktop_enabled] - optional / does the user have desktop enabled?
* @property {Array<string>} [security_providers] - whether security provider feature toggle is on or off
*/
// Mixpanel converts the zero address value to a truly anonymous event, which
@ -370,6 +374,7 @@ export const EVENT_NAMES = {
ONBOARDING_WALLET_IMPORT_ATTEMPTED: 'Wallet Import Attempted',
ONBOARDING_WALLET_VIDEO_PLAY: 'SRP Intro Video Played',
ONBOARDING_TWITTER_CLICK: 'External Link Clicked',
SERVICE_WORKER_RESTARTED: 'Service Worker Restarted',
};
export const EVENT = {
@ -443,6 +448,7 @@ export const EVENT = {
DAPP: 'dapp',
USER: 'user',
},
SERVICE_WORKERS: 'service_workers',
},
LOCATION: {
TOKEN_DETAILS: 'token_details',
@ -458,17 +464,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',
},
};

View File

@ -688,4 +688,31 @@ 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);
/**
* Represents the availability state of the currently selected network.
*/
export enum NetworkStatus {
/**
* The network may or may not be able to receive requests, but either no
* attempt has been made to determine this, or an attempt was made but was
* unsuccessful.
*/
Unknown = 'unknown',
/**
* The network is able to receive and respond to requests.
*/
Available = 'available',
/**
* The network is unable to receive and respond to requests for unknown
* reasons.
*/
Unavailable = 'unavailable',
/**
* The network is not only unavailable, but is also inaccessible for the user
* specifically based on their location. This state only applies to Infura
* networks.
*/
Blocked = 'blocked',
}

View File

@ -0,0 +1 @@
export const ACTION_QUEUE_METRICS_E2E_TEST = 'action_queue_metrics_e2e_test';

View File

@ -121,12 +121,17 @@ export async function getErrorHtml(
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
export const MMD_DOWNLOAD_LINK =
'https://github.com/MetaMask/metamask-desktop/releases';
function disableDesktop(backgroundConnection) {
backgroundConnection.disableDesktopError();
}
export function downloadDesktopApp() {
global.platform.openTab({ url: 'https://metamask.io/' });
global.platform.openTab({
url: MMD_DOWNLOAD_LINK,
});
}
export function downloadExtension() {
@ -139,7 +144,7 @@ export function restartExtension() {
export function openOrDownloadMMD() {
openCustomProtocol('metamask-desktop://pair').catch(() => {
window.open('https://metamask.io/download.html', '_blank').focus();
window.open(MMD_DOWNLOAD_LINK, '_blank').focus();
});
}

View File

@ -1,13 +1,48 @@
import browser from 'webextension-polyfill';
import { fetchLocale } from '../../ui/helpers/utils/i18n-helper';
import { SUPPORT_LINK } from './ui-utils';
import { getErrorHtml } from './error-utils';
import {
downloadDesktopApp,
openOrDownloadMMD,
downloadExtension,
getErrorHtml,
restartExtension,
registerDesktopErrorActions,
MMD_DOWNLOAD_LINK,
} from './error-utils';
import { openCustomProtocol } from './deep-linking';
jest.mock('../../ui/helpers/utils/i18n-helper', () => ({
fetchLocale: jest.fn(),
loadRelativeTimeFormatLocaleData: jest.fn(),
}));
jest.mock('./deep-linking', () => ({
openCustomProtocol: jest.fn(),
}));
jest.mock('webextension-polyfill', () => {
return {
runtime: {
reload: jest.fn(),
},
};
});
describe('Error utils Tests', function () {
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
global.platform = {
openTab: jest.fn(),
};
});
afterAll(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
delete global.platform;
});
it('should get error html', async function () {
const mockStore = {
localeMessages: {
@ -50,4 +85,93 @@ describe('Error utils Tests', function () {
expect(errorHtml).toContain(stillGettingMessageMessage);
expect(errorHtml).toContain(sendBugReportMessage);
});
describe('desktop', () => {
it('downloadDesktopApp opens a new tab on metamask-desktop releases url', () => {
downloadDesktopApp();
expect(global.platform.openTab).toHaveBeenCalledTimes(1);
expect(global.platform.openTab).toHaveBeenCalledWith({
url: MMD_DOWNLOAD_LINK,
});
});
it('downloadExtension opens a new tab on metamask extension url', () => {
downloadExtension();
expect(global.platform.openTab).toHaveBeenCalledTimes(1);
expect(global.platform.openTab).toHaveBeenCalledWith({
url: 'https://metamask.io/',
});
});
it('restartExtension calls runtime reload method', () => {
restartExtension();
expect(browser.runtime.reload).toHaveBeenCalledTimes(1);
});
describe('openOrDownloadMMD', () => {
it('launches installed desktop app by calling openCustomProtocol successfully', () => {
openCustomProtocol.mockResolvedValue();
openOrDownloadMMD();
expect(openCustomProtocol).toHaveBeenCalledTimes(1);
expect(openCustomProtocol).toHaveBeenCalledWith(
'metamask-desktop://pair',
);
});
it('opens metamask-desktop release url when fails to find and start a local metamask-desktop app', async () => {
openCustomProtocol.mockRejectedValue();
const focusMock = jest.fn();
jest.spyOn(window, 'open').mockReturnValue({
focus: focusMock,
});
openOrDownloadMMD();
// this ensures that we are awaiting for pending promises to resolve
// as the openOrDownloadMMD calls a promise, but returns before it is resolved
await new Promise(process.nextTick);
expect(openCustomProtocol).toHaveBeenCalledTimes(1);
expect(openCustomProtocol).toHaveBeenCalledWith(
'metamask-desktop://pair',
);
expect(window.open).toHaveBeenCalledTimes(1);
expect(window.open).toHaveBeenCalledWith(MMD_DOWNLOAD_LINK, '_blank');
expect(focusMock).toHaveBeenCalledTimes(1);
});
});
it('registerDesktopErrorActions add click event listeners for each desktop error elements', async () => {
const addEventListenerMock = jest.fn();
jest.spyOn(document, 'getElementById').mockReturnValue({
addEventListener: addEventListenerMock,
});
registerDesktopErrorActions();
expect(document.getElementById).toHaveBeenCalledTimes(4);
expect(document.getElementById).toHaveBeenNthCalledWith(
1,
'desktop-error-button-disable-mmd',
);
expect(document.getElementById).toHaveBeenNthCalledWith(
2,
'desktop-error-button-restart-mm',
);
expect(document.getElementById).toHaveBeenNthCalledWith(
3,
'desktop-error-button-download-mmd',
);
expect(document.getElementById).toHaveBeenNthCalledWith(
4,
'desktop-error-button-open-or-download-mmd',
);
expect(addEventListenerMock).toHaveBeenCalledTimes(4);
});
});
});

Some files were not shown because too many files have changed in this diff Show More